def serve_file(filename, data=None, mimetype='auto', download=False, charset='UTF-8'): headers = dict() if mimetype == 'auto': mimetype, encoding = mimetypes.guess_type(filename) if encoding: headers['Content-Encoding'] = encoding if mimetype: if mimetype[:5] == 'text/' and charset and 'charset' not in mimetype: mimetype += '; charset=%s' % charset headers['Content-Type'] = mimetype if download: headers['Content-Disposition'] = 'attachment; filename="%s"' % filename data.seek(0, os.SEEK_END) clen = data.tell() headers['Content-Length'] = clen lm = datetime.datetime.now().strftime("%a, %d %b %Y %H:%M:%S GMT") headers['Last-Modified'] = lm if bottle.request.method == 'HEAD': body = '' else: data.seek(0) body = data.read() headers["Accept-Ranges"] = "bytes" ranges = bottle.request.environ.get('HTTP_RANGE') if 'HTTP_RANGE' in bottle.request.environ: ranges = list(bottle.parse_range_header(bottle.request.environ['HTTP_RANGE'], clen)) if not ranges: return bottle.HTTPError(416, "Requested Range Not Satisfiable") offset, end = ranges[0] headers["Content-Range"] = "bytes %d-%d/%d" % (offset, end - 1, clen) headers["Content-Length"] = str(end - offset) if body: body = bottle._file_iter_range(body, offset, end - offset) return bottle.HTTPResponse(body, status=206, **headers) return bottle.HTTPResponse(body, **headers)
def get_item_file(self, id): item = self.lib.get_item(id) if item is None: return HTTPError(404, 'File does not exist.') if not os.access(item.path, os.R_OK): return HTTPError(403, 'You do not have permission to access this file.') stats = os.stat(item.path) headers = { 'Content-Type': 'application/octet-stream', 'Content-Length': stats.st_size, 'Last-Modified': time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(stats.st_mtime)), 'Date': time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime()) } ims = request.environ.get('HTTP_IF_MODIFIED_SINCE') if ims: ims = parse_date(ims.split(";")[0].strip()) if ims is not None and ims >= int(stats.st_mtime): return HTTPResponse(status=304, **headers) body = '' if request.method == 'HEAD' else open(item.path, 'rb') headers["Accept-Ranges"] = "bytes" range_header = request.environ.get('HTTP_RANGE') if range_header: ranges = list(parse_range_header(range_header, stats.st_size)) if not ranges: return HTTPError(416, "Requested Range Not Satisfiable") offset, end = ranges[0] headers["Content-Range"] = "bytes %d-%d/%d" % (offset, end - 1, stats.st_size) headers["Content-Length"] = str(end - offset) if body: body = _file_iter_range(body, offset, end - offset) return HTTPResponse(body, status=206, **headers) return HTTPResponse(body, **headers)
def static_file(filename, root, mimetype=True, download=False, charset='UTF-8', etag=None): """ Open a file in a safe way and return an instance of :exc:`HTTPResponse` that can be sent back to the client. :param filename: Name or path of the file to send, relative to ``root``. :param root: Root path for file lookups. Should be an absolute directory path. :param mimetype: Provide the content-type header (default: guess from file extension) :param download: If True, ask the browser to open a `Save as...` dialog instead of opening the file with the associated program. You can specify a custom filename as a string. If not specified, the original filename is used (default: False). :param charset: The charset for files with a ``text/*`` mime-type. (default: UTF-8) :param etag: Provide a pre-computed ETag header. If set to ``False``, ETag handling is disabled. (default: auto-generate ETag header) While checking user input is always a good idea, this function provides additional protection against malicious ``filename`` parameters from breaking out of the ``root`` directory and leaking sensitive information to an attacker. Read-protected files or files outside of the ``root`` directory are answered with ``403 Access Denied``. Missing files result in a ``404 Not Found`` response. Conditional requests (``If-Modified-Since``, ``If-None-Match``) are answered with ``304 Not Modified`` whenever possible. ``HEAD`` and ``Range`` requests (used by download managers to check or continue partial downloads) are also handled automatically. """ root = os.path.join(os.path.abspath(root), '') filename = os.path.abspath(os.path.join(root, filename.strip('/\\'))) headers = dict() if not filename.startswith(root): return HTTPError(403, "Access denied.") if not os.path.exists(filename) or not os.path.isfile(filename): return HTTPError(404, "File does not exist.") if not os.access(filename, os.R_OK): return HTTPError( 403, "You do not have permission to access this file.") if mimetype is True: if download and download is not True: mimetype, encoding = mimetypes.guess_type(download) else: mimetype, encoding = mimetypes.guess_type(filename) if encoding: headers['Content-Encoding'] = encoding if mimetype: if (mimetype[:5] == 'text/' or mimetype == 'application/javascript')\ and charset and 'charset' not in mimetype: mimetype += '; charset=%s' % charset headers['Content-Type'] = mimetype if download: download = os.path.basename( filename if download is True else download) headers[ 'Content-Disposition'] = 'attachment; filename="%s"' % download stats = os.stat(filename) headers['Content-Length'] = clen = stats.st_size headers['Last-Modified'] = email.utils.formatdate(stats.st_mtime, usegmt=True) headers['Date'] = email.utils.formatdate(time.time(), usegmt=True) getenv = request.environ.get if etag is None: etag = '%d:%d:%d:%d:%s' % (stats.st_dev, stats.st_ino, stats.st_mtime, clen, filename) etag = hashlib.sha1(tob(etag)).hexdigest() if etag: headers['ETag'] = etag check = getenv('HTTP_IF_NONE_MATCH') if check and check == etag: return HTTPResponse(status=304, **headers) if not (etag and check): ims = getenv('HTTP_IF_MODIFIED_SINCE') if ims: ims = parse_date(ims.split(";")[0].strip()) if ims is not None and ims >= int(stats.st_mtime): return HTTPResponse(status=304, **headers) body = '' if request.method == 'HEAD' else open(filename, 'rb') headers["Accept-Ranges"] = "bytes" range_header = getenv('HTTP_RANGE') if range_header: ranges = list(parse_range_header(range_header, clen)) if not ranges: return HTTPError(416, "Requested Range Not Satisfiable") offset, end = ranges[0] headers["Content-Range"] = "bytes %d-%d/%d" % (offset, end - 1, clen) headers["Content-Length"] = str(end - offset) if body: body = _file_iter_range(body, offset, end - offset) return HTTPResponse(body, status=206, **headers) return HTTPResponse(body, **headers)
def handle_subarchive_path(archivefile, subarchivepath, mimetype, encoding, download=False, charset='UTF-8', etag=None, format=None): """Show content of a path in a zip file. """ from bottle import parse_range_header, parse_date, _file_iter_range, tob if not os.access(archivefile, os.R_OK): return http_error(403, "You do not have permission to access this file.", format=format) try: zip = zipfile.ZipFile(archivefile) except: return http_error(500, "Unable to open the ZIP file.", format=format) try: # KeyError is raised if subarchivepath does not exist info = zip.getinfo(subarchivepath) except KeyError: # subarchivepath does not exist # possibility a missing directory entry? return handle_zip_directory_listing(zip, archivefile, subarchivepath) fh = zip.open(subarchivepath, 'r') headers = dict() if encoding: headers['Content-Encoding'] = encoding if mimetype is True: if download and download is not True: mimetype, encoding = mimetypes.guess_type(download) else: mimetype, encoding = mimetypes.guess_type(subarchivepath) if encoding: headers['Content-Encoding'] = encoding if mimetype: if (mimetype[:5] == 'text/' or mimetype == 'application/javascript')\ and charset and 'charset' not in mimetype: mimetype += '; charset=%s' % charset headers['Content-Type'] = mimetype if download: download = os.path.basename( subarchivepath if download is True else download) headers['Content-Disposition'] = 'attachment; filename="%s"' % download headers['Content-Length'] = clen = info.file_size lm = info.date_time epoch = int( time.mktime((lm[0], lm[1], lm[2], lm[3], lm[4], lm[5], 0, 0, -1))) headers['Last-Modified'] = email.utils.formatdate(epoch, usegmt=True) headers['Date'] = email.utils.formatdate(time.time(), usegmt=True) getenv = request.environ.get if etag is None: etag = '%d:%d:%s' % (epoch, clen, subarchivepath) etag = hashlib.sha1(tob(etag)).hexdigest() if etag: headers['ETag'] = etag check = getenv('HTTP_IF_NONE_MATCH') if check and check == etag: return HTTPResponse(status=304, **headers) if not (etag and check): ims = getenv('HTTP_IF_MODIFIED_SINCE') if ims: ims = parse_date(ims.split(";")[0].strip()) if ims is not None and ims >= int(epoch): return HTTPResponse(status=304, **headers) body = '' if request.method == 'HEAD' else fh headers["Accept-Ranges"] = "bytes" range_header = getenv('HTTP_RANGE') if range_header: ranges = list(parse_range_header(range_header, clen)) if not ranges: return http_error(416, "Requested Range Not Satisfiable") offset, end = ranges[0] headers["Content-Range"] = "bytes %d-%d/%d" % (offset, end - 1, clen) headers["Content-Length"] = str(end - offset) if body: body = _file_iter_range(body, offset, end - offset) return HTTPResponse(body, status=206, **headers) return HTTPResponse(body, **headers)
def static_file(filename, root, mimetype="auto", download=False, charset="UTF-8"): """ Open a file in a safe way and return :exc:`HTTPResponse` with status code 200, 305, 403 or 404. The ``Content-Type``, ``Content-Encoding``, ``Content-Length`` and ``Last-Modified`` headers are set if possible. Special support for ``If-Modified-Since``, ``Range`` and ``HEAD`` requests. :param filename: Name or path of the file to send. :param root: Root path for file lookups. Should be an absolute directory path. :param mimetype: Defines the content-type header (default: guess from file extension) :param download: If True, ask the browser to open a `Save as...` dialog instead of opening the file with the associated program. You can specify a custom filename as a string. If not specified, the original filename is used (default: False). :param charset: The charset to use for files with a ``text/*`` mime-type. (default: UTF-8) """ root = os.path.abspath(root) + os.sep filename = os.path.abspath(os.path.join(root, filename.strip("/\\"))) headers = dict() if not filename.startswith(root): return HTTPError(403, "Access denied.") if not os.path.exists(filename) or not os.path.isfile(filename): return HTTPError(404, "File does not exist.") if not os.access(filename, os.R_OK): return HTTPError(403, "You do not have permission to access this file.") if mimetype == "auto": mimetype, encoding = mimetypes.guess_type(filename) if encoding: headers["Content-Encoding"] = encoding if mimetype: if mimetype[:5] == "text/" and charset and "charset" not in mimetype: mimetype += "; charset=%s" % charset headers["Content-Type"] = mimetype if download: download = os.path.basename(filename if download == True else download) headers["Content-Disposition"] = 'attachment; filename="%s"' % download stats = os.stat(filename) headers["Content-Length"] = clen = stats.st_size lm = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(stats.st_mtime)) headers["Last-Modified"] = lm ### Added here: headers["Cache-Control"] = "max-age=3600, public" ims = request.environ.get("HTTP_IF_MODIFIED_SINCE") if ims: ims = parse_date(ims.split(";")[0].strip()) if ims is not None and ims >= int(stats.st_mtime): headers["Date"] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime()) return HTTPResponse(status=304, **headers) body = "" if request.method == "HEAD" else open(filename, "rb") headers["Accept-Ranges"] = "bytes" ranges = request.environ.get("HTTP_RANGE") if "HTTP_RANGE" in request.environ: ranges = list(parse_range_header(request.environ["HTTP_RANGE"], clen)) if not ranges: return HTTPError(416, "Requested Range Not Satisfiable") offset, end = ranges[0] headers["Content-Range"] = "bytes %d-%d/%d" % (offset, end - 1, clen) headers["Content-Length"] = str(end - offset) if body: body = _file_iter_range(body, offset, end - offset) return HTTPResponse(body, status=206, **headers) return HTTPResponse(body, **headers)