def get(self, path, include_body=True):
path = self.parse_url_path(path)
- hasher = hashlib.sha1()
- hasher.update(body)
- self.set_header("Etag", '"%s"' % hasher.hexdigest())
- if include_body:
- self.write(body)
- else:
- assert self.request.method == "HEAD"
- self.set_header("Content-Length", len(body))
+ if not self.set_headers(path):
+ return
+
+ body = self.get_content(path)
+ if not body:
+ return
+
++ self.set_etag_header()
++ if self.check_etag_header():
++ self.set_status(304)
++ return
++
++ self.set_header("Accept-Ranges", "bytes")
++ request_range = None
++ range_header = self.request.headers.get("Range")
++ if range_header:
++ request_range = httputil.parse_request_range(range_header)
++ if not request_range:
++ # 416: Range Not Satisfiable
++ self.set_status(416)
++ self.set_header("Content-Type", "text/plain")
++ self.write(utf8("The provided Range header is not valid: %r\n"
++ "Note: multiple ranges are not supported."
++ %(range_header, )))
++ return
++
++ with open(self.get_absolute_path(path), "rb") as file:
++ data = file.read()
++ if request_range:
++ # 206: Partial Content
++ self.set_status(206)
++ content_range = httputil.get_content_range(data, request_range)
++ self.set_header("Content-Range", content_range)
++ data = data[request_range]
++ if include_body:
++ self.write(data)
++ else:
++ assert self.request.method == "HEAD"
++ self.set_header("Content-Length", len(data))
++
+
+ def on_finish(self):
+ # Cleanup the cached computation of the absolute path associated with
+ # the requested resource. This is crucial in order to ensure accurate
+ # responses in upcoming requests which would otherwise utilize the same
+ # absolute path as the initial one - which would be chaotic.
+ self._abspath = None
+
+ def get_absolute_path(self, path):
+ """Retrieve the absolute path on the filesystem where the resource
+ corresponding to the given URL ``path`` can be found.
+
+ This method also handles the validation of the given path and ensures
+ resources outside of the static directory cannot be accessed.
+ """
+ # In case the ``_abspath`` attribute exists and contains a value
+ # other than None the abspath has already been computed and verified.
+ # It can be returned instantly in order to avoid recomputation.
+ abspath = getattr(self, '_abspath', None)
+ if abspath is not None:
+ return abspath
+
abspath = os.path.abspath(os.path.join(self.root, path))
# os.path.abspath strips a trailing /
# it needs to be temporarily added back for requests to root/
if not os.path.isfile(abspath):
raise HTTPError(403, "%s is not a file", path)
+ self._abspath = abspath
+ return abspath
+
+ def set_headers(self, path):
+ """Set the response headers in order to ensure that client browsers
+ will cache the requested resource and not proceed to retrieve its content
+ in the case of a 304 response.
+ """
+ abspath = self.get_absolute_path(path)
stat_result = os.stat(abspath)
- modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME])
+ modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
self.set_header("Last-Modified", modified)
ims_value = self.request.headers.get("If-Modified-Since")
if ims_value is not None:
date_tuple = email.utils.parsedate(ims_value)
- if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple))
+ if_since = datetime.datetime(*date_tuple[:6])
if if_since >= modified:
self.set_status(304)
- return
-
- self.set_etag_header()
- if self.check_etag_header():
- self.set_status(304)
- return
-
- self.set_header("Accept-Ranges", "bytes")
- request_range = None
- range_header = self.request.headers.get("Range")
- if range_header:
- request_range = httputil.parse_request_range(range_header)
- if not request_range:
- # 416: Range Not Satisfiable
- self.set_status(416)
- self.set_header("Content-Type", "text/plain")
- self.write(utf8("The provided Range header is not valid: %r\n"
- "Note: multiple ranges are not supported."
- %(range_header, )))
- return
+ return False
-
+ return True
+ def get_content(self, path):
+ """Retrieve the content of the requested resource which is located
+ at the given absolute ``path``.
+ """
+ abspath = self.get_absolute_path(path)
with open(abspath, "rb") as file:
- data = file.read()
- if request_range:
- # 206: Partial Content
- self.set_status(206)
- content_range = httputil.get_content_range(data, request_range)
- self.set_header("Content-Range", content_range)
- data = data[request_range]
- if include_body:
- self.write(data)
- else:
- assert self.request.method == "HEAD"
- self.set_header("Content-Length", len(data))
+ return file.read()
+ return None
def set_extra_headers(self, path):
"""For subclass to add extra headers to the response"""