diff --git a/tests/examples/helpers/static.py b/tests/examples/helpers/static.py index 8262238..ba33843 100644 --- a/tests/examples/helpers/static.py +++ b/tests/examples/helpers/static.py @@ -1,11 +1,12 @@ -import aiofiles -from aiofiles import os +import os import time import mimetypes from os import path mimetypes.init() -#try with https://github.com/mosquito/aiofile +# We have an version of this using aiofile and aiofiles +# This is an sync version without any dependencies is normally much faster in CPython and PyPy3 +# In production we highly recomend to use CDN like CloudFlare or/and NGINX or similar for static files async def send_file(res, req, filename): #read headers before the first await if_modified_since = req.get_header('if-modified-since') @@ -20,13 +21,13 @@ async def send_file(res, req, filename): if bytes_range[1]: end = int(bytes_range[1]) try: - exists = await os.path.exists(filename) + exists = path.exists(filename) #not found if not exists: return res.write_status(404).end(b'Not Found') #get size and last modified date - stats = await os.stat(filename) + stats = os.stat(filename) total_size = stats.st_size size = total_size last_modified = time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(stats.st_mtime)) @@ -44,13 +45,13 @@ async def send_file(res, req, filename): elif content_type: res.write_header(b'Content-Type', content_type) - async with aiofiles.open(filename, "rb") as fd: + with open(filename, "rb") as fd: #check range and support it if start > 0 or not end == -1: if end < 0 or end >= size: end = size - 1 size = end - start + 1 - await fd.seek(start) + fd.seek(start) if start > total_size or size > total_size or size < 0 or start < 0: return res.write_status(416).end_without_body() res.write_status(206) @@ -59,9 +60,8 @@ async def send_file(res, req, filename): res.write_status(200) #tells the browser that we support range - #TODO: FIX BYTE RANGE IN ASYNC - # res.write_header(b'Accept-Ranges', b'bytes') - # res.write_header(b'Content-Range', 'bytes %d-%d/%d' % (start, end, total_size)) + res.write_header(b'Accept-Ranges', b'bytes') + res.write_header(b'Content-Range', 'bytes %d-%d/%d' % (start, end, total_size)) pending_size = size #keep sending until abort or done @@ -69,7 +69,7 @@ async def send_file(res, req, filename): chunk_size = 16384 #16kb chunks if chunk_size > pending_size: chunk_size = pending_size - buffer = await fd.read(chunk_size) + buffer = fd.read(chunk_size) pending_size = pending_size - chunk_size (ok, done) = await res.send_chunk(buffer, size) if not ok or done: #if cannot send probably aborted diff --git a/tests/examples/helpers/static_aiofile.py b/tests/examples/helpers/static_aiofile.py new file mode 100644 index 0000000..13a17e4 --- /dev/null +++ b/tests/examples/helpers/static_aiofile.py @@ -0,0 +1,105 @@ +from aiofile import async_open +import os +import time +import mimetypes +from os import path + +mimetypes.init() +# In production we highly recomend to use CDN like CloudFlare or/and NGINX or similar for static files +async def send_file(res, req, filename): + #read headers before the first await + if_modified_since = req.get_header('if-modified-since') + range_header = req.get_header('range') + bytes_range = None + start = 0 + end = -1 + #parse range header + if range_header: + bytes_range = range_header.replace("bytes=", '').split('-') + start = int(bytes_range[0]) + if bytes_range[1]: + end = int(bytes_range[1]) + try: + exists = path.exists(filename) + #not found + if not exists: + return res.write_status(404).end(b'Not Found') + + #get size and last modified date + stats = os.stat(filename) + total_size = stats.st_size + size = total_size + last_modified = time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(stats.st_mtime)) + + #check if modified since is provided + if if_modified_since == last_modified: + return res.write_status(304).end_without_body() + #tells the broswer the last modified date + res.write_header(b'Last-Modified', last_modified) + + #add content type + (content_type, encoding) = mimetypes.guess_type(filename, strict=True) + if content_type and encoding: + res.write_header(b'Content-Type', '%s; %s' % (content_type, encoding)) + elif content_type: + res.write_header(b'Content-Type', content_type) + + async with async_open(filename, "rb") as fd: + #check range and support it + if start > 0 or not end == -1: + if end < 0 or end >= size: + end = size - 1 + size = end - start + 1 + fd.seek(start) + if start > total_size or size > total_size or size < 0 or start < 0: + return res.write_status(416).end_without_body() + res.write_status(206) + else: + end = size - 1 + res.write_status(200) + + #tells the browser that we support range + #TODO: FIX BYTE RANGE IN ASYNC + # res.write_header(b'Accept-Ranges', b'bytes') + # res.write_header(b'Content-Range', 'bytes %d-%d/%d' % (start, end, total_size)) + + pending_size = size + #keep sending until abort or done + while not res.aborted: + chunk_size = 16384 #16kb chunks + if chunk_size > pending_size: + chunk_size = pending_size + buffer = await fd.read(chunk_size) + pending_size = pending_size - chunk_size + (ok, done) = await res.send_chunk(buffer, size) + if not ok or done: #if cannot send probably aborted + break + + except Exception as error: + res.write_status(500).end("Internal Error") + +def in_directory(file, directory): + #make both absolute + directory = path.join(path.realpath(directory), '') + file = path.realpath(file) + #return true, if the common prefix of both is equal to directory + #e.g. /a/b/c/d.rst and directory is /a/b, the common prefix is /a/b + return path.commonprefix([file, directory]) == directory + + +def static_route(app, route, directory): + def route_handler(res, req): + url = req.get_url() + res.grab_aborted_handler() + url = url[len(route)::] + if url.startswith("/"): + url = url[1::] + filename = path.join(path.realpath(directory), url) + + if not in_directory(filename, directory): + res.write_status(404).end_without_body() + return + res.run_async(send_file(res, req, filename)) + if route.startswith("/"): + route = route[1::] + app.get("%s/*" % route, route_handler) \ No newline at end of file diff --git a/tests/examples/helpers/static_aiofiles.py b/tests/examples/helpers/static_aiofiles.py new file mode 100644 index 0000000..eeb77f3 --- /dev/null +++ b/tests/examples/helpers/static_aiofiles.py @@ -0,0 +1,105 @@ +import aiofiles +from aiofiles import os +import time +import mimetypes +from os import path + +mimetypes.init() +# In production we highly recomend to use CDN like CloudFlare or/and NGINX or similar for static files +async def send_file(res, req, filename): + #read headers before the first await + if_modified_since = req.get_header('if-modified-since') + range_header = req.get_header('range') + bytes_range = None + start = 0 + end = -1 + #parse range header + if range_header: + bytes_range = range_header.replace("bytes=", '').split('-') + start = int(bytes_range[0]) + if bytes_range[1]: + end = int(bytes_range[1]) + try: + exists = await os.path.exists(filename) + #not found + if not exists: + return res.write_status(404).end(b'Not Found') + + #get size and last modified date + stats = await os.stat(filename) + total_size = stats.st_size + size = total_size + last_modified = time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(stats.st_mtime)) + + #check if modified since is provided + if if_modified_since == last_modified: + return res.write_status(304).end_without_body() + #tells the broswer the last modified date + res.write_header(b'Last-Modified', last_modified) + + #add content type + (content_type, encoding) = mimetypes.guess_type(filename, strict=True) + if content_type and encoding: + res.write_header(b'Content-Type', '%s; %s' % (content_type, encoding)) + elif content_type: + res.write_header(b'Content-Type', content_type) + + async with aiofiles.open(filename, "rb") as fd: + #check range and support it + if start > 0 or not end == -1: + if end < 0 or end >= size: + end = size - 1 + size = end - start + 1 + await fd.seek(start) + if start > total_size or size > total_size or size < 0 or start < 0: + return res.write_status(416).end_without_body() + res.write_status(206) + else: + end = size - 1 + res.write_status(200) + + #tells the browser that we support range + #TODO: FIX BYTE RANGE IN ASYNC + # res.write_header(b'Accept-Ranges', b'bytes') + # res.write_header(b'Content-Range', 'bytes %d-%d/%d' % (start, end, total_size)) + + pending_size = size + #keep sending until abort or done + while not res.aborted: + chunk_size = 16384 #16kb chunks + if chunk_size > pending_size: + chunk_size = pending_size + buffer = await fd.read(chunk_size) + pending_size = pending_size - chunk_size + (ok, done) = await res.send_chunk(buffer, size) + if not ok or done: #if cannot send probably aborted + break + + except Exception as error: + res.write_status(500).end("Internal Error") + +def in_directory(file, directory): + #make both absolute + directory = path.join(path.realpath(directory), '') + file = path.realpath(file) + #return true, if the common prefix of both is equal to directory + #e.g. /a/b/c/d.rst and directory is /a/b, the common prefix is /a/b + return path.commonprefix([file, directory]) == directory + + +def static_route(app, route, directory): + def route_handler(res, req): + url = req.get_url() + res.grab_aborted_handler() + url = url[len(route)::] + if url.startswith("/"): + url = url[1::] + filename = path.join(path.realpath(directory), url) + + if not in_directory(filename, directory): + res.write_status(404).end_without_body() + return + res.run_async(send_file(res, req, filename)) + if route.startswith("/"): + route = route[1::] + app.get("%s/*" % route, route_handler) \ No newline at end of file diff --git a/tests/examples/requeriments.txt b/tests/examples/requeriments.txt index 9dc5ad6..581e708 100644 --- a/tests/examples/requeriments.txt +++ b/tests/examples/requeriments.txt @@ -1,4 +1,5 @@ aiohttp aiofiles +aiofile redis git+https://github.com/cirospaciari/socketify.py.git@main#socketify --global-option="build_ext" \ No newline at end of file