diff --git a/src/socketify/socketify.py b/src/socketify/socketify.py index 186eab6..f4eefe9 100644 --- a/src/socketify/socketify.py +++ b/src/socketify/socketify.py @@ -552,7 +552,7 @@ class AppResponse: return self._chunkFuture #failed to send chunk self._lastChunkOffset = self.get_write_offset() - + return self._chunkFuture def get_data(self): diff --git a/src/tests.py b/src/tests.py index 461d1b8..3c4af33 100644 --- a/src/tests.py +++ b/src/tests.py @@ -18,36 +18,80 @@ # try_end +# get_full_url # for_each_header # https://github.com/uNetworking/uWebSockets.js/blob/master/examples/VideoStreamer.js from socketify import App -import os +# import os import multiprocessing import asyncio import aiofiles - +from aiofiles import os +import time +import mimetypes +mimetypes.init() #need to fix get_data using sel._data etc async def home(res, req): - # res.write_header("Content-Type", "plain/text") - # await asyncio.sleep(0) - # res.write_header("Content-Type", "audio/mpeg") - res.write_header("Content-Type", "application/octet-stream") filename = "./file_example_MP3_5MG.mp3" - total = os.stat(filename).st_size - - async with aiofiles.open(filename, "rb") as fd: - while not res.aborted: - buffer = await fd.read(16*1024) - (ok, done) = await res.send_chunk(buffer, total) - if not ok or done: #if cannot send probably aborted - break + if_modified_since = req.get_header('if-modified-since') + range_header = req.get_header('range') + bytes_range = None + start = 0 + end = -1 + 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) + if not exists: + return res.write_status(404).end(b'Not Found') + stats = await os.stat(filename) + total_size = stats.st_size + last_modified = time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(stats.st_mtime)) + if if_modified_since == last_modified: + res.write_status(304).end_without_body() + return + res.write_header(b'Last-Modified', last_modified) + + (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: + if start > 0 or not end == -1: + if end < 1 or end >= total_size: + end = total_size + total_size = end - start + await fd.seek(start) + res.write_status(206) + else: + end = total_size + res.write_status(200) + + #tells the browser that we support ranges + res.write_header(b'Accept-Ranges', b'bytes') + res.write_header(b'Content-Range', 'bytes %d-%d/%d' % (start, end, total_size)) + + while not res.aborted: + buffer = await fd.read(16384) #16kb chunks + (ok, done) = await res.send_chunk(buffer, total_size) + if not ok or done: #if cannot send probably aborted + break + except Exception as error: + print(str(error)) + res.write_status(500).end("Internal Error") + def run_app(): app = App() app.get("/", home) - app.listen(3000, lambda config: print("PID %d Listening on port http://localhost:%d now\n" % (os.getpid(), config.port))) + app.listen(3000, lambda config: print("Listening on port http://localhost:%d now\n" % (config.port))) app.run() def create_fork(): diff --git a/tests/examples/file_stream.py b/tests/examples/file_stream.py new file mode 100644 index 0000000..1904842 --- /dev/null +++ b/tests/examples/file_stream.py @@ -0,0 +1,65 @@ +from socketify import App +import aiofiles +from aiofiles import os +import time +import mimetypes +from os import path + +mimetypes.init() + +async def home(res, req): + #there is also a helper called static with an send_file method see static_files.py for examples of usage + #here is an sample implementation, a more complete one is in static.send_file + + filename = "./public/media/flower.webm" + #read headers before the first await + if_modified_since = req.get_header('if-modified-since') + + 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 + 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 + #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: + res.write_status(200) + + pending_size = total_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, total_size) + if not ok or done or pending_size <= 0: #if cannot send probably aborted + break + + + except Exception as error: + res.write_status(500).end("Internal Error") + +app = App() +app.get("/", home) +app.listen(3000, lambda config: print("Listening on port http://localhost:%d now\n" % config.port)) +app.run() \ No newline at end of file diff --git a/tests/examples/helpers/static.py b/tests/examples/helpers/static.py new file mode 100644 index 0000000..8262238 --- /dev/null +++ b/tests/examples/helpers/static.py @@ -0,0 +1,105 @@ +import aiofiles +from aiofiles import os +import time +import mimetypes +from os import path + +mimetypes.init() +#try with https://github.com/mosquito/aiofile +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/public/css/main.css b/tests/examples/public/css/main.css new file mode 100644 index 0000000..e1391ea --- /dev/null +++ b/tests/examples/public/css/main.css @@ -0,0 +1,3 @@ +h1 { + color:red +} \ No newline at end of file diff --git a/tests/examples/public/images/favicon.png b/tests/examples/public/images/favicon.png new file mode 100644 index 0000000..d5696d6 Binary files /dev/null and b/tests/examples/public/images/favicon.png differ diff --git a/tests/examples/public/index.html b/tests/examples/public/index.html new file mode 100644 index 0000000..8a02122 --- /dev/null +++ b/tests/examples/public/index.html @@ -0,0 +1,19 @@ + + + + + + + Static Test + + + + + + +

This title Must be red

+ + + \ No newline at end of file diff --git a/tests/examples/public/js/main.js b/tests/examples/public/js/main.js new file mode 100644 index 0000000..a7eb881 --- /dev/null +++ b/tests/examples/public/js/main.js @@ -0,0 +1,3 @@ +div = document.createElement("div") +div.innerText = "This Element was added via scripts.js" +document.body.appendChild(div) \ No newline at end of file diff --git a/tests/examples/public/media/flower.webm b/tests/examples/public/media/flower.webm new file mode 100644 index 0000000..5b8edf7 Binary files /dev/null and b/tests/examples/public/media/flower.webm differ diff --git a/tests/examples/public/robots.txt b/tests/examples/public/robots.txt new file mode 100644 index 0000000..f677fae --- /dev/null +++ b/tests/examples/public/robots.txt @@ -0,0 +1 @@ +User-agent: * Disallow: / \ No newline at end of file diff --git a/tests/examples/requeriments.txt b/tests/examples/requeriments.txt index 63ccd82..9dc5ad6 100644 --- a/tests/examples/requeriments.txt +++ b/tests/examples/requeriments.txt @@ -1,3 +1,4 @@ aiohttp +aiofiles redis git+https://github.com/cirospaciari/socketify.py.git@main#socketify --global-option="build_ext" \ No newline at end of file diff --git a/tests/examples/static_files.py b/tests/examples/static_files.py new file mode 100644 index 0000000..8d92f11 --- /dev/null +++ b/tests/examples/static_files.py @@ -0,0 +1,20 @@ +from socketify import App +from helpers.static import static_route +from helpers.static import send_file + + +app = App() + +#serve all files in public folder under / route (main route but can be like /assets) +static_route(app, "/", "./public") + +#send home page index.html +async def home(res, req): + #sends the whole file with 304 and bytes range support + await send_file(res, req, "./public/index.html") + +app.get("/", home) + +app.listen(3000, lambda config: print("Listening on port http://localhost:%d now\n" % config.port)) +app.run() +