add app.static for serving static files, and sendfile as official helper

pull/39/head
Ciro 2022-11-05 17:06:32 -03:00
rodzic 5c7d5a8605
commit e6cc934060
5 zmienionych plików z 67 dodań i 31 usunięć

Wyświetl plik

@ -1,35 +1,45 @@
from socketify import App from socketify import App
import aiofiles import aiofiles
from aiofiles import os
import time import time
import mimetypes import mimetypes
import os
from os import path from os import path
mimetypes.init() mimetypes.init()
async def home(res, req): async def home(res, req):
#there is also a helper called static with an sendfile method see static_files.py for examples of usage #this is just an implementation example see static_files.py example for use of sendfile and app.static usage
#here is an sample implementation, a more complete one is in static.sendfile #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" filename = "./public/media/flower.webm"
#read headers before the first await #read headers before the first await
if_modified_since = req.get_header('if-modified-since') 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: try:
exists = await os.path.exists(filename) exists = path.exists(filename)
#not found #not found
if not exists: if not exists:
return res.write_status(404).end(b'Not Found') return res.write_status(404).end(b'Not Found')
#get size and last modified date #get size and last modified date
stats = await os.stat(filename) stats = os.stat(filename)
total_size = stats.st_size 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)) last_modified = time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(stats.st_mtime))
#check if modified since is provided #check if modified since is provided
if if_modified_since == last_modified: if if_modified_since == last_modified:
res.write_status(304).end_without_body() return res.write_status(304).end_without_body()
return
#tells the broswer the last modified date #tells the broswer the last modified date
res.write_header(b'Last-Modified', last_modified) res.write_header(b'Last-Modified', last_modified)
@ -40,24 +50,37 @@ async def home(res, req):
elif content_type: elif content_type:
res.write_header(b'Content-Type', content_type) res.write_header(b'Content-Type', content_type)
async with aiofiles.open(filename, "rb") as fd: with open(filename, "rb") as fd:
res.write_status(200) #check range and support it
if start > 0 or not end == -1:
pending_size = total_size 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 #keep sending until abort or done
while not res.aborted: while not res.aborted:
chunk_size = 16384 #16kb chunks chunk_size = 16384 #16kb chunks
if chunk_size > pending_size: if chunk_size > pending_size:
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 pending_size = pending_size - chunk_size
(ok, done) = await res.send_chunk(buffer, total_size) (ok, done) = await res.send_chunk(buffer, size)
if not ok or done or pending_size <= 0: #if cannot send probably aborted if not ok or done: #if cannot send probably aborted
break break
except Exception as error: except Exception as error:
res.write_status(500).end("Internal Error") res.write_status(500).end("Internal Error")
app = App() app = App()
app.get("/", home) app.get("/", home)

Wyświetl plik

@ -7,9 +7,9 @@
# nginx - try_files - 77630.15 req/s # nginx - try_files - 77630.15 req/s
# pypy3 - socketify static - 10245.82 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 - @fastify/static - 5437.16 req/s
# node.js - express.static - 4077.49 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_aiofile - 2390.96 req/s
# python3 - socketify static_aiofiles - 1615.12 req/s # python3 - socketify static_aiofiles - 1615.12 req/s
# python3 - scarlette static uvicorn - 1335.56 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 # Express production recommendations: https://expressjs.com/en/advanced/best-practice-performance.html
# Fastify production recommendations: https://www.fastify.io/docs/latest/Guides/Recommendations/ # Fastify production recommendations: https://www.fastify.io/docs/latest/Guides/Recommendations/
from socketify import App from socketify import App, sendfile
from helpers.static import static_route
from helpers.static import sendfile
app = App() app = App()
@ -44,8 +42,8 @@ async def home(res, req):
app.get("/", home) app.get("/", home)
#serve all files in public folder under / route (main route but can be like /assets) #serve all files in public folder under /* route (you can use any route like /assets)
static_route(app, "/", "./public") app.static("/", "./public")
app.listen(3000, lambda config: print("Listening on port http://localhost:%d now\n" % config.port)) app.listen(3000, lambda config: print("Listening on port http://localhost:%d now\n" % config.port))
app.run() app.run()

Wyświetl plik

@ -1 +1,2 @@
from .socketify import App, AppOptions, AppListenOptions from .socketify import App, AppOptions, AppListenOptions
from .helpers import sendfile

Wyświetl plik

@ -4,6 +4,7 @@ import mimetypes
from os import path from os import path
mimetypes.init() mimetypes.init()
# We have an version of this using aiofile and aiofiles # 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 # 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 # 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: except Exception as error:
res.write_status(500).end("Internal Error") res.write_status(500).end("Internal Error")
def in_directory(file, directory): def in_directory(file, directory):
#make both absolute #make both absolute
directory = path.join(path.realpath(directory), '') directory = path.join(path.realpath(directory), '')

Wyświetl plik

@ -1,16 +1,24 @@
import cffi import cffi
from datetime import datetime
from http import cookies
import inspect
import json
import mimetypes
import os 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 .loop import Loop
from .status_codes import status_codes from .status_codes import status_codes
import json from .helpers import static_route
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
import platform mimetypes.init()
is_python = platform.python_implementation() == 'CPython' is_python = platform.python_implementation() == 'CPython'
ffi = cffi.FFI() ffi = cffi.FFI()
@ -849,6 +857,10 @@ class App:
self.handlers = [] self.handlers = []
self.error_handler = None self.error_handler = None
def static(self, route, directory):
static_route(self, route, directory)
return self
def get(self, path, handler): def get(self, path, handler):
user_data = ffi.new_handle((handler, self)) user_data = ffi.new_handle((handler, self))
self.handlers.append(user_data) #Keep alive handler self.handlers.append(user_data) #Keep alive handler