# Copyright (c) 2016 Weitian LI # MIT license """ Products-related handlers ProductsAJAXHandler : Handle the AJAX requests to manipulate the simulation products. ProductsDownloadHandler : Handle the download request for the simulation products. """ import os import logging import shutil import mimetypes import tornado.ioloop import tornado.process from tornado.web import StaticFileHandler, HTTPError from tornado.escape import json_decode, json_encode from .base import BaseRequestHandler from ...errors import ManifestError logger = logging.getLogger(__name__) class ProductsAJAXHandler(BaseRequestHandler): """ Handle the AJAX requests from the client to manage the simulation products. Attributes ---------- from_localhost : bool ``True`` if the request is from the localhost, otherwise ``False``. """ def initialize(self): """Hook for subclass initialization. Called for each request.""" self.products = self.application.products if self.request.remote_ip == "127.0.0.1": self.from_localhost = True else: self.from_localhost = False def get(self): """ Handle the READ-ONLY products manifest manipulations. Supported actions: - get: Get the current products manifest - which: Locate the command/program (check whether the command/program can be found in PATH and is executable) - download: Download the specified product (HEALPix map / HPX image) - open: Open the HPX image of a specified product using a sub-process NOTE: Only allowed when accessing from the localhost """ action = self.get_argument("action", "get") if action == "get": # Get current products manifest success = True response = { "manifest": self.products.manifest, "localhost": self.from_localhost, } elif action == "which": # Locate (and check) the command/program cmd = json_decode(self.get_argument("cmd")) cmdpath = shutil.which(cmd) if cmdpath: success = True response = { "isExecutable": True, "cmdPath": cmdpath, } else: success = False reason = "Cannot locate the executable for: {0}".format(cmd) elif action == "open": # Open the HPX image of a specified product using a sub-process comp_id = json_decode(self.get_argument("compID")) freq_id = json_decode(self.get_argument("freqID")) viewer = json_decode(self.get_argument("viewer")) pid, error = self._open_hpx(comp_id, freq_id, viewer) if pid is not None: success = True response = {"pid": pid} else: success = False reason = error else: # ERROR: bad action success = False reason = "Bad request action: {0}".format(action) # if success: logger.debug("Response: {0}".format(response)) self.set_header("Content-Type", "application/json; charset=UTF-8") self.write(json_encode(response)) else: logger.warning("Request failed: {0}".format(reason)) self.send_error(400, reason=reason) @tornado.web.authenticated def post(self): """ Handle the READ-WRITE products manifest manipulations. Supported actions: - load: Load the products manifest from file - save: Save the current products manifest to file - reset: Reset existing products manifest - convert: Convert the product from HEALPix map to HPX image """ request = json_decode(self.request.body) logger.debug("Received request: {0}".format(request)) action = request.get("action") response = {"action": action} if action == "load": # Load the manifest from supplied file try: success, reason = self._load_products(request["manifestfile"]) except KeyError: success = False reason = "'manifestfile' is missing" elif action == "save": # Save current products manifest to file try: success, reason = self._save_products(request["outfile"], request["clobber"]) except KeyError: success = False reason = "'outfile' or 'clobber' is missing" elif action == "reset": # Reset existing products manifest success = self._reset_products() elif action == "convert": # Convert the product from HEALPix map to HPX image try: comp_id = request["compID"] freq_id = request["freqID"] success, reason = self._convert_hpx(comp_id, freq_id) data = self.products.get_product(comp_id, freq_id) response["data"] = data except KeyError: success = False reason = "'compID' or 'freqID' is missing" else: # ERROR: bad action success = False reason = "Bad request action: {0}".format(action) # if success: response["success"] = success logger.debug("Response: {0}".format(response)) self.set_header("Content-Type", "application/json; charset=UTF-8") self.write(json_encode(response)) else: logger.warning("Request failed: {0}".format(reason)) self.send_error(400, reason=reason) def _reset_products(self): """Reset the existing products manifest.""" self.products.reset() return True def _load_products(self, manifestfile): """ Load the products manifest from file. Parameters ---------- manifestfile: str The path to the products manifest file, which must be an *absolute path*. Returns ------- success : bool ``True`` if the operation succeeded, otherwise, ``False``. error : str If failed, this ``error`` saves the details, otherwise, ``None``. """ success = False error = None if os.path.isabs(os.path.expanduser(manifestfile)): self.products.load(manifestfile) success = True else: error = "Not an absolute path" return (success, error) def _save_products(self, outfile, clobber=False): """ Save current products manifest to file. Parameters ---------- outfile: str The filepath to the output manifest file, which must be an *absolute path*. clobber : bool, optional Whether overwrite the output file if already exists? Returns ------- success : bool ``True`` if the operation succeeded, otherwise, ``False``. error : str If failed, this ``error`` saves the details, otherwise, ``None``. """ success = False error = None try: self.products.dump(outfile, clobber=clobber) success = True except (ValueError, OSError) as e: error = str(e) return (success, error) def _convert_hpx(self, comp_id, freq_id): """ Convert the HEALPix map of the product to HPX projected FITS image. FIXME/TODO: make this non-blocking! """ success = False error = None try: self.products.convert_hpx(comp_id, freq_id, clobber=True) success = True except IOError as e: error = str(e) return (success, error) def _open_hpx(self, comp_id, freq_id, viewer): """ Open the HPX image of a specified product using a sub-process NOTE ---- Only allowed when accessing from the localhost Parameters ---------- comp_id : str ID of the component whose product will be checksum'ed freq_id : int The frequency ID of the specific product within the component. viewer : str The executable name or path to the FITS viewer. Returns ------- pid : int ID of the sub process which opened the HPX image. ``None`` if failed to open the image. error : str If failed, this ``error`` saves the details, otherwise, ``None``. """ pid = None error = None if self.from_localhost: try: filepath = self.products.get_product_abspath( comp_id, freq_id, ptype="hpx") cmd = [viewer, filepath] p = tornado.process.Subprocess(cmd) pid = p.pid logger.info("(PID: {0}) ".format(pid) + "Opened HPX image: {0}".format(" ".join(cmd))) except (ValueError, KeyError) as e: error = str(e) else: error = "Action 'open' only allowed from localhost" return (pid, error) class ProductsDownloadHandler(StaticFileHandler): """ Handle the download request for the simulation products. """ def initialize(self): """Hook for subclass initialization. Called for each request.""" try: self.root = self.application.products.get_root_dir() except ManifestError as e: self.root = None logger.warning(str(e)) @classmethod def get_absolute_path(cls, root, path): """ Return the absolute location of ``path`` relative to ``root``. ``root`` is the path configured for this handler, which is ``self.root`` """ if root is None: reason = "Manifest currently not loaded!" logger.error(reason) raise HTTPError(400, reason=reason) else: return os.path.join(root, path) def validate_absolute_path(self, root, absolute_path): """ Validate and return the absolute path. Credit: https://github.com/tornadoweb/tornado/blob/master/tornado/web.py """ root = os.path.abspath(root) if not root.endswith(os.path.sep): root += os.path.sep if not (absolute_path + os.path.sep).startswith(root): # Only files under the specified root can be accessed raise HTTPError(403, "%s is not in the root directory", self.path) if not os.path.exists(absolute_path): raise HTTPError(404) if not os.path.isfile(absolute_path): raise HTTPError(403, "%s is not a file", self.path) return absolute_path @classmethod def make_static_url(cls): """ This method originally constructs a versioned URL for the given path, which is not applicable here, so disable it. """ raise RuntimeError("Not supported!") def get_content_type(self): """ Returns the ``Content-Type`` header to be used for this request. """ # Add MIME types support used here mimetypes.add_type("application/fits", ".fits") mimetypes.add_type("text/plain", ".conf") return super().get_content_type()