aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoris2024-06-02 14:38:13 +0200
committerJoris2024-06-02 14:38:22 +0200
commit1019ea1ed341e3a7769c046aa0be5764789360b6 (patch)
tree1a0d8a4f00cff252d661c42fc23ed4c19795da6f
parente8da9790dc6d55cd2e8883322cdf9a7bf5b4f5b7 (diff)
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.
-rw-r--r--.gitignore4
-rw-r--r--Cargo.lock1306
-rw-r--r--Cargo.toml23
-rwxr-xr-xbin/dev-server16
-rw-r--r--flake.lock63
-rw-r--r--flake.nix51
-rw-r--r--init-db.sql4
-rw-r--r--src/controller.py58
-rw-r--r--src/db.py24
-rw-r--r--src/db.rs125
-rw-r--r--src/main-old.py33
-rw-r--r--src/main.py28
-rw-r--r--src/main.rs68
-rw-r--r--src/model.rs52
-rw-r--r--src/routes.rs209
-rw-r--r--src/server.py84
-rw-r--r--src/static/main.css (renamed from static/main.css)25
-rw-r--r--src/static/main.js (renamed from static/main.js)3
-rw-r--r--src/templates.py105
-rw-r--r--src/templates.rs134
-rw-r--r--src/util.rs125
-rw-r--r--src/utils.py8
22 files changed, 2171 insertions, 377 deletions
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 <joris@guyonvarch.me>"]
+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<Option<File>> {
+ 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("/<file_id:str>")
-async def file_page(request, file_id):
- return await controller.file(file_id, download = False)
-
-@app.get("/<file_id:str>/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<dyn std::error::Error>> {
+ 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<Local>,
+ pub content_length: usize,
+}
+
+pub fn local_time() -> DateTime<Local> {
+ 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<Local>) -> String {
+ dt.naive_utc().format(FORMAT).to_string()
+}
+
+pub fn decode_datetime(str: &str) -> Option<DateTime<Local>> {
+ 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<Incoming>,
+ db_conn: Connection,
+ authorized_key: String,
+ files_dir: String,
+) -> Result<Response<BoxBody<Bytes, std::io::Error>>> {
+ let path = &request.uri().path().split('/').collect::<Vec<&str>>()[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<Incoming>,
+ db_conn: Connection,
+ authorized_key: String,
+ files_dir: &Path,
+) -> Result<Response<BoxBody<Bytes, std::io::Error>>> {
+ 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<i64> =
+ get_header(&request, "X-Expiration").and_then(|s| s.parse().ok());
+ let content_length: Option<usize> =
+ 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<Incoming>, header: &str) -> Option<String> {
+ 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<Response<BoxBody<Bytes, std::io::Error>>> {
+ 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<BoxBody<Bytes, std::io::Error>> {
+ 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<BoxBody<Bytes, std::io::Error>> {
+ 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<BoxBody<Bytes, std::io::Error>> {
+ 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<BoxBody<Bytes, std::io::Error>> {
+ response(StatusCode::NOT_FOUND, templates::NOT_FOUND.to_string())
+}
+
+fn bad_request() -> Response<BoxBody<Bytes, std::io::Error>> {
+ response(StatusCode::BAD_REQUEST, templates::BAD_REQUEST.to_string())
+}
+
+fn internal_server_error() -> Response<BoxBody<Bytes, std::io::Error>> {
+ response(
+ StatusCode::INTERNAL_SERVER_ERROR,
+ templates::INTERNAL_SERVER_ERROR.to_string(),
+ )
+}
+
+pub fn with_headers(
+ response: Response<BoxBody<Bytes, std::io::Error>>,
+ headers: Vec<(HeaderName, &str)>,
+) -> Response<BoxBody<Bytes, std::io::Error>> {
+ 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/static/main.css b/src/static/main.css
index db9a678..af0ee54 100644
--- a/static/main.css
+++ b/src/static/main.css
@@ -1,11 +1,18 @@
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;
- font-family: sans-serif;
}
a {
@@ -32,8 +39,24 @@ label {
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 {
diff --git a/static/main.js b/src/static/main.js
index 1729d38..40e62d6 100644
--- a/static/main.js
+++ b/src/static/main.js
@@ -23,6 +23,7 @@ window.onload = function() {
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() {
@@ -38,7 +39,7 @@ window.onload = function() {
xhr.onerror = function () {
showError('Upload error')
}
- xhr.setRequestHeader('X-FileName', file.name)
+ xhr.setRequestHeader('X-FileName', filename)
xhr.setRequestHeader('X-Expiration', expiration)
xhr.setRequestHeader('X-Key', key)
xhr.send(file)
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 = '''
- <!doctype html>
- <html lang="fr">
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width">
-
- <title>Files</title>
- <link rel="stylesheet" href="/static/main.css">
- <script src="/static/main.js"></script>
-
- <a href="/">
- <h1>Files</h1>
- </a>
-'''
-
-pub index: str = f'''
- {page}
-
- <form>
- <label>
- File
- <input type="file" name="file" required>
- </label>
-
- <label>
- Expiration
- <select name="expiration">
- <option value="1">1 day</option>
- <option value="2">2 days</option>
- <option value="3">3 days</option>
- <option value="4">4 days</option>
- <option value="5">5 days</option>
- <option value="6">6 days</option>
- <option value="7" selected>7 days</option>
- <option value="8">8 days</option>
- <option value="9">9 days</option>
- <option value="10">10 days</option>
- <option value="11">11 days</option>
- <option value="12">12 days</option>
- <option value="13">13 days</option>
- <option value="14">14 days</option>
- <option value="15">15 days</option>
- <option value="16">16 days</option>
- <option value="17">17 days</option>
- <option value="18">18 days</option>
- <option value="19">19 days</option>
- <option value="20">20 days</option>
- <option value="21">21 days</option>
- <option value="22">22 days</option>
- <option value="23">23 days</option>
- <option value="24">24 days</option>
- <option value="25">25 days</option>
- <option value="26">26 days</option>
- <option value="27">27 days</option>
- <option value="28">28 days</option>
- <option value="29">29 days</option>
- <option value="30">30 days</option>
- <option value="31">31 days</option>
- </select>
- </label>
-
- <label>
- Key
- <input type="password" name="key" required>
- </label>
-
- <div class="g-Loading">
- <div class="g-Spinner"></div>
- Uploading…
- </div>
-
- <div class="g-Error">
- </div>
-
- <input type="submit" value="Upload">
- </form>
-'''
-
-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}
-
- <div>
- <a class="g-Link" href="{html.escape(href)}">{html.escape(filename)}</a>
- <div>
- Expires in {expires_in}
- </div>
- </div>
- '''
-
-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#"
+<!doctype html>
+<html lang="fr">
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width">
+
+<title>Files</title>
+<link rel="stylesheet" href="/static/main.css">
+<script src="/static/main.js"></script>
+
+<a href="/">
+ <h1>Files</h1>
+</a>
+"#;
+
+pub const INDEX: &str = const_format::concatcp!(
+ PAGE,
+ r#"
+<form>
+ <label>
+ File
+ <input type="file" name="file" required>
+ </label>
+
+ <label>
+ Expiration
+ <select name="expiration">
+ <option value="1">1 day</option>
+ <option value="2">2 days</option>
+ <option value="3">3 days</option>
+ <option value="4">4 days</option>
+ <option value="5">5 days</option>
+ <option value="6">6 days</option>
+ <option value="7" selected>7 days</option>
+ <option value="8">8 days</option>
+ <option value="9">9 days</option>
+ <option value="10">10 days</option>
+ <option value="11">11 days</option>
+ <option value="12">12 days</option>
+ <option value="13">13 days</option>
+ <option value="14">14 days</option>
+ <option value="15">15 days</option>
+ <option value="16">16 days</option>
+ <option value="17">17 days</option>
+ <option value="18">18 days</option>
+ <option value="19">19 days</option>
+ <option value="20">20 days</option>
+ <option value="21">21 days</option>
+ <option value="22">22 days</option>
+ <option value="23">23 days</option>
+ <option value="24">24 days</option>
+ <option value="25">25 days</option>
+ <option value="26">26 days</option>
+ <option value="27">27 days</option>
+ <option value="28">28 days</option>
+ <option value="29">29 days</option>
+ <option value="30">30 days</option>
+ <option value="31">31 days</option>
+ </select>
+ </label>
+
+ <label>
+ Key
+ <input type="password" name="key" required>
+ </label>
+
+ <div class="g-Loading">
+ <div class="g-Spinner"></div>
+ Uploading…
+ </div>
+
+ <div class="g-Error">
+ </div>
+
+ <input type="submit" value="Upload">
+</form>"#
+);
+
+pub const NOT_FOUND: &str = const_format::concatcp!(
+ PAGE,
+ r#"
+ <div>
+ Oops, not found.
+ </div>
+ "#
+);
+
+pub const BAD_REQUEST: &str = const_format::concatcp!(
+ PAGE,
+ r#"
+ <div>
+ Oops, bad request.
+ </div>
+ "#
+);
+
+pub const INTERNAL_SERVER_ERROR: &str = const_format::concatcp!(
+ PAGE,
+ r#"
+ <div>
+ Oops, internal server error.
+ </div>
+ "#
+);
+
+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}
+
+ <div>
+ <div>
+ <a class="g-Link" href="{href}">{filename}</a> – {size}
+ </div>
+ <div>
+ Expires in <b>{expiration}</b>.
+ </div>
+ </div>
+ "#,
+ 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::<Vec<String>>()
+ .join(".")
+}
+
+pub fn sanitize_filename_part(s: &str) -> String {
+ s.chars()
+ .map(|c| {
+ if c.is_ascii_alphanumeric() {
+ c.to_lowercase().collect::<String>()
+ } else {
+ " ".to_string()
+ }
+ })
+ .collect::<String>()
+ .split_whitespace()
+ .collect::<Vec<&str>>()
+ .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())