From 1019ea1ed341e3a7769c046aa0be5764789360b6 Mon Sep 17 00:00:00 2001 From: Joris Date: Sun, 2 Jun 2024 14:38:13 +0200 Subject: Migrate to Rust and Hyper With sanic, downloading a file locally is around ten times slower than with Rust and hyper. Maybe `pypy` could have helped, but I didn’t succeed to set it up quickly with the dependencies. --- .gitignore | 4 +- Cargo.lock | 1306 +++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 23 + bin/dev-server | 16 +- flake.lock | 63 ++- flake.nix | 51 +- init-db.sql | 4 +- src/controller.py | 58 --- src/db.py | 24 - src/db.rs | 125 +++++ src/main-old.py | 33 -- src/main.py | 28 -- src/main.rs | 68 +++ src/model.rs | 52 ++ src/routes.rs | 209 +++++++++ src/server.py | 84 ---- src/static/main.css | 94 ++++ src/static/main.js | 49 ++ src/templates.py | 105 ----- src/templates.rs | 134 ++++++ src/util.rs | 125 +++++ src/utils.py | 8 - static/main.css | 71 --- static/main.js | 48 -- 24 files changed, 2288 insertions(+), 494 deletions(-) create mode 100644 Cargo.lock create mode 100644 Cargo.toml delete mode 100644 src/controller.py delete mode 100644 src/db.py create mode 100644 src/db.rs delete mode 100644 src/main-old.py delete mode 100644 src/main.py create mode 100644 src/main.rs create mode 100644 src/model.rs create mode 100644 src/routes.rs delete mode 100644 src/server.py create mode 100644 src/static/main.css create mode 100644 src/static/main.js delete mode 100644 src/templates.py create mode 100644 src/templates.rs create mode 100644 src/util.rs delete mode 100644 src/utils.py delete mode 100644 static/main.css delete mode 100644 static/main.js diff --git a/.gitignore b/.gitignore index de6d4e7..19dbd02 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ -__pycache__ -files +target/ +files/ db.sqlite3 diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..208a59b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1306 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" + +[[package]] +name = "anstyle-parse" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytes" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" + +[[package]] +name = "cc" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.5", +] + +[[package]] +name = "colorchoice" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" + +[[package]] +name = "const_format" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "env_filter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + +[[package]] +name = "files" +version = "0.1.0" +dependencies = [ + "base64", + "chrono", + "const_format", + "env_logger", + "futures-util", + "html-escape", + "http-body-util", + "hyper", + "hyper-util", + "log", + "rand", + "rand_core", + "tempfile", + "tokio", + "tokio-rusqlite", + "tokio-util", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "h2" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d8d52be92d09acc2e01dddb7fde3ad983fc6489c7db4837e605bc3fca4cb63e" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "libsqlite3-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "miniz_oxide" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.5", +] + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" + +[[package]] +name = "rusqlite" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "syn" +version = "2.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio" +version = "1.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rusqlite" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2cc5f712424f089fc6549afe39773e9f8914ce170c45b546be24830b482b127" +dependencies = [ + "crossbeam-channel", + "rusqlite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + +[[package]] +name = "zerocopy" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0c69f2c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "files" +version = "0.1.0" +authors = ["Joris "] +edition = "2021" + +[dependencies] +base64 = "0.22" +chrono = "0.4" +const_format = "0.2" +env_logger = "0.11" +futures-util = "0.3" +html-escape = "0.2" +http-body-util = "0.1" +hyper = { version = "1", features = ["full"] } +hyper-util = { version = "0.1", features = ["full"] } +log = "0.4" +rand = { version = "0.8", features = ["getrandom"] } +rand_core = "0.6.0" +tempfile = "3.10" +tokio = { version = "1", features = ["full"] } +tokio-rusqlite = "0.5.1" +tokio-util = "0.7" diff --git a/bin/dev-server b/bin/dev-server index d489127..cb124df 100755 --- a/bin/dev-server +++ b/bin/dev-server @@ -3,14 +3,20 @@ set -euo pipefail if ! [ -f db.sqlite3 ]; then - echo "Creating databise" + echo "Creating database" sqlite3 db.sqlite3 < init-db.sql sleep 1 fi +export HOST="127.0.0.1" +export PORT="8080" +export KEY="1234" +export DB="db.sqlite3" +export FILES_DIR="files" + watchexec \ - --restart \ - --clear \ - --exts py \ - python src/main.py + --clear \ + --restart \ + -w src \ + "RUST_LOG=info cargo run" diff --git a/flake.lock b/flake.lock index ea71eaf..7ab693c 100644 --- a/flake.lock +++ b/flake.lock @@ -18,13 +18,31 @@ "type": "github" } }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1705309234, + "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, "nixpkgs": { "locked": { - "lastModified": 1716040799, - "narHash": "sha256-0U19tjIaggl2b+v1ozMj7yVMCoWb1MOcV8dzTuyEZB8=", + "lastModified": 1716641285, + "narHash": "sha256-kwSMlUXL1sayqznVuL0OCF46h9a/cg7ESko0ISEVGUo=", "owner": "nixos", "repo": "nixpkgs", - "rev": "cb7884d6de31c46736adb561533527238fe7d3c9", + "rev": "a44514a1227ee12947a55a569c24da3aedaf5e85", "type": "github" }, "original": { @@ -36,7 +54,29 @@ "root": { "inputs": { "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "flake-utils": "flake-utils_2", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1716603336, + "narHash": "sha256-81u/zd7V+XRTq88zwRLxw5GnwZyEiAvGA2BvAXUe864=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "4d0f1e4d5d65c23cdbb77e4b0d91940be7309bd4", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" } }, "systems": { @@ -53,6 +93,21 @@ "repo": "default", "type": "github" } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 170fe8d..fe059df 100644 --- a/flake.nix +++ b/flake.nix @@ -1,28 +1,35 @@ { inputs = { - nixpkgs.url = "github:nixos/nixpkgs"; - flake-utils.url = "github:numtide/flake-utils"; + nixpkgs.url = "github:nixos/nixpkgs"; + flake-utils.url = "github:numtide/flake-utils"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; - outputs = { self, nixpkgs, flake-utils }: - flake-utils.lib.eachDefaultSystem - (system: - let pkgs = nixpkgs.legacyPackages.${system}; - in { devShell = pkgs.mkShell { - buildInputs = with pkgs; [ - (pkgs.python3.withPackages (pythonPackages: with pythonPackages; [ - sqlite - watchexec - sanic - setuptools - ])) + outputs = { self, nixpkgs, rust-overlay, flake-utils, ... }: + flake-utils.lib.eachDefaultSystem (system: + let + overlays = [ (import rust-overlay) ]; + pkgs = import nixpkgs { + inherit system overlays; + }; + in + with pkgs; + { + devShell = mkShell { + buildInputs = [ + cargo-watch + lld + openssl + pkg-config + rust-bin.stable."1.78.0".default + sqlite + watchexec ]; - shellHook = '' - export DEBUG="TRUE" - export HOST="127.0.0.1" - export PORT="8080" - export KEY="1234" - ''; - }; } - ); + }; + } + ); } + diff --git a/init-db.sql b/init-db.sql index 57afd00..bbfe6c7 100644 --- a/init-db.sql +++ b/init-db.sql @@ -1,7 +1,7 @@ CREATE TABLE files( id TEXT PRIMARY KEY, + created_at STRING NOT NULL, + expires_at STRING NOT NULL, filename TEXT NOT NULL, - created TEXT NOT NULL, - expires TEXT NOT NULL, content_length INTEGER NOT NULL ) diff --git a/src/controller.py b/src/controller.py deleted file mode 100644 index 351d0bc..0000000 --- a/src/controller.py +++ /dev/null @@ -1,58 +0,0 @@ -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/db.py b/src/db.py deleted file mode 100644 index a6e29fd..0000000 --- a/src/db.py +++ /dev/null @@ -1,24 +0,0 @@ -import secrets - -def insert_file(conn, filename: str, expiration_days: int, content_length: int): - cur = conn.cursor() - file_id = secrets.token_urlsafe() - cur.execute( - 'INSERT INTO files(id, filename, created, expires, content_length) VALUES(?, ?, datetime(), datetime(datetime(), ?), ?)', - (file_id, filename, f'+{expiration_days} days', content_length) - ) - conn.commit() - return file_id - -def get_file(conn, file_id: str): - cur = conn.cursor() - res = cur.execute( - ''' - SELECT filename, expires, content_length - FROM files - WHERE id = ? AND expires > datetime() - ''', - (file_id,) - ) - return res.fetchone() - diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..e1bb7e3 --- /dev/null +++ b/src/db.rs @@ -0,0 +1,125 @@ +use tokio_rusqlite::{params, Connection, Result}; + +use crate::model::{decode_datetime, encode_datetime, File}; + +pub async fn insert_file(conn: &Connection, file: File) -> Result<()> { + conn.call(move |conn| { + conn.execute( + r#" + INSERT INTO + files(id, created_at, expires_at, filename, content_length) + VALUES + (?1, datetime(), ?2, ?3, ?4) + "#, + params![ + file.id, + encode_datetime(file.expires_at), + file.name, + file.content_length + ], + ) + .map_err(tokio_rusqlite::Error::Rusqlite) + }) + .await + .map(|_| ()) +} + +pub async fn get_file(conn: &Connection, file_id: String) -> Result> { + conn.call(move |conn| { + let mut stmt = conn.prepare( + r#" + SELECT + filename, expires_at, content_length + FROM + files + WHERE + id = ? + AND expires_at > datetime() + "#, + )?; + + let mut iter = stmt.query_map([file_id.clone()], |row| { + let res: (String, String, usize) = (row.get(0)?, row.get(1)?, row.get(2)?); + Ok(res) + })?; + + match iter.next() { + Some(Ok((filename, expires_at, content_length))) => { + match decode_datetime(&expires_at) { + Some(expires_at) => Ok(Some(File { + id: file_id.clone(), + name: filename, + expires_at, + content_length, + })), + _ => Err(rusqlite_other_error(&format!( + "Error decoding datetime: {expires_at}" + ))), + } + } + Some(_) => Err(rusqlite_other_error("Error reading file in DB")), + None => Ok(None), + } + }) + .await +} + +fn rusqlite_other_error(msg: &str) -> tokio_rusqlite::Error { + tokio_rusqlite::Error::Other(msg.into()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::model::local_time; + use chrono::Duration; + use std::ops::Add; + + #[tokio::test] + async fn test_insert_and_get_file() { + let conn = get_connection().await; + let file = dummy_file(Duration::minutes(1)); + assert!(insert_file(&conn, file.clone()).await.is_ok()); + let file_res = get_file(&conn, file.id.clone()).await; + assert!(file_res.is_ok()); + assert_eq!(file_res.unwrap(), Some(file)); + } + + #[tokio::test] + async fn test_expired_file_err() { + let conn = get_connection().await; + let file = dummy_file(Duration::zero()); + assert!(insert_file(&conn, file.clone()).await.is_ok()); + let file_res = get_file(&conn, file.id.clone()).await; + assert!(file_res.is_ok()); + assert!(file_res.unwrap().is_none()); + } + + #[tokio::test] + async fn test_wrong_file_err() { + let conn = get_connection().await; + let file = dummy_file(Duration::minutes(1)); + assert!(insert_file(&conn, file.clone()).await.is_ok()); + let file_res = get_file(&conn, "wrong-id".to_string()).await; + assert!(file_res.is_ok()); + assert!(file_res.unwrap().is_none()); + } + + fn dummy_file(td: Duration) -> File { + File { + id: "1234".to_string(), + name: "foo".to_string(), + expires_at: local_time().add(td), + content_length: 100, + } + } + + async fn get_connection() -> Connection { + let conn = Connection::open_in_memory().await.unwrap(); + let init_db = tokio::fs::read_to_string("init-db.sql").await.unwrap(); + + let res = conn.call(move |conn| Ok(conn.execute(&init_db, []))).await; + assert!(res.is_ok()); + conn + } +} diff --git a/src/main-old.py b/src/main-old.py deleted file mode 100644 index 42d7c8c..0000000 --- a/src/main-old.py +++ /dev/null @@ -1,33 +0,0 @@ -# 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 deleted file mode 100644 index b678aae..0000000 --- a/src/main.py +++ /dev/null @@ -1,28 +0,0 @@ -import sanic -import os - -import controller - -app = sanic.Sanic("Files") - -@app.get("/") -async def index(request): - return controller.index() - -@app.post("/", stream = True) -async def upload(request): - return await controller.upload(request) - -@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/main.rs b/src/main.rs new file mode 100644 index 0000000..27da278 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,68 @@ +use std::env; +use std::net::SocketAddr; + +use hyper::server::conn::http1; +use hyper::service::service_fn; +use hyper_util::rt::TokioIo; +use tokio::net::TcpListener; +use tokio_rusqlite::Connection; + +mod db; +mod model; +mod routes; +mod templates; +mod util; + +#[tokio::main] +async fn main() -> std::result::Result<(), Box> { + env_logger::init(); + + let host = get_env("HOST"); + let port = get_env("PORT"); + let db_path = get_env("DB"); + let authorized_key = get_env("KEY"); + let files_dir = get_env("FILES_DIR"); + + let db_conn = Connection::open(db_path) + .await + .expect("Error while openning DB conection"); + + let addr: SocketAddr = format!("{host}:{port}") + .parse() + .unwrap_or_else(|_| panic!("Invalid address: {host}:{port}")); + + let listener = TcpListener::bind(addr).await?; + log::info!("Listening on http://{}", addr); + + loop { + let (stream, _) = listener.accept().await?; + let io = TokioIo::new(stream); + + let db_conn = db_conn.clone(); + let authorized_key = authorized_key.clone(); + let files_dir = files_dir.clone(); + + tokio::task::spawn(async move { + if let Err(err) = http1::Builder::new() + .serve_connection( + io, + service_fn(move |req| { + routes::routes( + req, + db_conn.clone(), + authorized_key.clone(), + files_dir.clone(), + ) + }), + ) + .await + { + log::error!("Failed to serve connection: {:?}", err); + } + }); + } +} + +fn get_env(key: &str) -> String { + env::var(key).unwrap_or_else(|_| panic!("Missing environment variable {key}")) +} diff --git a/src/model.rs b/src/model.rs new file mode 100644 index 0000000..ed4fbf8 --- /dev/null +++ b/src/model.rs @@ -0,0 +1,52 @@ +use base64::{engine::general_purpose::URL_SAFE, Engine as _}; +use chrono::{DateTime, Local, NaiveDateTime, TimeZone}; +use rand_core::{OsRng, RngCore}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct File { + pub id: String, + pub name: String, + pub expires_at: DateTime, + pub content_length: usize, +} + +pub fn local_time() -> DateTime { + let dt = Local::now(); + match decode_datetime(&encode_datetime(dt)) { + Some(res) => res, + None => dt, + } +} + +// Using 20 bytes (160 bits) to file identifiers +// https://owasp.org/www-community/vulnerabilities/Insufficient_Session-ID_Length +// https://www.rfc-editor.org/rfc/rfc6749.html#section-10.10 +const FILE_ID_BYTES: usize = 20; + +pub fn generate_file_id() -> String { + let mut token = [0u8; FILE_ID_BYTES]; + OsRng.fill_bytes(&mut token); + URL_SAFE.encode(token) +} + +const FORMAT: &str = "%Y-%m-%d %H:%M:%S"; + +pub fn encode_datetime(dt: DateTime) -> String { + dt.naive_utc().format(FORMAT).to_string() +} + +pub fn decode_datetime(str: &str) -> Option> { + let naive_time = NaiveDateTime::parse_from_str(str, FORMAT).ok()?; + Some(Local.from_utc_datetime(&naive_time)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_datetime_serialization() { + let dt = local_time(); + assert_eq!(decode_datetime(&encode_datetime(dt)), Some(dt)) + } +} diff --git a/src/routes.rs b/src/routes.rs new file mode 100644 index 0000000..b54e565 --- /dev/null +++ b/src/routes.rs @@ -0,0 +1,209 @@ +use std::ops::Add; +use std::path::{Path, PathBuf}; + +use chrono::Duration; +use futures_util::TryStreamExt; +use http_body_util::{combinators::BoxBody, BodyExt, Full, StreamBody}; +use hyper::body::{Bytes, Frame, Incoming}; +use hyper::header::{HeaderName, HeaderValue, CONTENT_DISPOSITION, CONTENT_LENGTH, CONTENT_TYPE}; +use hyper::{Method, Request, Response, Result, StatusCode}; +use tokio::io::AsyncWriteExt; +use tokio::{fs, fs::File}; +use tokio_rusqlite::Connection; +use tokio_util::io::ReaderStream; + +use crate::db; +use crate::model; +use crate::templates; +use crate::util; + +pub async fn routes( + request: Request, + db_conn: Connection, + authorized_key: String, + files_dir: String, +) -> Result>> { + let path = &request.uri().path().split('/').collect::>()[1..]; + let files_dir = Path::new(&files_dir); + + match (request.method(), path) { + (&Method::GET, [""]) => Ok(response(StatusCode::OK, templates::INDEX.to_string())), + (&Method::GET, ["static", "main.js"]) => Ok(static_file( + include_str!("static/main.js").to_string(), + "application/javascript", + )), + (&Method::GET, ["static", "main.css"]) => Ok(static_file( + include_str!("static/main.css").to_string(), + "text/css", + )), + (&Method::POST, [""]) => upload_file(request, db_conn, authorized_key, files_dir).await, + (&Method::GET, [file_id]) => get(db_conn, file_id, GetFile::ShowPage, files_dir).await, + (&Method::GET, [file_id, "download"]) => { + get(db_conn, file_id, GetFile::Download, files_dir).await + } + _ => Ok(not_found()), + } +} + +async fn upload_file( + request: Request, + db_conn: Connection, + authorized_key: String, + files_dir: &Path, +) -> Result>> { + let key = get_header(&request, "X-Key"); + if key != Some(authorized_key) { + log::info!("Unauthorized file upload"); + Ok(response( + StatusCode::UNAUTHORIZED, + "Unauthorized".to_string(), + )) + } else { + let file_id = model::generate_file_id(); + let filename = get_header(&request, "X-Filename").map(|s| util::sanitize_filename(&s)); + let expiration_days: Option = + get_header(&request, "X-Expiration").and_then(|s| s.parse().ok()); + let content_length: Option = + get_header(&request, "Content-Length").and_then(|s| s.parse().ok()); + + match (filename, expiration_days, content_length) { + (Some(filename), Some(expiration_days), Some(content_length)) => { + let _ = fs::create_dir(files_dir).await; + let path = files_dir.join(&file_id); + let mut file = File::create(&path).await.unwrap(); + + let mut incoming = request.into_body(); + while let Some(frame) = incoming.frame().await { + if let Ok(data) = frame { + let _ = file.write_all(&data.into_data().unwrap()).await; + let _ = file.flush().await; + } + } + + let file = model::File { + id: file_id.clone(), + name: filename, + expires_at: model::local_time().add(Duration::days(expiration_days)), + content_length, + }; + + match db::insert_file(&db_conn, file.clone()).await { + Ok(_) => Ok(response(StatusCode::OK, file_id)), + Err(msg) => { + log::error!("Insert file: {msg}"); + if let Err(msg) = fs::remove_file(path).await { + log::error!("Remove file: {msg}"); + }; + Ok(internal_server_error()) + } + } + } + _ => Ok(bad_request()), + } + } +} + +fn get_header(request: &Request, header: &str) -> Option { + request + .headers() + .get(header)? + .to_str() + .ok() + .map(|str| str.to_string()) +} + +enum GetFile { + ShowPage, + Download, +} + +async fn get( + db_conn: Connection, + file_id: &str, + get_file: GetFile, + files_dir: &Path, +) -> Result>> { + let file = db::get_file(&db_conn, file_id.to_string()).await; + match (get_file, file) { + (GetFile::ShowPage, Ok(Some(file))) => { + Ok(response(StatusCode::OK, templates::file_page(file))) + } + (GetFile::Download, Ok(Some(file))) => { + let path = files_dir.join(file_id); + Ok(stream_file(path, file).await) + } + (_, Err(msg)) => { + log::error!("Getting file: {msg}"); + Ok(internal_server_error()) + } + (_, Ok(None)) => Ok(not_found()), + } +} + +fn static_file(text: String, content_type: &str) -> Response> { + let response = Response::builder() + .body(Full::new(text.into()).map_err(|e| match e {}).boxed()) + .unwrap(); + with_headers(response, vec![(CONTENT_TYPE, content_type)]) +} + +fn response(status_code: StatusCode, text: String) -> Response> { + Response::builder() + .status(status_code) + .body(Full::new(text.into()).map_err(|e| match e {}).boxed()) + .unwrap() +} + +async fn stream_file(path: PathBuf, file: model::File) -> Response> { + match File::open(path).await { + Err(e) => { + log::error!("Unable to open file: {e}"); + not_found() + } + Ok(disk_file) => { + let reader_stream = ReaderStream::new(disk_file); + let stream_body = StreamBody::new(reader_stream.map_ok(Frame::data)); + let boxed_body = stream_body.boxed(); + + let response = Response::builder().body(boxed_body).unwrap(); + + with_headers( + response, + vec![ + ( + CONTENT_DISPOSITION, + &format!("attachment; filename={}", file.name), + ), + (CONTENT_LENGTH, &file.content_length.to_string()), + ], + ) + } + } +} + +fn not_found() -> Response> { + response(StatusCode::NOT_FOUND, templates::NOT_FOUND.to_string()) +} + +fn bad_request() -> Response> { + response(StatusCode::BAD_REQUEST, templates::BAD_REQUEST.to_string()) +} + +fn internal_server_error() -> Response> { + response( + StatusCode::INTERNAL_SERVER_ERROR, + templates::INTERNAL_SERVER_ERROR.to_string(), + ) +} + +pub fn with_headers( + response: Response>, + headers: Vec<(HeaderName, &str)>, +) -> Response> { + let mut response = response; + let response_headers = response.headers_mut(); + for (name, value) in headers { + response_headers.insert(name, HeaderValue::from_str(value).unwrap()); + } + response +} diff --git a/src/server.py b/src/server.py deleted file mode 100644 index 5927052..0000000 --- a/src/server.py +++ /dev/null @@ -1,84 +0,0 @@ -import http.server -import logging -import os -import sqlite3 -import tempfile - -import db -import templates -import utils - -logger = logging.getLogger(__name__) -conn = sqlite3.connect('db.sqlite3') -files_directory = 'files' -authorized_key = os.environ['KEY'] - -class MyServer(http.server.BaseHTTPRequestHandler): - def do_GET(self): - match self.path: - case '/': - self._serve_str(templates.index, 200, 'text/html') - case '/main.js': - self._serve_file('public/main.js', 'application/javascript') - case '/main.css': - self._serve_file('public/main.css', 'text/css') - case path: - if path.endswith('?download'): - download = True - path = path[:-len('?download')] - else: - download = False - - file_id = path[1:] - 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: - headers = [ - ('Content-Disposition', f'attachment; filename={filename}'), - ('Content-Length', content_length) - ] - self._serve_file(disk_path, 'application/octet-stream', headers) - else: - href = f'{file_id}?download' - self._serve_str(templates.download(href, filename, expires), 200, 'text/html') - - def do_POST(self): - key = self.headers['X-Key'] - if not key == authorized_key: - logging.info('Unauthorized to upload file: wrong key') - self._serve_str('Unauthorized', 401) - - else: - logging.info('Uploading file') - content_length = int(self.headers['content-length']) - filename = utils.sanitize_filename(self.headers['X-FileName']) - expiration = self.headers['X-Expiration'] - - with tempfile.NamedTemporaryFile(delete = False) as tmp: - utils.transfer(self.rfile, tmp, content_length = content_length) - - 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)) - - self._serve_str(file_id, 200) - - def _serve_str(self, s, code, content_type='text/plain'): - self.send_response(code) - self.send_header('Content-type', content_type) - self.end_headers() - self.wfile.write(bytes(s, 'utf-8')) - - def _serve_file(self, filename, content_type, headers = []): - self.send_response(200) - self.send_header('Content-type', content_type) - for header_name, header_value in headers: - self.send_header(header_name, header_value) - self.end_headers() - with open(filename, 'rb') as f: - utils.transfer(f, self.wfile) diff --git a/src/static/main.css b/src/static/main.css new file mode 100644 index 0000000..af0ee54 --- /dev/null +++ b/src/static/main.css @@ -0,0 +1,94 @@ +html { + margin: 0 1rem; + font-size: 16px; + line-height: 1.4rem; + font-family: sans-serif; + box-sizing: border-box; +} + +*, *:before, *:after { + box-sizing: inherit; +} + +body { + max-width: 30rem; + margin: 0 auto; +} + +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, select { + font-size: inherit; + border: 1px solid black; + height: 2rem; + background: white; +} + +input[type=file] { + align-content: center; + padding-left: 2px; +} + +input[type=submit] { + width: 100%; + background: #06C; + cursor: pointer; + border: none; + color: white; +} + +.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/src/static/main.js b/src/static/main.js new file mode 100644 index 0000000..40e62d6 --- /dev/null +++ b/src/static/main.js @@ -0,0 +1,49 @@ +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] + const filename = file.name.replace(/[^0-9a-zA-Z\.]/gi, '-') + + // 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', filename) + xhr.setRequestHeader('X-Expiration', expiration) + xhr.setRequestHeader('X-Key', key) + xhr.send(file) + }, 500) + } + } +} diff --git a/src/templates.py b/src/templates.py deleted file mode 100644 index 8125f69..0000000 --- a/src/templates.py +++ /dev/null @@ -1,105 +0,0 @@ -import html -import datetime - -page: str = ''' - - - - - - Files - - - - -

Files

-
-''' - -pub index: str = f''' - {page} - -
- - - - - - -
-
- Uploading… -
- -
-
- - -
-''' - -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} - -
- {html.escape(filename)} -
- Expires in {expires_in} -
-
- ''' - -not_found: str = f''' - {page} - - Oops, not found! -''' diff --git a/src/templates.rs b/src/templates.rs new file mode 100644 index 0000000..b551bf6 --- /dev/null +++ b/src/templates.rs @@ -0,0 +1,134 @@ +use chrono::Local; + +use crate::model::File; +use crate::util; + +const PAGE: &str = r#" + + + + + +Files + + + + +

Files

+
+"#; + +pub const INDEX: &str = const_format::concatcp!( + PAGE, + r#" +
+ + + + + + +
+
+ Uploading… +
+ +
+
+ + +
"# +); + +pub const NOT_FOUND: &str = const_format::concatcp!( + PAGE, + r#" +
+ Oops, not found. +
+ "# +); + +pub const BAD_REQUEST: &str = const_format::concatcp!( + PAGE, + r#" +
+ Oops, bad request. +
+ "# +); + +pub const INTERNAL_SERVER_ERROR: &str = const_format::concatcp!( + PAGE, + r#" +
+ Oops, internal server error. +
+ "# +); + +pub fn file_page(file: File) -> String { + let href = format!("{}/download", file.id); + let expiration = file.expires_at.signed_duration_since(Local::now()); + + format!( + r#" + {page} + +
+
+ {filename} – {size} +
+
+ Expires in {expiration}. +
+
+ "#, + page = PAGE, + href = html_escape::encode_text(&href), + filename = html_escape::encode_text(&file.name), + expiration = html_escape::encode_text(&util::pretty_print_duration(expiration)), + size = html_escape::encode_text(&util::pretty_print_bytes(file.content_length)) + ) +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..9bc7cb9 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,125 @@ +use chrono::Duration; + +pub fn sanitize_filename(s: &str) -> String { + s.split('.') + .map(sanitize_filename_part) + .collect::>() + .join(".") +} + +pub fn sanitize_filename_part(s: &str) -> String { + s.chars() + .map(|c| { + if c.is_ascii_alphanumeric() { + c.to_lowercase().collect::() + } else { + " ".to_string() + } + }) + .collect::() + .split_whitespace() + .collect::>() + .join("-") +} + +pub fn pretty_print_duration(d: Duration) -> String { + if d.num_days() > 0 { + let plural = if d.num_days() > 1 { "s" } else { "" }; + format!("{} day{}", d.num_days(), plural) + } else if d.num_hours() > 0 { + format!("{} h", d.num_hours()) + } else if d.num_minutes() > 0 { + format!("{} min", d.num_minutes()) + } else { + format!("{} s", d.num_seconds()) + } +} + +pub fn pretty_print_bytes(bytes: usize) -> String { + let ko = bytes / 1024; + let mo = ko / 1024; + let go = mo / 1024; + if go > 0 { + format!("{} GB", go) + } else if mo > 0 { + format!("{} MB", mo) + } else if ko > 0 { + format!("{} KB", ko) + } else { + format!("{} B", bytes) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sanitize_filename() { + assert_eq!(sanitize_filename(""), ""); + assert_eq!(sanitize_filename("foo bar 123"), "foo-bar-123"); + assert_eq!(sanitize_filename("foo bar.123"), "foo-bar.123"); + assert_eq!(sanitize_filename("foo ( test+2 ).xml"), "foo-test-2.xml"); + } + + #[test] + fn test_sanitize_filename_part() { + assert_eq!(sanitize_filename_part(""), ""); + assert_eq!(sanitize_filename_part("foo123BAZ"), "foo123baz"); + assert_eq!(sanitize_filename_part("foo-123-BAZ"), "foo-123-baz"); + assert_eq!(sanitize_filename_part("[()] */+-!;?<'> ?:"), ""); + assert_eq!(sanitize_filename_part("foo [bar] -- BAZ3"), "foo-bar-baz3"); + } + + #[test] + fn test_pretty_print_duration() { + assert_eq!( + pretty_print_duration(Duration::days(2)), + "2 days".to_string() + ); + assert_eq!( + pretty_print_duration(Duration::hours(30)), + "1 day".to_string() + ); + assert_eq!( + pretty_print_duration(Duration::days(1)), + "1 day".to_string() + ); + assert_eq!( + pretty_print_duration(Duration::hours(15)), + "15 h".to_string() + ); + assert_eq!( + pretty_print_duration(Duration::minutes(70)), + "1 h".to_string() + ); + assert_eq!( + pretty_print_duration(Duration::minutes(44)), + "44 min".to_string() + ); + assert_eq!( + pretty_print_duration(Duration::seconds(100)), + "1 min".to_string() + ); + assert_eq!( + pretty_print_duration(Duration::seconds(7)), + "7 s".to_string() + ); + assert_eq!(pretty_print_duration(Duration::zero()), "0 s".to_string()); + } + + #[test] + fn test_pretty_print_bytes() { + assert_eq!(pretty_print_bytes(0), "0 B"); + assert_eq!(pretty_print_bytes(10), "10 B"); + assert_eq!(pretty_print_bytes(1024), "1 KB"); + assert_eq!(pretty_print_bytes(1100), "1 KB"); + assert_eq!(pretty_print_bytes(54 * 1024), "54 KB"); + assert_eq!(pretty_print_bytes(1024 * 1024), "1 MB"); + assert_eq!(pretty_print_bytes(1300 * 1024), "1 MB"); + assert_eq!(pretty_print_bytes(79 * 1024 * 1024), "79 MB"); + assert_eq!(pretty_print_bytes(1024 * 1024 * 1024), "1 GB"); + assert_eq!(pretty_print_bytes(1300 * 1024 * 1024), "1 GB"); + assert_eq!(pretty_print_bytes(245 * 1024 * 1024 * 1024), "245 GB"); + } +} diff --git a/src/utils.py b/src/utils.py deleted file mode 100644 index 151217f..0000000 --- a/src/utils.py +++ /dev/null @@ -1,8 +0,0 @@ -import io - -def sanitize_filename(s: str) -> str: - return '.'.join([sanitize_filename_part(p) for p in s.split('.')]) - -def sanitize_filename_part(s: str) -> str: - alnum_or_space = ''.join([c if c.isalnum() else ' ' for c in s]) - return '-'.join(alnum_or_space.split()) diff --git a/static/main.css b/static/main.css deleted file mode 100644 index db9a678..0000000 --- a/static/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/static/main.js b/static/main.js deleted file mode 100644 index 1729d38..0000000 --- a/static/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) - } - } -} -- cgit v1.2.3