From 06dc355cbbc5b3dbf93a0d65e134e5e933104198 Mon Sep 17 00:00:00 2001 From: FlareFlo Date: Fri, 24 Apr 2026 10:30:42 +0200 Subject: [PATCH 01/25] init wasm module --- tapfer_crypt/.gitignore | 2 + tapfer_crypt/Cargo.lock | 135 ++++++++++++++++++++++++++++++++++++++++ tapfer_crypt/Cargo.toml | 15 +++++ tapfer_crypt/src/lib.rs | 24 +++++++ 4 files changed, 176 insertions(+) create mode 100644 tapfer_crypt/.gitignore create mode 100644 tapfer_crypt/Cargo.lock create mode 100644 tapfer_crypt/Cargo.toml create mode 100644 tapfer_crypt/src/lib.rs diff --git a/tapfer_crypt/.gitignore b/tapfer_crypt/.gitignore new file mode 100644 index 0000000..5bcaadc --- /dev/null +++ b/tapfer_crypt/.gitignore @@ -0,0 +1,2 @@ +pkg +target diff --git a/tapfer_crypt/Cargo.lock b/tapfer_crypt/Cargo.lock new file mode 100644 index 0000000..80f159a --- /dev/null +++ b/tapfer_crypt/Cargo.lock @@ -0,0 +1,135 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "js-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tapfer-crypt" +version = "0.0.0" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "wasm-bindgen" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] diff --git a/tapfer_crypt/Cargo.toml b/tapfer_crypt/Cargo.toml new file mode 100644 index 0000000..8c34474 --- /dev/null +++ b/tapfer_crypt/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "tapfer-crypt" +publish = false +edition = "2024" +version = "0.0.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wasm-bindgen = "*" + +[dependencies.web-sys] +features = ['Document', 'Element', 'HtmlElement', 'Node', 'Window'] +version = "*" diff --git a/tapfer_crypt/src/lib.rs b/tapfer_crypt/src/lib.rs new file mode 100644 index 0000000..c927ac0 --- /dev/null +++ b/tapfer_crypt/src/lib.rs @@ -0,0 +1,24 @@ +use wasm_bindgen::prelude::*; + +// Called when the Wasm module is instantiated +#[wasm_bindgen(start)] +fn main() -> Result<(), JsValue> { + // Use `web_sys`'s global `window` function to get a handle on the global + // window object. + let window = web_sys::window().expect("no global `window` exists"); + let document = window.document().expect("should have a document on window"); + let body = document.body().expect("document should have a body"); + + // Manufacture the element we're gonna append + let val = document.create_element("p")?; + val.set_inner_html("Hello from Rust!"); + + body.append_child(&val)?; + + Ok(()) +} + +#[wasm_bindgen] +pub fn add(a: u32, b: u32) -> u32 { + a + b +} From fca579b3015e4bc96e15949a4f747aaabdff6f63 Mon Sep 17 00:00:00 2001 From: FlareFlo Date: Fri, 24 Apr 2026 14:57:14 +0200 Subject: [PATCH 02/25] Fix wasm and implement prototype for ENCDEC mechanism --- src/main.rs | 6 +- tapfer_crypt/Cargo.lock | 171 ++++++++++++++++++++++++++++++++++++++++ tapfer_crypt/Cargo.toml | 3 + tapfer_crypt/src/lib.rs | 94 ++++++++++++++++++---- templates/homepage.html | 8 ++ 5 files changed, 265 insertions(+), 17 deletions(-) diff --git a/src/main.rs b/src/main.rs index d67407e..a72b478 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,7 @@ use crate::retention_control::{GlobalRetentionPolicy, check_all_assets}; use crate::structs::error::TapferErrorExt; use crate::updown::upload_pool::UploadPool; use crate::websocket::{WsDestination, WsEvent}; -use axum::routing::{any, get_service}; +use axum::routing::{any}; use axum::{Router, extract::DefaultBodyLimit, middleware, routing::get}; use dashmap::DashMap; use handlers::homepage; @@ -65,7 +65,8 @@ async fn main() -> TapferResult<()> { init_datadir(); - let static_dir_service = get_service(ServeDir::new("static")); + let static_dir_service = ServeDir::new("static"); + let wasm_service = ServeDir::new("tapfer_crypt/pkg"); let cors = CorsLayer::new() .allow_methods(Any) @@ -109,6 +110,7 @@ async fn main() -> TapferResult<()> { .layer(RequestBodyLimitLayer::new(MAX_UPLOAD_SIZE)) .layer(tower_http::trace::TraceLayer::new_for_http()) .nest_service("/static", static_dir_service) + .nest_service("/wasm", wasm_service) .merge(Scalar::with_url("/docs", ::openapi())) .fallback_service(fallback_service) .layer(cors); diff --git a/tapfer_crypt/Cargo.lock b/tapfer_crypt/Cargo.lock index 80f159a..262e087 100644 --- a/tapfer_crypt/Cargo.lock +++ b/tapfer_crypt/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -14,6 +24,93 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "js-sys" version = "0.3.95" @@ -24,12 +121,35 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + [[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -48,12 +168,27 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.117" @@ -69,16 +204,46 @@ dependencies = [ name = "tapfer-crypt" version = "0.0.0" dependencies = [ + "chacha20poly1305", + "getrandom", "wasm-bindgen", "web-sys", ] +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasm-bindgen" version = "0.2.118" @@ -133,3 +298,9 @@ dependencies = [ "js-sys", "wasm-bindgen", ] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" diff --git a/tapfer_crypt/Cargo.toml b/tapfer_crypt/Cargo.toml index 8c34474..377d745 100644 --- a/tapfer_crypt/Cargo.toml +++ b/tapfer_crypt/Cargo.toml @@ -8,8 +8,11 @@ version = "0.0.0" crate-type = ["cdylib"] [dependencies] +chacha20poly1305 = {version = "0.10.1", features = ["std", "stream"]} +getrandom = { version = "0.2.7", features = ["js"]} # Override chacha20poly1305 default getrandom with JS feature wasm-bindgen = "*" + [dependencies.web-sys] features = ['Document', 'Element', 'HtmlElement', 'Node', 'Window'] version = "*" diff --git a/tapfer_crypt/src/lib.rs b/tapfer_crypt/src/lib.rs index c927ac0..dd3c6f3 100644 --- a/tapfer_crypt/src/lib.rs +++ b/tapfer_crypt/src/lib.rs @@ -1,24 +1,88 @@ +use chacha20poly1305::{ + aead::{ + stream::{DecryptorLE31, EncryptorLE31}, + OsRng, + }, + ChaCha20Poly1305, KeyInit, +}; use wasm_bindgen::prelude::*; -// Called when the Wasm module is instantiated -#[wasm_bindgen(start)] -fn main() -> Result<(), JsValue> { - // Use `web_sys`'s global `window` function to get a handle on the global - // window object. - let window = web_sys::window().expect("no global `window` exists"); - let document = window.document().expect("should have a document on window"); - let body = document.body().expect("document should have a body"); +#[wasm_bindgen] +pub struct FileEncrypter { + encryptor: Option>, + key: [u8; 32], + base_nonce: [u8; 8], +} + +#[wasm_bindgen] +impl FileEncrypter { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + let key = ChaCha20Poly1305::generate_key(&mut OsRng); + let cipher = ChaCha20Poly1305::new(&key); + + // EncryptorLE31 requires an 8-byte base nonce + let mut base_nonce = [0u8; 8]; + getrandom::getrandom(&mut base_nonce).expect("Failed to get random bytes"); + + let encryptor = EncryptorLE31::from_aead(cipher, &base_nonce.into()); - // Manufacture the element we're gonna append - let val = document.create_element("p")?; - val.set_inner_html("Hello from Rust!"); + Self { + encryptor: Some(encryptor), + key: key.into(), + base_nonce, + } + } - body.append_child(&val)?; + pub fn export_key(&self) -> Vec { + self.key.to_vec() + } - Ok(()) + pub fn export_nonce(&self) -> Vec { + self.base_nonce.to_vec() + } + + pub fn encrypt_chunk(&mut self, data: &[u8], is_last: bool) -> Result, JsValue> { + if is_last { + let encryptor = self.encryptor.take().ok_or_else(|| JsValue::from_str("Stream already finished"))?; + encryptor.encrypt_last(data).map_err(|_| JsValue::from_str("Encryption failed")) + } else { + let encryptor = self.encryptor.as_mut().ok_or_else(|| JsValue::from_str("Stream already finished"))?; + encryptor.encrypt_next(data).map_err(|_| JsValue::from_str("Encryption failed")) + } + } } #[wasm_bindgen] -pub fn add(a: u32, b: u32) -> u32 { - a + b +pub struct FileDecrypter { + decryptor: Option>, } + +#[wasm_bindgen] +impl FileDecrypter { + #[wasm_bindgen(constructor)] + pub fn new(key_bytes: &[u8], nonce_bytes: &[u8]) -> Result { + if key_bytes.len() != 32 || nonce_bytes.len() != 8 { + return Err(JsValue::from_str("Invalid length. Key must be 32 bytes and nonce 8 bytes.")); + } + + let cipher = ChaCha20Poly1305::new(key_bytes.into()); + let mut nonce = [0u8; 8]; + nonce.copy_from_slice(nonce_bytes); + let decryptor = DecryptorLE31::from_aead(cipher, &nonce.into()); + + Ok(Self { decryptor: Some(decryptor) }) + } + + pub fn decrypt_chunk(&mut self, data: &[u8], is_last: bool) -> Result, JsValue> { + if is_last { + // Consumes the decryptor so it can't be used again + let decryptor = self.decryptor.take().ok_or_else(|| JsValue::from_str("Stream already finished"))?; + decryptor.decrypt_last(data).map_err(|_| JsValue::from_str("Decryption failed. The data may be corrupted or this is the wrong key.")) + } else { + // Uses a mutable reference for continuous chunks + let decryptor = self.decryptor.as_mut().ok_or_else(|| JsValue::from_str("Stream already finished"))?; + decryptor.decrypt_next(data).map_err(|_| JsValue::from_str("Decryption failed. The data may be corrupted or this is the wrong key.")) + } + } +} \ No newline at end of file diff --git a/templates/homepage.html b/templates/homepage.html index 643b7fa..f07bbef 100644 --- a/templates/homepage.html +++ b/templates/homepage.html @@ -316,5 +316,13 @@ } }); + From 1d5d86455291055e74af8e0e32fc0277996bbc2f Mon Sep 17 00:00:00 2001 From: FlareFlo Date: Fri, 24 Apr 2026 15:57:03 +0200 Subject: [PATCH 03/25] Impl somewhat working ENCDEC --- README.md | 8 +- src/handlers/download.rs | 2 +- src/handlers/upload.rs | 69 ++++++++++--- templates/download.html | 113 ++++++++++++++++++++- templates/homepage.html | 212 ++++++++++++++++++++++----------------- 5 files changed, 289 insertions(+), 115 deletions(-) diff --git a/README.md b/README.md index 301554b..80ffb97 100644 --- a/README.md +++ b/README.md @@ -6,4 +6,10 @@ 2. edit `docker-compose.yml` HOST variable to match the domain that uploads will be available on (for QR code generation) 3. Configure your reverse proxy if applicable according to `rever_proxy`. Default configs are provided, replace {{TLD}} with your real TLD 4. (optional) Configure a ZFS storage quota (or similar) on the `data` folder or keep the data a volume without a local mountpoint -5. `docker-compose up -d --build` To build and deploy the container \ No newline at end of file +5. `docker-compose up -d --build` To build and deploy the container + +# Local dev +run caddy for local HTTP/2 support +```sh +caddy reverse-proxy --from localhost:4000 --to localhost:3000 +``` \ No newline at end of file diff --git a/src/handlers/download.rs b/src/handlers/download.rs index ac0c295..076a34a 100644 --- a/src/handlers/download.rs +++ b/src/handlers/download.rs @@ -70,7 +70,7 @@ pub async fn download_html( download_url: if !localhost { &format!("https://cdn.{host}/uploads/{id}/download") } else { - &format!("http://localhost:3000/uploads/{id}/download") + &format!("https://localhost:4000/uploads/{id}/download") }, mimetype: meta.content_type(), filesize: if meta.known_size().is_some() { diff --git a/src/handlers/upload.rs b/src/handlers/upload.rs index ac09442..d488fb6 100644 --- a/src/handlers/upload.rs +++ b/src/handlers/upload.rs @@ -9,12 +9,13 @@ use crate::updown::upload_pool::UploadFsm; use crate::websocket::WsEvent; use crate::{PROGRESS_TOKEN_LUT, UPLOAD_POOL, websocket}; use axum::extract::multipart::Field; -use axum::extract::{Multipart, Path, Query}; +use axum::extract::{FromRequest, Multipart, Path, Query, Request}; use axum::http::StatusCode; use axum::response::Html; use axum::response::IntoResponse; use axum_extra::extract::Host; use futures_util::TryStreamExt; +use futures_util::StreamExt; use scopeguard::defer; use std::io::Error; use std::pin::{Pin, pin}; @@ -34,6 +35,7 @@ pub struct UploadParameters { expiration: Option, timezone: Option, deposit: Option, + filename: Option, } #[utoipa::path( @@ -55,13 +57,13 @@ pub struct UploadParameters { pub async fn accept_form( Host(mut host): Host, Query(params): Query, - multipart: Multipart, + req: Request, ) -> TapferResult { let id = TapferId::new_random(); fs::create_dir(&format!("data/{id}")).await?; info!("Beginning upload of {id}"); - let res = do_upload(multipart, id, ¶ms).await; + let res = do_upload(req, id, ¶ms).await; if res.is_err() { delete_asset(id).await?; } @@ -80,7 +82,7 @@ pub async fn accept_form( } async fn do_upload( - mut multipart: Multipart, + req: Request, id: TapferId, params: &UploadParameters, ) -> TapferResult<()> { @@ -123,19 +125,54 @@ async fn do_upload( websocket::broadcast_event(deposit, WsEvent::DepositReady { id })?; } - while let Some(field) = multipart.next_field().await? { - let name = field - .name() - .ok_or(TapferError::MultipartFieldNameMissing)? - .to_string(); - if name.as_str() == "file" { - payload_field(field, id, meta.clone(), size).await?; - } else { - error!("Got unexpected form field {name}"); - Err(TapferError::UnknownMultipartField { - field_name: name.clone(), - })?; + let is_multipart = req + .headers() + .get(axum::http::header::CONTENT_TYPE) + .and_then(|v| v.to_str().ok()) + .map(|s| s.starts_with("multipart/form-data")) + .unwrap_or(false); + + if is_multipart { + let mut multipart = Multipart::from_request(req, &()) + .await + .map_err(|_| TapferError::MultipartFieldNameMissing)?; + + while let Some(field) = multipart.next_field().await? { + let name = field.name().ok_or(TapferError::MultipartFieldNameMissing)?.to_string(); + if name.as_str() == "file" { + payload_field(field, id, meta.clone(), size).await?; + } else { + error!("Got unexpected form field {name}"); + Err(TapferError::UnknownMultipartField { field_name: name.clone() })?; + } } + } else { + let file_name = params.filename.clone().unwrap_or_else(|| id.to_string()); + let content_type = req + .headers() + .get(axum::http::header::CONTENT_TYPE) + .and_then(|v| v.to_str().ok()) + .unwrap_or(mime::APPLICATION_OCTET_STREAM.as_ref()) + .to_string(); + + let metadata = meta.build(file_name.clone(), content_type.clone(), size); + let handle = UPLOAD_POOL.handle(id, metadata.clone()); + let f = File::create(format!("data/{id}/{file_name}")).await?; + let mut f = UpdownWriter::new(f, handle.clone(), metadata, size.is_none()); + + let mut s = BufReader::with_capacity( + UPLOAD_BUFSIZE, + StreamReader::new(req.into_body().into_data_stream().map_err(|e| { + std::io::Error::new(std::io::ErrorKind::Other, e.to_string()) + })), + ); + copy_buf(&mut s, &mut f).await?; + + let metadata = f.metadata(); + fs::write(format!("data/{id}/meta.toml"), toml::to_string_pretty(&metadata)?.as_bytes()).await?; + + handle.write_fsm().await.mark_complete(); + websocket::broadcast_event(id, WsEvent::UploadComplete)?; } Ok(()) } diff --git a/templates/download.html b/templates/download.html index eb102c1..c3fa642 100644 --- a/templates/download.html +++ b/templates/download.html @@ -35,7 +35,7 @@

Sha512: {{sha512}}

- Download +
@@ -49,13 +49,118 @@
+ + - From 369f20cc6cbed94d5eb8c5c2eb899ea62b78a329 Mon Sep 17 00:00:00 2001 From: FlareFlo Date: Fri, 24 Apr 2026 16:00:06 +0200 Subject: [PATCH 04/25] Add caddy proxyconfig --- src/handlers/deposit.rs | 4 +--- src/handlers/download.rs | 11 ++--------- src/handlers/mod.rs | 6 +----- src/handlers/upload.rs | 1 - src/websocket.rs | 6 ------ 5 files changed, 4 insertions(+), 24 deletions(-) diff --git a/src/handlers/deposit.rs b/src/handlers/deposit.rs index c8844be..bbc3cc8 100644 --- a/src/handlers/deposit.rs +++ b/src/handlers/deposit.rs @@ -1,6 +1,5 @@ use crate::configuration::{EMBED_DESCRIPTION, FAVICON, QR_CODE_ECC, QR_CODE_SIZE}; use crate::structs::error::TapferResult; -use crate::websocket::wss_method; use askama::Template; use axum::extract::{Query, WebSocketUpgrade}; use axum::response::{Html, IntoResponse, Response}; @@ -34,8 +33,7 @@ pub async fn show_form(Host(host): Host) -> TapferResult { qr_size: QR_CODE_SIZE, qr_b64: BASE64_STANDARD.encode(&qr_code), ws_url: format!( - "{}://{host}/deposit/ws?deposit={deposit_id}", - wss_method(&host) + "wss://{host}/deposit/ws?deposit={deposit_id}", ), }; diff --git a/src/handlers/download.rs b/src/handlers/download.rs index 076a34a..d3f8e02 100644 --- a/src/handlers/download.rs +++ b/src/handlers/download.rs @@ -1,7 +1,6 @@ use crate::configuration::{DOWNLOAD_CHUNKSIZE, EMBED_DESCRIPTION, QR_CODE_SIZE}; use crate::handlers; use crate::handlers::checksum::get_sha512_for_asset; -use crate::handlers::is_localhost; use crate::handlers::qrcode::base64_qr_from_id; use crate::retention_control::delete_asset; use crate::structs::error::{TapferError, TapferResult}; @@ -9,7 +8,6 @@ use crate::structs::file_meta::{FileMeta, RemovalPolicy}; use crate::structs::tapfer_id::TapferId; use crate::updown::upload_handle::UploadHandle; use crate::updown::upload_pool::UploadFsm; -use crate::websocket::wss_method; use askama::Template; use axum::body::Body; use axum::extract::Path; @@ -62,16 +60,11 @@ pub async fn download_html( RemovalPolicy::Expiry { .. } => meta.expires_on_utc().unwrap().format(&DES)?.clone(), }; - let localhost = is_localhost(&host); let sha512 = get_sha512_for_asset(id)?; let template = DownloadTemplate { filename: meta.name(), expiry: &expiry, - download_url: if !localhost { - &format!("https://cdn.{host}/uploads/{id}/download") - } else { - &format!("https://localhost:4000/uploads/{id}/download") - }, + download_url: &format!("https://cdn.{host}/uploads/{id}/download"), mimetype: meta.content_type(), filesize: if meta.known_size().is_some() { &human_bytes(meta.size() as f64) @@ -88,7 +81,7 @@ pub async fn download_html( unix_expiry: meta .expires_on_utc() .map_or(0, time::UtcDateTime::unix_timestamp), - ws_url: &format!("{}://{host}/uploads/{id}/ws", wss_method(&host)), + ws_url: &format!("wss://{host}/uploads/{id}/ws"), sha512: sha512.as_deref().unwrap_or("computing..."), sha512url: format!("/uploads/{id}/checksum.sha512"), }; diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index dbea3c4..32ded0c 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -57,8 +57,4 @@ async fn get_any_meta(path: &String) -> TapferResult<((TapferId, FileMeta), UpDo } }; Ok(res) -} - -pub fn is_localhost(host: &str) -> bool { - host.starts_with("localhost") || host.starts_with("127.0.0.1") -} +} \ No newline at end of file diff --git a/src/handlers/upload.rs b/src/handlers/upload.rs index d488fb6..cb89b7d 100644 --- a/src/handlers/upload.rs +++ b/src/handlers/upload.rs @@ -15,7 +15,6 @@ use axum::response::Html; use axum::response::IntoResponse; use axum_extra::extract::Host; use futures_util::TryStreamExt; -use futures_util::StreamExt; use scopeguard::defer; use std::io::Error; use std::pin::{Pin, pin}; diff --git a/src/websocket.rs b/src/websocket.rs index 10315c1..25826fc 100644 --- a/src/websocket.rs +++ b/src/websocket.rs @@ -1,4 +1,3 @@ -use crate::handlers::is_localhost; use crate::structs::error::TapferResult; use crate::structs::tapfer_id::TapferId; use axum::extract::ws::{Message, WebSocket}; @@ -58,11 +57,6 @@ pub fn broadcast_event(dst: impl Into + Copy, event: WsEvent) -> rx.send(event).unwrap(); Ok(()) } - -pub fn wss_method(host: &str) -> &str { - if is_localhost(host) { "ws" } else { "wss" } -} - // Impl #[axum::debug_handler] From 84856f677ea1b0c97c648d376106e7c775d694c2 Mon Sep 17 00:00:00 2001 From: FlareFlo Date: Fri, 24 Apr 2026 16:21:28 +0200 Subject: [PATCH 05/25] Fix firefox compat --- Cargo.lock | 12 +++++ Cargo.toml | 1 + src/handlers/download.rs | 7 ++- src/handlers/upload.rs | 95 ++++++++++++++++++++++++++++++++++++++++ src/main.rs | 7 ++- templates/download.html | 1 + templates/homepage.html | 92 +++++++++++++------------------------- 7 files changed, 149 insertions(+), 66 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f13947b..c252ba0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1336,6 +1336,7 @@ dependencies = [ "time", "time-tz", "tokio", + "tokio-stream", "tokio-util", "toml", "tower", @@ -1472,6 +1473,17 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-tungstenite" version = "0.29.0" diff --git a/Cargo.toml b/Cargo.toml index f989b16..4989b49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ tokio = { version = "1.0", features = ["full"] } tokio-util = { version = "0.7.14", features = ["io"] } futures-core = "0.3.31" futures-util = "0.3.31" +tokio-stream = "0.1.18" # Parsing and formats diff --git a/src/handlers/download.rs b/src/handlers/download.rs index d3f8e02..0b802d2 100644 --- a/src/handlers/download.rs +++ b/src/handlers/download.rs @@ -60,11 +60,16 @@ pub async fn download_html( RemovalPolicy::Expiry { .. } => meta.expires_on_utc().unwrap().format(&DES)?.clone(), }; + let prefix = if host.contains("localhost") { + "" + } else { + "cdn." + }; let sha512 = get_sha512_for_asset(id)?; let template = DownloadTemplate { filename: meta.name(), expiry: &expiry, - download_url: &format!("https://cdn.{host}/uploads/{id}/download"), + download_url: &format!("https://{prefix}{host}/uploads/{id}/download"), mimetype: meta.content_type(), filesize: if meta.known_size().is_some() { &human_bytes(meta.size() as f64) diff --git a/src/handlers/upload.rs b/src/handlers/upload.rs index cb89b7d..3b04972 100644 --- a/src/handlers/upload.rs +++ b/src/handlers/upload.rs @@ -14,8 +14,11 @@ use axum::http::StatusCode; use axum::response::Html; use axum::response::IntoResponse; use axum_extra::extract::Host; +use dashmap::DashMap; +use futures_util::StreamExt; use futures_util::TryStreamExt; use scopeguard::defer; +use std::sync::LazyLock; use std::io::Error; use std::pin::{Pin, pin}; use std::str::FromStr; @@ -24,9 +27,13 @@ use time::Duration as TimeDuration; use tokio::fs::File; use tokio::io::{AsyncWrite, BufReader, copy_buf}; use tokio::{fs, task}; +use tokio::sync::mpsc; +use tokio_stream::wrappers::ReceiverStream; use tokio_util::io::StreamReader; use tracing::{error, info, warn}; +static CHUNKED_UPLOADS: LazyLock>>> = LazyLock::new(DashMap::new); + #[derive(Debug, Clone, serde::Deserialize)] pub struct UploadParameters { file_size: Option, @@ -230,6 +237,94 @@ fn expiration_field(field: Option<&str>, meta: &mut FileMetaBuilder) -> TapferRe Ok(()) } +#[axum::debug_handler] +pub async fn init_chunked_upload( + Query(params): Query, +) -> TapferResult { + let id = TapferId::new_random(); + fs::create_dir(&format!("data/{id}")).await?; + + let mut meta = FileMetaBuilder::default(); + let size = params.file_size; + + if let Some(tz) = params.timezone.as_ref() { + meta.timezone = Some(tz.to_owned()); + } else { + error!("Missing tapfer-timezone parameter"); + } + expiration_field(params.expiration.as_deref(), &mut meta)?; + + if let Some(deposit) = params.deposit { + websocket::broadcast_event(deposit, WsEvent::DepositReady { id })?; + } + + let file_name = params.filename.clone().unwrap_or_else(|| id.to_string()); + let content_type = mime::APPLICATION_OCTET_STREAM.as_ref().to_string(); + + let metadata = meta.build(file_name.clone(), content_type.clone(), size); + let handle = UPLOAD_POOL.handle(id, metadata.clone()); + let f = File::create(format!("data/{id}/{file_name}")).await?; + let mut f = UpdownWriter::new(f, handle.clone(), metadata, size.is_none()); + + // Create a channel that will feed bytes to the UpdownWriter task + let (tx, rx) = mpsc::channel(16); + CHUNKED_UPLOADS.insert(id, tx); + + tokio::spawn(async move { + let mut s = BufReader::with_capacity( + UPLOAD_BUFSIZE, + StreamReader::new(ReceiverStream::new(rx)), + ); + if let Err(e) = copy_buf(&mut s, &mut f).await { + error!("Chunked upload error for {id}: {e}"); + return; + } + let metadata = f.metadata(); + if let Err(e) = fs::write(format!("data/{id}/meta.toml"), toml::to_string_pretty(&metadata).unwrap().as_bytes()).await { + error!("Failed to write meta for {id}: {e}"); + } + handle.write_fsm().await.mark_complete(); + let _ = websocket::broadcast_event(id, WsEvent::UploadComplete); + checksum::spawn_sha512_checksum(id); + }); + + Ok((StatusCode::OK, id.to_string())) +} + +#[axum::debug_handler] +pub async fn upload_chunk( + Path(id_str): Path, + req: Request, +) -> TapferResult { + let id = TapferId::from_str(&id_str)?; + // Using TokenDoesNotExist error for convenience as it yields a 404 + let tx = CHUNKED_UPLOADS.get(&id).ok_or(TapferError::TokenDoesNotExist(0))?.clone(); + + let mut stream = req.into_body().into_data_stream(); + while let Some(chunk) = stream.next().await { + let chunk = chunk.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?; + if tx.send(Ok(chunk)).await.is_err() { + return Err(TapferError::Custom { status_code: StatusCode::BAD_REQUEST, body: Html("Upload aborted".to_string())}); + } + } + Ok(StatusCode::OK) +} + +#[axum::debug_handler] +pub async fn finalize_chunked_upload( + Path(id_str): Path, + Host(mut host): Host, +) -> TapferResult { + let id = TapferId::from_str(&id_str)?; + + if CHUNKED_UPLOADS.remove(&id).is_none() { + return Err(TapferError::Custom { status_code: StatusCode::NOT_FOUND, body: Html("Upload session not found".to_string()) }); + } + + let method = if host.contains("localhost") { host = String::new(); "" } else { host = host.replace("cdn.", ""); "https://" }; + Ok((StatusCode::OK, format!("{method}{host}/uploads/{id}\n"))) +} + #[utoipa::path( get, path = "/uploads/query_id/{token}", diff --git a/src/main.rs b/src/main.rs index a72b478..d588682 100644 --- a/src/main.rs +++ b/src/main.rs @@ -92,10 +92,9 @@ async fn main() -> TapferResult<()> { .route("/", get(homepage::show_form).post(upload::accept_form)) .route("/deposit", get(deposit::show_form)) .route("/deposit/ws", any(deposit::start_ws)) - .route( - "/uploads/query_id/{token}", - get(handlers::upload::progress_token_to_id), - ) + .route("/upload/init", axum::routing::post(handlers::upload::init_chunked_upload)) + .route("/uploads/{id}/chunk", axum::routing::patch(handlers::upload::upload_chunk)) + .route("/uploads/{id}/finalize", axum::routing::post(handlers::upload::finalize_chunked_upload)) .route( "/uploads/{id}/download", get(handlers::download::download_file), diff --git a/templates/download.html b/templates/download.html index c3fa642..77b1654 100644 --- a/templates/download.html +++ b/templates/download.html @@ -219,6 +219,7 @@ switch (payload.event.key) { case "DeleteAsset": if (!window.is_downloading) { + // TODO: Delay until ready, then perform action instead of not doing it at all window.location.reload(); } break; diff --git a/templates/homepage.html b/templates/homepage.html index 7bf57e3..617ba66 100644 --- a/templates/homepage.html +++ b/templates/homepage.html @@ -198,11 +198,6 @@ // We handle the entire request ourselves e.preventDefault(); - // Generate any random 32 bit integer - const array = new Uint32Array(1); - window.crypto.getRandomValues(array); - const random_seed = array[0]; - const fileInput = form.querySelector('input[name="file"]'); const file = fileInput.files[0]; if (!file) return; @@ -231,7 +226,6 @@ const params = new URLSearchParams({ "file_size": totalEncryptedSize, - "progress_token": random_seed.toString(), "expiration": formDataParams.get("expiration"), "timezone": Intl.DateTimeFormat().resolvedOptions().timeZone, "filename": file.name @@ -250,16 +244,26 @@ const speedSamples = []; const maxSamples = 300; - let currentChunk = 0; let uploadedBytes = 0; - let fired = false; - let sent = Date.now(); - const stream = new ReadableStream({ - async pull(controller) { + const show_elems = document.getElementsByClassName("show_on_upload"); + for (const elem of show_elems) { + elem.style.visibility = "unset"; + } + try { - if (currentChunk < totalChunks) { - // Read, encrypt, and push ONLY a 1MB chunk to the network buffer + const initRes = await fetch(`${cdnUrl}/upload/init?${params.toString()}`, { method: 'POST' }); + if (!initRes.ok) throw new Error("Initialization failed"); + const uploadId = await initRes.text(); + + const fileUrl = `/uploads/${uploadId}#${secretString}`; + download_url.href = fileUrl; + download_url.innerText = "Encrypted Link Ready"; + const qrcode = document.getElementById('qrcode'); + qrcode.src = `/qrcg/${encodeURIComponent(fileUrl)}`; + qrcode.style.filter = 'none'; + + for (let currentChunk = 0; currentChunk < totalChunks; currentChunk++) { const start = currentChunk * CHUNK_SIZE; const end = Math.min(start + CHUNK_SIZE, file.size); const chunkBlob = file.slice(start, end); @@ -268,10 +272,17 @@ const isLast = (currentChunk === totalChunks - 1); const encrypted = encrypter.encrypt_chunk(chunk, isLast); - controller.enqueue(encrypted); + + const chunkRes = await fetch(`${cdnUrl}/uploads/${uploadId}/chunk`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/octet-stream' }, + body: encrypted + }); + + if (!chunkRes.ok) throw new Error("Chunk upload failed"); + uploadedBytes += encrypted.length; - // Calculate Progress const percent = (uploadedBytes / totalEncryptedSize) * 100; background.style.backgroundSize = percent + '% 100%'; percentageElement.innerText = `${percent.toFixed(1)}%`; @@ -288,61 +299,20 @@ progresstext.innerText = `${(uploadedBytes / 1024 / 1024).toFixed(1)}MB / ${(totalEncryptedSize / 1024 / 1024).toFixed(1)}MB ${(averageSpeed / 1024 / 1024).toFixed(1)}MB/s`; last_progress = now; last_loaded = uploadedBytes; - - // Fire Token Request - if (!fired && (Date.now() - sent) > 1000) { - fired = true; - const token_xhr = new XMLHttpRequest(); - token_xhr.open('GET', `/uploads/query_id/${random_seed}`, true); - token_xhr.onload = async function () { - if (token_xhr.status >= 200 && token_xhr.status < 300) { - const url = `/uploads/${token_xhr.responseText}#${secretString}`; - download_url.href = url; - download_url.innerText = "Encrypted Link Ready"; - const qrcode = document.getElementById('qrcode'); - // Encode URL so the # isn't truncated by the browser's router - qrcode.src = `/qrcg/${encodeURIComponent(url)}`; - qrcode.style.filter = 'none'; - } - }; - token_xhr.send(); } - currentChunk++; - } else { - controller.close(); - encrypter.free(); - } - } catch (err) { - controller.error(err); encrypter.free(); - show_toast("Encryption failed: " + err.toString(), "error"); - } - } - }); - const show_elems = document.getElementsByClassName("show_on_upload"); - for (const elem of show_elems) { - elem.style.visibility = "unset"; - } + const finRes = await fetch(`${cdnUrl}/uploads/${uploadId}/finalize`, { method: 'POST' }); - try { - const response = await fetch(`${cdnUrl}/?${params.toString()}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/octet-stream' - }, - body: stream, - duplex: 'half' - }); - - if (response.ok) { - const responseText = await response.text(); + if (finRes.ok) { + const responseText = await finRes.text(); console.log(`redirecting to ${responseText}`); window.location.href = responseText + "#" + secretString; } else { - throw new Error(`Server returned ${response.status}`); + throw new Error(`Server returned ${finRes.status}`); } } catch (err) { + try { if (typeof encrypter !== 'undefined') encrypter.free(); } catch(e) {} show_toast('Upload failed: ' + err.message, "error"); background.style.backgroundSize = '0% 100%'; } From 239174e3236339d9e9ed7d63ddda786384e607b3 Mon Sep 17 00:00:00 2001 From: FlareFlo Date: Fri, 24 Apr 2026 21:09:09 +0200 Subject: [PATCH 06/25] Increase encryption chunk size to 10MB --- templates/download.html | 2 +- templates/homepage.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/download.html b/templates/download.html index 77b1654..b950234 100644 --- a/templates/download.html +++ b/templates/download.html @@ -96,7 +96,7 @@ const decryptionTransformer = new TransformStream({ start() { // Each original 1MB chunk has a 16-byte tag appended by the encryptor. - this.CHUNK_SIZE = 1024 * 1024; + this.CHUNK_SIZE = 1024 * 1024 * 10; this.ENCRYPTED_CHUNK_SIZE = this.CHUNK_SIZE + 16; this.buffer = new Uint8Array(0); }, diff --git a/templates/homepage.html b/templates/homepage.html index 617ba66..2622949 100644 --- a/templates/homepage.html +++ b/templates/homepage.html @@ -211,7 +211,7 @@ combinedSecret.set(nonce, 32); const secretString = encodeBase64Url(combinedSecret); - const CHUNK_SIZE = 1024 * 1024; + const CHUNK_SIZE = 1024 * 1024 * 10; const totalChunks = Math.max(1, Math.ceil(file.size / CHUNK_SIZE)); const totalEncryptedSize = file.size + (totalChunks * 16); From 2bf9e5c49809c042d1418857d5264a0fe69d3ac1 Mon Sep 17 00:00:00 2001 From: FlareFlo Date: Sat, 25 Apr 2026 01:45:00 +0200 Subject: [PATCH 07/25] Add progress to decryption on download --- src/handlers/download.rs | 2 ++ templates/download.html | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/handlers/download.rs b/src/handlers/download.rs index 0b802d2..7a9ce01 100644 --- a/src/handlers/download.rs +++ b/src/handlers/download.rs @@ -36,6 +36,7 @@ struct DownloadTemplate<'a> { download_url: &'a str, mimetype: &'a str, filesize: &'a str, + file_size_bits: u64, embed_image_url: &'a str, qr_size: usize, embed_description: &'a str, @@ -78,6 +79,7 @@ pub async fn download_html( } else { &human_bytes(meta.size() as f64) }, + file_size_bits: meta.known_size().unwrap_or(0), embed_image_url: &format!("/qrcg/{id}"), qr_size: QR_CODE_SIZE, embed_description: EMBED_DESCRIPTION, diff --git a/templates/download.html b/templates/download.html index b950234..37cecd2 100644 --- a/templates/download.html +++ b/templates/download.html @@ -69,13 +69,15 @@ // prevent reload before download is complete window.is_downloading = true; - downloadButton.innerText = "Decrypting..."; + downloadButton.innerText = "Decrypting"; downloadButton.disabled = true; try { await init(); let base64 = hash.replace(/-/g, '+').replace(/_/g, '/'); + const file_size = {{file_size_bits}}; + let progress = 0; while (base64.length % 4) { base64 += '='; } const binaryString = atob(base64); const combined = new Uint8Array(binaryString.length); @@ -113,6 +115,8 @@ this.buffer = this.buffer.slice(this.ENCRYPTED_CHUNK_SIZE); const decrypted = decrypter.decrypt_chunk(chunkToDecrypt, false); controller.enqueue(decrypted); + downloadButton.innerText = `Decrypting ${progress / file_size * 100}%`; + progress += this.ENCRYPTED_CHUNK_SIZE; } }, flush(controller) { From 3f42271eb9f4efc500c25413ed1daf9e72b9c097 Mon Sep 17 00:00:00 2001 From: FlareFlo Date: Sat, 25 Apr 2026 02:04:13 +0200 Subject: [PATCH 08/25] Stream downloads too --- templates/download.html | 74 +++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 32 deletions(-) diff --git a/templates/download.html b/templates/download.html index 37cecd2..42ecf92 100644 --- a/templates/download.html +++ b/templates/download.html @@ -51,6 +51,7 @@
\ No newline at end of file diff --git a/templates/deposit.html b/templates/deposit.html index 7fc85d7..504cd36 100644 --- a/templates/deposit.html +++ b/templates/deposit.html @@ -30,30 +30,31 @@

