kopia lustrzana https://github.com/cirospaciari/socketify.py
424 wiersze
14 KiB
Python
424 wiersze
14 KiB
Python
import os
|
|
import time
|
|
import mimetypes
|
|
import inspect
|
|
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 recommend to use CDN like CloudFlare or/and NGINX or similar for static files
|
|
# TODO: this must be reimplemented pure C++ to avoid GIL
|
|
async def sendfile(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.cork(lambda res: 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.cork(lambda res: res.write_status(304).end_without_body())
|
|
|
|
# add content type
|
|
(content_type, encoding) = mimetypes.guess_type(filename, strict=True)
|
|
if content_type and encoding:
|
|
content_type = "%s; %s" % (content_type, encoding)
|
|
|
|
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:
|
|
if content_type:
|
|
return res.cork(
|
|
lambda res: res.write_header(b"Content-Type", content_type)
|
|
.write_status(416)
|
|
.end_without_body()
|
|
)
|
|
return res.cork(
|
|
lambda res: res.write_status(416).end_without_body()
|
|
)
|
|
status = 206
|
|
else:
|
|
end = size - 1
|
|
status = 200
|
|
|
|
def send_headers(res):
|
|
res.write_status(status)
|
|
# tells the broswer the last modified date
|
|
res.write_header(b"Last-Modified", last_modified)
|
|
|
|
# tells the browser that we support range
|
|
if content_type:
|
|
res.write_header(b"Content-Type", content_type)
|
|
res.write_header(b"Accept-Ranges", b"bytes")
|
|
res.write_header(
|
|
b"Content-Range", "bytes %d-%d/%d" % (start, end, total_size)
|
|
)
|
|
|
|
res.cork(send_headers)
|
|
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 = 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.cork(lambda res: 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.endswith("/"):
|
|
if url.startswith("/"):
|
|
url = url[1:-1]
|
|
else:
|
|
url = url[:-1]
|
|
elif 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(sendfile(res, req, filename))
|
|
|
|
if route.endswith("/"):
|
|
route = route[:-1]
|
|
app.get("%s/*" % route, route_handler)
|
|
|
|
|
|
def middleware(*functions):
|
|
syncs = []
|
|
asyncs = []
|
|
for function in functions:
|
|
# all is async after the first async
|
|
if inspect.iscoroutinefunction(function) or len(asyncs) > 0:
|
|
asyncs.append(function)
|
|
else:
|
|
syncs.append(function)
|
|
if len(asyncs) == 0: # pure sync
|
|
return sync_middleware(*functions)
|
|
if len(syncs) == 0: # pure async
|
|
return async_middleware(*functions)
|
|
|
|
# we use Optional data=None at the end so you can use and middleware inside a middleware
|
|
def optimized_middleware_route(res, req, data=None):
|
|
# cicle to all middlewares
|
|
for function in syncs:
|
|
# call middlewares
|
|
data = function(res, req, data)
|
|
# stops if returns Falsy
|
|
if not data:
|
|
return
|
|
|
|
async def wrapper(res, req, data):
|
|
# cicle to all middlewares
|
|
for function in asyncs:
|
|
# detect if is coroutine or not
|
|
if inspect.iscoroutinefunction(function):
|
|
data = await function(res, req, data)
|
|
else:
|
|
# call sync middleware
|
|
data = function(res, req, data)
|
|
# stops if returns Falsy
|
|
if not data:
|
|
break
|
|
return data
|
|
|
|
# in async query string, arguments and headers are only valid until the first await
|
|
# preserve queries, headers, parameters, url, full_url and method
|
|
req.preserve()
|
|
|
|
# go async
|
|
res.run_async(wrapper(res, req, data))
|
|
|
|
return optimized_middleware_route
|
|
|
|
|
|
def sync_middleware(*functions):
|
|
# we use Optional data=None at the end so you can use and middleware inside a middleware
|
|
def middleware_route(res, req, data=None):
|
|
# cicle to all middlewares
|
|
for function in functions:
|
|
# call middlewares
|
|
data = function(res, req, data)
|
|
# stops if returns Falsy
|
|
if not data:
|
|
break
|
|
return data
|
|
|
|
return middleware_route
|
|
|
|
|
|
def async_middleware(*functions):
|
|
# we use Optional data=None at the end so you can use and middleware inside a middleware
|
|
async def middleware_route(res, req, data=None):
|
|
some_async_as_run = False
|
|
# cicle to all middlewares
|
|
for function in functions:
|
|
# detect if is coroutine or not
|
|
if inspect.iscoroutinefunction(function):
|
|
# in async query string, arguments and headers are only valid until the first await
|
|
if not some_async_as_run:
|
|
# preserve queries, headers, parameters, url, full_url and method
|
|
req.preserve()
|
|
some_async_as_run = True
|
|
data = await function(res, req, data)
|
|
else:
|
|
# call middlewares
|
|
data = function(res, req, data)
|
|
# stops if returns Falsy
|
|
if not data:
|
|
break
|
|
return data
|
|
|
|
return middleware_route
|
|
|
|
|
|
class DecoratorRouter:
|
|
def __init__(self, app, prefix: str = "", *middlewares):
|
|
self.app = app
|
|
self.middlewares = list(middlewares)
|
|
self.prefix = prefix
|
|
|
|
def get(self, path):
|
|
path = f"{self.prefix}{path}"
|
|
|
|
def decorator(handler):
|
|
if len(self.middlewares) > 0:
|
|
middies = list(self.middlewares)
|
|
middies.append(handler)
|
|
self.app.get(path, middleware(*middies))
|
|
else:
|
|
self.app.get(path, handler)
|
|
return handler
|
|
|
|
return decorator
|
|
|
|
def post(self, path):
|
|
path = f"{self.prefix}{path}"
|
|
|
|
def decorator(handler):
|
|
if len(self.middlewares) > 0:
|
|
middies = list(self.middlewares)
|
|
middies.append(handler)
|
|
self.app.post(path, middleware(*middies))
|
|
else:
|
|
self.app.post(path, handler)
|
|
|
|
return decorator
|
|
|
|
def options(self, path):
|
|
path = f"{self.prefix}{path}"
|
|
|
|
def decorator(handler):
|
|
if len(self.middlewares) > 0:
|
|
middies = list(self.middlewares)
|
|
middies.append(handler)
|
|
self.app.options(path, middleware(*middies))
|
|
else:
|
|
self.app.options(path, handler)
|
|
|
|
return decorator
|
|
|
|
def delete(self, path):
|
|
path = f"{self.prefix}{path}"
|
|
|
|
def decorator(handler):
|
|
if len(self.middlewares) > 0:
|
|
middies = list(self.middlewares)
|
|
middies.append(handler)
|
|
self.app.delete(path, middleware(*middies))
|
|
else:
|
|
self.app.delete(path, handler)
|
|
|
|
return decorator
|
|
|
|
def patch(self, path):
|
|
path = f"{self.prefix}{path}"
|
|
|
|
def decorator(handler):
|
|
if len(self.middlewares) > 0:
|
|
middies = list(self.middlewares)
|
|
middies.append(handler)
|
|
self.app.patch(path, middleware(*middies))
|
|
else:
|
|
self.app.patch(path, handler)
|
|
|
|
return decorator
|
|
|
|
def put(self, path: str):
|
|
path = f"{self.prefix}{path}"
|
|
|
|
def decorator(handler):
|
|
if len(self.middlewares) > 0:
|
|
middies = list(self.middlewares)
|
|
middies.append(handler)
|
|
self.app.put(path, middleware(*middies))
|
|
else:
|
|
self.app.put(path, handler)
|
|
|
|
return decorator
|
|
|
|
def head(self, path):
|
|
path = f"{self.prefix}{path}"
|
|
|
|
def decorator(handler):
|
|
if len(self.middlewares) > 0:
|
|
middies = list(self.middlewares)
|
|
middies.append(handler)
|
|
self.app.head(path, middleware(*middies))
|
|
else:
|
|
self.app.head(path, handler)
|
|
|
|
return decorator
|
|
|
|
def connect(self, path):
|
|
path = f"{self.prefix}{path}"
|
|
|
|
def decorator(handler):
|
|
if len(self.middlewares) > 0:
|
|
middies = list(self.middlewares)
|
|
middies.append(handler)
|
|
self.app.connect(path, middleware(*middies))
|
|
else:
|
|
self.app.connect(path, handler)
|
|
|
|
return decorator
|
|
|
|
def trace(self, path):
|
|
path = f"{self.prefix}{path}"
|
|
|
|
def decorator(handler):
|
|
if len(self.middlewares) > 0:
|
|
middies = list(self.middlewares)
|
|
middies.append(handler)
|
|
self.app.trace(path, middleware(*middies))
|
|
else:
|
|
self.app.trace(path, handler)
|
|
|
|
return decorator
|
|
|
|
def any(self, path):
|
|
path = f"{self.prefix}{path}"
|
|
|
|
def decorator(handler):
|
|
if len(self.middlewares) > 0:
|
|
middies = list(self.middlewares)
|
|
middies.append(handler)
|
|
self.app.any(path, middleware(*middies))
|
|
else:
|
|
self.app.any(path, handler)
|
|
|
|
return decorator
|
|
|
|
|
|
class MiddlewareRouter:
|
|
def __init__(self, app, *middlewares):
|
|
self.app = app
|
|
self.middlewares = middlewares
|
|
|
|
def get(self, path, handler):
|
|
middies = list(self.middlewares)
|
|
middies.append(handler)
|
|
self.app.get(path, middleware(*middies))
|
|
return self
|
|
|
|
def post(self, path, handler):
|
|
middies = list(self.middlewares)
|
|
middies.append(handler)
|
|
self.app.post(path, middleware(*middies))
|
|
return self
|
|
|
|
def options(self, path, handler):
|
|
middies = list(self.middlewares)
|
|
middies.append(handler)
|
|
self.app.options(path, middleware(*middies))
|
|
return self
|
|
|
|
def delete(self, path, handler):
|
|
middies = list(self.middlewares)
|
|
middies.append(handler)
|
|
self.app.delete(path, middleware(*middies))
|
|
return self
|
|
|
|
def patch(self, path, handler):
|
|
middies = list(self.middlewares)
|
|
middies.append(handler)
|
|
self.app.patch(path, middleware(*middies))
|
|
return self
|
|
|
|
def put(self, path, handler):
|
|
middies = list(self.middlewares)
|
|
middies.append(handler)
|
|
self.app.put(path, middleware(*middies))
|
|
return self
|
|
|
|
def head(self, path, handler):
|
|
middies = list(self.middlewares)
|
|
middies.append(handler)
|
|
self.app.head(path, middleware(*middies))
|
|
return self
|
|
|
|
def connect(self, path, handler):
|
|
middies = list(self.middlewares)
|
|
middies.append(handler)
|
|
self.app.connect(path, middleware(*middies))
|
|
return self
|
|
|
|
def trace(self, path, handler):
|
|
middies = list(self.middlewares)
|
|
middies.append(handler)
|
|
self.app.trace(path, middleware(*middies))
|
|
return self
|
|
|
|
def any(self, path, handler):
|
|
middies = list(self.middlewares)
|
|
middies.append(handler)
|
|
self.app.any(path, middleware(*middies))
|
|
return self
|