From 20da86132be414975909f1b301384282e448213f Mon Sep 17 00:00:00 2001 From: xenofem Date: Tue, 26 Apr 2022 23:54:29 -0400 Subject: [PATCH] WIP file drop server, no downloads yet --- .gitignore | 1 + Cargo.lock | 1643 ++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 30 + README.md | 16 + src/download.rs | 769 ++++++++++++++++++++ src/file.rs | 45 ++ src/main.rs | 82 +++ src/upload.rs | 228 ++++++ src/zip.rs | 329 +++++++++ static/index.html | 22 + static/transbeam.css | 24 + static/upload.js | 105 +++ 12 files changed, 3294 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/download.rs create mode 100644 src/file.rs create mode 100644 src/main.rs create mode 100644 src/upload.rs create mode 100644 src/zip.rs create mode 100644 static/index.html create mode 100644 static/transbeam.css create mode 100644 static/upload.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..8e0f200 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1643 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "actix" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f728064aca1c318585bf4bb04ffcfac9e75e508ab4e8b1bd9ba5dfe04e2cbed5" +dependencies = [ + "actix-rt", + "actix_derive", + "bitflags", + "bytes", + "crossbeam-channel", + "futures-core", + "futures-sink", + "futures-task", + "futures-util", + "log", + "once_cell", + "parking_lot", + "pin-project-lite", + "smallvec", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-codec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a7559404a7f3573127aab53c08ce37a6c6a315c374a31070f3c91cd1b4a7fe" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-sink", + "log", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-files" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d81bde9a79336aa51ebed236e91fc1a0528ff67cfdf4f68ca4c61ede9fd26fb5" +dependencies = [ + "actix-http", + "actix-service", + "actix-utils", + "actix-web", + "askama_escape", + "bitflags", + "bytes", + "derive_more", + "futures-core", + "http-range", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", +] + +[[package]] +name = "actix-http" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5885cb81a0d4d0d322864bea1bb6c2a8144626b4fdc625d4c51eba197e7797a" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "ahash", + "base64", + "bitflags", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "futures-core", + "h2", + "http", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "rand", + "sha-1", + "smallvec", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "actix-router" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb60846b52c118f2f04a56cc90880a274271c489b2498623d58176f8ca21fa80" +dependencies = [ + "bytestring", + "firestorm", + "http", + "log", + "regex", + "serde", +] + +[[package]] +name = "actix-rt" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ea16c295198e958ef31930a6ef37d0fb64e9ca3b6116e6b93a8bdae96ee1000" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0da34f8e659ea1b077bb4637948b815cd3768ad5a188fdcd74ff4d84240cd824" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "num_cpus", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +dependencies = [ + "futures-core", + "paste", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e491cbaac2e7fc788dfff99ff48ef317e23b3cf63dbaf7aaab6418f40f92aa94" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e5ebffd51d50df56a3ae0de0e59487340ca456f05dd0b90c0a7a6dd6a74d31" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "ahash", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "futures-core", + "futures-util", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2", + "time", + "url", +] + +[[package]] +name = "actix-web-actors" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31efe7896f3933ce03dd4710be560254272334bb321a18fd8ff62b1a557d9d19" +dependencies = [ + "actix", + "actix-codec", + "actix-http", + "actix-web", + "bytes", + "bytestring", + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "actix-web-codegen" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7525bedf54704abb1d469e88d7e7e9226df73778798a69cea5022d53b2ae91bc" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "actix_derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d44b8fee1ced9671ba043476deddef739dd0959bf77030b26b738cc591737a7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "opaque-debug", +] + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ef4730490ad1c4eae5c4325b2a95f521d023e5c885853ff7aca0a6a1631db3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "697ed7edc0f1711de49ce108c541623a0af97c6c60b2f6e2b65229847ac843c2" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "base64ct" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a32fd6af2b5827bce66c29053ba0e7c42b9dcab01835835058558c10851a46b" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "bytestring" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90706ba19e97b90786e19dc0d5e2abd80008d99d4c0c5d1ad0b5e72cec7c494d" +dependencies = [ + "bytes", +] + +[[package]] +name = "bzip2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6afcd980b5f3a45017c57e57a2fcccbb351cc43a356ce117ef760ef8052b89b0" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cpufeatures" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" +dependencies = [ + "cfg-if", + "lazy_static", +] + +[[package]] +name = "crypto-common" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "encoding_rs" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "firestorm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d6188b8804df28032815ea256b6955c9625c24da7525f387a7af02fbb8f01" + +[[package]] +name = "flate2" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39522e96686d38f4bc984b9198e3a0613264abaebaff2c5c918bfa6b6da09af" +dependencies = [ + "cfg-if", + "crc32fast", + "libc", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" + +[[package]] +name = "futures-executor" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" + +[[package]] +name = "futures-macro" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" + +[[package]] +name = "futures-task" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" + +[[package]] +name = "futures-util" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.10.2+wasi-snapshot-preview1", +] + +[[package]] +name = "h2" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + +[[package]] +name = "httparse" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6330e8a36bd8c859f3fa6d9382911fbb7147ec39807f63b923933a247240b9ba" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + +[[package]] +name = "jobserver" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +dependencies = [ + "libc", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.124" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a41fed9d98f27ab1c6d161da622a4fa35e8a54a8adc24bbf3ddd0ef70b0e50" + +[[package]] +name = "local-channel" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6246c68cf195087205a0512559c97e15eaf95198bf0e206d662092cdcb03fe9f" +dependencies = [ + "futures-core", + "futures-sink", + "futures-util", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "902eb695eb0591864543cbfbf6d742510642a605a61fc5e97fe6ceb5a30ac4fb" + +[[package]] +name = "lock_api" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "wasi 0.11.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", +] + +[[package]] +name = "ntapi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" +dependencies = [ + "winapi", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aba1801fb138d8e85e11d0fc70baf4fe1cdfffda7c6cd34a854905df588e5ed0" +dependencies = [ + "libc", +] + +[[package]] +name = "once_cell" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "parking_lot" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "995f667a6c822200b0433ac218e05582f0e2efa1b922a3fd2fbaadc5f87bab37" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "password-hash" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d791538a6dcc1e7cb7fe6f6b58aca40e7f79403c45b2bc274008b5e647af1d8" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" + +[[package]] +name = "pbkdf2" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271779f35b581956db91a3e55737327a03aa051e90b1c47aeb189508533adfd7" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pin-project-lite" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" + +[[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.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "proc-macro2" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" + +[[package]] +name = "sanitise-file-name" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d36299972b96b8ae7e8f04ecbf75fb41a27bf3781af00abcf57609774cb911" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "semver" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65bd28f48be7196d222d95b9243287f48d27aca604e08497513019ff0502cc4" + +[[package]] +name = "serde" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha-1" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77f4e7f65455545c2153c1253d25056825e77ee2533f0e41deb65a93a34852f" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" + +[[package]] +name = "smallvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" + +[[package]] +name = "socket2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" +dependencies = [ + "itoa", + "libc", + "num_threads", + "time-macros", +] + +[[package]] +name = "time-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" + +[[package]] +name = "tinyvec" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" +dependencies = [ + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "once_cell", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0edfdeb067411dba2044da6d1cb2df793dd35add7888d73c16e3381ded401764" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tracing" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e65ce065b4b5c53e73bb28912318cb8c9e9ad3921f1d669eb0e68b4c8143a2b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "transbeam" +version = "0.1.0" +dependencies = [ + "actix", + "actix-files", + "actix-http", + "actix-web", + "actix-web-actors", + "bytes", + "crc32fast", + "env_logger", + "futures", + "log", + "rand", + "sanitise-file-name", + "serde", + "serde_json", + "thiserror", + "time", + "tokio", + "zip", +] + +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5acdd78cb4ba54c0045ac14f62d8f94a03d10047904ae2a40afa1e99d8f70825" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" + +[[package]] +name = "windows_i686_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" + +[[package]] +name = "windows_i686_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" + +[[package]] +name = "zip" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf225bcf73bb52cbb496e70475c7bd7a3f769df699c0020f6c7bd9a96dcf0b8d" +dependencies = [ + "aes", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac", + "pbkdf2", + "sha1", + "time", + "zstd", +] + +[[package]] +name = "zstd" +version = "0.10.0+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b1365becbe415f3f0fcd024e2f7b45bacfb5bdd055f0dc113571394114e7bdd" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "4.1.4+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f7cd17c9af1a4d6c24beb1cc54b17e2ef7b593dc92f19e9d9acad8b182bbaee" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "1.6.3+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc49afa5c8d634e75761feda8c592051e7eeb4683ba827211eb0d731d3402ea8" +dependencies = [ + "cc", + "libc", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..76afdc9 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "transbeam" +version = "0.1.0" +authors = ["xenofem "] +edition = "2021" +license = "MIT" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +actix = "0.13" +actix-files = "0.6.0" +actix-http = "3.0.4" +actix-web = "4.0.1" +actix-web-actors = "4.1.0" +bytes = "1.1.0" +crc32fast = "1.3.2" +env_logger = "0.9" +futures = "0.3" +log = "0.4" +rand = "0.8.5" +sanitise-file-name = "1.0.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "1" +time = "0.3.9" +tokio = { version = "1.17.0", features = ["full"] } + +[dev-dependencies] +zip = "0.6.2" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..cc0923e --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# transbeam + +A low-frills low-latency file drop server + +## installation + +## todo + +- [ ] file uploading +- [ ] real-time file downloading +- [ ] upload progress bar +- [ ] uploader auth +- [ ] downloader auth +- [ ] sanitize filenames +- [ ] make sure upload errors are passed along to downloaders in a halfway reasonable way +- [ ] delete uploads after a while diff --git a/src/download.rs b/src/download.rs new file mode 100644 index 0000000..a476348 --- /dev/null +++ b/src/download.rs @@ -0,0 +1,769 @@ +use std::{ + fs::Metadata, + io, + path::{Path, PathBuf}, + time::{SystemTime, UNIX_EPOCH}, +}; + +use actix_web::{ + body::{self, BoxBody, SizedStream}, + dev::{ + self, AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory, + ServiceRequest, ServiceResponse, + }, + http::{ + header::{ + self, Charset, ContentDisposition, ContentEncoding, DispositionParam, + DispositionType, ExtendedValue, HeaderValue, + }, + StatusCode, + }, + Error, HttpMessage, HttpRequest, HttpResponse, Responder, +}; +use bitflags::bitflags; +use derive_more::{Deref, DerefMut}; +use futures_core::future::LocalBoxFuture; +use mime::Mime; +use mime_guess::from_path; + +use crate::{encoding::equiv_utf8_text, range::HttpRange}; + +bitflags! { + pub(crate) struct Flags: u8 { + const ETAG = 0b0000_0001; + const LAST_MD = 0b0000_0010; + const CONTENT_DISPOSITION = 0b0000_0100; + const PREFER_UTF8 = 0b0000_1000; + } +} + +impl Default for Flags { + fn default() -> Self { + Flags::from_bits_truncate(0b0000_1111) + } +} + +/// A file with an associated name. +/// +/// `NamedFile` can be registered as services: +/// ``` +/// use actix_web::App; +/// use actix_files::NamedFile; +/// +/// # async fn run() -> Result<(), Box> { +/// let file = NamedFile::open_async("./static/index.html").await?; +/// let app = App::new().service(file); +/// # Ok(()) +/// # } +/// ``` +/// +/// They can also be returned from handlers: +/// ``` +/// use actix_web::{Responder, get}; +/// use actix_files::NamedFile; +/// +/// #[get("/")] +/// async fn index() -> impl Responder { +/// NamedFile::open_async("./static/index.html").await +/// } +/// ``` +#[derive(Debug, Deref, DerefMut)] +pub struct NamedFile { + #[deref] + #[deref_mut] + file: File, + path: PathBuf, + modified: Option, + pub(crate) md: Metadata, + pub(crate) flags: Flags, + pub(crate) status_code: StatusCode, + pub(crate) content_type: Mime, + pub(crate) content_disposition: ContentDisposition, + pub(crate) encoding: Option, +} + +pub(crate) use std::fs::File; + +use super::chunked; + +impl NamedFile { + /// Creates an instance from a previously opened file. + /// + /// The given `path` need not exist and is only used to determine the `ContentType` and + /// `ContentDisposition` headers. + /// + /// # Examples + /// ```ignore + /// use std::{ + /// io::{self, Write as _}, + /// env, + /// fs::File + /// }; + /// use actix_files::NamedFile; + /// + /// let mut file = File::create("foo.txt")?; + /// file.write_all(b"Hello, world!")?; + /// let named_file = NamedFile::from_file(file, "bar.txt")?; + /// # std::fs::remove_file("foo.txt"); + /// Ok(()) + /// ``` + pub fn from_file>(file: File, path: P) -> io::Result { + let path = path.as_ref().to_path_buf(); + + // Get the name of the file and use it to construct default Content-Type + // and Content-Disposition values + let (content_type, content_disposition) = { + let filename = match path.file_name() { + Some(name) => name.to_string_lossy(), + None => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Provided path has no filename", + )); + } + }; + + let ct = from_path(&path).first_or_octet_stream(); + + let disposition = match ct.type_() { + mime::IMAGE | mime::TEXT | mime::AUDIO | mime::VIDEO => DispositionType::Inline, + mime::APPLICATION => match ct.subtype() { + mime::JAVASCRIPT | mime::JSON => DispositionType::Inline, + name if name == "wasm" => DispositionType::Inline, + _ => DispositionType::Attachment, + }, + _ => DispositionType::Attachment, + }; + + let mut parameters = + vec![DispositionParam::Filename(String::from(filename.as_ref()))]; + + if !filename.is_ascii() { + parameters.push(DispositionParam::FilenameExt(ExtendedValue { + charset: Charset::Ext(String::from("UTF-8")), + language_tag: None, + value: filename.into_owned().into_bytes(), + })) + } + + let cd = ContentDisposition { + disposition, + parameters, + }; + + (ct, cd) + }; + + let md = { + { + file.metadata()? + } + }; + + let modified = md.modified().ok(); + let encoding = None; + + Ok(NamedFile { + path, + file, + content_type, + content_disposition, + md, + modified, + encoding, + status_code: StatusCode::OK, + flags: Flags::default(), + }) + } + + /// Attempts to open a file in read-only mode. + /// + /// # Examples + /// ``` + /// use actix_files::NamedFile; + /// let file = NamedFile::open("foo.txt"); + /// ``` + pub fn open>(path: P) -> io::Result { + let file = File::open(&path)?; + Self::from_file(file, path) + } + + /// Attempts to open a file asynchronously in read-only mode. + /// + /// When the `experimental-io-uring` crate feature is enabled, this will be async. Otherwise, it + /// will behave just like `open`. + /// + /// # Examples + /// ``` + /// use actix_files::NamedFile; + /// # async fn open() { + /// let file = NamedFile::open_async("foo.txt").await.unwrap(); + /// # } + /// ``` + pub async fn open_async>(path: P) -> io::Result { + let file = { + { + File::open(&path)? + } + }; + + Self::from_file(file, path) + } + + /// Returns reference to the underlying file object. + #[inline] + pub fn file(&self) -> &File { + &self.file + } + + /// Returns the filesystem path to this file. + /// + /// # Examples + /// ``` + /// # use std::io; + /// use actix_files::NamedFile; + /// + /// # async fn path() -> io::Result<()> { + /// let file = NamedFile::open_async("test.txt").await?; + /// assert_eq!(file.path().as_os_str(), "foo.txt"); + /// # Ok(()) + /// # } + /// ``` + #[inline] + pub fn path(&self) -> &Path { + self.path.as_path() + } + + /// Returns the time the file was last modified. + /// + /// Returns `None` only on unsupported platforms; see [`std::fs::Metadata::modified()`]. + /// Therefore, it is usually safe to unwrap this. + #[inline] + pub fn modified(&self) -> Option { + self.modified + } + + /// Returns the filesystem metadata associated with this file. + #[inline] + pub fn metadata(&self) -> &Metadata { + &self.md + } + + /// Returns the `Content-Type` header that will be used when serving this file. + #[inline] + pub fn content_type(&self) -> &Mime { + &self.content_type + } + + /// Returns the `Content-Disposition` that will be used when serving this file. + #[inline] + pub fn content_disposition(&self) -> &ContentDisposition { + &self.content_disposition + } + + /// Returns the `Content-Encoding` that will be used when serving this file. + /// + /// A return value of `None` indicates that the content is not already using a compressed + /// representation and may be subject to compression downstream. + #[inline] + pub fn content_encoding(&self) -> Option { + self.encoding + } + + /// Set response status code. + #[deprecated(since = "0.7.0", note = "Prefer `Responder::customize()`.")] + pub fn set_status_code(mut self, status: StatusCode) -> Self { + self.status_code = status; + self + } + + /// Sets the `Content-Type` header that will be used when serving this file. By default the + /// `Content-Type` is inferred from the filename extension. + #[inline] + pub fn set_content_type(mut self, mime_type: Mime) -> Self { + self.content_type = mime_type; + self + } + + /// Set the Content-Disposition for serving this file. This allows changing the + /// `inline/attachment` disposition as well as the filename sent to the peer. + /// + /// By default the disposition is `inline` for `text/*`, `image/*`, `video/*` and + /// `application/{javascript, json, wasm}` mime types, and `attachment` otherwise, and the + /// filename is taken from the path provided in the `open` method after converting it to UTF-8 + /// (using `to_string_lossy`). + #[inline] + pub fn set_content_disposition(mut self, cd: ContentDisposition) -> Self { + self.content_disposition = cd; + self.flags.insert(Flags::CONTENT_DISPOSITION); + self + } + + /// Disables `Content-Disposition` header. + /// + /// By default, the `Content-Disposition` header is sent. + #[inline] + pub fn disable_content_disposition(mut self) -> Self { + self.flags.remove(Flags::CONTENT_DISPOSITION); + self + } + + /// Sets content encoding for this file. + /// + /// This prevents the `Compress` middleware from modifying the file contents and signals to + /// browsers/clients how to decode it. For example, if serving a compressed HTML file (e.g., + /// `index.html.gz`) then use `.set_content_encoding(ContentEncoding::Gzip)`. + #[inline] + pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self { + self.encoding = Some(enc); + self + } + + /// Specifies whether to return `ETag` header in response. + /// + /// Default is true. + #[inline] + pub fn use_etag(mut self, value: bool) -> Self { + self.flags.set(Flags::ETAG, value); + self + } + + /// Specifies whether to return `Last-Modified` header in response. + /// + /// Default is true. + #[inline] + pub fn use_last_modified(mut self, value: bool) -> Self { + self.flags.set(Flags::LAST_MD, value); + self + } + + /// Specifies whether text responses should signal a UTF-8 encoding. + /// + /// Default is false (but will default to true in a future version). + #[inline] + pub fn prefer_utf8(mut self, value: bool) -> Self { + self.flags.set(Flags::PREFER_UTF8, value); + self + } + + /// Creates an `ETag` in a format is similar to Apache's. + pub(crate) fn etag(&self) -> Option { + self.modified.as_ref().map(|mtime| { + let ino = { + #[cfg(unix)] + { + #[cfg(unix)] + use std::os::unix::fs::MetadataExt as _; + + self.md.ino() + } + + #[cfg(not(unix))] + { + 0 + } + }; + + let dur = mtime + .duration_since(UNIX_EPOCH) + .expect("modification time must be after epoch"); + + header::EntityTag::new_strong(format!( + "{:x}:{:x}:{:x}:{:x}", + ino, + self.md.len(), + dur.as_secs(), + dur.subsec_nanos() + )) + }) + } + + pub(crate) fn last_modified(&self) -> Option { + self.modified.map(|mtime| mtime.into()) + } + + /// Creates an `HttpResponse` with file as a streaming body. + pub fn into_response(self, req: &HttpRequest) -> HttpResponse { + if self.status_code != StatusCode::OK { + let mut res = HttpResponse::build(self.status_code); + + let ct = if self.flags.contains(Flags::PREFER_UTF8) { + equiv_utf8_text(self.content_type.clone()) + } else { + self.content_type + }; + + res.insert_header((header::CONTENT_TYPE, ct.to_string())); + + if self.flags.contains(Flags::CONTENT_DISPOSITION) { + res.insert_header(( + header::CONTENT_DISPOSITION, + self.content_disposition.to_string(), + )); + } + + if let Some(current_encoding) = self.encoding { + res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str())); + } + + let reader = chunked::new_chunked_read(self.md.len(), 0, self.file); + + return res.streaming(reader); + } + + let etag = if self.flags.contains(Flags::ETAG) { + self.etag() + } else { + None + }; + + let last_modified = if self.flags.contains(Flags::LAST_MD) { + self.last_modified() + } else { + None + }; + + // check preconditions + let precondition_failed = if !any_match(etag.as_ref(), req) { + true + } else if let (Some(ref m), Some(header::IfUnmodifiedSince(ref since))) = + (last_modified, req.get_header()) + { + let t1: SystemTime = (*m).into(); + let t2: SystemTime = (*since).into(); + + match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) { + (Ok(t1), Ok(t2)) => t1.as_secs() > t2.as_secs(), + _ => false, + } + } else { + false + }; + + // check last modified + let not_modified = if !none_match(etag.as_ref(), req) { + true + } else if req.headers().contains_key(header::IF_NONE_MATCH) { + false + } else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) = + (last_modified, req.get_header()) + { + let t1: SystemTime = (*m).into(); + let t2: SystemTime = (*since).into(); + + match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) { + (Ok(t1), Ok(t2)) => t1.as_secs() <= t2.as_secs(), + _ => false, + } + } else { + false + }; + + let mut res = HttpResponse::build(self.status_code); + + let ct = if self.flags.contains(Flags::PREFER_UTF8) { + equiv_utf8_text(self.content_type.clone()) + } else { + self.content_type + }; + + res.insert_header((header::CONTENT_TYPE, ct.to_string())); + + if self.flags.contains(Flags::CONTENT_DISPOSITION) { + res.insert_header(( + header::CONTENT_DISPOSITION, + self.content_disposition.to_string(), + )); + } + + if let Some(current_encoding) = self.encoding { + res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str())); + } + + if let Some(lm) = last_modified { + res.insert_header((header::LAST_MODIFIED, lm.to_string())); + } + + if let Some(etag) = etag { + res.insert_header((header::ETAG, etag.to_string())); + } + + res.insert_header((header::ACCEPT_RANGES, "bytes")); + + let mut length = self.md.len(); + let mut offset = 0; + + // check for range header + if let Some(ranges) = req.headers().get(header::RANGE) { + if let Ok(ranges_header) = ranges.to_str() { + if let Ok(ranges) = HttpRange::parse(ranges_header, length) { + length = ranges[0].length; + offset = ranges[0].start; + + // don't allow compression middleware to modify partial content + res.insert_header(( + header::CONTENT_ENCODING, + HeaderValue::from_static("identity"), + )); + + res.insert_header(( + header::CONTENT_RANGE, + format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()), + )); + } else { + res.insert_header((header::CONTENT_RANGE, format!("bytes */{}", length))); + return res.status(StatusCode::RANGE_NOT_SATISFIABLE).finish(); + }; + } else { + return res.status(StatusCode::BAD_REQUEST).finish(); + }; + }; + + if precondition_failed { + return res.status(StatusCode::PRECONDITION_FAILED).finish(); + } else if not_modified { + return res + .status(StatusCode::NOT_MODIFIED) + .body(body::None::new()) + .map_into_boxed_body(); + } + + let reader = chunked::new_chunked_read(length, offset, self.file); + + if offset != 0 || length != self.md.len() { + res.status(StatusCode::PARTIAL_CONTENT); + } + + res.body(SizedStream::new(length, reader)) + } +} + +/// Returns true if `req` has no `If-Match` header or one which matches `etag`. +fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { + match req.get_header::() { + None | Some(header::IfMatch::Any) => true, + + Some(header::IfMatch::Items(ref items)) => { + if let Some(some_etag) = etag { + for item in items { + if item.strong_eq(some_etag) { + return true; + } + } + } + + false + } + } +} + +/// Returns true if `req` doesn't have an `If-None-Match` header matching `req`. +fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { + match req.get_header::() { + Some(header::IfNoneMatch::Any) => false, + + Some(header::IfNoneMatch::Items(ref items)) => { + if let Some(some_etag) = etag { + for item in items { + if item.weak_eq(some_etag) { + return false; + } + } + } + + true + } + + None => true, + } +} + +impl Responder for NamedFile { + type Body = BoxBody; + + fn respond_to(self, req: &HttpRequest) -> HttpResponse { + self.into_response(req) + } +} + +impl ServiceFactory for NamedFile { + type Response = ServiceResponse; + type Error = Error; + type Config = (); + type Service = NamedFileService; + type InitError = (); + type Future = LocalBoxFuture<'static, Result>; + + fn new_service(&self, _: ()) -> Self::Future { + let service = NamedFileService { + path: self.path.clone(), + }; + + Box::pin(async move { Ok(service) }) + } +} + +#[doc(hidden)] +#[derive(Debug)] +pub struct NamedFileService { + path: PathBuf, +} + +impl Service for NamedFileService { + type Response = ServiceResponse; + type Error = Error; + type Future = LocalBoxFuture<'static, Result>; + + dev::always_ready!(); + + fn call(&self, req: ServiceRequest) -> Self::Future { + let (req, _) = req.into_parts(); + + let path = self.path.clone(); + Box::pin(async move { + let file = NamedFile::open_async(path).await?; + let res = file.into_response(&req); + Ok(ServiceResponse::new(req, res)) + }) + } +} + +impl HttpServiceFactory for NamedFile { + fn register(self, config: &mut AppService) { + config.register_service( + ResourceDef::root_prefix(self.path.to_string_lossy().as_ref()), + None, + self, + None, + ) + } +} + + +use std::{ + cmp, fmt, + future::Future, + io, + pin::Pin, + task::{Context, Poll}, +}; + +use actix_web::{error::Error, web::Bytes}; +use futures_core::{ready, Stream}; +use pin_project_lite::pin_project; + +use super::named::File; + +pin_project! { + /// Adapter to read a `std::file::File` in chunks. + #[doc(hidden)] + pub struct ChunkedReadFile { + size: u64, + offset: u64, + #[pin] + state: ChunkedReadFileState, + counter: u64, + callback: F, + } +} + +pin_project! { + #[project = ChunkedReadFileStateProj] + #[project_replace = ChunkedReadFileStateProjReplace] + enum ChunkedReadFileState { + File { file: Option, }, + Future { #[pin] fut: Fut }, + } +} + +impl fmt::Debug for ChunkedReadFile { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("ChunkedReadFile") + } +} + +pub(crate) fn new_chunked_read( + size: u64, + offset: u64, + file: File, +) -> impl Stream> { + ChunkedReadFile { + size, + offset, + state: ChunkedReadFileState::File { file: Some(file) }, + counter: 0, + callback: chunked_read_file_callback, + } +} + +async fn chunked_read_file_callback( + mut file: File, + offset: u64, + max_bytes: usize, +) -> Result<(File, Bytes), Error> { + use io::{Read as _, Seek as _}; + + let res = actix_web::web::block(move || { + let mut buf = Vec::with_capacity(max_bytes); + + file.seek(io::SeekFrom::Start(offset))?; + + let n_bytes = file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?; + + if n_bytes == 0 { + Err(io::Error::from(io::ErrorKind::UnexpectedEof)) + } else { + Ok((file, Bytes::from(buf))) + } + }) + .await??; + + Ok(res) +} + +impl Stream for ChunkedReadFile +where + F: Fn(File, u64, usize) -> Fut, + Fut: Future>, +{ + type Item = Result; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let mut this = self.as_mut().project(); + match this.state.as_mut().project() { + ChunkedReadFileStateProj::File { file } => { + let size = *this.size; + let offset = *this.offset; + let counter = *this.counter; + + if size == counter { + Poll::Ready(None) + } else { + let max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize; + + let file = file + .take() + .expect("ChunkedReadFile polled after completion"); + + let fut = (this.callback)(file, offset, max_bytes); + + this.state + .project_replace(ChunkedReadFileState::Future { fut }); + + self.poll_next(cx) + } + } + ChunkedReadFileStateProj::Future { fut } => { + let (file, bytes) = ready!(fut.poll(cx))?; + + this.state + .project_replace(ChunkedReadFileState::File { file: Some(file) }); + + *this.offset += bytes.len() as u64; + *this.counter += bytes.len() as u64; + + Poll::Ready(Some(Ok(bytes))) + } + } + } +} diff --git a/src/file.rs b/src/file.rs new file mode 100644 index 0000000..a2c026d --- /dev/null +++ b/src/file.rs @@ -0,0 +1,45 @@ +use std::{fs::File, task::Waker, io::Write, path::PathBuf}; + +pub trait LiveWriter: Write { + fn add_waker(&mut self, waker: Waker); +} + +/// A simple wrapper for a file that can be read while we're still appending data +pub struct LiveFileWriter { + file: File, + /// Wake handles for contexts that are waiting for us to write more + wakers: Vec, +} + +impl LiveFileWriter { + pub fn new(path: &PathBuf) -> std::io::Result { + Ok(Self { + file: File::options().write(true).create_new(true).open(path)?, + wakers: Vec::new(), + }) + } +} + +impl LiveWriter for LiveFileWriter { + fn add_waker(&mut self, waker: Waker) { + self.wakers.push(waker); + } +} + +impl Write for LiveFileWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let result = self.file.write(buf); + if let Ok(n) = result { + if n > 0 { + for waker in self.wakers.drain(..) { + waker.wake(); + } + } + } + result + } + + fn flush(&mut self) -> std::io::Result<()> { + self.file.flush() + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2785188 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,82 @@ +//mod download; +mod file; +mod upload; +mod zip; + +use std::{collections::HashMap, task::Waker, sync::{mpsc::Sender, RwLock}, path::PathBuf}; + +use actix::Addr; +use actix_web::{ + get, middleware::Logger, web, App, HttpResponse, HttpServer, + Responder, HttpRequest, +}; +use actix_web_actors::ws; +use time::OffsetDateTime; + +pub struct UploadedFile { + name: String, + size: usize, + modtime: OffsetDateTime, +} + +impl UploadedFile { + fn new(name: &str, size: usize, modtime: OffsetDateTime) -> Self { + Self { + name: sanitise_file_name::sanitise(name), + size, + modtime, + } + } +} + + +pub struct DownloadableFile { + name: String, + size: usize, + modtime: OffsetDateTime, + uploader: Option>, +} + +type AppData = web::Data>>; + +fn storage_dir() -> PathBuf { + PathBuf::from(std::env::var("STORAGE_DIR").unwrap_or_else(|_| String::from("storage"))) +} + +fn app_name() -> String { + std::env::var("APP_NAME").unwrap_or_else(|_| String::from("transbeam")) +} + +#[get("/upload")] +async fn upload_socket( + req: HttpRequest, + stream: web::Payload, + data: AppData, +) -> impl Responder { + ws::start( + upload::Uploader::new(data), + &req, + stream + ) +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + env_logger::init(); + + let ip = "0.0.0.0:3000"; + + let data: AppData = web::Data::new(RwLock::new(HashMap::new())); + + HttpServer::new(move || { + App::new() + .app_data(data.clone()) + .wrap(Logger::default()) + .service(upload_socket) + .service(actix_files::Files::new("/", "./static").index_file("index.html")) + }) + .bind(ip)? + .run() + .await?; + Ok(()) +} diff --git a/src/upload.rs b/src/upload.rs new file mode 100644 index 0000000..2a29c9e --- /dev/null +++ b/src/upload.rs @@ -0,0 +1,228 @@ +use std::{collections::HashSet, fmt::Display, io::Write}; + +use actix::{Actor, StreamHandler, ActorContext, AsyncContext}; +use actix_http::ws::{Item, CloseReason}; +use actix_web_actors::ws::{self, CloseCode}; +use log::{error, debug, info, trace}; +use rand::distributions::{Alphanumeric, DistString}; +use serde::Deserialize; +use time::OffsetDateTime; + +use crate::{UploadedFile, DownloadableFile, file::LiveWriter}; + +const FILENAME_DATE_FORMAT: &[time::format_description::FormatItem] = + time::macros::format_description!("[year]-[month]-[day]-[hour][minute][second]"); + +#[derive(thiserror::Error, Debug)] +enum Error { + #[error("Failed to parse file metadata")] + Parse(#[from] serde_json::Error), + #[error("Error writing to stored file")] + Storage(#[from] std::io::Error), + #[error("Time formatting error")] + TimeFormat(#[from] time::error::Format), + #[error("Lock on app state is poisoned")] + LockPoisoned, + #[error("Duplicate filename could not be deduplicated")] + DuplicateFilename, + #[error("This message type was not expected at this stage")] + UnexpectedMessageType, + #[error("Metadata contained an empty list of files")] + NoFiles, + #[error("Websocket was closed by client before completing transfer")] + ClosedEarly(Option), + #[error("Client sent more data than they were supposed to")] + TooMuchData, +} + +impl Error { + fn close_code(&self) -> CloseCode { + match self { + Self::Parse(_) => CloseCode::Invalid, + Self::Storage(_) => CloseCode::Error, + Self::TimeFormat(_) => CloseCode::Error, + Self::LockPoisoned => CloseCode::Error, + Self::DuplicateFilename => CloseCode::Policy, + Self::UnexpectedMessageType => CloseCode::Invalid, + Self::NoFiles => CloseCode::Policy, + Self::ClosedEarly(_) => CloseCode::Invalid, + Self::TooMuchData => CloseCode::Invalid, + } + } +} + +pub struct Uploader { + writer: Option>, + app_data: super::AppData, + bytes_remaining: usize, +} + +impl Uploader { + pub fn new(app_data: super::AppData) -> Self { + Self { + writer: None, + app_data, + bytes_remaining: 0, + } + } +} + +impl Actor for Uploader { + type Context = ws::WebsocketContext; +} + +#[derive(Debug, Deserialize)] +struct RawUploadedFile { + name: String, + size: usize, + modtime: i64, +} + +impl RawUploadedFile { + fn process(&self) -> UploadedFile { + UploadedFile::new( + &self.name, + self.size, + OffsetDateTime::from_unix_timestamp(self.modtime / 1000) + .unwrap_or_else(|_| OffsetDateTime::now_utc()), + ) + } +} + +impl StreamHandler> for Uploader { + fn handle(&mut self, msg: Result, ctx: &mut Self::Context) { + let msg = match msg { + Ok(m) => m, + Err(e) => { + error!("Websocket error: {:?}", e); + ctx.stop(); + return; + }, + }; + + match self.handle_message(msg, ctx) { + Err(e) => { + error!("{:?}", e); + ctx.close(Some(ws::CloseReason { + code: e.close_code(), + description: Some(e.to_string()), + })); + ctx.stop(); + } + Ok(true) => { + info!("Finished uploading data"); + self.writer.as_mut().map(|w| w.flush()); + ctx.close(Some(ws::CloseReason { + code: CloseCode::Normal, + description: None, + })); +// self.app_data.write().unwrap().entry( + ctx.stop(); + } + _ => () + } + } +} + +fn ack(ctx: &mut ::Context) { + ctx.text("ack"); +} + +impl Uploader { + fn handle_message(&mut self, msg: ws::Message, ctx: &mut ::Context) -> Result{ + trace!("Websocket message: {:?}", msg); + match msg { + ws::Message::Text(text) => { + if self.writer.is_some() { + return Err(Error::UnexpectedMessageType) + } + let raw_files: Vec = serde_json::from_slice(text.as_bytes())?; + info!("Received file list: {} files", raw_files.len()); + debug!("{:?}", raw_files); + let mut filenames: HashSet = HashSet::new(); + let mut files = Vec::new(); + for raw_file in raw_files.iter() { + let mut file = raw_file.process(); + while filenames.contains(&file.name) { + info!("Duplicate file name: {}", file.name); + if file.name.len() >= sanitise_file_name::Options::DEFAULT.length_limit { + return Err(Error::DuplicateFilename); + } + file.name.insert(0, '_'); + } + filenames.insert(file.name.clone()); + self.bytes_remaining += file.size; + files.push(file); + } + if files.is_empty() { + return Err(Error::NoFiles); + } + let storage_filename = Alphanumeric.sample_string(&mut rand::thread_rng(), 8); + let storage_path = super::storage_dir().join(storage_filename.clone()); + info!("storing to: {:?}", storage_path); + let writer = super::file::LiveFileWriter::new(&storage_path)?; + if files.len() > 1 { + info!("Wrapping in zipfile generator"); + let now = OffsetDateTime::now_utc(); + let writer = super::zip::ZipGenerator::new(files, Box::new(writer)); + let size = writer.total_size(); + self.writer = Some(Box::new(writer)); + let download_filename = super::app_name() + + &now.format(FILENAME_DATE_FORMAT)? + + ".zip"; + let modtime = now; + self.app_data.write().map_err(|_| Error::LockPoisoned)?.insert(storage_filename, DownloadableFile { + name: download_filename, + size, + modtime, + uploader: Some(ctx.address()), + }); + } else { + self.writer = Some(Box::new(writer)); + self.app_data.write().map_err(|_| Error::LockPoisoned)?.insert(storage_filename, DownloadableFile { + name: files[0].name.clone(), + size: files[0].size, + modtime: files[0].modtime, + uploader: Some(ctx.address()), + }); + } + ack(ctx); + } + ws::Message::Binary(data) + | ws::Message::Continuation(Item::FirstBinary(data)) + | ws::Message::Continuation(Item::Continue(data)) + | ws::Message::Continuation(Item::Last(data)) => + { + if let Some(ref mut writer) = self.writer { + if data.len() > self.bytes_remaining { + return Err(Error::TooMuchData); + } + self.bytes_remaining -= data.len(); + writer.write_all(&data)?; + ack(ctx); + if self.bytes_remaining == 0 { + return Ok(true); + } + } else { + return Err(Error::UnexpectedMessageType); + } + } + ws::Message::Close(reason) => { + if self.bytes_remaining > 0 { + return Err(Error::ClosedEarly(reason)); + } else { + return Ok(true); + } + } + ws::Message::Ping(ping) => { + debug!("Ping received, ponging"); + ctx.pong(&ping); + } + ws::Message::Nop | ws::Message::Pong(_) => (), + _ => { + return Err(Error::UnexpectedMessageType); + } + } + Ok(false) + } +} diff --git a/src/zip.rs b/src/zip.rs new file mode 100644 index 0000000..247840a --- /dev/null +++ b/src/zip.rs @@ -0,0 +1,329 @@ +use std::io::Write; +use std::task::Waker; + +use crc32fast::Hasher; +use log::debug; +use time::OffsetDateTime; + +use crate::UploadedFile; +use crate::file::LiveWriter; + +const SIGNATURE_SIZE: usize = 4; +const SHARED_FIELDS_SIZE: usize = 26; +const EXTRA_FIELD_SIZE: usize = 41; +const LOCAL_HEADER_SIZE_MINUS_FILENAME: usize = SIGNATURE_SIZE + SHARED_FIELDS_SIZE + EXTRA_FIELD_SIZE; +const DATA_DESCRIPTOR_SIZE: usize = 24; +const FILE_ENTRY_SIZE_MINUS_FILENAME_AND_FILE: usize = LOCAL_HEADER_SIZE_MINUS_FILENAME + DATA_DESCRIPTOR_SIZE; + +const CENTRAL_DIRECTORY_HEADER_SIZE_MINUS_FILENAME: usize = SIGNATURE_SIZE + 2 + SHARED_FIELDS_SIZE + 14 + EXTRA_FIELD_SIZE; + +const EOCD64_RECORD_SIZE: usize = 56; +const EOCD64_LOCATOR_SIZE: usize = 20; +const EOCD_RECORD_SIZE: usize = 22; +const EOCD_TOTAL_SIZE: usize = EOCD64_RECORD_SIZE + EOCD64_LOCATOR_SIZE + EOCD_RECORD_SIZE; + +const EMPTY_STRING_CRC32: u32 = 0; + +fn file_entry_size(file: &UploadedFile) -> usize { + FILE_ENTRY_SIZE_MINUS_FILENAME_AND_FILE + file.name.len() + file.size +} + +fn file_entries_size(files: &[UploadedFile]) -> usize { + let mut total = 0; + for file in files.iter() { + total += file_entry_size(file) + } + total +} + +fn central_directory_size(files: &[UploadedFile]) -> usize { + let mut total = 0; + for file in files.iter() { + total += CENTRAL_DIRECTORY_HEADER_SIZE_MINUS_FILENAME + file.name.len(); + } + total +} + +fn zipfile_size(files: &[UploadedFile]) -> usize { + file_entries_size(files) + central_directory_size(files) + EOCD_TOTAL_SIZE +} + +fn fat_timestamp(time: OffsetDateTime) -> u32 { + (((time.year() - 1980) as u32) << 25) + | ((time.month() as u32) << 21) + | ((time.day() as u32) << 16) + | ((time.hour() as u32) << 11) + | ((time.minute() as u32) << 5) + | ((time.second() as u32) >> 1) +} + +/// Append a value to a byte vector as little-endian bytes +fn append_value(data: &mut Vec, mut value: u64, len: usize) { + data.resize_with(data.len() + len, || { let byte = value as u8; value >>= 8; byte }); +} + +fn append_repeated_byte(data: &mut Vec, byte: u8, count: usize) { + data.resize(data.len() + count, byte); +} + +fn append_0(data: &mut Vec, count: usize) { + append_repeated_byte(data, 0, count); +} + +fn append_ff(data: &mut Vec, count: usize) { + append_repeated_byte(data, 0xff, count); +} + +impl UploadedFile { + /// Returns the fields shared by the ZIP local file header and + /// central directory file header - "Version needed to extract" + /// through "Extra field length". + fn shared_header_fields(&self, hash: Option) -> Vec { + let mut fields = vec![ + 45, 0, // Minimum version required to extract: 4.5 for ZIP64 extensions + 0b00001000, 0, // General purpose bit flag: size and CRC-32 in data descriptor + 0, 0, // Compression method: none + ]; + append_value(&mut fields, fat_timestamp(self.modtime) as u64, 4); + // Use 0s as a placeholder if the CRC-32 hash isn't known yet + append_value(&mut fields, hash.unwrap_or(0) as u64, 4); + // Placeholders for compressed and uncompressed size in ZIP64 record, 4 bytes each + append_ff(&mut fields, 8); + append_value(&mut fields, self.name.len() as u64, 2); + // Extra field length: 32 bytes for zip64, 9 bytes for timestamp + fields.append(&mut vec![41, 0]); + fields + } + + fn extra_field(&self, local_header_offset: usize) -> Vec { + let mut field = vec![ + 0x01, 0x00, // Zip64 extended information + 28, 0, // 28 bytes of data + ]; + // Original size and compressed size - if this is in the local + // header, we're supposed to leave these blank and point to + // the data descriptor, but I'm assuming it won't hurt to fill + // them in regardless + append_value(&mut field, self.size as u64, 8); + append_value(&mut field, self.size as u64, 8); + append_value(&mut field, local_header_offset as u64, 8); + append_0(&mut field, 4); // File starts on disk 0, there's no other disk + + field.append(&mut vec![ + 0x55, 0x54, // Extended timestamp + 5, 0, // 5 bytes of data + 0b00000001, // Flags: Only modification time is present + ]); + append_value(&mut field, self.modtime.unix_timestamp() as u64, 4); + + field + } + + fn local_header(&self, local_header_offset: usize) -> Vec { + let mut header = vec![0x50, 0x4b, 0x03, 0x04]; // Local file header signature + header.append(&mut self.shared_header_fields(None)); + header.append(&mut self.name.clone().into_bytes()); + header.append(&mut self.extra_field(local_header_offset)); + header + } + + fn central_directory_header(&self, local_header_offset: usize, hash: u32) -> Vec { + let mut header = vec![ + 0x50, 0x4b, 0x01, 0x02, // Central directory file header signature + 45, 3, // Made by a Unix system supporting version 4.5 + ]; + header.append(&mut self.shared_header_fields(Some(hash))); + header.append(&mut vec![ + 0, 0, // File comment length: 0 + 0, 0, // Disk number where file starts: 0 + 0, 0, // Internal file attributes: nothing + 0, 0, 0, 0, // External file attributes: nothing + 0xff, 0xff, 0xff, 0xff, // Relative offset of local file header: placeholder, see ZIP64 data + ]); + header.append(&mut self.name.clone().into_bytes()); + header.append(&mut self.extra_field(local_header_offset)); + header + } + + fn data_descriptor(&self, hash: u32) -> Vec { + let mut descriptor = vec![0x50, 0x4b, 0x07, 0x08]; // Data descriptor signature + append_value(&mut descriptor, hash as u64, 4); + // Compressed and uncompressed sizes + append_value(&mut descriptor, self.size as u64, 8); + append_value(&mut descriptor, self.size as u64, 8); + descriptor + } +} + +fn end_of_central_directory(files: &[UploadedFile]) -> Vec { + let entries_size = file_entries_size(files) as u64; + let directory_size = central_directory_size(files) as u64; + + let mut eocd = vec![ + 0x50, 0x4b, 0x06, 0x06, // EOCD64 record signature + 44, // Size of remaining EOCD64 record + ]; + append_0(&mut eocd, 7); // pad out the rest of the size field + eocd.append(&mut vec![ + 45, 3, // Made by a Unix system supporting version 4.5 + 45, 0, // Minimum version 4.5 to extract + ]); + append_0(&mut eocd, 8); // Two 4-byte disk numbers, both 0 + // Number of central directory records, on this disk and in total + append_value(&mut eocd, files.len() as u64, 8); + append_value(&mut eocd, files.len() as u64, 8); + append_value(&mut eocd, directory_size, 8); + append_value(&mut eocd, entries_size, 8); // Offset of start of central directory + + eocd.append(&mut vec![0x50, 0x4b, 0x06, 0x07]); // EOCD64 locator signature + append_0(&mut eocd, 4); // disk number + append_value(&mut eocd, entries_size + directory_size, 8); // EOCD64 record offset + append_0(&mut eocd, 4); // total number of disks; + + eocd.append(&mut vec![0x50, 0x4b, 0x05, 0x06]); // EOCD record signature + append_ff(&mut eocd, 16); // Zip64 placeholders for disk numbers, record counts, and offsets + append_0(&mut eocd, 2); // Comment length: 0 + + eocd +} + +pub struct ZipGenerator<'a> { + files: Vec, + file_index: usize, + byte_index: usize, + pending_metadata: Vec, + hasher: Hasher, + hashes: Vec, + output: Box +} + +impl<'a> ZipGenerator<'a> { + pub fn new(files: Vec, output: Box) -> Self { + let mut result = Self { + files, + file_index: 0, + byte_index: 0, + pending_metadata: vec![], + hasher: Hasher::new(), + hashes: vec![], + output, + }; + result.start_new_file(); + result + } + + pub fn total_size(&self) -> usize { + zipfile_size(&self.files) + } + + fn finish_file(&mut self) { + let hash = std::mem::replace(&mut self.hasher, Hasher::new()).finalize(); + self.hashes.push(hash); + self.pending_metadata.append(&mut self.files[self.file_index].data_descriptor(hash)); + debug!("Finishing file entry in zipfile: {}, hash {}", self.files[self.file_index].name, hash); + self.file_index += 1; + self.start_new_file(); + } + + fn start_new_file(&mut self) { + let mut offset = file_entries_size(&self.files[..self.file_index]); + while self.file_index < self.files.len() && self.files[self.file_index].size == 0 { + debug!("Empty file entry in zipfile: {}", self.files[self.file_index].name); + self.hashes.push(EMPTY_STRING_CRC32); + let mut local_header = self.files[self.file_index].local_header(offset); + let mut data_descriptor = self.files[self.file_index].data_descriptor(EMPTY_STRING_CRC32); + offset += local_header.len() + data_descriptor.len(); + self.file_index += 1; + self.pending_metadata.append(&mut local_header); + self.pending_metadata.append(&mut data_descriptor); + } + if self.file_index < self.files.len() { + debug!("Starting file entry in zipfile: {}", self.files[self.file_index].name); + self.byte_index = 0; + self.pending_metadata.append(&mut self.files[self.file_index].local_header(offset)); + } else { + self.finish_zipfile(); + } + } + + fn finish_zipfile(&mut self) { + debug!("Writing zipfile central directory"); + let mut offset = 0; + for (i, file) in self.files.iter().enumerate() { + debug!("Writing central directory entry: {}, hash {}", file.name, self.hashes[i]); + self.pending_metadata.append(&mut file.central_directory_header(offset, self.hashes[i])); + offset += file_entry_size(file); + } + debug!("Writing end of central directory"); + self.pending_metadata.append(&mut end_of_central_directory(&self.files)); + } +} + +impl<'a> LiveWriter for ZipGenerator<'a> { + fn add_waker(&mut self, waker: Waker) { + self.output.add_waker(waker); + } +} + +impl<'a> Write for ZipGenerator<'a> { + fn write(&mut self, mut buf: &[u8]) -> std::io::Result { + while !self.pending_metadata.is_empty() { + let result = self.output.write(self.pending_metadata.as_slice()); + match result { + Ok(0) | Err(_) => { return result; } + Ok(n) => { self.pending_metadata.drain(..n); } + } + } + if self.file_index >= self.files.len() { + return Ok(0); + } + let bytes_remaining = self.files[self.file_index].size - self.byte_index; + if bytes_remaining < buf.len() { + buf = &buf[..bytes_remaining]; + } + let result = self.output.write(buf); + match result { + Ok(0) | Err(_) => (), + Ok(n) => { + self.hasher.update(&buf[..n]); + self.byte_index += n; + if n == bytes_remaining { + self.finish_file(); + } + } + } + result + } + + fn flush(&mut self) -> std::io::Result<()> { + debug!("Flushing zipfile writer"); + if !self.pending_metadata.is_empty() { + self.output.write_all(self.pending_metadata.as_slice())?; + self.pending_metadata.clear(); + } + self.output.flush() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_no_files() { + let mut output: Vec = vec![]; + { + let mut zipgen = ZipGenerator::new(vec![], Box::new(std::io::Cursor::new(&mut output))); + zipgen.write_all(&[]).unwrap(); + zipgen.flush().unwrap(); + } + + eprintln!("{:?}", &output); + { + let mut reader = std::io::BufReader::new(output.as_slice()); + let zipfile = zip::read::read_zipfile_from_stream(&mut reader).unwrap(); + assert!(zipfile.is_none()); + } + let archive = zip::ZipArchive::new(std::io::Cursor::new(output)).unwrap(); + assert!(archive.is_empty()); + } +} diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..735628d --- /dev/null +++ b/static/index.html @@ -0,0 +1,22 @@ + + + + + + + Upload Test + + +
+ +
+ +

Files selected:

+
    +
+ + + diff --git a/static/transbeam.css b/static/transbeam.css new file mode 100644 index 0000000..29dfbbb --- /dev/null +++ b/static/transbeam.css @@ -0,0 +1,24 @@ +input[type="file"] { + display: none; +} + +button, .fake_button { + font-size: 18px; + font-family: sans-serif; + color: #000; + background-color: #ccc; + border: 1px solid #bbb; + border-radius: 4px; + padding: 6px 12px; + cursor: pointer; +} + +button:hover, .fake_button:hover { + background-color: #aaa; +} + +button:disabled, button:disabled:hover { + color: #aaa; + background-color: #eee; + border-color: #ddd; +} diff --git a/static/upload.js b/static/upload.js new file mode 100644 index 0000000..3cb582c --- /dev/null +++ b/static/upload.js @@ -0,0 +1,105 @@ +let files = []; + +let socket = null; +let fileIndex = 0; +let byteIndex = 0; + +function sendMetadata() { + const metadata = files.map((file) => ({ + name: file.name, + size: file.size, + modtime: file.lastModified, + })); + socket.send(JSON.stringify(metadata)); +} + +function finishSending() { + if (socket.bufferedAmount > 0) { + window.setTimeout(finishSending, 1000); + return; + } + socket.close(); + alert("done"); +} + +function sendData() { + if (fileIndex >= files.length) { + finishSending(); + } + const currentFile = files[fileIndex]; + if (byteIndex < currentFile.size) { + const endpoint = Math.min(byteIndex+8192, currentFile.size); + const data = currentFile.slice(byteIndex, endpoint); + socket.send(data); + byteIndex = endpoint; + } else { + fileIndex += 1; + byteIndex = 0; + sendData(); + } +} + +const fileInput = document.getElementById('file_input'); +const fileInputMessage = document.getElementById('file_input_message'); +const fileList = document.getElementById('file_list'); +const uploadButton = document.getElementById('upload'); + +function updateButtons() { + if (files.length === 0) { + uploadButton.disabled = true; + fileInputMessage.textContent = 'Select files to upload...'; + } else { + uploadButton.disabled = false; + fileInputMessage.textContent = 'Select more files to upload...'; + } +} + +updateButtons(); + +function addFile(newFile) { + if (files.some((oldFile) => newFile.name === oldFile.name)) { return; } + + files.push(newFile); + + const listEntry = document.createElement('li'); + const deleteButton = document.createElement('button'); + deleteButton.textContent = 'x'; + deleteButton.addEventListener('click', () => { + removeFile(newFile.name); + listEntry.remove(); + updateButtons(); + }); + const entryName = document.createElement('span'); + entryName.textContent = newFile.name; + listEntry.appendChild(deleteButton); + listEntry.appendChild(entryName); + + fileList.appendChild(listEntry); +} + +function removeFile(name) { + files = files.filter((file) => file.name !== name); +} + +fileInput.addEventListener('input', (e) => { + for (const file of e.target.files) { addFile(file); } + updateButtons(); + e.target.value = ''; +}); + +uploadButton.addEventListener('click', (e) => { + if (files.length === 0) { return; } + + fileInput.disabled = true; + for (const button of document.getElementsByTagName('button')) { + button.disabled = true; + } + + socket = new WebSocket('ws://localhost:3000/upload'); + socket.addEventListener('open', sendMetadata); + socket.addEventListener('message', (msg) => { + if (msg.data === 'ack') { + sendData(); + } + }); +})