예제 #1
0
 def test_range_parser(self):
     r = lambda rs: list(parse_range_header(rs, 100))
     self.assertEqual([(90, 100)], r('bytes=-10'))
     self.assertEqual([(10, 100)], r('bytes=10-'))
     self.assertEqual([(5, 11)], r('bytes=5-10'))
     self.assertEqual([(10, 100), (90, 100), (5, 11)],
                      r('bytes=10-,-10,5-10'))
예제 #2
0
 def video():
   print(list(bottle.request.headers.items()))
   print(self.transcoder.fn)
   ranges = list(bottle.parse_range_header(bottle.request.environ['HTTP_RANGE'], 1000000000000))
   print('ranges', ranges)
   offset, end = ranges[0]
   self.transcoder.wait_for_byte(offset)
   response = bottle.static_file(self.transcoder.fn, root='/')
   response.headers['Access-Control-Allow-Origin'] = '*'
   response.headers['Access-Control-Allow-Methods'] = 'GET, HEAD'
   response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
   return response
예제 #3
0
def send_file(content, filename, size, timestamp):
    """ Send a file represented by file object

    The code is partly based on ``bottle.static_file``.

    :param content:     file-like object
    :param filename:    filename to use
    :param size:        file size in bytes
    :param timestamp:   file's timestamp seconds since epoch
    """
    headers = {}
    ctype = get_mimetype(filename)

    if ctype.startswith('text/'):
        # We expect and assume all text files are encoded UTF-8. It's
        # broadcaster's job to ensure this is true.
        ctype += '; charset=%s' % CHARSET

    # Set basic headers
    headers['Content-Type'] = ctype
    headers['Content-Length'] = size
    headers['Last-Modified'] = format_ts(timestamp)

    # Check if If-Modified-Since header is in request and respond early if so
    modsince = request.environ.get('HTTP_IF_MODIFIED_SINCE')
    modsince = modsince and parse_date(modsince.split(';')[0].strip())
    if modsince is not None and modsince >= timestamp:
        headers['Date'] = format_ts()
        return HTTPResponse(status=304, **headers)

    if request.method == 'HEAD':
        # Request is a HEAD, so remove any content body
        content = ''


    headers['Accept-Ranges'] = 'bytes'
    ranges = request.environ.get('HTTP_RANGE')
    if ranges:
        ranges = list(parse_range_header(ranges, size))
        if not ranges:
            return HTTPError(416, "Request Range Not Satisfiable")
        start, end = ranges[0]
        headers['Content-Range'] = 'bytes %d-%d/%d' % (start, end - 1, size)
        headers['Content-Length'] = str(end - start)
        content = iter_read_range(content, start, end - start)
    return HTTPResponse(content, **headers)
예제 #4
0
def send_file(content, filename, size, timestamp):
    """ Send a file represented by file object

    The code is partly based on ``bottle.static_file``.

    :param content:     file-like object
    :param filename:    filename to use
    :param size:        file size in bytes
    :param timestamp:   file's timestamp seconds since epoch
    """
    headers = {}
    ctype = get_mimetype(filename)

    if ctype.startswith('text/'):
        # We expect and assume all text files are encoded UTF-8. It's
        # broadcaster's job to ensure this is true.
        ctype += '; charset=%s' % CHARSET

    # Set basic headers
    headers['Content-Type'] = ctype
    headers['Content-Length'] = size
    headers['Last-Modified'] = format_ts(timestamp)

    # Check if If-Modified-Since header is in request and respond early if so
    modsince = request.environ.get('HTTP_IF_MODIFIED_SINCE')
    modsince = modsince and parse_date(modsince.split(';')[0].strip())
    if modsince is not None and modsince >= timestamp:
        headers['Date'] = format_ts()
        return HTTPResponse(status=304, **headers)

    if request.method == 'HEAD':
        # Request is a HEAD, so remove any content body
        content = ''

    headers['Accept-Ranges'] = 'bytes'
    ranges = request.environ.get('HTTP_RANGE')
    if ranges:
        ranges = list(parse_range_header(ranges, size))
        if not ranges:
            return HTTPError(416, "Request Range Not Satisfiable")
        start, end = ranges[0]
        headers['Content-Range'] = 'bytes %d-%d/%d' % (start, end - 1, size)
        headers['Content-Length'] = str(end - start)
        content = iter_read_range(content, start, end - start)
    return HTTPResponse(content, **headers)
예제 #5
0
    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)
예제 #6
0
파일: common.py 프로젝트: ale4souza/firewad
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)
예제 #7
0
 def test_range_parser(self):
     r = lambda rs: list(parse_range_header(rs, 100))
     self.assertEqual([(90, 100)], r('bytes=-10'))
     self.assertEqual([(10, 100)], r('bytes=10-'))
     self.assertEqual([(5, 11)],  r('bytes=5-10'))
     self.assertEqual([(10, 100), (90, 100), (5, 11)],  r('bytes=10-,-10,5-10'))
예제 #8
0
    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)
