class FileResponse(object): def __init__(self, f, disposition=None, filename=None, block_size=16384, mimetype=None, headers={}, auto_etag=False): self.f = f self.disposition = disposition self.filename = filename self.block_size = block_size self.mimetype = mimetype self.headers = Headers(headers) self.auto_etag = auto_etag self._stat = None self._last_modified = None def _get_filename(self): if self.filename is not None: return self.filename return self.f.name # TODO: Check if this is correct def _get_mimetype(self): if self.mimetype is not None: return self.mimetype return mimetypes.guess_type(self.f.name)[0] def _get_disposition(self): if self.disposition is not None: return self.disposition return "attachment" def _get_stat(self): if self._stat is None: self._stat = os.fstat(self.f.fileno()) return self._stat def _get_filesize(self): return self._get_stat().st_size def _get_last_modified(self): if self._last_modified is not None: return self._last_modified if "Last-Modified" in self.headers: return self.headers["Last-Modified"] epoch = self._get_stat().st_mtime dt = datetime.utcfromtimestamp(epoch) self._last_modified = dt.strftime("%a, %d %b %Y %H:%M:%S GMT") return self._last_modified def _get_etag(self): return '"{}"'.format(self._get_stat().st_mtime) def _check_if_range(self, environ): if "HTTP_IF_RANGE" not in environ: return True ifrange = environ["HTTP_IF_RANGE"] if not self.auto_etag and self.headers.get("Etag") == ifrange: return True if self.auto_etag and ifrange == self._get_etag(): return True if ifrange == self._get_last_modified(): return True return False def _build_content_disposition(self): if self.disposition is not None and (self.filename is None or self.filename is False): return self._get_disposition() if self.filename is not None and self.filename is not False: return "{}; filename=\"{}\"".format(self._get_disposition(), self._get_filename()) return None def _get_range_data(self, environ, size): start = 0 end = size - 1 # Not a range request, sending 200 if "HTTP_RANGE" not in environ: return 200, start, end, size # If-Range "failed" if not self._check_if_range(environ): return 200, start, end, size unit_ranges = environ["HTTP_RANGE"].split("=") # Missing ranges ("bytes="), or not "bytes". Sending 200 if len(unit_ranges) <= 1 or unit_ranges[0] != "bytes": return 200, start, end, size ranges = unit_ranges[1].split(",") ran = ranges[0].split("-") # Too few params in range (for example "10" not "10-20") if len(ran) <= 1: return 200, start, end, size try: # Example: -10 if len(ran[0]) == 0: start = end - int(ran[1]) + 1 # Example: 10- elif len(ran[1]) == 0: start = int(ran[0]) # Start byte is after end byte (20-10). Sending 200 elif int(ran[0]) > int(ran[1]): return 200, start, end, size # Example: 10-20 else: start = int(ran[0]) end = int(ran[1]) except Exception: # Most likely happens if the range did not contain a number # Example: 10-abc return 200, start, end, size return (206, start, end, (end - start) + 1) def _build_headers(self, status, start, end, length, size): headers = self.headers.copy() if status == 206: headers["Content-Range"] = "bytes {}-{}/{}".format( start, end, size) content_disposition = self._build_content_disposition() if content_disposition is not None: headers["Content-Disposition"] = content_disposition if self.auto_etag: headers["Etag"] = self._get_etag() headers["Accept-Ranges"] = "bytes" headers["Content-Type"] = self._get_mimetype() headers["Content-Length"] = length headers["Last-Modified"] = self._get_last_modified() return headers def __call__(self, environ, start_response): size = self._get_filesize() status, start, end, length = self._get_range_data(environ, size) headers = self._build_headers(status, start, end, length, size) start_response('{} {}'.format(status, HTTP_STATUS_CODES[status]), headers.to_wsgi_list()) if status == 200 and "wsgi.file_wrapper" in environ: print("wsgi.file_wrapper") return environ["wsgi.file_wrapper"](self.f, self.block_size) self.f.seek(start) return self._generator(end) def _generator(self, end): with self.f: block = self.block_size while self.f.tell() <= end: pos = self.f.tell() if pos + block > end: block = end - pos + 1 yield self.f.read(block)
def test_headers(): # simple header tests headers = Headers() headers.add('Content-Type', 'text/plain') headers.add('X-Foo', 'bar') assert 'x-Foo' in headers assert 'Content-type' in headers headers['Content-Type'] = 'foo/bar' assert headers['Content-Type'] == 'foo/bar' assert len(headers.getlist('Content-Type')) == 1 # list conversion assert headers.to_list() == [ ('Content-Type', 'foo/bar'), ('X-Foo', 'bar') ] assert str(headers) == ( "Content-Type: foo/bar\r\n" "X-Foo: bar\r\n" "\r\n") assert str(Headers()) == "\r\n" # extended add headers.add('Content-Disposition', 'attachment', filename='foo') assert headers['Content-Disposition'] == 'attachment; filename=foo' headers.add('x', 'y', z='"') assert headers['x'] == r'y; z="\""' # defaults headers = Headers([ ('Content-Type', 'text/plain'), ('X-Foo', 'bar'), ('X-Bar', '1'), ('X-Bar', '2') ]) assert headers.getlist('x-bar') == ['1', '2'] assert headers.get('x-Bar') == '1' assert headers.get('Content-Type') == 'text/plain' assert headers.setdefault('X-Foo', 'nope') == 'bar' assert headers.setdefault('X-Bar', 'nope') == '1' assert headers.setdefault('X-Baz', 'quux') == 'quux' assert headers.setdefault('X-Baz', 'nope') == 'quux' headers.pop('X-Baz') # type conversion assert headers.get('x-bar', type=int) == 1 assert headers.getlist('x-bar', type=int) == [1, 2] # list like operations assert headers[0] == ('Content-Type', 'text/plain') assert headers[:1] == Headers([('Content-Type', 'text/plain')]) del headers[:2] del headers[-1] assert headers == Headers([('X-Bar', '1')]) # copying a = Headers([('foo', 'bar')]) b = a.copy() a.add('foo', 'baz') assert a.getlist('foo') == ['bar', 'baz'] assert b.getlist('foo') == ['bar'] headers = Headers([('a', 1)]) assert headers.pop('a') == 1 assert headers.pop('b', 2) == 2 assert_raises(KeyError, headers.pop, 'c') # set replaces and accepts same arguments as add a = Headers() a.set('Content-Disposition', 'useless') a.set('Content-Disposition', 'attachment', filename='foo') assert a['Content-Disposition'] == 'attachment; filename=foo'
def test_headers(): # simple header tests headers = Headers() headers.add("Content-Type", "text/plain") headers.add("X-Foo", "bar") assert "x-Foo" in headers assert "Content-type" in headers headers["Content-Type"] = "foo/bar" assert headers["Content-Type"] == "foo/bar" assert len(headers.getlist("Content-Type")) == 1 # list conversion assert headers.to_list() == [("Content-Type", "foo/bar"), ("X-Foo", "bar")] assert str(headers) == ("Content-Type: foo/bar\r\n" "X-Foo: bar\r\n" "\r\n") assert str(Headers()) == "\r\n" # extended add headers.add("Content-Disposition", "attachment", filename="foo") assert headers["Content-Disposition"] == "attachment; filename=foo" headers.add("x", "y", z='"') assert headers["x"] == r'y; z="\""' # defaults headers = Headers([("Content-Type", "text/plain"), ("X-Foo", "bar"), ("X-Bar", "1"), ("X-Bar", "2")]) assert headers.getlist("x-bar") == ["1", "2"] assert headers.get("x-Bar") == "1" assert headers.get("Content-Type") == "text/plain" assert headers.setdefault("X-Foo", "nope") == "bar" assert headers.setdefault("X-Bar", "nope") == "1" assert headers.setdefault("X-Baz", "quux") == "quux" assert headers.setdefault("X-Baz", "nope") == "quux" headers.pop("X-Baz") # type conversion assert headers.get("x-bar", type=int) == 1 assert headers.getlist("x-bar", type=int) == [1, 2] # list like operations assert headers[0] == ("Content-Type", "text/plain") assert headers[:1] == Headers([("Content-Type", "text/plain")]) del headers[:2] del headers[-1] assert headers == Headers([("X-Bar", "1")]) # copying a = Headers([("foo", "bar")]) b = a.copy() a.add("foo", "baz") assert a.getlist("foo") == ["bar", "baz"] assert b.getlist("foo") == ["bar"] headers = Headers([("a", 1)]) assert headers.pop("a") == 1 assert headers.pop("b", 2) == 2 assert_raises(KeyError, headers.pop, "c") # set replaces and accepts same arguments as add a = Headers() a.set("Content-Disposition", "useless") a.set("Content-Disposition", "attachment", filename="foo") assert a["Content-Disposition"] == "attachment; filename=foo"
def test_headers(): # simple header tests headers = Headers() headers.add('Content-Type', 'text/plain') headers.add('X-Foo', 'bar') assert 'x-Foo' in headers assert 'Content-type' in headers headers['Content-Type'] = 'foo/bar' assert headers['Content-Type'] == 'foo/bar' assert len(headers.getlist('Content-Type')) == 1 # list conversion assert headers.to_list() == [('Content-Type', 'foo/bar'), ('X-Foo', 'bar')] assert str(headers) == ("Content-Type: foo/bar\r\n" "X-Foo: bar\r\n" "\r\n") assert str(Headers()) == "\r\n" # extended add headers.add('Content-Disposition', 'attachment', filename='foo') assert headers['Content-Disposition'] == 'attachment; filename=foo' headers.add('x', 'y', z='"') assert headers['x'] == r'y; z="\""' # defaults headers = Headers([('Content-Type', 'text/plain'), ('X-Foo', 'bar'), ('X-Bar', '1'), ('X-Bar', '2')]) assert headers.getlist('x-bar') == ['1', '2'] assert headers.get('x-Bar') == '1' assert headers.get('Content-Type') == 'text/plain' assert headers.setdefault('X-Foo', 'nope') == 'bar' assert headers.setdefault('X-Bar', 'nope') == '1' assert headers.setdefault('X-Baz', 'quux') == 'quux' assert headers.setdefault('X-Baz', 'nope') == 'quux' headers.pop('X-Baz') # type conversion assert headers.get('x-bar', type=int) == 1 assert headers.getlist('x-bar', type=int) == [1, 2] # list like operations assert headers[0] == ('Content-Type', 'text/plain') assert headers[:1] == Headers([('Content-Type', 'text/plain')]) del headers[:2] del headers[-1] assert headers == Headers([('X-Bar', '1')]) # copying a = Headers([('foo', 'bar')]) b = a.copy() a.add('foo', 'baz') assert a.getlist('foo') == ['bar', 'baz'] assert b.getlist('foo') == ['bar'] headers = Headers([('a', 1)]) assert headers.pop('a') == 1 assert headers.pop('b', 2) == 2 assert_raises(KeyError, headers.pop, 'c') # set replaces and accepts same arguments as add a = Headers() a.set('Content-Disposition', 'useless') a.set('Content-Disposition', 'attachment', filename='foo') assert a['Content-Disposition'] == 'attachment; filename=foo'