Skip to content

Commit

Permalink
better support for sending file using file path
Browse files Browse the repository at this point in the history
  • Loading branch information
joamag committed Nov 28, 2014
1 parent a930460 commit 30ee3ac
Showing 1 changed file with 129 additions and 125 deletions.
254 changes: 129 additions & 125 deletions src/appier/base.py
Expand Up @@ -1343,6 +1343,130 @@ def send_file(self, contents, content_type = None, etag = None):
if etag: self.request.set_header("Etag", etag)
return contents

def send_path(self, file_path, url_path = None, cache = True):
# default the url path value to the provided file path, this is
# just a fallback behavior and should be avoided whenever possible
# to be able to provide the best experience on error messages
url_path = url_path or file_path

# verifies if the resource exists and in case it does not raises
# an exception about the problem (going to be serialized)
if not os.path.exists(file_path):
raise exceptions.NotFoundError(
message = "Resource '%s' does not exist" % url_path,
code = 404
)

# checks if the path refers a directory and in case it does raises
# an exception because no directories are valid for static serving
if os.path.isdir(file_path):
raise exceptions.NotFoundError(
message = "Resource '%s' refers a directory" % url_path,
code = 404
)

# tries to use the current mime sub system to guess the mime type
# for the file to be returned in the request and then uses this type
# to update the request object content type value
type, _encoding = mimetypes.guess_type(
url_path, strict = True
)
self.request.content_type = type

# retrieves the last modified timestamp for the file path and
# uses it to create the etag for the resource to be served
modified = os.path.getmtime(file_path)
etag = "appier-%.2f" % modified

# retrieves the provided etag for verification and checks if the
# etag remains the same if that's the case the file has not been
# modified and the response should indicate exactly that
_etag = self.request.get_header("If-None-Match", None)
not_modified = etag == _etag

# in case the file has not been modified a not modified response
# must be returned inside the response to the client
if not_modified: self.request.set_code(304); yield 0; return

# retrieves the value of the range header value and updates the
# is partial flag value with the proper boolean value in case the
# header exists or not (as expected by specification)
range_s = self.request.get_header("Range", None)
is_partial = True if range_s else False

# retrieves the size of the resource file in bytes, this value is
# going to be used in the computation of the range values
file_size = os.path.getsize(file_path)

# convert the current string based representation of the range
# into a tuple based presentation otherwise creates the default
# tuple containing the initial position and the final one
if is_partial:
range_s = range_s[6:]
start_s, end_s = range_s.split("-", 1)
start = int(start_s) if start_s else 0
end = int(end_s) if end_s else file_size - 1
range = (start, end)
else: range = (0, file_size - 1)

# creates the string that will represent the content range that is
# going to be returned to the client in the current request
content_range_s = "bytes %d-%d/%d" % (range[0], range[1], file_size)

# retrieves the current date value and increments the cache overflow value
# to it so that the proper expire value is set, then formats the date as
# a string based value in order to be set in the headers
current = datetime.datetime.utcnow()
target = current + self.cache
target_s = target.strftime("%a, %d %b %Y %H:%M:%S UTC")

# sets the complete set of headers expected for the current request
# this is done before the field yielding operation so that the may
# be correctly sent as the first part of the message sending
self.request.set_header("Etag", etag)
if cache: self.request.set_header("Expires", target_s)
else: self.request.set_header("Cache-Control", "no-cache, must-revalidate")
if is_partial: self.request.set_header("Content-Range", content_range_s)
if not is_partial: self.request.set_header("Accept-Ranges", "bytes")

# in case the current request is a partial request the status code
# must be set to the appropriate one (partial content)
if is_partial: self.request.set_code(206)

# calculates the real data size of the chunk that is going to be
# sent to the client this must use the normal range approach then
# yields this result because its going to be used by the upper layer
# of the framework to "know" the correct content length to be sent
data_size = range[1] - range[0] + 1
yield data_size

# opens the file for binary reading this is going to be used for the
# complete reading of the contents, suing a generator based approach
# this way static file serving may be fast and memory efficient
file = open(file_path, "rb")

try:
# seeks the file to the initial target position so that the reading
# starts on the requested starting point as expected
file.seek(range[0])

# iterates continuously reading a series of chunks from the
# the file until no value is returned (end of file) this chunks
# are going to be yield to the parent method to be sent in a
# recursive fashion (avoid memory problems)
while True:
if not data_size: break
size = data_size if BUFFER_SIZE > data_size else BUFFER_SIZE
data = file.read(size)
if not data: break
data_l = len(data)
data_size -= data_l
yield data
finally:
# in case there's an exception in the middle of the reading the
# file must be correctly, in order to avoid extra leak problems
file.close()

def content_type(self, content_type):
self.request.content_type = str(content_type)