Upload to this device from the scanning device

- +
diff --git a/templates/download.html b/templates/download.html index 359194c..00b1401 100644 --- a/templates/download.html +++ b/templates/download.html @@ -6,20 +6,22 @@ {% include "components/favicon.html" %} {% include "components/meta.html" %} @@ -32,7 +34,8 @@

MIME Type: {{mimetype}}

Expires: {{expiry}}

Size: {{filesize}}

-

Sha512: {{sha512}}

+

Sha512: {{sha512}}

@@ -43,14 +46,15 @@
- +
diff --git a/templates/homepage.html b/templates/homepage.html index aa2843e..91a4289 100644 --- a/templates/homepage.html +++ b/templates/homepage.html @@ -38,27 +38,27 @@ white-space: nowrap; } - #qrcode { - filter: blur(5px); - } - - #filelink { - border-bottom-style: dot-dot-dash; - transition: color 0.5s ease; - } - - #filelink:hover { - color: crimson; - } - - .show_on_upload { - visibility: hidden; - } - - /* Prevents purple link when clicked before */ - a[href="/deposit"]:visited { - color: #0000EE; - } + #qrcode { + filter: blur(5px); + } + + #filelink { + border-bottom-style: dot-dot-dash; + transition: color 0.5s ease; + } + + #filelink:hover { + color: crimson; + } + + .show_on_upload { + visibility: hidden; + } + + /* Prevents purple link when clicked before */ + a[href="/deposit"]:visited { + color: #0000EE; + } @@ -69,10 +69,11 @@
- Drop file anywhere, select a file or upload to this device + Drop file anywhere, select + a file or upload to this device
- +

