From e8da9790dc6d55cd2e8883322cdf9a7bf5b4f5b7 Mon Sep 17 00:00:00 2001 From: Joris Date: Mon, 20 May 2024 20:09:01 +0200 Subject: Migrate to sanic --- flake.nix | 3 +++ public/main.css | 71 ------------------------------------------------------- public/main.js | 48 ------------------------------------- src/controller.py | 58 +++++++++++++++++++++++++++++++++++++++++++++ src/main-old.py | 33 ++++++++++++++++++++++++++ src/main.py | 40 +++++++++++++++++-------------- src/templates.py | 14 +++++++---- src/utils.py | 11 --------- static/main.css | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ static/main.js | 48 +++++++++++++++++++++++++++++++++++++ 10 files changed, 245 insertions(+), 152 deletions(-) delete mode 100644 public/main.css delete mode 100644 public/main.js create mode 100644 src/controller.py create mode 100644 src/main-old.py create mode 100644 static/main.css create mode 100644 static/main.js diff --git a/flake.nix b/flake.nix index 92951f0..170fe8d 100644 --- a/flake.nix +++ b/flake.nix @@ -13,9 +13,12 @@ (pkgs.python3.withPackages (pythonPackages: with pythonPackages; [ sqlite watchexec + sanic + setuptools ])) ]; shellHook = '' + export DEBUG="TRUE" export HOST="127.0.0.1" export PORT="8080" export KEY="1234" diff --git a/public/main.css b/public/main.css deleted file mode 100644 index db9a678..0000000 --- a/public/main.css +++ /dev/null @@ -1,71 +0,0 @@ -html { - margin: 0 1rem; -} - -body { - max-width: 30rem; - margin: 0 auto; - font-family: sans-serif; -} - -a { - text-decoration: none; - color: #06C; -} - -h1 { - text-align: center; - font-variant: small-caps; - font-size: 40px; - letter-spacing: 0.2rem; - margin-bottom: 4rem; -} - -.g-Link { - text-decoration: underline; -} - -label { - display: flex; - gap: 0.5rem; - flex-direction: column; - margin-bottom: 2rem; -} - -input[type=submit] { - width: 100%; -} - -.g-Loading { - display: none; - align-items: center; - justify-content: center; - gap: 1rem; - margin-bottom: 2rem; -} - -.g-Error { - text-align: center; - margin-bottom: 2rem; - color: #C00; -} - -.g-Spinner { - width: 25px; - height: 25px; - border: 4px solid #06C; - border-bottom-color: transparent; - border-radius: 50%; - display: inline-block; - box-sizing: border-box; - animation: rotation 1s linear infinite; -} - -@keyframes rotation { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} diff --git a/public/main.js b/public/main.js deleted file mode 100644 index 1729d38..0000000 --- a/public/main.js +++ /dev/null @@ -1,48 +0,0 @@ -window.onload = function() { - const form = document.querySelector('form') - - if (form !== null) { - const submit = document.querySelector('input[type="submit"]') - const loading = document.querySelector('.g-Loading') - const error = document.querySelector('.g-Error') - - function showError(msg) { - loading.style.display = 'none' - submit.disabled = false - error.innerText = msg - error.style.display = 'block' - } - - form.onsubmit = function(event) { - event.preventDefault() - - loading.style.display = 'flex' - submit.disabled = true - error.style.display = 'none' - - const key = document.querySelector('input[name="key"]').value - const expiration = document.querySelector('select[name="expiration"]').value - const file = document.querySelector('input[name="file"]').files[0] - - // Wait a bit to prevent showing the loader too briefly - setTimeout(function() { - const xhr = new XMLHttpRequest() - xhr.open('POST', '/', true) - xhr.onload = function () { - if (xhr.status === 200) { - window.location = `/${xhr.responseText}` - } else { - showError(`Error uploading: ${xhr.status}`) - } - } - xhr.onerror = function () { - showError('Upload error') - } - xhr.setRequestHeader('X-FileName', file.name) - xhr.setRequestHeader('X-Expiration', expiration) - xhr.setRequestHeader('X-Key', key) - xhr.send(file) - }, 500) - } - } -} diff --git a/src/controller.py b/src/controller.py new file mode 100644 index 0000000..351d0bc --- /dev/null +++ b/src/controller.py @@ -0,0 +1,58 @@ +import io +import logging +import os +import sanic +import sqlite3 +import tempfile + +import db +import templates +import utils + +conn = sqlite3.connect('db.sqlite3') +files_directory = 'files' +authorized_key = os.environ['KEY'] + +def index(): + return sanic.html(templates.index) + +async def upload(request): + key = request.headers.get('X-Key') + if not key == authorized_key: + sanic.log.logging.info('Unauthorized to upload file: wrong key') + return sanic.text('Unauthorized', status = 401) + else: + sanic.log.logging.info('Uploading file') + content_length = int(request.headers.get('content-length')) + filename = utils.sanitize_filename(request.headers.get('X-FileName')) + expiration = request.headers.get('X-Expiration') + + with tempfile.NamedTemporaryFile(delete = False) as tmp: + while data := await request.stream.read(): + tmp.write(data) + + sanic.log.logging.info('File uploaded') + file_id = db.insert_file(conn, filename, expiration, content_length) + os.makedirs(files_directory, exist_ok=True) + os.rename(tmp.name, os.path.join(files_directory, file_id)) + + return sanic.text(file_id) + +async def file(file_id: str, download: bool): + res = db.get_file(conn, file_id) + if res is None: + self._serve_str(templates.not_found, 404, 'text/html') + else: + filename, expires, content_length = res + disk_path = os.path.join(files_directory, file_id) + if download: + return await sanic.response.file_stream( + disk_path, + chunk_size = io.DEFAULT_BUFFER_SIZE, + headers = { + 'Content-Disposition': f'attachment; filename={filename}', + 'Content-Length': content_length + } + ) + else: + return sanic.html(templates.file_page(file_id, filename, expires)) diff --git a/src/main-old.py b/src/main-old.py new file mode 100644 index 0000000..42d7c8c --- /dev/null +++ b/src/main-old.py @@ -0,0 +1,33 @@ +# import http.server +# import logging +# import os +# import sys + +# import server + +# logger = logging.getLogger(__name__) +# hostName = os.environ['HOST'] +# serverPort = int(os.environ['PORT']) + +# if __name__ == '__main__': +# logging.basicConfig(stream=sys.stdout, level=logging.INFO) +# webServer = http.server.HTTPServer((hostName, serverPort), server.MyServer) +# logger.info('Server started at http://%s:%s.' % (hostName, serverPort)) + +# try: +# webServer.serve_forever() +# except KeyboardInterrupt: +# pass + +# webServer.server_close() +# conn.close() +# logger.info('Server stopped.') + +from sanic import Sanic +from sanic.response import text + +app = Sanic("MyHelloWorldApp") + +@app.get("/") +async def hello_world(request): + return text("Hello, world.") diff --git a/src/main.py b/src/main.py index 56c8e9e..b678aae 100644 --- a/src/main.py +++ b/src/main.py @@ -1,24 +1,28 @@ -import http.server -import logging +import sanic import os -import sys -import server +import controller -logger = logging.getLogger(__name__) -hostName = os.environ['HOST'] -serverPort = int(os.environ['PORT']) +app = sanic.Sanic("Files") -if __name__ == '__main__': - logging.basicConfig(stream=sys.stdout, level=logging.INFO) - webServer = http.server.HTTPServer((hostName, serverPort), server.MyServer) - logger.info('Server started at http://%s:%s.' % (hostName, serverPort)) +@app.get("/") +async def index(request): + return controller.index() - try: - webServer.serve_forever() - except KeyboardInterrupt: - pass +@app.post("/", stream = True) +async def upload(request): + return await controller.upload(request) - webServer.server_close() - conn.close() - logger.info('Server stopped.') +@app.get("/") +async def file_page(request, file_id): + return await controller.file(file_id, download = False) + +@app.get("//download") +async def file_download(request, file_id): + return await controller.file(file_id, download = True) + +app.static("/static/", "static/") + +if __name__ == "__main__": + debug = 'DEBUG' in os.environ and os.environ['DEBUG'] == 'TRUE' + app.run(debug=debug, access_log=True) diff --git a/src/templates.py b/src/templates.py index c605e57..8125f69 100644 --- a/src/templates.py +++ b/src/templates.py @@ -8,15 +8,15 @@ page: str = ''' Files - - + +

Files

''' -index: str = f''' +pub index: str = f''' {page}
@@ -79,8 +79,14 @@ index: str = f'''
''' -def download(href: str, filename: str, expires: str) -> str: +def file_page(file_id: str, filename: str, expires: str) -> str: + href = f'{file_id}/download' expires_in = datetime.datetime.strptime(expires, '%Y-%m-%d %H:%M:%S') - datetime.datetime.now() + + print() + print(href) + print() + return f''' {page} diff --git a/src/utils.py b/src/utils.py index ccf92c0..151217f 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,16 +1,5 @@ import io -def transfer(reader, writer, content_length = None, buffer_size = io.DEFAULT_BUFFER_SIZE): - if content_length is None: - while (data := reader.read(buffer_size)): - writer.write(data) - else: - remaining = content_length - while remaining > 0: - size = min(buffer_size, remaining) - writer.write(reader.read(size)) - remaining -= size - def sanitize_filename(s: str) -> str: return '.'.join([sanitize_filename_part(p) for p in s.split('.')]) diff --git a/static/main.css b/static/main.css new file mode 100644 index 0000000..db9a678 --- /dev/null +++ b/static/main.css @@ -0,0 +1,71 @@ +html { + margin: 0 1rem; +} + +body { + max-width: 30rem; + margin: 0 auto; + font-family: sans-serif; +} + +a { + text-decoration: none; + color: #06C; +} + +h1 { + text-align: center; + font-variant: small-caps; + font-size: 40px; + letter-spacing: 0.2rem; + margin-bottom: 4rem; +} + +.g-Link { + text-decoration: underline; +} + +label { + display: flex; + gap: 0.5rem; + flex-direction: column; + margin-bottom: 2rem; +} + +input[type=submit] { + width: 100%; +} + +.g-Loading { + display: none; + align-items: center; + justify-content: center; + gap: 1rem; + margin-bottom: 2rem; +} + +.g-Error { + text-align: center; + margin-bottom: 2rem; + color: #C00; +} + +.g-Spinner { + width: 25px; + height: 25px; + border: 4px solid #06C; + border-bottom-color: transparent; + border-radius: 50%; + display: inline-block; + box-sizing: border-box; + animation: rotation 1s linear infinite; +} + +@keyframes rotation { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/static/main.js b/static/main.js new file mode 100644 index 0000000..1729d38 --- /dev/null +++ b/static/main.js @@ -0,0 +1,48 @@ +window.onload = function() { + const form = document.querySelector('form') + + if (form !== null) { + const submit = document.querySelector('input[type="submit"]') + const loading = document.querySelector('.g-Loading') + const error = document.querySelector('.g-Error') + + function showError(msg) { + loading.style.display = 'none' + submit.disabled = false + error.innerText = msg + error.style.display = 'block' + } + + form.onsubmit = function(event) { + event.preventDefault() + + loading.style.display = 'flex' + submit.disabled = true + error.style.display = 'none' + + const key = document.querySelector('input[name="key"]').value + const expiration = document.querySelector('select[name="expiration"]').value + const file = document.querySelector('input[name="file"]').files[0] + + // Wait a bit to prevent showing the loader too briefly + setTimeout(function() { + const xhr = new XMLHttpRequest() + xhr.open('POST', '/', true) + xhr.onload = function () { + if (xhr.status === 200) { + window.location = `/${xhr.responseText}` + } else { + showError(`Error uploading: ${xhr.status}`) + } + } + xhr.onerror = function () { + showError('Upload error') + } + xhr.setRequestHeader('X-FileName', file.name) + xhr.setRequestHeader('X-Expiration', expiration) + xhr.setRequestHeader('X-Key', key) + xhr.send(file) + }, 500) + } + } +} -- cgit v1.2.3