Expand Down Expand Up @@ -1622,7 +1746,11 @@ def static(
# raise exception for error situations or return a generator object
# for the sending of the file in case of success, the cache flag should
# control the server side caching using etag values
return self.send(resource_path_f, url_path = resource_path_o, cache = cache)
return self.send_path(
resource_path_f,
url_path = resource_path_o,
cache = cache
)

def static_res(self, data = {}):
static_path = os.path.join(self.res_path, "static")
Expand All @@ -1641,130 +1769,6 @@ def static_part(self, part, data = {}):
prefix_l = part_l + 9
)

def send(self, file_path, url_path = None, cache = True):
# default the url path value to the provided file path, this is
# just a fallback behavior and should be avoided whenever possible
# to be able to provide the best experience on error messages
url_path = url_path or file_path

# verifies if the resource exists and in case it does not raises
# an exception about the problem (going to be serialized)
if not os.path.exists(file_path):
raise exceptions.NotFoundError(
message = "Resource '%s' does not exist" % url_path,
code = 404
)

# checks if the path refers a directory and in case it does raises
# an exception because no directories are valid for static serving
if os.path.isdir(file_path):
raise exceptions.NotFoundError(
message = "Resource '%s' refers a directory" % url_path,
code = 404
)

# tries to use the current mime sub system to guess the mime type
# for the file to be returned in the request and then uses this type
# to update the request object content type value
type, _encoding = mimetypes.guess_type(
url_path, strict = True
)
self.request.content_type = type

# retrieves the last modified timestamp for the file path and
# uses it to create the etag for the resource to be served
modified = os.path.getmtime(file_path)
etag = "appier-%.2f" % modified

# retrieves the provided etag for verification and checks if the
# etag remains the same if that's the case the file has not been
# modified and the response should indicate exactly that
_etag = self.request.get_header("If-None-Match", None)
not_modified = etag == _etag

# in case the file has not been modified a not modified response
# must be returned inside the response to the client
if not_modified: self.request.set_code(304); yield 0; return

# retrieves the value of the range header value and updates the
# is partial flag value with the proper boolean value in case the
# header exists or not (as expected by specification)
range_s = self.request.get_header("Range", None)
is_partial = True if range_s else False

# retrieves the size of the resource file in bytes, this value is
# going to be used in the computation of the range values
file_size = os.path.getsize(file_path)

# convert the current string based representation of the range
# into a tuple based presentation otherwise creates the default
# tuple containing the initial position and the final one
if is_partial:
range_s = range_s[6:]
start_s, end_s = range_s.split("-", 1)
start = int(start_s) if start_s else 0
end = int(end_s) if end_s else file_size - 1
range = (start, end)
else: range = (0, file_size - 1)

# creates the string that will represent the content range that is
# going to be returned to the client in the current request
content_range_s = "bytes %d-%d/%d" % (range[0], range[1], file_size)

# retrieves the current date value and increments the cache overflow value
# to it so that the proper expire value is set, then formats the date as
# a string based value in order to be set in the headers
current = datetime.datetime.utcnow()
target = current + self.cache
target_s = target.strftime("%a, %d %b %Y %H:%M:%S UTC")

# sets the complete set of headers expected for the current request
# this is done before the field yielding operation so that the may
# be correctly sent as the first part of the message sending
self.request.set_header("Etag", etag)
if cache: self.request.set_header("Expires", target_s)
else: self.request.set_header("Cache-Control", "no-cache, must-revalidate")
if is_partial: self.request.set_header("Content-Range", content_range_s)
if not is_partial: self.request.set_header("Accept-Ranges", "bytes")

# in case the current request is a partial request the status code
# must be set to the appropriate one (partial content)
if is_partial: self.request.set_code(206)

# calculates the real data size of the chunk that is going to be
# sent to the client this must use the normal range approach then
# yields this result because its going to be used by the upper layer
# of the framework to "know" the correct content length to be sent
data_size = range[1] - range[0] + 1
yield data_size

# opens the file for binary reading this is going to be used for the
# complete reading of the contents, suing a generator based approach
# this way static file serving may be fast and memory efficient
file = open(file_path, "rb")

try:
# seeks the file to the initial target position so that the reading
# starts on the requested starting point as expected
file.seek(range[0])

# iterates continuously reading a series of chunks from the
# the file until no value is returned (end of file) this chunks
# are going to be yield to the parent method to be sent in a
# recursive fashion (avoid memory problems)
while True:
if not data_size: break
size = data_size if BUFFER_SIZE > data_size else BUFFER_SIZE
data = file.read(size)
if not data: break
data_l = len(data)
data_size -= data_l
yield data
finally:
# in case there's an exception in the middle of the reading the
# file must be correctly, in order to avoid extra leak problems
file.close()

def icon(self, data = {}):
pass

Expand Down

0 comments on commit 30ee3ac

Please sign in to comment.