Delete after:

@@ -96,32 +97,37 @@
Or use CURL
- curl -X POST https://example.com?deposit={deposit_id} -F file=@$FILEPATH + curl -X POST + https://example.com?deposit={deposit_id} -F + file=@$FILEPATH
0MB/0MB 0MB/s
Abort upload
- - Download URL + + Download URL
- +
{% include "components/footer.html" %} @@ -241,8 +245,9 @@ const payload = JSON.parse(e.data); switch (payload.event.key) { case "DeleteAsset": - if (!window.is_downloading) { - // TODO: Delay until ready, then perform action instead of not doing it at all + if (window.is_downloading) { + window.needs_reload_after_download = true; + } else { window.location.reload(); } break; From 87a77308cfcebb79ce5650d1a93fd1cc60d97b0f Mon Sep 17 00:00:00 2001 From: FlareFlo Date: Sun, 3 May 2026 17:55:14 +0200 Subject: [PATCH 17/25] Allow encryption option and use selects --- templates/homepage.html | 74 ++++++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 31 deletions(-) diff --git a/templates/homepage.html b/templates/homepage.html index 91a4289..1265a9b 100644 --- a/templates/homepage.html +++ b/templates/homepage.html @@ -73,20 +73,22 @@ a file or upload to this device
- -
-

Delete after:

- - - - +
+
+ + +
+
+ + +
diff --git a/templates/homepage.html b/templates/homepage.html index 1265a9b..8a7474a 100644 --- a/templates/homepage.html +++ b/templates/homepage.html @@ -42,13 +42,18 @@ filter: blur(5px); } - #filelink { - border-bottom-style: dot-dot-dash; + .filelink { transition: color 0.5s ease; + border-radius: 5px; + padding: 0.5rem 1rem; + border: 2px dotted #2975D2; + color: #2975D2 !important; + text-decoration: none; } - #filelink:hover { + .filelink:hover { color: crimson; + text-decoration: underline; } .show_on_upload { @@ -69,21 +74,20 @@
-
-
- +
+
+
-
- +
+