From e6cc9340607b365f7a0a767bfae41b8cfb46d251 Mon Sep 17 00:00:00 2001 From: Ciro Date: Sat, 5 Nov 2022 17:06:32 -0300 Subject: [PATCH] add app.static for serving static files, and sendfile as official helper --- examples/file_stream.py | 57 +++++++++++++------ examples/static_files.py | 10 ++-- src/socketify/__init__.py | 1 + .../static.py => src/socketify/helpers.py | 2 + src/socketify/socketify.py | 28 ++++++--- 5 files changed, 67 insertions(+), 31 deletions(-) rename examples/helpers/static.py => src/socketify/helpers.py (98%) diff --git a/examples/file_stream.py b/examples/file_stream.py index 2644671..25218fc 100644 --- a/examples/file_stream.py +++ b/examples/file_stream.py @@ -1,35 +1,45 @@ from socketify import App import aiofiles -from aiofiles import os import time import mimetypes +import os from os import path mimetypes.init() async def home(res, req): - #there is also a helper called static with an sendfile method see static_files.py for examples of usage - #here is an sample implementation, a more complete one is in static.sendfile + #this is just an implementation example see static_files.py example for use of sendfile and app.static usage + #there is an static_aiofile.py helper and static.aiofiles helper using async implementation of this + #asyncio with IO is really slow so, we will implement "aiofile" using libuv inside socketify in future filename = "./public/media/flower.webm" #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) + 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)) #check if modified since is provided if if_modified_since == last_modified: - res.write_status(304).end_without_body() - return + return res.write_status(304).end_without_body() #tells the broswer the last modified date res.write_header(b'Last-Modified', last_modified) @@ -40,24 +50,37 @@ async def home(res, req): elif content_type: res.write_header(b'Content-Type', content_type) - async with aiofiles.open(filename, "rb") as fd: - res.write_status(200) - - pending_size = total_size + 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 + 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 + 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) + buffer = fd.read(chunk_size) pending_size = pending_size - chunk_size - (ok, done) = await res.send_chunk(buffer, total_size) - if not ok or done or pending_size <= 0: #if cannot send probably aborted + (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") + res.write_status(500).end("Internal Error") app = App() app.get("/", home) diff --git a/examples/static_files.py b/examples/static_files.py index 0510ad9..854ace4 100644 --- a/examples/static_files.py +++ b/examples/static_files.py @@ -7,9 +7,9 @@ # nginx - try_files - 77630.15 req/s # pypy3 - socketify static - 10245.82 req/s +# python3 - socketify static - 8273.71 req/s # node.js - @fastify/static - 5437.16 req/s # node.js - express.static - 4077.49 req/s -# python3 - socketify static - 2438.06 req/s # python3 - socketify static_aiofile - 2390.96 req/s # python3 - socketify static_aiofiles - 1615.12 req/s # python3 - scarlette static uvicorn - 1335.56 req/s @@ -29,9 +29,7 @@ # Express production recommendations: https://expressjs.com/en/advanced/best-practice-performance.html # Fastify production recommendations: https://www.fastify.io/docs/latest/Guides/Recommendations/ -from socketify import App -from helpers.static import static_route -from helpers.static import sendfile +from socketify import App, sendfile app = App() @@ -44,8 +42,8 @@ async def home(res, req): app.get("/", home) -#serve all files in public folder under / route (main route but can be like /assets) -static_route(app, "/", "./public") +#serve all files in public folder under /* route (you can use any route like /assets) +app.static("/", "./public") app.listen(3000, lambda config: print("Listening on port http://localhost:%d now\n" % config.port)) app.run() diff --git a/src/socketify/__init__.py b/src/socketify/__init__.py index 9502af2..a1368c6 100644 --- a/src/socketify/__init__.py +++ b/src/socketify/__init__.py @@ -1 +1,2 @@ from .socketify import App, AppOptions, AppListenOptions +from .helpers import sendfile \ No newline at end of file diff --git a/examples/helpers/static.py b/src/socketify/helpers.py similarity index 98% rename from examples/helpers/static.py rename to src/socketify/helpers.py index 5ccc09b..773b136 100644 --- a/examples/helpers/static.py +++ b/src/socketify/helpers.py @@ -4,6 +4,7 @@ import mimetypes from os import path mimetypes.init() + # 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 @@ -77,6 +78,7 @@ async def sendfile(res, req, filename): 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), '') diff --git a/src/socketify/socketify.py b/src/socketify/socketify.py index 557bcc0..e2f9c41 100644 --- a/src/socketify/socketify.py +++ b/src/socketify/socketify.py @@ -1,16 +1,24 @@ import cffi +from datetime import datetime +from http import cookies +import inspect +import json +import mimetypes import os +from os import path +import platform +import signal +from threading import Thread, local, Lock +import time +from urllib.parse import parse_qs, quote_plus, unquote_plus + from .loop import Loop from .status_codes import status_codes -import json -import inspect -import signal -from http import cookies -from datetime import datetime -from urllib.parse import parse_qs, quote_plus, unquote_plus -from threading import Thread, local, Lock +from .helpers import static_route -import platform +mimetypes.init() + + is_python = platform.python_implementation() == 'CPython' ffi = cffi.FFI() @@ -849,6 +857,10 @@ class App: self.handlers = [] self.error_handler = None + def static(self, route, directory): + static_route(self, route, directory) + return self + def get(self, path, handler): user_data = ffi.new_handle((handler, self)) self.handlers.append(user_data) #Keep alive handler