예제 #9
0
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)
예제 #10
0
def send_file(content, filename, size=None, timestamp=None):
    """
    Convert file data into an HTTP response.

    This method is used when the file data does not exist on disk, such as when
    it is dynamically generated.

    Because the file does not exist on disk, the basic metadata which is
    usually read from the file itself must be supplied as arguments. The
    ``filename`` argument is the supposed filename of the file data. It is only
    used to set the Content-Type header, and you may safely pass in just the
    extension with leading period.

    The ``size`` argument is the payload size in bytes. For streaming files,
    this can be particularly important as the ranges are calculated baed on
    content length. If ``size`` is omitted, then support for ranges is not
    advertise and ranges are never returned.

    ``timestamp`` is expected to be in seconds since UNIX epoch, and is used to
    calculate Last-Modified HTTP headers, as well as handle If-Modified-Since
    header. If omitted, current time is used, and If-Modified-Since is never
    checked.

    .. note::
        The returned response is a completely new response object.
        Modifying the reponse object in the current request context is not
        going to affect the object returned by this function. You should modify
        the object returned by this function instead.

    Example::

        def some_handler():
            import StringIO
            f = StringIO.StringIO('foo')
            return send_file(f, 'file.txt', 3, 1293281312)

    The code is partly based on ``bottle.static_file``, with the main
    difference being the use of file-like objects instead of files on disk.
    """
    headers = {}
    ctype = get_mimetype(filename)

    if ctype.startswith('text/'):
        # We expect and assume all text files are encoded UTF-8. It's
        # user's job to ensure this is true.
        ctype += '; charset=UTF-8'

    # Set basic headers
    headers['Content-Type'] = ctype
    if size:
        headers['Content-Length'] = size
    headers['Last-Modified'] = format_ts(timestamp)

    # Check if If-Modified-Since header is in request and respond early if so
    if timestamp:
        modsince = request.environ.get('HTTP_IF_MODIFIED_SINCE')
        modsince = modsince and parse_date(modsince.split(';')[0].strip())
        if modsince is not None and modsince >= timestamp:
            headers['Date'] = format_ts()
            return HTTPResponse(status=304, **headers)

    if request.method == 'HEAD':
        # Request is a HEAD, so remove any content body
        content = ''

    if size:
        headers['Accept-Ranges'] = 'bytes'

    ranges = request.environ.get('HTTP_RANGE')
    if ranges and size:
        ranges = list(parse_range_header(ranges, size))
        if not ranges:
            return HTTPError(416, "Request Range Not Satisfiable")
        start, end = ranges[0]
        headers['Content-Range'] = 'bytes %d-%d/%d' % (start, end - 1, size)
        headers['Content-Length'] = str(end - start)
        content = iter_read_range(content, start, end - start)
    return HTTPResponse(content, **headers)
예제 #11
0
    def process_response(self, content, ranges_specifier=None):
        """
        Determine if and what partial content should be sent to the client.
        Modifies response headers to accomodate partial content.

        Since we only care about ranges for proxied content, only process the
        ranges of content of a specific type (i.e., bytes and iterables of bytes).

        References:
            https://www.ietf.org/rfc/rfc2616.txt
            https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests
            https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Ranges

        Arguments:
            content -- response content of a type that Bottle accepts
            ranges_specifier -- str of RFC2616 ranges specified by client

        Returns:
            bytes or None -- partial content specified by ranges

        """
        if response.status_code == codes.PARTIAL_CONTENT:
            return  # Already dealt with.
        elif response.status_code >= codes.bad:
            return  # Do not process a bad response.

        # Assume a proxied content type will only be bytes or iterable of bytes.
        # TODO: consider content size and use a buffer or file object.
        if not isinstance(content, bytes):
            try:
                content = b''.join(
                    b for b in (content() if callable(content) else content))
            except TypeError:
                return

        # Show support for ranges.
        response.set_header('Accept-Ranges', UNITS)

        clen = len(content)
        # `parse_range_header` follows RFC2616 specification for parsing byte
        # ranges.
        ranges = list(parse_range_header(ranges_specifier, clen))
        if ranges_specifier and not ranges:
            self.abort(ranges_specifier=ranges_specifier)
        elif ranges:
            response.status = codes.PARTIAL_CONTENT

            if len(ranges) == 1:
                # Single-part ranges.
                start, end = ranges[0]

                response.set_header(
                    'Content-Range', '{units} {start}-{end}/{clen}'.format(
                        units=UNITS,
                        start=start,
                        end=end - 1,
                        clen=clen,
                    ))

                # Set partial content.
                content = content[start:end]
                clen = end - start
            else:
                # Multi-part ranges.
                ctype = response.content_type
                charset = response.charset
                while True:
                    boundary = uuid4().hex.encode(response.charset)
                    if boundary not in content:
                        break

                response.set_header(
                    'Content-Type',
                    'multipart/byteranges; boundary={0}'.format(
                        boundary.decode()))

                # Build partial content bytes that will be joined by newlines.
                parts = []
                for start, end in ranges:
                    parts.extend([
                        b'--%s' % boundary,
                        b'Content-Type: %s' % ctype.encode(charset),
                        b'Content-Range: %(units)s %(start)d-%(end)d/%(clen)d'
                        % {
                            b'units': UNITS.encode(charset),
                            b'start': start,
                            b'end': end - 1,
                            b'clen': clen,
                        },
                        b'',  # Separate content from headers with a newline.
                        content[start:end],
                    ])
                parts.append(b'--%s--' % boundary)

                # Set partial content.
                content = b'\n'.join(parts)
                clen = len(content)

        response.content_length = str(clen)
        return content
예제 #12
0
def send_file(fd, filename=None, size=None, timestamp=None, ctype=None,
              charset=CHARSET, attachment=False, wrapper=DEFAULT_WRAPPER):
    """ Send a file represented by file object

    This function constcuts a HTTPResponse object that uses a file descriptor
    as response body. The file descriptor is suppled as ``fd`` argument and it
    must have a ``read()`` method. ``ValueError`` is raised when this is not
    the case. It supports `byte serving`_ using Range header, and makes the
    best effort to set all appropriate headers. It also supports HEAD queries.

    Because we are dealing with file descriptors and not physical files, the
    user must also supply the file metadata such as filename, size, and
    timestamp.

    The ``filename`` argument is an arbitrary filename. It is used to guess the
    content type, and also to set the content disposition in case of
    attachments.

    The ``size`` argument is the payload size in bytes. If it is omitted, the
    content length header is not set, and byte serving does not work.

    The ``timestamp`` argument is the number of seconds since Unix epoch when
    the file was created or last modified. If this argument is omitted,
    If-Modified-Since request headers cannot be honored.

    To explicitly specify the content type, the ``ctype`` argument can be used.
    This should be a valid MIME type of the payload.

    Default encoding (used as charset parameter in Content-Type header) is
    'UTF-8'. This can be overridden by using the ``charset`` argument.

    The ``attachment`` argumnet can be set to ``True`` to add the
    Content-Dispositon response header. Value of the header is then set to the
    filename.

    The ``wrapper`` argument is used to wrap the file descriptor when doing
    byte serving. The default is to use ``fdsend.rangewrapper.RangeWrapper``
    class, but there are alternatives as ``fdsend.rangewrapper.range_iter`` and
    ``bottle._file_iter_range``. The wrappers provided by this package are
    written to specifically handle file handles that do not have a ``seek()``
    method. If this is not your case, you may safely use the bottle's wrapper.

    The primary difference between ``fdsend.rangewrapper.RangeWrapper`` and
    ``fdsend.rangewrapper.range_iter`` is that the former returns a file-like
    object with ``read()`` method, which may or may not increase performance
    when used on a WSGI server that supports ``wsgi.file_wrapper`` feature. The
    latter returns an iterator and the response is returned as is without the
    use of a ``file_wrapper``. This may have some benefits when it comes to
    memory usage.

    Benchmarking and profiling is the best way to determine which wrapper you
    want to use, or you need to implement your own.

    To implement your own wrapper, you need to create a callable or a class
    that takes the following arguments:

    - file descriptor
    - offset (in bytes from start of the file)
    - length (total number of bytes in the range)

    The return value of the wrapper must be either an iterable or file-like
    object that implements ``read()`` and ``close()`` methods with the usual
    semantics.

    The code is partly based on ``bottle.static_file``.

    .. _byte serving: https://tools.ietf.org/html/rfc2616#page-138
    """
    if not hasattr(fd, 'read'):
        raise ValueError("Object '{}' has no read() method".format(fd))

    headers = {}
    status = 200

    if not ctype and filename is not None:
        ctype, enc = mimetypes.guess_type(filename)
        if enc:
            headers['Content-Encoding'] = enc

    if ctype:
        if ctype.startswith('text/'):
            # We expect and assume all text files are encoded UTF-8. It's
            # broadcaster's job to ensure this is true.
            ctype += '; charset=%s' % charset
        headers['Content-Type'] = ctype

    if size:
        headers['Content-Length'] = size
        headers['Accept-Ranges'] = 'bytes'

    if timestamp:
        headers['Last-Modified'] = format_ts(timestamp)

        # Check if If-Modified-Since header is in request and respond early
        modsince = request.environ.get('HTTP_IF_MODIFIED_SINCE')
        print(modsince)
        modsince = modsince and parse_date(modsince.split(';')[0].strip())
        if modsince is not None and modsince >= timestamp:
            headers['Date'] = format_ts()
            return HTTPResponse(status=304, **headers)

    if attachment and filename:
        headers['Content-Disposition'] = 'attachment; filename="%s"' % filename

    if request.method == 'HEAD':
        # Request is a HEAD, so remove any fd body
        fd = ''

    ranges = request.environ.get('HTTP_RANGE')
    if size and ranges:
        ranges = list(parse_range_header(ranges, size))
        if not ranges:
            return HTTPError(416, 'Request Range Not Satisfiable')
        start, end = ranges[0]
        headers['Content-Range'] = 'bytes %d-%d/%d' % (start, end - 1, size)
        length = end - start
        headers['Content-Length'] = str(length)
        fd = wrapper(fd, start, length)
        status = 206

    return HTTPResponse(fd, status=status, **headers)
예제 #13
0
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)