From 4816c9f4cf8c8f696013b72bb22eba80f8a7f2da Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 19 Sep 2023 18:31:30 -0400 Subject: [PATCH 01/36] Import orizentic --- orizentic/CODE_OF_CONDUCT.md | 74 +++++ orizentic/CONTRIBUTORS | 4 + orizentic/Cargo.lock | 446 ++++++++++++++++++++++++++++ orizentic/Cargo.toml | 39 +++ orizentic/LICENSE | 30 ++ orizentic/readme.md | 73 +++++ orizentic/shell.nix | 20 ++ orizentic/src/bin.rs | 251 ++++++++++++++++ orizentic/src/core.rs | 303 +++++++++++++++++++ orizentic/src/filedb.rs | 37 +++ orizentic/src/lib.rs | 30 ++ orizentic/tests/integration_test.rs | 429 ++++++++++++++++++++++++++ 12 files changed, 1736 insertions(+) create mode 100644 orizentic/CODE_OF_CONDUCT.md create mode 100644 orizentic/CONTRIBUTORS create mode 100644 orizentic/Cargo.lock create mode 100644 orizentic/Cargo.toml create mode 100644 orizentic/LICENSE create mode 100644 orizentic/readme.md create mode 100644 orizentic/shell.nix create mode 100644 orizentic/src/bin.rs create mode 100644 orizentic/src/core.rs create mode 100644 orizentic/src/filedb.rs create mode 100644 orizentic/src/lib.rs create mode 100644 orizentic/tests/integration_test.rs diff --git a/orizentic/CODE_OF_CONDUCT.md b/orizentic/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..6a39a9a --- /dev/null +++ b/orizentic/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +education, socio-economic status, nationality, personal appearance, race, +religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at savanni@luminescent-dreams.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + diff --git a/orizentic/CONTRIBUTORS b/orizentic/CONTRIBUTORS new file mode 100644 index 0000000..b3610d3 --- /dev/null +++ b/orizentic/CONTRIBUTORS @@ -0,0 +1,4 @@ +* [Savanni D'Gerinel](http://github.com/savannidgerinel) +* [Daria Phoebe Brasea](http://github.com/dariaphoebe) +* [Aria Stewart](http://github.com/aredridel) + diff --git a/orizentic/Cargo.lock b/orizentic/Cargo.lock new file mode 100644 index 0000000..6d7130f --- /dev/null +++ b/orizentic/Cargo.lock @@ -0,0 +1,446 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + +[[package]] +name = "atty" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc4a1aa4c24c0718a250f0681885c1af91419d242f29eb8f2ab28502d80dbd1" +dependencies = [ + "libc", + "termion", + "winapi", +] + +[[package]] +name = "base64" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85415d2594767338a74a30c1d370b2f3262ec1b4ed2d7bba5b3faf4de40467d9" +dependencies = [ + "byteorder", + "safemem", +] + +[[package]] +name = "bitflags" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0c54bb8f454c567f21197eefcdbf5679d0bd99f2ddbe52e84c77061952e6789" + +[[package]] +name = "byteorder" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74c0b906e9446b0a2e4f760cdb3fa4b2c48cdc6db8766a845c54b6ff063fd2e9" + +[[package]] +name = "cc" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9ce8bb087aacff865633f0bd5aeaed910fe2fe55b55f4739527f2e023a2e53d" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6962c635d530328acc53ac6a955e83093fedc91c5809dfac1fa60fa470830a37" +dependencies = [ + "num-integer", + "num-traits", + "serde", + "time", +] + +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "dtoa" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab" + +[[package]] +name = "either" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be565ca5c557d7f59e7cfcf1844f9e3033650c929c6566f511e8005f205c1d0" + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "itertools" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069bbec61e1ca5a596166e55dfe4773ff745c3d16b700013bcaff9a6df2c682" + +[[package]] +name = "jsonwebtoken" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d438ea707d465c230305963b67f8357a1d56fcfad9434797d7cb1c46c2e41df" +dependencies = [ + "base64", + "chrono", + "ring", + "serde", + "serde_derive", + "serde_json", + "untrusted", +] + +[[package]] +name = "lazy_static" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14" + +[[package]] +name = "libc" +version = "0.2.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6" + +[[package]] +name = "linked-hash-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70fb39025bc7cdd76305867c4eccf2f2dcf6e9a57f5b21a93e1c2d86cd03ec9e" + +[[package]] +name = "num-integer" +version = "0.1.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83d528d2677f0518c570baf2b7abdcf0cd2d248860b68507bdcb3e91d4c0cea" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630de1ef5cc79d0cdd78b7e33b81f083cbfe90de0f4b2b2f07f905867c70e9fe" + +[[package]] +name = "orizentic" +version = "1.0.1" +dependencies = [ + "chrono", + "clap", + "itertools", + "jsonwebtoken", + "serde", + "serde_derive", + "serde_json", + "thiserror", + "uuid", + "version_check", + "yaml-rust", +] + +[[package]] +name = "proc-macro2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effdb53b25cdad54f8f48843d67398f7ef2e14f12c1b4cb4effc549a6462a4d6" +dependencies = [ + "unicode-xid 0.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d" +dependencies = [ + "unicode-xid 0.2.2", +] + +[[package]] +name = "quote" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e44651a0dc4cdd99f71c83b561e221f714912d11af1a4dff0631f923d53af035" +dependencies = [ + "proc-macro2 0.4.6", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2 1.0.29", +] + +[[package]] +name = "redox_syscall" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c214e91d3ecf43e9a4e41e578973adeb14b474f2bee858742d127af75a0112b1" + +[[package]] +name = "redox_termios" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" +dependencies = [ + "redox_syscall", +] + +[[package]] +name = "ring" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4db68a2e35f3497146b7e4563df7d4773a2433230c5e4b448328e31740458a" +dependencies = [ + "cc", + "lazy_static", + "libc", + "untrusted", +] + +[[package]] +name = "safemem" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f" + +[[package]] +name = "serde" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "210e5a3b159c566d7527e9b22e44be73f2e0fcc330bb78fef4dbccb56d2e74c8" + +[[package]] +name = "serde_derive" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd724d68017ae3a7e63600ee4b2fdb3cad2158ffd1821d44aff4580f63e2b593" +dependencies = [ + "proc-macro2 0.4.6", + "quote 0.6.3", + "syn 0.14.4", +] + +[[package]] +name = "serde_json" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84b8035cabe9b35878adec8ac5fe03d5f6bc97ff6edd7ccb96b44c1276ba390e" +dependencies = [ + "dtoa", + "itoa", + "serde", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "syn" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2beff8ebc3658f07512a413866875adddd20f4fd47b2a4e6c9da65cd281baaea" +dependencies = [ + "proc-macro2 0.4.6", + "quote 0.6.3", + "unicode-xid 0.1.0", +] + +[[package]] +name = "syn" +version = "1.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5239bc68e0fef57495900cfea4e8dc75596d9a319d7e16b1e0a440d24e6fe0a0" +dependencies = [ + "proc-macro2 1.0.29", + "quote 1.0.9", + "unicode-xid 0.2.2", +] + +[[package]] +name = "termion" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" +dependencies = [ + "libc", + "redox_syscall", + "redox_termios", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602eca064b2d83369e2b2f34b09c70b605402801927c65c11071ac911d299b88" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad553cc2c78e8de258400763a647e80e6d1b31ee237275d756f6836d204494c" +dependencies = [ + "proc-macro2 1.0.29", + "quote 1.0.9", + "syn 1.0.77", +] + +[[package]] +name = "time" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d825be0eb33fda1a7e68012d51e9c7f451dc1a69391e7fdc197060bb8c56667b" +dependencies = [ + "libc", + "redox_syscall", + "winapi", +] + +[[package]] +name = "unicode-width" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526" + +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "untrusted" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cd1f4b4e96b46aeb8d4855db4a7a9bd96eeeb5c6a1ab54593328761642ce2f" + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom", + "serde", +] + +[[package]] +name = "vec_map" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" + +[[package]] +name = "version_check" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "winapi" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773ef9dcc5f24b7d850d0ff101e542ff24c3b090a9768e03ff889fdef41f00fd" +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-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "yaml-rust" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ab38ee1a4a266ed033496cf9af1828d8d6e6c1cfa5f643a2809effcae4d628" +dependencies = [ + "linked-hash-map", +] diff --git a/orizentic/Cargo.toml b/orizentic/Cargo.toml new file mode 100644 index 0000000..c76b14c --- /dev/null +++ b/orizentic/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "orizentic" +version = "1.0.1" +authors = ["Savanni D'Gerinel "] +description = "A library for inerfacing with a JWT auth token database and a command line tool for managing it." +license = "GPL3" +documentation = "https://docs.rs/orizentic" +homepage = "https://github.com/luminescent-dreams/orizentic" +repository = "https://github.com/luminescent-dreams/orizentic" +categories = ["authentication", "command-line-utilities"] + +include = [ + "**/*.rs", + "Cargo.toml", + "build.rs", +] + +[build-dependencies] +version_check = "0.1.5" + +[dependencies] +chrono = { version = "0.4", features = ["serde"] } +clap = "2.33" +itertools = "0.10" +jsonwebtoken = "5" +serde = "1" +serde_derive = "1" +serde_json = "1" +thiserror = "1" +uuid = { version = "0.8", features = ["v4", "serde"] } +yaml-rust = "0.4" + +[lib] +name = "orizentic" +path = "src/lib.rs" + +[[bin]] +name = "orizentic" +path = "src/bin.rs" diff --git a/orizentic/LICENSE b/orizentic/LICENSE new file mode 100644 index 0000000..1704a41 --- /dev/null +++ b/orizentic/LICENSE @@ -0,0 +1,30 @@ +Copyright Savanni D'Gerinel (c) 2017 - 2019 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Savanni D'Gerinel nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/orizentic/readme.md b/orizentic/readme.md new file mode 100644 index 0000000..816c4f7 --- /dev/null +++ b/orizentic/readme.md @@ -0,0 +1,73 @@ +# Orizentic + +[![CircleCI](https://circleci.com/gh/luminescent-dreams/orizentic/tree/sol.svg?style=svg)](https://circleci.com/gh/luminescent-dreams/orizentic/tree/sol) + +[Documentation](https://docs.rs/orizentic") + +Orizentic provides a library that streamlines token-based authentication, and a CLI tool for maintaining a database of tokens. + +## Credit + +The name is a contraction of Auth(oriz)ation/Auth(entic)ation, and credit goes to [Daria Phoebe Brashear](https://github.com/dariaphoebe). + +The original idea has been debated online for many years, but the push to make this useful comes from [Aria Stewart](https://github.com/aredridel). + +## Tokens + +Tokens are simple [JWTs](https://jwt.io/). This library simplifies the process by easily generating and checking JWTs that have only an issuer, an optional time-to-live, a resource name, a username, and a list of permissions. A typical resulting JWT would look like this: + + { iss = Savanni + , sub = health + , aud = "Savanni Desktop" + , exp = null + , nbf = null + , iat = 1499650083 + , jti = 9d57a8d8-d11e-43b2-a4d6-7b82ad043994 + , unregisteredClaims = { perms: [ "read", "write" ] } + } + +The `issuer` and `audience` (or username) are almost entirely for human readability. In this instance, I issued a token that was intended to be used on my desktop system. + +The `subject` in this case is synonymous with Resource and is a name for the resource for which access is being granted. Permissions are a simple list of freeform strings. Both of these are flexible within your application and your authorization checks will use them to verify that the token can be used for the specified purpose. + +## CLI Usage + +## Library Usage + +[orizentic - Rust](https://docs.rs/orizentic/1.0.0/orizentic/) + +There are multiple errata for the documentation: + +* There are, in fact, now [two functions](https://docs.rs/orizentic/1.0.0/orizentic/filedb/index.html) for saving and loading a database. +* An example for how to use the library is currently here [for loading the database](https://github.com/luminescent-dreams/fitnesstrax/blob/8c9f3f418ff75675874f7a8e3928ad3f7d134eb4/server/src/web.rs#L64) and here [as part of the AuthMiddleware for an Iron server](https://github.com/luminescent-dreams/fitnesstrax/blob/8c9f3f418ff75675874f7a8e3928ad3f7d134eb4/server/src/server.rs#L156). I apologize for not writing this in more detail yet. + +## Language support + +This library and application is only supported for Rust. Haskell and Go support has been discontinued, but can be revived if I discover folks have an interest. The token database is compatible across tools. See readmes in the language directory for usage information. + +Future Haskell, Go, and other language versions of the library will be done through language bindings against the Rust utilities instead of through my previous clean-room re-implementations. + +## Nix installation + +If you have Nix installed on your system, or you run NixOS, create this derivation: + +orizentic.nix: + +``` +{ fetchFromGitHub }: +let src = fetchFromGitHub { + owner = "luminescent-dreams"; + repo = "orizentic"; + rev = "896140f594fe3c106662ffe2550f289bb68bc0cb"; + sha256 = "05g7b0jiyy0pv74zf89yikf65vi3jrn1da0maj0k9fxnxb2vv7a4"; + }; +in import "${src}/default.nix" {} +``` + +At this time, you must have nixpkgs-19.03 defined (and preferably pointing to the 19.03 channel). I will parameterize this and update the instructions in the future. + +I import this into my shell.nix `with import ./orizentic.nix { inherit (pkgs) fetchFromGitHub; };`. + +For a complete example, see my [shell.nix](https://github.com/savannidgerinel/nix-shell/blob/sol/shell.nix) file. + +I have not bundled this application for any other distribution, but you should nave no trouble just building with just cargo build --release with Rust-1.33 and Cargo. diff --git a/orizentic/shell.nix b/orizentic/shell.nix new file mode 100644 index 0000000..7d40cd1 --- /dev/null +++ b/orizentic/shell.nix @@ -0,0 +1,20 @@ +let + rust_overlay = import (builtins.fetchTarball "https://github.com/oxalica/rust-overlay/archive/master.tar.gz"); + pkgs = import { overlays = [ rust_overlay ]; }; + unstable = import {}; + rust = pkgs.rust-bin.stable."1.59.0".default.override { + extensions = [ "rust-src" ]; + }; + +in pkgs.mkShell { + name = "datasphere"; + + nativeBuildInputs = [ + rust + unstable.rust-analyzer + ]; + + shellHook = '' + if [ -e ~/.nixpkgs/shellhook.sh ]; then . ~/.nixpkgs/shellhook.sh; fi + ''; +} diff --git a/orizentic/src/bin.rs b/orizentic/src/bin.rs new file mode 100644 index 0000000..f465b09 --- /dev/null +++ b/orizentic/src/bin.rs @@ -0,0 +1,251 @@ +extern crate chrono; +extern crate clap; +extern crate orizentic; + +use chrono::Duration; +use clap::{App, Arg, ArgMatches, SubCommand}; +use std::env; + +use orizentic::*; + +#[derive(Debug)] +enum OrizenticErr { + ParseError(std::num::ParseIntError), +} + +// ORIZENTIC_DB +// ORIZENTIC_SECRET +// +// list +// create +// revoke +// encode +pub fn main() { + let db_path = env::var_os("ORIZENTIC_DB").map(|str| { + str.into_string() + .expect("ORIZENTIC_DB contains invalid Unicode sequences") + }); + let secret = env::var_os("ORIZENTIC_SECRET").map(|str| { + Secret( + str.into_string() + .map(|s| s.into_bytes()) + .expect("ORIZENTIC_SECRET contains invalid Unicode sequences"), + ) + }); + + let matches = App::new("orizentic cli") + .subcommand(SubCommand::with_name("list")) + .subcommand( + SubCommand::with_name("create") + .arg( + Arg::with_name("issuer") + .long("issuer") + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name("ttl") + .long("ttl") + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name("resource") + .long("resource") + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name("username") + .long("username") + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name("perms") + .long("perms") + .takes_value(true) + .required(true), + ), + ) + .subcommand( + SubCommand::with_name("revoke").arg( + Arg::with_name("id") + .long("id") + .takes_value(true) + .required(true), + ), + ) + .subcommand( + SubCommand::with_name("encode").arg( + Arg::with_name("id") + .long("id") + .takes_value(true) + .required(true), + ), + ) + .get_matches(); + + match matches.subcommand() { + ("list", _) => list_tokens(db_path), + ("create", Some(args)) => create_token(db_path, secret, args), + ("revoke", Some(args)) => revoke_token(db_path, args), + ("encode", Some(args)) => encode_token(db_path, secret, args), + (cmd, _) => { + println!("unknown subcommand: {}", cmd); + } + } +} + +fn list_tokens(db_path: Option) { + let db_path_ = db_path.expect("ORIZENTIC_DB is required for this operation"); + let claimsets = orizentic::filedb::load_claims_from_file(&db_path_); + match claimsets { + Ok(claimsets_) => { + for claimset in claimsets_ { + println!("[{}]", claimset.id); + println!("Audience: {}", String::from(claimset.audience)); + match claimset.expiration { + Some(expiration) => println!( + "Expiration: {}", + expiration.format("%Y-%m-%d %H:%M:%S") + ), + None => println!("Expiration: None"), + } + println!("Issuer: {}", claimset.issuer.0); + println!( + "Issued At: {}", + claimset.issued_at.format("%Y-%m-%d %H:%M:%S") + ); + println!("Resource Name: {}", claimset.resource.0); + + let perm_val: String = itertools::Itertools::intersperse( + claimset.permissions.0.clone().into_iter(), + String::from(", "), + ) + .collect(); + println!("Permissions: {}", perm_val); + println!("") + } + } + Err(err) => { + println!("claimset failed to load: {}", err); + std::process::exit(1); + } + } +} + +fn create_token(db_path: Option, secret: Option, args: &ArgMatches) { + let db_path_ = db_path.expect("ORIZENTIC_DB is required for this operation"); + let secret_ = secret.expect("ORIZENTIC_SECRET is required for this operation"); + let issuer = args + .value_of("issuer") + .map(|x| Issuer(String::from(x))) + .expect("--issuer is a required parameter"); + let ttl: Option = args.value_of("ttl").map(|x| { + x.parse() + .and_then(|d| Ok(TTL(Duration::seconds(d)))) + .map_err(|err| OrizenticErr::ParseError(err)) + .expect("Failed to parse TTL") + }); + let resource_name = args + .value_of("resource") + .map(|x| ResourceName(String::from(x))) + .expect("--resource is a required parameter"); + let username = args + .value_of("username") + .map(|x| Username::from(x)) + .expect("--username is a required parameter"); + let perms: Permissions = args + .value_of("perms") + .map(|str| Permissions(str.split(',').map(|s| String::from(s)).collect())) + .expect("--permissions is a required parameter"); + + let claimsets = orizentic::filedb::load_claims_from_file(&db_path_); + match claimsets { + Err(err) => { + println!("claimset failed to load: {}", err); + std::process::exit(1); + } + Ok(claimsets_) => { + let new_claimset = ClaimSet::new(issuer, ttl, resource_name, username, perms); + let mut ctx = orizentic::OrizenticCtx::new(secret_, claimsets_); + ctx.add_claimset(new_claimset.clone()); + match orizentic::filedb::save_claims_to_file(&ctx.list_claimsets(), &db_path_) { + Err(err) => { + println!("Failed to write claimset to file: {:?}", err); + std::process::exit(1); + } + Ok(_) => match ctx.encode_claimset(&new_claimset) { + Ok(token) => println!("{}", token.text), + Err(err) => { + println!("token could not be encoded: {:?}", err); + std::process::exit(1); + } + }, + } + } + } +} + +fn revoke_token(db_path: Option, args: &ArgMatches) { + let db_path_ = db_path.expect("ORIZENTIC_DB is required for this operation"); + let claimsets = orizentic::filedb::load_claims_from_file(&db_path_); + + match claimsets { + Err(err) => { + println!("claimset failed to load: {}", err); + std::process::exit(1); + } + Ok(claimsets_) => { + let id = args + .value_of("id") + .map(String::from) + .expect("--id is a required parameter"); + let mut ctx = + orizentic::OrizenticCtx::new(Secret(String::from("").into_bytes()), claimsets_); + ctx.revoke_by_uuid(&id); + match orizentic::filedb::save_claims_to_file(&ctx.list_claimsets(), &db_path_) { + Err(err) => { + println!("Failed to write claimset to file: {:?}", err); + std::process::exit(1); + } + Ok(_) => {} + } + } + } +} + +fn encode_token(db_path: Option, secret: Option, args: &ArgMatches) { + let db_path_ = db_path.expect("ORIZENTIC_DB is required for this operation"); + let secret_ = secret.expect("ORIZENTIC_SECRET is required for this operation"); + let id = args + .value_of("id") + .map(String::from) + .expect("--id is a required parameter"); + + let claimsets = orizentic::filedb::load_claims_from_file(&db_path_); + match claimsets { + Err(err) => { + println!("claimset failed to load: {}", err); + std::process::exit(1); + } + Ok(claimsets_) => { + let ctx = orizentic::OrizenticCtx::new(secret_, claimsets_); + let claimset = ctx.find_claimset(&id); + match claimset { + Some(claimset_) => match ctx.encode_claimset(&claimset_) { + Ok(token) => println!("{}", token.text), + Err(err) => { + println!("token could not be encoded: {:?}", err); + std::process::exit(1); + } + }, + None => { + println!("No claimset found"); + std::process::exit(1); + } + } + } + } +} diff --git a/orizentic/src/core.rs b/orizentic/src/core.rs new file mode 100644 index 0000000..596458c --- /dev/null +++ b/orizentic/src/core.rs @@ -0,0 +1,303 @@ +extern crate chrono; +extern crate jsonwebtoken as jwt; +extern crate serde; +extern crate serde_json; +extern crate uuid; +extern crate yaml_rust; + +use core::chrono::prelude::*; +use core::uuid::Uuid; +use std::collections::HashMap; +use thiserror::Error; + +/// Orizentic Errors +#[derive(Debug, Error)] +pub enum Error { + /// An underlying JWT decoding error. May be replaced with Orizentic semantic errors to better + /// encapsulate the JWT library. + #[error("JWT failed to decode: {0}")] + JWTError(jwt::errors::Error), + /// Token decoded and verified but was not present in the database. + #[error("Token not recognized")] + UnknownToken, +} + +/// ResourceName is application-defined and names a resource to which access should be controlled +#[derive(Debug, PartialEq, Clone)] +pub struct ResourceName(pub String); + +/// Permissions are application-defined descriptions of what can be done with the named resource +#[derive(Debug, PartialEq, Clone)] +pub struct Permissions(pub Vec); + +/// Issuers are typically informative, but should generally describe who or what created the token +#[derive(Debug, PartialEq, Clone)] +pub struct Issuer(pub String); + +/// Time to live is the number of seconds until a token expires. This is used for creating tokens +/// but tokens store their actual expiration time. +#[derive(Debug, PartialEq, Clone)] +pub struct TTL(pub chrono::Duration); + +/// Username, or Audience in JWT terms, should describe who or what is supposed to be using this +/// token +#[derive(Debug, PartialEq, Clone)] +pub struct Username(String); + +impl From for String { + fn from(u: Username) -> String { + u.0.clone() + } +} + +impl From<&str> for Username { + fn from(s: &str) -> Username { + Username(s.to_owned()) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Secret(pub Vec); + +/// A ClaimSet represents one set of permissions and claims. It is a standardized way of specifying +/// the owner, issuer, expiration time, relevant resources, and specific permissions on that +/// resource. By itself, this is only an informative data structure and so should never be trusted +/// when passed over the wire. See `VerifiedToken` and `UnverifiedToken`. +#[derive(Debug, PartialEq, Clone)] +pub struct ClaimSet { + pub id: String, + pub audience: Username, + pub expiration: Option>, + pub issuer: Issuer, + pub issued_at: DateTime, + pub resource: ResourceName, + pub permissions: Permissions, +} + +impl ClaimSet { + /// Create a new `ClaimSet`. This will return a claimset with the expiration time calculated + /// from the TTL if the TTL is provided. No expiration will be set if no TTL is provided. + pub fn new( + issuer: Issuer, + ttl: Option, + resource_name: ResourceName, + user_name: Username, + perms: Permissions, + ) -> ClaimSet { + let issued_at: DateTime = Utc::now().with_nanosecond(0).unwrap(); + let expiration = match ttl { + Some(TTL(ttl_)) => issued_at.checked_add_signed(ttl_), + None => None, + }; + ClaimSet { + id: String::from(Uuid::new_v4().to_hyphenated().to_string()), + audience: user_name, + expiration, + issuer, + issued_at, + resource: resource_name, + permissions: perms, + } + } + + pub fn to_json(&self) -> Result { + serde_json::to_string(&(ClaimSetJS::from_claimset(self))) + } + + pub fn from_json(text: &String) -> Result { + serde_json::from_str(&text).map(|x| ClaimSetJS::to_claimset(&x)) + } +} + +/// ClaimSetJS is an intermediary data structure between JWT serialization and a more usable +/// ClaimSet. +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct ClaimSetJS { + jti: String, + aud: String, + exp: Option, + iss: String, + iat: i64, + sub: String, + perms: Vec, +} + +impl ClaimSetJS { + pub fn from_claimset(claims: &ClaimSet) -> ClaimSetJS { + ClaimSetJS { + jti: claims.id.clone(), + aud: claims.audience.0.clone(), + exp: claims.expiration.map(|t| t.timestamp()), + iss: claims.issuer.0.clone(), + iat: claims.issued_at.timestamp(), + sub: claims.resource.0.clone(), + perms: claims.permissions.0.clone(), + } + } + + pub fn to_claimset(&self) -> ClaimSet { + ClaimSet { + id: self.jti.clone(), + audience: Username(self.aud.clone()), + expiration: self.exp.map(|t| Utc.timestamp(t, 0)), + issuer: Issuer(self.iss.clone()), + issued_at: Utc.timestamp(self.iat, 0), + resource: ResourceName(self.sub.clone()), + permissions: Permissions(self.perms.clone()), + } + } +} + +/// The Orizentic Context encapsulates a set of claims and an associated secret. This provides the +/// overall convenience of easily creating and validating tokens. Generated claimsets are stored +/// here on the theory that, even with validation, only those claims actually stored in the +/// database should be considered valid. +pub struct OrizenticCtx(Secret, HashMap); + +/// An UnverifiedToken is a combination of the JWT serialization and the decoded `ClaimSet`. As this +/// is unverified, this should only be used for information purposes, such as determining what a +/// user can do with a token even when the decoding key is absent. +#[derive(Debug)] +pub struct UnverifiedToken { + pub text: String, + pub claims: ClaimSet, +} + +impl UnverifiedToken { + /// Decode a JWT text string without verification + pub fn decode_text(text: String) -> Result { + let res = jwt::dangerous_unsafe_decode::(&text); + match res { + Ok(res_) => Ok(UnverifiedToken { + text, + claims: res_.claims.to_claimset(), + }), + Err(err) => Err(Error::JWTError(err)), + } + } +} + +/// An VerifiedToken is a combination of the JWT serialization and the decoded `ClaimSet`. This will +/// only be created by the `validate_function`, and thus will represent a token which has been +/// validated via signature, expiration time, and presence in the database. +#[derive(Debug)] +pub struct VerifiedToken { + pub text: String, + pub claims: ClaimSet, +} + +impl VerifiedToken { + /// Given a `VerifiedToken`, pass the resource name and permissions to a user-defined function. The + /// function should return true if the caller should be granted access to the resource and false, + /// otherwise. That result will be passed back to the caller. + pub fn check_authorizations bool>( + &self, + f: F, + ) -> bool { + f(&self.claims.resource, &self.claims.permissions) + } +} + +impl OrizenticCtx { + /// Create a new Orizentic Context with an initial set of claims. + pub fn new(secret: Secret, claims_lst: Vec) -> OrizenticCtx { + let mut hm = HashMap::new(); + for claimset in claims_lst { + hm.insert(claimset.id.clone(), claimset); + } + OrizenticCtx(secret, hm) + } + + /// Validate a token by checking its signature, that it is not expired, and that it is still + /// present in the database. Return an error if any check fails, but return a `VerifiedToken` + /// if it all succeeds. + pub fn validate_token(&self, token: &UnverifiedToken) -> Result { + let validator = match token.claims.expiration { + Some(_) => jwt::Validation::default(), + None => jwt::Validation { + validate_exp: false, + ..jwt::Validation::default() + }, + }; + let res = jwt::decode::(&token.text, &(self.0).0, &validator); + match res { + Ok(res_) => { + let claims = res_.claims; + let in_db = self.1.get(&claims.jti); + if in_db.is_some() { + Ok(VerifiedToken { + text: token.text.clone(), + claims: claims.to_claimset(), + }) + } else { + Err(Error::UnknownToken) + } + } + Err(err) => Err(Error::JWTError(err)), + } + } + + /// Given a text string, as from a web application's `Authorization` header, decode the string + /// and then validate the token. + pub fn decode_and_validate_text(&self, text: String) -> Result { + // it is necessary to first decode the token because we need the validator to know whether + // to attempt to validate the expiration. Without that check, the validator will fail any + // expiration set to None. + match UnverifiedToken::decode_text(text) { + Ok(unverified) => self.validate_token(&unverified), + Err(err) => Err(err), + } + } + + /// Add a claimset to the database. + pub fn add_claimset(&mut self, claimset: ClaimSet) { + self.1.insert(claimset.id.clone(), claimset); + } + + /// Remove a claims set from the database so that all additional validation checks fail. + pub fn revoke_claimset(&mut self, claim: &ClaimSet) { + self.1.remove(&claim.id); + } + + /// Revoke a ClaimsSet given its ID, which is set in the `jti` claim of a JWT or the `id` field + /// of a `ClaimSet`. + pub fn revoke_by_uuid(&mut self, claim_id: &String) { + self.1.remove(claim_id); + } + + /// *NOT IMPLEMENTED* + pub fn replace_claimsets(&mut self, _claims_lst: Vec) { + unimplemented!() + } + + /// List all of the `ClaimSet` IDs in the database. + pub fn list_claimsets(&self) -> Vec<&ClaimSet> { + self.1.values().map(|item| item).collect() + } + + /// Find a `ClaimSet` by ID. + pub fn find_claimset(&self, claims_id: &String) -> Option<&ClaimSet> { + self.1.get(claims_id) + } + + /// Encode and sign a claimset, returning the result as a `VerifiedToken`. + pub fn encode_claimset(&self, claims: &ClaimSet) -> Result { + let in_db = self.1.get(&claims.id); + if in_db.is_some() { + let text = jwt::encode( + &jwt::Header::default(), + &ClaimSetJS::from_claimset(&claims), + &(self.0).0, + ); + match text { + Ok(text_) => Ok(VerifiedToken { + text: text_, + claims: claims.clone(), + }), + Err(err) => Err(Error::JWTError(err)), + } + } else { + Err(Error::UnknownToken) + } + } +} diff --git a/orizentic/src/filedb.rs b/orizentic/src/filedb.rs new file mode 100644 index 0000000..3986ac6 --- /dev/null +++ b/orizentic/src/filedb.rs @@ -0,0 +1,37 @@ +extern crate serde_json; + +use core; + +use std::fs::File; +use std::path::Path; +use std::io::{Read, Error, Write}; + +pub fn save_claims_to_file(claimsets: &Vec<&core::ClaimSet>, path: &String) -> Result<(), Error> { + let path = Path::new(path); + let mut file = File::create(&path)?; + + let claimsets_js: Vec = claimsets + .into_iter() + .map(|claims| core::ClaimSetJS::from_claimset(claims)) + .collect(); + let claimset_str = serde_json::to_string(&claimsets_js)?; + file.write_fmt(format_args!("{}", claimset_str))?; + + Ok(()) +} + +pub fn load_claims_from_file(path: &String) -> Result, Error> { + let path = Path::new(path); + let mut file = File::open(&path)?; + let mut text = String::new(); + + file.read_to_string(&mut text)?; + + let claimsets_js: Vec = serde_json::from_str(&text)?; + let claimsets = claimsets_js + .into_iter() + .map(|cl_js| core::ClaimSetJS::to_claimset(&cl_js)) + .collect(); + + Ok(claimsets) +} diff --git a/orizentic/src/lib.rs b/orizentic/src/lib.rs new file mode 100644 index 0000000..fe966d1 --- /dev/null +++ b/orizentic/src/lib.rs @@ -0,0 +1,30 @@ +//! The Orizentic token management library +//! +//! This library provides a high level interface for authentication token management. It wraps +//! around the [JWT](https://jwt.io/) standard using the +//! [`jsonwebtoken`](https://github.com/Keats/jsonwebtoken) library for serialization and +//! validation. +//! +//! Functionality revolves around the relationship between a [ClaimSet](struct.ClaimSet.html), a +//! [VerifiedToken](struct.VerifiedToken.html), and an +//! [UnverifiedToken](struct.UnverifiedToken.html). A [ClaimSet](struct.ClaimSet.html) is +//! considered informative and stores all of the information about the permissions and resources +//! that the token bearer should have access to. [VerifiedToken](struct.VerifiedToken.html) and +//! [UnverifiedToken](struct.UnverifiedToken.html) are the result of the process of decoding a +//! string JWT, and inherently specify whether the decoding process verified the signature, +//! expiration time, and presence in the database. +//! +//! This library does not currently contain database save and load features, but those are a likely +//! upcoming feature. +//! +//! No setup is necessary when using this library to decode JWT strings. Refer to the standalone +//! [decode_text](fn.decode_text.html) function. + +#[macro_use] +extern crate serde_derive; +extern crate thiserror; + +pub use core::*; + +mod core; +pub mod filedb; diff --git a/orizentic/tests/integration_test.rs b/orizentic/tests/integration_test.rs new file mode 100644 index 0000000..da62b55 --- /dev/null +++ b/orizentic/tests/integration_test.rs @@ -0,0 +1,429 @@ +extern crate chrono; +extern crate orizentic; + +use orizentic::filedb::*; +use orizentic::*; +use std::fs; +use std::ops; +use std::thread; +use std::time; + +struct FileCleanup(String); + +impl FileCleanup { + fn new(path: &str) -> FileCleanup { + FileCleanup(String::from(path)) + } +} + +impl ops::Drop for FileCleanup { + fn drop(&mut self) { + fs::remove_file(&self.0).expect("failed to remove time series file"); + } +} + +#[test] +fn can_create_a_new_claimset() { + let mut ctx = OrizenticCtx::new(Secret("abcdefg".to_string().into_bytes()), Vec::new()); + let claims = ClaimSet::new( + Issuer(String::from("test")), + Some(TTL(chrono::Duration::seconds(3600))), + ResourceName(String::from("resource-1")), + Username::from("Savanni"), + Permissions(vec![ + String::from("read"), + String::from("write"), + String::from("grant"), + ]), + ); + ctx.add_claimset(claims.clone()); + assert_eq!(claims.audience, Username::from("Savanni")); + match claims.expiration { + Some(ttl) => assert_eq!(ttl - claims.issued_at, chrono::Duration::seconds(3600)), + None => panic!("ttl should not be None"), + } + assert_eq!(claims.issuer, Issuer(String::from("test"))); + assert_eq!(claims.resource, ResourceName(String::from("resource-1"))); + assert_eq!( + claims.permissions, + Permissions(vec![ + String::from("read"), + String::from("write"), + String::from("grant"), + ]) + ); + { + let tok_list = ctx.list_claimsets(); + assert_eq!(tok_list.len(), 1); + assert!(tok_list.contains(&&claims)); + } + + let claims2 = ClaimSet::new( + Issuer(String::from("test")), + Some(TTL(chrono::Duration::seconds(3600))), + ResourceName(String::from("resource-2")), + Username::from("Savanni"), + Permissions(vec![ + String::from("read"), + String::from("write"), + String::from("grant"), + ]), + ); + ctx.add_claimset(claims2.clone()); + + assert_ne!(claims2.id, claims.id); + assert_eq!(claims2.resource, ResourceName(String::from("resource-2"))); + + let tok_list = ctx.list_claimsets(); + assert_eq!(tok_list.len(), 2); + assert!(tok_list.contains(&&claims)); + assert!(tok_list.contains(&&claims2)); +} + +#[test] +fn can_retrieve_claim_by_id() { + let mut ctx = OrizenticCtx::new(Secret("abcdefg".to_string().into_bytes()), Vec::new()); + let claims = ClaimSet::new( + Issuer(String::from("test")), + Some(TTL(chrono::Duration::seconds(3600))), + ResourceName(String::from("resource-1")), + Username::from("Savanni"), + Permissions(vec![ + String::from("read"), + String::from("write"), + String::from("grant"), + ]), + ); + let claims2 = ClaimSet::new( + Issuer(String::from("test")), + Some(TTL(chrono::Duration::seconds(3600))), + ResourceName(String::from("resource-2")), + Username::from("Savanni"), + Permissions(vec![ + String::from("read"), + String::from("write"), + String::from("grant"), + ]), + ); + ctx.add_claimset(claims.clone()); + ctx.add_claimset(claims2.clone()); + + assert_eq!(ctx.find_claimset(&claims.id), Some(&claims)); + assert_eq!(ctx.find_claimset(&claims2.id), Some(&claims2)); + + ctx.revoke_claimset(&claims); + assert_eq!(ctx.find_claimset(&claims.id), None); + assert_eq!(ctx.find_claimset(&claims2.id), Some(&claims2)); +} + +#[test] +fn can_revoke_claim_by_id() { + let mut ctx = OrizenticCtx::new(Secret("abcdefg".to_string().into_bytes()), Vec::new()); + let claims = ClaimSet::new( + Issuer(String::from("test")), + Some(TTL(chrono::Duration::seconds(3600))), + ResourceName(String::from("resource-1")), + Username::from("Savanni"), + Permissions(vec![ + String::from("read"), + String::from("write"), + String::from("grant"), + ]), + ); + let claims2 = ClaimSet::new( + Issuer(String::from("test")), + Some(TTL(chrono::Duration::seconds(3600))), + ResourceName(String::from("resource-2")), + Username::from("Savanni"), + Permissions(vec![ + String::from("read"), + String::from("write"), + String::from("grant"), + ]), + ); + + ctx.add_claimset(claims.clone()); + ctx.add_claimset(claims2.clone()); + + assert_eq!(ctx.find_claimset(&claims.id), Some(&claims)); + assert_eq!(ctx.find_claimset(&claims2.id), Some(&claims2)); + + ctx.revoke_by_uuid(&claims.id); + assert_eq!(ctx.find_claimset(&claims.id), None); + assert_eq!(ctx.find_claimset(&claims2.id), Some(&claims2)); +} + +#[test] +fn can_revoke_a_token() { + let mut ctx = OrizenticCtx::new(Secret("abcdefg".to_string().into_bytes()), Vec::new()); + let claims = ClaimSet::new( + Issuer(String::from("test")), + Some(TTL(chrono::Duration::seconds(3600))), + ResourceName(String::from("resource-1")), + Username::from("Savanni"), + Permissions(vec![ + String::from("read"), + String::from("write"), + String::from("grant"), + ]), + ); + let claims2 = ClaimSet::new( + Issuer(String::from("test")), + Some(TTL(chrono::Duration::seconds(3600))), + ResourceName(String::from("resource-2")), + Username::from("Savanni"), + Permissions(vec![ + String::from("read"), + String::from("write"), + String::from("grant"), + ]), + ); + ctx.add_claimset(claims.clone()); + ctx.add_claimset(claims2.clone()); + + ctx.revoke_claimset(&claims); + let tok_list = ctx.list_claimsets(); + assert_eq!(tok_list.len(), 1); + assert!(!tok_list.contains(&&claims)); + assert!(tok_list.contains(&&claims2)); +} + +#[test] +fn rejects_tokens_with_an_invalid_secret() { + let mut ctx1 = OrizenticCtx::new(Secret("ctx1".to_string().into_bytes()), Vec::new()); + let ctx2 = OrizenticCtx::new(Secret("ctx2".to_string().into_bytes()), Vec::new()); + let claims = ClaimSet::new( + Issuer(String::from("test")), + Some(TTL(chrono::Duration::seconds(3600))), + ResourceName(String::from("resource-1")), + Username::from("Savanni"), + Permissions(vec![ + String::from("read"), + String::from("write"), + String::from("grant"), + ]), + ); + ctx1.add_claimset(claims.clone()); + let encoded_token = ctx1.encode_claimset(&claims).ok().unwrap(); + assert!(ctx2.decode_and_validate_text(encoded_token.text).is_err()); +} + +#[test] +fn rejects_tokens_that_are_absent_from_the_database() { + let mut ctx = OrizenticCtx::new(Secret("ctx".to_string().into_bytes()), Vec::new()); + let claims = ClaimSet::new( + Issuer(String::from("test")), + Some(TTL(chrono::Duration::seconds(3600))), + ResourceName(String::from("resource-1")), + Username::from("Savanni"), + Permissions(vec![ + String::from("read"), + String::from("write"), + String::from("grant"), + ]), + ); + ctx.add_claimset(claims.clone()); + let encoded_token = ctx.encode_claimset(&claims).ok().unwrap(); + + ctx.revoke_claimset(&claims); + assert!(ctx.decode_and_validate_text(encoded_token.text).is_err()); +} + +#[test] +fn validates_present_tokens_with_a_valid_secret() { + let mut ctx = OrizenticCtx::new(Secret("ctx".to_string().into_bytes()), Vec::new()); + let claims = ClaimSet::new( + Issuer(String::from("test")), + Some(TTL(chrono::Duration::seconds(3600))), + ResourceName(String::from("resource-1")), + Username::from("Savanni"), + Permissions(vec![ + String::from("read"), + String::from("write"), + String::from("grant"), + ]), + ); + ctx.add_claimset(claims.clone()); + let encoded_token = ctx.encode_claimset(&claims).ok().unwrap(); + assert!(ctx.decode_and_validate_text(encoded_token.text).is_ok()); +} + +#[test] +fn rejects_expired_tokens() { + let mut ctx = OrizenticCtx::new(Secret("ctx".to_string().into_bytes()), Vec::new()); + let claims = ClaimSet::new( + Issuer(String::from("test")), + Some(TTL(chrono::Duration::seconds(1))), + ResourceName(String::from("resource-1")), + Username::from("Savanni"), + Permissions(vec![ + String::from("read"), + String::from("write"), + String::from("grant"), + ]), + ); + ctx.add_claimset(claims.clone()); + thread::sleep(time::Duration::from_secs(2)); + let encoded_token = ctx.encode_claimset(&claims).ok().unwrap(); + assert!(ctx.decode_and_validate_text(encoded_token.text).is_err()); +} + +#[test] +fn accepts_tokens_that_have_no_expiration() { + let mut ctx = OrizenticCtx::new(Secret("ctx".to_string().into_bytes()), Vec::new()); + let claims = ClaimSet::new( + Issuer(String::from("test")), + None, + ResourceName(String::from("resource-1")), + Username::from("Savanni"), + Permissions(vec![ + String::from("read"), + String::from("write"), + String::from("grant"), + ]), + ); + ctx.add_claimset(claims.clone()); + let encoded_token = ctx.encode_claimset(&claims).ok().unwrap(); + assert!(ctx.decode_and_validate_text(encoded_token.text).is_ok()); +} + +#[test] +fn authorizes_a_token_with_the_correct_resource_and_permissions() { + let mut ctx = OrizenticCtx::new(Secret("ctx".to_string().into_bytes()), Vec::new()); + let claims = ClaimSet::new( + Issuer(String::from("test")), + None, + ResourceName(String::from("resource-1")), + Username::from("Savanni"), + Permissions(vec![ + String::from("read"), + String::from("write"), + String::from("grant"), + ]), + ); + ctx.add_claimset(claims.clone()); + let encoded_token = ctx.encode_claimset(&claims).ok().unwrap(); + let token = ctx + .decode_and_validate_text(encoded_token.text) + .ok() + .unwrap(); + let res = token.check_authorizations(|rn: &ResourceName, perms: &Permissions| { + *rn == ResourceName(String::from("resource-1")) && perms.0.contains(&String::from("grant")) + }); + assert!(res); +} + +#[test] +fn rejects_a_token_with_the_incorrect_permissions() { + let mut ctx = OrizenticCtx::new(Secret("ctx".to_string().into_bytes()), Vec::new()); + let claims = ClaimSet::new( + Issuer(String::from("test")), + None, + ResourceName(String::from("resource-1")), + Username::from("Savanni"), + Permissions(vec![String::from("read"), String::from("write")]), + ); + ctx.add_claimset(claims.clone()); + let encoded_token = ctx.encode_claimset(&claims).ok().unwrap(); + let token = ctx + .decode_and_validate_text(encoded_token.text) + .ok() + .unwrap(); + let res = token.check_authorizations(|rn: &ResourceName, perms: &Permissions| { + *rn == ResourceName(String::from("resource-1")) && perms.0.contains(&String::from("grant")) + }); + assert!(!res); +} + +#[test] +fn rejects_a_token_with_the_incorrect_resource_name() { + let mut ctx = OrizenticCtx::new(Secret("ctx".to_string().into_bytes()), Vec::new()); + let claims = ClaimSet::new( + Issuer(String::from("test")), + None, + ResourceName(String::from("resource-2")), + Username::from("Savanni"), + Permissions(vec![ + String::from("read"), + String::from("write"), + String::from("grant"), + ]), + ); + ctx.add_claimset(claims.clone()); + let encoded_token = ctx.encode_claimset(&claims).ok().unwrap(); + let token = ctx + .decode_and_validate_text(encoded_token.text) + .ok() + .unwrap(); + let res = token.check_authorizations(|rn: &ResourceName, perms: &Permissions| { + *rn == ResourceName(String::from("resource-1")) && perms.0.contains(&String::from("grant")) + }); + assert!(!res); +} + +#[test] +fn claims_serialize_to_json() { + let claims = ClaimSet::new( + Issuer(String::from("test")), + None, + ResourceName(String::from("resource-2")), + Username::from("Savanni"), + Permissions(vec![ + String::from("read"), + String::from("write"), + String::from("grant"), + ]), + ); + + let expected_jti = format!("\"jti\":\"{}\"", claims.id); + + let claim_str = claims.to_json().expect("to_json threw an error"); + //.expect(assert!(false, format!("[claims_serilazie_to_json] {}", err))); + assert!(claim_str.contains(&expected_jti)); + + let claims_ = ClaimSet::from_json(&claim_str).expect("from_json threw an error"); + assert_eq!(claims, claims_); +} + +#[test] +fn save_and_load() { + let _file_cleanup = FileCleanup::new("var/claims.db"); + let mut ctx = OrizenticCtx::new(Secret("ctx".to_string().into_bytes()), Vec::new()); + let claims = ClaimSet::new( + Issuer(String::from("test")), + None, + ResourceName(String::from("resource-2")), + Username::from("Savanni"), + Permissions(vec![ + String::from("read"), + String::from("write"), + String::from("grant"), + ]), + ); + ctx.add_claimset(claims.clone()); + + let claims2 = ClaimSet::new( + Issuer(String::from("test")), + Some(TTL(chrono::Duration::seconds(3600))), + ResourceName(String::from("resource-2")), + Username::from("Savanni"), + Permissions(vec![ + String::from("read"), + String::from("write"), + String::from("grant"), + ]), + ); + ctx.add_claimset(claims2.clone()); + + let res = save_claims_to_file(&ctx.list_claimsets(), &String::from("var/claims.db")); + assert!(res.is_ok()); + + let claimset = load_claims_from_file(&String::from("var/claims.db")); + match claimset { + Ok(claimset_) => { + assert!(claimset_.contains(&claims)); + assert!(claimset_.contains(&claims2)); + } + Err(err) => assert!(false, "{}", err), + } +} -- 2.44.1 From 7077724e15c30c030becacbdf3bec580926129ed Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 19 Sep 2023 18:55:53 -0400 Subject: [PATCH 02/36] Import a questionably refactored version of file-service --- file-service/Cargo.lock | 1404 +++++++++++++++++ file-service/Cargo.toml | 25 + file-service/authdb.json | 1 + file-service/fixtures/.metadata/.placeholder | 0 .../fixtures/.thumbnails/.placeholder | 0 file-service/fixtures/rawr.png | Bin 0 -> 23777 bytes file-service/src/cookies.rs | 61 + file-service/src/lib/error.rs | 31 + file-service/src/lib/file.rs | 151 ++ file-service/src/lib/fileinfo.rs | 160 ++ file-service/src/lib/mod.rs | 56 + file-service/src/lib/thumbnail.rs | 82 + file-service/src/lib/utils.rs | 17 + file-service/src/main.rs | 341 ++++ file-service/src/middleware/authentication.rs | 51 + file-service/src/middleware/logging.rs | 16 + file-service/src/middleware/mod.rs | 6 + file-service/src/middleware/restform.rs | 34 + file-service/templates/file.html | 15 + file-service/templates/index.html | 54 + file-service/templates/script.js | 10 + file-service/templates/style.css | 103 ++ 22 files changed, 2618 insertions(+) create mode 100644 file-service/Cargo.lock create mode 100644 file-service/Cargo.toml create mode 100644 file-service/authdb.json create mode 100644 file-service/fixtures/.metadata/.placeholder create mode 100644 file-service/fixtures/.thumbnails/.placeholder create mode 100644 file-service/fixtures/rawr.png create mode 100644 file-service/src/cookies.rs create mode 100644 file-service/src/lib/error.rs create mode 100644 file-service/src/lib/file.rs create mode 100644 file-service/src/lib/fileinfo.rs create mode 100644 file-service/src/lib/mod.rs create mode 100644 file-service/src/lib/thumbnail.rs create mode 100644 file-service/src/lib/utils.rs create mode 100644 file-service/src/main.rs create mode 100644 file-service/src/middleware/authentication.rs create mode 100644 file-service/src/middleware/logging.rs create mode 100644 file-service/src/middleware/mod.rs create mode 100644 file-service/src/middleware/restform.rs create mode 100644 file-service/templates/file.html create mode 100644 file-service/templates/index.html create mode 100644 file-service/templates/script.js create mode 100644 file-service/templates/style.css diff --git a/file-service/Cargo.lock b/file-service/Cargo.lock new file mode 100644 index 0000000..2e99d09 --- /dev/null +++ b/file-service/Cargo.lock @@ -0,0 +1,1404 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "adler32" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b077b825e468cc974f0020d4082ee6e03132512f207ef1a02fd5d00d1f32d" + +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + +[[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 = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" + +[[package]] +name = "autocfg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" + +[[package]] +name = "base64" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" +dependencies = [ + "byteorder", + "safemem 0.3.3", +] + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +dependencies = [ + "block-padding", + "byte-tools", + "byteorder", + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +dependencies = [ + "byte-tools", +] + +[[package]] +name = "bodyparser" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f023abfa58aad6f6bc4ae0630799e24d5ee0ab8bb2e49f651d9b1f9aa4f52f30" +dependencies = [ + "iron", + "persistent", + "plugin", + "serde 1.0.110", + "serde_json", +] + +[[package]] +name = "buf_redux" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9279646319ff816b05fb5897883ece50d7d854d12b59992683d4f8a71b0f949" +dependencies = [ + "memchr 1.0.2", + "safemem 0.2.0", +] + +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + +[[package]] +name = "bytemuck" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37fa13df2292ecb479ec23aa06f4507928bef07839be9ef15281411076629431" + +[[package]] +name = "byteorder" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" + +[[package]] +name = "cc" +version = "1.0.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "404b1fe4f65288577753b17e3b36a04596ee784493ec249bf81c7f2d2acd751c" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "chrono" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2" +dependencies = [ + "num-integer", + "num-traits", + "serde 1.0.110", + "time", +] + +[[package]] +name = "clap" +version = "2.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdfa80d47f954d53a35a64987ca1422f495b8d6483c0fe9f7117b36c2a792129" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "color_quant" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dbbb57365263e881e805dc77d94697c9118fd94d8da011240555aa7b23445bd" + +[[package]] +name = "crc32fast" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", + "maybe-uninit", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" +dependencies = [ + "autocfg 1.0.0", + "cfg-if", + "crossbeam-utils", + "lazy_static", + "maybe-uninit", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-queue" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "maybe-uninit", +] + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg 1.0.0", + "cfg-if", + "lazy_static", +] + +[[package]] +name = "deflate" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e5d2a2273fed52a7f947ee55b092c4057025d7a3e04e5ecdbd25d6c3fb1bd7" +dependencies = [ + "adler32", + "byteorder", +] + +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "either" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" + +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + +[[package]] +name = "file-service" +version = "0.1.0" +dependencies = [ + "chrono", + "hex-string", + "image", + "iron", + "logger", + "mime 0.3.16", + "mime_guess 2.0.3", + "mustache", + "orizentic", + "params", + "router", + "serde 1.0.110", + "serde_json", + "sha2", + "thiserror", + "uuid 0.4.0", +] + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "generic-array" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec" +dependencies = [ + "typenum", +] + +[[package]] +name = "gif" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471d90201b3b223f3451cd4ad53e34295f16a1df17b1edf3736d47761c3981af" +dependencies = [ + "color_quant", + "lzw", +] + +[[package]] +name = "hermit-abi" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91780f809e750b0a89f5544be56617ff6b1227ee485bcb06ebe10cdf89bd3b71" +dependencies = [ + "libc", +] + +[[package]] +name = "hex-string" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "848ec2dd093df965a34b434580d94852197fc83feac5b2c1962399bbf2cb4f0b" + +[[package]] +name = "httparse" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" + +[[package]] +name = "hyper" +version = "0.10.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a0652d9a2609a968c14be1a9ea00bf4b1d64e2e1f53a1b51b6fff3a6e829273" +dependencies = [ + "base64", + "httparse", + "language-tags", + "log 0.3.9", + "mime 0.2.6", + "num_cpus", + "time", + "traitobject", + "typeable", + "unicase 1.4.2", + "url", +] + +[[package]] +name = "idna" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "image" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d534e95ad8b9d5aa614322d02352b4f1bf962254adcf02ac6f2def8be18498e8" +dependencies = [ + "bytemuck", + "byteorder", + "gif", + "jpeg-decoder", + "num-iter", + "num-rational 0.2.4", + "num-traits", + "png", + "scoped_threadpool", + "tiff", +] + +[[package]] +name = "iron" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6d308ca2d884650a8bf9ed2ff4cb13fbb2207b71f64cda11dc9b892067295e8" +dependencies = [ + "hyper", + "log 0.3.9", + "mime_guess 1.8.8", + "modifier", + "num_cpus", + "plugin", + "typemap", + "url", +] + +[[package]] +name = "itertools" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d47946d458e94a1b7bcabbf6521ea7c037062c81f534615abcad76e84d4970d" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" + +[[package]] +name = "jpeg-decoder" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b47b4c4e017b01abdc5bcc126d2d1002e5a75bbe3ce73f9f4f311a916363704" +dependencies = [ + "byteorder", + "rayon", +] + +[[package]] +name = "jsonwebtoken" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d438ea707d465c230305963b67f8357a1d56fcfad9434797d7cb1c46c2e41df" +dependencies = [ + "base64", + "chrono", + "ring", + "serde 1.0.110", + "serde_derive", + "serde_json", + "untrusted", +] + +[[package]] +name = "language-tags" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" + +[[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.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3baa92041a6fec78c687fa0cc2b3fae8884f743d672cf551bed1d6dac6988d0f" + +[[package]] +name = "linked-hash-map" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" + +[[package]] +name = "log" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" +dependencies = [ + "log 0.4.8", +] + +[[package]] +name = "log" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "logger" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c9172cb4c2f6c52117e25570983edcbb322f130b1031ae5d5d6b1abe7eeb493" +dependencies = [ + "iron", + "log 0.3.9", + "time", +] + +[[package]] +name = "lzw" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084" + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" + +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + +[[package]] +name = "memchr" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" + +[[package]] +name = "memoffset" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4fc2c02a7e374099d4ee95a193111f72d2110197fe200272371758f6c3643d8" +dependencies = [ + "autocfg 1.0.0", +] + +[[package]] +name = "mime" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba626b8a6de5da682e1caa06bdb42a335aee5a84db8e5046a3e8ab17ba0a3ae0" +dependencies = [ + "log 0.3.9", +] + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mime_guess" +version = "1.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216929a5ee4dd316b1702eedf5e74548c123d370f47841ceaac38ca154690ca3" +dependencies = [ + "mime 0.2.6", + "phf", + "phf_codegen", + "unicase 1.4.2", +] + +[[package]] +name = "mime_guess" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" +dependencies = [ + "mime 0.3.16", + "unicase 2.6.0", +] + +[[package]] +name = "miniz_oxide" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" +dependencies = [ + "adler32", +] + +[[package]] +name = "modifier" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f5c9112cb662acd3b204077e0de5bc66305fa8df65c8019d5adb10e9ab6e58" + +[[package]] +name = "multipart" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92f54eb45230c3aa20864ccf0c277eeaeadcf5e437e91731db498dbf7fbe0ec6" +dependencies = [ + "buf_redux", + "httparse", + "log 0.3.9", + "mime 0.2.6", + "mime_guess 1.8.8", + "rand 0.3.23", + "safemem 0.2.0", + "tempdir", + "twoway", +] + +[[package]] +name = "mustache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51956ef1c5d20a1384524d91e616fb44dfc7d8f249bf696d49c97dd3289ecab5" +dependencies = [ + "log 0.3.9", + "serde 1.0.110", +] + +[[package]] +name = "num" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4703ad64153382334aa8db57c637364c322d3372e097840c72000dabdcf6156e" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational 0.1.42", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e63899ad0da84ce718c14936262a41cee2c79c981fc0a0e7c7beb47d5a07e8c1" +dependencies = [ + "num-integer", + "num-traits", + "rand 0.4.6", + "rustc-serialize", +] + +[[package]] +name = "num-complex" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b288631d7878aaf59442cffd36910ea604ecd7745c36054328595114001c9656" +dependencies = [ + "num-traits", + "rustc-serialize", +] + +[[package]] +name = "num-integer" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba" +dependencies = [ + "autocfg 1.0.0", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfb0800a0291891dd9f4fe7bd9c19384f98f7fbe0cd0f39a2c6b88b9868bbc00" +dependencies = [ + "autocfg 1.0.0", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee314c74bd753fc86b4780aa9475da469155f3848473a261d2d18e35245a784e" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", + "rustc-serialize", +] + +[[package]] +name = "num-rational" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +dependencies = [ + "autocfg 1.0.0", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" +dependencies = [ + "autocfg 1.0.0", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + +[[package]] +name = "orizentic" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f4af9d9002d3b129cdad6ebdb5b745b2b7d5e8c1c85c8addd7b447695af6a3" +dependencies = [ + "chrono", + "clap", + "itertools", + "jsonwebtoken", + "serde 1.0.110", + "serde_derive", + "serde_json", + "uuid 0.6.5", + "version_check 0.1.5", + "yaml-rust", +] + +[[package]] +name = "params" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c789fdad2cfdaa551ea0e3a9eadb74c5d634968a9fb3a8c767d89be470d21589" +dependencies = [ + "bodyparser", + "iron", + "multipart", + "num", + "plugin", + "serde_json", + "tempdir", + "urlencoded", +] + +[[package]] +name = "percent-encoding" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" + +[[package]] +name = "persistent" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8fa0009c4f3d350281309909c618abddf10bb7e3145f28410782f6a5ec74c5" +dependencies = [ + "iron", + "plugin", +] + +[[package]] +name = "phf" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b03e85129e324ad4166b06b2c7491ae27fe3ec353af72e72cd1654c7225d517e" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662" +dependencies = [ + "phf_shared", + "rand 0.6.5", +] + +[[package]] +name = "phf_shared" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" +dependencies = [ + "siphasher", + "unicase 1.4.2", +] + +[[package]] +name = "plugin" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a6a0dc3910bc8db877ffed8e457763b317cf880df4ae19109b9f77d277cf6e0" +dependencies = [ + "typemap", +] + +[[package]] +name = "png" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ccdd66f6fe4b2433b07e4728e9a013e43233120427046e93ceb709c3a439bf" +dependencies = [ + "bitflags", + "crc32fast", + "deflate", + "miniz_oxide", +] + +[[package]] +name = "proc-macro2" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53f5ffe53a6b28e37c9c1ce74893477864d64f74778a93a4beb43c8fa167f639" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42934bc9c8ab0d3b273a16d8551c8f0fcff46be73276ca083ec2414c15c4ba5e" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" +dependencies = [ + "libc", + "rand 0.4.6", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "autocfg 0.1.7", + "libc", + "rand_chacha", + "rand_core 0.4.2", + "rand_hc", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg", + "rand_xorshift", + "winapi", +] + +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +dependencies = [ + "autocfg 0.1.7", + "rand_core 0.3.1", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +dependencies = [ + "libc", + "rand_core 0.4.2", + "winapi", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.4.2", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +dependencies = [ + "autocfg 0.1.7", + "rand_core 0.4.2", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rayon" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f02856753d04e03e26929f820d0a0a337ebe71f849801eea335d464b349080" +dependencies = [ + "autocfg 1.0.0", + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e92e15d89083484e11353891f1af602cc661426deb9564c298b270c726973280" +dependencies = [ + "crossbeam-deque", + "crossbeam-queue", + "crossbeam-utils", + "lazy_static", + "num_cpus", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "remove_dir_all" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" +dependencies = [ + "winapi", +] + +[[package]] +name = "ring" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4db68a2e35f3497146b7e4563df7d4773a2433230c5e4b448328e31740458a" +dependencies = [ + "cc", + "lazy_static", + "libc", + "untrusted", +] + +[[package]] +name = "route-recognizer" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea509065eb0b3c446acdd0102f0d46567dc30902dc0be91d6552035d92b0f4f8" + +[[package]] +name = "router" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc63b6f3b8895b0d04e816b2b1aa58fdba2d5acca3cbb8f0ab8e017347d57397" +dependencies = [ + "iron", + "route-recognizer", + "url", +] + +[[package]] +name = "rustc-serialize" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" + +[[package]] +name = "ryu" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3d612bc64430efeb3f7ee6ef26d590dce0c43249217bddc62112540c7941e1" + +[[package]] +name = "safemem" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f" + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b623917345a631dc9608d5194cc206b3fe6c3554cd1c75b937e55e285254af" + +[[package]] +name = "serde" +version = "1.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99e7b308464d16b56eba9964e4972a3eee817760ab60d88c3f86e1fecb08204c" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818fbf6bfa9a42d3bfcaca148547aa00c7b915bec71d1757aa2d44ca68771984" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993948e75b189211a9b31a7528f950c6adc21f9720b6438ff80a7fa2f864cea2" +dependencies = [ + "itoa", + "ryu", + "serde 1.0.110", +] + +[[package]] +name = "sha2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69" +dependencies = [ + "block-buffer", + "digest", + "fake-simd", + "opaque-debug", +] + +[[package]] +name = "siphasher" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" + +[[package]] +name = "smallvec" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7cb5678e1615754284ec264d9bb5b4c27d2018577fd90ac0ceb578591ed5ee4" + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "syn" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1425de3c33b0941002740a420b1a906a350b88d08b82b2c8a01035a3f9447bac" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand 0.4.6", + "remove_dir_all", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tiff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3b8a87c4da944c3f27e5943289171ac71a6150a79ff6bacfff06d159dfff2f" +dependencies = [ + "byteorder", + "lzw", + "miniz_oxide", +] + +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "traitobject" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079" + +[[package]] +name = "twoway" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" +dependencies = [ + "memchr 2.3.3", +] + +[[package]] +name = "typeable" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1410f6f91f21d1612654e7cc69193b0334f909dcf2c790c4826254fbb86f8887" + +[[package]] +name = "typemap" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "653be63c80a3296da5551e1bfd2cca35227e13cdd08c6668903ae2f4f77aa1f6" +dependencies = [ + "unsafe-any", +] + +[[package]] +name = "typenum" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" + +[[package]] +name = "unicase" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4765f83163b74f957c797ad9253caf97f103fb064d3999aea9568d09fc8a33" +dependencies = [ + "version_check 0.1.5", +] + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check 0.9.1", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +dependencies = [ + "matches", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5479532badd04e128284890390c1e876ef7a993d0570b3597ae43dfa1d59afa4" +dependencies = [ + "smallvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" + +[[package]] +name = "unicode-xid" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" + +[[package]] +name = "unsafe-any" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30360d7979f5e9c6e6cea48af192ea8fab4afb3cf72597154b8f08935bc9c7f" +dependencies = [ + "traitobject", +] + +[[package]] +name = "untrusted" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cd1f4b4e96b46aeb8d4855db4a7a9bd96eeeb5c6a1ab54593328761642ce2f" + +[[package]] +name = "url" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" +dependencies = [ + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "urlencoded" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a52f50139118b60ae91af08bf15ed158817d34b91b9d24c11ffbe21195d33e3" +dependencies = [ + "bodyparser", + "iron", + "plugin", + "url", +] + +[[package]] +name = "uuid" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cfec50b0842181ba6e713151b72f4ec84a6a7e2c9c8a8a3ffc37bb1cd16b231" +dependencies = [ + "rand 0.3.23", + "serde 0.9.15", +] + +[[package]] +name = "uuid" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1436e58182935dcd9ce0add9ea0b558e8a87befe01c1a301e6020aeb0876363" +dependencies = [ + "cfg-if", + "rand 0.4.6", +] + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" + +[[package]] +name = "version_check" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" + +[[package]] +name = "winapi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" +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-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "yaml-rust" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65923dd1784f44da1d2c3dbbc5e822045628c590ba72123e1c73d3c230c4434d" +dependencies = [ + "linked-hash-map", +] diff --git a/file-service/Cargo.toml b/file-service/Cargo.toml new file mode 100644 index 0000000..fa1be72 --- /dev/null +++ b/file-service/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "file-service" +version = "0.1.0" +authors = ["savanni@luminescent-dreams.com"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +chrono = { version = "0.4", features = ["serde"] } +hex-string = "0.1.0" +iron = "0.6.1" +logger = "*" +mime = "0.3.16" +mime_guess = "2.0.3" +mustache = "0.9.0" +orizentic = "1.0.0" +params = "*" +router = "*" +serde_json = "*" +serde = { version = "1.0", features = ["derive"] } +sha2 = "0.8.2" +uuid = { version = "0.4", features = ["serde", "v4"] } +thiserror = "1.0.20" +image = "0.23.5" \ No newline at end of file diff --git a/file-service/authdb.json b/file-service/authdb.json new file mode 100644 index 0000000..3bd3ffb --- /dev/null +++ b/file-service/authdb.json @@ -0,0 +1 @@ +[{"jti":"ac3a46c6-3fa1-4d0a-af12-e7d3fefdc878","aud":"savanni","exp":1621351436,"iss":"savanni","iat":1589729036,"sub":"https://savanni.luminescent-dreams.com/file-service/","perms":["admin"]}] \ No newline at end of file diff --git a/file-service/fixtures/.metadata/.placeholder b/file-service/fixtures/.metadata/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/file-service/fixtures/.thumbnails/.placeholder b/file-service/fixtures/.thumbnails/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/file-service/fixtures/rawr.png b/file-service/fixtures/rawr.png new file mode 100644 index 0000000000000000000000000000000000000000..edaf3239e74ac0e4f61e6856ba4333063c3fbcba GIT binary patch literal 23777 zcmeFYWpo_fk}X(bX66!F7Be$j%w#c_n3(cZcI=cJzkd>4l)Jqj+&on^ zos|vUfcB1dW*}=*ptFa)DbUm%WCj4ZFT2F7!1VwT#ecJkOOE`o7%H>KBXE9!Zk+VJ z=^Gze^Qm-~H!7T(@$Sb6ChJ7T>`otYThb{{ zV7EjKE$+4zqO{E@=Q{X&S~=f2`O242k0IZ1NL zWzED-eskaRfE2if=M!$t!svh((CG9bu=pG<>Yj!Dj5)YJkb<>_du;wi6c>}h4pZ9*m} z0MF;n^G;x6>TC#fx3RW$;&JCE`-_+7{rr!Zkqr13#Mz3UOjAw~C~D_u3S?toV_>2e zcL%w$kO{y8`5aBmc$CE?{!Z~e;wQ6kcDCnXWOQ?LV{l_-uyZtLWaj4PW@KVvWMQFy zN6L3XylKb(d}b}r8RWMuF2z`v6_gUtSe-PY;v zNqA2Otn z$k@@&$$yMHyIKcc^C z6Ug^>mw8kT&7EXHPR@TS_U|>TV(Rd>(ceng8YJ*{dOpTKCHy}A8}_f!Kfrv9|1Bfm ze=a8;Swkmh8B>pUXHDL(y?-$(Vrc&V-RM8Se2o9^^nBKiJQg5RM?*(r3y`s)wTg?8 zrK$11#`_PpzlZ<7j`ts6KF0qAFW*0EmFImi46W_VRZN|o{{IL2|H#T`V$9?8zbmf) z%=Nc5|Cig?dl-L{z26?+@3xHpyxsnN`TmsA|H0Q^rT2f3|1au)8~Gn)_}}UJce?&Z z8TcQ8|6N`GPS^h^1OFrNzpLwiTe{%?qhT|(eecrT-rF&&(nY%WRu0l!SWXxKsEtE- zF@$;_1C6DWxs}z>^LD;MixjDe=8ez}ZV_IlYh3 z|GdG>bEe;WX(7NTF=17Au+vVh8r=nt^wv9pYi_r$762?NP$Cdh+%osjnNFrq{PQ8A zGH-1{T^5sarX~N4%&_j^*|6EKCsMV;voyZJPg-iaDMla8l66xUek78%qHAcG$;e4# zey|6J1s`yB7-l_x>+;Ki2xR-10Av(TZaw2SKd-;T|LyT=wfND8P$0Bt81(@coJ4fP z7}gsxpgR(E#CA%4eerAy`?P=f=Nm4#pcm}w$R`pBtBUhiM{9XJU&K`Q$o9ByTX=91 zY_JZjOS{+CDty0#NztEr4u|%gJUSKg%J_)aR@-MU2<8)SOLy%w8!=)tzTNsp-US1n zZ*1YiQOX}VgC0iej|U=_GG-ri)C(ESKME$BYT@GyE*Y^syc(V_yNze_KkG$o^dDVI zwq+lMO|?CrcXzKHtHs4gq=Y0tY0NKJm8?@P=zT~x>-Kb0m+Mw4+tL3R$)I2ZDPmLAGA6L7ZTq;Jk-A&|L1)169F0MV~>r`*2g zW39h@pXGG8s-C-9;{E;gk?FTAe#!GNs6#U!g%$Y2ECIS(2M>qfH0DGt^08Mz(mBs0qU#sZh=Vi-7hy!z#sg{5Q)B~N2 zR_}R3YDw>Xm0st~R-r$^kEmxa^|OrcIA8QfXtpJ<_@3vI)dVJPt31Eijk}%kwYNP@ zGmU=5ywKRKOF{>J2JH?@_PDB9l8Et9^sWZQ8??Sijc}l$uRTOTGTHu<^R$DYVh%~9q^zb z^cFE?kscFT-~hLk&y62vvasn%il&*L_t`*xub)4zK&l)ldimj`ck~fome5 zt~Eqg`&hq6hnI~wPFR~em=XRESRT;rvv}p?(dc1A`IXnt^jR83Dl)0(2YlvdJTsKN zvIn~>>0N+S5FLCbV8kC@J2O~36d>ghgKHw}+qO2Aor9`^{)iin5(`t{7x?7PRlP%W z0v!^{z4-z!zIUn`-DDNBT+fM$GO-SuT5q*5jq~;Ap2lInhO(Y_S7J#!bdY898lStF z&FKk!xqz!O6eQ%}Ef{a zX5CzbNF>50-f%lEJ+937F~HpDiYC)&M#JD6T#^d&CG=b9O@|caAnCJpH$N|@Dt9h5 zn%_2^N?sPpPg$1+h#$=e;8Y5kS9~HG1Tzcw9j!vLC;(uvXUghOkM@-p(h(ExY*Y+k zXq(GQrr74c`pfj#p32k;gJ6DhbzY6mwVypb?)DlyI2F+OKv#V&gZ?!PN@hj|@RMI8 z8Ngv#}K#bhfh)2p1H8hYEvRBntLpB8L7)$#DB&32n zv?ple$xo5`w&5d+oX>l`aGJlQgyp9A@!`jk{Q7m$2-8nm7i4B0fhNVHi8qJ4tcN0| z%ed{iBl&1H3=7CAv)~6@qyqR}sy0IG{Gl^Zf_o*I!B4_>=MNT|Q)1`j&a@0$QzdS3@|Bl>KxDl{aeuqTHg!gB+|74#`4HsOn~#cyHV?RZqB zAdV&^;E}G-5x@{h{Vf|)8!b-C?xdKC@Wo?eyY4^Jlhf-LG z2obs@5{Yg_SwR5i+XN9LNa)8n;jr$fM+~>vumTM8w$h49qvu#@_Gfl@Z`6(n>e?a9 z(6*7G8)#{oPm8gueVaj4{$;RY*~qDps@%b&IY_8r#o&g(z)+x`~Hr@=rFJIepOlHTmcMUvroC{-cbkHRy4|wKmHjQ5#;$ z=oU$&Ddsh__}sW(A6Kb~^P}CQAL=82=fZ8o_$smp1$8X za3WBB)L@4-*(lVOy>qOM>hq(=;zYi7tUOl5(7~Grd14(x_oAQRP>3fU3v?1bgSG0_ z(}d@D5PEPkFmbBhM#!}Rh32`Jz=B*E~l5uYE_ zKK&%b#G$8q)IOTU58r|2=}K_NfIu9}iX~h0AxLK9{g8Fxsp#BYw3k z=N>~I|MdIpd7Lml?8(Qch)K(m=5OF9;-UE%1W*meKb1(tLOc_~zp%N7tKtRjp)C-L zO;*m61{)eVC)&h?y4UrwaD_5np0l58e`kzo&O6;g(9+|T(uz{`AjD&aNuY)y%DbWU z_$^_MG5NTJ(Kn7Jgf#$e-6~Z}^M(0H874L-<#^MH-o7ZnluOoRPcziNXr;>`ot4{MG5jo?7x^%^9HVM8r=mWg9Mgaw zhX0D*le>cT+tCjhQRmKTqyWssrD;UW0GkF2TvekvY*IB2w6!0r7==t;7tnR*(4Qr)t z#}{?|_DuB&efiI6^U7Uq8KG7Lh@@y^>eXL1)k%SA5}(D!cl7G#1m|K%3L3>#O~1kM zAu>HMA>r~H#9cMqxPl6z(Ll?vqSi6-PzEIQiAe1v%qKIMC|>}PNwgo#BSW_GPKAFj zd@TszK^F+*+yGnUg;fl5##LepC(B{?DO)P_-&tztu+#Xm))~*!4h&wwp4s|U#MaIg zCT7sG5csTw6bbsBj*q-Hg|ccWikfzhM~KV=@~uhUtEMj*lxd%SdMrO&UXnTNli64C zq?cJ)Xi|1VzzUF_%*nmeD1xN)-wm7>Az==j^haXp*)XBuhHd_7IXfN0Cqxg#xWIBA z=7$Ex;yGGRN*_7u3+c9GB%3FC9;3WcXRh9`P`i_{?eyglPkf`%MSc<@yS*V zGwy^&G}y{hTLEsa-J>Hy{5Gvb)?vH4KS0K&P+$A%jt;jn5J1kC+W3Zy)J2zE%cT#& z3X&%Z5H{*@GH9{*)?S~OqJb1v6hcuf2O9G#iM@Lxb@2@3p;@@-QV3f&5eDtE+9cF1 zC6(U#3n=;Gfz=OLD}E&|F21Lv6gwKUYd3@AHFIVDIJY2NOTh4EclbhF6>K$F^7yp% z7qBTOFYKAofKJeTUMI+RUD{-m$P|(ZGeXRaEBiLfS z#f_`6b3?|&OXp_GGbi*~00MU;mGS+QJM1~|0d47N>}H62zKXLJPH5!BHon`78P#v4 z_LUS0A81W?3lM97A#)3a>?L)qNY+k)>{CPkxQSng9&X%)S0TlO3MV>|64XBPGG_h7 zs`*kp9=YG?b_+`aMJkpF`jcIrQrqtLSMMbCt~lIVUQY7m?W{Aiuo^bCYIEEo&?>^$ z02rWOa1z{yMpaYJ^d_oyaompI8DaM_fT@V;_20K1o{vr`r_O!?r4s|1eCK^oLDnOS zI$_@9)IdqY!-_ZMFxk-ctM6 zH&X;ASGKmeNt9pg@?}&+yUUpJl(#{!anq61iB`AERRBq&cByQb?h@@1KST4kwAT|^ z)Mt3O+|pdg^WCdDarB(%DrQS9i16Y-lPP=C0eGxfY(>9&LP1}~Y)d3avlc8nr4OT2 zex8jht-o(thXr*sE`Or?Gcgb^T^Ef%Dc>5Vn)uMK9|&?C#G2n^)K5at^$1fB69O#c zSaqg{6Tg*)P>J&0zMMgLP5)ZCFVe*a5zRY}3?jD^azwZ!^bISb_d>R!^=dqh=qE7v zF4wWtE(zmHVfq1?LZg{BPpU$kfkZ=`Jxph}!fo%H31f!4{VU%H!hC?9@PtV6wCxPW zrTxzMp_}q|%{ypV(lzc_AknA{xloiz;-?xtUNA2|q*;At`fWXmyaD6fJT0EjhgjX+ zcI&U}Z%q3L#Xiwy9bwh}E~d5%mk!+zE}@Uzfgh^)7m0CNv())!xMTQj@f?^>wznzd zh>WdmO_@*m8yHUo1IO2!0~gJ`8RNkUSE*pcB@#dsf(0VJDGQ)rJn1=9lo!y`@!{6p zlUIAN?{w&${Qif&fEgcQ& z6s;0Vxs^UdY33pROIdMcV<>4irc4sdg6CS;KIgKOwIX{dq!N)z?KLqo_2a1i`a>Ph zz(hKm;r^ef{!DOuY?hU~qot0(b;4$t&r|F-^cx~OpbL2_2vHM_0|%&a+l{T$lMERI z7`*Luj+`%=hsX(-J%%o{PMbY-_fn|b8iC=tVi%!|OopOWoK#vKc2;DV_LzWsmK?4+%RAl=j(8E3UVD^b z5f;k;o0Lf7oW7T-^CL;Ri8OvOV~+MOHrcb2B9uWYn_2r=hO&vKndx6C4Zjb-_+p#_ z4Ajvi3Fr(9dBDXh=pxOBw4%i2tiGa)s?y{QlK6z6YF0<9_FO>qR*4$8Bo*e_JNJ?y z#Ll`_NDTCd60bX-1!nMNli!YlaQh&5Ls$dDwHB1}Y z3lU;Pj?grXiVoBsTZWi}KGPfCZLb-^!TZpd>qAFzt!M&xY-8rwkQdhw^3% zZu!_IQ=Y>DnH2FDToC5@6C+8O%Y*_vEF8+1)0}{E0LvRCYj;W0QozXF;$tj;f(gNS zQ0eKlvrpqCDn2I(&)5{3<`90(5}k&H#0TmiU4N z^G&lj&>EOA2z8;SbEVHg)o%79oZ|Pje68<>Sdbf9(4&`nM07+Crk?a?u&V^8@jug* zlCIJep}bP3$8O8zvXi%G`aFO4}5>-P+5WzO4T+3`=@fJ3FXH{Lf*?(eJZ zI-{s@iG4KKxC|5dgKffDTxhLT8z9zky8^eRejM^(T=^(!7kZtV#?F)~Jzcpp2K_xx z`eOI;MYQ_*dXVWtRYOZlJbnedWwZSuItkBmB(;7lqY_=+C~p8Z;W5T^O6iAj7;hOd z4}O$D9UU#{EO}lVYyx3bd<$pbl}x&HA`sAuRRPT(FELRphFdNGbgds0UEa|Kw=%WD ze}8aStT1p?3XFl<$SvH7rPDZh-}P$z&8HD0*HB1=!YK}DfH&uJ@1G?>hN&9CNxl*7 z{XiBd*a{(pD)a#1UL0;SZh)8vAC7pu8p4h~IW#2E=6-18`PMpfI4t<);Cb4(pxnG7 zBkp{G|zlyymk!3nI80RZ+1YSvd_1ArpnWouo!jB2&>^R8RA1dg@ zCNB~UNPF{{l9`ZL+FiIkvI}2HSouokpA_|BKXkhnjRQnS{(Y6Upc^4t1Qoe01i?{h z;&ZqZ7}<}~w+)6cBa30wa(?wIomNlwEZ;Y09+#u`!zFWlq5ZU>G)O898*A(0x%W+3 zt9?chzDBl**)v)5P`IK{tcbNM_*!?)L7#yqavzlyDAT6}M|L<4v5HW75G|+y$Y_<@ zxS~m#pHTZ5rQxi&DWIOicHS1oOG$nVMClK4YVs(&Bq2ONG9B1Et11vT0w*)@Pixha{QAp`Jg1MMQx__k!Z7lXta6GX#@0q%} z%)d>@TW;KbQgzV#%3m0eNt1<}&rqG86Rmz)-Qp<-R;I(9fTbghYlP<&eRn&vH&3P2 zsC%U;H@2sa5OFtLlfXil5{!`MD}BLTK){{Jo^MJp8c8bcMwutz9V#j%5{rza=^T1F zQDDt>KoW%NXJbZHp2_9#dkOKC8-Gsyy2JM=!~Cs&pW?*51rOZ7FUvELc&4Nlg9=ap z&k|g(V^$*Rnw<`Rd9~_JV8C*!-5y$<%PH0GQQ_B$Os3i1l-<<6QgP#wf&`iG{=Qow zBM(6X*RzRzXf<0>q-UPh6(*lg*l*98k@mzzkp5~ zaXEM2w8)$55__JFY0Vsl3TlRgx=&ROoh}u<4>xrXs*1Eww+Bkbt3byx~IUD7itg4hCM zRZ)2bnVD3S7-_aV_X}c*1aHM^Vv4ovz!lLII@W+C|2OcW6g4dj>pXrb^=q&X)6l|+ z1e(l@A?MLc%#mO7wiKAJ4{FKBv+dGL)*1B*7e73nSis`0$sz?{mg2)ss&PZPF;<%E zy8?zUU7XYP@CcDAY${5ks`X`$H5M=sLgSGSMEShiQQ8;`7zr3rQvgko^&jkN=n$j2 z4RDxN0&zJZ6yqItMmC*?dsk-AgZ-JHdv-Q6vbne`&(-`32qxRuPzuXWvXa3Qyj5U$ zzO(|Yf6tmKjif9v6#v-QkJ~II5OLQ2sN2_ru%i$!xl-JobYfR_Bn@6I++(X-QV^Lt zLLfnfi_8lKuEf&PMlFG^_(cFJS{k`2m#qgz)S+^mlm=Ne^9S499)9t{)dI|ej}y8- zwCwAS_ILPabSBNWRfVs#X+4>SQv?g9Nkc`~Y18b5J*&&o-iy&g0#I6+2Qg;jK+h8P<1&oAHsvH^c z>`0)%0Yw1m?QllXajouvy|E?yp00<{W7+;QIT`y$+48KrsuOt>@27gu{R@k)|NhL3 zg5cNjE>F_JfY1GRb?(~-3B1!0ym3X_ZtE#~_Hb6_BeS6Zj(AsH68l(Oxay81KDY$f z3o>T0KryC8iI?v2t^lXBNreOJ_&y*dm*ycBdjFX31D;qcQa&4P1gu2*-B+_ZAR%jp?*o zW-G)kOb1yg%nR%#?$3SV$(9%X zuhSUxkRfVgc{I(44eqm38al?nV^egQaSS?1Z7`=)7#b`Gt8W95B=OUe9Rzzx!wcBS zAQ=l*h8a2>8FW1#!Nm~G!5>M*ETL`CuW5@bWj+BnEyA@6ed=;{;}fW3_y*b+EMkwM zvDH-#3+Gz0p3uRGsUoeJQ=*ApH{+?$3JuuM$5U3_g=z-)3aE>CU<;P{qcj3s#)!de+%-ilzYkp56 zp0`VBGebxCZFp5f;qk_ei-$ybAEPtXG2Z}3tcw#ue7l~4NU3&TDNv1HPn!qMom(VV zV(@M)5OTYuJxu$^Y?AgAuJiBgI61zpdv3O>WP1yIjA$o9#)=ttD^WQ!=%2mXmF zmktKzLe^8hPa*;GThumvJ@n-T@(+Yms7v!jm=55w-Qrkfn;54QIbLHiG!_7`#d*cJ zAnQ)0vft*AGv_YtP{Y55+fh!CKe`I6vVn0y_+tq*OK(JVD4VA?1@O#rUmO#Pq(L@p z=YbKrU!Dxkx?hE;=9u3jx7lNN>KWEq=oIi3Mqzg)(P!W7ZlbFJ=pE;-3xp!sKNk(9 zgbSm$;6d(uKUGz!Vkp+?$#G1NHGTEVA?i=l??@kieURn(eN$f63|6lsu_qwdGJAEJ z(P?DEJF%g8tiBMXct$A}5dyrpy`L9ud-Ht zN&KPRUN%f`XV(*9*I^<89rmn{LO1}7(RhMqnD?(-mdwtAFaUv>5HjI$u;?PFLD1xm zt<7Co<9x5uTRquK$oE8@HY7xder6jN0#nAX&?oW7IZRg!6 z-m3CIiHmO}psp?D2KzMkLW{RFUzSm}+!Ff|_CrPLbBTSB%^xfm!d@yWBAjpV-&9~g zr$#1)(u^z6c727xSUl&nut+KU+92vvz3fO*&k?pV&-X<_ z3mhsyv?bLM>6?FIIu`8H%_+vI*jR9uKW5%iGL ztbY)YGBkOn1g-Sz*>23u5v%$1wE|z7-oxdg^X?dBYTYH`Xn=5mY_nUdlayVdJkgH@ z&^B5lgE(t)lT;b>H_iK|3D5@WF)F1qdMuM9YHg`1OD>_QKRT@hmLlcwgvknCLHumF z9dh%X4N|j$;ylF7>zQTan%+{gAUpEOXMMj@c9tZudl_K$VKF#XMQMV#-f{r4Q zsk304yl`G!B4>C7WYsc982j8zN*_BJjKTC>chRdJXV5Zlw)t%>SU6$zzvnoHe#kx(r~zZRV=QiuIJ_rcenw+?x0GGrw98f ze+Of1n8T~@j8h;JauH!Kjm;-?FvElp4MuDSmg}a$%zo&Sz>4klZudQJPaC6Cl#AzK zUw(?{;Z}=ENi@pT*r zW4keGdE{h02FojYr8=?Fglh=-@q0>$iCO;xbASN%RQK19DcTkCewNtU}Fl9coKV}7;tx) zsi;1z#Nc?x$n#fvZvpjsQyM&M==pyKm7p$ZC^Ku=f7?1fTn;HO>smA`U6iJ;vKdD@ z5Q4NpiE^wW2J#kFoOd-(DXf&Q*uO$AR5lEjz)T1IsF0N*nUv2ZT(#rH+6Gf=aq;IT z%MWfq7c%a9G3<#1zfv&&cp6tnJ-U+s!B43tg>n_F3kZl(hwIZ$hTWR~9(}7%aW>iOX>+nL52t;K2VL_hsXi*en1ktg8Pxx zkX*L;!SN>nKN}kVTo)3BBgqI^C2dI7h0@li??t&9F)1En(gi^9S)zMS>0<+KbN<4i z95I6OU~YQ)`v#ROC`CiEIozRX_4+CA3}U?^ith^sWhCzI)sXg`V1Ds$2FK+w)pe1k z-alWRgA8##X`8o3iPk865qBnOxsFvG3^_I$uNUOYY0^R~)bHdRm^9{p>+9NWc)sHY z;Vdw>?{h1VsVO2&_?!=i-#8rAy!o*m6cI;>L$3r{3~a{P;{$t3AT9tbJux5)05S;_ zi|!8XWKjEHL!4AO?7W0S=DCH9|EnQvR1S-HjeV?cFL2NoZ{WCruS`V2kPUo`H9N8* zf5&-+0=XnH|8_FCm%$&U3i71~K z<0{KnWlgG!Q0IAMu0)V9L4E~1G4Q$9eYQU4D1r0fz`a|bzWux?_uJFB;Nh;k`t8wK zK9_FNQ((is9ugI4xxUT3fR&RoY_Yq3{kRe+;sS{&^PE~Inj?^k{L6>pFQ+OKJFXOUScdAk z;0U_M5ciQkI~WS{F9`eLn-}U^ZY9c8F+%SH98D*~Z&D0lUbxl)blhqYfHFgzWKl7- zU@O>Bn~Ql)%6ut4Qou3m14do6>i}i+r%9ArX#nPE9~SnY*~$spfn)K8I{}w()b~<9 z{%*C$yLyu8yy^2OfPBeJ(C?$3iS;3o37DavU#A@=q#9{wUC&J?cN4_Y?Obm~eHnt> z?e^xH{et~V0J`vkZC$S49I!h;>^DScavd0$cJh+G@40HRD*cFClc9~X=4cTx%!6Dc zEr*v*EqE+CnzTp4(a_1TQ*tL~I%sHw@aQG?k?z}K90>~vb^?NA9p2-{f!#4V(h$VZ z%9QF66TtkKZeqG0B;Fq*`liE1L;|VH;&ntL{3!^nKVO)kkE;x(B`jo{_OG5JF)X~z zsNj&?W3`wB z8rP=WISQ#LkvITni7Py@gY3yle+i#L>kbP;a;@!g(OnJrxRp!|F}>QKKBtTD-!1h2 zd_Xa(CcQuf!?%fa-UaXg}sLu47c`Khwbp7zbAX|Qwp_06zG)}RbZI0W< zJ%5dHwQ}91mRQSME9;uvagn1?$R)wxqCU|aFpr`kt(YaJJ6RciuCcrb{{u{k`Q8^c`byPwE@9JK-qyv zDazQD(0f0*T|E#tL{S&&?MRD+z%t2UZ&8YA&t4~HK3SjhYBtOa95$AHOW6!&GI}o;5U~OM{61?Y%S1 zF6JN*j)XIHaH`+Tf;G@~|I}uu?^(D>H*8+z{9FRq`>>a~C(w9*_qud-RRu2pon|*; z3_!hzUJwJ7hLO`=>M9Y9hbHU!DDp8^fI+;rgGG=x?XE`WMEO}{Sp>`CHspGw3&LWL zJi>8$$WZ?y^V%~1?ZuKcf!2`=8p^8eLBh;TzSfR*HS7!?Bu&3R9b8CLNK78-Jv302 z9lE4WcbvD6BON{9a`&KzH^I*9u^I42^ra2_J35y*(so>9#MjR@;m5>ZH<}#2(xjGA z9dGL7-F|IcPS=>_7n4+YJb-{Z7X!Gm8w}5Ri7stl{o*1O;PG>9w-}#?o*N&Nf~eX& z)R3*)O}iMYebSJ>RFJd9p64GrUz7=}?y-f3#~vUJ#T#brJ}aIiwo4pbK(&JTc7nZi zY6~W%LjKSn0sb9QahJvh09Fw+w4UemQCAHulg1?A+c*wfA1Z7FTT-1Nrwt-6{!@;# z3-*!M`3F4>QT6lwujbm*I%$wE$OcLpaBQ=fZbPX(s?(2h=7

Z7$1qzu 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 WJpW!ZQ0$Tb0000 for Cookie { + fn from(s: &str) -> Cookie { + let parts: Vec<&str> = s.split("=").collect(); + Cookie { + name: String::from(parts[0]), + value: String::from(parts[1]), + } + } +} + +impl From<&String> for Cookie { + fn from(s: &String) -> Cookie { + Cookie::from(s.as_str()) + } +} + +impl From for Cookie { + fn from(s: String) -> Cookie { + Cookie::from(s.as_str()) + } +} + +#[derive(Debug)] +pub struct CookieJar(HashMap); + +impl CookieJar { + pub fn new() -> CookieJar { + CookieJar(HashMap::new()) + } + + pub fn add_cookie(&mut self, name: String, value: Cookie) { + self.0.insert(name, value); + } + + pub fn lookup(&self, name: &str) -> Option<&Cookie> { + self.0.get(name) + } +} + +// Some(Cookie(["auth=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJhYzNhNDZjNi0zZmExLTRkMGEtYWYxMi1lN2QzZmVmZGM4NzgiLCJhdWQiOiJzYXZhbm5pIiwiZXhwIjoxNjIxMzUxNDM2LCJpc3MiOiJzYXZhbm5pIiwiaWF0IjoxNTg5NzI5MDM2LCJzdWIiOiJodHRwczovL3NhdmFubmkubHVtaW5lc2NlbnQtZHJlYW1zLmNvbS9maWxlLXNlcnZpY2UvIiwicGVybXMiOlsiYWRtaW4iXX0.8zjAbZ7Ut0d6EcDeyik39GKhXvH4qkMDdaiQVNKWiuM"])) +impl From<&headers::Cookie> for CookieJar { + fn from(c: &headers::Cookie) -> CookieJar { + let jar = CookieJar::new(); + + let headers::Cookie(cs) = c; + cs.iter().fold(jar, |mut jar, c_| { + let cookie = Cookie::from(c_); + jar.add_cookie(cookie.name.clone(), cookie); + jar + }) + } +} diff --git a/file-service/src/lib/error.rs b/file-service/src/lib/error.rs new file mode 100644 index 0000000..53dc55f --- /dev/null +++ b/file-service/src/lib/error.rs @@ -0,0 +1,31 @@ +use std::path::PathBuf; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("not implemented")] + NotImplemented, + + #[error("file not found: `{0}`")] + FileNotFound(PathBuf), + + #[error("file is not an image: `{0}`")] + NotAnImage(PathBuf), + + #[error("path is not a file: `{0}`")] + NotAFile(PathBuf), + + #[error("Image loading error")] + ImageError(#[from] image::ImageError), + + #[error("IO error")] + IOError(#[from] std::io::Error), + + #[error("JSON error")] + JSONError(#[from] serde_json::error::Error), + + #[error("UTF8 Error")] + UTF8Error(#[from] std::str::Utf8Error), +} + +pub type Result = std::result::Result; diff --git a/file-service/src/lib/file.rs b/file-service/src/lib/file.rs new file mode 100644 index 0000000..3a085fd --- /dev/null +++ b/file-service/src/lib/file.rs @@ -0,0 +1,151 @@ +use super::error::{Error, Result}; +use super::fileinfo::FileInfo; +use super::thumbnail::Thumbnail; +use std::fs::{copy, read_dir, remove_file}; +use std::path::{Path, PathBuf}; + +#[derive(Debug)] +pub struct File { + info: FileInfo, + tn: Thumbnail, + root: PathBuf, +} + +impl File { + pub fn new( + id: &str, + root: &Path, + temp_path: &PathBuf, + filename: &Option, + ) -> Result { + let mut dest_path = PathBuf::from(root); + dest_path.push(id); + match filename { + Some(fname) => match fname.extension() { + Some(ext) => { + dest_path.set_extension(ext); + () + } + None => (), + }, + None => (), + }; + copy(temp_path, dest_path.clone())?; + let info = FileInfo::from_path(&dest_path)?; + let tn = Thumbnail::from_path(&dest_path)?; + Ok(File { + info, + tn, + root: PathBuf::from(root), + }) + } + + pub fn open(id: &str, root: &Path) -> Result { + let mut file_path = PathBuf::from(root); + file_path.push(id.clone()); + + if !file_path.exists() { + return Err(Error::FileNotFound(file_path)); + } + if !file_path.is_file() { + return Err(Error::NotAFile(file_path)); + } + + let info = match FileInfo::open(id, root) { + Ok(i) => Ok(i), + Err(Error::FileNotFound(_)) => { + let info = FileInfo::from_path(&file_path)?; + info.save(&root)?; + Ok(info) + } + Err(err) => Err(err), + }?; + + let tn = Thumbnail::open(id, root)?; + + Ok(File { + info, + tn, + root: PathBuf::from(root), + }) + } + + pub fn list(root: &Path) -> Vec> { + let dir_iter = read_dir(&root).unwrap(); + dir_iter + .filter(|entry| { + let entry_ = entry.as_ref().unwrap(); + let filename = entry_.file_name(); + !(filename.to_string_lossy().starts_with(".")) + }) + .map(|entry| { + let entry_ = entry.unwrap(); + let id = entry_.file_name().into_string().unwrap(); + File::open(&id, root) + }) + .collect() + } + + pub fn info(&self) -> FileInfo { + self.info.clone() + } + + pub fn thumbnail(&self) -> Thumbnail { + self.tn.clone() + } + + pub fn stream(&self) -> Result { + let mut path = self.root.clone(); + path.push(self.info.id.clone()); + std::fs::File::open(path).map_err(Error::from) + } + + pub fn delete(&self) -> Result<()> { + let mut path = self.root.clone(); + path.push(self.info.id.clone()); + remove_file(path)?; + self.tn.delete()?; + self.info.delete() + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::lib::utils::FileCleanup; + use std::path::{Path, PathBuf}; + + #[test] + fn it_opens_a_file() { + let _md = FileCleanup(PathBuf::from("fixtures/.metadata/rawr.png.json")); + let _tn = FileCleanup(PathBuf::from("fixtures/.thumbnails/rawr.png")); + + File::open("rawr.png", Path::new("fixtures/")).expect("to succeed"); + } + + #[test] + fn it_can_return_a_thumbnail() { + let f = File::open("rawr.png", Path::new("fixtures/")).expect("to succeed"); + assert_eq!( + f.thumbnail(), + Thumbnail { + id: String::from("rawr.png"), + root: PathBuf::from("fixtures/"), + }, + ); + } + + #[test] + fn it_can_return_a_file_stream() { + let f = File::open("rawr.png", Path::new("fixtures/")).expect("to succeed"); + f.stream().expect("to succeed"); + } + + #[test] + fn it_raises_an_error_when_file_not_found() { + match File::open("garbage", Path::new("fixtures/")) { + Err(Error::FileNotFound(_)) => assert!(true), + _ => assert!(false), + } + } +} diff --git a/file-service/src/lib/fileinfo.rs b/file-service/src/lib/fileinfo.rs new file mode 100644 index 0000000..ee8350e --- /dev/null +++ b/file-service/src/lib/fileinfo.rs @@ -0,0 +1,160 @@ +use chrono::prelude::*; +use hex_string::HexString; +use serde::{Deserialize, Serialize}; +use serde_json; +use sha2::{Digest, Sha256}; +use std::fs::remove_file; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; + +use super::error::{Error, Result}; +use super::utils::append_extension; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct FileInfo { + pub id: String, + pub size: u64, + pub created: DateTime, + pub file_type: String, + pub hash: String, + + #[serde(skip)] + root: PathBuf, +} + +impl FileInfo { + pub fn save(&self, root: &Path) -> Result<()> { + let ser = serde_json::to_string(self).unwrap(); + std::fs::File::create(FileInfo::metadata_path(&self.id, root)) + .and_then(|mut stream| stream.write(ser.as_bytes()).map(|_| (()))) + .map_err(Error::from) + } + + pub fn open(id: &str, root: &Path) -> Result { + let mut buf = Vec::new(); + + let md_path = FileInfo::metadata_path(id, root); + std::fs::File::open(md_path.clone()) + .and_then(|mut stream| stream.read_to_end(&mut buf)) + .map_err(move |err| match err.kind() { + std::io::ErrorKind::NotFound => Error::FileNotFound(md_path), + _ => Error::IOError(err), + })?; + + let str_repr = std::str::from_utf8(&buf)?; + + serde_json::from_str(&str_repr).map_err(Error::from) + } + + pub fn from_path(path: &Path) -> Result { + match (path.is_file(), path.is_dir()) { + (false, false) => Err(Error::FileNotFound(PathBuf::from(path))), + (false, true) => Err(Error::NotAFile(PathBuf::from(path))), + (true, _) => Ok(()), + }?; + + let metadata = path.metadata().map_err(Error::IOError)?; + let id = path + .file_name() + .map(|s| String::from(s.to_string_lossy())) + .ok_or(Error::NotAFile(PathBuf::from(path)))?; + let created = metadata + .created() + .map(|m| DateTime::from(m)) + .map_err(|err| Error::IOError(err))?; + let file_type = String::from( + mime_guess::from_path(path) + .first_or_octet_stream() + .essence_str(), + ); + let hash = FileInfo::hash_file(path)?; + Ok(FileInfo { + id, + size: metadata.len(), + created: created, + file_type, + hash: hash.as_string(), + root: PathBuf::from(path.parent().unwrap()), + }) + } + + fn hash_file(path: &Path) -> Result { + let mut buf = Vec::new(); + let mut file = std::fs::File::open(path).map_err(Error::from)?; + + file.read_to_end(&mut buf).map_err(Error::from)?; + let mut vec = Vec::new(); + vec.extend_from_slice(Sha256::digest(&buf).as_slice()); + Ok(HexString::from_bytes(&vec)) + } + + fn metadata_path(id: &str, root: &Path) -> PathBuf { + let mut path = PathBuf::from(root); + path.push(".metadata"); + path.push(id.clone()); + append_extension(&path, "json") + } + + pub fn delete(&self) -> Result<()> { + let path = FileInfo::metadata_path(&self.id, &self.root); + remove_file(path).map_err(Error::from) + } +} + +#[cfg(test)] +mod test { + use super::*; + + use crate::lib::utils::FileCleanup; + + #[test] + fn it_generates_information_from_file_path() { + let path = Path::new("fixtures/rawr.png"); + match FileInfo::from_path(&path) { + Ok(FileInfo { + id, + size, + file_type, + hash, + .. + }) => { + assert_eq!(id, "rawr.png"); + assert_eq!(size, 23777); + assert_eq!(file_type, "image/png"); + assert_eq!( + hash, + "b6cd35e113b95d62f53d9cbd27ccefef47d3e324aef01a2db6c0c6d3a43c89ee" + ); + } + Err(err) => { + println!("error loading file path: {}", err); + assert!(false); + } + } + } + + #[test] + fn it_saves_and_loads_metadata() { + let path = Path::new("fixtures/rawr.png"); + let _ = FileCleanup(append_extension(path, "json")); + let info = FileInfo::from_path(&path).unwrap(); + info.save(Path::new("fixtures")).unwrap(); + + assert!(Path::new("fixtures/.metadata/rawr.png.json").is_file()); + + let info_ = FileInfo::open("rawr.png", Path::new("fixtures")).unwrap(); + assert_eq!(info_.id, "rawr.png"); + assert_eq!(info_.size, 23777); + assert_eq!(info_.created, info.created); + assert_eq!(info_.file_type, "image/png"); + assert_eq!(info_.hash, info.hash); + } + + #[test] + fn it_extends_a_file_extension() { + assert_eq!( + append_extension(Path::new("fixtures/rawr.png"), "json"), + Path::new("fixtures/rawr.png.json") + ); + } +} diff --git a/file-service/src/lib/mod.rs b/file-service/src/lib/mod.rs new file mode 100644 index 0000000..40e80c2 --- /dev/null +++ b/file-service/src/lib/mod.rs @@ -0,0 +1,56 @@ +use std::path::{Path, PathBuf}; +use uuid::Uuid; + +mod error; +mod file; +mod fileinfo; +mod thumbnail; +mod utils; + +pub use error::{Error, Result}; +pub use file::File; +pub use fileinfo::FileInfo; +pub use thumbnail::Thumbnail; + +pub struct App { + files_root: PathBuf, +} + +impl App { + pub fn new(files_root: &Path) -> App { + App { + files_root: PathBuf::from(files_root), + } + } + + pub fn list_files(&self) -> Vec> { + File::list(&self.files_root) + } + + pub fn add_file(&mut self, temp_path: &PathBuf, filename: &Option) -> Result { + let id = Uuid::new_v4().hyphenated().to_string(); + File::new(&id, &self.files_root, temp_path, filename) + } + + pub fn delete_file(&mut self, id: String) -> Result<()> { + let f = File::open(&id, &self.files_root)?; + f.delete() + } + + pub fn get_metadata(&self, id: String) -> Result { + FileInfo::open(&id, &self.files_root) + } + + pub fn get_file(&self, id: String) -> Result<(FileInfo, std::fs::File)> { + let f = File::open(&id, &self.files_root)?; + let info = f.info(); + let stream = f.stream()?; + Ok((info, stream)) + } + + pub fn get_thumbnail(&self, id: &str) -> Result<(FileInfo, std::fs::File)> { + let f = File::open(id, &self.files_root)?; + let stream = f.thumbnail().stream()?; + Ok((f.info(), stream)) + } +} diff --git a/file-service/src/lib/thumbnail.rs b/file-service/src/lib/thumbnail.rs new file mode 100644 index 0000000..738b4aa --- /dev/null +++ b/file-service/src/lib/thumbnail.rs @@ -0,0 +1,82 @@ +use image::imageops::FilterType; +use std::fs::remove_file; +use std::path::{Path, PathBuf}; + +use super::error::{Error, Result}; + +#[derive(Clone, Debug, PartialEq)] +pub struct Thumbnail { + pub id: String, + pub root: PathBuf, +} + +impl Thumbnail { + pub fn open(id: &str, root: &Path) -> Result { + let mut source_path = PathBuf::from(root); + source_path.push(id); + + let self_ = Thumbnail { + id: String::from(id), + root: PathBuf::from(root), + }; + + let thumbnail_path = Thumbnail::thumbnail_path(id, root); + if !thumbnail_path.exists() { + let img = image::open(source_path)?; + let tn = img.resize(640, 640, FilterType::Nearest); + tn.save(thumbnail_path)?; + } + + Ok(self_) + } + + pub fn from_path(path: &Path) -> Result { + let id = path + .file_name() + .map(|s| String::from(s.to_string_lossy())) + .ok_or(Error::NotAnImage(PathBuf::from(path)))?; + + let root = path + .parent() + .ok_or(Error::FileNotFound(PathBuf::from(path)))?; + + Thumbnail::open(&id, root) + } + + fn thumbnail_path(id: &str, root: &Path) -> PathBuf { + let mut path = PathBuf::from(root); + path.push(".thumbnails"); + path.push(id.clone()); + path + } + + pub fn stream(&self) -> Result { + let thumbnail_path = Thumbnail::thumbnail_path(&self.id, &self.root); + std::fs::File::open(thumbnail_path.clone()).map_err(|err| { + if err.kind() == std::io::ErrorKind::NotFound { + Error::FileNotFound(thumbnail_path) + } else { + Error::from(err) + } + }) + } + + pub fn delete(&self) -> Result<()> { + let path = Thumbnail::thumbnail_path(&self.id, &self.root); + remove_file(path).map_err(Error::from) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::lib::utils::FileCleanup; + + #[test] + fn it_creates_a_thumbnail_if_one_does_not_exist() { + let _ = FileCleanup(PathBuf::from("fixtures/.thumbnails/rawr.png")); + let _ = + Thumbnail::open("rawr.png", Path::new("fixtures")).expect("thumbnail open must work"); + assert!(Path::new("fixtures/.thumbnails/rawr.png").is_file()); + } +} diff --git a/file-service/src/lib/utils.rs b/file-service/src/lib/utils.rs new file mode 100644 index 0000000..9427daf --- /dev/null +++ b/file-service/src/lib/utils.rs @@ -0,0 +1,17 @@ +use std::path::{Path, PathBuf}; + +pub struct FileCleanup(pub PathBuf); + +impl Drop for FileCleanup { + fn drop(&mut self) { + let _ = std::fs::remove_file(&self.0); + } +} + +pub fn append_extension(path: &Path, extra_ext: &str) -> PathBuf { + let ext_ = match path.extension() { + None => String::from(extra_ext), + Some(ext) => [ext.to_string_lossy(), std::borrow::Cow::from(extra_ext)].join("."), + }; + path.with_extension(ext_) +} diff --git a/file-service/src/main.rs b/file-service/src/main.rs new file mode 100644 index 0000000..67f1401 --- /dev/null +++ b/file-service/src/main.rs @@ -0,0 +1,341 @@ +use iron::headers; +use iron::middleware::Handler; +use iron::modifiers::{Header, Redirect}; +use iron::prelude::*; +use iron::response::BodyReader; +use iron::status; +use mustache::{compile_path, Template}; +use orizentic::{Permissions, ResourceName, Secret}; +use params::{Params, Value}; +use router::Router; +use serde::Serialize; +use std::collections::HashMap; +use std::fs::File; +use std::io::Read; +use std::path::Path; +use std::path::PathBuf; +use std::sync::{Arc, RwLock}; + +mod cookies; +mod lib; +mod middleware; + +use lib::{App, FileInfo}; +use middleware::{Authentication, RestForm}; + +fn is_admin(resource: &ResourceName, permissions: &Permissions) -> bool { + let Permissions(perms) = permissions; + ResourceName(String::from( + "https://savanni.luminescent-dreams.com/file-service/", + )) == *resource + && perms.contains(&String::from("admin")) +} + +pub fn compare_etags(info: FileInfo, etag_list: &headers::IfNoneMatch) -> bool { + let current_etag = headers::EntityTag::new(false, info.hash); + match etag_list { + headers::IfNoneMatch::Any => false, + headers::IfNoneMatch::Items(lst) => lst.iter().any(|etag| etag.weak_eq(¤t_etag)), + } +} + +mod files { + use super::*; + + pub struct IndexHandler { + pub app: Arc>, + pub template: Template, + } + + #[derive(Serialize)] + pub enum TemplateFile { + #[serde(rename = "error")] + Error { error: String }, + #[serde(rename = "file")] + File { + id: String, + size: u64, + date: String, + type_: String, + }, + } + + #[derive(Serialize)] + pub struct IndexTemplateParams { + files: Vec, + } + + impl Handler for IndexHandler { + fn handle(&self, req: &mut Request) -> IronResult { + let app = self.app.read().unwrap(); + let m_token = req.extensions.get::(); + match m_token { + Some(token) => { + if token.check_authorizations(is_admin) { + let files: Vec = app + .list_files() + .into_iter() + .map(|entry| match entry { + Ok(file) => TemplateFile::File { + id: file.info().id, + size: file.info().size, + date: format!( + "{}", + file.info().created.format("%Y-%m-%d %H:%M:%S") + ), + type_: file.info().file_type, + }, + Err(err) => TemplateFile::Error { + error: format!("{}", err), + }, + }) + .collect(); + Ok(Response::with(( + status::Ok, + Header(headers::ContentType::html()), + Header(headers::SetCookie(vec![format!("auth={}", token.text)])), + self.template + .render_to_string(&IndexTemplateParams { files }) + .expect("the template to render"), + ))) + } else { + Ok(Response::with(status::Forbidden)) + } + } + None => Ok(Response::with(status::Forbidden)), + } + } + } + + pub struct GetHandler { + pub app: Arc>, + } + + impl Handler for GetHandler { + fn handle(&self, req: &mut Request) -> IronResult { + let app = self.app.read().unwrap(); + let capture = req.extensions.get::().unwrap().clone(); + let old_etags = req.headers.get::(); + match capture.find("id") { + Some(id) => { + let info = app.get_metadata(String::from(id)); + match (info, old_etags) { + (Ok(info_), Some(if_none_match)) => { + if compare_etags(info_, if_none_match) { + return Ok(Response::with(status::NotModified)); + } + } + _ => (), + } + match app.get_file(String::from(id)) { + Ok((info, stream)) => Ok(Response::with(( + status::Ok, + Header(headers::ContentType( + info.file_type.parse::().unwrap(), + )), + Header(headers::ETag(headers::EntityTag::new(false, info.hash))), + BodyReader(stream), + ))), + Err(_err) => Ok(Response::with(status::NotFound)), + } + } + _ => Ok(Response::with(status::BadRequest)), + } + } + } + + pub struct GetThumbnailHandler { + pub app: Arc>, + } + + impl Handler for GetThumbnailHandler { + fn handle(&self, req: &mut Request) -> IronResult { + let app = self.app.read().unwrap(); + let capture = req.extensions.get::().unwrap().clone(); + let old_etags = req.headers.get::(); + match capture.find("id") { + Some(id) => { + let info = app.get_metadata(String::from(id)); + match (info, old_etags) { + (Ok(info_), Some(if_none_match)) => { + if compare_etags(info_, if_none_match) { + return Ok(Response::with(status::NotModified)); + } + } + _ => (), + } + match app.get_thumbnail(id) { + Ok((info, stream)) => Ok(Response::with(( + status::Ok, + Header(headers::ContentType( + info.file_type.parse::().unwrap(), + )), + Header(headers::ETag(headers::EntityTag::new(false, info.hash))), + BodyReader(stream), + ))), + Err(_err) => Ok(Response::with(status::NotFound)), + } + } + _ => Ok(Response::with(status::BadRequest)), + } + } + } + pub struct PostHandler { + pub app: Arc>, + } + + impl Handler for PostHandler { + fn handle(&self, req: &mut Request) -> IronResult { + let mut app = self.app.write().unwrap(); + let m_token = req.extensions.get::(); + match m_token { + Some(token) => { + if token.check_authorizations(is_admin) { + let params = req.get_ref::().unwrap(); + if let Value::File(f_info) = params.get("file").unwrap() { + match app.add_file( + &f_info.path, + &f_info.filename.clone().map(|fname| PathBuf::from(fname)), + ) { + Ok(_) => Ok(Response::with(( + status::MovedPermanently, + Redirect(router::url_for(req, "index", HashMap::new())), + ))), + Err(_) => Ok(Response::with(status::InternalServerError)), + } + } else { + Ok(Response::with(status::BadRequest)) + } + } else { + Ok(Response::with(status::Forbidden)) + } + } + None => Ok(Response::with(status::Forbidden)), + } + } + } + + pub struct DeleteHandler { + pub app: Arc>, + } + + impl Handler for DeleteHandler { + fn handle(&self, req: &mut Request) -> IronResult { + let mut app = self.app.write().unwrap(); + let capture = req.extensions.get::().unwrap().clone(); + let m_token = req.extensions.get::(); + match m_token { + Some(token) => { + if token.check_authorizations(is_admin) { + match capture.find("id") { + Some(id) => match app.delete_file(String::from(id)) { + Ok(()) => Ok(Response::with(( + status::MovedPermanently, + Redirect(router::url_for(req, "index", HashMap::new())), + ))), + Err(_) => Ok(Response::with(status::InternalServerError)), + }, + None => Ok(Response::with(status::BadRequest)), + } + } else { + Ok(Response::with(status::Forbidden)) + } + } + None => Ok(Response::with(status::Forbidden)), + } + } + } +} + +fn css(_: &mut Request) -> IronResult { + let mut css: String = String::from(""); + File::open("templates/style.css") + .unwrap() + .read_to_string(&mut css) + .unwrap(); + Ok(Response::with(( + status::Ok, + Header(headers::ContentType(iron::mime::Mime( + iron::mime::TopLevel::Text, + iron::mime::SubLevel::Css, + vec![], + ))), + css, + ))) +} + +fn script(_: &mut Request) -> IronResult { + let mut js: String = String::from(""); + File::open("templates/script.js") + .unwrap() + .read_to_string(&mut js) + .unwrap(); + Ok(Response::with(( + status::Ok, + Header(headers::ContentType(iron::mime::Mime( + iron::mime::TopLevel::Text, + iron::mime::SubLevel::Javascript, + vec![], + ))), + js, + ))) +} + +fn main() { + let auth_db_path = std::env::var("ORIZENTIC_DB").unwrap(); + let secret = Secret(Vec::from( + std::env::var("ORIZENTIC_SECRET").unwrap().as_bytes(), + )); + let auth_middleware = Authentication::new(secret, auth_db_path); + + let app = Arc::new(RwLock::new(App::new(Path::new( + &std::env::var("FILE_SHARE_DIR").unwrap(), + )))); + + let mut router = Router::new(); + router.get( + "/", + files::IndexHandler { + app: app.clone(), + template: compile_path("templates/index.html").expect("the template to compile"), + }, + "index", + ); + + router.get( + "/:id", + files::GetFileHandler { + app: app.clone(), + template: compile_path("templates/file.html").expect("the template to compile"), + }, + "get-file-page", + ); + + router.get( + "/:id/raw", + files::GetHandler { app: app.clone() }, + "get-file", + ); + + router.get( + "/:id/tn", + files::GetThumbnailHandler { app: app.clone() }, + "get-thumbnail", + ); + + router.post("/", files::PostHandler { app: app.clone() }, "upload-file"); + + router.delete( + "/:id", + files::DeleteHandler { app: app.clone() }, + "delete-file", + ); + router.get("/css", css, "styles"); + router.get("/script", script, "script"); + + let mut chain = Chain::new(router); + chain.link_before(auth_middleware); + chain.link_before(RestForm {}); + + Iron::new(chain).http("0.0.0.0:3000").unwrap(); +} diff --git a/file-service/src/middleware/authentication.rs b/file-service/src/middleware/authentication.rs new file mode 100644 index 0000000..87c1445 --- /dev/null +++ b/file-service/src/middleware/authentication.rs @@ -0,0 +1,51 @@ +use iron::headers; +use iron::middleware::BeforeMiddleware; +use iron::prelude::*; +use iron::typemap::Key; +use orizentic::{filedb, OrizenticCtx, Secret}; +use params::{FromValue, Params}; + +use crate::cookies::{Cookie, CookieJar}; + +pub struct Authentication { + pub auth: OrizenticCtx, +} + +impl Key for Authentication { + type Value = orizentic::VerifiedToken; +} + +impl Authentication { + pub fn new(secret: Secret, auth_db_path: String) -> Authentication { + let claims = filedb::load_claims_from_file(&auth_db_path).expect("claims did not load"); + let orizentic = OrizenticCtx::new(secret, claims); + Authentication { auth: orizentic } + } + + fn authenticate_user( + &self, + token_str: String, + ) -> Result { + self.auth.decode_and_validate_text(&token_str) + } +} + +impl BeforeMiddleware for Authentication { + fn before(&self, req: &mut Request) -> IronResult<()> { + let params = req.get_ref::().unwrap(); + let token = match params.get("auth").and_then(|v| String::from_value(v)) { + Some(token_str) => self.authenticate_user(token_str).ok(), + None => { + let m_jar = req + .headers + .get::() + .map(|cookies| CookieJar::from(cookies)); + m_jar + .and_then(|jar| jar.lookup("auth").cloned()) + .and_then(|Cookie { value, .. }| self.authenticate_user(value.clone()).ok()) + } + }; + token.map(|t| req.extensions.insert::(t)); + Ok(()) + } +} diff --git a/file-service/src/middleware/logging.rs b/file-service/src/middleware/logging.rs new file mode 100644 index 0000000..2aceb1d --- /dev/null +++ b/file-service/src/middleware/logging.rs @@ -0,0 +1,16 @@ +use iron::middleware::{AfterMiddleware, BeforeMiddleware}; +use iron::prelude::*; + +pub struct Logging {} + +impl BeforeMiddleware for Logging { + fn before(&self, _: &mut Request) -> IronResult<()> { + Ok(()) + } +} + +impl AfterMiddleware for Logging { + fn after(&self, _: &mut Request, res: Response) -> IronResult { + Ok(res) + } +} diff --git a/file-service/src/middleware/mod.rs b/file-service/src/middleware/mod.rs new file mode 100644 index 0000000..1bab46b --- /dev/null +++ b/file-service/src/middleware/mod.rs @@ -0,0 +1,6 @@ +mod authentication; +mod logging; +mod restform; + +pub use authentication::Authentication; +pub use restform::RestForm; diff --git a/file-service/src/middleware/restform.rs b/file-service/src/middleware/restform.rs new file mode 100644 index 0000000..5cfadd9 --- /dev/null +++ b/file-service/src/middleware/restform.rs @@ -0,0 +1,34 @@ +use iron::method::Method; +use iron::middleware::BeforeMiddleware; +use iron::prelude::*; +use params::{Params, Value}; + +pub struct RestForm {} + +impl RestForm { + fn method(&self, v: &Value) -> Option { + match v { + Value::String(method_str) => match method_str.as_str() { + "delete" => Some(Method::Delete), + _ => None, + }, + _ => None, + } + } +} + +impl BeforeMiddleware for RestForm { + fn before(&self, req: &mut Request) -> IronResult<()> { + if req.method == Method::Post { + let method = { + let params = req.get_ref::().unwrap(); + params + .get("_method") + .and_then(|m| self.method(m)) + .unwrap_or(Method::Post) + }; + req.method = method; + } + Ok(()) + } +} diff --git a/file-service/templates/file.html b/file-service/templates/file.html new file mode 100644 index 0000000..45002cc --- /dev/null +++ b/file-service/templates/file.html @@ -0,0 +1,15 @@ + + + + {{title}} + + + + + + + + + + + \ No newline at end of file diff --git a/file-service/templates/index.html b/file-service/templates/index.html new file mode 100644 index 0000000..6c44b20 --- /dev/null +++ b/file-service/templates/index.html @@ -0,0 +1,54 @@ + + + + Admin list of files + + + + + +

Admin list of files

+ +
+
+
+ + +
+ +
+
+ +
+ {{#files}} +
+ {{#error}} +
+

{{error}}

+
+ {{/error}} + + {{#file}} +
+ +
+
+
    +
  • {{date}}
  • +
  • {{type_}}
  • +
  • {{size}}
  • +
+
+
+ + +
+
+
+ {{/file}} +
+ {{/files}} +
+ + + \ No newline at end of file diff --git a/file-service/templates/script.js b/file-service/templates/script.js new file mode 100644 index 0000000..9a820db --- /dev/null +++ b/file-service/templates/script.js @@ -0,0 +1,10 @@ +const selectFile = (selectorId) => { + console.log("wide arrow functions work: " + selectorId); + const input = document.querySelector("#" + selectorId + " input[type='file']") + const label = document.querySelector("#" + selectorId + " label") + input.addEventListener("change", (e) => { + if (input.files.length > 0) { + label.innerHTML = input.files[0].name + } + }) +} diff --git a/file-service/templates/style.css b/file-service/templates/style.css new file mode 100644 index 0000000..eeb6f8b --- /dev/null +++ b/file-service/templates/style.css @@ -0,0 +1,103 @@ +body { + font-family: 'Ariel', sans-serif; +} + +.files { + display: flex; + flex-wrap: wrap; +} + +.file { + display: flex; + margin: 1em; + border: 1px solid #449dfc; + border-radius: 5px; + padding: 1em; +} + +.thumbnail { + max-width: 320px; + margin: 1em; +} + +img { + max-width: 100%; +} + +[type="submit"] { + border-radius: 1em; + margin: 1em; + padding: 1em; +} + +.uploadform { + display: flex; + margin: 1em; + background-color: #e5f0fc; + border: 1px solid #449dfc; + border-radius: 5px; + padding: 1em; +} + +/* https://benmarshall.me/styling-file-inputs/ */ +[type="file"] { + border: 0; + clip: rect(0, 0, 0, 0); + height: 1px; + overflow: hidden; + padding: 0; + position: absolute !important; + white-space: nowrap; + width: 1px; +} + +[type="file"] + label { + background-color: rgb(0, 86, 112); + border-radius: 1em; + color: #fff; + cursor: pointer; + display: inline-block; + padding: 1em; + margin: 1em; + transition: background-color 0.3s; +} + +[type="file"]:focus + label, +[type="file"] + label:hover { + background-color: #67b0ff; +} + +[type="file"]:focus + label { + outline: 1px dotted #000; + outline: -webkit-focus-ring-color auto 5px; +} + +@media screen and (max-width: 980px) { /* This is the screen width of a OnePlus 5t */ + body { + font-size: xx-large; + } + + [type="submit"] { + font-size: xx-large; + width: 100%; + } + + .uploadform { + display: flex; + } + + [type="file"] + label { + width: 100%; + } + + .thumbnail { + max-width: 100%; + margin: 1em; + } + + .file { + display: flex; + flex-direction: column; + width: 100%; + } +} -- 2.44.1 From e36657591b5ff0db56dead3e273f7c6f74b1d388 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 19 Sep 2023 19:01:09 -0400 Subject: [PATCH 03/36] Add orizentic and file-service to the build --- Cargo.lock | 1483 ++++++++++++++--- Cargo.toml | 2 + build.sh | 2 + file-service/Cargo.toml | 32 +- file-service/src/middleware/authentication.rs | 2 +- 5 files changed, 1253 insertions(+), 268 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 90571a0..f15073f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,12 +18,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] -name = "aho-corasick" -version = "1.0.5" +name = "adler32" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] +name = "aho-corasick" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" dependencies = [ - "memchr", + "memchr 2.6.4", ] [[package]] @@ -41,12 +47,41 @@ dependencies = [ "libc", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "anyhow" version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" +dependencies = [ + "autocfg 1.1.0", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -63,16 +98,26 @@ dependencies = [ "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.7.1", "object", "rustc-demangle", ] [[package]] name = "base64" -version = "0.21.3" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53" +checksum = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" +dependencies = [ + "byteorder", + "safemem 0.3.3", +] + +[[package]] +name = "base64" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" [[package]] name = "bit-set" @@ -108,16 +153,66 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" [[package]] -name = "bumpalo" -version = "3.13.0" +name = "block-buffer" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +dependencies = [ + "block-padding", + "byte-tools", + "byteorder", + "generic-array 0.12.4", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +dependencies = [ + "byte-tools", +] + +[[package]] +name = "bodyparser" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f023abfa58aad6f6bc4ae0630799e24d5ee0ab8bb2e49f651d9b1f9aa4f52f30" +dependencies = [ + "iron", + "persistent", + "plugin", + "serde 1.0.188", + "serde_json", +] + +[[package]] +name = "buf_redux" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9279646319ff816b05fb5897883ece50d7d854d12b59992683d4f8a71b0f949" +dependencies = [ + "memchr 1.0.2", + "safemem 0.2.0", +] + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" [[package]] name = "bytemuck" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" [[package]] name = "byteorder" @@ -127,9 +222,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cairo-rs" @@ -167,9 +262,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b40ccee03b5175c18cde8f37e7d2a33bcef6f8ec8f7cc0d81090d1bb380949c9" +checksum = "03915af431787e6ffdcc74c645077518c6b6e01f80b761e0fbbfa288536311b3" dependencies = [ "smallvec", "target-lexicon", @@ -190,16 +285,15 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.27" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f56b4c72906975ca04becb8a30e102dfecddd0c06181e3e95ddc444be28881f8" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", - "serde", - "time", + "serde 1.0.188", "wasm-bindgen", "windows-targets", ] @@ -212,8 +306,8 @@ checksum = "f1369bc6b9e9a7dfdae2055f6ec151fe9c554a9d23d357c0237cee2e25eaabb7" dependencies = [ "chrono", "chrono-tz-build", - "phf", - "serde", + "phf 0.11.2", + "serde 1.0.188", ] [[package]] @@ -223,8 +317,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2f5ebdc942f57ed96d560a6d1a459bae5851102a25d5bf89dc04ae453e31ecf" dependencies = [ "parse-zoneinfo", - "phf", - "phf_codegen", + "phf 0.11.2", + "phf_codegen 0.11.2", +] + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags 1.3.2", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags 1.3.2", ] [[package]] @@ -239,7 +357,7 @@ version = "0.1.0" dependencies = [ "config-derive", "cool_asserts", - "serde", + "serde 1.0.188", "serde_json", "thiserror", ] @@ -295,16 +413,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam-channel" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - [[package]] name = "crossbeam-deque" version = "0.8.3" @@ -322,7 +430,7 @@ version = "0.9.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" dependencies = [ - "autocfg", + "autocfg 1.1.0", "cfg-if", "crossbeam-utils", "memoffset", @@ -356,7 +464,7 @@ dependencies = [ [[package]] name = "dashboard" -version = "0.1.0" +version = "0.1.1" dependencies = [ "cairo-rs", "chrono", @@ -374,22 +482,41 @@ dependencies = [ "libadwaita", "memorycache", "reqwest", - "serde", + "serde 1.0.188", "serde_derive", "serde_json", "tokio", "unic-langid", ] +[[package]] +name = "deflate" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" +dependencies = [ + "adler32", + "byteorder", +] + +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array 0.12.4", +] + [[package]] name = "dimensioned" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2517b0555262aeeda0d107a40ecfbbcf185921180ffb4acf316ebe0887467e26" dependencies = [ - "generic-array", + "generic-array 0.11.2", "num-traits", - "serde", + "serde 1.0.188", "typenum", ] @@ -401,7 +528,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -417,7 +544,7 @@ dependencies = [ "chrono", "chrono-tz", "dimensioned", - "serde", + "serde 1.0.188", "serde_derive", "serde_json", "tempfile", @@ -442,9 +569,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" +checksum = "add4f07d43996f76ef320709726a556a9d4f965d9410d8d0271132d2f8293480" dependencies = [ "errno-dragonfly", "libc", @@ -463,25 +590,31 @@ dependencies = [ [[package]] name = "exr" -version = "1.7.0" +version = "1.71.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1e481eb11a482815d3e9d618db8c42a93207134662873809335a92327440c18" +checksum = "832a761f35ab3e6664babfbdc6cef35a4860e816ec3916dcfd0882954e98a8a8" dependencies = [ "bit_field", "flume", "half", "lebe", - "miniz_oxide", + "miniz_oxide 0.7.1", "rayon-core", "smallvec", "zune-inflate", ] [[package]] -name = "fastrand" -version = "2.0.0" +name = "fake-simd" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "fdeflate" @@ -502,6 +635,28 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "file-service" +version = "0.1.0" +dependencies = [ + "chrono", + "hex-string", + "image 0.23.14", + "iron", + "logger", + "mime 0.3.17", + "mime_guess 2.0.4", + "mustache", + "orizentic", + "params", + "router", + "serde 1.0.188", + "serde_json", + "sha2", + "thiserror", + "uuid 0.4.0", +] + [[package]] name = "flate2" version = "1.0.27" @@ -509,7 +664,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.7.1", ] [[package]] @@ -575,14 +730,10 @@ dependencies = [ [[package]] name = "flume" -version = "0.10.14" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" dependencies = [ - "futures-core", - "futures-sink", - "nanorand", - "pin-project", "spin", ] @@ -613,9 +764,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ - "percent-encoding", + "percent-encoding 2.3.0", ] +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + [[package]] name = "futures" version = "0.3.28" @@ -672,7 +829,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -699,7 +856,7 @@ dependencies = [ "futures-macro", "futures-sink", "futures-task", - "memchr", + "memchr 2.6.4", "pin-project-lite", "pin-utils", "slab", @@ -774,6 +931,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + [[package]] name = "geo-types" version = "0.1.0" @@ -785,10 +951,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", - "wasm-bindgen", +] + +[[package]] +name = "gif" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" +dependencies = [ + "color_quant", + "weezl", ] [[package]] @@ -829,16 +1003,16 @@ dependencies = [ [[package]] name = "gio" -version = "0.18.1" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7884cba6b1c5db1607d970cadf44b14a43913d42bc68766eea6a5e2fe0891524" +checksum = "57052f84e8e5999b258e8adf8f5f2af0ac69033864936b8b6838321db2f759b1" dependencies = [ "futures-channel", "futures-core", "futures-io", "futures-util", "gio-sys 0.18.1", - "glib 0.18.1", + "glib 0.18.2", "libc", "once_cell", "pin-project-lite", @@ -889,7 +1063,7 @@ dependencies = [ "glib-sys 0.17.10", "gobject-sys 0.17.10", "libc", - "memchr", + "memchr 2.6.4", "once_cell", "smallvec", "thiserror", @@ -897,9 +1071,9 @@ dependencies = [ [[package]] name = "glib" -version = "0.18.1" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331156127e8166dd815cf8d2db3a5beb492610c716c03ee6db4f2d07092af0a7" +checksum = "1c316afb01ce8067c5eaab1fc4f2cd47dc21ce7b6296358605e2ffab23ccbd19" dependencies = [ "bitflags 2.4.0", "futures-channel", @@ -908,11 +1082,11 @@ dependencies = [ "futures-task", "futures-util", "gio-sys 0.18.1", - "glib-macros 0.18.0", + "glib-macros 0.18.2", "glib-sys 0.18.1", "gobject-sys 0.18.0", "libc", - "memchr", + "memchr 2.6.4", "once_cell", "smallvec", "thiserror", @@ -947,16 +1121,16 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "179643c50bf28d20d2f6eacd2531a88f2f5d9747dd0b86b8af1e8bb5dd0de3c0" +checksum = "f8da903822b136d42360518653fcf154455defc437d3e7a81475bf9a95ff1e47" dependencies = [ "heck", "proc-macro-crate", "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -992,7 +1166,7 @@ dependencies = [ "glib-build-tools 0.16.3", "gtk4", "libadwaita", - "serde", + "serde 1.0.188", "serde_json", "tokio", ] @@ -1175,9 +1349,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" [[package]] name = "heck" @@ -1187,9 +1361,18 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.2" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "hex-grid" @@ -1201,9 +1384,15 @@ dependencies = [ "glib 0.17.10", "glib-build-tools 0.16.3", "gtk4", - "image", + "image 0.24.7", ] +[[package]] +name = "hex-string" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "848ec2dd093df965a34b434580d94852197fc83feac5b2c1962399bbf2cb4f0b" + [[package]] name = "http" version = "0.2.9" @@ -1238,6 +1427,25 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hyper" +version = "0.10.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a0652d9a2609a968c14be1a9ea00bf4b1d64e2e1f53a1b51b6fff3a6e829273" +dependencies = [ + "base64 0.9.3", + "httparse", + "language-tags", + "log 0.3.9", + "mime 0.2.6", + "num_cpus", + "time", + "traitobject", + "typeable", + "unicase 1.4.2", + "url 1.7.2", +] + [[package]] name = "hyper" version = "0.14.27" @@ -1269,7 +1477,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper", + "hyper 0.14.27", "native-tls", "tokio", "tokio-native-tls", @@ -1298,6 +1506,17 @@ dependencies = [ "cc", ] +[[package]] +name = "idna" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idna" version = "0.4.0" @@ -1313,10 +1532,29 @@ name = "ifc" version = "0.1.0" dependencies = [ "chrono", - "serde", + "serde 1.0.188", "thiserror", ] +[[package]] +name = "image" +version = "0.23.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "gif 0.11.4", + "jpeg-decoder 0.1.22", + "num-iter", + "num-rational 0.3.2", + "num-traits", + "png 0.16.8", + "scoped_threadpool", + "tiff 0.6.1", +] + [[package]] name = "image" version = "0.24.7" @@ -1327,13 +1565,13 @@ dependencies = [ "byteorder", "color_quant", "exr", - "gif", - "jpeg-decoder", - "num-rational", + "gif 0.12.0", + "jpeg-decoder 0.3.0", + "num-rational 0.4.1", "num-traits", - "png", + "png 0.17.10", "qoi", - "tiff", + "tiff 0.9.0", ] [[package]] @@ -1348,18 +1586,18 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ - "autocfg", + "autocfg 1.1.0", "hashbrown 0.12.3", ] [[package]] name = "indexmap" -version = "2.0.0" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" dependencies = [ "equivalent", - "hashbrown 0.14.0", + "hashbrown 0.14.1", ] [[package]] @@ -1387,12 +1625,46 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" +[[package]] +name = "iron" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6d308ca2d884650a8bf9ed2ff4cb13fbb2207b71f64cda11dc9b892067295e8" +dependencies = [ + "hyper 0.10.16", + "log 0.3.9", + "mime_guess 1.8.8", + "modifier", + "num_cpus", + "plugin", + "typemap", + "url 1.7.2", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +[[package]] +name = "jpeg-decoder" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" +dependencies = [ + "rayon", +] + [[package]] name = "jpeg-decoder" version = "0.3.0" @@ -1411,6 +1683,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d438ea707d465c230305963b67f8357a1d56fcfad9434797d7cb1c46c2e41df" +dependencies = [ + "base64 0.9.3", + "chrono", + "ring", + "serde 1.0.188", + "serde_derive", + "serde_json", + "untrusted", +] + [[package]] name = "kifu-core" version = "0.1.0" @@ -1420,7 +1707,7 @@ dependencies = [ "config-derive", "cool_asserts", "grid", - "serde", + "serde 1.0.188", "serde_json", "sgf", "thiserror", @@ -1436,7 +1723,7 @@ dependencies = [ "glib 0.17.10", "glib-build-tools 0.17.10", "gtk4", - "image", + "image 0.24.7", "kifu-core", "libadwaita", "pango 0.18.0", @@ -1444,6 +1731,12 @@ dependencies = [ "tokio", ] +[[package]] +name = "language-tags" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" + [[package]] name = "lazy_static" version = "1.4.0" @@ -1491,9 +1784,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.147" +version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" [[package]] name = "libm" @@ -1502,10 +1795,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" [[package]] -name = "linux-raw-sys" -version = "0.4.5" +name = "linked-hash-map" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3852614a3bd9ca9804678ba6be5e3b8ce76dfc902cae004e3e0c44051b6e88db" [[package]] name = "lock_api" @@ -1513,10 +1812,19 @@ version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ - "autocfg", + "autocfg 1.1.0", "scopeguard", ] +[[package]] +name = "log" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" +dependencies = [ + "log 0.4.20", +] + [[package]] name = "log" version = "0.4.20" @@ -1524,10 +1832,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] -name = "memchr" -version = "2.6.1" +name = "logger" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f478948fd84d9f8e86967bf432640e46adfb5a4bd4f14ef7e864ab38220534ae" +checksum = "6c9172cb4c2f6c52117e25570983edcbb322f130b1031ae5d5d6b1abe7eeb493" +dependencies = [ + "iron", + "log 0.3.9", + "time", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "memchr" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "memoffset" @@ -1535,7 +1869,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ - "autocfg", + "autocfg 1.1.0", ] [[package]] @@ -1544,23 +1878,73 @@ version = "0.1.0" dependencies = [ "chrono", "futures", - "serde", + "serde 1.0.188", "serde_derive", "tokio", ] +[[package]] +name = "mime" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba626b8a6de5da682e1caa06bdb42a335aee5a84db8e5046a3e8ab17ba0a3ae0" +dependencies = [ + "log 0.3.9", +] + [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "1.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216929a5ee4dd316b1702eedf5e74548c123d370f47841ceaac38ca154690ca3" +dependencies = [ + "mime 0.2.6", + "phf 0.7.24", + "phf_codegen 0.7.24", + "unicase 1.4.2", +] + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime 0.3.17", + "unicase 2.7.0", +] + [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" +dependencies = [ + "adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg 1.1.0", +] + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -1583,12 +1967,36 @@ dependencies = [ ] [[package]] -name = "nanorand" -version = "0.7.0" +name = "modifier" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +checksum = "41f5c9112cb662acd3b204077e0de5bc66305fa8df65c8019d5adb10e9ab6e58" + +[[package]] +name = "multipart" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92f54eb45230c3aa20864ccf0c277eeaeadcf5e437e91731db498dbf7fbe0ec6" dependencies = [ - "getrandom", + "buf_redux", + "httparse", + "log 0.3.9", + "mime 0.2.6", + "mime_guess 1.8.8", + "rand 0.3.23", + "safemem 0.2.0", + "tempdir", + "twoway", +] + +[[package]] +name = "mustache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51956ef1c5d20a1384524d91e616fb44dfc7d8f249bf696d49c97dd3289ecab5" +dependencies = [ + "log 0.3.9", + "serde 1.0.188", ] [[package]] @@ -1599,7 +2007,7 @@ checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" dependencies = [ "lazy_static", "libc", - "log", + "log 0.4.20", "openssl", "openssl-probe", "openssl-sys", @@ -1621,7 +2029,7 @@ version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ - "memchr", + "memchr 2.6.4", "minimal-lexical", ] @@ -1633,13 +2041,83 @@ dependencies = [ "nom", ] +[[package]] +name = "num" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4703ad64153382334aa8db57c637364c322d3372e097840c72000dabdcf6156e" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational 0.1.42", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e63899ad0da84ce718c14936262a41cee2c79c981fc0a0e7c7beb47d5a07e8c1" +dependencies = [ + "num-integer", + "num-traits", + "rand 0.4.6", + "rustc-serialize", +] + +[[package]] +name = "num-complex" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b288631d7878aaf59442cffd36910ea604ecd7745c36054328595114001c9656" +dependencies = [ + "num-traits", + "rustc-serialize", +] + [[package]] name = "num-integer" version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ - "autocfg", + "autocfg 1.1.0", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg 1.1.0", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee314c74bd753fc86b4780aa9475da469155f3848473a261d2d18e35245a784e" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", + "rustc-serialize", +] + +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg 1.1.0", + "num-integer", "num-traits", ] @@ -1649,7 +2127,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" dependencies = [ - "autocfg", + "autocfg 1.1.0", "num-integer", "num-traits", ] @@ -1660,7 +2138,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ - "autocfg", + "autocfg 1.1.0", "libm", ] @@ -1670,17 +2148,17 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.3", "libc", ] [[package]] name = "object" -version = "0.32.0" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" dependencies = [ - "memchr", + "memchr 2.6.4", ] [[package]] @@ -1689,6 +2167,12 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + [[package]] name = "openssl" version = "0.10.57" @@ -1712,7 +2196,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -1723,9 +2207,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.92" +version = "0.9.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db7e971c2c2bba161b2d2fdf37080177eff520b3bc044787c7f1f5f9e78d869b" +checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" dependencies = [ "cc", "libc", @@ -1733,6 +2217,23 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "orizentic" +version = "1.0.1" +dependencies = [ + "chrono", + "clap", + "itertools", + "jsonwebtoken", + "serde 1.0.188", + "serde_derive", + "serde_json", + "thiserror", + "uuid 0.8.2", + "version_check 0.1.5", + "yaml-rust", +] + [[package]] name = "pango" version = "0.17.10" @@ -1753,8 +2254,8 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06a9e54b831d033206160096b825f2070cf5fda7e35167b1c01e9e774f9202d1" dependencies = [ - "gio 0.18.1", - "glib 0.18.1", + "gio 0.18.2", + "glib 0.18.2", "libc", "once_cell", "pango-sys 0.18.0", @@ -1784,6 +2285,22 @@ dependencies = [ "system-deps", ] +[[package]] +name = "params" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c789fdad2cfdaa551ea0e3a9eadb74c5d634968a9fb3a8c767d89be470d21589" +dependencies = [ + "bodyparser", + "iron", + "multipart", + "num", + "plugin", + "serde_json", + "tempdir", + "urlencoded", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -1816,19 +2333,54 @@ dependencies = [ "regex", ] +[[package]] +name = "percent-encoding" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" + [[package]] name = "percent-encoding" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +[[package]] +name = "persistent" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8fa0009c4f3d350281309909c618abddf10bb7e3145f28410782f6a5ec74c5" +dependencies = [ + "iron", + "plugin", +] + +[[package]] +name = "phf" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18" +dependencies = [ + "phf_shared 0.7.24", +] + [[package]] name = "phf" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ - "phf_shared", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_codegen" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b03e85129e324ad4166b06b2c7491ae27fe3ec353af72e72cd1654c7225d517e" +dependencies = [ + "phf_generator 0.7.24", + "phf_shared 0.7.24", ] [[package]] @@ -1837,8 +2389,18 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.11.2", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_generator" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662" +dependencies = [ + "phf_shared 0.7.24", + "rand 0.6.5", ] [[package]] @@ -1847,8 +2409,18 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" dependencies = [ - "phf_shared", - "rand", + "phf_shared 0.11.2", + "rand 0.8.5", +] + +[[package]] +name = "phf_shared" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" +dependencies = [ + "siphasher 0.2.3", + "unicase 1.4.2", ] [[package]] @@ -1857,27 +2429,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.29", + "siphasher 0.3.11", ] [[package]] @@ -1898,6 +2450,27 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "plugin" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a6a0dc3910bc8db877ffed8e457763b317cf880df4ae19109b9f77d277cf6e0" +dependencies = [ + "typemap", +] + +[[package]] +name = "png" +version = "0.16.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "deflate", + "miniz_oxide 0.3.7", +] + [[package]] name = "png" version = "0.17.10" @@ -1908,7 +2481,7 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", - "miniz_oxide", + "miniz_oxide 0.7.1", ] [[package]] @@ -1924,7 +2497,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "toml_edit", + "toml_edit 0.19.15", ] [[package]] @@ -1937,7 +2510,7 @@ dependencies = [ "proc-macro2", "quote", "syn 1.0.109", - "version_check", + "version_check 0.9.4", ] [[package]] @@ -1948,33 +2521,33 @@ checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ "proc-macro2", "quote", - "version_check", + "version_check 0.9.4", ] [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" dependencies = [ "unicode-ident", ] [[package]] name = "proptest" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e35c06b98bf36aba164cc17cb25f7e232f5c4aeea73baa14b8a9f0d92dbfa65" +checksum = "7c003ac8c77cb07bb74f5f198bce836a689bcd5a42574612bf14d17bfd08c20e" dependencies = [ "bit-set", - "bitflags 1.3.2", - "byteorder", + "bit-vec", + "bitflags 2.4.0", "lazy_static", "num-traits", - "rand", - "rand_chacha", - "rand_xorshift", - "regex-syntax 0.6.29", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_xorshift 0.3.0", + "regex-syntax", "rusty-fork", "tempfile", "unarray", @@ -2004,6 +2577,48 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" +dependencies = [ + "libc", + "rand 0.4.6", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "autocfg 0.1.8", + "libc", + "rand_chacha 0.1.1", + "rand_core 0.4.2", + "rand_hc", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg", + "rand_xorshift 0.1.1", + "winapi", +] + [[package]] name = "rand" version = "0.8.5" @@ -2011,8 +2626,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.3.1", ] [[package]] @@ -2022,9 +2647,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", ] +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + [[package]] name = "rand_core" version = "0.6.4" @@ -2034,20 +2674,82 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +dependencies = [ + "libc", + "rand_core 0.4.2", + "winapi", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.4.2", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.4.2", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "rand_xorshift" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" dependencies = [ - "rand_core", + "rand_core 0.6.4", ] [[package]] name = "rayon" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" dependencies = [ "either", "rayon-core", @@ -2055,14 +2757,21 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" dependencies = [ - "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", - "num_cpus", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", ] [[package]] @@ -2076,33 +2785,27 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.4" +version = "1.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29" +checksum = "ebee201405406dbf528b8b672104ae6d6d63e6d118cb10e4d51abbc7b58044ff" dependencies = [ "aho-corasick", - "memchr", + "memchr 2.6.4", "regex-automata", - "regex-syntax 0.7.5", + "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.3.7" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629" +checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" dependencies = [ "aho-corasick", - "memchr", - "regex-syntax 0.7.5", + "memchr 2.6.4", + "regex-syntax", ] -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - [[package]] name = "regex-syntax" version = "0.7.5" @@ -2110,12 +2813,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] -name = "reqwest" -version = "0.11.20" +name = "remove_dir_all" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ - "base64", + "winapi", +] + +[[package]] +name = "reqwest" +version = "0.11.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +dependencies = [ + "base64 0.21.4", "bytes", "encoding_rs", "futures-core", @@ -2123,29 +2835,59 @@ dependencies = [ "h2", "http", "http-body", - "hyper", + "hyper 0.14.27", "hyper-tls", "ipnet", "js-sys", - "log", - "mime", + "log 0.4.20", + "mime 0.3.17", "native-tls", "once_cell", - "percent-encoding", + "percent-encoding 2.3.0", "pin-project-lite", - "serde", + "serde 1.0.188", "serde_json", "serde_urlencoded", + "system-configuration", "tokio", "tokio-native-tls", "tower-service", - "url", + "url 2.4.1", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", "winreg", ] +[[package]] +name = "ring" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4db68a2e35f3497146b7e4563df7d4773a2433230c5e4b448328e31740458a" +dependencies = [ + "cc", + "lazy_static", + "libc", + "untrusted", +] + +[[package]] +name = "route-recognizer" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea509065eb0b3c446acdd0102f0d46567dc30902dc0be91d6552035d92b0f4f8" + +[[package]] +name = "router" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc63b6f3b8895b0d04e816b2b1aa58fdba2d5acca3cbb8f0ab8e017347d57397" +dependencies = [ + "iron", + "route-recognizer", + "url 1.7.2", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -2158,6 +2900,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-serialize" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" + [[package]] name = "rustc_version" version = "0.4.0" @@ -2169,9 +2917,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.10" +version = "0.38.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6248e1caa625eb708e266e06159f135e8c26f2bb7ceb72dc4b2766d0340964" +checksum = "d2f9da0cbd88f9f09e7814e388301c8414c51c62aa6ce1e4b5c551d49d96e531" dependencies = [ "bitflags 2.4.0", "errno", @@ -2198,6 +2946,18 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +[[package]] +name = "safemem" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f" + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + [[package]] name = "schannel" version = "0.1.22" @@ -2207,6 +2967,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" + [[package]] name = "scopeguard" version = "1.2.0" @@ -2251,9 +3017,15 @@ checksum = "1ef965a420fe14fdac7dd018862966a4c14094f900e1650bbc71ddd7d580c8af" [[package]] name = "semver" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" +checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0" + +[[package]] +name = "serde" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b623917345a631dc9608d5194cc206b3fe6c3554cd1c75b937e55e285254af" [[package]] name = "serde" @@ -2272,18 +3044,18 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] name = "serde_json" -version = "1.0.105" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" dependencies = [ "itoa", "ryu", - "serde", + "serde 1.0.188", ] [[package]] @@ -2292,7 +3064,7 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" dependencies = [ - "serde", + "serde 1.0.188", ] [[package]] @@ -2304,7 +3076,7 @@ dependencies = [ "form_urlencoded", "itoa", "ryu", - "serde", + "serde 1.0.188", ] [[package]] @@ -2314,11 +3086,23 @@ dependencies = [ "chrono", "cool_asserts", "nom", - "serde", + "serde 1.0.188", "thiserror", "typeshare", ] +[[package]] +name = "sha2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69" +dependencies = [ + "block-buffer", + "digest", + "fake-simd", + "opaque-debug", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -2334,6 +3118,12 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "siphasher" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" + [[package]] name = "siphasher" version = "0.3.11" @@ -2346,14 +3136,14 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ - "autocfg", + "autocfg 1.1.0", ] [[package]] name = "smallvec" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "socket2" @@ -2367,9 +3157,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" dependencies = [ "libc", "windows-sys", @@ -2384,6 +3174,12 @@ dependencies = [ "lock_api", ] +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + [[package]] name = "syn" version = "1.0.109" @@ -2397,9 +3193,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.29" +version = "2.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" +checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" dependencies = [ "proc-macro2", "quote", @@ -2407,10 +3203,31 @@ dependencies = [ ] [[package]] -name = "system-deps" -version = "6.1.1" +name = "system-configuration" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30c2de8a4d8f4b823d634affc9cd2a74ec98c53a756f317e529a48046cbf71f3" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "6.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94af52f9402f94aac4948a2518b43359be8d9ce6cd9efc1c4de3b2f7b7e897d6" dependencies = [ "cfg-expr", "heck", @@ -2425,6 +3242,16 @@ version = "0.12.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a" +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand 0.4.6", + "remove_dir_all", +] + [[package]] name = "tempfile" version = "3.8.0" @@ -2439,23 +3266,43 @@ dependencies = [ ] [[package]] -name = "thiserror" -version = "1.0.47" +name = "textwrap" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.47" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" +checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", +] + +[[package]] +name = "tiff" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437" +dependencies = [ + "jpeg-decoder 0.1.22", + "miniz_oxide 0.4.4", + "weezl", ] [[package]] @@ -2465,7 +3312,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d172b0f4d3fba17ba89811858b9d3d97f928aece846475bbda076ca46736211" dependencies = [ "flate2", - "jpeg-decoder", + "jpeg-decoder 0.3.0", "weezl", ] @@ -2482,9 +3329,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.1" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ac3f5b6856e931e15e07b478e98c8045239829a65f9156d4fa7e7788197a5ef" +checksum = "d5d0e245e80bdc9b4e5356fc45a72184abbc3861992603f515270e9340f5a219" dependencies = [ "displaydoc", ] @@ -2518,7 +3365,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.3", + "socket2 0.5.4", "tokio-macros", "windows-sys", ] @@ -2531,7 +3378,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -2546,9 +3393,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" dependencies = [ "bytes", "futures-core", @@ -2560,14 +3407,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.7.6" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" dependencies = [ - "serde", + "serde 1.0.188", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_edit 0.20.2", ] [[package]] @@ -2576,17 +3423,28 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" dependencies = [ - "serde", + "serde 1.0.188", ] [[package]] name = "toml_edit" -version = "0.19.14" +version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.0.0", - "serde", + "indexmap 2.0.2", + "toml_datetime", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.0.2", + "serde 1.0.188", "serde_spanned", "toml_datetime", "winnow", @@ -2618,12 +3476,27 @@ dependencies = [ "once_cell", ] +[[package]] +name = "traitobject" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079" + [[package]] name = "try-lock" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "twoway" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" +dependencies = [ + "memchr 2.6.4", +] + [[package]] name = "type-map" version = "0.4.0" @@ -2634,10 +3507,25 @@ dependencies = [ ] [[package]] -name = "typenum" -version = "1.16.0" +name = "typeable" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "1410f6f91f21d1612654e7cc69193b0334f909dcf2c790c4826254fbb86f8887" + +[[package]] +name = "typemap" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "653be63c80a3296da5551e1bfd2cca35227e13cdd08c6668903ae2f4f77aa1f6" +dependencies = [ + "unsafe-any", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "typeshare" @@ -2646,7 +3534,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f44d1a2f454cb35fbe05b218c410792697e76bd868f48d3a418f2cd1a7d527d6" dependencies = [ "chrono", - "serde", + "serde 1.0.188", "serde_json", "typeshare-annotation", ] @@ -2685,6 +3573,24 @@ dependencies = [ "tinystr", ] +[[package]] +name = "unicase" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4765f83163b74f957c797ad9253caf97f103fb064d3999aea9568d09fc8a33" +dependencies = [ + "version_check 0.1.5", +] + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check 0.9.4", +] + [[package]] name = "unicode-bidi" version = "0.3.13" @@ -2693,9 +3599,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" @@ -2706,6 +3612,38 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "unsafe-any" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30360d7979f5e9c6e6cea48af192ea8fab4afb3cf72597154b8f08935bc9c7f" +dependencies = [ + "traitobject", +] + +[[package]] +name = "untrusted" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cd1f4b4e96b46aeb8d4855db4a7a9bd96eeeb5c6a1ab54593328761642ce2f" + +[[package]] +name = "url" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" +dependencies = [ + "idna 0.1.5", + "matches", + "percent-encoding 1.0.1", +] + [[package]] name = "url" version = "2.4.1" @@ -2713,8 +3651,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", - "idna", - "percent-encoding", + "idna 0.4.0", + "percent-encoding 2.3.0", +] + +[[package]] +name = "urlencoded" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a52f50139118b60ae91af08bf15ed158817d34b91b9d24c11ffbe21195d33e3" +dependencies = [ + "bodyparser", + "iron", + "plugin", + "url 1.7.2", +] + +[[package]] +name = "uuid" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cfec50b0842181ba6e713151b72f4ec84a6a7e2c9c8a8a3ffc37bb1cd16b231" +dependencies = [ + "rand 0.3.23", + "serde 0.9.15", ] [[package]] @@ -2724,7 +3684,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ "getrandom", - "serde", + "serde 1.0.188", ] [[package]] @@ -2742,12 +3702,24 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "version-compare" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" +[[package]] +name = "version_check" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" + [[package]] name = "version_check" version = "0.9.4" @@ -2801,11 +3773,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", - "log", + "log 0.4.20", "once_cell", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", "wasm-bindgen-shared", ] @@ -2839,7 +3811,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2969,7 +3941,7 @@ version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" dependencies = [ - "memchr", + "memchr 2.6.4", ] [[package]] @@ -2982,6 +3954,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "zune-inflate" version = "0.2.54" diff --git a/Cargo.toml b/Cargo.toml index 6060504..f2c4da9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "cyberpunk-splash", "dashboard", "emseries", + "file-service", "flow", "fluent-ergonomics", "geo-types", @@ -16,6 +17,7 @@ members = [ "kifu/core", "kifu/gtk", "memorycache", + "orizentic", "screenplay", "sgf", "nom-training", diff --git a/build.sh b/build.sh index 1d443b2..41eee5c 100755 --- a/build.sh +++ b/build.sh @@ -10,6 +10,7 @@ RUST_ALL_TARGETS=( "cyberpunk-splash" "dashboard" "emseries" + "file-service" "flow" "fluent-ergonomics" "geo-types" @@ -19,6 +20,7 @@ RUST_ALL_TARGETS=( "kifu-core" "kifu-gtk" "memorycache" + "orizentic" "screenplay" "sgf" ) diff --git a/file-service/Cargo.toml b/file-service/Cargo.toml index fa1be72..d1e2f81 100644 --- a/file-service/Cargo.toml +++ b/file-service/Cargo.toml @@ -7,19 +7,19 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -chrono = { version = "0.4", features = ["serde"] } -hex-string = "0.1.0" -iron = "0.6.1" -logger = "*" -mime = "0.3.16" -mime_guess = "2.0.3" -mustache = "0.9.0" -orizentic = "1.0.0" -params = "*" -router = "*" -serde_json = "*" -serde = { version = "1.0", features = ["derive"] } -sha2 = "0.8.2" -uuid = { version = "0.4", features = ["serde", "v4"] } -thiserror = "1.0.20" -image = "0.23.5" \ No newline at end of file +chrono = { version = "0.4", features = ["serde"] } +hex-string = "0.1.0" +image = "0.23.5" +iron = "0.6.1" +logger = "*" +mime = "0.3.16" +mime_guess = "2.0.3" +mustache = "0.9.0" +orizentic = { path = "../orizentic" } +params = "*" +router = "*" +serde_json = "*" +serde = { version = "1.0", features = ["derive"] } +sha2 = "0.8.2" +thiserror = "1.0.20" +uuid = { version = "0.4", features = ["serde", "v4"] } diff --git a/file-service/src/middleware/authentication.rs b/file-service/src/middleware/authentication.rs index 87c1445..658bee8 100644 --- a/file-service/src/middleware/authentication.rs +++ b/file-service/src/middleware/authentication.rs @@ -26,7 +26,7 @@ impl Authentication { &self, token_str: String, ) -> Result { - self.auth.decode_and_validate_text(&token_str) + self.auth.decode_and_validate_text(token_str) } } -- 2.44.1 From 634c404ae9f70ab8a81fe5404a6b7ac833cc0af3 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Wed, 20 Sep 2023 23:06:34 -0400 Subject: [PATCH 04/36] Swap from iron to warp and start rebuilding the app --- Cargo.lock | 236 +++++++++++++++++- file-service/Cargo.toml | 6 +- file-service/fixtures/.metadata/rawr.png.json | 1 + file-service/fixtures/.thumbnails/rawr.png | Bin 0 -> 48869 bytes file-service/src/html.rs | 135 ++++++++++ file-service/src/lib/error.rs | 31 --- file-service/src/lib/file.rs | 55 +++- file-service/src/lib/fileinfo.rs | 39 ++- file-service/src/lib/mod.rs | 22 +- file-service/src/lib/thumbnail.rs | 20 +- file-service/src/main.rs | 73 ++++-- file-service/src/middleware/authentication.rs | 51 ---- file-service/src/middleware/logging.rs | 16 -- file-service/src/middleware/mod.rs | 6 - file-service/src/middleware/restform.rs | 34 --- file-service/src/pages.rs | 40 +++ 16 files changed, 553 insertions(+), 212 deletions(-) create mode 100644 file-service/fixtures/.metadata/rawr.png.json create mode 100644 file-service/fixtures/.thumbnails/rawr.png create mode 100644 file-service/src/html.rs delete mode 100644 file-service/src/lib/error.rs delete mode 100644 file-service/src/middleware/authentication.rs delete mode 100644 file-service/src/middleware/logging.rs delete mode 100644 file-service/src/middleware/mod.rs delete mode 100644 file-service/src/middleware/restform.rs create mode 100644 file-service/src/pages.rs diff --git a/Cargo.lock b/Cargo.lock index f15073f..6a473bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -164,6 +164,15 @@ dependencies = [ "generic-array 0.12.4", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array 0.14.7", +] + [[package]] name = "block-padding" version = "0.1.5" @@ -196,6 +205,12 @@ dependencies = [ "safemem 0.2.0", ] +[[package]] +name = "build_html" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3108fe6fe7ac796fb7625bdde8fa2b67b5a7731496251ca57c7b8cadd78a16a1" + [[package]] name = "bumpalo" version = "3.14.0" @@ -404,6 +419,15 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.3.2" @@ -452,6 +476,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array 0.14.7", + "typenum", +] + [[package]] name = "cyberpunk-splash" version = "0.1.0" @@ -489,6 +523,12 @@ dependencies = [ "unic-langid", ] +[[package]] +name = "data-encoding" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" + [[package]] name = "deflate" version = "0.8.6" @@ -508,6 +548,16 @@ dependencies = [ "generic-array 0.12.4", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", +] + [[package]] name = "dimensioned" version = "0.7.0" @@ -639,8 +689,10 @@ dependencies = [ name = "file-service" version = "0.1.0" dependencies = [ + "build_html", "chrono", "hex-string", + "http", "image 0.23.14", "iron", "logger", @@ -654,7 +706,9 @@ dependencies = [ "serde_json", "sha2", "thiserror", + "tokio", "uuid 0.4.0", + "warp", ] [[package]] @@ -940,6 +994,16 @@ dependencies = [ "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 0.9.4", +] + [[package]] name = "geo-types" version = "0.1.0" @@ -1353,6 +1417,30 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" +[[package]] +name = "headers" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" +dependencies = [ + "base64 0.21.4", + "bytes", + "headers-core", + "http", + "httpdate", + "mime 0.3.17", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.4.1" @@ -1972,6 +2060,24 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41f5c9112cb662acd3b204077e0de5bc66305fa8df65c8019d5adb10e9ab6e58" +[[package]] +name = "multer" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "log 0.4.20", + "memchr 2.6.4", + "mime 0.3.17", + "spin", + "version_check 0.9.4", +] + [[package]] name = "multipart" version = "0.13.6" @@ -2432,6 +2538,26 @@ dependencies = [ "siphasher 0.3.11", ] +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -2928,6 +3054,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64 0.21.4", +] + [[package]] name = "rusty-fork" version = "0.3.0" @@ -2967,6 +3102,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scoped_threadpool" version = "0.1.9" @@ -3091,14 +3232,25 @@ dependencies = [ "typeshare", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + [[package]] name = "sha2" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69" dependencies = [ - "block-buffer", - "digest", + "block-buffer 0.7.3", + "digest 0.8.1", "fake-simd", "opaque-debug", ] @@ -3391,6 +3543,29 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" +dependencies = [ + "futures-util", + "log 0.4.20", + "tokio", + "tungstenite", +] + [[package]] name = "tokio-util" version = "0.7.9" @@ -3463,6 +3638,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", + "log 0.4.20", "pin-project-lite", "tracing-core", ] @@ -3488,6 +3664,25 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log 0.4.20", + "rand 0.8.5", + "sha1", + "thiserror", + "url 2.4.1", + "utf-8", +] + [[package]] name = "twoway" version = "0.1.8" @@ -3667,6 +3862,12 @@ dependencies = [ "url 1.7.2", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "uuid" version = "0.4.0" @@ -3744,6 +3945,37 @@ dependencies = [ "try-lock", ] +[[package]] +name = "warp" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e92e22e03ff1230c03a1a8ee37d2f89cd489e2e541b7550d6afad96faed169" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "headers", + "http", + "hyper 0.14.27", + "log 0.4.20", + "mime 0.3.17", + "mime_guess 2.0.4", + "multer", + "percent-encoding 2.3.0", + "pin-project", + "rustls-pemfile", + "scoped-tls", + "serde 1.0.188", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-stream", + "tokio-tungstenite", + "tokio-util", + "tower-service", + "tracing", +] + [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" diff --git a/file-service/Cargo.toml b/file-service/Cargo.toml index d1e2f81..0afba1a 100644 --- a/file-service/Cargo.toml +++ b/file-service/Cargo.toml @@ -7,8 +7,10 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +build_html = { version = "2" } chrono = { version = "0.4", features = ["serde"] } hex-string = "0.1.0" +http = { version = "0.2" } image = "0.23.5" iron = "0.6.1" logger = "*" @@ -22,4 +24,6 @@ serde_json = "*" serde = { version = "1.0", features = ["derive"] } sha2 = "0.8.2" thiserror = "1.0.20" -uuid = { version = "0.4", features = ["serde", "v4"] } +tokio = { version = "1", features = [ "full" ] } +uuid = { version = "0.4", features = [ "serde", "v4" ] } +warp = { version = "0.3" } diff --git a/file-service/fixtures/.metadata/rawr.png.json b/file-service/fixtures/.metadata/rawr.png.json new file mode 100644 index 0000000..0c6259a --- /dev/null +++ b/file-service/fixtures/.metadata/rawr.png.json @@ -0,0 +1 @@ +{"id":"rawr.png","size":23777,"created":"2020-06-04T16:04:10.085680927Z","file_type":"image/png","hash":"b6cd35e113b95d62f53d9cbd27ccefef47d3e324aef01a2db6c0c6d3a43c89ee"} \ No newline at end of file diff --git a/file-service/fixtures/.thumbnails/rawr.png b/file-service/fixtures/.thumbnails/rawr.png new file mode 100644 index 0000000000000000000000000000000000000000..94ecf15f050cb1d1226a9de11b72a46ef9910c4b GIT binary patch literal 48869 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

eRTJ3zZ#y(l~ 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 Q4$Og-Y-Sm>SXQTt(g%~sKeFX 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, + elements: String, +} + +impl Form { + pub fn new() -> Self { + Self { + method: "get".to_owned(), + encoding: None, + elements: "".to_owned(), + } + } + + pub fn with_method(mut self, method: &str) -> Self { + self.method = method.to_owned(); + self + } + + pub fn with_encoding(mut self, encoding: &str) -> Self { + self.encoding = Some(encoding.to_owned()); + self + } +} + +impl Html for Form { + fn to_html_string(&self) -> String { + let encoding = match self.encoding { + Some(ref encoding) => format!("encoding={encoding}", encoding = encoding), + None => format!(""), + }; + format!( + "

\n{elements}\n\n", + method = self.method, + encoding = encoding, + elements = self.elements.to_html_string() + ) + } +} + +impl HtmlContainer for Form { + fn add_html(&mut self, html: H) { + self.elements.push_str(&html.to_html_string()); + } +} + +#[derive(Clone, Debug)] +pub struct Input { + ty: String, + name: String, + id: String, + value: Option, +} + +impl Html for Input { + fn to_html_string(&self) -> String { + format!( + "{value}\n", + ty = self.ty, + name = self.name, + id = self.id, + value = self.value.clone().unwrap_or("".to_owned()), + ) + } +} + +impl Input { + pub fn new(ty: &str, name: &str, id: &str) -> Self { + Self { + ty: ty.to_owned(), + name: name.to_owned(), + id: id.to_owned(), + value: None, + } + } + + pub fn with_value(mut self, val: &str) -> Self { + self.value = Some(val.to_owned()); + self + } +} + +#[derive(Clone, Debug)] +pub struct Label { + target: String, + text: String, +} + +impl Label { + pub fn new(target: &str, text: &str) -> Self { + Self { + target: target.to_owned(), + text: text.to_owned(), + } + } +} + +impl Html for Label { + fn to_html_string(&self) -> String { + format!( + "", + target = self.target, + text = self.text + ) + } +} + +#[derive(Clone, Debug)] +pub struct Button { + name: String, + text: String, +} + +impl Button { + pub fn new(name: &str, text: &str) -> Self { + Self { + name: name.to_owned(), + text: text.to_owned(), + } + } +} + +impl Html for Button { + fn to_html_string(&self) -> String { + format!( + "", + name = self.name, + text = self.text + ) + } +} diff --git a/file-service/src/lib/error.rs b/file-service/src/lib/error.rs deleted file mode 100644 index 53dc55f..0000000 --- a/file-service/src/lib/error.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::path::PathBuf; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum Error { - #[error("not implemented")] - NotImplemented, - - #[error("file not found: `{0}`")] - FileNotFound(PathBuf), - - #[error("file is not an image: `{0}`")] - NotAnImage(PathBuf), - - #[error("path is not a file: `{0}`")] - NotAFile(PathBuf), - - #[error("Image loading error")] - ImageError(#[from] image::ImageError), - - #[error("IO error")] - IOError(#[from] std::io::Error), - - #[error("JSON error")] - JSONError(#[from] serde_json::error::Error), - - #[error("UTF8 Error")] - UTF8Error(#[from] std::str::Utf8Error), -} - -pub type Result = std::result::Result; diff --git a/file-service/src/lib/file.rs b/file-service/src/lib/file.rs index 3a085fd..fad9e08 100644 --- a/file-service/src/lib/file.rs +++ b/file-service/src/lib/file.rs @@ -1,9 +1,38 @@ -use super::error::{Error, Result}; use super::fileinfo::FileInfo; use super::thumbnail::Thumbnail; use std::fs::{copy, read_dir, remove_file}; use std::path::{Path, PathBuf}; +use thiserror::Error; +#[derive(Error, Debug)] +pub enum FileError { + #[error("not implemented")] + NotImplemented, + + #[error("file not found: `{0}`")] + FileNotFound(PathBuf), + + #[error("file is not an image: `{0}`")] + NotAnImage(PathBuf), + + #[error("path is not a file: `{0}`")] + NotAFile(PathBuf), + + #[error("Image loading error")] + ImageError(#[from] image::ImageError), + + #[error("IO error")] + IOError(#[from] std::io::Error), + + #[error("JSON error")] + JSONError(#[from] serde_json::error::Error), + + #[error("UTF8 Error")] + UTF8Error(#[from] std::str::Utf8Error), +} + +/// One file in the database, complete with the path of the file and information about the +/// thumbnail of the file. #[derive(Debug)] pub struct File { info: FileInfo, @@ -17,7 +46,7 @@ impl File { root: &Path, temp_path: &PathBuf, filename: &Option, - ) -> Result { + ) -> Result { let mut dest_path = PathBuf::from(root); dest_path.push(id); match filename { @@ -33,27 +62,27 @@ impl File { copy(temp_path, dest_path.clone())?; let info = FileInfo::from_path(&dest_path)?; let tn = Thumbnail::from_path(&dest_path)?; - Ok(File { + Ok(Self { info, tn, root: PathBuf::from(root), }) } - pub fn open(id: &str, root: &Path) -> Result { + pub fn open(id: &str, root: &Path) -> Result { let mut file_path = PathBuf::from(root); file_path.push(id.clone()); if !file_path.exists() { - return Err(Error::FileNotFound(file_path)); + return Err(FileError::FileNotFound(file_path)); } if !file_path.is_file() { - return Err(Error::NotAFile(file_path)); + return Err(FileError::NotAFile(file_path)); } let info = match FileInfo::open(id, root) { Ok(i) => Ok(i), - Err(Error::FileNotFound(_)) => { + Err(FileError::FileNotFound(_)) => { let info = FileInfo::from_path(&file_path)?; info.save(&root)?; Ok(info) @@ -63,14 +92,14 @@ impl File { let tn = Thumbnail::open(id, root)?; - Ok(File { + Ok(Self { info, tn, root: PathBuf::from(root), }) } - pub fn list(root: &Path) -> Vec> { + pub fn list(root: &Path) -> Vec> { let dir_iter = read_dir(&root).unwrap(); dir_iter .filter(|entry| { @@ -81,7 +110,7 @@ impl File { .map(|entry| { let entry_ = entry.unwrap(); let id = entry_.file_name().into_string().unwrap(); - File::open(&id, root) + Self::open(&id, root) }) .collect() } @@ -94,13 +123,13 @@ impl File { self.tn.clone() } - pub fn stream(&self) -> Result { + pub fn stream(&self) -> Result { let mut path = self.root.clone(); path.push(self.info.id.clone()); - std::fs::File::open(path).map_err(Error::from) + std::fs::File::open(path).map_err(FileError::from) } - pub fn delete(&self) -> Result<()> { + pub fn delete(&self) -> Result<(), FileError> { let mut path = self.root.clone(); path.push(self.info.id.clone()); remove_file(path)?; diff --git a/file-service/src/lib/fileinfo.rs b/file-service/src/lib/fileinfo.rs index ee8350e..b3e6d49 100644 --- a/file-service/src/lib/fileinfo.rs +++ b/file-service/src/lib/fileinfo.rs @@ -7,8 +7,7 @@ use std::fs::remove_file; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; -use super::error::{Error, Result}; -use super::utils::append_extension; +use crate::{append_extension, FileError}; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct FileInfo { @@ -23,45 +22,45 @@ pub struct FileInfo { } impl FileInfo { - pub fn save(&self, root: &Path) -> Result<()> { + pub fn save(&self, root: &Path) -> Result<(), FileError> { let ser = serde_json::to_string(self).unwrap(); std::fs::File::create(FileInfo::metadata_path(&self.id, root)) .and_then(|mut stream| stream.write(ser.as_bytes()).map(|_| (()))) - .map_err(Error::from) + .map_err(FileError::from) } - pub fn open(id: &str, root: &Path) -> Result { + pub fn open(id: &str, root: &Path) -> Result { let mut buf = Vec::new(); let md_path = FileInfo::metadata_path(id, root); std::fs::File::open(md_path.clone()) .and_then(|mut stream| stream.read_to_end(&mut buf)) .map_err(move |err| match err.kind() { - std::io::ErrorKind::NotFound => Error::FileNotFound(md_path), - _ => Error::IOError(err), + std::io::ErrorKind::NotFound => FileError::FileNotFound(md_path), + _ => FileError::IOError(err), })?; let str_repr = std::str::from_utf8(&buf)?; - serde_json::from_str(&str_repr).map_err(Error::from) + serde_json::from_str(&str_repr).map_err(FileError::from) } - pub fn from_path(path: &Path) -> Result { + pub fn from_path(path: &Path) -> Result { match (path.is_file(), path.is_dir()) { - (false, false) => Err(Error::FileNotFound(PathBuf::from(path))), - (false, true) => Err(Error::NotAFile(PathBuf::from(path))), + (false, false) => Err(FileError::FileNotFound(PathBuf::from(path))), + (false, true) => Err(FileError::NotAFile(PathBuf::from(path))), (true, _) => Ok(()), }?; - let metadata = path.metadata().map_err(Error::IOError)?; + let metadata = path.metadata().map_err(FileError::IOError)?; let id = path .file_name() .map(|s| String::from(s.to_string_lossy())) - .ok_or(Error::NotAFile(PathBuf::from(path)))?; + .ok_or(FileError::NotAFile(PathBuf::from(path)))?; let created = metadata .created() .map(|m| DateTime::from(m)) - .map_err(|err| Error::IOError(err))?; + .map_err(|err| FileError::IOError(err))?; let file_type = String::from( mime_guess::from_path(path) .first_or_octet_stream() @@ -71,18 +70,18 @@ impl FileInfo { Ok(FileInfo { id, size: metadata.len(), - created: created, + created, file_type, hash: hash.as_string(), root: PathBuf::from(path.parent().unwrap()), }) } - fn hash_file(path: &Path) -> Result { + fn hash_file(path: &Path) -> Result { let mut buf = Vec::new(); - let mut file = std::fs::File::open(path).map_err(Error::from)?; + let mut file = std::fs::File::open(path).map_err(FileError::from)?; - file.read_to_end(&mut buf).map_err(Error::from)?; + file.read_to_end(&mut buf).map_err(FileError::from)?; let mut vec = Vec::new(); vec.extend_from_slice(Sha256::digest(&buf).as_slice()); Ok(HexString::from_bytes(&vec)) @@ -95,9 +94,9 @@ impl FileInfo { append_extension(&path, "json") } - pub fn delete(&self) -> Result<()> { + pub fn delete(&self) -> Result<(), FileError> { let path = FileInfo::metadata_path(&self.id, &self.root); - remove_file(path).map_err(Error::from) + remove_file(path).map_err(FileError::from) } } diff --git a/file-service/src/lib/mod.rs b/file-service/src/lib/mod.rs index 40e80c2..3c9d0af 100644 --- a/file-service/src/lib/mod.rs +++ b/file-service/src/lib/mod.rs @@ -1,14 +1,12 @@ use std::path::{Path, PathBuf}; use uuid::Uuid; -mod error; mod file; mod fileinfo; mod thumbnail; -mod utils; +pub mod utils; -pub use error::{Error, Result}; -pub use file::File; +pub use file::{File, FileError}; pub use fileinfo::FileInfo; pub use thumbnail::Thumbnail; @@ -23,32 +21,36 @@ impl App { } } - pub fn list_files(&self) -> Vec> { + pub fn list_files(&self) -> Vec> { File::list(&self.files_root) } - pub fn add_file(&mut self, temp_path: &PathBuf, filename: &Option) -> Result { + pub fn add_file( + &mut self, + temp_path: &PathBuf, + filename: &Option, + ) -> Result { let id = Uuid::new_v4().hyphenated().to_string(); File::new(&id, &self.files_root, temp_path, filename) } - pub fn delete_file(&mut self, id: String) -> Result<()> { + pub fn delete_file(&mut self, id: String) -> Result<(), FileError> { let f = File::open(&id, &self.files_root)?; f.delete() } - pub fn get_metadata(&self, id: String) -> Result { + pub fn get_metadata(&self, id: String) -> Result { FileInfo::open(&id, &self.files_root) } - pub fn get_file(&self, id: String) -> Result<(FileInfo, std::fs::File)> { + pub fn get_file(&self, id: String) -> Result<(FileInfo, std::fs::File), FileError> { let f = File::open(&id, &self.files_root)?; let info = f.info(); let stream = f.stream()?; Ok((info, stream)) } - pub fn get_thumbnail(&self, id: &str) -> Result<(FileInfo, std::fs::File)> { + pub fn get_thumbnail(&self, id: &str) -> Result<(FileInfo, std::fs::File), FileError> { let f = File::open(id, &self.files_root)?; let stream = f.thumbnail().stream()?; Ok((f.info(), stream)) diff --git a/file-service/src/lib/thumbnail.rs b/file-service/src/lib/thumbnail.rs index 738b4aa..1e458ad 100644 --- a/file-service/src/lib/thumbnail.rs +++ b/file-service/src/lib/thumbnail.rs @@ -2,7 +2,7 @@ use image::imageops::FilterType; use std::fs::remove_file; use std::path::{Path, PathBuf}; -use super::error::{Error, Result}; +use crate::FileError; #[derive(Clone, Debug, PartialEq)] pub struct Thumbnail { @@ -11,7 +11,7 @@ pub struct Thumbnail { } impl Thumbnail { - pub fn open(id: &str, root: &Path) -> Result { + pub fn open(id: &str, root: &Path) -> Result { let mut source_path = PathBuf::from(root); source_path.push(id); @@ -30,15 +30,15 @@ impl Thumbnail { Ok(self_) } - pub fn from_path(path: &Path) -> Result { + pub fn from_path(path: &Path) -> Result { let id = path .file_name() .map(|s| String::from(s.to_string_lossy())) - .ok_or(Error::NotAnImage(PathBuf::from(path)))?; + .ok_or(FileError::NotAnImage(PathBuf::from(path)))?; let root = path .parent() - .ok_or(Error::FileNotFound(PathBuf::from(path)))?; + .ok_or(FileError::FileNotFound(PathBuf::from(path)))?; Thumbnail::open(&id, root) } @@ -50,20 +50,20 @@ impl Thumbnail { path } - pub fn stream(&self) -> Result { + pub fn stream(&self) -> Result { let thumbnail_path = Thumbnail::thumbnail_path(&self.id, &self.root); std::fs::File::open(thumbnail_path.clone()).map_err(|err| { if err.kind() == std::io::ErrorKind::NotFound { - Error::FileNotFound(thumbnail_path) + FileError::FileNotFound(thumbnail_path) } else { - Error::from(err) + FileError::from(err) } }) } - pub fn delete(&self) -> Result<()> { + pub fn delete(&self) -> Result<(), FileError> { let path = Thumbnail::thumbnail_path(&self.id, &self.root); - remove_file(path).map_err(Error::from) + remove_file(path).map_err(FileError::from) } } diff --git a/file-service/src/main.rs b/file-service/src/main.rs index 67f1401..addd532 100644 --- a/file-service/src/main.rs +++ b/file-service/src/main.rs @@ -1,28 +1,31 @@ +/* use iron::headers; use iron::middleware::Handler; use iron::modifiers::{Header, Redirect}; use iron::prelude::*; use iron::response::BodyReader; use iron::status; -use mustache::{compile_path, Template}; -use orizentic::{Permissions, ResourceName, Secret}; -use params::{Params, Value}; -use router::Router; -use serde::Serialize; -use std::collections::HashMap; -use std::fs::File; -use std::io::Read; -use std::path::Path; -use std::path::PathBuf; -use std::sync::{Arc, RwLock}; +*/ +use http::status::StatusCode; +// use mustache::{compile_path, Template}; +// use orizentic::{Permissions, ResourceName, Secret}; +use build_html::Html; +use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + path::Path, + sync::{Arc, RwLock}, +}; +use warp::Filter; mod cookies; +mod html; mod lib; mod middleware; +mod pages; -use lib::{App, FileInfo}; -use middleware::{Authentication, RestForm}; +use lib::{utils::append_extension, App, File, FileError, FileInfo}; +/* fn is_admin(resource: &ResourceName, permissions: &Permissions) -> bool { let Permissions(perms) = permissions; ResourceName(String::from( @@ -280,18 +283,17 @@ fn script(_: &mut Request) -> IronResult { js, ))) } +*/ -fn main() { +#[tokio::main] +pub async fn main() { + /* let auth_db_path = std::env::var("ORIZENTIC_DB").unwrap(); let secret = Secret(Vec::from( std::env::var("ORIZENTIC_SECRET").unwrap().as_bytes(), )); let auth_middleware = Authentication::new(secret, auth_db_path); - let app = Arc::new(RwLock::new(App::new(Path::new( - &std::env::var("FILE_SHARE_DIR").unwrap(), - )))); - let mut router = Router::new(); router.get( "/", @@ -338,4 +340,39 @@ fn main() { chain.link_before(RestForm {}); Iron::new(chain).http("0.0.0.0:3000").unwrap(); + */ + + /* + let root = warp::path!().and(warp::get()).map({ + || { + warp::http::Response::builder() + .header("content-type", "text/html") + .status(StatusCode::NOT_MODIFIED) + .body(()) + } + }); + let server = warp::serve(root); + server + .run(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8002)) + .await; + */ + + let app = Arc::new(RwLock::new(App::new(Path::new( + &std::env::var("FILE_SHARE_DIR").unwrap(), + )))); + + let root = warp::path!().map({ + let app = app.clone(); + move || { + warp::http::Response::builder() + .header("content-type", "text/html") + .status(StatusCode::OK) + .body(pages::index(app.read().unwrap().list_files()).to_html_string()) + } + }); + + let server = warp::serve(root); + server + .run(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8002)) + .await; } diff --git a/file-service/src/middleware/authentication.rs b/file-service/src/middleware/authentication.rs deleted file mode 100644 index 658bee8..0000000 --- a/file-service/src/middleware/authentication.rs +++ /dev/null @@ -1,51 +0,0 @@ -use iron::headers; -use iron::middleware::BeforeMiddleware; -use iron::prelude::*; -use iron::typemap::Key; -use orizentic::{filedb, OrizenticCtx, Secret}; -use params::{FromValue, Params}; - -use crate::cookies::{Cookie, CookieJar}; - -pub struct Authentication { - pub auth: OrizenticCtx, -} - -impl Key for Authentication { - type Value = orizentic::VerifiedToken; -} - -impl Authentication { - pub fn new(secret: Secret, auth_db_path: String) -> Authentication { - let claims = filedb::load_claims_from_file(&auth_db_path).expect("claims did not load"); - let orizentic = OrizenticCtx::new(secret, claims); - Authentication { auth: orizentic } - } - - fn authenticate_user( - &self, - token_str: String, - ) -> Result { - self.auth.decode_and_validate_text(token_str) - } -} - -impl BeforeMiddleware for Authentication { - fn before(&self, req: &mut Request) -> IronResult<()> { - let params = req.get_ref::().unwrap(); - let token = match params.get("auth").and_then(|v| String::from_value(v)) { - Some(token_str) => self.authenticate_user(token_str).ok(), - None => { - let m_jar = req - .headers - .get::() - .map(|cookies| CookieJar::from(cookies)); - m_jar - .and_then(|jar| jar.lookup("auth").cloned()) - .and_then(|Cookie { value, .. }| self.authenticate_user(value.clone()).ok()) - } - }; - token.map(|t| req.extensions.insert::(t)); - Ok(()) - } -} diff --git a/file-service/src/middleware/logging.rs b/file-service/src/middleware/logging.rs deleted file mode 100644 index 2aceb1d..0000000 --- a/file-service/src/middleware/logging.rs +++ /dev/null @@ -1,16 +0,0 @@ -use iron::middleware::{AfterMiddleware, BeforeMiddleware}; -use iron::prelude::*; - -pub struct Logging {} - -impl BeforeMiddleware for Logging { - fn before(&self, _: &mut Request) -> IronResult<()> { - Ok(()) - } -} - -impl AfterMiddleware for Logging { - fn after(&self, _: &mut Request, res: Response) -> IronResult { - Ok(res) - } -} diff --git a/file-service/src/middleware/mod.rs b/file-service/src/middleware/mod.rs deleted file mode 100644 index 1bab46b..0000000 --- a/file-service/src/middleware/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -mod authentication; -mod logging; -mod restform; - -pub use authentication::Authentication; -pub use restform::RestForm; diff --git a/file-service/src/middleware/restform.rs b/file-service/src/middleware/restform.rs deleted file mode 100644 index 5cfadd9..0000000 --- a/file-service/src/middleware/restform.rs +++ /dev/null @@ -1,34 +0,0 @@ -use iron::method::Method; -use iron::middleware::BeforeMiddleware; -use iron::prelude::*; -use params::{Params, Value}; - -pub struct RestForm {} - -impl RestForm { - fn method(&self, v: &Value) -> Option { - match v { - Value::String(method_str) => match method_str.as_str() { - "delete" => Some(Method::Delete), - _ => None, - }, - _ => None, - } - } -} - -impl BeforeMiddleware for RestForm { - fn before(&self, req: &mut Request) -> IronResult<()> { - if req.method == Method::Post { - let method = { - let params = req.get_ref::().unwrap(); - params - .get("_method") - .and_then(|m| self.method(m)) - .unwrap_or(Method::Post) - }; - req.method = method; - } - Ok(()) - } -} diff --git a/file-service/src/pages.rs b/file-service/src/pages.rs new file mode 100644 index 0000000..e1385df --- /dev/null +++ b/file-service/src/pages.rs @@ -0,0 +1,40 @@ +use crate::{html::*, File, FileError}; +use build_html::{self, Container, ContainerType, HtmlContainer}; + +pub fn index(files: Vec>) -> build_html::HtmlPage { + let mut page = build_html::HtmlPage::new() + .with_title("Admin list of files") + .with_header(1, "Admin list of files") + .with_html( + Form::new() + .with_method("post") + .with_encoding("multipart/form-data") + .with_container( + Container::new(ContainerType::Div) + .with_html(Input::new("file", "file", "file-selector-input")) + .with_html(Label::new("for-selector-input", "Select a file")), + ) + .with_html(Button::new("upload", "Upload file")), + ); + + for file in files { + let mut container = + Container::new(ContainerType::Div).with_attributes(vec![("class", "file")]); + match file { + Ok(file) => { + let tn = Container::new(ContainerType::Div) + .with_attributes(vec![("class", "thumbnail")]) + .with_link( + format!("/file/{}", file.info().id), + "

paragraph within the link

".to_owned(), + ); + container.add_html(tn); + } + Err(err) => { + container.add_paragraph(format!("{:?}", err)); + } + } + page.add_container(container); + } + page +} -- 2.44.1 From f05e0a15f14f8999a9364fee739102704929fe41 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Wed, 20 Sep 2023 23:31:52 -0400 Subject: [PATCH 05/36] Render thumbnails --- file-service/src/html.rs | 19 ++++++++ file-service/src/main.rs | 95 +++++++++------------------------------ file-service/src/pages.rs | 4 +- 3 files changed, 42 insertions(+), 76 deletions(-) diff --git a/file-service/src/html.rs b/file-service/src/html.rs index d83b34a..02cffd3 100644 --- a/file-service/src/html.rs +++ b/file-service/src/html.rs @@ -133,3 +133,22 @@ impl Html for Button { ) } } + +#[derive(Clone, Debug)] +pub struct Image { + path: String, +} + +impl Image { + pub fn new(path: &str) -> Self { + Self { + path: path.to_owned(), + } + } +} + +impl Html for Image { + fn to_html_string(&self) -> String { + format!("", path = self.path,) + } +} diff --git a/file-service/src/main.rs b/file-service/src/main.rs index addd532..f15d2a0 100644 --- a/file-service/src/main.rs +++ b/file-service/src/main.rs @@ -11,6 +11,7 @@ use http::status::StatusCode; // use orizentic::{Permissions, ResourceName, Secret}; use build_html::Html; use std::{ + io::Read, net::{IpAddr, Ipv4Addr, SocketAddr}, path::Path, sync::{Arc, RwLock}, @@ -45,71 +46,6 @@ pub fn compare_etags(info: FileInfo, etag_list: &headers::IfNoneMatch) -> bool { mod files { use super::*; - pub struct IndexHandler { - pub app: Arc>, - pub template: Template, - } - - #[derive(Serialize)] - pub enum TemplateFile { - #[serde(rename = "error")] - Error { error: String }, - #[serde(rename = "file")] - File { - id: String, - size: u64, - date: String, - type_: String, - }, - } - - #[derive(Serialize)] - pub struct IndexTemplateParams { - files: Vec, - } - - impl Handler for IndexHandler { - fn handle(&self, req: &mut Request) -> IronResult { - let app = self.app.read().unwrap(); - let m_token = req.extensions.get::(); - match m_token { - Some(token) => { - if token.check_authorizations(is_admin) { - let files: Vec = app - .list_files() - .into_iter() - .map(|entry| match entry { - Ok(file) => TemplateFile::File { - id: file.info().id, - size: file.info().size, - date: format!( - "{}", - file.info().created.format("%Y-%m-%d %H:%M:%S") - ), - type_: file.info().file_type, - }, - Err(err) => TemplateFile::Error { - error: format!("{}", err), - }, - }) - .collect(); - Ok(Response::with(( - status::Ok, - Header(headers::ContentType::html()), - Header(headers::SetCookie(vec![format!("auth={}", token.text)])), - self.template - .render_to_string(&IndexTemplateParams { files }) - .expect("the template to render"), - ))) - } else { - Ok(Response::with(status::Forbidden)) - } - } - None => Ok(Response::with(status::Forbidden)), - } - } - } - pub struct GetHandler { pub app: Arc>, } @@ -295,14 +231,6 @@ pub async fn main() { let auth_middleware = Authentication::new(secret, auth_db_path); let mut router = Router::new(); - router.get( - "/", - files::IndexHandler { - app: app.clone(), - template: compile_path("templates/index.html").expect("the template to compile"), - }, - "index", - ); router.get( "/:id", @@ -371,7 +299,26 @@ pub async fn main() { } }); - let server = warp::serve(root); + let thumbnail = warp::path!(String / "tn").map({ + let app = app.clone(); + move |id: String| { + let mut content = Vec::new(); + match app.read().unwrap().get_thumbnail(&id) { + Ok((info, mut stream)) => { + let _ = stream.read_to_end(&mut content); + warp::http::Response::builder() + .header("content-type", info.file_type) + .status(StatusCode::OK) + .body(content) + } + Err(_err) => warp::http::Response::builder() + .status(StatusCode::NOT_FOUND) + .body(content), + } + } + }); + + let server = warp::serve(root.or(thumbnail)); server .run(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8002)) .await; diff --git a/file-service/src/pages.rs b/file-service/src/pages.rs index e1385df..3f61170 100644 --- a/file-service/src/pages.rs +++ b/file-service/src/pages.rs @@ -1,5 +1,5 @@ use crate::{html::*, File, FileError}; -use build_html::{self, Container, ContainerType, HtmlContainer}; +use build_html::{self, Container, ContainerType, Html, HtmlContainer}; pub fn index(files: Vec>) -> build_html::HtmlPage { let mut page = build_html::HtmlPage::new() @@ -26,7 +26,7 @@ pub fn index(files: Vec>) -> build_html::HtmlPage { .with_attributes(vec![("class", "thumbnail")]) .with_link( format!("/file/{}", file.info().id), - "

paragraph within the link

".to_owned(), + Image::new(&format!("{}/tn", file.info().id)).to_html_string(), ); container.add_html(tn); } -- 2.44.1 From a06c9fae2579ddb213d715c079aaf6918dd09272 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Thu, 21 Sep 2023 00:00:09 -0400 Subject: [PATCH 06/36] Attempt to add etag caching --- file-service/src/main.rs | 49 +++++++++++++++++++++------------------ file-service/src/pages.rs | 2 +- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/file-service/src/main.rs b/file-service/src/main.rs index f15d2a0..7410ca8 100644 --- a/file-service/src/main.rs +++ b/file-service/src/main.rs @@ -247,12 +247,6 @@ pub async fn main() { "get-file", ); - router.get( - "/:id/tn", - files::GetThumbnailHandler { app: app.clone() }, - "get-thumbnail", - ); - router.post("/", files::PostHandler { app: app.clone() }, "upload-file"); router.delete( @@ -299,24 +293,35 @@ pub async fn main() { } }); - let thumbnail = warp::path!(String / "tn").map({ - let app = app.clone(); - move |id: String| { - let mut content = Vec::new(); - match app.read().unwrap().get_thumbnail(&id) { - Ok((info, mut stream)) => { - let _ = stream.read_to_end(&mut content); - warp::http::Response::builder() - .header("content-type", info.file_type) - .status(StatusCode::OK) - .body(content) + let thumbnail = warp::path!(String / "tn") + .and(warp::header::optional::("if-none-match")) + .map({ + let app = app.clone(); + move |id: String, old_etags: Option| { + let mut content = Vec::new(); + match app.read().unwrap().get_thumbnail(&id) { + Ok((info, mut stream)) => match old_etags { + Some(old_etags) if old_etags != info.hash => { + warp::http::Response::builder() + .header("content-type", info.file_type) + .status(StatusCode::NOT_MODIFIED) + .body(content) + } + _ => { + let _ = stream.read_to_end(&mut content); + warp::http::Response::builder() + .header("content-type", info.file_type) + .header("etag", info.hash) + .status(StatusCode::OK) + .body(content) + } + }, + Err(_err) => warp::http::Response::builder() + .status(StatusCode::NOT_FOUND) + .body(content), } - Err(_err) => warp::http::Response::builder() - .status(StatusCode::NOT_FOUND) - .body(content), } - } - }); + }); let server = warp::serve(root.or(thumbnail)); server diff --git a/file-service/src/pages.rs b/file-service/src/pages.rs index 3f61170..12a92e1 100644 --- a/file-service/src/pages.rs +++ b/file-service/src/pages.rs @@ -25,7 +25,7 @@ pub fn index(files: Vec>) -> build_html::HtmlPage { let tn = Container::new(ContainerType::Div) .with_attributes(vec![("class", "thumbnail")]) .with_link( - format!("/file/{}", file.info().id), + format!("/{}", file.info().id), Image::new(&format!("{}/tn", file.info().id)).to_html_string(), ); container.add_html(tn); -- 2.44.1 From 4a7b23544e924dc914c00a119b44691fd722b731 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Thu, 21 Sep 2023 22:15:58 -0400 Subject: [PATCH 07/36] Refactor file and thumbnail serving to common code --- file-service/src/lib/mod.rs | 2 +- file-service/src/main.rs | 68 ++++++++++++++++++++++++------------- 2 files changed, 45 insertions(+), 25 deletions(-) diff --git a/file-service/src/lib/mod.rs b/file-service/src/lib/mod.rs index 3c9d0af..0d43cac 100644 --- a/file-service/src/lib/mod.rs +++ b/file-service/src/lib/mod.rs @@ -43,7 +43,7 @@ impl App { FileInfo::open(&id, &self.files_root) } - pub fn get_file(&self, id: String) -> Result<(FileInfo, std::fs::File), FileError> { + pub fn get_file(&self, id: &str) -> Result<(FileInfo, std::fs::File), FileError> { let f = File::open(&id, &self.files_root)?; let info = f.info(); let stream = f.stream()?; diff --git a/file-service/src/main.rs b/file-service/src/main.rs index 7410ca8..14ccb28 100644 --- a/file-service/src/main.rs +++ b/file-service/src/main.rs @@ -221,6 +221,28 @@ fn script(_: &mut Request) -> IronResult { } */ +fn serve_file( + info: FileInfo, + mut file: std::fs::File, + old_etags: Option, +) -> http::Result>> { + let mut content = Vec::new(); + match old_etags { + Some(old_etags) if old_etags != info.hash => warp::http::Response::builder() + .header("content-type", info.file_type) + .status(StatusCode::NOT_MODIFIED) + .body(content), + _ => { + let _ = file.read_to_end(&mut content); + warp::http::Response::builder() + .header("content-type", info.file_type) + .header("etag", info.hash) + .status(StatusCode::OK) + .body(content) + } + } +} + #[tokio::main] pub async fn main() { /* @@ -297,33 +319,31 @@ pub async fn main() { .and(warp::header::optional::("if-none-match")) .map({ let app = app.clone(); - move |id: String, old_etags: Option| { - let mut content = Vec::new(); - match app.read().unwrap().get_thumbnail(&id) { - Ok((info, mut stream)) => match old_etags { - Some(old_etags) if old_etags != info.hash => { - warp::http::Response::builder() - .header("content-type", info.file_type) - .status(StatusCode::NOT_MODIFIED) - .body(content) - } - _ => { - let _ = stream.read_to_end(&mut content); - warp::http::Response::builder() - .header("content-type", info.file_type) - .header("etag", info.hash) - .status(StatusCode::OK) - .body(content) - } - }, - Err(_err) => warp::http::Response::builder() - .status(StatusCode::NOT_FOUND) - .body(content), - } + move |id: String, old_etags: Option| match app + .read() + .unwrap() + .get_thumbnail(&id) + { + Ok((info, file)) => serve_file(info, file, old_etags), + Err(_err) => warp::http::Response::builder() + .status(StatusCode::NOT_FOUND) + .body(vec![]), } }); - let server = warp::serve(root.or(thumbnail)); + let file = warp::path!(String) + .and(warp::header::optional::("if-none-match")) + .map({ + let app = app.clone(); + move |id: String, old_etags: Option| match app.read().unwrap().get_file(&id) { + Ok((info, file)) => serve_file(info, file, old_etags), + Err(_err) => warp::http::Response::builder() + .status(StatusCode::NOT_FOUND) + .body(vec![]), + } + }); + + let server = warp::serve(root.or(file).or(thumbnail)); server .run(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8002)) .await; -- 2.44.1 From 8521db333bf52954854f7aecf40f6759dcfafe38 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Fri, 22 Sep 2023 00:38:26 -0400 Subject: [PATCH 08/36] Set up the delete route Sets up the delete route, including post-delete redirect back to the root. Also adds logging. Delete does not actually delete things yet. --- Cargo.lock | 60 +++++++++++++++++++++++++++++++++++ file-service/Cargo.toml | 2 ++ file-service/src/html.rs | 66 +++++++++++++++++++++++++++++---------- file-service/src/main.rs | 59 ++++++++++++++++++++++++---------- file-service/src/pages.rs | 47 +++++++++++++++++----------- 5 files changed, 182 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6a473bb..42f19e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -611,6 +611,19 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log 0.4.20", + "regex", + "termcolor", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -695,12 +708,14 @@ dependencies = [ "http", "image 0.23.14", "iron", + "log 0.4.20", "logger", "mime 0.3.17", "mime_guess 2.0.4", "mustache", "orizentic", "params", + "pretty_env_logger", "router", "serde 1.0.188", "serde_json", @@ -1515,6 +1530,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.10.16" @@ -1729,6 +1750,17 @@ dependencies = [ "url 1.7.2", ] +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi 0.3.2", + "rustix", + "windows-sys", +] + [[package]] name = "itertools" version = "0.10.5" @@ -2616,6 +2648,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "pretty_env_logger" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c" +dependencies = [ + "env_logger", + "log 0.4.20", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -3417,6 +3459,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "termcolor" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" +dependencies = [ + "winapi-util", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -4086,6 +4137,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/file-service/Cargo.toml b/file-service/Cargo.toml index 0afba1a..6baf42a 100644 --- a/file-service/Cargo.toml +++ b/file-service/Cargo.toml @@ -27,3 +27,5 @@ thiserror = "1.0.20" tokio = { version = "1", features = [ "full" ] } uuid = { version = "0.4", features = [ "serde", "v4" ] } warp = { version = "0.3" } +pretty_env_logger = { version = "0.5" } +log = { version = "0.4" } diff --git a/file-service/src/html.rs b/file-service/src/html.rs index 02cffd3..b11881b 100644 --- a/file-service/src/html.rs +++ b/file-service/src/html.rs @@ -2,6 +2,7 @@ use build_html::{self, Html, HtmlContainer}; #[derive(Clone, Debug)] pub struct Form { + path: String, method: String, encoding: Option, elements: String, @@ -10,12 +11,18 @@ pub struct Form { impl Form { pub fn new() -> Self { Self { + path: "/".to_owned(), method: "get".to_owned(), encoding: None, elements: "".to_owned(), } } + pub fn with_path(mut self, path: &str) -> Self { + self.path = path.to_owned(); + self + } + pub fn with_method(mut self, method: &str) -> Self { self.method = method.to_owned(); self @@ -30,11 +37,12 @@ impl Form { impl Html for Form { fn to_html_string(&self) -> String { let encoding = match self.encoding { - Some(ref encoding) => format!("encoding={encoding}", encoding = encoding), + Some(ref encoding) => format!("encoding=\"{encoding}\"", encoding = encoding), None => format!(""), }; format!( - "
\n{elements}\n
\n", + "
\n{elements}\n
\n", + path = self.path, method = self.method, encoding = encoding, elements = self.elements.to_html_string() @@ -52,36 +60,58 @@ impl HtmlContainer for Form { pub struct Input { ty: String, name: String, - id: String, + id: Option, value: Option, + content: Option, } impl Html for Input { fn to_html_string(&self) -> String { + let id = match self.id { + Some(ref id) => format!("id=\"{}\"", id), + None => "".to_owned(), + }; + let value = match self.value { + Some(ref value) => format!("value=\"{}\"", value), + None => "".to_owned(), + }; + format!( - "{value}\n", + "{content}\n", ty = self.ty, name = self.name, - id = self.id, - value = self.value.clone().unwrap_or("".to_owned()), + id = id, + value = value, + content = self.content.clone().unwrap_or("".to_owned()), ) } } impl Input { - pub fn new(ty: &str, name: &str, id: &str) -> Self { + pub fn new(ty: &str, name: &str) -> Self { Self { ty: ty.to_owned(), name: name.to_owned(), - id: id.to_owned(), + id: None, value: None, + content: None, } } + pub fn with_id(mut self, val: &str) -> Self { + self.id = Some(val.to_owned()); + self + } + pub fn with_value(mut self, val: &str) -> Self { self.value = Some(val.to_owned()); self } + + pub fn with_content(mut self, val: &str) -> Self { + self.content = Some(val.to_owned()); + self + } } #[derive(Clone, Debug)] @@ -111,25 +141,29 @@ impl Html for Label { #[derive(Clone, Debug)] pub struct Button { - name: String, - text: String, + name: Option, + label: String, } impl Button { - pub fn new(name: &str, text: &str) -> Self { + pub fn new(label: &str) -> Self { Self { - name: name.to_owned(), - text: text.to_owned(), + name: None, + label: label.to_owned(), } } } impl Html for Button { fn to_html_string(&self) -> String { + let name = match self.name { + Some(ref name) => format!("name={}", name), + None => "".to_owned(), + }; format!( - "", - name = self.name, - text = self.text + "", + name = name, + label = self.label ) } } diff --git a/file-service/src/main.rs b/file-service/src/main.rs index 14ccb28..efb3dbb 100644 --- a/file-service/src/main.rs +++ b/file-service/src/main.rs @@ -6,11 +6,15 @@ use iron::prelude::*; use iron::response::BodyReader; use iron::status; */ +#[macro_use] +extern crate log; + use http::status::StatusCode; // use mustache::{compile_path, Template}; // use orizentic::{Permissions, ResourceName, Secret}; use build_html::Html; use std::{ + collections::HashMap, io::Read, net::{IpAddr, Ipv4Addr, SocketAddr}, path::Path, @@ -254,21 +258,6 @@ pub async fn main() { let mut router = Router::new(); - router.get( - "/:id", - files::GetFileHandler { - app: app.clone(), - template: compile_path("templates/file.html").expect("the template to compile"), - }, - "get-file-page", - ); - - router.get( - "/:id/raw", - files::GetHandler { app: app.clone() }, - "get-file", - ); - router.post("/", files::PostHandler { app: app.clone() }, "upload-file"); router.delete( @@ -301,13 +290,18 @@ pub async fn main() { .await; */ + pretty_env_logger::init(); + let app = Arc::new(RwLock::new(App::new(Path::new( &std::env::var("FILE_SHARE_DIR").unwrap(), )))); - let root = warp::path!().map({ + let log = warp::log("file_service"); + + let root = warp::path!().and(warp::get()).map({ let app = app.clone(); move || { + info!("root handler"); warp::http::Response::builder() .header("content-type", "text/html") .status(StatusCode::OK) @@ -315,7 +309,20 @@ pub async fn main() { } }); + let post_handler = warp::path!(String) + .and(warp::post()) + .and(warp::filters::body::form()) + .map(|id: String, form: HashMap| { + info!("post_delete {}", id); + info!("form: {:?}", form); + warp::http::Response::builder() + .header("location", "/") + .status(StatusCode::SEE_OTHER) + .body(vec![]) + }); + let thumbnail = warp::path!(String / "tn") + .and(warp::get()) .and(warp::header::optional::("if-none-match")) .map({ let app = app.clone(); @@ -332,6 +339,7 @@ pub async fn main() { }); let file = warp::path!(String) + .and(warp::get()) .and(warp::header::optional::("if-none-match")) .map({ let app = app.clone(); @@ -343,7 +351,24 @@ pub async fn main() { } }); - let server = warp::serve(root.or(file).or(thumbnail)); + let upload = warp::path!().and(warp::post()).map(|| { + println!("upload"); + warp::reply() + }); + + let delete = warp::path!(String).and(warp::delete()).map(|id: String| { + println!("delete {}", id); + warp::reply() + }); + + let server = warp::serve( + root.or(post_handler) + .or(file) + .or(thumbnail) + .or(upload) + .or(delete) + .with(log), + ); server .run(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8002)) .await; diff --git a/file-service/src/pages.rs b/file-service/src/pages.rs index 12a92e1..790b644 100644 --- a/file-service/src/pages.rs +++ b/file-service/src/pages.rs @@ -11,30 +11,39 @@ pub fn index(files: Vec>) -> build_html::HtmlPage { .with_encoding("multipart/form-data") .with_container( Container::new(ContainerType::Div) - .with_html(Input::new("file", "file", "file-selector-input")) + .with_html(Input::new("file", "file")) .with_html(Label::new("for-selector-input", "Select a file")), ) - .with_html(Button::new("upload", "Upload file")), + .with_html(Button::new("Upload file")), ); for file in files { - let mut container = - Container::new(ContainerType::Div).with_attributes(vec![("class", "file")]); - match file { - Ok(file) => { - let tn = Container::new(ContainerType::Div) - .with_attributes(vec![("class", "thumbnail")]) - .with_link( - format!("/{}", file.info().id), - Image::new(&format!("{}/tn", file.info().id)).to_html_string(), - ); - container.add_html(tn); - } - Err(err) => { - container.add_paragraph(format!("{:?}", err)); - } - } - page.add_container(container); + let container = match file { + Ok(ref file) => thumbnail(file).with_html( + Form::new() + .with_path(&format!("/{}", file.info().id)) + .with_method("post") + .with_html(Input::new("hidden", "_method").with_value("delete")) + .with_html(Button::new("Delete")), + ), + + Err(err) => Container::new(ContainerType::Div) + .with_attributes(vec![("class", "file")]) + .with_paragraph(format!("{:?}", err)), + }; + page.add_container(container) } page } + +pub fn thumbnail(file: &File) -> Container { + let mut container = Container::new(ContainerType::Div).with_attributes(vec![("class", "file")]); + let tn = Container::new(ContainerType::Div) + .with_attributes(vec![("class", "thumbnail")]) + .with_link( + format!("/{}", file.info().id), + Image::new(&format!("{}/tn", file.info().id)).to_html_string(), + ); + container.add_html(tn); + container +} -- 2.44.1 From 396f6e3bcf2516d1e3a5e4e7fb10eb42ffe0ebe6 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Fri, 22 Sep 2023 20:03:58 -0400 Subject: [PATCH 09/36] Start ripping out lots of infrastructure Much of the infrastructure is old and seems to be based on some assumptions about how Iron handled multipart posts. I don't understand how much of this works, so I'm slowly ripping parts out and rebuilding how the separation of concerns works. --- Cargo.lock | 2 + file-service/.gitignore | 1 + file-service/.ignore | 1 + file-service/Cargo.toml | 3 + file-service/src/lib/file.rs | 120 +++++++++++++++++++++++++---- file-service/src/lib/fileinfo.rs | 73 +++++++++++++----- file-service/src/lib/mod.rs | 124 ++++++++++++++++++++++++++---- file-service/src/lib/thumbnail.rs | 25 +++--- file-service/src/main.rs | 57 ++++++++++++-- file-service/src/pages.rs | 10 +-- 10 files changed, 349 insertions(+), 67 deletions(-) create mode 100644 file-service/.gitignore create mode 100644 file-service/.ignore diff --git a/Cargo.lock b/Cargo.lock index 42f19e4..40643e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -703,7 +703,9 @@ name = "file-service" version = "0.1.0" dependencies = [ "build_html", + "bytes", "chrono", + "futures-util", "hex-string", "http", "image 0.23.14", diff --git a/file-service/.gitignore b/file-service/.gitignore new file mode 100644 index 0000000..116caa1 --- /dev/null +++ b/file-service/.gitignore @@ -0,0 +1 @@ +fixtures diff --git a/file-service/.ignore b/file-service/.ignore new file mode 100644 index 0000000..116caa1 --- /dev/null +++ b/file-service/.ignore @@ -0,0 +1 @@ +fixtures diff --git a/file-service/Cargo.toml b/file-service/Cargo.toml index 6baf42a..103fdb6 100644 --- a/file-service/Cargo.toml +++ b/file-service/Cargo.toml @@ -29,3 +29,6 @@ uuid = { version = "0.4", features = [ "serde", "v4" ] } warp = { version = "0.3" } pretty_env_logger = { version = "0.5" } log = { version = "0.4" } +bytes = { version = "1" } +futures-util = { version = "0.3" } + diff --git a/file-service/src/lib/file.rs b/file-service/src/lib/file.rs index fad9e08..9a78543 100644 --- a/file-service/src/lib/file.rs +++ b/file-service/src/lib/file.rs @@ -1,9 +1,17 @@ -use super::fileinfo::FileInfo; -use super::thumbnail::Thumbnail; -use std::fs::{copy, read_dir, remove_file}; -use std::path::{Path, PathBuf}; +use super::{ + fileinfo::FileInfo, thumbnail::Thumbnail, FileId, FileRoot, PathResolver, ReadFileError, + WriteFileError, +}; +use chrono::prelude::*; +use std::{ + fs::{copy, read_dir, remove_file}, + io::{Read, Write}, + path::{Path, PathBuf}, +}; use thiserror::Error; +use uuid::Uuid; +/* #[derive(Error, Debug)] pub enum FileError { #[error("not implemented")] @@ -30,21 +38,79 @@ pub enum FileError { #[error("UTF8 Error")] UTF8Error(#[from] std::str::Utf8Error), } +*/ /// One file in the database, complete with the path of the file and information about the /// thumbnail of the file. #[derive(Debug)] pub struct File { - info: FileInfo, - tn: Thumbnail, - root: PathBuf, + pub id: FileId, + path: PathResolver, + pub info: FileInfo, + tn: Option, } impl File { + /// Create a new entry in the database + pub fn new(filename: String, context: CTX) -> Result { + let id = FileId::from(Uuid::new_v4().hyphenated().to_string()); + + let mut path = context.root(); + path.push((*id).to_owned()); + let path = PathResolver(path); + + let file_type = mime_guess::from_ext(&filename) + .first_or_text_plain() + .essence_str() + .to_owned(); + + let info = FileInfo { + id: (*id).to_owned(), + size: 0, + created: Utc::now(), + file_type, + hash: "".to_owned(), + root: context.root().to_owned(), + }; + + let mut md_file = std::fs::File::create(path.metadata_path())?; + md_file.write(&serde_json::to_vec(&info)?)?; + + Ok(Self { + id, + path, + info, + tn: None, + }) + } + + pub fn load(id: FileId, context: CTX) -> Result { + let mut path = context.root(); + path.push((*id).to_owned()); + let path = PathResolver(path); + + Ok(Self { + id: id.clone(), + path, + info: FileInfo::load(id, context)?, + tn: None, + }) + } + + pub fn set_content(&self, content: Vec) -> Result<(), WriteFileError> { + let mut content_file = std::fs::File::create(self.path.file_path())?; + content_file.write(&content)?; + Ok(()) + } + + pub fn content(&self) -> Result, ReadFileError> { + unimplemented!() + } + + /* pub fn new( id: &str, root: &Path, - temp_path: &PathBuf, filename: &Option, ) -> Result { let mut dest_path = PathBuf::from(root); @@ -136,6 +202,7 @@ impl File { self.tn.delete()?; self.info.delete() } + */ } #[cfg(test)] @@ -144,17 +211,34 @@ mod test { use crate::lib::utils::FileCleanup; use std::path::{Path, PathBuf}; + struct FileContext(PathBuf); + + impl FileRoot for FileContext { + fn root(&self) -> PathBuf { + self.0.clone() + } + } + #[test] fn it_opens_a_file() { let _md = FileCleanup(PathBuf::from("fixtures/.metadata/rawr.png.json")); let _tn = FileCleanup(PathBuf::from("fixtures/.thumbnails/rawr.png")); - File::open("rawr.png", Path::new("fixtures/")).expect("to succeed"); + File::new( + "rawr.png".to_owned(), + FileContext(PathBuf::from("fixtures/")), + ) + .expect("to succeed"); } #[test] fn it_can_return_a_thumbnail() { - let f = File::open("rawr.png", Path::new("fixtures/")).expect("to succeed"); + let f = File::new( + "rawr.png".to_owned(), + FileContext(PathBuf::from("fixtures/")), + ) + .expect("to succeed"); + /* assert_eq!( f.thumbnail(), Thumbnail { @@ -162,18 +246,26 @@ mod test { root: PathBuf::from("fixtures/"), }, ); + */ } #[test] fn it_can_return_a_file_stream() { - let f = File::open("rawr.png", Path::new("fixtures/")).expect("to succeed"); - f.stream().expect("to succeed"); + let f = File::new( + "rawr.png".to_owned(), + FileContext(PathBuf::from("fixtures/")), + ) + .expect("to succeed"); + // f.stream().expect("to succeed"); } #[test] fn it_raises_an_error_when_file_not_found() { - match File::open("garbage", Path::new("fixtures/")) { - Err(Error::FileNotFound(_)) => assert!(true), + match File::load( + FileId::from("rawr.png"), + FileContext(PathBuf::from("fixtures/")), + ) { + Err(ReadFileError::FileNotFound) => assert!(true), _ => assert!(false), } } diff --git a/file-service/src/lib/fileinfo.rs b/file-service/src/lib/fileinfo.rs index b3e6d49..5ddf3dc 100644 --- a/file-service/src/lib/fileinfo.rs +++ b/file-service/src/lib/fileinfo.rs @@ -7,7 +7,8 @@ use std::fs::remove_file; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; -use crate::{append_extension, FileError}; +use super::{FileId, FileRoot, PathResolver, ReadFileError}; +use crate::append_extension; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct FileInfo { @@ -18,17 +19,34 @@ pub struct FileInfo { pub hash: String, #[serde(skip)] - root: PathBuf, + pub root: PathBuf, } impl FileInfo { - pub fn save(&self, root: &Path) -> Result<(), FileError> { - let ser = serde_json::to_string(self).unwrap(); - std::fs::File::create(FileInfo::metadata_path(&self.id, root)) - .and_then(|mut stream| stream.write(ser.as_bytes()).map(|_| (()))) - .map_err(FileError::from) + pub fn load(id: FileId, context: CTX) -> Result { + let mut content: Vec = Vec::new(); + let mut file = std::fs::File::open(Self::path(id, context))?; + file.read_to_end(&mut content)?; + let js = serde_json::from_slice(&content)?; + + Ok(js) } + pub fn path(id: FileId, context: CTX) -> PathBuf { + let mut path = context.root(); + path.push((*id).to_owned()); + path.set_extension("json"); + path + } + + pub fn save(&self, root: &PathResolver) -> Result<(), ReadFileError> { + let ser = serde_json::to_string(self).unwrap(); + std::fs::File::create(root.metadata_path()) + .and_then(|mut stream| stream.write(ser.as_bytes()).map(|_| (()))) + .map_err(ReadFileError::from) + } + + /* pub fn open(id: &str, root: &Path) -> Result { let mut buf = Vec::new(); @@ -44,23 +62,24 @@ impl FileInfo { serde_json::from_str(&str_repr).map_err(FileError::from) } + */ - pub fn from_path(path: &Path) -> Result { + pub fn from_path(path: &Path) -> Result { match (path.is_file(), path.is_dir()) { - (false, false) => Err(FileError::FileNotFound(PathBuf::from(path))), - (false, true) => Err(FileError::NotAFile(PathBuf::from(path))), + (false, false) => Err(ReadFileError::FileNotFound), + (false, true) => Err(ReadFileError::NotAFile), (true, _) => Ok(()), }?; - let metadata = path.metadata().map_err(FileError::IOError)?; + let metadata = path.metadata().map_err(ReadFileError::IOError)?; let id = path .file_name() .map(|s| String::from(s.to_string_lossy())) - .ok_or(FileError::NotAFile(PathBuf::from(path)))?; + .ok_or(ReadFileError::NotAFile)?; let created = metadata .created() .map(|m| DateTime::from(m)) - .map_err(|err| FileError::IOError(err))?; + .map_err(|err| ReadFileError::IOError(err))?; let file_type = String::from( mime_guess::from_path(path) .first_or_octet_stream() @@ -77,16 +96,17 @@ impl FileInfo { }) } - fn hash_file(path: &Path) -> Result { + fn hash_file(path: &Path) -> Result { let mut buf = Vec::new(); - let mut file = std::fs::File::open(path).map_err(FileError::from)?; + let mut file = std::fs::File::open(path).map_err(ReadFileError::from)?; - file.read_to_end(&mut buf).map_err(FileError::from)?; + file.read_to_end(&mut buf).map_err(ReadFileError::from)?; let mut vec = Vec::new(); vec.extend_from_slice(Sha256::digest(&buf).as_slice()); Ok(HexString::from_bytes(&vec)) } + /* fn metadata_path(id: &str, root: &Path) -> PathBuf { let mut path = PathBuf::from(root); path.push(".metadata"); @@ -98,14 +118,23 @@ impl FileInfo { let path = FileInfo::metadata_path(&self.id, &self.root); remove_file(path).map_err(FileError::from) } + */ } #[cfg(test)] mod test { use super::*; - use crate::lib::utils::FileCleanup; + struct FileContext(PathBuf); + + impl FileRoot for FileContext { + fn root(&self) -> PathBuf { + self.0.clone() + } + } + + /* #[test] fn it_generates_information_from_file_path() { let path = Path::new("fixtures/rawr.png"); @@ -131,17 +160,23 @@ mod test { } } } + */ #[test] fn it_saves_and_loads_metadata() { let path = Path::new("fixtures/rawr.png"); let _ = FileCleanup(append_extension(path, "json")); let info = FileInfo::from_path(&path).unwrap(); - info.save(Path::new("fixtures")).unwrap(); + info.save(&PathResolver(PathBuf::from("fixtures/rawr.png"))) + .unwrap(); assert!(Path::new("fixtures/.metadata/rawr.png.json").is_file()); - let info_ = FileInfo::open("rawr.png", Path::new("fixtures")).unwrap(); + let info_ = FileInfo::load( + FileId::from("rawr.png"), + FileContext(PathBuf::from("fixtures")), + ) + .unwrap(); assert_eq!(info_.id, "rawr.png"); assert_eq!(info_.size, 23777); assert_eq!(info_.created, info.created); diff --git a/file-service/src/lib/mod.rs b/file-service/src/lib/mod.rs index 0d43cac..99b2f5b 100644 --- a/file-service/src/lib/mod.rs +++ b/file-service/src/lib/mod.rs @@ -1,4 +1,8 @@ -use std::path::{Path, PathBuf}; +use std::{ + ops::Deref, + path::{Path, PathBuf}, +}; +use thiserror::Error; use uuid::Uuid; mod file; @@ -6,10 +10,98 @@ mod fileinfo; mod thumbnail; pub mod utils; -pub use file::{File, FileError}; +pub use file::File; pub use fileinfo::FileInfo; pub use thumbnail::Thumbnail; +#[derive(Debug, Error)] +pub enum WriteFileError { + #[error("root file path does not exist")] + RootNotFound, + + #[error("permission denied")] + PermissionDenied, + + #[error("JSON error")] + JSONError(#[from] serde_json::error::Error), + + #[error("IO error")] + IOError(#[from] std::io::Error), +} + +#[derive(Debug, Error)] +pub enum ReadFileError { + #[error("file not found")] + FileNotFound, + + #[error("path is not a file")] + NotAFile, + + #[error("permission denied")] + PermissionDenied, + + #[error("JSON error")] + JSONError(#[from] serde_json::error::Error), + + #[error("IO error")] + IOError(#[from] std::io::Error), +} + +#[derive(Clone, Debug)] +pub struct PathResolver(pub PathBuf); + +impl PathResolver { + fn file_path(&self) -> PathBuf { + self.0.clone() + } + + fn metadata_path(&self) -> PathBuf { + let mut path = self.0.clone(); + path.set_extension("json"); + path + } + + fn thumbnail_path(&self) -> PathBuf { + let mut path = self.0.clone(); + path.set_extension("tn"); + path + } +} + +#[derive(Clone, Debug)] +pub struct FileId(String); + +impl From for FileId { + fn from(s: String) -> Self { + Self(s) + } +} + +impl From<&str> for FileId { + fn from(s: &str) -> Self { + Self(s.to_owned()) + } +} + +impl Deref for FileId { + type Target = String; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +pub trait FileRoot { + fn root(&self) -> PathBuf; +} + +pub struct Context(PathBuf); + +impl FileRoot for Context { + fn root(&self) -> PathBuf { + self.0.clone() + } +} + pub struct App { files_root: PathBuf, } @@ -21,19 +113,18 @@ impl App { } } - pub fn list_files(&self) -> Vec> { - File::list(&self.files_root) + pub fn list_files(&self) -> Vec> { + unimplemented!() } - pub fn add_file( - &mut self, - temp_path: &PathBuf, - filename: &Option, - ) -> Result { - let id = Uuid::new_v4().hyphenated().to_string(); - File::new(&id, &self.files_root, temp_path, filename) + pub fn add_file(&mut self, filename: String, content: Vec) -> Result { + let context = Context(self.files_root.clone()); + let mut file = File::new(filename, context)?; + file.set_content(content)?; + Ok(file) } + /* pub fn delete_file(&mut self, id: String) -> Result<(), FileError> { let f = File::open(&id, &self.files_root)?; f.delete() @@ -42,17 +133,24 @@ impl App { pub fn get_metadata(&self, id: String) -> Result { FileInfo::open(&id, &self.files_root) } + */ - pub fn get_file(&self, id: &str) -> Result<(FileInfo, std::fs::File), FileError> { + pub fn get_file(&self, id: &str) -> Result<(FileInfo, std::fs::File), ReadFileError> { + /* let f = File::open(&id, &self.files_root)?; let info = f.info(); let stream = f.stream()?; Ok((info, stream)) + */ + unimplemented!() } - pub fn get_thumbnail(&self, id: &str) -> Result<(FileInfo, std::fs::File), FileError> { + pub fn get_thumbnail(&self, id: &str) -> Result<(FileInfo, std::fs::File), ReadFileError> { + /* let f = File::open(id, &self.files_root)?; let stream = f.thumbnail().stream()?; Ok((f.info(), stream)) + */ + unimplemented!() } } diff --git a/file-service/src/lib/thumbnail.rs b/file-service/src/lib/thumbnail.rs index 1e458ad..5bdbd36 100644 --- a/file-service/src/lib/thumbnail.rs +++ b/file-service/src/lib/thumbnail.rs @@ -2,7 +2,7 @@ use image::imageops::FilterType; use std::fs::remove_file; use std::path::{Path, PathBuf}; -use crate::FileError; +use super::{ReadFileError, WriteFileError}; #[derive(Clone, Debug, PartialEq)] pub struct Thumbnail { @@ -11,7 +11,8 @@ pub struct Thumbnail { } impl Thumbnail { - pub fn open(id: &str, root: &Path) -> Result { + pub fn open(id: &str, root: &Path) -> Result { + /* let mut source_path = PathBuf::from(root); source_path.push(id); @@ -28,20 +29,24 @@ impl Thumbnail { } Ok(self_) + */ + unimplemented!() } - pub fn from_path(path: &Path) -> Result { + /* + pub fn from_path(path: &Path) -> Result { let id = path .file_name() .map(|s| String::from(s.to_string_lossy())) - .ok_or(FileError::NotAnImage(PathBuf::from(path)))?; + .ok_or(ReadFileError::NotAnImage(PathBuf::from(path)))?; let root = path .parent() - .ok_or(FileError::FileNotFound(PathBuf::from(path)))?; + .ok_or(ReadFileError::FileNotFound(PathBuf::from(path)))?; Thumbnail::open(&id, root) } + */ fn thumbnail_path(id: &str, root: &Path) -> PathBuf { let mut path = PathBuf::from(root); @@ -50,20 +55,20 @@ impl Thumbnail { path } - pub fn stream(&self) -> Result { + pub fn stream(&self) -> Result { let thumbnail_path = Thumbnail::thumbnail_path(&self.id, &self.root); std::fs::File::open(thumbnail_path.clone()).map_err(|err| { if err.kind() == std::io::ErrorKind::NotFound { - FileError::FileNotFound(thumbnail_path) + ReadFileError::FileNotFound } else { - FileError::from(err) + ReadFileError::from(err) } }) } - pub fn delete(&self) -> Result<(), FileError> { + pub fn delete(&self) -> Result<(), WriteFileError> { let path = Thumbnail::thumbnail_path(&self.id, &self.root); - remove_file(path).map_err(FileError::from) + remove_file(path).map_err(WriteFileError::from) } } diff --git a/file-service/src/main.rs b/file-service/src/main.rs index efb3dbb..11ba5ec 100644 --- a/file-service/src/main.rs +++ b/file-service/src/main.rs @@ -13,6 +13,8 @@ use http::status::StatusCode; // use mustache::{compile_path, Template}; // use orizentic::{Permissions, ResourceName, Secret}; use build_html::Html; +use bytes::Buf; +use futures_util::{Stream, StreamExt}; use std::{ collections::HashMap, io::Read, @@ -20,7 +22,7 @@ use std::{ path::Path, sync::{Arc, RwLock}, }; -use warp::Filter; +use warp::{filters::multipart::Part, Filter}; mod cookies; mod html; @@ -28,7 +30,7 @@ mod lib; mod middleware; mod pages; -use lib::{utils::append_extension, App, File, FileError, FileInfo}; +use lib::{utils::append_extension, App, File, FileInfo}; /* fn is_admin(resource: &ResourceName, permissions: &Permissions) -> bool { @@ -247,6 +249,32 @@ fn serve_file( } } +async fn collect_content(mut part: Part) -> Result<(Option, Vec), String> { + let mut content: Vec = Vec::new(); + + while let Some(Ok(data)) = part.data().await { + let mut reader = data.reader(); + reader.read_to_end(&mut content).unwrap(); + } + + Ok((part.filename().map(|s| s.to_owned()), content)) +} + +async fn collect_multipart( + mut stream: warp::filters::multipart::FormData, +) -> Result, Vec)>, warp::Error> { + let mut content: Vec<(Option, Vec)> = Vec::new(); + + while let Some(part) = stream.next().await { + match part { + Ok(part) => content.push(collect_content(part).await.unwrap()), + Err(err) => return Err(err), + } + } + + Ok(content) +} + #[tokio::main] pub async fn main() { /* @@ -309,6 +337,7 @@ pub async fn main() { } }); + /* let post_handler = warp::path!(String) .and(warp::post()) .and(warp::filters::body::form()) @@ -320,6 +349,7 @@ pub async fn main() { .status(StatusCode::SEE_OTHER) .body(vec![]) }); + */ let thumbnail = warp::path!(String / "tn") .and(warp::get()) @@ -351,16 +381,29 @@ pub async fn main() { } }); - let upload = warp::path!().and(warp::post()).map(|| { - println!("upload"); - warp::reply() - }); + let upload = warp::path!() + .and(warp::post()) + .and(warp::filters::multipart::form().max_length(1024 * 1024 * 32)) + .then(|form: warp::filters::multipart::FormData| async move { + let files = collect_multipart(form).await; + /* + for (filename, content) in files { + app.write() + .unwrap() + .add_file(Some(filename), content) + .unwrap(); + } + */ + println!("file length: {:?}", files.map(|f| f.len())); + warp::reply() + }); let delete = warp::path!(String).and(warp::delete()).map(|id: String| { println!("delete {}", id); warp::reply() }); + /* let server = warp::serve( root.or(post_handler) .or(file) @@ -369,6 +412,8 @@ pub async fn main() { .or(delete) .with(log), ); + */ + let server = warp::serve(root.or(upload).with(log)); server .run(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8002)) .await; diff --git a/file-service/src/pages.rs b/file-service/src/pages.rs index 790b644..7058f65 100644 --- a/file-service/src/pages.rs +++ b/file-service/src/pages.rs @@ -1,7 +1,7 @@ -use crate::{html::*, File, FileError}; +use crate::{html::*, lib::ReadFileError, File}; use build_html::{self, Container, ContainerType, Html, HtmlContainer}; -pub fn index(files: Vec>) -> build_html::HtmlPage { +pub fn index(files: Vec>) -> build_html::HtmlPage { let mut page = build_html::HtmlPage::new() .with_title("Admin list of files") .with_header(1, "Admin list of files") @@ -21,7 +21,7 @@ pub fn index(files: Vec>) -> build_html::HtmlPage { let container = match file { Ok(ref file) => thumbnail(file).with_html( Form::new() - .with_path(&format!("/{}", file.info().id)) + .with_path(&format!("/{}", *file.id)) .with_method("post") .with_html(Input::new("hidden", "_method").with_value("delete")) .with_html(Button::new("Delete")), @@ -41,8 +41,8 @@ pub fn thumbnail(file: &File) -> Container { let tn = Container::new(ContainerType::Div) .with_attributes(vec![("class", "thumbnail")]) .with_link( - format!("/{}", file.info().id), - Image::new(&format!("{}/tn", file.info().id)).to_html_string(), + format!("/{}", *file.id), + Image::new(&format!("{}/tn", *file.id)).to_html_string(), ); container.add_html(tn); container -- 2.44.1 From 9787ed3e678b7ef8bcacafae018594269c7a201d Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Fri, 22 Sep 2023 21:56:43 -0400 Subject: [PATCH 10/36] Add some testing for the PathResolver --- file-service/src/lib/mod.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/file-service/src/lib/mod.rs b/file-service/src/lib/mod.rs index 99b2f5b..8bcd48a 100644 --- a/file-service/src/lib/mod.rs +++ b/file-service/src/lib/mod.rs @@ -68,6 +68,18 @@ impl PathResolver { } } +impl From for PathResolver { + fn from(s: String) -> Self { + Self(PathBuf::from(s)) + } +} + +impl From<&str> for PathResolver { + fn from(s: &str) -> Self { + Self(PathBuf::from(s.to_owned())) + } +} + #[derive(Clone, Debug)] pub struct FileId(String); @@ -154,3 +166,25 @@ impl App { unimplemented!() } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn paths() { + let resolver = PathResolver::from("path/82420255-d3c8-4d90-a582-f94be588c70c"); + assert_eq!( + resolver.file_path(), + PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c") + ); + assert_eq!( + resolver.metadata_path(), + PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.json") + ); + assert_eq!( + resolver.thumbnail_path(), + PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.tn") + ); + } +} -- 2.44.1 From 22e25256a5c1337ebc89bc46df16b2c951741ab3 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Fri, 22 Sep 2023 23:43:45 -0400 Subject: [PATCH 11/36] Add some tests to verify that a file can be added to the system Still gutting a lot of the old code, but this MR focuses more on ensuring that a file can be added and that the metadata gets saved. --- Cargo.lock | 72 +- file-service/.ignore | 1 + file-service/Cargo.lock | 1404 ------------------ file-service/Cargo.toml | 2 +- file-service/src/lib/fileinfo.rs | 194 --- file-service/src/main.rs | 8 +- file-service/src/pages.rs | 5 +- file-service/src/{lib => store}/file.rs | 95 +- file-service/src/store/fileinfo.rs | 122 ++ file-service/src/{lib => store}/mod.rs | 55 +- file-service/src/{lib => store}/thumbnail.rs | 2 +- file-service/src/{lib => store}/utils.rs | 0 file-service/var/.placeholder | 0 13 files changed, 221 insertions(+), 1739 deletions(-) delete mode 100644 file-service/Cargo.lock delete mode 100644 file-service/src/lib/fileinfo.rs rename file-service/src/{lib => store}/file.rs (78%) create mode 100644 file-service/src/store/fileinfo.rs rename file-service/src/{lib => store}/mod.rs (71%) rename file-service/src/{lib => store}/thumbnail.rs (98%) rename file-service/src/{lib => store}/utils.rs (100%) create mode 100644 file-service/var/.placeholder diff --git a/Cargo.lock b/Cargo.lock index 40643e0..ce56ba8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -152,18 +152,6 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" -[[package]] -name = "block-buffer" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" -dependencies = [ - "block-padding", - "byte-tools", - "byteorder", - "generic-array 0.12.4", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -173,15 +161,6 @@ dependencies = [ "generic-array 0.14.7", ] -[[package]] -name = "block-padding" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" -dependencies = [ - "byte-tools", -] - [[package]] name = "bodyparser" version = "0.8.0" @@ -217,12 +196,6 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" -[[package]] -name = "byte-tools" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" - [[package]] name = "bytemuck" version = "1.14.0" @@ -539,22 +512,13 @@ dependencies = [ "byteorder", ] -[[package]] -name = "digest" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" -dependencies = [ - "generic-array 0.12.4", -] - [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer 0.10.4", + "block-buffer", "crypto-common", ] @@ -667,12 +631,6 @@ dependencies = [ "zune-inflate", ] -[[package]] -name = "fake-simd" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" - [[package]] name = "fastrand" version = "2.0.1" @@ -1002,15 +960,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "generic-array" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" -dependencies = [ - "typenum", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -2307,12 +2256,6 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" -[[package]] -name = "opaque-debug" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" - [[package]] name = "openssl" version = "0.10.57" @@ -3284,19 +3227,18 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.7", + "digest", ] [[package]] name = "sha2" -version = "0.8.2" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" dependencies = [ - "block-buffer 0.7.3", - "digest 0.8.1", - "fake-simd", - "opaque-debug", + "cfg-if", + "cpufeatures", + "digest", ] [[package]] diff --git a/file-service/.ignore b/file-service/.ignore index 116caa1..bfd1343 100644 --- a/file-service/.ignore +++ b/file-service/.ignore @@ -1 +1,2 @@ fixtures +var diff --git a/file-service/Cargo.lock b/file-service/Cargo.lock deleted file mode 100644 index 2e99d09..0000000 --- a/file-service/Cargo.lock +++ /dev/null @@ -1,1404 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -[[package]] -name = "adler32" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b077b825e468cc974f0020d4082ee6e03132512f207ef1a02fd5d00d1f32d" - -[[package]] -name = "ansi_term" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" -dependencies = [ - "winapi", -] - -[[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 = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" - -[[package]] -name = "autocfg" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" - -[[package]] -name = "base64" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" -dependencies = [ - "byteorder", - "safemem 0.3.3", -] - -[[package]] -name = "bitflags" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" - -[[package]] -name = "block-buffer" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" -dependencies = [ - "block-padding", - "byte-tools", - "byteorder", - "generic-array", -] - -[[package]] -name = "block-padding" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" -dependencies = [ - "byte-tools", -] - -[[package]] -name = "bodyparser" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f023abfa58aad6f6bc4ae0630799e24d5ee0ab8bb2e49f651d9b1f9aa4f52f30" -dependencies = [ - "iron", - "persistent", - "plugin", - "serde 1.0.110", - "serde_json", -] - -[[package]] -name = "buf_redux" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9279646319ff816b05fb5897883ece50d7d854d12b59992683d4f8a71b0f949" -dependencies = [ - "memchr 1.0.2", - "safemem 0.2.0", -] - -[[package]] -name = "byte-tools" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" - -[[package]] -name = "bytemuck" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37fa13df2292ecb479ec23aa06f4507928bef07839be9ef15281411076629431" - -[[package]] -name = "byteorder" -version = "1.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" - -[[package]] -name = "cc" -version = "1.0.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "404b1fe4f65288577753b17e3b36a04596ee784493ec249bf81c7f2d2acd751c" - -[[package]] -name = "cfg-if" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" - -[[package]] -name = "chrono" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2" -dependencies = [ - "num-integer", - "num-traits", - "serde 1.0.110", - "time", -] - -[[package]] -name = "clap" -version = "2.33.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdfa80d47f954d53a35a64987ca1422f495b8d6483c0fe9f7117b36c2a792129" -dependencies = [ - "ansi_term", - "atty", - "bitflags", - "strsim", - "textwrap", - "unicode-width", - "vec_map", -] - -[[package]] -name = "cloudabi" -version = "0.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" -dependencies = [ - "bitflags", -] - -[[package]] -name = "color_quant" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dbbb57365263e881e805dc77d94697c9118fd94d8da011240555aa7b23445bd" - -[[package]] -name = "crc32fast" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-deque" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", - "maybe-uninit", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" -dependencies = [ - "autocfg 1.0.0", - "cfg-if", - "crossbeam-utils", - "lazy_static", - "maybe-uninit", - "memoffset", - "scopeguard", -] - -[[package]] -name = "crossbeam-queue" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "maybe-uninit", -] - -[[package]] -name = "crossbeam-utils" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" -dependencies = [ - "autocfg 1.0.0", - "cfg-if", - "lazy_static", -] - -[[package]] -name = "deflate" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7e5d2a2273fed52a7f947ee55b092c4057025d7a3e04e5ecdbd25d6c3fb1bd7" -dependencies = [ - "adler32", - "byteorder", -] - -[[package]] -name = "digest" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" -dependencies = [ - "generic-array", -] - -[[package]] -name = "either" -version = "1.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" - -[[package]] -name = "fake-simd" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" - -[[package]] -name = "file-service" -version = "0.1.0" -dependencies = [ - "chrono", - "hex-string", - "image", - "iron", - "logger", - "mime 0.3.16", - "mime_guess 2.0.3", - "mustache", - "orizentic", - "params", - "router", - "serde 1.0.110", - "serde_json", - "sha2", - "thiserror", - "uuid 0.4.0", -] - -[[package]] -name = "fuchsia-cprng" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" - -[[package]] -name = "generic-array" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec" -dependencies = [ - "typenum", -] - -[[package]] -name = "gif" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "471d90201b3b223f3451cd4ad53e34295f16a1df17b1edf3736d47761c3981af" -dependencies = [ - "color_quant", - "lzw", -] - -[[package]] -name = "hermit-abi" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91780f809e750b0a89f5544be56617ff6b1227ee485bcb06ebe10cdf89bd3b71" -dependencies = [ - "libc", -] - -[[package]] -name = "hex-string" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "848ec2dd093df965a34b434580d94852197fc83feac5b2c1962399bbf2cb4f0b" - -[[package]] -name = "httparse" -version = "1.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" - -[[package]] -name = "hyper" -version = "0.10.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a0652d9a2609a968c14be1a9ea00bf4b1d64e2e1f53a1b51b6fff3a6e829273" -dependencies = [ - "base64", - "httparse", - "language-tags", - "log 0.3.9", - "mime 0.2.6", - "num_cpus", - "time", - "traitobject", - "typeable", - "unicase 1.4.2", - "url", -] - -[[package]] -name = "idna" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" -dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "image" -version = "0.23.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d534e95ad8b9d5aa614322d02352b4f1bf962254adcf02ac6f2def8be18498e8" -dependencies = [ - "bytemuck", - "byteorder", - "gif", - "jpeg-decoder", - "num-iter", - "num-rational 0.2.4", - "num-traits", - "png", - "scoped_threadpool", - "tiff", -] - -[[package]] -name = "iron" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6d308ca2d884650a8bf9ed2ff4cb13fbb2207b71f64cda11dc9b892067295e8" -dependencies = [ - "hyper", - "log 0.3.9", - "mime_guess 1.8.8", - "modifier", - "num_cpus", - "plugin", - "typemap", - "url", -] - -[[package]] -name = "itertools" -version = "0.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d47946d458e94a1b7bcabbf6521ea7c037062c81f534615abcad76e84d4970d" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" - -[[package]] -name = "jpeg-decoder" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b47b4c4e017b01abdc5bcc126d2d1002e5a75bbe3ce73f9f4f311a916363704" -dependencies = [ - "byteorder", - "rayon", -] - -[[package]] -name = "jsonwebtoken" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d438ea707d465c230305963b67f8357a1d56fcfad9434797d7cb1c46c2e41df" -dependencies = [ - "base64", - "chrono", - "ring", - "serde 1.0.110", - "serde_derive", - "serde_json", - "untrusted", -] - -[[package]] -name = "language-tags" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" - -[[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.70" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3baa92041a6fec78c687fa0cc2b3fae8884f743d672cf551bed1d6dac6988d0f" - -[[package]] -name = "linked-hash-map" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" - -[[package]] -name = "log" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" -dependencies = [ - "log 0.4.8", -] - -[[package]] -name = "log" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "logger" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c9172cb4c2f6c52117e25570983edcbb322f130b1031ae5d5d6b1abe7eeb493" -dependencies = [ - "iron", - "log 0.3.9", - "time", -] - -[[package]] -name = "lzw" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084" - -[[package]] -name = "matches" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" - -[[package]] -name = "maybe-uninit" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" - -[[package]] -name = "memchr" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a" -dependencies = [ - "libc", -] - -[[package]] -name = "memchr" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" - -[[package]] -name = "memoffset" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4fc2c02a7e374099d4ee95a193111f72d2110197fe200272371758f6c3643d8" -dependencies = [ - "autocfg 1.0.0", -] - -[[package]] -name = "mime" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba626b8a6de5da682e1caa06bdb42a335aee5a84db8e5046a3e8ab17ba0a3ae0" -dependencies = [ - "log 0.3.9", -] - -[[package]] -name = "mime" -version = "0.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" - -[[package]] -name = "mime_guess" -version = "1.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216929a5ee4dd316b1702eedf5e74548c123d370f47841ceaac38ca154690ca3" -dependencies = [ - "mime 0.2.6", - "phf", - "phf_codegen", - "unicase 1.4.2", -] - -[[package]] -name = "mime_guess" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" -dependencies = [ - "mime 0.3.16", - "unicase 2.6.0", -] - -[[package]] -name = "miniz_oxide" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" -dependencies = [ - "adler32", -] - -[[package]] -name = "modifier" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f5c9112cb662acd3b204077e0de5bc66305fa8df65c8019d5adb10e9ab6e58" - -[[package]] -name = "multipart" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92f54eb45230c3aa20864ccf0c277eeaeadcf5e437e91731db498dbf7fbe0ec6" -dependencies = [ - "buf_redux", - "httparse", - "log 0.3.9", - "mime 0.2.6", - "mime_guess 1.8.8", - "rand 0.3.23", - "safemem 0.2.0", - "tempdir", - "twoway", -] - -[[package]] -name = "mustache" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51956ef1c5d20a1384524d91e616fb44dfc7d8f249bf696d49c97dd3289ecab5" -dependencies = [ - "log 0.3.9", - "serde 1.0.110", -] - -[[package]] -name = "num" -version = "0.1.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4703ad64153382334aa8db57c637364c322d3372e097840c72000dabdcf6156e" -dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational 0.1.42", - "num-traits", -] - -[[package]] -name = "num-bigint" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e63899ad0da84ce718c14936262a41cee2c79c981fc0a0e7c7beb47d5a07e8c1" -dependencies = [ - "num-integer", - "num-traits", - "rand 0.4.6", - "rustc-serialize", -] - -[[package]] -name = "num-complex" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b288631d7878aaf59442cffd36910ea604ecd7745c36054328595114001c9656" -dependencies = [ - "num-traits", - "rustc-serialize", -] - -[[package]] -name = "num-integer" -version = "0.1.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba" -dependencies = [ - "autocfg 1.0.0", - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfb0800a0291891dd9f4fe7bd9c19384f98f7fbe0cd0f39a2c6b88b9868bbc00" -dependencies = [ - "autocfg 1.0.0", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.1.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee314c74bd753fc86b4780aa9475da469155f3848473a261d2d18e35245a784e" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", - "rustc-serialize", -] - -[[package]] -name = "num-rational" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" -dependencies = [ - "autocfg 1.0.0", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" -dependencies = [ - "autocfg 1.0.0", -] - -[[package]] -name = "num_cpus" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "opaque-debug" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" - -[[package]] -name = "orizentic" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f4af9d9002d3b129cdad6ebdb5b745b2b7d5e8c1c85c8addd7b447695af6a3" -dependencies = [ - "chrono", - "clap", - "itertools", - "jsonwebtoken", - "serde 1.0.110", - "serde_derive", - "serde_json", - "uuid 0.6.5", - "version_check 0.1.5", - "yaml-rust", -] - -[[package]] -name = "params" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c789fdad2cfdaa551ea0e3a9eadb74c5d634968a9fb3a8c767d89be470d21589" -dependencies = [ - "bodyparser", - "iron", - "multipart", - "num", - "plugin", - "serde_json", - "tempdir", - "urlencoded", -] - -[[package]] -name = "percent-encoding" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" - -[[package]] -name = "persistent" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8fa0009c4f3d350281309909c618abddf10bb7e3145f28410782f6a5ec74c5" -dependencies = [ - "iron", - "plugin", -] - -[[package]] -name = "phf" -version = "0.7.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18" -dependencies = [ - "phf_shared", -] - -[[package]] -name = "phf_codegen" -version = "0.7.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b03e85129e324ad4166b06b2c7491ae27fe3ec353af72e72cd1654c7225d517e" -dependencies = [ - "phf_generator", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.7.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662" -dependencies = [ - "phf_shared", - "rand 0.6.5", -] - -[[package]] -name = "phf_shared" -version = "0.7.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" -dependencies = [ - "siphasher", - "unicase 1.4.2", -] - -[[package]] -name = "plugin" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a6a0dc3910bc8db877ffed8e457763b317cf880df4ae19109b9f77d277cf6e0" -dependencies = [ - "typemap", -] - -[[package]] -name = "png" -version = "0.16.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ccdd66f6fe4b2433b07e4728e9a013e43233120427046e93ceb709c3a439bf" -dependencies = [ - "bitflags", - "crc32fast", - "deflate", - "miniz_oxide", -] - -[[package]] -name = "proc-macro2" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53f5ffe53a6b28e37c9c1ce74893477864d64f74778a93a4beb43c8fa167f639" -dependencies = [ - "unicode-xid", -] - -[[package]] -name = "quote" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42934bc9c8ab0d3b273a16d8551c8f0fcff46be73276ca083ec2414c15c4ba5e" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" -dependencies = [ - "libc", - "rand 0.4.6", -] - -[[package]] -name = "rand" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" -dependencies = [ - "fuchsia-cprng", - "libc", - "rand_core 0.3.1", - "rdrand", - "winapi", -] - -[[package]] -name = "rand" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" -dependencies = [ - "autocfg 0.1.7", - "libc", - "rand_chacha", - "rand_core 0.4.2", - "rand_hc", - "rand_isaac", - "rand_jitter", - "rand_os", - "rand_pcg", - "rand_xorshift", - "winapi", -] - -[[package]] -name = "rand_chacha" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" -dependencies = [ - "autocfg 0.1.7", - "rand_core 0.3.1", -] - -[[package]] -name = "rand_core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" -dependencies = [ - "rand_core 0.4.2", -] - -[[package]] -name = "rand_core" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" - -[[package]] -name = "rand_hc" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" -dependencies = [ - "rand_core 0.3.1", -] - -[[package]] -name = "rand_isaac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" -dependencies = [ - "rand_core 0.3.1", -] - -[[package]] -name = "rand_jitter" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" -dependencies = [ - "libc", - "rand_core 0.4.2", - "winapi", -] - -[[package]] -name = "rand_os" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" -dependencies = [ - "cloudabi", - "fuchsia-cprng", - "libc", - "rand_core 0.4.2", - "rdrand", - "winapi", -] - -[[package]] -name = "rand_pcg" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" -dependencies = [ - "autocfg 0.1.7", - "rand_core 0.4.2", -] - -[[package]] -name = "rand_xorshift" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" -dependencies = [ - "rand_core 0.3.1", -] - -[[package]] -name = "rayon" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f02856753d04e03e26929f820d0a0a337ebe71f849801eea335d464b349080" -dependencies = [ - "autocfg 1.0.0", - "crossbeam-deque", - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e92e15d89083484e11353891f1af602cc661426deb9564c298b270c726973280" -dependencies = [ - "crossbeam-deque", - "crossbeam-queue", - "crossbeam-utils", - "lazy_static", - "num_cpus", -] - -[[package]] -name = "rdrand" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" -dependencies = [ - "rand_core 0.3.1", -] - -[[package]] -name = "remove_dir_all" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" -dependencies = [ - "winapi", -] - -[[package]] -name = "ring" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4db68a2e35f3497146b7e4563df7d4773a2433230c5e4b448328e31740458a" -dependencies = [ - "cc", - "lazy_static", - "libc", - "untrusted", -] - -[[package]] -name = "route-recognizer" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea509065eb0b3c446acdd0102f0d46567dc30902dc0be91d6552035d92b0f4f8" - -[[package]] -name = "router" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc63b6f3b8895b0d04e816b2b1aa58fdba2d5acca3cbb8f0ab8e017347d57397" -dependencies = [ - "iron", - "route-recognizer", - "url", -] - -[[package]] -name = "rustc-serialize" -version = "0.3.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" - -[[package]] -name = "ryu" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3d612bc64430efeb3f7ee6ef26d590dce0c43249217bddc62112540c7941e1" - -[[package]] -name = "safemem" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f" - -[[package]] -name = "safemem" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" - -[[package]] -name = "scoped_threadpool" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" - -[[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "serde" -version = "0.9.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b623917345a631dc9608d5194cc206b3fe6c3554cd1c75b937e55e285254af" - -[[package]] -name = "serde" -version = "1.0.110" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99e7b308464d16b56eba9964e4972a3eee817760ab60d88c3f86e1fecb08204c" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.110" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "818fbf6bfa9a42d3bfcaca148547aa00c7b915bec71d1757aa2d44ca68771984" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993948e75b189211a9b31a7528f950c6adc21f9720b6438ff80a7fa2f864cea2" -dependencies = [ - "itoa", - "ryu", - "serde 1.0.110", -] - -[[package]] -name = "sha2" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69" -dependencies = [ - "block-buffer", - "digest", - "fake-simd", - "opaque-debug", -] - -[[package]] -name = "siphasher" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" - -[[package]] -name = "smallvec" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7cb5678e1615754284ec264d9bb5b4c27d2018577fd90ac0ceb578591ed5ee4" - -[[package]] -name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - -[[package]] -name = "syn" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1425de3c33b0941002740a420b1a906a350b88d08b82b2c8a01035a3f9447bac" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "tempdir" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" -dependencies = [ - "rand 0.4.6", - "remove_dir_all", -] - -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] - -[[package]] -name = "thiserror" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tiff" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f3b8a87c4da944c3f27e5943289171ac71a6150a79ff6bacfff06d159dfff2f" -dependencies = [ - "byteorder", - "lzw", - "miniz_oxide", -] - -[[package]] -name = "time" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "traitobject" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079" - -[[package]] -name = "twoway" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" -dependencies = [ - "memchr 2.3.3", -] - -[[package]] -name = "typeable" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1410f6f91f21d1612654e7cc69193b0334f909dcf2c790c4826254fbb86f8887" - -[[package]] -name = "typemap" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "653be63c80a3296da5551e1bfd2cca35227e13cdd08c6668903ae2f4f77aa1f6" -dependencies = [ - "unsafe-any", -] - -[[package]] -name = "typenum" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" - -[[package]] -name = "unicase" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4765f83163b74f957c797ad9253caf97f103fb064d3999aea9568d09fc8a33" -dependencies = [ - "version_check 0.1.5", -] - -[[package]] -name = "unicase" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" -dependencies = [ - "version_check 0.9.1", -] - -[[package]] -name = "unicode-bidi" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" -dependencies = [ - "matches", -] - -[[package]] -name = "unicode-normalization" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5479532badd04e128284890390c1e876ef7a993d0570b3597ae43dfa1d59afa4" -dependencies = [ - "smallvec", -] - -[[package]] -name = "unicode-width" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" - -[[package]] -name = "unicode-xid" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" - -[[package]] -name = "unsafe-any" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30360d7979f5e9c6e6cea48af192ea8fab4afb3cf72597154b8f08935bc9c7f" -dependencies = [ - "traitobject", -] - -[[package]] -name = "untrusted" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cd1f4b4e96b46aeb8d4855db4a7a9bd96eeeb5c6a1ab54593328761642ce2f" - -[[package]] -name = "url" -version = "1.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" -dependencies = [ - "idna", - "matches", - "percent-encoding", -] - -[[package]] -name = "urlencoded" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a52f50139118b60ae91af08bf15ed158817d34b91b9d24c11ffbe21195d33e3" -dependencies = [ - "bodyparser", - "iron", - "plugin", - "url", -] - -[[package]] -name = "uuid" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cfec50b0842181ba6e713151b72f4ec84a6a7e2c9c8a8a3ffc37bb1cd16b231" -dependencies = [ - "rand 0.3.23", - "serde 0.9.15", -] - -[[package]] -name = "uuid" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1436e58182935dcd9ce0add9ea0b558e8a87befe01c1a301e6020aeb0876363" -dependencies = [ - "cfg-if", - "rand 0.4.6", -] - -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - -[[package]] -name = "version_check" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" - -[[package]] -name = "version_check" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" - -[[package]] -name = "winapi" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" -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-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "yaml-rust" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65923dd1784f44da1d2c3dbbc5e822045628c590ba72123e1c73d3c230c4434d" -dependencies = [ - "linked-hash-map", -] diff --git a/file-service/Cargo.toml b/file-service/Cargo.toml index 103fdb6..8452602 100644 --- a/file-service/Cargo.toml +++ b/file-service/Cargo.toml @@ -22,7 +22,7 @@ params = "*" router = "*" serde_json = "*" serde = { version = "1.0", features = ["derive"] } -sha2 = "0.8.2" +sha2 = "0.10" thiserror = "1.0.20" tokio = { version = "1", features = [ "full" ] } uuid = { version = "0.4", features = [ "serde", "v4" ] } diff --git a/file-service/src/lib/fileinfo.rs b/file-service/src/lib/fileinfo.rs deleted file mode 100644 index 5ddf3dc..0000000 --- a/file-service/src/lib/fileinfo.rs +++ /dev/null @@ -1,194 +0,0 @@ -use chrono::prelude::*; -use hex_string::HexString; -use serde::{Deserialize, Serialize}; -use serde_json; -use sha2::{Digest, Sha256}; -use std::fs::remove_file; -use std::io::{Read, Write}; -use std::path::{Path, PathBuf}; - -use super::{FileId, FileRoot, PathResolver, ReadFileError}; -use crate::append_extension; - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct FileInfo { - pub id: String, - pub size: u64, - pub created: DateTime, - pub file_type: String, - pub hash: String, - - #[serde(skip)] - pub root: PathBuf, -} - -impl FileInfo { - pub fn load(id: FileId, context: CTX) -> Result { - let mut content: Vec = Vec::new(); - let mut file = std::fs::File::open(Self::path(id, context))?; - file.read_to_end(&mut content)?; - let js = serde_json::from_slice(&content)?; - - Ok(js) - } - - pub fn path(id: FileId, context: CTX) -> PathBuf { - let mut path = context.root(); - path.push((*id).to_owned()); - path.set_extension("json"); - path - } - - pub fn save(&self, root: &PathResolver) -> Result<(), ReadFileError> { - let ser = serde_json::to_string(self).unwrap(); - std::fs::File::create(root.metadata_path()) - .and_then(|mut stream| stream.write(ser.as_bytes()).map(|_| (()))) - .map_err(ReadFileError::from) - } - - /* - pub fn open(id: &str, root: &Path) -> Result { - let mut buf = Vec::new(); - - let md_path = FileInfo::metadata_path(id, root); - std::fs::File::open(md_path.clone()) - .and_then(|mut stream| stream.read_to_end(&mut buf)) - .map_err(move |err| match err.kind() { - std::io::ErrorKind::NotFound => FileError::FileNotFound(md_path), - _ => FileError::IOError(err), - })?; - - let str_repr = std::str::from_utf8(&buf)?; - - serde_json::from_str(&str_repr).map_err(FileError::from) - } - */ - - pub fn from_path(path: &Path) -> Result { - match (path.is_file(), path.is_dir()) { - (false, false) => Err(ReadFileError::FileNotFound), - (false, true) => Err(ReadFileError::NotAFile), - (true, _) => Ok(()), - }?; - - let metadata = path.metadata().map_err(ReadFileError::IOError)?; - let id = path - .file_name() - .map(|s| String::from(s.to_string_lossy())) - .ok_or(ReadFileError::NotAFile)?; - let created = metadata - .created() - .map(|m| DateTime::from(m)) - .map_err(|err| ReadFileError::IOError(err))?; - let file_type = String::from( - mime_guess::from_path(path) - .first_or_octet_stream() - .essence_str(), - ); - let hash = FileInfo::hash_file(path)?; - Ok(FileInfo { - id, - size: metadata.len(), - created, - file_type, - hash: hash.as_string(), - root: PathBuf::from(path.parent().unwrap()), - }) - } - - fn hash_file(path: &Path) -> Result { - let mut buf = Vec::new(); - let mut file = std::fs::File::open(path).map_err(ReadFileError::from)?; - - file.read_to_end(&mut buf).map_err(ReadFileError::from)?; - let mut vec = Vec::new(); - vec.extend_from_slice(Sha256::digest(&buf).as_slice()); - Ok(HexString::from_bytes(&vec)) - } - - /* - fn metadata_path(id: &str, root: &Path) -> PathBuf { - let mut path = PathBuf::from(root); - path.push(".metadata"); - path.push(id.clone()); - append_extension(&path, "json") - } - - pub fn delete(&self) -> Result<(), FileError> { - let path = FileInfo::metadata_path(&self.id, &self.root); - remove_file(path).map_err(FileError::from) - } - */ -} - -#[cfg(test)] -mod test { - use super::*; - use crate::lib::utils::FileCleanup; - - struct FileContext(PathBuf); - - impl FileRoot for FileContext { - fn root(&self) -> PathBuf { - self.0.clone() - } - } - - /* - #[test] - fn it_generates_information_from_file_path() { - let path = Path::new("fixtures/rawr.png"); - match FileInfo::from_path(&path) { - Ok(FileInfo { - id, - size, - file_type, - hash, - .. - }) => { - assert_eq!(id, "rawr.png"); - assert_eq!(size, 23777); - assert_eq!(file_type, "image/png"); - assert_eq!( - hash, - "b6cd35e113b95d62f53d9cbd27ccefef47d3e324aef01a2db6c0c6d3a43c89ee" - ); - } - Err(err) => { - println!("error loading file path: {}", err); - assert!(false); - } - } - } - */ - - #[test] - fn it_saves_and_loads_metadata() { - let path = Path::new("fixtures/rawr.png"); - let _ = FileCleanup(append_extension(path, "json")); - let info = FileInfo::from_path(&path).unwrap(); - info.save(&PathResolver(PathBuf::from("fixtures/rawr.png"))) - .unwrap(); - - assert!(Path::new("fixtures/.metadata/rawr.png.json").is_file()); - - let info_ = FileInfo::load( - FileId::from("rawr.png"), - FileContext(PathBuf::from("fixtures")), - ) - .unwrap(); - assert_eq!(info_.id, "rawr.png"); - assert_eq!(info_.size, 23777); - assert_eq!(info_.created, info.created); - assert_eq!(info_.file_type, "image/png"); - assert_eq!(info_.hash, info.hash); - } - - #[test] - fn it_extends_a_file_extension() { - assert_eq!( - append_extension(Path::new("fixtures/rawr.png"), "json"), - Path::new("fixtures/rawr.png.json") - ); - } -} diff --git a/file-service/src/main.rs b/file-service/src/main.rs index 11ba5ec..22586f6 100644 --- a/file-service/src/main.rs +++ b/file-service/src/main.rs @@ -19,18 +19,18 @@ use std::{ collections::HashMap, io::Read, net::{IpAddr, Ipv4Addr, SocketAddr}, - path::Path, + path::PathBuf, sync::{Arc, RwLock}, }; use warp::{filters::multipart::Part, Filter}; mod cookies; mod html; -mod lib; mod middleware; mod pages; +mod store; -use lib::{utils::append_extension, App, File, FileInfo}; +pub use store::{FileInfo, Store}; /* fn is_admin(resource: &ResourceName, permissions: &Permissions) -> bool { @@ -320,7 +320,7 @@ pub async fn main() { pretty_env_logger::init(); - let app = Arc::new(RwLock::new(App::new(Path::new( + let app = Arc::new(RwLock::new(Store::new(PathBuf::from( &std::env::var("FILE_SHARE_DIR").unwrap(), )))); diff --git a/file-service/src/pages.rs b/file-service/src/pages.rs index 7058f65..8fac465 100644 --- a/file-service/src/pages.rs +++ b/file-service/src/pages.rs @@ -1,4 +1,7 @@ -use crate::{html::*, lib::ReadFileError, File}; +use crate::{ + html::*, + store::{File, ReadFileError}, +}; use build_html::{self, Container, ContainerType, Html, HtmlContainer}; pub fn index(files: Vec>) -> build_html::HtmlPage { diff --git a/file-service/src/lib/file.rs b/file-service/src/store/file.rs similarity index 78% rename from file-service/src/lib/file.rs rename to file-service/src/store/file.rs index 9a78543..b81d56b 100644 --- a/file-service/src/lib/file.rs +++ b/file-service/src/store/file.rs @@ -3,51 +3,21 @@ use super::{ WriteFileError, }; use chrono::prelude::*; +use hex_string::HexString; +use sha2::{Digest, Sha256}; use std::{ - fs::{copy, read_dir, remove_file}, io::{Read, Write}, path::{Path, PathBuf}, }; -use thiserror::Error; use uuid::Uuid; -/* -#[derive(Error, Debug)] -pub enum FileError { - #[error("not implemented")] - NotImplemented, - - #[error("file not found: `{0}`")] - FileNotFound(PathBuf), - - #[error("file is not an image: `{0}`")] - NotAnImage(PathBuf), - - #[error("path is not a file: `{0}`")] - NotAFile(PathBuf), - - #[error("Image loading error")] - ImageError(#[from] image::ImageError), - - #[error("IO error")] - IOError(#[from] std::io::Error), - - #[error("JSON error")] - JSONError(#[from] serde_json::error::Error), - - #[error("UTF8 Error")] - UTF8Error(#[from] std::str::Utf8Error), -} -*/ - /// One file in the database, complete with the path of the file and information about the /// thumbnail of the file. #[derive(Debug)] pub struct File { pub id: FileId, - path: PathResolver, + pub path: PathResolver, pub info: FileInfo, - tn: Option, } impl File { @@ -65,41 +35,44 @@ impl File { .to_owned(); let info = FileInfo { - id: (*id).to_owned(), size: 0, created: Utc::now(), file_type, hash: "".to_owned(), - root: context.root().to_owned(), }; let mut md_file = std::fs::File::create(path.metadata_path())?; md_file.write(&serde_json::to_vec(&info)?)?; - Ok(Self { - id, - path, - info, - tn: None, - }) + Ok(Self { id, path, info }) } - pub fn load(id: FileId, context: CTX) -> Result { - let mut path = context.root(); - path.push((*id).to_owned()); - let path = PathResolver(path); - + pub fn load(resolver: PathResolver) -> Result { + /* Ok(Self { - id: id.clone(), - path, - info: FileInfo::load(id, context)?, - tn: None, + id: FileId::from( + resolver + .file_path() + .file_stem() + .unwrap() + .to_string_lossy() + .to_owned(), + ), + path: resolver, + info: FileInfo::load(resolver.metadata_path())?, }) + */ + unimplemented!() } - pub fn set_content(&self, content: Vec) -> Result<(), WriteFileError> { + pub fn set_content(&mut self, content: Vec) -> Result<(), WriteFileError> { let mut content_file = std::fs::File::create(self.path.file_path())?; - content_file.write(&content)?; + let byte_count = content_file.write(&content)?; + self.info.size = byte_count; + + let mut md_file = std::fs::File::create(self.path.metadata_path())?; + md_file.write(&serde_json::to_vec(&self.info)?)?; + Ok(()) } @@ -107,6 +80,14 @@ impl File { unimplemented!() } + pub fn hash_content(&self) -> Result { + let mut buf = Vec::new(); + let mut file = std::fs::File::open(self.path.file_path())?; + file.read_to_end(&mut buf).map_err(ReadFileError::from)?; + + Ok(HexString::from_bytes(&Sha256::digest(&buf).to_vec())) + } + /* pub fn new( id: &str, @@ -208,8 +189,8 @@ impl File { #[cfg(test)] mod test { use super::*; - use crate::lib::utils::FileCleanup; - use std::path::{Path, PathBuf}; + use crate::store::utils::FileCleanup; + use std::path::PathBuf; struct FileContext(PathBuf); @@ -261,10 +242,8 @@ mod test { #[test] fn it_raises_an_error_when_file_not_found() { - match File::load( - FileId::from("rawr.png"), - FileContext(PathBuf::from("fixtures/")), - ) { + let resolver = PathResolver(PathBuf::from("fixtures/rawr.png")); + match File::load(resolver) { Err(ReadFileError::FileNotFound) => assert!(true), _ => assert!(false), } diff --git a/file-service/src/store/fileinfo.rs b/file-service/src/store/fileinfo.rs new file mode 100644 index 0000000..ad8f778 --- /dev/null +++ b/file-service/src/store/fileinfo.rs @@ -0,0 +1,122 @@ +use chrono::prelude::*; +use serde::{Deserialize, Serialize}; +use serde_json; +use std::io::{Read, Write}; +use std::path::PathBuf; + +use super::{ReadFileError, WriteFileError}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct FileInfo { + pub size: usize, + pub created: DateTime, + pub file_type: String, + pub hash: String, +} + +impl FileInfo { + pub fn load(path: PathBuf) -> Result { + let mut content: Vec = Vec::new(); + let mut file = std::fs::File::open(path)?; + file.read_to_end(&mut content)?; + let js = serde_json::from_slice(&content)?; + + Ok(js) + } + + pub fn save(&self, path: PathBuf) -> Result<(), WriteFileError> { + let ser = serde_json::to_string(self).unwrap(); + let mut file = std::fs::File::create(path)?; + file.write(ser.as_bytes())?; + Ok(()) + } + + /* + pub fn from_path(path: &Path) -> Result { + match (path.is_file(), path.is_dir()) { + (false, false) => Err(ReadFileError::FileNotFound), + (false, true) => Err(ReadFileError::NotAFile), + (true, _) => Ok(()), + }?; + + let metadata = path.metadata().map_err(ReadFileError::IOError)?; + let id = path + .file_name() + .map(|s| String::from(s.to_string_lossy())) + .ok_or(ReadFileError::NotAFile)?; + let created = metadata + .created() + .map(|m| DateTime::from(m)) + .map_err(|err| ReadFileError::IOError(err))?; + let file_type = String::from( + mime_guess::from_path(path) + .first_or_octet_stream() + .essence_str(), + ); + let hash = FileInfo::hash_file(path)?; + Ok(FileInfo { + id, + size: metadata.len(), + created, + file_type, + hash: hash.as_string(), + root: PathBuf::from(path.parent().unwrap()), + }) + } + */ + + /* + fn hash_file(path: &Path) -> Result { + let mut buf = Vec::new(); + let mut file = std::fs::File::open(path).map_err(ReadFileError::from)?; + + file.read_to_end(&mut buf).map_err(ReadFileError::from)?; + let mut vec = Vec::new(); + vec.extend_from_slice(Sha256::digest(&buf).as_slice()); + Ok(HexString::from_bytes(&vec)) + } + */ + + /* + fn metadata_path(id: &str, root: &Path) -> PathBuf { + let mut path = PathBuf::from(root); + path.push(".metadata"); + path.push(id.clone()); + append_extension(&path, "json") + } + + pub fn delete(&self) -> Result<(), FileError> { + let path = FileInfo::metadata_path(&self.id, &self.root); + remove_file(path).map_err(FileError::from) + } + */ +} + +#[cfg(test)] +mod test { + use super::*; + use crate::store::utils::FileCleanup; + use crate::store::PathResolver; + + #[test] + fn it_saves_and_loads_metadata() { + let resolver = PathResolver::from("fixtures/1617654d-a588-4714-b4fa-e00ed0a8a607"); + let _cleanup = FileCleanup(resolver.metadata_path()); + + let created = Utc::now(); + + let info = FileInfo { + size: 23777, + created, + file_type: "image/png".to_owned(), + hash: "abcdefg".to_owned(), + }; + info.save(resolver.metadata_path()).unwrap(); + + let info_ = FileInfo::load(resolver.metadata_path()).unwrap(); + assert_eq!(info_.size, 23777); + assert_eq!(info_.created, info.created); + assert_eq!(info_.file_type, "image/png"); + assert_eq!(info_.hash, info.hash); + } +} diff --git a/file-service/src/lib/mod.rs b/file-service/src/store/mod.rs similarity index 71% rename from file-service/src/lib/mod.rs rename to file-service/src/store/mod.rs index 8bcd48a..1e732a3 100644 --- a/file-service/src/lib/mod.rs +++ b/file-service/src/store/mod.rs @@ -114,15 +114,13 @@ impl FileRoot for Context { } } -pub struct App { +pub struct Store { files_root: PathBuf, } -impl App { - pub fn new(files_root: &Path) -> App { - App { - files_root: PathBuf::from(files_root), - } +impl Store { + pub fn new(files_root: PathBuf) -> Self { + Self { files_root } } pub fn list_files(&self) -> Vec> { @@ -136,16 +134,18 @@ impl App { Ok(file) } - /* - pub fn delete_file(&mut self, id: String) -> Result<(), FileError> { + pub fn delete_file(&mut self, id: String) -> Result<(), WriteFileError> { + /* let f = File::open(&id, &self.files_root)?; f.delete() + */ + unimplemented!() } - pub fn get_metadata(&self, id: String) -> Result { - FileInfo::open(&id, &self.files_root) + pub fn get_metadata(&self, id: String) -> Result { + // FileInfo::open(&id, &self.files_root) + unimplemented!() } - */ pub fn get_file(&self, id: &str) -> Result<(FileInfo, std::fs::File), ReadFileError> { /* @@ -169,7 +169,10 @@ impl App { #[cfg(test)] mod test { + use crate::store::utils::FileCleanup; + use super::*; + use std::io::Read; #[test] fn paths() { @@ -187,4 +190,34 @@ mod test { PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.tn") ); } + + #[test] + fn adds_files() { + let mut buf = Vec::new(); + let mut file = std::fs::File::open("fixtures/rawr.png").unwrap(); + file.read_to_end(&mut buf).unwrap(); + + let mut store = Store::new(PathBuf::from("var/")); + let file_record = store.add_file("rawr.png".to_owned(), buf).unwrap(); + + let _file = FileCleanup(PathBuf::from(format!("var/{}", *file_record.id))); + let _md = FileCleanup(PathBuf::from(format!("var/{}.json", *file_record.id))); + let _tn = FileCleanup(PathBuf::from(format!("var/{}.tn", *file_record.id))); + + assert!(PathBuf::from(format!("var/{}", *file_record.id)).exists()); + assert!(PathBuf::from(format!("var/{}.json", *file_record.id)).exists()); + assert!(PathBuf::from(format!("var/{}.tn", *file_record.id)).exists()); + } + + #[test] + fn sets_up_metadata_for_file() {} + + #[test] + fn sets_up_thumbnail_for_file() {} + + #[test] + fn deletes_associated_files() {} + + #[test] + fn lists_files_in_the_db() {} } diff --git a/file-service/src/lib/thumbnail.rs b/file-service/src/store/thumbnail.rs similarity index 98% rename from file-service/src/lib/thumbnail.rs rename to file-service/src/store/thumbnail.rs index 5bdbd36..99ddd4d 100644 --- a/file-service/src/lib/thumbnail.rs +++ b/file-service/src/store/thumbnail.rs @@ -75,7 +75,7 @@ impl Thumbnail { #[cfg(test)] mod test { use super::*; - use crate::lib::utils::FileCleanup; + use crate::store::utils::FileCleanup; #[test] fn it_creates_a_thumbnail_if_one_does_not_exist() { diff --git a/file-service/src/lib/utils.rs b/file-service/src/store/utils.rs similarity index 100% rename from file-service/src/lib/utils.rs rename to file-service/src/store/utils.rs diff --git a/file-service/var/.placeholder b/file-service/var/.placeholder new file mode 100644 index 0000000..e69de29 -- 2.44.1 From 89a1aa7ee56a42849ec6883e5b66336a03145765 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sat, 23 Sep 2023 15:17:49 -0400 Subject: [PATCH 12/36] Get thumbnail creation working again --- file-service/src/store/file.rs | 9 +-- file-service/src/store/fileinfo.rs | 16 ++--- file-service/src/store/mod.rs | 98 ++++++++++++++++++++++++----- file-service/src/store/thumbnail.rs | 51 +++++++-------- 4 files changed, 119 insertions(+), 55 deletions(-) diff --git a/file-service/src/store/file.rs b/file-service/src/store/file.rs index b81d56b..c9e969f 100644 --- a/file-service/src/store/file.rs +++ b/file-service/src/store/file.rs @@ -6,6 +6,7 @@ use chrono::prelude::*; use hex_string::HexString; use sha2::{Digest, Sha256}; use std::{ + convert::TryFrom, io::{Read, Write}, path::{Path, PathBuf}, }; @@ -26,8 +27,8 @@ impl File { let id = FileId::from(Uuid::new_v4().hyphenated().to_string()); let mut path = context.root(); - path.push((*id).to_owned()); - let path = PathResolver(path); + path.push(filename.clone()); + let path = PathResolver::try_from(path).map_err(|_| WriteFileError::InvalidPath)?; let file_type = mime_guess::from_ext(&filename) .first_or_text_plain() @@ -190,7 +191,7 @@ impl File { mod test { use super::*; use crate::store::utils::FileCleanup; - use std::path::PathBuf; + use std::{convert::TryFrom, path::PathBuf}; struct FileContext(PathBuf); @@ -242,7 +243,7 @@ mod test { #[test] fn it_raises_an_error_when_file_not_found() { - let resolver = PathResolver(PathBuf::from("fixtures/rawr.png")); + let resolver = PathResolver::try_from("fixtures/rawr.png").expect("a valid path"); match File::load(resolver) { Err(ReadFileError::FileNotFound) => assert!(true), _ => assert!(false), diff --git a/file-service/src/store/fileinfo.rs b/file-service/src/store/fileinfo.rs index ad8f778..ab797b1 100644 --- a/file-service/src/store/fileinfo.rs +++ b/file-service/src/store/fileinfo.rs @@ -1,10 +1,11 @@ +use super::{ReadFileError, WriteFileError}; use chrono::prelude::*; use serde::{Deserialize, Serialize}; use serde_json; -use std::io::{Read, Write}; -use std::path::PathBuf; - -use super::{ReadFileError, WriteFileError}; +use std::{ + io::{Read, Write}, + path::PathBuf, +}; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct FileInfo { @@ -95,12 +96,13 @@ impl FileInfo { #[cfg(test)] mod test { use super::*; - use crate::store::utils::FileCleanup; - use crate::store::PathResolver; + use crate::store::{utils::FileCleanup, PathResolver}; + use std::convert::TryFrom; #[test] fn it_saves_and_loads_metadata() { - let resolver = PathResolver::from("fixtures/1617654d-a588-4714-b4fa-e00ed0a8a607"); + let resolver = PathResolver::try_from("fixtures/1617654d-a588-4714-b4fa-e00ed0a8a607.png") + .expect("a valid path"); let _cleanup = FileCleanup(resolver.metadata_path()); let created = Utc::now(); diff --git a/file-service/src/store/mod.rs b/file-service/src/store/mod.rs index 1e732a3..1832879 100644 --- a/file-service/src/store/mod.rs +++ b/file-service/src/store/mod.rs @@ -1,4 +1,5 @@ use std::{ + convert::TryFrom, ops::Deref, path::{Path, PathBuf}, }; @@ -22,6 +23,12 @@ pub enum WriteFileError { #[error("permission denied")] PermissionDenied, + #[error("invalid path")] + InvalidPath, + + #[error("image conversion failed")] + ImageError(#[from] image::ImageError), + #[error("JSON error")] JSONError(#[from] serde_json::error::Error), @@ -40,6 +47,9 @@ pub enum ReadFileError { #[error("permission denied")] PermissionDenied, + #[error("invalid path")] + InvalidPath, + #[error("JSON error")] JSONError(#[from] serde_json::error::Error), @@ -47,36 +57,89 @@ pub enum ReadFileError { IOError(#[from] std::io::Error), } +#[derive(Debug, Error)] +pub enum PathError { + #[error("path cannot be derived from input")] + InvalidPath, +} + #[derive(Clone, Debug)] -pub struct PathResolver(pub PathBuf); +pub struct PathResolver { + base: PathBuf, + id: String, + extension: String, +} impl PathResolver { + fn new(base: &Path, id: &str, extension: &str) -> Self { + Self { + base: base.to_owned(), + id: id.to_owned(), + extension: extension.to_owned(), + } + } + fn file_path(&self) -> PathBuf { - self.0.clone() + let mut path = self.base.clone(); + path.push(self.id.clone()); + path.set_extension(self.extension.clone()); + path } fn metadata_path(&self) -> PathBuf { - let mut path = self.0.clone(); + let mut path = self.base.clone(); + path.push(self.id.clone()); path.set_extension("json"); path } fn thumbnail_path(&self) -> PathBuf { - let mut path = self.0.clone(); - path.set_extension("tn"); + let mut path = self.base.clone(); + path.push(self.id.clone()); + path.set_extension(format!("tn.{}", self.extension)); path } } -impl From for PathResolver { - fn from(s: String) -> Self { - Self(PathBuf::from(s)) +impl TryFrom for PathResolver { + type Error = PathError; + fn try_from(s: String) -> Result { + PathResolver::try_from(s.as_str()) } } -impl From<&str> for PathResolver { - fn from(s: &str) -> Self { - Self(PathBuf::from(s.to_owned())) +impl TryFrom<&str> for PathResolver { + type Error = PathError; + fn try_from(s: &str) -> Result { + let path = Path::new(s); + PathResolver::try_from(Path::new(s)) + } +} + +impl TryFrom for PathResolver { + type Error = PathError; + fn try_from(path: PathBuf) -> Result { + PathResolver::try_from(path.as_path()) + } +} + +impl TryFrom<&Path> for PathResolver { + type Error = PathError; + fn try_from(path: &Path) -> Result { + Ok(Self { + base: path + .parent() + .map(|s| s.to_owned()) + .ok_or(PathError::InvalidPath)?, + id: path + .file_stem() + .and_then(|s| s.to_str().map(|s| s.to_owned())) + .ok_or(PathError::InvalidPath)?, + extension: path + .extension() + .and_then(|s| s.to_str().map(|s| s.to_owned())) + .ok_or(PathError::InvalidPath)?, + }) } } @@ -176,10 +239,11 @@ mod test { #[test] fn paths() { - let resolver = PathResolver::from("path/82420255-d3c8-4d90-a582-f94be588c70c"); + let resolver = PathResolver::try_from("path/82420255-d3c8-4d90-a582-f94be588c70c.png") + .expect("to have a valid path"); assert_eq!( resolver.file_path(), - PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c") + PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.png") ); assert_eq!( resolver.metadata_path(), @@ -187,7 +251,7 @@ mod test { ); assert_eq!( resolver.thumbnail_path(), - PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.tn") + PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.tn.png") ); } @@ -204,9 +268,9 @@ mod test { let _md = FileCleanup(PathBuf::from(format!("var/{}.json", *file_record.id))); let _tn = FileCleanup(PathBuf::from(format!("var/{}.tn", *file_record.id))); - assert!(PathBuf::from(format!("var/{}", *file_record.id)).exists()); - assert!(PathBuf::from(format!("var/{}.json", *file_record.id)).exists()); - assert!(PathBuf::from(format!("var/{}.tn", *file_record.id)).exists()); + assert!(PathBuf::from(format!("var/{}.png", *file_record.id)).exists()); + assert!(PathBuf::from(format!("var/{}.png.json", *file_record.id)).exists()); + assert!(PathBuf::from(format!("var/{}.png.tn", *file_record.id)).exists()); } #[test] diff --git a/file-service/src/store/thumbnail.rs b/file-service/src/store/thumbnail.rs index 99ddd4d..1b7c6ad 100644 --- a/file-service/src/store/thumbnail.rs +++ b/file-service/src/store/thumbnail.rs @@ -6,31 +6,25 @@ use super::{ReadFileError, WriteFileError}; #[derive(Clone, Debug, PartialEq)] pub struct Thumbnail { - pub id: String, - pub root: PathBuf, + pub path: PathBuf, } impl Thumbnail { - pub fn open(id: &str, root: &Path) -> Result { - /* - let mut source_path = PathBuf::from(root); - source_path.push(id); - - let self_ = Thumbnail { - id: String::from(id), - root: PathBuf::from(root), + pub fn open( + origin_path: PathBuf, + thumbnail_path: PathBuf, + ) -> Result { + let s = Thumbnail { + path: PathBuf::from(thumbnail_path), }; - let thumbnail_path = Thumbnail::thumbnail_path(id, root); - if !thumbnail_path.exists() { - let img = image::open(source_path)?; + if !s.path.exists() { + let img = image::open(&origin_path)?; let tn = img.resize(640, 640, FilterType::Nearest); - tn.save(thumbnail_path)?; + tn.save(&s.path)?; } - Ok(self_) - */ - unimplemented!() + Ok(s) } /* @@ -40,7 +34,7 @@ impl Thumbnail { .map(|s| String::from(s.to_string_lossy())) .ok_or(ReadFileError::NotAnImage(PathBuf::from(path)))?; - let root = path + let path = path .parent() .ok_or(ReadFileError::FileNotFound(PathBuf::from(path)))?; @@ -48,16 +42,17 @@ impl Thumbnail { } */ + /* fn thumbnail_path(id: &str, root: &Path) -> PathBuf { let mut path = PathBuf::from(root); path.push(".thumbnails"); path.push(id.clone()); path } + */ pub fn stream(&self) -> Result { - let thumbnail_path = Thumbnail::thumbnail_path(&self.id, &self.root); - std::fs::File::open(thumbnail_path.clone()).map_err(|err| { + std::fs::File::open(self.path.clone()).map_err(|err| { if err.kind() == std::io::ErrorKind::NotFound { ReadFileError::FileNotFound } else { @@ -66,9 +61,8 @@ impl Thumbnail { }) } - pub fn delete(&self) -> Result<(), WriteFileError> { - let path = Thumbnail::thumbnail_path(&self.id, &self.root); - remove_file(path).map_err(WriteFileError::from) + pub fn delete(self) -> Result<(), WriteFileError> { + remove_file(self.path).map_err(WriteFileError::from) } } @@ -79,9 +73,12 @@ mod test { #[test] fn it_creates_a_thumbnail_if_one_does_not_exist() { - let _ = FileCleanup(PathBuf::from("fixtures/.thumbnails/rawr.png")); - let _ = - Thumbnail::open("rawr.png", Path::new("fixtures")).expect("thumbnail open must work"); - assert!(Path::new("fixtures/.thumbnails/rawr.png").is_file()); + let _ = FileCleanup(PathBuf::from("var/rawr.tn.png")); + let _ = Thumbnail::open( + PathBuf::from("fixtures/rawr.png"), + PathBuf::from("var/rawr.tn.png"), + ) + .expect("thumbnail open must work"); + assert!(Path::new("var/rawr.tn.png").is_file()); } } -- 2.44.1 From 88938e44c8f1c723ec1c39fd37475cb1e8733e25 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sat, 23 Sep 2023 19:15:56 -0400 Subject: [PATCH 13/36] Load file by ID --- file-service/src/main.rs | 27 ++++++---- file-service/src/store/file.rs | 35 ++++++------ file-service/src/store/fileinfo.rs | 11 +++- file-service/src/store/mod.rs | 83 ++++++++++++++++++++--------- file-service/src/store/thumbnail.rs | 11 ++-- 5 files changed, 104 insertions(+), 63 deletions(-) diff --git a/file-service/src/main.rs b/file-service/src/main.rs index 22586f6..d8d5acc 100644 --- a/file-service/src/main.rs +++ b/file-service/src/main.rs @@ -30,7 +30,7 @@ mod middleware; mod pages; mod store; -pub use store::{FileInfo, Store}; +pub use store::{File, FileId, FileInfo, Store}; /* fn is_admin(resource: &ResourceName, permissions: &Permissions) -> bool { @@ -229,23 +229,24 @@ fn script(_: &mut Request) -> IronResult { fn serve_file( info: FileInfo, - mut file: std::fs::File, + file: File, old_etags: Option, ) -> http::Result>> { - let mut content = Vec::new(); match old_etags { Some(old_etags) if old_etags != info.hash => warp::http::Response::builder() .header("content-type", info.file_type) .status(StatusCode::NOT_MODIFIED) - .body(content), - _ => { - let _ = file.read_to_end(&mut content); - warp::http::Response::builder() + .body(vec![]), + _ => match file.content() { + Ok(content) => warp::http::Response::builder() .header("content-type", info.file_type) .header("etag", info.hash) .status(StatusCode::OK) - .body(content) - } + .body(content), + Err(err) => warp::http::Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(vec![]), + }, } } @@ -359,7 +360,7 @@ pub async fn main() { move |id: String, old_etags: Option| match app .read() .unwrap() - .get_thumbnail(&id) + .get_thumbnail(FileId::from(id)) { Ok((info, file)) => serve_file(info, file, old_etags), Err(_err) => warp::http::Response::builder() @@ -373,7 +374,11 @@ pub async fn main() { .and(warp::header::optional::("if-none-match")) .map({ let app = app.clone(); - move |id: String, old_etags: Option| match app.read().unwrap().get_file(&id) { + move |id: String, old_etags: Option| match app + .read() + .unwrap() + .get_file(FileId::from(id)) + { Ok((info, file)) => serve_file(info, file, old_etags), Err(_err) => warp::http::Response::builder() .status(StatusCode::NOT_FOUND) diff --git a/file-service/src/store/file.rs b/file-service/src/store/file.rs index c9e969f..4abf8fc 100644 --- a/file-service/src/store/file.rs +++ b/file-service/src/store/file.rs @@ -27,19 +27,29 @@ impl File { let id = FileId::from(Uuid::new_v4().hyphenated().to_string()); let mut path = context.root(); - path.push(filename.clone()); - let path = PathResolver::try_from(path).map_err(|_| WriteFileError::InvalidPath)?; + let extension = PathBuf::from(filename) + .extension() + .and_then(|s| s.to_str().map(|s| s.to_owned())) + .ok_or(WriteFileError::InvalidPath)?; + path.push((*id).clone()); + let path = PathResolver { + base: context.root().clone(), + id: (*id).clone(), + extension: extension.clone(), + }; - let file_type = mime_guess::from_ext(&filename) + let file_type = mime_guess::from_ext(&extension) .first_or_text_plain() .essence_str() .to_owned(); let info = FileInfo { + id: id.clone(), size: 0, created: Utc::now(), file_type, hash: "".to_owned(), + extension, }; let mut md_file = std::fs::File::create(path.metadata_path())?; @@ -49,21 +59,12 @@ impl File { } pub fn load(resolver: PathResolver) -> Result { - /* + let info = FileInfo::load(resolver.metadata_path())?; Ok(Self { - id: FileId::from( - resolver - .file_path() - .file_stem() - .unwrap() - .to_string_lossy() - .to_owned(), - ), + id: info.id.clone(), path: resolver, - info: FileInfo::load(resolver.metadata_path())?, + info, }) - */ - unimplemented!() } pub fn set_content(&mut self, content: Vec) -> Result<(), WriteFileError> { @@ -74,6 +75,8 @@ impl File { let mut md_file = std::fs::File::create(self.path.metadata_path())?; md_file.write(&serde_json::to_vec(&self.info)?)?; + Thumbnail::open(self.path.file_path(), self.path.thumbnail_path())?; + Ok(()) } @@ -245,7 +248,7 @@ mod test { fn it_raises_an_error_when_file_not_found() { let resolver = PathResolver::try_from("fixtures/rawr.png").expect("a valid path"); match File::load(resolver) { - Err(ReadFileError::FileNotFound) => assert!(true), + Err(ReadFileError::FileNotFound(_)) => assert!(true), _ => assert!(false), } } diff --git a/file-service/src/store/fileinfo.rs b/file-service/src/store/fileinfo.rs index ab797b1..38e8917 100644 --- a/file-service/src/store/fileinfo.rs +++ b/file-service/src/store/fileinfo.rs @@ -1,3 +1,5 @@ +use crate::FileId; + use super::{ReadFileError, WriteFileError}; use chrono::prelude::*; use serde::{Deserialize, Serialize}; @@ -9,16 +11,19 @@ use std::{ #[derive(Clone, Debug, Serialize, Deserialize)] pub struct FileInfo { + pub id: FileId, pub size: usize, pub created: DateTime, pub file_type: String, pub hash: String, + pub extension: String, } impl FileInfo { pub fn load(path: PathBuf) -> Result { let mut content: Vec = Vec::new(); - let mut file = std::fs::File::open(path)?; + let mut file = + std::fs::File::open(path.clone()).map_err(|_| ReadFileError::FileNotFound(path))?; file.read_to_end(&mut content)?; let js = serde_json::from_slice(&content)?; @@ -96,7 +101,7 @@ impl FileInfo { #[cfg(test)] mod test { use super::*; - use crate::store::{utils::FileCleanup, PathResolver}; + use crate::store::{utils::FileCleanup, FileId, PathResolver}; use std::convert::TryFrom; #[test] @@ -108,10 +113,12 @@ mod test { let created = Utc::now(); let info = FileInfo { + id: FileId("temp-id".to_owned()), size: 23777, created, file_type: "image/png".to_owned(), hash: "abcdefg".to_owned(), + extension: "png".to_owned(), }; info.save(resolver.metadata_path()).unwrap(); diff --git a/file-service/src/store/mod.rs b/file-service/src/store/mod.rs index 1832879..8c9b050 100644 --- a/file-service/src/store/mod.rs +++ b/file-service/src/store/mod.rs @@ -1,3 +1,4 @@ +use serde::{Deserialize, Serialize}; use std::{ convert::TryFrom, ops::Deref, @@ -39,7 +40,7 @@ pub enum WriteFileError { #[derive(Debug, Error)] pub enum ReadFileError { #[error("file not found")] - FileNotFound, + FileNotFound(PathBuf), #[error("path is not a file")] NotAFile, @@ -143,7 +144,7 @@ impl TryFrom<&Path> for PathResolver { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct FileId(String); impl From for FileId { @@ -197,7 +198,7 @@ impl Store { Ok(file) } - pub fn delete_file(&mut self, id: String) -> Result<(), WriteFileError> { + pub fn delete_file(&mut self, id: FileId) -> Result<(), WriteFileError> { /* let f = File::open(&id, &self.files_root)?; f.delete() @@ -205,36 +206,36 @@ impl Store { unimplemented!() } - pub fn get_metadata(&self, id: String) -> Result { - // FileInfo::open(&id, &self.files_root) - unimplemented!() + pub fn get_metadata(&self, id: FileId) -> Result { + let mut path = self.files_root.clone(); + path.push(PathBuf::from((*id).clone())); + path.set_extension("json"); + FileInfo::load(path) } - pub fn get_file(&self, id: &str) -> Result<(FileInfo, std::fs::File), ReadFileError> { - /* - let f = File::open(&id, &self.files_root)?; - let info = f.info(); - let stream = f.stream()?; - Ok((info, stream)) - */ - unimplemented!() + pub fn get_file(&self, id: FileId) -> Result<(FileInfo, File), ReadFileError> { + let info = self.get_metadata(id.clone())?; + + let resolver = PathResolver { + base: self.files_root.clone(), + id: (*id).clone(), + extension: info.extension.clone(), + }; + + let f = File::load(resolver)?; + Ok((info, f)) } - pub fn get_thumbnail(&self, id: &str) -> Result<(FileInfo, std::fs::File), ReadFileError> { - /* - let f = File::open(id, &self.files_root)?; - let stream = f.thumbnail().stream()?; - Ok((f.info(), stream)) - */ + pub fn get_thumbnail(&self, id: FileId) -> Result<(FileInfo, File), ReadFileError> { unimplemented!() } } #[cfg(test)] mod test { - use crate::store::utils::FileCleanup; - use super::*; + use crate::store::utils::FileCleanup; + use cool_asserts::assert_matches; use std::io::Read; #[test] @@ -264,17 +265,47 @@ mod test { let mut store = Store::new(PathBuf::from("var/")); let file_record = store.add_file("rawr.png".to_owned(), buf).unwrap(); - let _file = FileCleanup(PathBuf::from(format!("var/{}", *file_record.id))); + let _file = FileCleanup(PathBuf::from(format!("var/{}.png", *file_record.id))); let _md = FileCleanup(PathBuf::from(format!("var/{}.json", *file_record.id))); let _tn = FileCleanup(PathBuf::from(format!("var/{}.tn", *file_record.id))); + store + .get_file(file_record.id.clone()) + .expect("to retrieve the file"); + assert!(PathBuf::from(format!("var/{}.png", *file_record.id)).exists()); - assert!(PathBuf::from(format!("var/{}.png.json", *file_record.id)).exists()); - assert!(PathBuf::from(format!("var/{}.png.tn", *file_record.id)).exists()); + assert!(PathBuf::from(format!("var/{}.json", *file_record.id)).exists()); + assert!(PathBuf::from(format!("var/{}.tn.png", *file_record.id)).exists()); } #[test] - fn sets_up_metadata_for_file() {} + fn sets_up_metadata_for_file() { + let mut buf = Vec::new(); + let mut file = std::fs::File::open("fixtures/rawr.png").unwrap(); + file.read_to_end(&mut buf).unwrap(); + + let mut store = Store::new(PathBuf::from("var/")); + let file_record = store.add_file("rawr.png".to_owned(), buf).unwrap(); + + let _file = FileCleanup(PathBuf::from(format!("var/{}.png", *file_record.id))); + let _md = FileCleanup(PathBuf::from(format!("var/{}.json", *file_record.id))); + let _tn = FileCleanup(PathBuf::from(format!("var/{}.tn", *file_record.id))); + + let info = store + .get_metadata(file_record.id.clone()) + .expect("to retrieve the file"); + + assert_matches!(info, FileInfo { size, file_type, hash, extension, .. } => { + assert_eq!(size, 23777); + assert_eq!(file_type, "image/png"); + assert_eq!(hash, "".to_owned()); + assert_eq!(extension, "png".to_owned()); + }); + + assert!(PathBuf::from(format!("var/{}.png", *file_record.id)).exists()); + assert!(PathBuf::from(format!("var/{}.json", *file_record.id)).exists()); + assert!(PathBuf::from(format!("var/{}.tn.png", *file_record.id)).exists()); + } #[test] fn sets_up_thumbnail_for_file() {} diff --git a/file-service/src/store/thumbnail.rs b/file-service/src/store/thumbnail.rs index 1b7c6ad..250c46c 100644 --- a/file-service/src/store/thumbnail.rs +++ b/file-service/src/store/thumbnail.rs @@ -43,14 +43,6 @@ impl Thumbnail { */ /* - fn thumbnail_path(id: &str, root: &Path) -> PathBuf { - let mut path = PathBuf::from(root); - path.push(".thumbnails"); - path.push(id.clone()); - path - } - */ - pub fn stream(&self) -> Result { std::fs::File::open(self.path.clone()).map_err(|err| { if err.kind() == std::io::ErrorKind::NotFound { @@ -60,10 +52,13 @@ impl Thumbnail { } }) } + */ + /* pub fn delete(self) -> Result<(), WriteFileError> { remove_file(self.path).map_err(WriteFileError::from) } + */ } #[cfg(test)] -- 2.44.1 From 3e87e13526a815c67c4bab087061bc6975062053 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sat, 23 Sep 2023 20:20:35 -0400 Subject: [PATCH 14/36] Provide a unified interface for the File and Thumbnail --- file-service/src/main.rs | 10 +-- file-service/src/store/file.rs | 43 +++++---- file-service/src/store/fileinfo.rs | 2 +- file-service/src/store/mod.rs | 131 +++++++++++++++++----------- file-service/src/store/thumbnail.rs | 31 ++++++- 5 files changed, 135 insertions(+), 82 deletions(-) diff --git a/file-service/src/main.rs b/file-service/src/main.rs index d8d5acc..5c3575f 100644 --- a/file-service/src/main.rs +++ b/file-service/src/main.rs @@ -30,7 +30,7 @@ mod middleware; mod pages; mod store; -pub use store::{File, FileId, FileInfo, Store}; +pub use store::{File, FileId, FileInfo, HasContent, Store}; /* fn is_admin(resource: &ResourceName, permissions: &Permissions) -> bool { @@ -229,7 +229,7 @@ fn script(_: &mut Request) -> IronResult { fn serve_file( info: FileInfo, - file: File, + file: impl HasContent, old_etags: Option, ) -> http::Result>> { match old_etags { @@ -243,7 +243,7 @@ fn serve_file( .header("etag", info.hash) .status(StatusCode::OK) .body(content), - Err(err) => warp::http::Response::builder() + Err(_) => warp::http::Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .body(vec![]), }, @@ -360,7 +360,7 @@ pub async fn main() { move |id: String, old_etags: Option| match app .read() .unwrap() - .get_thumbnail(FileId::from(id)) + .get_thumbnail(&FileId::from(id)) { Ok((info, file)) => serve_file(info, file, old_etags), Err(_err) => warp::http::Response::builder() @@ -377,7 +377,7 @@ pub async fn main() { move |id: String, old_etags: Option| match app .read() .unwrap() - .get_file(FileId::from(id)) + .get_file(&FileId::from(id)) { Ok((info, file)) => serve_file(info, file, old_etags), Err(_err) => warp::http::Response::builder() diff --git a/file-service/src/store/file.rs b/file-service/src/store/file.rs index 4abf8fc..5449ec3 100644 --- a/file-service/src/store/file.rs +++ b/file-service/src/store/file.rs @@ -1,6 +1,6 @@ use super::{ - fileinfo::FileInfo, thumbnail::Thumbnail, FileId, FileRoot, PathResolver, ReadFileError, - WriteFileError, + fileinfo::FileInfo, thumbnail::Thumbnail, FileId, FileRoot, HasContent, PathResolver, + ReadFileError, WriteFileError, }; use chrono::prelude::*; use hex_string::HexString; @@ -80,10 +80,6 @@ impl File { Ok(()) } - pub fn content(&self) -> Result, ReadFileError> { - unimplemented!() - } - pub fn hash_content(&self) -> Result { let mut buf = Vec::new(); let mut file = std::fs::File::open(self.path.file_path())?; @@ -190,6 +186,17 @@ impl File { */ } +impl HasContent for File { + fn content(&self) -> Result, ReadFileError> { + let mut content: Vec = Vec::new(); + + let mut file = std::fs::File::open(self.path.file_path())?; + file.read_to_end(&mut content)?; + + Ok(content) + } +} + #[cfg(test)] mod test { use super::*; @@ -209,26 +216,19 @@ mod test { let _md = FileCleanup(PathBuf::from("fixtures/.metadata/rawr.png.json")); let _tn = FileCleanup(PathBuf::from("fixtures/.thumbnails/rawr.png")); - File::new( - "rawr.png".to_owned(), - FileContext(PathBuf::from("fixtures/")), - ) - .expect("to succeed"); + File::new("rawr.png".to_owned(), FileContext(PathBuf::from("var/"))).expect("to succeed"); } #[test] fn it_can_return_a_thumbnail() { - let f = File::new( - "rawr.png".to_owned(), - FileContext(PathBuf::from("fixtures/")), - ) - .expect("to succeed"); + let f = File::new("rawr.png".to_owned(), FileContext(PathBuf::from("var/"))) + .expect("to succeed"); /* assert_eq!( f.thumbnail(), Thumbnail { id: String::from("rawr.png"), - root: PathBuf::from("fixtures/"), + root: PathBuf::from("var/"), }, ); */ @@ -236,17 +236,14 @@ mod test { #[test] fn it_can_return_a_file_stream() { - let f = File::new( - "rawr.png".to_owned(), - FileContext(PathBuf::from("fixtures/")), - ) - .expect("to succeed"); + let f = File::new("rawr.png".to_owned(), FileContext(PathBuf::from("var/"))) + .expect("to succeed"); // f.stream().expect("to succeed"); } #[test] fn it_raises_an_error_when_file_not_found() { - let resolver = PathResolver::try_from("fixtures/rawr.png").expect("a valid path"); + let resolver = PathResolver::try_from("var/rawr.png").expect("a valid path"); match File::load(resolver) { Err(ReadFileError::FileNotFound(_)) => assert!(true), _ => assert!(false), diff --git a/file-service/src/store/fileinfo.rs b/file-service/src/store/fileinfo.rs index 38e8917..55223a4 100644 --- a/file-service/src/store/fileinfo.rs +++ b/file-service/src/store/fileinfo.rs @@ -106,7 +106,7 @@ mod test { #[test] fn it_saves_and_loads_metadata() { - let resolver = PathResolver::try_from("fixtures/1617654d-a588-4714-b4fa-e00ed0a8a607.png") + let resolver = PathResolver::try_from("var/1617654d-a588-4714-b4fa-e00ed0a8a607.png") .expect("a valid path"); let _cleanup = FileCleanup(resolver.metadata_path()); diff --git a/file-service/src/store/mod.rs b/file-service/src/store/mod.rs index 8c9b050..4d098e3 100644 --- a/file-service/src/store/mod.rs +++ b/file-service/src/store/mod.rs @@ -64,6 +64,10 @@ pub enum PathError { InvalidPath, } +pub trait HasContent { + fn content(&self) -> Result, ReadFileError>; +} + #[derive(Clone, Debug)] pub struct PathResolver { base: PathBuf, @@ -159,6 +163,19 @@ impl From<&str> for FileId { } } +impl From for PathBuf { + fn from(s: FileId) -> Self { + Self::from(&s) + } +} + +impl From<&FileId> for PathBuf { + fn from(s: &FileId) -> Self { + let FileId(s) = s; + Self::from(s) + } +} + impl Deref for FileId { type Target = String; fn deref(&self) -> &Self::Target { @@ -198,7 +215,7 @@ impl Store { Ok(file) } - pub fn delete_file(&mut self, id: FileId) -> Result<(), WriteFileError> { + pub fn delete_file(&mut self, id: &FileId) -> Result<(), WriteFileError> { /* let f = File::open(&id, &self.files_root)?; f.delete() @@ -206,19 +223,19 @@ impl Store { unimplemented!() } - pub fn get_metadata(&self, id: FileId) -> Result { + pub fn get_metadata(&self, id: &FileId) -> Result { let mut path = self.files_root.clone(); - path.push(PathBuf::from((*id).clone())); + path.push(PathBuf::from(id)); path.set_extension("json"); FileInfo::load(path) } - pub fn get_file(&self, id: FileId) -> Result<(FileInfo, File), ReadFileError> { - let info = self.get_metadata(id.clone())?; + pub fn get_file(&self, id: &FileId) -> Result<(FileInfo, File), ReadFileError> { + let info = self.get_metadata(id)?; let resolver = PathResolver { base: self.files_root.clone(), - id: (*id).clone(), + id: (**id).clone(), extension: info.extension.clone(), }; @@ -226,8 +243,17 @@ impl Store { Ok((info, f)) } - pub fn get_thumbnail(&self, id: FileId) -> Result<(FileInfo, File), ReadFileError> { - unimplemented!() + pub fn get_thumbnail(&self, id: &FileId) -> Result<(FileInfo, Thumbnail), ReadFileError> { + let info = self.get_metadata(id)?; + + let resolver = PathResolver { + base: self.files_root.clone(), + id: (**id).clone(), + extension: info.extension.clone(), + }; + + let f = Thumbnail::load(resolver.thumbnail_path())?; + Ok((info, f)) } } @@ -238,6 +264,24 @@ mod test { use cool_asserts::assert_matches; use std::io::Read; + fn with_file(test_fn: F) + where + F: FnOnce(Store, FileId), + { + let mut buf = Vec::new(); + let mut file = std::fs::File::open("fixtures/rawr.png").unwrap(); + file.read_to_end(&mut buf).unwrap(); + + let mut store = Store::new(PathBuf::from("var/")); + let file_record = store.add_file("rawr.png".to_owned(), buf).unwrap(); + + let _file = FileCleanup(PathBuf::from(format!("var/{}.png", *file_record.id))); + let _md = FileCleanup(PathBuf::from(format!("var/{}.json", *file_record.id))); + let _tn = FileCleanup(PathBuf::from(format!("var/{}.tn.png", *file_record.id))); + + test_fn(store, file_record.id); + } + #[test] fn paths() { let resolver = PathResolver::try_from("path/82420255-d3c8-4d90-a582-f94be588c70c.png") @@ -258,60 +302,49 @@ mod test { #[test] fn adds_files() { - let mut buf = Vec::new(); - let mut file = std::fs::File::open("fixtures/rawr.png").unwrap(); - file.read_to_end(&mut buf).unwrap(); + with_file(|store, id| { + let (_, file) = store.get_file(&id).expect("to retrieve the file"); - let mut store = Store::new(PathBuf::from("var/")); - let file_record = store.add_file("rawr.png".to_owned(), buf).unwrap(); + assert_eq!(file.content().map(|file| file.len()).unwrap(), 23777); - let _file = FileCleanup(PathBuf::from(format!("var/{}.png", *file_record.id))); - let _md = FileCleanup(PathBuf::from(format!("var/{}.json", *file_record.id))); - let _tn = FileCleanup(PathBuf::from(format!("var/{}.tn", *file_record.id))); - - store - .get_file(file_record.id.clone()) - .expect("to retrieve the file"); - - assert!(PathBuf::from(format!("var/{}.png", *file_record.id)).exists()); - assert!(PathBuf::from(format!("var/{}.json", *file_record.id)).exists()); - assert!(PathBuf::from(format!("var/{}.tn.png", *file_record.id)).exists()); + assert!(PathBuf::from(format!("var/{}.png", *id)).exists()); + assert!(PathBuf::from(format!("var/{}.json", *id)).exists()); + assert!(PathBuf::from(format!("var/{}.tn.png", *id)).exists()); + }); } #[test] fn sets_up_metadata_for_file() { - let mut buf = Vec::new(); - let mut file = std::fs::File::open("fixtures/rawr.png").unwrap(); - file.read_to_end(&mut buf).unwrap(); + with_file(|store, id| { + let info = store.get_metadata(&id).expect("to retrieve the metadata"); - let mut store = Store::new(PathBuf::from("var/")); - let file_record = store.add_file("rawr.png".to_owned(), buf).unwrap(); - - let _file = FileCleanup(PathBuf::from(format!("var/{}.png", *file_record.id))); - let _md = FileCleanup(PathBuf::from(format!("var/{}.json", *file_record.id))); - let _tn = FileCleanup(PathBuf::from(format!("var/{}.tn", *file_record.id))); - - let info = store - .get_metadata(file_record.id.clone()) - .expect("to retrieve the file"); - - assert_matches!(info, FileInfo { size, file_type, hash, extension, .. } => { - assert_eq!(size, 23777); - assert_eq!(file_type, "image/png"); - assert_eq!(hash, "".to_owned()); - assert_eq!(extension, "png".to_owned()); + assert_matches!(info, FileInfo { size, file_type, hash, extension, .. } => { + assert_eq!(size, 23777); + assert_eq!(file_type, "image/png"); + assert_eq!(hash, "".to_owned()); + assert_eq!(extension, "png".to_owned()); + }); }); - - assert!(PathBuf::from(format!("var/{}.png", *file_record.id)).exists()); - assert!(PathBuf::from(format!("var/{}.json", *file_record.id)).exists()); - assert!(PathBuf::from(format!("var/{}.tn.png", *file_record.id)).exists()); } #[test] - fn sets_up_thumbnail_for_file() {} + fn sets_up_thumbnail_for_file() { + with_file(|store, id| { + let (_, thumbnail) = store.get_thumbnail(&id).expect("to retrieve the thumbnail"); + assert_eq!(thumbnail.content().map(|file| file.len()).unwrap(), 48869); + }); + } #[test] - fn deletes_associated_files() {} + fn deletes_associated_files() { + with_file(|mut store, id| { + store.delete_file(&id).expect("file to be deleted"); + + assert!(!PathBuf::from(format!("var/{}.png", *id)).exists()); + assert!(!PathBuf::from(format!("var/{}.json", *id)).exists()); + assert!(!PathBuf::from(format!("var/{}.tn.png", *id)).exists()); + }); + } #[test] fn lists_files_in_the_db() {} diff --git a/file-service/src/store/thumbnail.rs b/file-service/src/store/thumbnail.rs index 250c46c..1c706cc 100644 --- a/file-service/src/store/thumbnail.rs +++ b/file-service/src/store/thumbnail.rs @@ -1,8 +1,10 @@ +use super::{HasContent, ReadFileError, WriteFileError}; use image::imageops::FilterType; -use std::fs::remove_file; -use std::path::{Path, PathBuf}; - -use super::{ReadFileError, WriteFileError}; +use std::{ + fs::remove_file, + io::Read, + path::{Path, PathBuf}, +}; #[derive(Clone, Debug, PartialEq)] pub struct Thumbnail { @@ -27,6 +29,16 @@ impl Thumbnail { Ok(s) } + pub fn load(path: PathBuf) -> Result { + let s = Thumbnail { path: path.clone() }; + + if !s.path.exists() { + return Err(ReadFileError::FileNotFound(path)); + } + + Ok(s) + } + /* pub fn from_path(path: &Path) -> Result { let id = path @@ -61,6 +73,17 @@ impl Thumbnail { */ } +impl HasContent for Thumbnail { + fn content(&self) -> Result, ReadFileError> { + let mut content: Vec = Vec::new(); + + let mut file = std::fs::File::open(&self.path)?; + file.read_to_end(&mut content)?; + + Ok(content) + } +} + #[cfg(test)] mod test { use super::*; -- 2.44.1 From da6bf3bfead7275d51289a998a118e281d72b57f Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sat, 23 Sep 2023 20:20:57 -0400 Subject: [PATCH 15/36] Add cool_asserts --- Cargo.lock | 1 + file-service/Cargo.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index ce56ba8..bcf0082 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -663,6 +663,7 @@ dependencies = [ "build_html", "bytes", "chrono", + "cool_asserts", "futures-util", "hex-string", "http", diff --git a/file-service/Cargo.toml b/file-service/Cargo.toml index 8452602..531ef4c 100644 --- a/file-service/Cargo.toml +++ b/file-service/Cargo.toml @@ -31,4 +31,5 @@ pretty_env_logger = { version = "0.5" } log = { version = "0.4" } bytes = { version = "1" } futures-util = { version = "0.3" } +cool_asserts = { version = "2" } -- 2.44.1 From 68b62464f0851fade2b81b0f763112e140ba3154 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sun, 24 Sep 2023 12:08:09 -0400 Subject: [PATCH 16/36] Clean up the filehandle logic --- file-service/src/main.rs | 40 ++-- file-service/src/pages.rs | 18 +- file-service/src/store/file.rs | 252 ----------------------- file-service/src/store/filehandle.rs | 294 +++++++++++++++++++++++++++ file-service/src/store/fileinfo.rs | 62 +----- file-service/src/store/mod.rs | 186 ++++------------- file-service/src/store/thumbnail.rs | 13 +- 7 files changed, 366 insertions(+), 499 deletions(-) delete mode 100644 file-service/src/store/file.rs create mode 100644 file-service/src/store/filehandle.rs diff --git a/file-service/src/main.rs b/file-service/src/main.rs index 5c3575f..549854e 100644 --- a/file-service/src/main.rs +++ b/file-service/src/main.rs @@ -30,7 +30,7 @@ mod middleware; mod pages; mod store; -pub use store::{File, FileId, FileInfo, HasContent, Store}; +pub use store::{FileHandle, FileId, FileInfo, ReadFileError, Store}; /* fn is_admin(resource: &ResourceName, permissions: &Permissions) -> bool { @@ -228,19 +228,18 @@ fn script(_: &mut Request) -> IronResult { */ fn serve_file( - info: FileInfo, - file: impl HasContent, + file: FileHandle, old_etags: Option, ) -> http::Result>> { match old_etags { - Some(old_etags) if old_etags != info.hash => warp::http::Response::builder() - .header("content-type", info.file_type) + Some(old_etags) if old_etags != file.info.hash => warp::http::Response::builder() + .header("content-type", file.info.file_type) .status(StatusCode::NOT_MODIFIED) .body(vec![]), _ => match file.content() { Ok(content) => warp::http::Response::builder() - .header("content-type", info.file_type) - .header("etag", info.hash) + .header("content-type", file.info.file_type) + .header("etag", file.info.hash) .status(StatusCode::OK) .body(content), Err(_) => warp::http::Response::builder() @@ -331,10 +330,23 @@ pub async fn main() { let app = app.clone(); move || { info!("root handler"); - warp::http::Response::builder() - .header("content-type", "text/html") - .status(StatusCode::OK) - .body(pages::index(app.read().unwrap().list_files()).to_html_string()) + let app = app.read().unwrap(); + match app.list_files() { + Ok(ids) => { + let files = ids + .into_iter() + .map(|id| app.get_file(&id)) + .collect::>>(); + warp::http::Response::builder() + .header("content-type", "text/html") + .status(StatusCode::OK) + .body(pages::index(files).to_html_string()) + } + Err(_) => warp::http::Response::builder() + .header("content-type", "text/html") + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body("".to_owned()), + } } }); @@ -360,9 +372,9 @@ pub async fn main() { move |id: String, old_etags: Option| match app .read() .unwrap() - .get_thumbnail(&FileId::from(id)) + .get_file(&FileId::from(id)) { - Ok((info, file)) => serve_file(info, file, old_etags), + Ok(file) => serve_file(file, old_etags), Err(_err) => warp::http::Response::builder() .status(StatusCode::NOT_FOUND) .body(vec![]), @@ -379,7 +391,7 @@ pub async fn main() { .unwrap() .get_file(&FileId::from(id)) { - Ok((info, file)) => serve_file(info, file, old_etags), + Ok(file) => serve_file(file, old_etags), Err(_err) => warp::http::Response::builder() .status(StatusCode::NOT_FOUND) .body(vec![]), diff --git a/file-service/src/pages.rs b/file-service/src/pages.rs index 8fac465..a95e70a 100644 --- a/file-service/src/pages.rs +++ b/file-service/src/pages.rs @@ -1,10 +1,10 @@ use crate::{ html::*, - store::{File, ReadFileError}, + store::{FileHandle, FileId, ReadFileError, Thumbnail}, }; use build_html::{self, Container, ContainerType, Html, HtmlContainer}; -pub fn index(files: Vec>) -> build_html::HtmlPage { +pub fn index(handles: Vec>) -> build_html::HtmlPage { let mut page = build_html::HtmlPage::new() .with_title("Admin list of files") .with_header(1, "Admin list of files") @@ -20,11 +20,11 @@ pub fn index(files: Vec>) -> build_html::HtmlPage { .with_html(Button::new("Upload file")), ); - for file in files { - let container = match file { - Ok(ref file) => thumbnail(file).with_html( + for handle in handles { + let container = match handle { + Ok(ref handle) => thumbnail(&handle.id).with_html( Form::new() - .with_path(&format!("/{}", *file.id)) + .with_path(&format!("/{}", *handle.id)) .with_method("post") .with_html(Input::new("hidden", "_method").with_value("delete")) .with_html(Button::new("Delete")), @@ -39,13 +39,13 @@ pub fn index(files: Vec>) -> build_html::HtmlPage { page } -pub fn thumbnail(file: &File) -> Container { +pub fn thumbnail(id: &FileId) -> Container { let mut container = Container::new(ContainerType::Div).with_attributes(vec![("class", "file")]); let tn = Container::new(ContainerType::Div) .with_attributes(vec![("class", "thumbnail")]) .with_link( - format!("/{}", *file.id), - Image::new(&format!("{}/tn", *file.id)).to_html_string(), + format!("/{}", **id), + Image::new(&format!("{}/tn", **id)).to_html_string(), ); container.add_html(tn); container diff --git a/file-service/src/store/file.rs b/file-service/src/store/file.rs deleted file mode 100644 index 5449ec3..0000000 --- a/file-service/src/store/file.rs +++ /dev/null @@ -1,252 +0,0 @@ -use super::{ - fileinfo::FileInfo, thumbnail::Thumbnail, FileId, FileRoot, HasContent, PathResolver, - ReadFileError, WriteFileError, -}; -use chrono::prelude::*; -use hex_string::HexString; -use sha2::{Digest, Sha256}; -use std::{ - convert::TryFrom, - io::{Read, Write}, - path::{Path, PathBuf}, -}; -use uuid::Uuid; - -/// One file in the database, complete with the path of the file and information about the -/// thumbnail of the file. -#[derive(Debug)] -pub struct File { - pub id: FileId, - pub path: PathResolver, - pub info: FileInfo, -} - -impl File { - /// Create a new entry in the database - pub fn new(filename: String, context: CTX) -> Result { - let id = FileId::from(Uuid::new_v4().hyphenated().to_string()); - - let mut path = context.root(); - let extension = PathBuf::from(filename) - .extension() - .and_then(|s| s.to_str().map(|s| s.to_owned())) - .ok_or(WriteFileError::InvalidPath)?; - path.push((*id).clone()); - let path = PathResolver { - base: context.root().clone(), - id: (*id).clone(), - extension: extension.clone(), - }; - - let file_type = mime_guess::from_ext(&extension) - .first_or_text_plain() - .essence_str() - .to_owned(); - - let info = FileInfo { - id: id.clone(), - size: 0, - created: Utc::now(), - file_type, - hash: "".to_owned(), - extension, - }; - - let mut md_file = std::fs::File::create(path.metadata_path())?; - md_file.write(&serde_json::to_vec(&info)?)?; - - Ok(Self { id, path, info }) - } - - pub fn load(resolver: PathResolver) -> Result { - let info = FileInfo::load(resolver.metadata_path())?; - Ok(Self { - id: info.id.clone(), - path: resolver, - info, - }) - } - - pub fn set_content(&mut self, content: Vec) -> Result<(), WriteFileError> { - let mut content_file = std::fs::File::create(self.path.file_path())?; - let byte_count = content_file.write(&content)?; - self.info.size = byte_count; - - let mut md_file = std::fs::File::create(self.path.metadata_path())?; - md_file.write(&serde_json::to_vec(&self.info)?)?; - - Thumbnail::open(self.path.file_path(), self.path.thumbnail_path())?; - - Ok(()) - } - - pub fn hash_content(&self) -> Result { - let mut buf = Vec::new(); - let mut file = std::fs::File::open(self.path.file_path())?; - file.read_to_end(&mut buf).map_err(ReadFileError::from)?; - - Ok(HexString::from_bytes(&Sha256::digest(&buf).to_vec())) - } - - /* - pub fn new( - id: &str, - root: &Path, - filename: &Option, - ) -> Result { - let mut dest_path = PathBuf::from(root); - dest_path.push(id); - match filename { - Some(fname) => match fname.extension() { - Some(ext) => { - dest_path.set_extension(ext); - () - } - None => (), - }, - None => (), - }; - copy(temp_path, dest_path.clone())?; - let info = FileInfo::from_path(&dest_path)?; - let tn = Thumbnail::from_path(&dest_path)?; - Ok(Self { - info, - tn, - root: PathBuf::from(root), - }) - } - - pub fn open(id: &str, root: &Path) -> Result { - let mut file_path = PathBuf::from(root); - file_path.push(id.clone()); - - if !file_path.exists() { - return Err(FileError::FileNotFound(file_path)); - } - if !file_path.is_file() { - return Err(FileError::NotAFile(file_path)); - } - - let info = match FileInfo::open(id, root) { - Ok(i) => Ok(i), - Err(FileError::FileNotFound(_)) => { - let info = FileInfo::from_path(&file_path)?; - info.save(&root)?; - Ok(info) - } - Err(err) => Err(err), - }?; - - let tn = Thumbnail::open(id, root)?; - - Ok(Self { - info, - tn, - root: PathBuf::from(root), - }) - } - - pub fn list(root: &Path) -> Vec> { - let dir_iter = read_dir(&root).unwrap(); - dir_iter - .filter(|entry| { - let entry_ = entry.as_ref().unwrap(); - let filename = entry_.file_name(); - !(filename.to_string_lossy().starts_with(".")) - }) - .map(|entry| { - let entry_ = entry.unwrap(); - let id = entry_.file_name().into_string().unwrap(); - Self::open(&id, root) - }) - .collect() - } - - pub fn info(&self) -> FileInfo { - self.info.clone() - } - - pub fn thumbnail(&self) -> Thumbnail { - self.tn.clone() - } - - pub fn stream(&self) -> Result { - let mut path = self.root.clone(); - path.push(self.info.id.clone()); - std::fs::File::open(path).map_err(FileError::from) - } - - pub fn delete(&self) -> Result<(), FileError> { - let mut path = self.root.clone(); - path.push(self.info.id.clone()); - remove_file(path)?; - self.tn.delete()?; - self.info.delete() - } - */ -} - -impl HasContent for File { - fn content(&self) -> Result, ReadFileError> { - let mut content: Vec = Vec::new(); - - let mut file = std::fs::File::open(self.path.file_path())?; - file.read_to_end(&mut content)?; - - Ok(content) - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::store::utils::FileCleanup; - use std::{convert::TryFrom, path::PathBuf}; - - struct FileContext(PathBuf); - - impl FileRoot for FileContext { - fn root(&self) -> PathBuf { - self.0.clone() - } - } - - #[test] - fn it_opens_a_file() { - let _md = FileCleanup(PathBuf::from("fixtures/.metadata/rawr.png.json")); - let _tn = FileCleanup(PathBuf::from("fixtures/.thumbnails/rawr.png")); - - File::new("rawr.png".to_owned(), FileContext(PathBuf::from("var/"))).expect("to succeed"); - } - - #[test] - fn it_can_return_a_thumbnail() { - let f = File::new("rawr.png".to_owned(), FileContext(PathBuf::from("var/"))) - .expect("to succeed"); - /* - assert_eq!( - f.thumbnail(), - Thumbnail { - id: String::from("rawr.png"), - root: PathBuf::from("var/"), - }, - ); - */ - } - - #[test] - fn it_can_return_a_file_stream() { - let f = File::new("rawr.png".to_owned(), FileContext(PathBuf::from("var/"))) - .expect("to succeed"); - // f.stream().expect("to succeed"); - } - - #[test] - fn it_raises_an_error_when_file_not_found() { - let resolver = PathResolver::try_from("var/rawr.png").expect("a valid path"); - match File::load(resolver) { - Err(ReadFileError::FileNotFound(_)) => assert!(true), - _ => assert!(false), - } - } -} diff --git a/file-service/src/store/filehandle.rs b/file-service/src/store/filehandle.rs new file mode 100644 index 0000000..50f79d5 --- /dev/null +++ b/file-service/src/store/filehandle.rs @@ -0,0 +1,294 @@ +use super::{fileinfo::FileInfo, FileId, ReadFileError, WriteFileError}; +use chrono::prelude::*; +use hex_string::HexString; +use image::imageops::FilterType; +use sha2::{Digest, Sha256}; +use std::{ + convert::TryFrom, + io::{Read, Write}, + path::{Path, PathBuf}, +}; +use thiserror::Error; +use uuid::Uuid; + +#[derive(Debug, Error)] +pub enum PathError { + #[error("path cannot be derived from input")] + InvalidPath, +} + +#[derive(Clone, Debug)] +pub struct PathResolver { + base: PathBuf, + id: FileId, +} + +impl PathResolver { + pub fn new(base: &Path, id: FileId) -> Self { + Self { + base: base.to_owned(), + id, + } + } + + pub fn id(&self) -> FileId { + self.id.clone() + } + + pub fn file_path(&self) -> Result { + let info = FileInfo::load(self.metadata_path())?; + + let mut path = self.base.clone(); + path.push(PathBuf::from(self.id.clone())); + path.set_extension(info.extension); + Ok(path) + } + + pub fn metadata_path(&self) -> PathBuf { + let mut path = self.base.clone(); + path.push(PathBuf::from(self.id.clone())); + path.set_extension("json"); + path + } + + pub fn thumbnail_path(&self) -> Result { + let info = FileInfo::load(self.metadata_path())?; + + let mut path = self.base.clone(); + path.push(PathBuf::from(self.id.clone())); + path.set_extension(format!("tn.{}", info.extension)); + Ok(path) + } +} + +impl TryFrom for PathResolver { + type Error = PathError; + fn try_from(s: String) -> Result { + PathResolver::try_from(s.as_str()) + } +} + +impl TryFrom<&str> for PathResolver { + type Error = PathError; + fn try_from(s: &str) -> Result { + let path = Path::new(s); + PathResolver::try_from(Path::new(s)) + } +} + +impl TryFrom for PathResolver { + type Error = PathError; + fn try_from(path: PathBuf) -> Result { + PathResolver::try_from(path.as_path()) + } +} + +impl TryFrom<&Path> for PathResolver { + type Error = PathError; + fn try_from(path: &Path) -> Result { + Ok(Self { + base: path + .parent() + .map(|s| s.to_owned()) + .ok_or(PathError::InvalidPath)?, + id: path + .file_stem() + .and_then(|s| s.to_str().map(|s| FileId::from(s))) + .ok_or(PathError::InvalidPath)?, + }) + } +} + +/// One file in the database, complete with the path of the file and information about the +/// thumbnail of the file. +#[derive(Debug)] +pub struct FileHandle { + pub id: FileId, + pub path: PathResolver, + pub info: FileInfo, +} + +impl FileHandle { + /// Create a new entry in the database + pub fn new(filename: String, root: PathBuf) -> Result { + let id = FileId::from(Uuid::new_v4().hyphenated().to_string()); + + let extension = PathBuf::from(filename) + .extension() + .and_then(|s| s.to_str().map(|s| s.to_owned())) + .ok_or(WriteFileError::InvalidPath)?; + let path = PathResolver { + base: root.clone(), + id: id.clone(), + }; + + let file_type = mime_guess::from_ext(&extension) + .first_or_text_plain() + .essence_str() + .to_owned(); + + let info = FileInfo { + id: id.clone(), + size: 0, + created: Utc::now(), + file_type, + hash: "".to_owned(), + extension, + }; + + let mut md_file = std::fs::File::create(path.metadata_path())?; + md_file.write(&serde_json::to_vec(&info)?)?; + + Ok(Self { id, path, info }) + } + + pub fn load(id: &FileId, root: &Path) -> Result { + let resolver = PathResolver::new(root, id.clone()); + let info = FileInfo::load(resolver.metadata_path())?; + Ok(Self { + id: info.id.clone(), + path: resolver, + info, + }) + } + + pub fn set_content(&mut self, content: Vec) -> Result<(), WriteFileError> { + let mut content_file = std::fs::File::create( + self.path + .file_path() + .map_err(|_| WriteFileError::NoMetadata)?, + )?; + let byte_count = content_file.write(&content)?; + self.info.size = byte_count; + + let mut md_file = std::fs::File::create(self.path.metadata_path())?; + md_file.write(&serde_json::to_vec(&self.info)?)?; + + self.write_thumbnail(); + + Ok(()) + } + + pub fn content(&self) -> Result, ReadFileError> { + load_content(&self.path.file_path()?) + } + + pub fn thumbnail(&self) -> Result, ReadFileError> { + load_content(&self.path.thumbnail_path()?) + } + + fn hash_content(&self) -> Result { + let mut buf = Vec::new(); + let mut file = std::fs::File::open(self.path.file_path()?)?; + file.read_to_end(&mut buf).map_err(ReadFileError::from)?; + + Ok(HexString::from_bytes(&Sha256::digest(&buf).to_vec())) + } + + fn write_thumbnail(&self) -> Result<(), WriteFileError> { + let img = image::open( + &self + .path + .file_path() + .map_err(|_| WriteFileError::NoMetadata)?, + )?; + let tn = img.resize(640, 640, FilterType::Nearest); + tn.save( + &self + .path + .thumbnail_path() + .map_err(|_| WriteFileError::NoMetadata)?, + )?; + Ok(()) + } + + pub fn delete(self) -> Result<(), WriteFileError> { + std::fs::remove_file( + self.path + .thumbnail_path() + .map_err(|_| WriteFileError::NoMetadata)?, + )?; + std::fs::remove_file(self.path.metadata_path())?; + std::fs::remove_file( + self.path + .file_path() + .map_err(|_| WriteFileError::NoMetadata)?, + )?; + + Ok(()) + } +} + +fn load_content(path: &Path) -> Result, ReadFileError> { + let mut buf = Vec::new(); + let mut file = std::fs::File::open(&path)?; + file.read_to_end(&mut buf)?; + Ok(buf) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::store::utils::FileCleanup; + use cool_asserts::assert_matches; + use std::{convert::TryFrom, path::PathBuf}; + + #[test] + fn paths() { + let resolver = PathResolver::try_from("path/82420255-d3c8-4d90-a582-f94be588c70c.png") + .expect("to have a valid path"); + assert_matches!( + resolver.file_path(), + Ok(path) => assert_eq!(path, PathBuf::from( + "path/82420255-d3c8-4d90-a582-f94be588c70c.png" + )) + ); + assert_eq!( + resolver.metadata_path(), + PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.json") + ); + assert_matches!( + resolver.thumbnail_path(), + Ok(path) => assert_eq!(path, PathBuf::from( + "path/82420255-d3c8-4d90-a582-f94be588c70c.tn.png" + )) + ); + } + + #[test] + fn it_opens_a_file() { + let _md = FileCleanup(PathBuf::from("fixtures/.metadata/rawr.png.json")); + let _tn = FileCleanup(PathBuf::from("fixtures/.thumbnails/rawr.png")); + + FileHandle::new("rawr.png".to_owned(), PathBuf::from("var/")).expect("to succeed"); + } + + #[test] + fn it_can_return_a_thumbnail() { + let f = FileHandle::new("rawr.png".to_owned(), PathBuf::from("var/")).expect("to succeed"); + /* + assert_eq!( + f.thumbnail(), + Thumbnail { + id: String::from("rawr.png"), + root: PathBuf::from("var/"), + }, + ); + */ + } + + #[test] + fn it_can_return_a_file_stream() { + let f = FileHandle::new("rawr.png".to_owned(), PathBuf::from("var/")).expect("to succeed"); + // f.stream().expect("to succeed"); + } + + #[test] + fn it_raises_an_error_when_file_not_found() { + let resolver = PathResolver::try_from("var/rawr.png").expect("a valid path"); + match FileHandle::load(&FileId::from("rawr"), &PathBuf::from("var/")) { + Err(ReadFileError::FileNotFound(_)) => assert!(true), + _ => assert!(false), + } + } +} diff --git a/file-service/src/store/fileinfo.rs b/file-service/src/store/fileinfo.rs index 55223a4..6f253ff 100644 --- a/file-service/src/store/fileinfo.rs +++ b/file-service/src/store/fileinfo.rs @@ -36,72 +36,12 @@ impl FileInfo { file.write(ser.as_bytes())?; Ok(()) } - - /* - pub fn from_path(path: &Path) -> Result { - match (path.is_file(), path.is_dir()) { - (false, false) => Err(ReadFileError::FileNotFound), - (false, true) => Err(ReadFileError::NotAFile), - (true, _) => Ok(()), - }?; - - let metadata = path.metadata().map_err(ReadFileError::IOError)?; - let id = path - .file_name() - .map(|s| String::from(s.to_string_lossy())) - .ok_or(ReadFileError::NotAFile)?; - let created = metadata - .created() - .map(|m| DateTime::from(m)) - .map_err(|err| ReadFileError::IOError(err))?; - let file_type = String::from( - mime_guess::from_path(path) - .first_or_octet_stream() - .essence_str(), - ); - let hash = FileInfo::hash_file(path)?; - Ok(FileInfo { - id, - size: metadata.len(), - created, - file_type, - hash: hash.as_string(), - root: PathBuf::from(path.parent().unwrap()), - }) - } - */ - - /* - fn hash_file(path: &Path) -> Result { - let mut buf = Vec::new(); - let mut file = std::fs::File::open(path).map_err(ReadFileError::from)?; - - file.read_to_end(&mut buf).map_err(ReadFileError::from)?; - let mut vec = Vec::new(); - vec.extend_from_slice(Sha256::digest(&buf).as_slice()); - Ok(HexString::from_bytes(&vec)) - } - */ - - /* - fn metadata_path(id: &str, root: &Path) -> PathBuf { - let mut path = PathBuf::from(root); - path.push(".metadata"); - path.push(id.clone()); - append_extension(&path, "json") - } - - pub fn delete(&self) -> Result<(), FileError> { - let path = FileInfo::metadata_path(&self.id, &self.root); - remove_file(path).map_err(FileError::from) - } - */ } #[cfg(test)] mod test { use super::*; - use crate::store::{utils::FileCleanup, FileId, PathResolver}; + use crate::store::{filehandle::PathResolver, utils::FileCleanup, FileId}; use std::convert::TryFrom; #[test] diff --git a/file-service/src/store/mod.rs b/file-service/src/store/mod.rs index 4d098e3..4a9ff6a 100644 --- a/file-service/src/store/mod.rs +++ b/file-service/src/store/mod.rs @@ -1,18 +1,16 @@ use serde::{Deserialize, Serialize}; use std::{ - convert::TryFrom, ops::Deref, path::{Path, PathBuf}, }; use thiserror::Error; -use uuid::Uuid; -mod file; +mod filehandle; mod fileinfo; mod thumbnail; pub mod utils; -pub use file::File; +pub use filehandle::FileHandle; pub use fileinfo::FileInfo; pub use thumbnail::Thumbnail; @@ -27,6 +25,12 @@ pub enum WriteFileError { #[error("invalid path")] InvalidPath, + #[error("no metadata available")] + NoMetadata, + + #[error("file could not be loaded")] + LoadError(#[from] ReadFileError), + #[error("image conversion failed")] ImageError(#[from] image::ImageError), @@ -58,97 +62,7 @@ pub enum ReadFileError { IOError(#[from] std::io::Error), } -#[derive(Debug, Error)] -pub enum PathError { - #[error("path cannot be derived from input")] - InvalidPath, -} - -pub trait HasContent { - fn content(&self) -> Result, ReadFileError>; -} - -#[derive(Clone, Debug)] -pub struct PathResolver { - base: PathBuf, - id: String, - extension: String, -} - -impl PathResolver { - fn new(base: &Path, id: &str, extension: &str) -> Self { - Self { - base: base.to_owned(), - id: id.to_owned(), - extension: extension.to_owned(), - } - } - - fn file_path(&self) -> PathBuf { - let mut path = self.base.clone(); - path.push(self.id.clone()); - path.set_extension(self.extension.clone()); - path - } - - fn metadata_path(&self) -> PathBuf { - let mut path = self.base.clone(); - path.push(self.id.clone()); - path.set_extension("json"); - path - } - - fn thumbnail_path(&self) -> PathBuf { - let mut path = self.base.clone(); - path.push(self.id.clone()); - path.set_extension(format!("tn.{}", self.extension)); - path - } -} - -impl TryFrom for PathResolver { - type Error = PathError; - fn try_from(s: String) -> Result { - PathResolver::try_from(s.as_str()) - } -} - -impl TryFrom<&str> for PathResolver { - type Error = PathError; - fn try_from(s: &str) -> Result { - let path = Path::new(s); - PathResolver::try_from(Path::new(s)) - } -} - -impl TryFrom for PathResolver { - type Error = PathError; - fn try_from(path: PathBuf) -> Result { - PathResolver::try_from(path.as_path()) - } -} - -impl TryFrom<&Path> for PathResolver { - type Error = PathError; - fn try_from(path: &Path) -> Result { - Ok(Self { - base: path - .parent() - .map(|s| s.to_owned()) - .ok_or(PathError::InvalidPath)?, - id: path - .file_stem() - .and_then(|s| s.to_str().map(|s| s.to_owned())) - .ok_or(PathError::InvalidPath)?, - extension: path - .extension() - .and_then(|s| s.to_str().map(|s| s.to_owned())) - .ok_or(PathError::InvalidPath)?, - }) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)] pub struct FileId(String); impl From for FileId { @@ -204,23 +118,28 @@ impl Store { Self { files_root } } - pub fn list_files(&self) -> Vec> { + pub fn list_files(&self) -> Result, ReadFileError> { unimplemented!() } - pub fn add_file(&mut self, filename: String, content: Vec) -> Result { - let context = Context(self.files_root.clone()); - let mut file = File::new(filename, context)?; + pub fn add_file( + &mut self, + filename: String, + content: Vec, + ) -> Result { + let mut file = FileHandle::new(filename, self.files_root.clone())?; file.set_content(content)?; Ok(file) } + pub fn get_file(&self, id: &FileId) -> Result { + FileHandle::load(id, &self.files_root) + } + pub fn delete_file(&mut self, id: &FileId) -> Result<(), WriteFileError> { - /* - let f = File::open(&id, &self.files_root)?; - f.delete() - */ - unimplemented!() + let handle = FileHandle::load(id, &self.files_root)?; + handle.delete(); + Ok(()) } pub fn get_metadata(&self, id: &FileId) -> Result { @@ -229,32 +148,6 @@ impl Store { path.set_extension("json"); FileInfo::load(path) } - - pub fn get_file(&self, id: &FileId) -> Result<(FileInfo, File), ReadFileError> { - let info = self.get_metadata(id)?; - - let resolver = PathResolver { - base: self.files_root.clone(), - id: (**id).clone(), - extension: info.extension.clone(), - }; - - let f = File::load(resolver)?; - Ok((info, f)) - } - - pub fn get_thumbnail(&self, id: &FileId) -> Result<(FileInfo, Thumbnail), ReadFileError> { - let info = self.get_metadata(id)?; - - let resolver = PathResolver { - base: self.files_root.clone(), - id: (**id).clone(), - extension: info.extension.clone(), - }; - - let f = Thumbnail::load(resolver.thumbnail_path())?; - Ok((info, f)) - } } #[cfg(test)] @@ -262,7 +155,7 @@ mod test { use super::*; use crate::store::utils::FileCleanup; use cool_asserts::assert_matches; - use std::io::Read; + use std::{collections::HashSet, io::Read}; fn with_file(test_fn: F) where @@ -282,28 +175,10 @@ mod test { test_fn(store, file_record.id); } - #[test] - fn paths() { - let resolver = PathResolver::try_from("path/82420255-d3c8-4d90-a582-f94be588c70c.png") - .expect("to have a valid path"); - assert_eq!( - resolver.file_path(), - PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.png") - ); - assert_eq!( - resolver.metadata_path(), - PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.json") - ); - assert_eq!( - resolver.thumbnail_path(), - PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.tn.png") - ); - } - #[test] fn adds_files() { with_file(|store, id| { - let (_, file) = store.get_file(&id).expect("to retrieve the file"); + let file = store.get_file(&id).expect("to retrieve the file"); assert_eq!(file.content().map(|file| file.len()).unwrap(), 23777); @@ -327,6 +202,7 @@ mod test { }); } + /* #[test] fn sets_up_thumbnail_for_file() { with_file(|store, id| { @@ -334,6 +210,7 @@ mod test { assert_eq!(thumbnail.content().map(|file| file.len()).unwrap(), 48869); }); } + */ #[test] fn deletes_associated_files() { @@ -347,5 +224,12 @@ mod test { } #[test] - fn lists_files_in_the_db() {} + fn lists_files_in_the_db() { + with_file(|store, id| { + let resolvers = store.list_files().expect("file listing to succeed"); + let ids = resolvers.into_iter().collect::>(); + + assert_eq!(ids.len(), 1); + }); + } } diff --git a/file-service/src/store/thumbnail.rs b/file-service/src/store/thumbnail.rs index 1c706cc..52c531f 100644 --- a/file-service/src/store/thumbnail.rs +++ b/file-service/src/store/thumbnail.rs @@ -1,4 +1,4 @@ -use super::{HasContent, ReadFileError, WriteFileError}; +use super::{ReadFileError, WriteFileError}; use image::imageops::FilterType; use std::{ fs::remove_file, @@ -73,17 +73,6 @@ impl Thumbnail { */ } -impl HasContent for Thumbnail { - fn content(&self) -> Result, ReadFileError> { - let mut content: Vec = Vec::new(); - - let mut file = std::fs::File::open(&self.path)?; - file.read_to_end(&mut content)?; - - Ok(content) - } -} - #[cfg(test)] mod test { use super::*; -- 2.44.1 From 14f0a74af8fc39be170c20c0f6134d017b677389 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sun, 24 Sep 2023 23:52:29 -0400 Subject: [PATCH 17/36] Lots more refactoring :( --- file-service/src/main.rs | 17 ++------ file-service/src/pages.rs | 2 +- file-service/src/store/filehandle.rs | 62 +++++++++++++++------------- file-service/src/store/fileinfo.rs | 13 +++--- file-service/src/store/mod.rs | 35 +++++++++------- file-service/src/store/utils.rs | 22 +++++----- 6 files changed, 74 insertions(+), 77 deletions(-) diff --git a/file-service/src/main.rs b/file-service/src/main.rs index 549854e..b43dac8 100644 --- a/file-service/src/main.rs +++ b/file-service/src/main.rs @@ -1,11 +1,3 @@ -/* -use iron::headers; -use iron::middleware::Handler; -use iron::modifiers::{Header, Redirect}; -use iron::prelude::*; -use iron::response::BodyReader; -use iron::status; -*/ #[macro_use] extern crate log; @@ -14,9 +6,8 @@ use http::status::StatusCode; // use orizentic::{Permissions, ResourceName, Secret}; use build_html::Html; use bytes::Buf; -use futures_util::{Stream, StreamExt}; +use futures_util::StreamExt; use std::{ - collections::HashMap, io::Read, net::{IpAddr, Ipv4Addr, SocketAddr}, path::PathBuf, @@ -24,9 +15,9 @@ use std::{ }; use warp::{filters::multipart::Part, Filter}; -mod cookies; +// mod cookies; mod html; -mod middleware; +// mod middleware; mod pages; mod store; @@ -430,7 +421,7 @@ pub async fn main() { .with(log), ); */ - let server = warp::serve(root.or(upload).with(log)); + let server = warp::serve(root.or(upload).or(thumbnail).or(file).or(delete).with(log)); server .run(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8002)) .await; diff --git a/file-service/src/pages.rs b/file-service/src/pages.rs index a95e70a..c69b075 100644 --- a/file-service/src/pages.rs +++ b/file-service/src/pages.rs @@ -1,6 +1,6 @@ use crate::{ html::*, - store::{FileHandle, FileId, ReadFileError, Thumbnail}, + store::{FileHandle, FileId, ReadFileError}, }; use build_html::{self, Container, ContainerType, Html, HtmlContainer}; diff --git a/file-service/src/store/filehandle.rs b/file-service/src/store/filehandle.rs index 50f79d5..d81159f 100644 --- a/file-service/src/store/filehandle.rs +++ b/file-service/src/store/filehandle.rs @@ -71,7 +71,6 @@ impl TryFrom for PathResolver { impl TryFrom<&str> for PathResolver { type Error = PathError; fn try_from(s: &str) -> Result { - let path = Path::new(s); PathResolver::try_from(Path::new(s)) } } @@ -160,11 +159,12 @@ impl FileHandle { )?; let byte_count = content_file.write(&content)?; self.info.size = byte_count; + self.info.hash = self.hash_content(&content).as_string(); let mut md_file = std::fs::File::create(self.path.metadata_path())?; md_file.write(&serde_json::to_vec(&self.info)?)?; - self.write_thumbnail(); + self.write_thumbnail()?; Ok(()) } @@ -177,12 +177,8 @@ impl FileHandle { load_content(&self.path.thumbnail_path()?) } - fn hash_content(&self) -> Result { - let mut buf = Vec::new(); - let mut file = std::fs::File::open(self.path.file_path()?)?; - file.read_to_end(&mut buf).map_err(ReadFileError::from)?; - - Ok(HexString::from_bytes(&Sha256::digest(&buf).to_vec())) + fn hash_content(&self, data: &Vec) -> HexString { + HexString::from_bytes(&Sha256::digest(data).to_vec()) } fn write_thumbnail(&self) -> Result<(), WriteFileError> { @@ -202,20 +198,20 @@ impl FileHandle { Ok(()) } - pub fn delete(self) -> Result<(), WriteFileError> { - std::fs::remove_file( - self.path - .thumbnail_path() - .map_err(|_| WriteFileError::NoMetadata)?, - )?; - std::fs::remove_file(self.path.metadata_path())?; - std::fs::remove_file( - self.path - .file_path() - .map_err(|_| WriteFileError::NoMetadata)?, - )?; - - Ok(()) + pub fn delete(self) { + match self.path.thumbnail_path() { + Ok(path) => { + let _ = std::fs::remove_file(path); + } + Err(_) => {} + }; + match self.path.file_path() { + Ok(path) => { + let _ = std::fs::remove_file(path); + } + Err(_) => {} + }; + let _ = std::fs::remove_file(self.path.metadata_path()); } } @@ -229,7 +225,7 @@ fn load_content(path: &Path) -> Result, ReadFileError> { #[cfg(test)] mod test { use super::*; - use crate::store::utils::FileCleanup; + use crate::store::utils::DirCleanup; use cool_asserts::assert_matches; use std::{convert::TryFrom, path::PathBuf}; @@ -237,6 +233,7 @@ mod test { fn paths() { let resolver = PathResolver::try_from("path/82420255-d3c8-4d90-a582-f94be588c70c.png") .expect("to have a valid path"); + assert_matches!( resolver.file_path(), Ok(path) => assert_eq!(path, PathBuf::from( @@ -257,15 +254,21 @@ mod test { #[test] fn it_opens_a_file() { - let _md = FileCleanup(PathBuf::from("fixtures/.metadata/rawr.png.json")); - let _tn = FileCleanup(PathBuf::from("fixtures/.thumbnails/rawr.png")); - + let _cleanup = DirCleanup(PathBuf::from("var/")); FileHandle::new("rawr.png".to_owned(), PathBuf::from("var/")).expect("to succeed"); } #[test] - fn it_can_return_a_thumbnail() { + fn it_deletes_a_file() { + let _cleanup = DirCleanup(PathBuf::from("var/")); let f = FileHandle::new("rawr.png".to_owned(), PathBuf::from("var/")).expect("to succeed"); + f.delete(); + } + + #[test] + fn it_can_return_a_thumbnail() { + let _cleanup = DirCleanup(PathBuf::from("var/")); + let _ = FileHandle::new("rawr.png".to_owned(), PathBuf::from("var/")).expect("to succeed"); /* assert_eq!( f.thumbnail(), @@ -279,13 +282,14 @@ mod test { #[test] fn it_can_return_a_file_stream() { - let f = FileHandle::new("rawr.png".to_owned(), PathBuf::from("var/")).expect("to succeed"); + let _cleanup = DirCleanup(PathBuf::from("var/")); + let _ = FileHandle::new("rawr.png".to_owned(), PathBuf::from("var/")).expect("to succeed"); // f.stream().expect("to succeed"); } #[test] fn it_raises_an_error_when_file_not_found() { - let resolver = PathResolver::try_from("var/rawr.png").expect("a valid path"); + let _cleanup = DirCleanup(PathBuf::from("var/")); match FileHandle::load(&FileId::from("rawr"), &PathBuf::from("var/")) { Err(ReadFileError::FileNotFound(_)) => assert!(true), _ => assert!(false), diff --git a/file-service/src/store/fileinfo.rs b/file-service/src/store/fileinfo.rs index 6f253ff..ba71c40 100644 --- a/file-service/src/store/fileinfo.rs +++ b/file-service/src/store/fileinfo.rs @@ -41,15 +41,11 @@ impl FileInfo { #[cfg(test)] mod test { use super::*; - use crate::store::{filehandle::PathResolver, utils::FileCleanup, FileId}; - use std::convert::TryFrom; + use crate::store::{utils::DirCleanup, FileId}; #[test] fn it_saves_and_loads_metadata() { - let resolver = PathResolver::try_from("var/1617654d-a588-4714-b4fa-e00ed0a8a607.png") - .expect("a valid path"); - let _cleanup = FileCleanup(resolver.metadata_path()); - + let _cleanup = DirCleanup(PathBuf::from("var/")); let created = Utc::now(); let info = FileInfo { @@ -60,9 +56,10 @@ mod test { hash: "abcdefg".to_owned(), extension: "png".to_owned(), }; - info.save(resolver.metadata_path()).unwrap(); + info.save(PathBuf::from(format!("var/{}", *info.id))) + .unwrap(); - let info_ = FileInfo::load(resolver.metadata_path()).unwrap(); + let info_ = FileInfo::load(PathBuf::from(format!("var/{}", *info.id))).unwrap(); assert_eq!(info_.size, 23777); assert_eq!(info_.created, info.created); assert_eq!(info_.file_type, "image/png"); diff --git a/file-service/src/store/mod.rs b/file-service/src/store/mod.rs index 4a9ff6a..227d8bb 100644 --- a/file-service/src/store/mod.rs +++ b/file-service/src/store/mod.rs @@ -1,18 +1,14 @@ use serde::{Deserialize, Serialize}; -use std::{ - ops::Deref, - path::{Path, PathBuf}, -}; +use std::collections::HashSet; +use std::{ops::Deref, path::PathBuf}; use thiserror::Error; mod filehandle; mod fileinfo; -mod thumbnail; pub mod utils; pub use filehandle::FileHandle; pub use fileinfo::FileInfo; -pub use thumbnail::Thumbnail; #[derive(Debug, Error)] pub enum WriteFileError { @@ -118,8 +114,20 @@ impl Store { Self { files_root } } - pub fn list_files(&self) -> Result, ReadFileError> { - unimplemented!() + pub fn list_files(&self) -> Result, ReadFileError> { + let paths = std::fs::read_dir(&self.files_root)?; + Ok(paths + .into_iter() + .map(|path| { + FileId::from( + path.unwrap() + .path() + .file_stem() + .and_then(|s| s.to_str()) + .unwrap(), + ) + }) + .collect::>()) } pub fn add_file( @@ -152,8 +160,7 @@ impl Store { #[cfg(test)] mod test { - use super::*; - use crate::store::utils::FileCleanup; + use super::{utils::DirCleanup, *}; use cool_asserts::assert_matches; use std::{collections::HashSet, io::Read}; @@ -161,6 +168,8 @@ mod test { where F: FnOnce(Store, FileId), { + let _cleanup = DirCleanup(PathBuf::from("var/")); + let mut buf = Vec::new(); let mut file = std::fs::File::open("fixtures/rawr.png").unwrap(); file.read_to_end(&mut buf).unwrap(); @@ -168,10 +177,6 @@ mod test { let mut store = Store::new(PathBuf::from("var/")); let file_record = store.add_file("rawr.png".to_owned(), buf).unwrap(); - let _file = FileCleanup(PathBuf::from(format!("var/{}.png", *file_record.id))); - let _md = FileCleanup(PathBuf::from(format!("var/{}.json", *file_record.id))); - let _tn = FileCleanup(PathBuf::from(format!("var/{}.tn.png", *file_record.id))); - test_fn(store, file_record.id); } @@ -191,6 +196,7 @@ mod test { #[test] fn sets_up_metadata_for_file() { with_file(|store, id| { + assert!(PathBuf::from(format!("var/{}.png", *id)).exists()); let info = store.get_metadata(&id).expect("to retrieve the metadata"); assert_matches!(info, FileInfo { size, file_type, hash, extension, .. } => { @@ -229,6 +235,7 @@ mod test { let resolvers = store.list_files().expect("file listing to succeed"); let ids = resolvers.into_iter().collect::>(); + println!("ids: {:?}", ids); assert_eq!(ids.len(), 1); }); } diff --git a/file-service/src/store/utils.rs b/file-service/src/store/utils.rs index 9427daf..92b7099 100644 --- a/file-service/src/store/utils.rs +++ b/file-service/src/store/utils.rs @@ -1,17 +1,15 @@ -use std::path::{Path, PathBuf}; +use std::{ffi::OsStr, path::PathBuf}; -pub struct FileCleanup(pub PathBuf); +pub struct DirCleanup(pub PathBuf); -impl Drop for FileCleanup { +impl Drop for DirCleanup { fn drop(&mut self) { - let _ = std::fs::remove_file(&self.0); + let files = std::fs::read_dir(&self.0).unwrap(); + for file in files { + let filename = file.unwrap().path(); + if filename.file_name() != Some(&OsStr::new(".placeholder")) { + let _ = std::fs::remove_file(filename); + } + } } } - -pub fn append_extension(path: &Path, extra_ext: &str) -> PathBuf { - let ext_ = match path.extension() { - None => String::from(extra_ext), - Some(ext) => [ext.to_string_lossy(), std::borrow::Cow::from(extra_ext)].join("."), - }; - path.with_extension(ext_) -} -- 2.44.1 From 561ec70a65da18788fe60a7a762e131deeec5c54 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sun, 24 Sep 2023 23:52:44 -0400 Subject: [PATCH 18/36] Remove old test files --- file-service/fixtures/.metadata/rawr.png.json | 1 - file-service/fixtures/.thumbnails/rawr.png | Bin 48869 -> 0 bytes 2 files changed, 1 deletion(-) delete mode 100644 file-service/fixtures/.metadata/rawr.png.json delete mode 100644 file-service/fixtures/.thumbnails/rawr.png diff --git a/file-service/fixtures/.metadata/rawr.png.json b/file-service/fixtures/.metadata/rawr.png.json deleted file mode 100644 index 0c6259a..0000000 --- a/file-service/fixtures/.metadata/rawr.png.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"rawr.png","size":23777,"created":"2020-06-04T16:04:10.085680927Z","file_type":"image/png","hash":"b6cd35e113b95d62f53d9cbd27ccefef47d3e324aef01a2db6c0c6d3a43c89ee"} \ No newline at end of file diff --git a/file-service/fixtures/.thumbnails/rawr.png b/file-service/fixtures/.thumbnails/rawr.png deleted file mode 100644 index 94ecf15f050cb1d1226a9de11b72a46ef9910c4b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48869 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

eRTJ3zZ#y(l~ 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 Q4$Og-Y-Sm>SXQTt(g%~sKeFX z5KIb~>?)4``IVw&Mi50=9n%+{wAKRr&ni17(&JV9sr5KLZfEp z1gtVXO~?P&dJumK8U8>xO*CPk0ZjTAfuag_K) zU6^gT^w|+&`A7I~fKtNV{PHLKTJHSxY%?S3hPVP}2V zx4<(pevjkkj40jFkZB0K3~RAOry=QJu(}f*7V8HavA{M{-=ZEU&^YZOS+q9?c1oCM zM&!Uk&|U@PcFzj8u~S;iLpso3&M$L&RPT*X-dY^+7tbkS9I-pYO=jU3 zg9IdN3X178B-Cn*J_=?GO%<-oke^&suz|fpOa621khg}9SeYeF-qgb?Svm4-=`myx z!22oo@*pVP?e;tvCj>P@zw*IaD9m#{7bNDSS57%2ggh!^^zac^fVNrn{pTx(vuxbi zKI+PXp;cGN6%x76KYgJP$319LeFYSl0^d)u`NZ+vY|l3&ZPJ}MW%j%*FtG`5;Q{CE z5#k@o(Ny;=rNZKiK*u5Q#EHwe7PWXboo&Mj#S+chzz`x&{_~s0WF||riXeTIwty!O zb~7yc+JtbM+}HW+{=$hgD_vvWJ#)Yng|yl^UM^e}Trdx|b8D+;RjhO5;T~j`qaI=8 zA&LpBMY(fq&nbxzymvDX&e5qO^LN}s`^KZSw&Ym^E2&2m>i16v<+3mzF(vU`D)XGh z+#Zn`b{!jzpEK#T=8@+^>!za*sCoAuun)u3&lO_9@?px@Q}=^Sa`xm{=$Lh1Jj`C= zI-({unfDbq{%~qDW8;a&$_GFA_VQmVLyA8+279_46;j6xrL)vk`hAtNDu+z*f4Owt^!^~IGlx#JmNPEML=SNr@ic_95gv~G$pr^qel`=dJ6L3{^XdQx zdnxt$hI-YPu;RUNB4GZHW}w_AktPp=<8mgZ9&{mWu6d%m><~*zHIJ+(>H(y+x=*p| z5dSDwI;Ms~cZB^Sh&(q>B%5`XB#@DN%Dnd_?4dG{tqE5Z)B$`Q(9Z=RQ>YL8x`^E) z4jPS0{T(nz&zj-$fFBLKtbNK3(8AR@i9%1`X<0-LWH*pfvdAiIs|KcGI#)&Bf@^Lu z9_vI|s`BnJQK=`9!5zCb@C#bz1fTmU?)LZ&2WjufM^8w@iu59q=4m%fc705zr5bc! zGvq8f>Y+*`mnx+p%l$cxq{JnE6a)Q-xmj8RYLL#VwK_ ziKyqk;9rDpRNv9I3DCLV6KqB*P8SjVVnfagdrHk_jW2x|xATfp&g##LvvR*#vD)p$ zGO8mpXvNUzYuG{5T8kt#Ggks?>!-MXm(W^~=Qly67hhiX&I*^BCZHtkqSK`XUe~9q z*71BCspXCQYfU}7L?rwGA0$D)FwCQ$#PUx=&@S4_t8A3d)Z>NnO7VDS>cLFwybSUD z>Q`58&v3r{fh7j-_KIaV@?*5O9*$dR?=r0V+O?>=ETO#J`rBoO8;jr7vHDCo_0=Yt zpEawkP9c^nv+TmeH_{)Q5KgK8aj$23;8jf4gYIpMRz{IlC0W6WCc~*?FG=z{Y6$8TCR%W8k?(QnQt~`O}niTHnc*g zrzKlp+o_9s;8+KUAuz=OLq@EYLQ7APH6|PangPYq;Hsa9wH;YD5GYBQ(P?Q>!cyqg zNFxrM^J=lhqT4&*6MyG9Fd-pZusPb@GST=FbC>TlEaF&uP$p2h+k0Gs%@mxRh`!xj zX~Bo#i(qH**$UNO_zFRSU+S&kz0mjgnI1xyi|7>?Sx1si&rbg${(0NW+6;2_)sham zlX14ue>tN~KH_;i%UKur{yNBeCSE}I9AlOf2Lp{5WeP2`bLJ}1vvs+F8YDLx_-vFByQEr3v%}#|aZPZJcct!7R~0y^kov|k8F1eWOuT+}J9Y0x;d-i@ z*{{c^0RU;hvUh1RFdwlCEn?I#?~>OK`Xef{Y7XRUh{nZ*-rle?ep|D1>SvJnu@(%x zXJysC8}u`8AeiPOl`#aK-A}vhM#T)?z-@gah)RVoEhQz+8roR`7!3eAs?0Vu_c(M_ zS-au$BPSBDO+Zx1m$;0`1GOA1hrFK!j^{Z53oonP1dVY-!q{lrEgZIr7CghhP~_e8 z9{C*!whVqswLXYi8AGVCBB8y~Ow;`r@LShz-QowiT3r}G)_>?~K-C5#fv(bi#tmo$ z)%heaAFrED-!itS4(G|yNq#_%ObB_&FaJ@Ee1G_@ou7yDUPKX269H)X4mAvcvdTY- z;`MZi;Z8#CI8=&5MCQNjNNA0s)(wXbkXir{S~cVkU@U7403>BM(1=QAV+dW2a?4`b zk&r$*7hRY3X#rq{=k@iFe%rM%bPxJT%LZB)cafCljVc7|sYz$kG z7<146UKI_V`lL&Ldm=9o+eSC}eM*9!tw&@fb!K4hbc8=b;zdqr5X$lv7b_g{@(5!EtB86YR2Wsn6__w`HPe1e`i*#@mp@jV zBbD>P_E4T9owz&Vz6~7e<83!?>Y)!8K7y?wF1h_Vv;%VMU9#NE`Z2E04R1ci2eT2B z>yWeght|npUSJKsumyrT-v0H0+xAv|BOsO1B*$A~T?VzKzpIuy#{x7r3$&o4^FDM4 zon5}9$8`# zPZGlbf!l#BYRHM2Mm8cP3&IJZv{pzKU6be71|nAhSO5s^xW^7lxN;d%baoZzNu{|G zB~1UM*qdneD*4kMUMMsZNl)T?1pmb~a^#o4Uxwnk+ zkIw&Tr!CSqsDR-50UyPZ0j|8x6iYPot>+J<2a+0!i=+}_8j@ct|Cj@p2&HmALkN1nWwS#+O%w1gT*#NrFc78^eJf(4^bAihb6SqOFe zIcz{5oWu+o#1vB2T&CN46|cG#g;ZQ}RXbpqhZ2oREDco@ZiRFLf05Mj{A@!K zC&3xqeiQcsp>G=Pu{5^Y2k^2e?6yGFu5`HL)+H4kA@yDo9|LGO79enA7KCizJp-u} zBn`v1l|oCVq{}1Ml;PHQi)X(mz$jlQ8aSR|P#H_I15F1;J87B`_OoEBT5rRG1rzi? zPK`GDc@%C)aSt)au%Y=XbD)bO{`VEsz9p~*=XV3#=>=#Um@5dU)${jMpi)rHu>h$@ zIw*tU(s~4jmCC@C2N}!o92x2J6Nsm-;<#cYKc}ud$Ern(@`S+4Fn@kH2ozvo;cJ{Q z41R(8;ke=NACUJ{h=HvGM`Tq3EGe%KLCor8Ho!$HEGCM2TQmAAW{n?$L5959c|;e; z+@x-3Q(RwhI-gYUfybPe5NbB(;#8d#ecF+i;jV}jX?DW-q@=oFIj36m6o&!JUCcAN z&9?Y!?`Agg(kO-bq)lL$0aFwWO=_KZd>?^SKEacQHKB&6@XX~F@$*2^$j#&X4>~lR zrxv%_jJ#X~<^uVW2DcE|!dQibk1U;P?EW$ZSl*X3`Zn6n7i=bTOx^S7!>=0TvE;~q zGFOh6rizTO{Au}D>Hgk1 z>kXK+GqcAnJX~S+v*JYZ6uQgDqk1pQ@7j9aL-VORKy}TUmHB(~`tLtGynV+zC=hR?Ke+2;WK~WUpj0&97l7ywkc* zW!E)ph1~eiWo+I595{|*r5M9zH3%Dv90NLmhClW-{#jYmK_K@joB@85$-o0DQC(; z21|fjv=lQyi)pn!JU?qT#3(M6!52|_z(}ZCT9Yrvd1_Fh{PI(MN3ML#N^A^@=gCV& zdP_2TP}jlucyGP7)E~Jq_`+`KT>)+P-t48F8m1hwI^Z{Jv4}BBEmY_0j^=?2cN8t> z|G7^6CU@BDX;Ojq#pNfNJa=d6Z=aF6&?iLp=0%)HAqdR zWZC^}z(soQ3v>zK*E(Z#$lD20kE%zi3_(p<5UIOFrqP3s+EiQl)ivu2+E+wek}eu$ zxzx(0CLp=U5PE3<&oCcrfXrt{`#O=f6z1Ot4G`5;l)f##Ku#ZeNg4q?(2?A)rtzmh zOJ-y)W5t%9FXrT>!Iq}d3P-A9Q~Ry|b@6I(hT-_5LB2eoc`39yFgpB@7G7e?@?AO$ zt~7O%4kPlyYC#Ey<9 zbk01MJIi7zS^fwk6qqm03Z2c`XtMaF>D1R}nJsl|3&>%;#C0}DU zFEh*%-9WRJp|~Y0$HQ%s`;w{Yvqn3;K2^G=kY`nlx4ON<-s<3^_6%(Yfxk5^#1OY5 zGBsLAJv(i@8aulBR(dOIEIjy+Z8JUxK9TxCCEX%6d|vSL1%>_r;oN~03Y z*7BEqmA1~P)NIph>lXW`yqqA)#-TM5w~Ril5VUMU-;@>@PV^X3*qSvs^rk!hjyGR- zUmKJMD1}f{TY8nHFLiWdoL^i7jw>zn&B4w_O5QZ@p)FS0DLXI1C$ZFf(J`;?(EzLd zU7L(Qaw(fT1NM6Zxyly^ZVOI?F47y(BMDS<1W>kzMFD_T&=)AjY-`IFbvM_T!mDCK6!P=(AYL4TD1#0tzB&(4ocisV|I||Pk=d}lNUc)AuJ&3s(e-&r&#d7e zl%Q&YN?%fu5VR%7fQ~d5PJoi)9LyCQhGXpCuYtrNG@2AuJpxO`5D^$Ye#nZ6v@)~^B{>N0L<*9>;syt3g-H=P%5%OM86EZ`#&`yEDhKi$C^}NNB+DZ zmlIzunyB*5pqNUQLQ*5Y9F`ia_H4pjUywqCU2nrPyox)TI(sAl3Wz=cc)YI|&2n`H zt!CoivaXw|J_ZEN|H;95FiO7oqe^o<84D28U8(2HZCW33AtCbCe8AOQqyF41+GWPr zuo%aYYv+?5X0<;Y$cnyXIkJ#JKEy*!rgKw&;%a>3%TCM5#%A@o)B34=QonRbIZ10v z_VS5J7i!Dv?!O$BOWk3MHlW7POKdioaB7jU&U`THzgZNn#M*@UoPUo*(zCX|vogtS zhfJG+_Y+xjo;+n*%z%Ay*6*b#JCPv4t2S|ZboiVoSsfB4dX?hz=T>og%eu$}HgIe%7zdY|>?$4> zq4IV5lEx^n*|)J22NbtubiUI`)@r$zeqAaQY|8O4h=!os5`i54s`PFCj=u^lj&p zuY?fjjZ8&rLv7HPib}_x4wDvN**Q;L1J$J%!N2zgLL0*OIUAx3XQ4n013Aq(=n9OU zB2A0x+gD;v2__H&1;_3w@bY3|^G$=B-;~94J18A(1$73xjlm=zW#qEM`Q)6g2XP)9HQ)~d>_jV(-ttRU(VLERnJ>~G3b+eXZS ziFoLE&4oOAWw617jx=MW4E#TrQ|Bwusr*spl@8mTm;WlVZ@_xj?Xzgx;`@|8yTP zkW6xgubf}psu<4Bi0tB{UUR55tX5~**KCgfP!>S)JEmLPSNxruf*xPC`TWFUJLI~& zns^nEt(?CtfO;=tQ*c^RZmMdTCAldllJ!2C+c0I<3(DObOS$aL9fQQf!fH)B`eE6P zMd$$H!Kya~(C%B`2_>YtgckDze?7yDDPN_BbXg=}tA&`1yh$_`#>_xZCxT!oFkrXF z5jxKKx^QS2bL&Cj^#c&F3sNSwGeQt$3vilU1SPmXY#AWl19lH zGXV5O3gLe*~mN$eQ(-N{lfZX4>aM1fpp_&+Gf{oovQq5c6XM62w^)D;o! zP0o~Al(peKgI9h!iGo>R__UUfMUkG-b+6(1zr_ZZinGQfU1uIG>IUxXdvs{i2`ZVy zK1`Jn8YbmisHIUZq;XQa=GZvlDdhP*zOAoUt0geTCIKJPlWrMCi_*U}RE0p&R^>^C za_#M0noye!yg*LgBvRTNZESg8Fpcvf3yt*mNQc>URpU>KK@@}k3&R%G$q+_82N7=< z(J}fS#+%*?nx)MdqXr|HW}NzZ=WAo+j6~VjWFktE=beutGRHb4v5|N+{q9E6v{eP$ zc~-hE0-g@F0-Zl!FWjGW;KkN!wL7IRq8uFm_J7vIBX4i{!$s;qcg&(SEv>MTMS1RL z>o2k*E*F2ez^lrLvJSx%a&%b^Xj$~Rj3wQV6GWD1#Q(XPy4 z%Mc;65G5pJ3>C%Rh9py_%$X{7$Sj#jlv!mSA`(9`lQGWP{W|A5|9#)N&iBvfKQ6ZG zeV^x9&sz6d_kBaj=tK0l3xePz!3Px|Fn5-wDubRAl}W+D0kWNWga3R1MCu?>EBC7z z$jn`b@IghK7X(mw_n!DSdtw!E6k`iigDNANCO|43IY1OHT9DYTxXXkep`T7shS>)8 z7u|Icg`8j)E_CPE_+vgiww6|t>-7i#VOJ{YGZQGC^C7I@$oZKr|HB;&ku;ZUV`r5=3@^5hxaYn zF25zMxf}zTZq}^(gDT%`Q5qS3aiarAVDhR~&shmI+oguFeeuYW;B{Zq*S=!#iWIHO ziK$JrETH<(pxVn_Xw7^H|2WQiA?zRQ!IrO@ct2`?r}tGNFDBs?Aj#)#Yah(a&PX z($(k(D?K&~VXSc@QUPR*Wjtq($}=v$W5_tNmLN3VV+$~)zj*^7n+eh|3pxhsOIuh_ z0~Lp~4RE8DM!>7+rifVy{&dH+tOqKwAoGECB!P;~@+*v9dL6)+4k<>8vDytq#H`rJ z!ShHpoXGh@wVlE}UZvI{YKRcXpp|&X+oJ^OC3fLyL4)w;J4zx_=E6>NWx}`+t308m zJ^nW7vq`WsGnk4X0h;b?1soR&1w|+oim3#NLb}!NZa{K){}y5n7d%9X5bhJ2cG}f) zyJFVREq2=z>b*0qi|6T+!pEuE8r1hWG*BS{@=@rF`B@Y95`&jT{w7WWj@;l}@K`c& zn#|Z`gs9V_nA@$#Z*gSlxqzFv+kufRe-{+w<@zFY30YF07_%tMJemEhs&l|(bmq-8 zROrGRf>|tHDXi|G1XjU*YsIYSb{pFbahe;FGpp(+u=IN5Rf(%FAU6>heQN9HcZ7{@ zhHesHg;H9}N$*vD7V(yPW4qj(!fZNEz3kIrK>&bi#95yR7DY#I`M*%SYii6dYbSTb ze?g@=#vPOOmC$XJn^G~?Qny!@xm>#XbXYoW>8X9g@S_Mq_rUcjP=+cOQLD#pn2E7& z;YqLUFEd$t3iNyOD_$4z<^Odl`l0*sE}VJ|fnQw@;=bSwGi^s$)4k`e6~bK1Cgo-0 zcbkSQg>=0(iygMzC`-|=l^!$cuxC8hyz0SKq~==!@UGov_1dUmpcoZ*{VLP6#No(H zh%$xkL_KIY=8%bEuBJh^rVQ*uvmn)(*V!`LzPP~Fwh73P2aze@j9+CY`=AF)IB=tg zt{g_sv!JWUBB2W*Wth=fNFNfL4S~J<2QPn7Ps8X}2hm}3LJGSb7GNU;pw;~8!H{9- zMSgKH*Q=Zid8Ec3zb6m?@y`FYh3wNiuEwi>9xqxLg8w?qVJ&+v!DMd8i; zAL~8xAGW%e^xV?j!^qkCVo>-hT`fjyTcB(>ye~6r*f{+Qy|_kiX29@0o{PQUz?i5t zsA1`G%>bkf?uhcAc!+e|b}{9!m_vXs!jlC3zceZ>qW9hxm_G$RGilzKYN%cmRR*j z>MgFslJ3?;pE~`OE_&u1i2pIl$q) z-YfA4k(NEBVEJaR&2M~pE#)z%#zN52m^9VT-pX3}kC^H^OM6EknNg_5f$zxn4-d`@ z^fKSCG_ZxPijuv9wb#G&Q(zTKKTT-DLV*uwct>zAyfO$#16hyP4nWuu=4NcGdHP|O zR1wWNsTw*1{y+QE*TJhwWF2~bire1wp*b!J<(!&aq9qkDoquK<08RJgu~0s)e!tD3 zlXGiHLqnp`=%T_}CQYl)k+W-Frmoa6A@(c}g-V^xGEAI1q3wHN4DH0`mo8^?+nr}2 zajLyvO)4P5=u8z7{8hiy{y836+c_@6?VH!ngTj6d#9>v+NT!p#lvwTJ(w=!m<7P#@ zDGjxBcXK^uuPr`VU5m$8H-(!UPn|dV)%u0*hKRmUvQB6)tqi~UQ$Lf*BaI{s!dkzC ze^5xpFj+7^<2D*oA3b0AesiY>HO<{0HKsBuViKTo80~(qV$v6~uJ8|Hb$xQ_p=?Et zmaF!dtBl<|dQW>yDi-@XuY5vJKHOxNl$ut8M3Yb(G#-3A>x^+HMmU>UDb2g44JXO0 z72298FVt&expH!%_^A8Vw)#?DP^o}Cw56Vt`Hk;=6%|^%d+lyBIGx2-ej?^O4!^1R z{Bc_n2m8#?>zVt)A4%*i_4hf?y2HHzijhYjm9UWS6T8HBKFvd;7hf#~hJkkqnAaM0 zG)MwBNQm7U9|lFeePX#j+97m6n=JE?j0;*jAu49!e-ugSs%O0bFK@7nhx<={AZ&X= z;03;M2T$a-)KYQ^9S|EpE_QJT6vLdST`yi z+hGbP^ehg1euT6(`4tYkbCsVvlFHaCCS{a(f-z0;MssDv+vc`*cJE2OuGqcfK(D&D z2%x}<{dpb*h6n*O& zu~emR__hYPmHb6rE=r-z>%2LmB2A+ALkgw&VLtwark^D6I&VTCwbiM@^F{Ul416e`DkT`!NM4VSjCe3jFBGc;WK?)ifXkaFINy>-uof5qXX16^vkm7n7FRIaFN9zx1 zTAdZ2y~c}mawAuZ54|a7l^8-nO;HRQS)scBKw=Ll(#XgwdzA(ZXKy+ zhQUp242K%M#>{FgK%kd<`qO@ndXZ7P0Qw{|B%7%yVNtw>Q=?CexUk7fx%d2F|E4al zx3S(#NAMZXOF|hE$Vnoi)13R!QwJ`~Vh|pq`k^DkpfXhZ)Zl0nQXRyFetdgm^4`TZkgRWiXPX=mE?@qa&}IJQA&{zF}NM;$Fw^)hMr@vMlJOYMk$QPQfGyF(O!d zNxrQ6nKa@HN_rS!KMGB&{tg;G1$DDGY+yaalpdXTFUKjv3PRCy$^D&>Ql3|Xol$mR zNh^>lmv*d-_2w7w-H0v`Xw0@B`N0x{_-uaA;PL*&=ag~MRGa!n&=g8DLq}k*?(DYs zAU0VL#|FOHrPa}@QIu>flrv9`N3K{ml*;XVcep1mh0K50%+z7BH^m`mx3I`4|#nS2yGKNi6k zpgo9(ED8KXZGgx@W+k0a12L!|LiyR51b&s7g61_x?>^$Df$E+KkC-pH@X^`6I|RuR zwDZ`%{7IZEzyAh4h4YcQ)Jic{xw|KUvYSU7mHC%C)8liO_c0fP6kwl#5`9Y~wps=| z`N5nKf6E5;_Knt_%3CWn=2!nnDueSS85h34noSd(R0I@a7}=rtaQ06^FYbL%ide7( z%H5QbRGE3(Y{N3GwA!aMxalt%g^pkQ^(YD~B6h$FT&x`q8$!-qex*$8@R#h2+jEC< z-l|fQe4Nhi3_qQx9rDWEr0I~`RSmZV^Cw!HNKT+e9#L`!k69?GIc~>e$UP@o&MpA;5 zzL-cpc&3yD)l#7sGL~_7H5y5(uvs2ftmQ z*)SXXnwBzkjsRXX{Hvj|b&ayplQpWNJr7rd{^4$O?;<-&Mzu0;wmazN| z=v)J>sQKJ&!ty}QV6$T9m9LMO@`hlaOnVH`!N{Lu^xWc@JiH65;GFJzHG#iUaT^Ww z*4zR+k94A~S8oT%JH0usGP6}e(@U+o{p116=mD;B9t;Md$RcWIR}d;EAWJ|hTD7+;uG2?PN9FMVuHPXWy>Cg?~48KP8*UOF8N_ zQE;p^@4MUQndNUmGHzU~LHQ{b7Bqq#{Bb>v@SXglzR^U4@hCmGSX-{|VaQ_?$!Xxj+_sj#*;0@ns*_;dQaq-e2r4L4 z4dt1~PmRQGgggv8UX#FDmh@L?gNAsc@co}3PYf9dc5HCc?dN3@cU-<|*8`oiKN>+Y z>~O@RPm#x5XK&&3;QGg7qQT#m;t?(9rC>L!iQO3gC6&e?*(hLfTCnAk*!0JCvILe< zEM26Dj7Bf|{*DCpu13nb3dptOdjn~gz#4n)bNG|()6Wbl)lw9KWS9 zUcnUnYdWjs1nHR{(}Up{jj%q5(trKmWJC&%sn|Hr?)Jaysk1QZx3OxeVNbyvawz_5 zF$=Os)EAncTHEJS`;MEl3@3!O`5fs;Ot3lA!gbPwSnm95&GC)!*5F7)dFIj;DwW;a z`esl3SxX&c!74m_0nI1NutHh$W{RfCkYRSG7npnz)7n6bY_TI7f`bRZ0Ty70g zyeD%)2hdBM*4h$ql%Mpqt_gG{(QW@>sDWy4pT>bKNN~>n*oP=Ijj|Tsk#Z^mZoYB6 zYSnc?oqo`_>ANcz61;5=^THJA>o1%?4ioH5Hb47aHHzhKY=9Jx)+eEixkOzRSeW~ok zVBe95`pl&f&m&%V%t#Dijei6IK;6BQqlkP14C1!t=H;N znq0WC#d^??dlHroMyV#r6(A2SD)v)Ab{yrxP}gWSAcmB_fQpL5^vwgUGGWv3t_53? z!MMI0(45IWLyxcD=snoe=ko;%;RTfOqVd}PcV7UPO0Ed*#++D_zw>}Rror~?femyx z`syGddSf6Ca~U3=WIl&CUzIy9!sY#Y#^$jiau4~U$Ng_dPG^~u+gr8s`pwS z#~u1>a^I3q>PT|D6cBtE>;im677ufV8JH`S)M}*59OOlKxg5+6QuPNr7V2 zI=;?FFa}#|v8YaO@2E%+wAnw${xw9BC@_BuwCwuAuO+T2W}>ak9$#tTA4l!q zWxj^ch-~glmky?M->GHR%*Y=J1f(Rk4j5hq!Z3wrd*cmEE=oCD@iN|8f6;YjP(uK1 zb=6H)NGego_-T=X!7r-0>>{|+ncp1TzrVRE(rVV?UgP2`dqy_ty%?XFkS3FIcmkg7 zX$Xsk)Yqe>ZWy=3kZ3Eeun?~EcB~XxJYDJ-(2z#&kssmJrnjJ0#;{4grMnyX_{>bd zB{R;$ib7YZJISz9cFZ?-XE(`6L9eE7!^BJB)wtQQvEBsZa0ipx4C|VmuS?@54ow`F zkDo;1xg4=bg)(Ba#f{J-4-pE_%e{ESB@iJjs~~*HVUnY>1q2e3(L*G9-yR_v$T1XG zkEK2->Mb=Y;~gzY6e5JiW>jY#Lic|T)7_s<4b6PC*(B1kaj;k=eSY&!_BCTnTZxZR zGvRIyn}~YlOiA4NR2)sVGWO${@pn4Qd5%d2sZ+Gk+5)>Sx01IHbi)_dN%RcsvaEl` zDzQDq%hjOzpTk_?R^dt*u+1p1O;>zi{%Y)Tnzi2Xlk9Dm6tu zKPsau0U};wkWQ*H9Ukf)E0H zC_MLN;&aoJ=*?&$y^oI~UKUD`2*vuvUOYBIZL{n?!O7SAR=)K)x9$t|udRE(tUnTI z`Bl!wc^PwOzf?i5c&@khWGY&zSaRsDnMhLaerJd0Y!jaIrSmu4#%c2_jbUl6o|EIK zOMw;m3n_}oTu`C?4Q;`vRQSStAw=DkgtjFmT@Mqiwh|PozbLqPbG&W7wMj%>ZlnH- z37M;>s93d!*3+_^l^ic=-bj@n$lX?AY*ym%Hd$s`wvxF$)#c<=g(OCVg|&ujU6!Cj z{AogtADu0kO}j(Oh{{mx)>ZS5fEDlc;)dsEmgMTW@kwFfga{hzo0cAmVuQ6O%%4Zl zJ`#@DjP|Vi!wNe~jAT+ME)u-~*h}kI|Lav8MK-+sL=HQlv8ap!DtOJc@S4Bw zt}t|uO6aJp?R?GO=*s`)4?ujLQ!&wk4Cdl@bglU7CwrH^0ANmO*lMn zJ(rp2^&8>Z5`H^-xiPoIlPx9xj;3{)Y-%oxS%1wd>$9;x)EK9vakvHQr)G<9=~LZh z7F88~Kg_9nVcplPTPBq~aF^2%ll-i$(`+*;Wla@lMqqJAJe%SMMn5>D(EbkO+3E_E zsCC)flxI0|i_vCn)Dn))rThmVuLIz@GLo!wTSWqCLLsgg_5j`MR5l?0jK=H<{({dm|p3Hv*3oJMxrnzusD z?bN1tUjG`~`PpwN(vsBKDQFxn)|zgNNoLzkH^Ai1@0wq6SS+ml)oILtHrJW-bIZb`-YCeKfZ7;lJUcC=E`D z&l7}Dc@&|-f`G&%qMkiof)?*%lQ0b$&68WersK(^^58a4iWY8Gww6d zc9kjD*eCbioMX9sO!1gw+2lX+DpmHc8qEVQq;(9wBsZ?1ib(6P$kgj*Pi^r$=7zR9VN2{!h*+H9ZuwX8)DkT28P=L;*!XuwTF6_2rOO--e%2P zed)O01&8s5U6P6j?_9Kcdo%^CW}H>(c$U?SG(AEh7JBOeqTb6J5zk8pH;GWAOubrT zmn$EFGCel1g@h9>96x!12d)&o0C&HzfM??x2)an9V1_0p}IM)~&_ef4hH@Uo7+wm!CVH>Rzm*GHYsiEY=}%(&7c zCA)Q3M7_4+w=Uz<=jy_7J+rb|dB-xA?gy)^n*y%!H#YNFbcWt8Jujb?<5Zci=3+5r z`lPN@Z)H@XBvkl{h3)3SW?JY>^s~kzx9jSBo4}@KOuAu*}@~J3pz{V^?fk!t?Ir=iANh#sJ)cha+x{OzHhIF9Lhso6*Zgb9x1@b%k^I>i=Syab$ zUyOaUH+sM1ow2Zp$W25kk{6ky8w|5I)MPY2kwpl8cCkGxGP!xK=gwV(!%ZWd)bu23 zLio!LPwI==fEES^>C?vXddQm+RZ`}a@sm^TEJ>Du4@ zCSSMc{f?MO%;}tHFlAa)%9zO#ATRRKcU?tVO56@e6qqGt@qJ-FvE--XAMVo5RuWoC z@~`%$oWmE3jpDG~YC1Fsf Date: Mon, 25 Sep 2023 00:05:45 -0400 Subject: [PATCH 19/36] Refactor PathResolver so it cannot fail --- file-service/src/store/filehandle.rs | 87 +++++++++++----------------- 1 file changed, 34 insertions(+), 53 deletions(-) diff --git a/file-service/src/store/filehandle.rs b/file-service/src/store/filehandle.rs index d81159f..4259954 100644 --- a/file-service/src/store/filehandle.rs +++ b/file-service/src/store/filehandle.rs @@ -21,27 +21,34 @@ pub enum PathError { pub struct PathResolver { base: PathBuf, id: FileId, + extension: String, } impl PathResolver { - pub fn new(base: &Path, id: FileId) -> Self { + pub fn new(base: &Path, id: FileId, extension: String) -> Self { Self { base: base.to_owned(), id, + extension, } } + pub fn metadata_path_by_id(base: &Path, id: FileId) -> PathBuf { + let mut path = base.to_path_buf(); + path.push(PathBuf::from(id.clone())); + path.set_extension("json"); + path + } + pub fn id(&self) -> FileId { self.id.clone() } - pub fn file_path(&self) -> Result { - let info = FileInfo::load(self.metadata_path())?; - + pub fn file_path(&self) -> PathBuf { let mut path = self.base.clone(); path.push(PathBuf::from(self.id.clone())); - path.set_extension(info.extension); - Ok(path) + path.set_extension(self.extension.clone()); + path } pub fn metadata_path(&self) -> PathBuf { @@ -51,13 +58,11 @@ impl PathResolver { path } - pub fn thumbnail_path(&self) -> Result { - let info = FileInfo::load(self.metadata_path())?; - + pub fn thumbnail_path(&self) -> PathBuf { let mut path = self.base.clone(); path.push(PathBuf::from(self.id.clone())); - path.set_extension(format!("tn.{}", info.extension)); - Ok(path) + path.set_extension(format!("tn.{}", self.extension)); + path } } @@ -94,6 +99,10 @@ impl TryFrom<&Path> for PathResolver { .file_stem() .and_then(|s| s.to_str().map(|s| FileId::from(s))) .ok_or(PathError::InvalidPath)?, + extension: path + .extension() + .and_then(|s| s.to_str().map(|s| s.to_owned())) + .ok_or(PathError::InvalidPath)?, }) } } @@ -119,6 +128,7 @@ impl FileHandle { let path = PathResolver { base: root.clone(), id: id.clone(), + extension: extension.clone(), }; let file_type = mime_guess::from_ext(&extension) @@ -142,8 +152,8 @@ impl FileHandle { } pub fn load(id: &FileId, root: &Path) -> Result { - let resolver = PathResolver::new(root, id.clone()); - let info = FileInfo::load(resolver.metadata_path())?; + let info = FileInfo::load(PathResolver::metadata_path_by_id(root, id.clone()))?; + let resolver = PathResolver::new(root, id.clone(), info.extension.clone()); Ok(Self { id: info.id.clone(), path: resolver, @@ -152,11 +162,7 @@ impl FileHandle { } pub fn set_content(&mut self, content: Vec) -> Result<(), WriteFileError> { - let mut content_file = std::fs::File::create( - self.path - .file_path() - .map_err(|_| WriteFileError::NoMetadata)?, - )?; + let mut content_file = std::fs::File::create(self.path.file_path())?; let byte_count = content_file.write(&content)?; self.info.size = byte_count; self.info.hash = self.hash_content(&content).as_string(); @@ -170,11 +176,11 @@ impl FileHandle { } pub fn content(&self) -> Result, ReadFileError> { - load_content(&self.path.file_path()?) + load_content(&self.path.file_path()) } pub fn thumbnail(&self) -> Result, ReadFileError> { - load_content(&self.path.thumbnail_path()?) + load_content(&self.path.thumbnail_path()) } fn hash_content(&self, data: &Vec) -> HexString { @@ -182,35 +188,15 @@ impl FileHandle { } fn write_thumbnail(&self) -> Result<(), WriteFileError> { - let img = image::open( - &self - .path - .file_path() - .map_err(|_| WriteFileError::NoMetadata)?, - )?; + let img = image::open(&self.path.file_path())?; let tn = img.resize(640, 640, FilterType::Nearest); - tn.save( - &self - .path - .thumbnail_path() - .map_err(|_| WriteFileError::NoMetadata)?, - )?; + tn.save(&self.path.thumbnail_path())?; Ok(()) } pub fn delete(self) { - match self.path.thumbnail_path() { - Ok(path) => { - let _ = std::fs::remove_file(path); - } - Err(_) => {} - }; - match self.path.file_path() { - Ok(path) => { - let _ = std::fs::remove_file(path); - } - Err(_) => {} - }; + let _ = std::fs::remove_file(self.path.thumbnail_path()); + let _ = std::fs::remove_file(self.path.file_path()); let _ = std::fs::remove_file(self.path.metadata_path()); } } @@ -226,7 +212,6 @@ fn load_content(path: &Path) -> Result, ReadFileError> { mod test { use super::*; use crate::store::utils::DirCleanup; - use cool_asserts::assert_matches; use std::{convert::TryFrom, path::PathBuf}; #[test] @@ -234,21 +219,17 @@ mod test { let resolver = PathResolver::try_from("path/82420255-d3c8-4d90-a582-f94be588c70c.png") .expect("to have a valid path"); - assert_matches!( + assert_eq!( resolver.file_path(), - Ok(path) => assert_eq!(path, PathBuf::from( - "path/82420255-d3c8-4d90-a582-f94be588c70c.png" - )) + PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.png") ); assert_eq!( resolver.metadata_path(), PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.json") ); - assert_matches!( + assert_eq!( resolver.thumbnail_path(), - Ok(path) => assert_eq!(path, PathBuf::from( - "path/82420255-d3c8-4d90-a582-f94be588c70c.tn.png" - )) + PathBuf::from("path/82420255-d3c8-4d90-a582-f94be588c70c.tn.png") ); } -- 2.44.1 From 94aa67a1562439969e446f15bff32d5d4290a7cb Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 25 Sep 2023 00:17:34 -0400 Subject: [PATCH 20/36] Correctly set up file ids from list_files --- file-service/src/store/mod.rs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/file-service/src/store/mod.rs b/file-service/src/store/mod.rs index 227d8bb..9975788 100644 --- a/file-service/src/store/mod.rs +++ b/file-service/src/store/mod.rs @@ -116,18 +116,19 @@ impl Store { pub fn list_files(&self) -> Result, ReadFileError> { let paths = std::fs::read_dir(&self.files_root)?; - Ok(paths + let info_files = paths .into_iter() - .map(|path| { - FileId::from( - path.unwrap() - .path() - .file_stem() - .and_then(|s| s.to_str()) - .unwrap(), - ) + .filter_map(|path| { + let path_ = path.unwrap().path(); + if path_.extension().and_then(|s| s.to_str()) == Some("json") { + let stem = path_.file_stem().and_then(|s| s.to_str()).unwrap(); + Some(FileId::from(FileId::from(stem))) + } else { + None + } }) - .collect::>()) + .collect::>(); + Ok(info_files) } pub fn add_file( @@ -237,6 +238,7 @@ mod test { println!("ids: {:?}", ids); assert_eq!(ids.len(), 1); + assert!(ids.contains(&id)); }); } } -- 2.44.1 From 75a90bbdff7fcd2520c2d8ae8890ffac6786f111 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 25 Sep 2023 00:58:35 -0400 Subject: [PATCH 21/36] Set up temperory working directories --- Cargo.lock | 1 + file-service/Cargo.toml | 2 +- file-service/src/store/filehandle.rs | 25 ++++++++++--------- file-service/src/store/fileinfo.rs | 12 +++++---- file-service/src/store/mod.rs | 37 ++++++++++++++-------------- file-service/src/store/utils.rs | 15 ----------- 6 files changed, 41 insertions(+), 51 deletions(-) delete mode 100644 file-service/src/store/utils.rs diff --git a/Cargo.lock b/Cargo.lock index bcf0082..4c86352 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -681,6 +681,7 @@ dependencies = [ "serde 1.0.188", "serde_json", "sha2", + "tempdir", "thiserror", "tokio", "uuid 0.4.0", diff --git a/file-service/Cargo.toml b/file-service/Cargo.toml index 531ef4c..f080ffe 100644 --- a/file-service/Cargo.toml +++ b/file-service/Cargo.toml @@ -32,4 +32,4 @@ log = { version = "0.4" } bytes = { version = "1" } futures-util = { version = "0.3" } cool_asserts = { version = "2" } - +tempdir = { version = "0.3" } diff --git a/file-service/src/store/filehandle.rs b/file-service/src/store/filehandle.rs index 4259954..101254d 100644 --- a/file-service/src/store/filehandle.rs +++ b/file-service/src/store/filehandle.rs @@ -211,8 +211,8 @@ fn load_content(path: &Path) -> Result, ReadFileError> { #[cfg(test)] mod test { use super::*; - use crate::store::utils::DirCleanup; use std::{convert::TryFrom, path::PathBuf}; + use tempdir::TempDir; #[test] fn paths() { @@ -235,21 +235,23 @@ mod test { #[test] fn it_opens_a_file() { - let _cleanup = DirCleanup(PathBuf::from("var/")); - FileHandle::new("rawr.png".to_owned(), PathBuf::from("var/")).expect("to succeed"); + let tmp = TempDir::new("var").unwrap(); + FileHandle::new("rawr.png".to_owned(), PathBuf::from(tmp.path())).expect("to succeed"); } #[test] fn it_deletes_a_file() { - let _cleanup = DirCleanup(PathBuf::from("var/")); - let f = FileHandle::new("rawr.png".to_owned(), PathBuf::from("var/")).expect("to succeed"); + let tmp = TempDir::new("var").unwrap(); + let f = + FileHandle::new("rawr.png".to_owned(), PathBuf::from(tmp.path())).expect("to succeed"); f.delete(); } #[test] fn it_can_return_a_thumbnail() { - let _cleanup = DirCleanup(PathBuf::from("var/")); - let _ = FileHandle::new("rawr.png".to_owned(), PathBuf::from("var/")).expect("to succeed"); + let tmp = TempDir::new("var").unwrap(); + let _ = + FileHandle::new("rawr.png".to_owned(), PathBuf::from(tmp.path())).expect("to succeed"); /* assert_eq!( f.thumbnail(), @@ -263,15 +265,16 @@ mod test { #[test] fn it_can_return_a_file_stream() { - let _cleanup = DirCleanup(PathBuf::from("var/")); - let _ = FileHandle::new("rawr.png".to_owned(), PathBuf::from("var/")).expect("to succeed"); + let tmp = TempDir::new("var").unwrap(); + let _ = + FileHandle::new("rawr.png".to_owned(), PathBuf::from(tmp.path())).expect("to succeed"); // f.stream().expect("to succeed"); } #[test] fn it_raises_an_error_when_file_not_found() { - let _cleanup = DirCleanup(PathBuf::from("var/")); - match FileHandle::load(&FileId::from("rawr"), &PathBuf::from("var/")) { + let tmp = TempDir::new("var").unwrap(); + match FileHandle::load(&FileId::from("rawr"), tmp.path()) { Err(ReadFileError::FileNotFound(_)) => assert!(true), _ => assert!(false), } diff --git a/file-service/src/store/fileinfo.rs b/file-service/src/store/fileinfo.rs index ba71c40..69da79f 100644 --- a/file-service/src/store/fileinfo.rs +++ b/file-service/src/store/fileinfo.rs @@ -41,11 +41,12 @@ impl FileInfo { #[cfg(test)] mod test { use super::*; - use crate::store::{utils::DirCleanup, FileId}; + use crate::store::FileId; + use tempdir::TempDir; #[test] fn it_saves_and_loads_metadata() { - let _cleanup = DirCleanup(PathBuf::from("var/")); + let tmp = TempDir::new("var").unwrap(); let created = Utc::now(); let info = FileInfo { @@ -56,10 +57,11 @@ mod test { hash: "abcdefg".to_owned(), extension: "png".to_owned(), }; - info.save(PathBuf::from(format!("var/{}", *info.id))) - .unwrap(); + let mut path = tmp.path().to_owned(); + path.push(&PathBuf::from(info.id.clone())); + info.save(path.clone()).unwrap(); - let info_ = FileInfo::load(PathBuf::from(format!("var/{}", *info.id))).unwrap(); + let info_ = FileInfo::load(path).unwrap(); assert_eq!(info_.size, 23777); assert_eq!(info_.created, info.created); assert_eq!(info_.file_type, "image/png"); diff --git a/file-service/src/store/mod.rs b/file-service/src/store/mod.rs index 9975788..c023673 100644 --- a/file-service/src/store/mod.rs +++ b/file-service/src/store/mod.rs @@ -5,7 +5,6 @@ use thiserror::Error; mod filehandle; mod fileinfo; -pub mod utils; pub use filehandle::FileHandle; pub use fileinfo::FileInfo; @@ -161,49 +160,50 @@ impl Store { #[cfg(test)] mod test { - use super::{utils::DirCleanup, *}; + use super::*; use cool_asserts::assert_matches; use std::{collections::HashSet, io::Read}; + use tempdir::TempDir; fn with_file(test_fn: F) where - F: FnOnce(Store, FileId), + F: FnOnce(Store, FileId, TempDir), { - let _cleanup = DirCleanup(PathBuf::from("var/")); + let tmp = TempDir::new("var").unwrap(); let mut buf = Vec::new(); let mut file = std::fs::File::open("fixtures/rawr.png").unwrap(); file.read_to_end(&mut buf).unwrap(); - let mut store = Store::new(PathBuf::from("var/")); + let mut store = Store::new(PathBuf::from(tmp.path())); let file_record = store.add_file("rawr.png".to_owned(), buf).unwrap(); - test_fn(store, file_record.id); + test_fn(store, file_record.id, tmp); } #[test] fn adds_files() { - with_file(|store, id| { + with_file(|store, id, tmp| { let file = store.get_file(&id).expect("to retrieve the file"); assert_eq!(file.content().map(|file| file.len()).unwrap(), 23777); - assert!(PathBuf::from(format!("var/{}.png", *id)).exists()); - assert!(PathBuf::from(format!("var/{}.json", *id)).exists()); - assert!(PathBuf::from(format!("var/{}.tn.png", *id)).exists()); + assert!(tmp.path().join(&(*id)).with_extension("png").exists()); + assert!(tmp.path().join(&(*id)).with_extension("json").exists()); + assert!(tmp.path().join(&(*id)).with_extension("tn.png").exists()); }); } #[test] fn sets_up_metadata_for_file() { - with_file(|store, id| { - assert!(PathBuf::from(format!("var/{}.png", *id)).exists()); + with_file(|store, id, tmp| { + assert!(tmp.path().join(&(*id)).with_extension("png").exists()); let info = store.get_metadata(&id).expect("to retrieve the metadata"); assert_matches!(info, FileInfo { size, file_type, hash, extension, .. } => { assert_eq!(size, 23777); assert_eq!(file_type, "image/png"); - assert_eq!(hash, "".to_owned()); + assert_eq!(hash, "b6cd35e113b95d62f53d9cbd27ccefef47d3e324aef01a2db6c0c6d3a43c89ee".to_owned()); assert_eq!(extension, "png".to_owned()); }); }); @@ -221,22 +221,21 @@ mod test { #[test] fn deletes_associated_files() { - with_file(|mut store, id| { + with_file(|mut store, id, tmp| { store.delete_file(&id).expect("file to be deleted"); - assert!(!PathBuf::from(format!("var/{}.png", *id)).exists()); - assert!(!PathBuf::from(format!("var/{}.json", *id)).exists()); - assert!(!PathBuf::from(format!("var/{}.tn.png", *id)).exists()); + assert!(!tmp.path().join(&(*id)).with_extension("png").exists()); + assert!(!tmp.path().join(&(*id)).with_extension("json").exists()); + assert!(!tmp.path().join(&(*id)).with_extension("tn.png").exists()); }); } #[test] fn lists_files_in_the_db() { - with_file(|store, id| { + with_file(|store, id, _| { let resolvers = store.list_files().expect("file listing to succeed"); let ids = resolvers.into_iter().collect::>(); - println!("ids: {:?}", ids); assert_eq!(ids.len(), 1); assert!(ids.contains(&id)); }); diff --git a/file-service/src/store/utils.rs b/file-service/src/store/utils.rs deleted file mode 100644 index 92b7099..0000000 --- a/file-service/src/store/utils.rs +++ /dev/null @@ -1,15 +0,0 @@ -use std::{ffi::OsStr, path::PathBuf}; - -pub struct DirCleanup(pub PathBuf); - -impl Drop for DirCleanup { - fn drop(&mut self) { - let files = std::fs::read_dir(&self.0).unwrap(); - for file in files { - let filename = file.unwrap().path(); - if filename.file_name() != Some(&OsStr::new(".placeholder")) { - let _ = std::fs::remove_file(filename); - } - } - } -} -- 2.44.1 From b448ab765692649d222375899edd21e84a4bfcb2 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 26 Sep 2023 22:43:33 -0400 Subject: [PATCH 22/36] Complete upload --- file-service/src/html.rs | 15 ++++- file-service/src/main.rs | 126 ++++++++++++++++++++++++-------------- file-service/src/pages.rs | 5 +- 3 files changed, 95 insertions(+), 51 deletions(-) diff --git a/file-service/src/html.rs b/file-service/src/html.rs index b11881b..4539696 100644 --- a/file-service/src/html.rs +++ b/file-service/src/html.rs @@ -37,7 +37,7 @@ impl Form { impl Html for Form { fn to_html_string(&self) -> String { let encoding = match self.encoding { - Some(ref encoding) => format!("encoding=\"{encoding}\"", encoding = encoding), + Some(ref encoding) => format!("enctype=\"{encoding}\"", encoding = encoding), None => format!(""), }; format!( @@ -141,6 +141,7 @@ impl Html for Label { #[derive(Clone, Debug)] pub struct Button { + ty: Option, name: Option, label: String, } @@ -148,20 +149,30 @@ pub struct Button { impl Button { pub fn new(label: &str) -> Self { Self { + ty: None, name: None, label: label.to_owned(), } } + + pub fn with_type(mut self, ty: &str) -> Self { + self.ty = Some(ty.to_owned()); + self + } } impl Html for Button { fn to_html_string(&self) -> String { + let ty = match self.ty { + Some(ref ty) => format!("type={}", ty), + None => "".to_owned(), + }; let name = match self.name { Some(ref name) => format!("name={}", name), None => "".to_owned(), }; format!( - "", + "", name = name, label = self.label ) diff --git a/file-service/src/main.rs b/file-service/src/main.rs index b43dac8..2a2a000 100644 --- a/file-service/src/main.rs +++ b/file-service/src/main.rs @@ -8,6 +8,7 @@ use build_html::Html; use bytes::Buf; use futures_util::StreamExt; use std::{ + collections::HashMap, io::Read, net::{IpAddr, Ipv4Addr, SocketAddr}, path::PathBuf, @@ -218,19 +219,23 @@ fn script(_: &mut Request) -> IronResult { } */ -fn serve_file( - file: FileHandle, +fn serve_file( + info: FileInfo, + file: F, old_etags: Option, -) -> http::Result>> { +) -> http::Result>> +where + F: FnOnce() -> Result, ReadFileError>, +{ match old_etags { - Some(old_etags) if old_etags != file.info.hash => warp::http::Response::builder() - .header("content-type", file.info.file_type) + Some(old_etags) if old_etags != info.hash => warp::http::Response::builder() + .header("content-type", info.file_type) .status(StatusCode::NOT_MODIFIED) .body(vec![]), - _ => match file.content() { + _ => match file() { Ok(content) => warp::http::Response::builder() - .header("content-type", file.info.file_type) - .header("etag", file.info.hash) + .header("content-type", info.file_type) + .header("etag", info.hash) .status(StatusCode::OK) .body(content), Err(_) => warp::http::Response::builder() @@ -240,7 +245,9 @@ fn serve_file( } } -async fn collect_content(mut part: Part) -> Result<(Option, Vec), String> { +async fn collect_content( + mut part: Part, +) -> Result<(Option, Option, Vec), String> { let mut content: Vec = Vec::new(); while let Some(Ok(data)) = part.data().await { @@ -248,13 +255,17 @@ async fn collect_content(mut part: Part) -> Result<(Option, Vec), St reader.read_to_end(&mut content).unwrap(); } - Ok((part.filename().map(|s| s.to_owned()), content)) + Ok(( + part.content_type().map(|s| s.to_owned()), + part.filename().map(|s| s.to_owned()), + content, + )) } async fn collect_multipart( mut stream: warp::filters::multipart::FormData, -) -> Result, Vec)>, warp::Error> { - let mut content: Vec<(Option, Vec)> = Vec::new(); +) -> Result, Option, Vec)>, warp::Error> { + let mut content: Vec<(Option, Option, Vec)> = Vec::new(); while let Some(part) = stream.next().await { match part { @@ -266,6 +277,40 @@ async fn collect_multipart( Ok(content) } +async fn handle_upload( + form: warp::filters::multipart::FormData, + app: Arc>, +) -> warp::http::Result> { + let files = collect_multipart(form).await; + match files { + Ok(files) => { + for (_, filename, content) in files { + match filename { + Some(filename) => { + app.write().unwrap().add_file(filename, content).unwrap(); + } + None => { + return warp::http::Response::builder() + .status(StatusCode::BAD_REQUEST) + .body("".to_owned()) + } + } + } + } + Err(_err) => { + return warp::http::Response::builder() + .status(StatusCode::BAD_REQUEST) + .body("".to_owned()) + } + } + + // println!("file length: {:?}", files.map(|f| f.len())); + warp::http::Response::builder() + .header("location", "/") + .status(StatusCode::SEE_OTHER) + .body("".to_owned()) +} + #[tokio::main] pub async fn main() { /* @@ -315,11 +360,15 @@ pub async fn main() { &std::env::var("FILE_SHARE_DIR").unwrap(), )))); + let app_filter = { + let app = app.clone(); + warp::any().map(move || app.clone()) + }; + let log = warp::log("file_service"); - let root = warp::path!().and(warp::get()).map({ - let app = app.clone(); - move || { + let root = warp::path!().and(warp::get()).and(app_filter.clone()).map({ + move |app: Arc>| { info!("root handler"); let app = app.read().unwrap(); match app.list_files() { @@ -341,8 +390,18 @@ pub async fn main() { } }); + let post_upload_handler = warp::path!("upload") + .and(warp::post()) + .and(warp::filters::multipart::form().max_length(1024 * 1024 * 32)) + .and(app_filter.clone()) + .then( + |form: warp::filters::multipart::FormData, app: Arc>| { + handle_upload(form, app) + }, + ); + /* - let post_handler = warp::path!(String) + let post_delete_handler = warp::path!(String) .and(warp::post()) .and(warp::filters::body::form()) .map(|id: String, form: HashMap| { @@ -365,7 +424,7 @@ pub async fn main() { .unwrap() .get_file(&FileId::from(id)) { - Ok(file) => serve_file(file, old_etags), + Ok(file) => serve_file(file.info.clone(), || file.thumbnail(), old_etags), Err(_err) => warp::http::Response::builder() .status(StatusCode::NOT_FOUND) .body(vec![]), @@ -382,46 +441,19 @@ pub async fn main() { .unwrap() .get_file(&FileId::from(id)) { - Ok(file) => serve_file(file, old_etags), + Ok(file) => serve_file(file.info.clone(), || file.content(), old_etags), Err(_err) => warp::http::Response::builder() .status(StatusCode::NOT_FOUND) .body(vec![]), } }); - let upload = warp::path!() - .and(warp::post()) - .and(warp::filters::multipart::form().max_length(1024 * 1024 * 32)) - .then(|form: warp::filters::multipart::FormData| async move { - let files = collect_multipart(form).await; - /* - for (filename, content) in files { - app.write() - .unwrap() - .add_file(Some(filename), content) - .unwrap(); - } - */ - println!("file length: {:?}", files.map(|f| f.len())); - warp::reply() - }); - - let delete = warp::path!(String).and(warp::delete()).map(|id: String| { - println!("delete {}", id); - warp::reply() - }); - - /* let server = warp::serve( - root.or(post_handler) - .or(file) + root.or(post_upload_handler) .or(thumbnail) - .or(upload) - .or(delete) + .or(file) .with(log), ); - */ - let server = warp::serve(root.or(upload).or(thumbnail).or(file).or(delete).with(log)); server .run(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8002)) .await; diff --git a/file-service/src/pages.rs b/file-service/src/pages.rs index c69b075..3dfbc5e 100644 --- a/file-service/src/pages.rs +++ b/file-service/src/pages.rs @@ -10,14 +10,15 @@ pub fn index(handles: Vec>) -> build_html::Htm .with_header(1, "Admin list of files") .with_html( Form::new() + .with_path("/upload") .with_method("post") .with_encoding("multipart/form-data") .with_container( Container::new(ContainerType::Div) - .with_html(Input::new("file", "file")) + .with_html(Input::new("file", "file").with_id("for-selector-input")) .with_html(Label::new("for-selector-input", "Select a file")), ) - .with_html(Button::new("Upload file")), + .with_html(Button::new("Upload file").with_type("submit")), ); for handle in handles { -- 2.44.1 From da8281636a6ebabf32d510207bf60a3019600049 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 3 Oct 2023 11:10:37 -0400 Subject: [PATCH 23/36] Set up authentication routes --- Cargo.lock | 753 +++++++++++++++++++++++++--------- file-service/Cargo.toml | 51 ++- file-service/src/handlers.rs | 113 +++++ file-service/src/main.rs | 414 ++++--------------- file-service/src/pages.rs | 17 +- file-service/src/store/mod.rs | 137 ++++++- 6 files changed, 929 insertions(+), 556 deletions(-) create mode 100644 file-service/src/handlers.rs diff --git a/Cargo.lock b/Cargo.lock index 4c86352..41a60d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,15 +23,33 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check 0.9.4", +] + [[package]] name = "aho-corasick" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" dependencies = [ - "memchr 2.6.4", + "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -62,6 +80,15 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atty" version = "0.2.14" @@ -110,7 +137,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" dependencies = [ "byteorder", - "safemem 0.3.3", + "safemem", ] [[package]] @@ -119,6 +146,12 @@ version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bit-set" version = "0.5.3" @@ -151,6 +184,9 @@ name = "bitflags" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +dependencies = [ + "serde 1.0.188", +] [[package]] name = "block-buffer" @@ -161,29 +197,6 @@ dependencies = [ "generic-array 0.14.7", ] -[[package]] -name = "bodyparser" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f023abfa58aad6f6bc4ae0630799e24d5ee0ab8bb2e49f651d9b1f9aa4f52f30" -dependencies = [ - "iron", - "persistent", - "plugin", - "serde 1.0.188", - "serde_json", -] - -[[package]] -name = "buf_redux" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9279646319ff816b05fb5897883ece50d7d854d12b59992683d4f8a71b0f949" -dependencies = [ - "memchr 1.0.2", - "safemem 0.2.0", -] - [[package]] name = "build_html" version = "2.4.0" @@ -358,6 +371,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "const-oid" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" + [[package]] name = "cool_asserts" version = "2.0.3" @@ -401,6 +420,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" + [[package]] name = "crc32fast" version = "1.3.2" @@ -434,6 +468,16 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.16" @@ -512,6 +556,17 @@ dependencies = [ "byteorder", ] +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "digest" version = "0.10.7" @@ -519,7 +574,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -545,11 +602,20 @@ dependencies = [ "syn 2.0.37", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "either" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +dependencies = [ + "serde 1.0.188", +] [[package]] name = "emseries" @@ -615,6 +681,23 @@ dependencies = [ "libc", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "exr" version = "1.71.0" @@ -668,19 +751,15 @@ dependencies = [ "hex-string", "http", "image 0.23.14", - "iron", "log 0.4.20", "logger", "mime 0.3.17", "mime_guess 2.0.4", - "mustache", - "orizentic", - "params", "pretty_env_logger", - "router", "serde 1.0.188", "serde_json", "sha2", + "sqlx", "tempdir", "thiserror", "tokio", @@ -688,6 +767,12 @@ dependencies = [ "warp", ] +[[package]] +name = "finl_unicode" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" + [[package]] name = "flate2" version = "1.0.27" @@ -765,7 +850,9 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" dependencies = [ - "spin", + "futures-core", + "futures-sink", + "spin 0.9.8", ] [[package]] @@ -846,6 +933,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.28" @@ -887,7 +985,7 @@ dependencies = [ "futures-macro", "futures-sink", "futures-task", - "memchr 2.6.4", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -1095,7 +1193,7 @@ dependencies = [ "glib-sys 0.17.10", "gobject-sys 0.17.10", "libc", - "memchr 2.6.4", + "memchr", "once_cell", "smallvec", "thiserror", @@ -1118,7 +1216,7 @@ dependencies = [ "glib-sys 0.18.1", "gobject-sys 0.18.0", "libc", - "memchr 2.6.4", + "memchr", "once_cell", "smallvec", "thiserror", @@ -1384,6 +1482,19 @@ name = "hashbrown" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.1", +] [[package]] name = "headers" @@ -1414,6 +1525,9 @@ name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] [[package]] name = "hermit-abi" @@ -1430,6 +1544,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hex-grid" version = "0.1.0" @@ -1449,6 +1569,33 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "848ec2dd093df965a34b434580d94852197fc83feac5b2c1962399bbf2cb4f0b" +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys", +] + [[package]] name = "http" version = "0.2.9" @@ -1709,7 +1856,7 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ - "hermit-abi 0.3.2", + "hermit-abi 0.3.3", "rustix", "windows-sys", ] @@ -1723,6 +1870,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -1815,6 +1971,9 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] [[package]] name = "lebe" @@ -1867,6 +2026,17 @@ version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" +[[package]] +name = "libsqlite3-sys" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -1922,12 +2092,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] -name = "memchr" -version = "1.0.2" +name = "md-5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ - "libc", + "cfg-if", + "digest", ] [[package]] @@ -2057,39 +2228,12 @@ dependencies = [ "http", "httparse", "log 0.4.20", - "memchr 2.6.4", + "memchr", "mime 0.3.17", - "spin", + "spin 0.9.8", "version_check 0.9.4", ] -[[package]] -name = "multipart" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92f54eb45230c3aa20864ccf0c277eeaeadcf5e437e91731db498dbf7fbe0ec6" -dependencies = [ - "buf_redux", - "httparse", - "log 0.3.9", - "mime 0.2.6", - "mime_guess 1.8.8", - "rand 0.3.23", - "safemem 0.2.0", - "tempdir", - "twoway", -] - -[[package]] -name = "mustache" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51956ef1c5d20a1384524d91e616fb44dfc7d8f249bf696d49c97dd3289ecab5" -dependencies = [ - "log 0.3.9", - "serde 1.0.188", -] - [[package]] name = "native-tls" version = "0.2.11" @@ -2120,7 +2264,7 @@ version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ - "memchr 2.6.4", + "memchr", "minimal-lexical", ] @@ -2133,39 +2277,20 @@ dependencies = [ ] [[package]] -name = "num" -version = "0.1.42" +name = "num-bigint-dig" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4703ad64153382334aa8db57c637364c322d3372e097840c72000dabdcf6156e" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" dependencies = [ - "num-bigint", - "num-complex", + "byteorder", + "lazy_static", + "libm", "num-integer", "num-iter", - "num-rational 0.1.42", "num-traits", -] - -[[package]] -name = "num-bigint" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e63899ad0da84ce718c14936262a41cee2c79c981fc0a0e7c7beb47d5a07e8c1" -dependencies = [ - "num-integer", - "num-traits", - "rand 0.4.6", - "rustc-serialize", -] - -[[package]] -name = "num-complex" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b288631d7878aaf59442cffd36910ea604ecd7745c36054328595114001c9656" -dependencies = [ - "num-traits", - "rustc-serialize", + "rand 0.8.5", + "smallvec", + "zeroize", ] [[package]] @@ -2189,18 +2314,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-rational" -version = "0.1.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee314c74bd753fc86b4780aa9475da469155f3848473a261d2d18e35245a784e" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", - "rustc-serialize", -] - [[package]] name = "num-rational" version = "0.3.2" @@ -2249,7 +2362,7 @@ version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" dependencies = [ - "memchr 2.6.4", + "memchr", ] [[package]] @@ -2308,7 +2421,7 @@ version = "1.0.1" dependencies = [ "chrono", "clap", - "itertools", + "itertools 0.10.5", "jsonwebtoken", "serde 1.0.188", "serde_derive", @@ -2370,22 +2483,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "params" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c789fdad2cfdaa551ea0e3a9eadb74c5d634968a9fb3a8c767d89be470d21589" -dependencies = [ - "bodyparser", - "iron", - "multipart", - "num", - "plugin", - "serde_json", - "tempdir", - "urlencoded", -] - [[package]] name = "parking_lot" version = "0.12.1" @@ -2418,6 +2515,21 @@ dependencies = [ "regex", ] +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "1.0.1" @@ -2430,16 +2542,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" -[[package]] -name = "persistent" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8fa0009c4f3d350281309909c618abddf10bb7e3145f28410782f6a5ec74c5" -dependencies = [ - "iron", - "plugin", -] - [[package]] name = "phf" version = "0.7.24" @@ -2549,6 +2651,27 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.27" @@ -2905,7 +3028,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebee201405406dbf528b8b672104ae6d6d63e6d118cb10e4d51abbc7b58044ff" dependencies = [ "aho-corasick", - "memchr 2.6.4", + "memchr", "regex-automata", "regex-syntax", ] @@ -2917,7 +3040,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" dependencies = [ "aho-corasick", - "memchr 2.6.4", + "memchr", "regex-syntax", ] @@ -2987,20 +3110,25 @@ dependencies = [ ] [[package]] -name = "route-recognizer" -version = "0.1.13" +name = "rsa" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea509065eb0b3c446acdd0102f0d46567dc30902dc0be91d6552035d92b0f4f8" - -[[package]] -name = "router" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc63b6f3b8895b0d04e816b2b1aa58fdba2d5acca3cbb8f0ab8e017347d57397" +checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8" dependencies = [ - "iron", - "route-recognizer", - "url 1.7.2", + "byteorder", + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", ] [[package]] @@ -3015,12 +3143,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" -[[package]] -name = "rustc-serialize" -version = "0.3.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" - [[package]] name = "rustc_version" version = "0.4.0" @@ -3070,12 +3192,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" -[[package]] -name = "safemem" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f" - [[package]] name = "safemem" version = "0.3.3" @@ -3234,9 +3350,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", @@ -3252,6 +3368,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -3305,6 +3431,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "spin" version = "0.9.8" @@ -3314,12 +3446,244 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spki" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlformat" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7b278788e7be4d0d29c0f39497a0eef3fba6bbc8e70d8bf7fde46edeaa9e85" +dependencies = [ + "itertools 0.11.0", + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e50c216e3624ec8e7ecd14c6a6a6370aad6ee5d8cfc3ab30b5162eeeef2ed33" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d6753e460c998bbd4cd8c6f0ed9a64346fcca0723d6e75e52fdc351c5d2169d" +dependencies = [ + "ahash", + "atoi", + "byteorder", + "bytes", + "crc", + "crossbeam-queue", + "dotenvy", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap 2.0.2", + "log 0.4.20", + "memchr", + "once_cell", + "paste", + "percent-encoding 2.3.0", + "serde 1.0.188", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url 2.4.1", +] + +[[package]] +name = "sqlx-macros" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a793bb3ba331ec8359c1853bd39eed32cdd7baaf22c35ccf5c92a7e8d1189ec" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a4ee1e104e00dedb6aa5ffdd1343107b0a4702e862a84320ee7cc74782d96fc" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde 1.0.188", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "tokio", + "url 2.4.1", +] + +[[package]] +name = "sqlx-mysql" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864b869fdf56263f4c95c45483191ea0af340f9f3e3e7b4d57a61c7c87a970db" +dependencies = [ + "atoi", + "base64 0.21.4", + "bitflags 2.4.0", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array 0.14.7", + "hex", + "hkdf", + "hmac", + "itoa", + "log 0.4.20", + "md-5", + "memchr", + "once_cell", + "percent-encoding 2.3.0", + "rand 0.8.5", + "rsa", + "serde 1.0.188", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7ae0e6a97fb3ba33b23ac2671a5ce6e3cabe003f451abd5a56e7951d975624" +dependencies = [ + "atoi", + "base64 0.21.4", + "bitflags 2.4.0", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log 0.4.20", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde 1.0.188", + "serde_json", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59dc83cf45d89c555a577694534fcd1b55c545a816c816ce51f20bbe56a4f3f" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log 0.4.20", + "percent-encoding 2.3.0", + "serde 1.0.188", + "sqlx-core", + "tracing", + "url 2.4.1", +] + +[[package]] +name = "stringprep" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" +dependencies = [ + "finl_unicode", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "strsim" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "syn" version = "1.0.109" @@ -3637,9 +4001,21 @@ dependencies = [ "cfg-if", "log 0.4.20", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", +] + [[package]] name = "tracing-core" version = "0.1.31" @@ -3680,15 +4056,6 @@ dependencies = [ "utf-8", ] -[[package]] -name = "twoway" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" -dependencies = [ - "memchr 2.6.4", -] - [[package]] name = "type-map" version = "0.4.0" @@ -3804,12 +4171,24 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + [[package]] name = "unicode-width" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "unsafe-any" version = "0.4.2" @@ -3847,18 +4226,6 @@ dependencies = [ "percent-encoding 2.3.0", ] -[[package]] -name = "urlencoded" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a52f50139118b60ae91af08bf15ed158817d34b91b9d24c11ffbe21195d33e3" -dependencies = [ - "bodyparser", - "iron", - "plugin", - "url 1.7.2", -] - [[package]] name = "utf-8" version = "0.7.6" @@ -4067,6 +4434,12 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" +[[package]] +name = "whoami" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" + [[package]] name = "winapi" version = "0.3.9" @@ -4179,7 +4552,7 @@ version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" dependencies = [ - "memchr 2.6.4", + "memchr", ] [[package]] @@ -4201,6 +4574,12 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" + [[package]] name = "zune-inflate" version = "0.2.54" diff --git a/file-service/Cargo.toml b/file-service/Cargo.toml index f080ffe..c1571fb 100644 --- a/file-service/Cargo.toml +++ b/file-service/Cargo.toml @@ -7,29 +7,28 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -build_html = { version = "2" } -chrono = { version = "0.4", features = ["serde"] } -hex-string = "0.1.0" -http = { version = "0.2" } -image = "0.23.5" -iron = "0.6.1" -logger = "*" -mime = "0.3.16" -mime_guess = "2.0.3" -mustache = "0.9.0" -orizentic = { path = "../orizentic" } -params = "*" -router = "*" -serde_json = "*" -serde = { version = "1.0", features = ["derive"] } -sha2 = "0.10" -thiserror = "1.0.20" -tokio = { version = "1", features = [ "full" ] } -uuid = { version = "0.4", features = [ "serde", "v4" ] } -warp = { version = "0.3" } -pretty_env_logger = { version = "0.5" } -log = { version = "0.4" } -bytes = { version = "1" } -futures-util = { version = "0.3" } -cool_asserts = { version = "2" } -tempdir = { version = "0.3" } +build_html = { version = "2" } +bytes = { version = "1" } +chrono = { version = "0.4", features = ["serde"] } +futures-util = { version = "0.3" } +hex-string = "0.1.0" +http = { version = "0.2" } +image = "0.23.5" +logger = "*" +log = { version = "0.4" } +mime = "0.3.16" +mime_guess = "2.0.3" +pretty_env_logger = { version = "0.5" } +serde_json = "*" +serde = { version = "1.0", features = ["derive"] } +sha2 = "0.10" +sqlx = { version = "0.7", features = [ "runtime-tokio", "sqlite" ] } +thiserror = "1.0.20" +tokio = { version = "1", features = [ "full" ] } +uuid = { version = "0.4", features = [ "serde", "v4" ] } +warp = { version = "0.3" } + +[dev-dependencies] +cool_asserts = { version = "2" } +tempdir = { version = "0.3" } + diff --git a/file-service/src/handlers.rs b/file-service/src/handlers.rs new file mode 100644 index 0000000..cb6d926 --- /dev/null +++ b/file-service/src/handlers.rs @@ -0,0 +1,113 @@ +use build_html::Html; +use http::{Error, StatusCode}; +use std::{collections::HashMap, future::Future}; +use warp::http::Response; + +use crate::{pages, App, FileHandle, FileId, FileInfo, ReadFileError, SessionToken}; + +pub async fn handle_index( + app: App, + token: Option, +) -> Result, Error> { + match token { + Some(token) => match app.auth_session(token).await { + Ok(_) => render_gallery_page(app).await, + Err(err) => render_auth_page(Some(format!("authentication refused: {:?}", err))), + }, + None => render_auth_page(None), + } +} + +pub fn render_auth_page(message: Option) -> Result, Error> { + Response::builder() + .status(StatusCode::OK) + .body(pages::auth(message).to_html_string()) +} + +pub async fn render_gallery_page(app: App) -> Result, Error> { + match app.list_files().await { + Ok(ids) => { + let mut files = vec![]; + for id in ids.into_iter() { + let file = app.get_file(&id).await; + files.push(file); + } + Response::builder() + .header("content-type", "text/html") + .status(StatusCode::OK) + .body(pages::gallery(files).to_html_string()) + } + Err(_) => Response::builder() + .header("content-type", "text/html") + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body("".to_owned()), + } +} + +pub async fn thumbnail( + app: App, + id: String, + old_etags: Option, +) -> Result>, Error> { + match app.get_file(&FileId::from(id)).await { + Ok(file) => serve_file(file.info.clone(), || file.thumbnail(), old_etags), + Err(_err) => Response::builder() + .status(StatusCode::NOT_FOUND) + .body(vec![]), + } +} + +pub async fn file( + app: App, + id: String, + old_etags: Option, +) -> Result>, Error> { + match app.get_file(&FileId::from(id)).await { + Ok(file) => serve_file(file.info.clone(), || file.thumbnail(), old_etags), + Err(_err) => Response::builder() + .status(StatusCode::NOT_FOUND) + .body(vec![]), + } +} + +pub async fn handle_auth(_form: HashMap) -> Result, Error> { + Response::builder() + .status(StatusCode::NOT_IMPLEMENTED) + .body("".to_owned()) +} + +pub async fn handle_upload( + _app: App, + _token: SessionToken, +) -> Result, Error> { + println!("handle_upload"); + Response::builder() + .status(StatusCode::NOT_IMPLEMENTED) + .body("".to_owned()) +} + +fn serve_file( + info: FileInfo, + file: F, + old_etags: Option, +) -> http::Result>> +where + F: FnOnce() -> Result, ReadFileError>, +{ + match old_etags { + Some(old_etags) if old_etags != info.hash => Response::builder() + .header("content-type", info.file_type) + .status(StatusCode::NOT_MODIFIED) + .body(vec![]), + _ => match file() { + Ok(content) => Response::builder() + .header("content-type", info.file_type) + .header("etag", info.hash) + .status(StatusCode::OK) + .body(content), + Err(_) => Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(vec![]), + }, + } +} diff --git a/file-service/src/main.rs b/file-service/src/main.rs index 2a2a000..191db78 100644 --- a/file-service/src/main.rs +++ b/file-service/src/main.rs @@ -1,250 +1,44 @@ #[macro_use] extern crate log; +use handlers::{file, handle_auth, handle_upload, thumbnail}; use http::status::StatusCode; // use mustache::{compile_path, Template}; // use orizentic::{Permissions, ResourceName, Secret}; -use build_html::Html; use bytes::Buf; use futures_util::StreamExt; use std::{ collections::HashMap, + convert::Infallible, io::Read, net::{IpAddr, Ipv4Addr, SocketAddr}, path::PathBuf, sync::{Arc, RwLock}, }; -use warp::{filters::multipart::Part, Filter}; +use store::Username; +use warp::{filters::multipart::Part, Filter, Rejection}; -// mod cookies; +mod handlers; mod html; -// mod middleware; mod pages; mod store; -pub use store::{FileHandle, FileId, FileInfo, ReadFileError, Store}; +pub use handlers::handle_index; +pub use store::{App, AuthDB, FileHandle, FileId, FileInfo, ReadFileError, SessionToken, Store}; /* -fn is_admin(resource: &ResourceName, permissions: &Permissions) -> bool { - let Permissions(perms) = permissions; - ResourceName(String::from( - "https://savanni.luminescent-dreams.com/file-service/", - )) == *resource - && perms.contains(&String::from("admin")) -} - -pub fn compare_etags(info: FileInfo, etag_list: &headers::IfNoneMatch) -> bool { - let current_etag = headers::EntityTag::new(false, info.hash); - match etag_list { - headers::IfNoneMatch::Any => false, - headers::IfNoneMatch::Items(lst) => lst.iter().any(|etag| etag.weak_eq(¤t_etag)), +async fn authenticate_user(app: App, auth_token: String) -> Result { + match app.auth_session(SessionToken::from(auth_token)).await { + Ok(username) => Ok(username), + Err(_) => Err(warp::reject::not_found()), } } - -mod files { - use super::*; - - pub struct GetHandler { - pub app: Arc>, - } - - impl Handler for GetHandler { - fn handle(&self, req: &mut Request) -> IronResult { - let app = self.app.read().unwrap(); - let capture = req.extensions.get::().unwrap().clone(); - let old_etags = req.headers.get::(); - match capture.find("id") { - Some(id) => { - let info = app.get_metadata(String::from(id)); - match (info, old_etags) { - (Ok(info_), Some(if_none_match)) => { - if compare_etags(info_, if_none_match) { - return Ok(Response::with(status::NotModified)); - } - } - _ => (), - } - match app.get_file(String::from(id)) { - Ok((info, stream)) => Ok(Response::with(( - status::Ok, - Header(headers::ContentType( - info.file_type.parse::().unwrap(), - )), - Header(headers::ETag(headers::EntityTag::new(false, info.hash))), - BodyReader(stream), - ))), - Err(_err) => Ok(Response::with(status::NotFound)), - } - } - _ => Ok(Response::with(status::BadRequest)), - } - } - } - - pub struct GetThumbnailHandler { - pub app: Arc>, - } - - impl Handler for GetThumbnailHandler { - fn handle(&self, req: &mut Request) -> IronResult { - let app = self.app.read().unwrap(); - let capture = req.extensions.get::().unwrap().clone(); - let old_etags = req.headers.get::(); - match capture.find("id") { - Some(id) => { - let info = app.get_metadata(String::from(id)); - match (info, old_etags) { - (Ok(info_), Some(if_none_match)) => { - if compare_etags(info_, if_none_match) { - return Ok(Response::with(status::NotModified)); - } - } - _ => (), - } - match app.get_thumbnail(id) { - Ok((info, stream)) => Ok(Response::with(( - status::Ok, - Header(headers::ContentType( - info.file_type.parse::().unwrap(), - )), - Header(headers::ETag(headers::EntityTag::new(false, info.hash))), - BodyReader(stream), - ))), - Err(_err) => Ok(Response::with(status::NotFound)), - } - } - _ => Ok(Response::with(status::BadRequest)), - } - } - } - pub struct PostHandler { - pub app: Arc>, - } - - impl Handler for PostHandler { - fn handle(&self, req: &mut Request) -> IronResult { - let mut app = self.app.write().unwrap(); - let m_token = req.extensions.get::(); - match m_token { - Some(token) => { - if token.check_authorizations(is_admin) { - let params = req.get_ref::().unwrap(); - if let Value::File(f_info) = params.get("file").unwrap() { - match app.add_file( - &f_info.path, - &f_info.filename.clone().map(|fname| PathBuf::from(fname)), - ) { - Ok(_) => Ok(Response::with(( - status::MovedPermanently, - Redirect(router::url_for(req, "index", HashMap::new())), - ))), - Err(_) => Ok(Response::with(status::InternalServerError)), - } - } else { - Ok(Response::with(status::BadRequest)) - } - } else { - Ok(Response::with(status::Forbidden)) - } - } - None => Ok(Response::with(status::Forbidden)), - } - } - } - - pub struct DeleteHandler { - pub app: Arc>, - } - - impl Handler for DeleteHandler { - fn handle(&self, req: &mut Request) -> IronResult { - let mut app = self.app.write().unwrap(); - let capture = req.extensions.get::().unwrap().clone(); - let m_token = req.extensions.get::(); - match m_token { - Some(token) => { - if token.check_authorizations(is_admin) { - match capture.find("id") { - Some(id) => match app.delete_file(String::from(id)) { - Ok(()) => Ok(Response::with(( - status::MovedPermanently, - Redirect(router::url_for(req, "index", HashMap::new())), - ))), - Err(_) => Ok(Response::with(status::InternalServerError)), - }, - None => Ok(Response::with(status::BadRequest)), - } - } else { - Ok(Response::with(status::Forbidden)) - } - } - None => Ok(Response::with(status::Forbidden)), - } - } - } -} - -fn css(_: &mut Request) -> IronResult { - let mut css: String = String::from(""); - File::open("templates/style.css") - .unwrap() - .read_to_string(&mut css) - .unwrap(); - Ok(Response::with(( - status::Ok, - Header(headers::ContentType(iron::mime::Mime( - iron::mime::TopLevel::Text, - iron::mime::SubLevel::Css, - vec![], - ))), - css, - ))) -} - -fn script(_: &mut Request) -> IronResult { - let mut js: String = String::from(""); - File::open("templates/script.js") - .unwrap() - .read_to_string(&mut js) - .unwrap(); - Ok(Response::with(( - status::Ok, - Header(headers::ContentType(iron::mime::Mime( - iron::mime::TopLevel::Text, - iron::mime::SubLevel::Javascript, - vec![], - ))), - js, - ))) -} */ -fn serve_file( - info: FileInfo, - file: F, - old_etags: Option, -) -> http::Result>> -where - F: FnOnce() -> Result, ReadFileError>, -{ - match old_etags { - Some(old_etags) if old_etags != info.hash => warp::http::Response::builder() - .header("content-type", info.file_type) - .status(StatusCode::NOT_MODIFIED) - .body(vec![]), - _ => match file() { - Ok(content) => warp::http::Response::builder() - .header("content-type", info.file_type) - .header("etag", info.hash) - .status(StatusCode::OK) - .body(content), - Err(_) => warp::http::Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(vec![]), - }, - } -} +/* +*/ +/* async fn collect_content( mut part: Part, ) -> Result<(Option, Option, Vec), String> { @@ -261,7 +55,9 @@ async fn collect_content( content, )) } +*/ +/* async fn collect_multipart( mut stream: warp::filters::multipart::FormData, ) -> Result, Option, Vec)>, warp::Error> { @@ -276,10 +72,12 @@ async fn collect_multipart( Ok(content) } +*/ +/* async fn handle_upload( form: warp::filters::multipart::FormData, - app: Arc>, + app: App, ) -> warp::http::Result> { let files = collect_multipart(form).await; match files { @@ -287,7 +85,7 @@ async fn handle_upload( for (_, filename, content) in files { match filename { Some(filename) => { - app.write().unwrap().add_file(filename, content).unwrap(); + app.add_file(filename, content).unwrap(); } None => { return warp::http::Response::builder() @@ -310,150 +108,84 @@ async fn handle_upload( .status(StatusCode::SEE_OTHER) .body("".to_owned()) } +*/ + +fn with_app(app: App) -> impl Filter + Clone { + warp::any().map(move || app.clone()) +} + +fn maybe_with_session() -> impl Filter,), Error = Rejection> + Copy +{ + warp::any() + .and(warp::header::optional::("cookie")) + .map(|cookies| { + println!("cookies retrieved: {:?}", cookies); + None + }) +} + +fn with_session() -> impl Filter + Copy { + warp::any() + .and(warp::header::("cookie")) + .map(|token: String| SessionToken::from(token)) +} #[tokio::main] pub async fn main() { - /* - let auth_db_path = std::env::var("ORIZENTIC_DB").unwrap(); - let secret = Secret(Vec::from( - std::env::var("ORIZENTIC_SECRET").unwrap().as_bytes(), - )); - let auth_middleware = Authentication::new(secret, auth_db_path); - - let mut router = Router::new(); - - router.post("/", files::PostHandler { app: app.clone() }, "upload-file"); - - router.delete( - "/:id", - files::DeleteHandler { app: app.clone() }, - "delete-file", - ); - router.get("/css", css, "styles"); - router.get("/script", script, "script"); - - let mut chain = Chain::new(router); - chain.link_before(auth_middleware); - chain.link_before(RestForm {}); - - Iron::new(chain).http("0.0.0.0:3000").unwrap(); - */ - - /* - let root = warp::path!().and(warp::get()).map({ - || { - warp::http::Response::builder() - .header("content-type", "text/html") - .status(StatusCode::NOT_MODIFIED) - .body(()) - } - }); - let server = warp::serve(root); - server - .run(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8002)) - .await; - */ - pretty_env_logger::init(); - let app = Arc::new(RwLock::new(Store::new(PathBuf::from( - &std::env::var("FILE_SHARE_DIR").unwrap(), - )))); + let authdb = AuthDB::new(PathBuf::from(":memory:")).await.unwrap(); + let store = Store::new(PathBuf::from(&std::env::var("FILE_SHARE_DIR").unwrap())); - let app_filter = { + let app = App::new(authdb, store); + + /* + let with_app = { let app = app.clone(); warp::any().map(move || app.clone()) }; + */ let log = warp::log("file_service"); + let root = warp::path!() + .and(warp::get()) + .and(with_app(app.clone())) + .and(maybe_with_session()) + .then(handle_index); - let root = warp::path!().and(warp::get()).and(app_filter.clone()).map({ - move |app: Arc>| { - info!("root handler"); - let app = app.read().unwrap(); - match app.list_files() { - Ok(ids) => { - let files = ids - .into_iter() - .map(|id| app.get_file(&id)) - .collect::>>(); - warp::http::Response::builder() - .header("content-type", "text/html") - .status(StatusCode::OK) - .body(pages::index(files).to_html_string()) - } - Err(_) => warp::http::Response::builder() - .header("content-type", "text/html") - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body("".to_owned()), - } - } - }); - - let post_upload_handler = warp::path!("upload") - .and(warp::post()) - .and(warp::filters::multipart::form().max_length(1024 * 1024 * 32)) - .and(app_filter.clone()) - .then( - |form: warp::filters::multipart::FormData, app: Arc>| { - handle_upload(form, app) - }, - ); - - /* - let post_delete_handler = warp::path!(String) + let auth = warp::path!("auth") .and(warp::post()) .and(warp::filters::body::form()) - .map(|id: String, form: HashMap| { - info!("post_delete {}", id); - info!("form: {:?}", form); - warp::http::Response::builder() - .header("location", "/") - .status(StatusCode::SEE_OTHER) - .body(vec![]) - }); - */ + .then(handle_auth); + + let upload_handler = warp::path!("upload") + .and(warp::post()) + .and(with_app(app.clone())) + .and(with_session()) + .then(handle_upload); let thumbnail = warp::path!(String / "tn") .and(warp::get()) .and(warp::header::optional::("if-none-match")) - .map({ - let app = app.clone(); - move |id: String, old_etags: Option| match app - .read() - .unwrap() - .get_file(&FileId::from(id)) - { - Ok(file) => serve_file(file.info.clone(), || file.thumbnail(), old_etags), - Err(_err) => warp::http::Response::builder() - .status(StatusCode::NOT_FOUND) - .body(vec![]), - } - }); + .and(with_app(app.clone())) + .then(move |id, old_etags, app: App| thumbnail(app, id, old_etags)); let file = warp::path!(String) .and(warp::get()) .and(warp::header::optional::("if-none-match")) - .map({ - let app = app.clone(); - move |id: String, old_etags: Option| match app - .read() - .unwrap() - .get_file(&FileId::from(id)) - { - Ok(file) => serve_file(file.info.clone(), || file.content(), old_etags), - Err(_err) => warp::http::Response::builder() - .status(StatusCode::NOT_FOUND) - .body(vec![]), - } - }); + .and(with_app(app.clone())) + .then(move |id, old_etags, app: App| file(app, id, old_etags)); let server = warp::serve( - root.or(post_upload_handler) - .or(thumbnail) - .or(file) - .with(log), + root.or(auth).with(log), /* + root.or(auth) + .or(thumbnail) + .or(file) + .or(upload_handler) + .with(log), + */ ); + server .run(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8002)) .await; diff --git a/file-service/src/pages.rs b/file-service/src/pages.rs index 3dfbc5e..45d5fdf 100644 --- a/file-service/src/pages.rs +++ b/file-service/src/pages.rs @@ -4,7 +4,22 @@ use crate::{ }; use build_html::{self, Container, ContainerType, Html, HtmlContainer}; -pub fn index(handles: Vec>) -> build_html::HtmlPage { +pub fn auth(_message: Option) -> build_html::HtmlPage { + build_html::HtmlPage::new() + .with_title("Authentication") + .with_html( + Form::new() + .with_path("/auth") + .with_method("post") + .with_container( + Container::new(ContainerType::Div) + .with_html(Input::new("token", "token").with_id("for-token-input")) + .with_html(Label::new("for-token-input", "Authentication Token")), + ), + ) +} + +pub fn gallery(handles: Vec>) -> build_html::HtmlPage { let mut page = build_html::HtmlPage::new() .with_title("Admin list of files") .with_header(1, "Admin list of files") diff --git a/file-service/src/store/mod.rs b/file-service/src/store/mod.rs index c023673..e757643 100644 --- a/file-service/src/store/mod.rs +++ b/file-service/src/store/mod.rs @@ -1,7 +1,9 @@ use serde::{Deserialize, Serialize}; +use sqlx::sqlite::SqlitePool; use std::collections::HashSet; -use std::{ops::Deref, path::PathBuf}; +use std::{ops::Deref, path::PathBuf, sync::Arc}; use thiserror::Error; +use tokio::sync::RwLock; mod filehandle; mod fileinfo; @@ -57,6 +59,82 @@ pub enum ReadFileError { IOError(#[from] std::io::Error), } +#[derive(Debug, Error)] +pub enum AuthError { + #[error("database failed")] + SqlError, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)] +pub struct Username(String); + +impl From for Username { + fn from(s: String) -> Self { + Self(s) + } +} + +impl From<&str> for Username { + fn from(s: &str) -> Self { + Self(s.to_owned()) + } +} + +impl From for PathBuf { + fn from(s: Username) -> Self { + Self::from(&s) + } +} + +impl From<&Username> for PathBuf { + fn from(s: &Username) -> Self { + let Username(s) = s; + Self::from(s) + } +} + +impl Deref for Username { + type Target = String; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)] +pub struct SessionToken(String); + +impl From for SessionToken { + fn from(s: String) -> Self { + Self(s) + } +} + +impl From<&str> for SessionToken { + fn from(s: &str) -> Self { + Self(s.to_owned()) + } +} + +impl From for PathBuf { + fn from(s: SessionToken) -> Self { + Self::from(&s) + } +} + +impl From<&SessionToken> for PathBuf { + fn from(s: &SessionToken) -> Self { + let SessionToken(s) = s; + Self::from(s) + } +} + +impl Deref for SessionToken { + type Target = String; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + #[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)] pub struct FileId(String); @@ -104,6 +182,63 @@ impl FileRoot for Context { } } +#[derive(Clone)] +pub struct App { + authdb: Arc>, + store: Arc>, +} + +impl App { + pub fn new(authdb: AuthDB, store: Store) -> Self { + Self { + authdb: Arc::new(RwLock::new(authdb)), + store: Arc::new(RwLock::new(store)), + } + } + + pub async fn auth_session(&self, token: SessionToken) -> Result { + let authdb = self.authdb.read(); + // authdb.auth_session(token).await + unimplemented!() + } + + pub async fn list_files(&self) -> Result, ReadFileError> { + self.store.read().await.list_files() + } + + pub async fn get_file(&self, id: &FileId) -> Result { + self.store.read().await.get_file(id) + } + + pub async fn add_file( + &self, + filename: String, + content: Vec, + ) -> Result { + self.store.write().await.add_file(filename, content) + } +} + +#[derive(Clone)] +pub struct AuthDB { + pool: SqlitePool, +} + +impl AuthDB { + pub async fn new(path: PathBuf) -> Result { + let pool = SqlitePool::connect(&format!("sqlite://{}", path.to_str().unwrap())).await?; + Ok(Self { pool }) + } + + pub async fn auth_session(&self, _token: SessionToken) -> Result { + /* + let conn = self.pool.acquire().await.map_err(|_| AuthError::SqlError)?; + conn.transaction(|tr| {}) + */ + unimplemented!() + } +} + pub struct Store { files_root: PathBuf, } -- 2.44.1 From 535ea6cd9d057aab65d7194979981ca98c8d4a31 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 3 Oct 2023 11:28:09 -0400 Subject: [PATCH 24/36] Finish the auth handler and create app auth stubs --- file-service/src/handlers.rs | 20 ++++++++++---- file-service/src/main.rs | 5 +++- file-service/src/store/mod.rs | 49 ++++++++++++++++++++++++++++++++--- 3 files changed, 64 insertions(+), 10 deletions(-) diff --git a/file-service/src/handlers.rs b/file-service/src/handlers.rs index cb6d926..7d8fb22 100644 --- a/file-service/src/handlers.rs +++ b/file-service/src/handlers.rs @@ -3,7 +3,7 @@ use http::{Error, StatusCode}; use std::{collections::HashMap, future::Future}; use warp::http::Response; -use crate::{pages, App, FileHandle, FileId, FileInfo, ReadFileError, SessionToken}; +use crate::{pages, App, AuthToken, FileHandle, FileId, FileInfo, ReadFileError, SessionToken}; pub async fn handle_index( app: App, @@ -70,10 +70,20 @@ pub async fn file( } } -pub async fn handle_auth(_form: HashMap) -> Result, Error> { - Response::builder() - .status(StatusCode::NOT_IMPLEMENTED) - .body("".to_owned()) +pub async fn handle_auth( + app: App, + form: HashMap, +) -> Result, Error> { + match form.get("token") { + Some(token) => match app + .auth_token(AuthToken::from(AuthToken::from(token.clone()))) + .await + { + Ok(_session_token) => render_gallery_page(app).await, + Err(_) => render_auth_page(Some(format!("invalid auth token"))), + }, + None => render_auth_page(Some(format!("no token available"))), + } } pub async fn handle_upload( diff --git a/file-service/src/main.rs b/file-service/src/main.rs index 191db78..6dd12f5 100644 --- a/file-service/src/main.rs +++ b/file-service/src/main.rs @@ -24,7 +24,9 @@ mod pages; mod store; pub use handlers::handle_index; -pub use store::{App, AuthDB, FileHandle, FileId, FileInfo, ReadFileError, SessionToken, Store}; +pub use store::{ + App, AuthDB, AuthToken, FileHandle, FileId, FileInfo, ReadFileError, SessionToken, Store, +}; /* async fn authenticate_user(app: App, auth_token: String) -> Result { @@ -155,6 +157,7 @@ pub async fn main() { let auth = warp::path!("auth") .and(warp::post()) + .and(with_app(app.clone())) .and(warp::filters::body::form()) .then(handle_auth); diff --git a/file-service/src/store/mod.rs b/file-service/src/store/mod.rs index e757643..907c4ee 100644 --- a/file-service/src/store/mod.rs +++ b/file-service/src/store/mod.rs @@ -100,6 +100,41 @@ impl Deref for Username { } } +#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)] +pub struct AuthToken(String); + +impl From for AuthToken { + fn from(s: String) -> Self { + Self(s) + } +} + +impl From<&str> for AuthToken { + fn from(s: &str) -> Self { + Self(s.to_owned()) + } +} + +impl From for PathBuf { + fn from(s: AuthToken) -> Self { + Self::from(&s) + } +} + +impl From<&AuthToken> for PathBuf { + fn from(s: &AuthToken) -> Self { + let AuthToken(s) = s; + Self::from(s) + } +} + +impl Deref for AuthToken { + type Target = String; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + #[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)] pub struct SessionToken(String); @@ -196,10 +231,12 @@ impl App { } } + pub async fn auth_token(&self, token: AuthToken) -> Result { + self.authdb.read().await.auth_token(token).await + } + pub async fn auth_session(&self, token: SessionToken) -> Result { - let authdb = self.authdb.read(); - // authdb.auth_session(token).await - unimplemented!() + self.authdb.read().await.auth_session(token).await } pub async fn list_files(&self) -> Result, ReadFileError> { @@ -230,7 +267,11 @@ impl AuthDB { Ok(Self { pool }) } - pub async fn auth_session(&self, _token: SessionToken) -> Result { + async fn auth_token(&self, _token: AuthToken) -> Result { + unimplemented!() + } + + async fn auth_session(&self, _token: SessionToken) -> Result { /* let conn = self.pool.acquire().await.map_err(|_| AuthError::SqlError)?; conn.transaction(|tr| {}) -- 2.44.1 From 6aedff8cdab4c919458b3090d7b6ec29030ea2ad Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 3 Oct 2023 13:31:43 -0400 Subject: [PATCH 25/36] Create the initial database migration --- Cargo.lock | 1 + file-service/Cargo.toml | 1 + .../migrations/20231003154201_initial_auth_db.sql | 12 ++++++++++++ flake.nix | 1 + 4 files changed, 15 insertions(+) create mode 100644 file-service/migrations/20231003154201_initial_auth_db.sql diff --git a/Cargo.lock b/Cargo.lock index 41a60d5..f3f6976 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -743,6 +743,7 @@ dependencies = [ name = "file-service" version = "0.1.0" dependencies = [ + "base64ct", "build_html", "bytes", "chrono", diff --git a/file-service/Cargo.toml b/file-service/Cargo.toml index c1571fb..e026c4d 100644 --- a/file-service/Cargo.toml +++ b/file-service/Cargo.toml @@ -27,6 +27,7 @@ thiserror = "1.0.20" tokio = { version = "1", features = [ "full" ] } uuid = { version = "0.4", features = [ "serde", "v4" ] } warp = { version = "0.3" } +base64ct = { version = "1", features = [ "alloc" ] } [dev-dependencies] cool_asserts = { version = "2" } diff --git a/file-service/migrations/20231003154201_initial_auth_db.sql b/file-service/migrations/20231003154201_initial_auth_db.sql new file mode 100644 index 0000000..f049c62 --- /dev/null +++ b/file-service/migrations/20231003154201_initial_auth_db.sql @@ -0,0 +1,12 @@ +-- Add migration script here +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY NOT NULL, + username TEXT NOT NULL, + token TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS session_tokens ( + token TEXT NOT NULL, + user INTEGER, + FOREIGN KEY(user) REFERENCES user(id) +); diff --git a/flake.nix b/flake.nix index 3a4d491..1eab404 100644 --- a/flake.nix +++ b/flake.nix @@ -43,6 +43,7 @@ pkgs.cargo-nextest pkgs.crate2nix pkgs.wasm-pack + pkgs.sqlx-cli typeshare.packages."x86_64-linux".default ]; LIBCLANG_PATH="${pkgs.llvmPackages.libclang.lib}/lib"; -- 2.44.1 From 4a7d74122462a3e7cfae7a850eb434bb1b705406 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 3 Oct 2023 13:31:58 -0400 Subject: [PATCH 26/36] Add the ability to create and list users --- file-service/src/store/mod.rs | 94 ++++++++++++++++++++++++++++++++--- 1 file changed, 86 insertions(+), 8 deletions(-) diff --git a/file-service/src/store/mod.rs b/file-service/src/store/mod.rs index 907c4ee..96445f0 100644 --- a/file-service/src/store/mod.rs +++ b/file-service/src/store/mod.rs @@ -1,9 +1,15 @@ +use base64ct::{Base64, Encoding}; use serde::{Deserialize, Serialize}; -use sqlx::sqlite::SqlitePool; +use sha2::{Digest, Sha256}; +use sqlx::{ + sqlite::{SqlitePool, SqliteRow}, + Executor, Row, +}; use std::collections::HashSet; use std::{ops::Deref, path::PathBuf, sync::Arc}; use thiserror::Error; use tokio::sync::RwLock; +use uuid::Uuid; mod filehandle; mod fileinfo; @@ -62,7 +68,13 @@ pub enum ReadFileError { #[derive(Debug, Error)] pub enum AuthError { #[error("database failed")] - SqlError, + SqlError(sqlx::Error), +} + +impl From for AuthError { + fn from(err: sqlx::Error) -> AuthError { + AuthError::SqlError(err) + } } #[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)] @@ -80,13 +92,13 @@ impl From<&str> for Username { } } -impl From for PathBuf { +impl From for String { fn from(s: Username) -> Self { Self::from(&s) } } -impl From<&Username> for PathBuf { +impl From<&Username> for String { fn from(s: &Username) -> Self { let Username(s) = s; Self::from(s) @@ -100,6 +112,13 @@ impl Deref for Username { } } +impl sqlx::FromRow<'_, SqliteRow> for Username { + fn from_row(row: &SqliteRow) -> sqlx::Result { + let name: String = row.try_get("username")?; + Ok(Username::from(name)) + } +} + #[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)] pub struct AuthToken(String); @@ -263,19 +282,47 @@ pub struct AuthDB { impl AuthDB { pub async fn new(path: PathBuf) -> Result { + let migrator = sqlx::migrate!("./migrations"); let pool = SqlitePool::connect(&format!("sqlite://{}", path.to_str().unwrap())).await?; + migrator.run(&pool).await?; Ok(Self { pool }) } + async fn add_user(&self, username: Username) -> Result { + let auth_plaintext = format!("{}:{}", Uuid::new_v4(), username.to_string()); + + let mut hasher = Sha256::new(); + hasher.update(auth_plaintext); + let auth_token = Base64::encode_string(&hasher.finalize()); + + let _ = sqlx::query("INSERT INTO users (username, token) VALUES ($1, $2)") + .bind(username.to_string()) + .bind(auth_token.clone()) + .execute(&self.pool) + .await?; + + Ok(AuthToken::from(auth_token)) + } + + async fn list_users(&self) -> Result, AuthError> { + let usernames = sqlx::query_as::<_, Username>("SELECT (username) FROM users") + .fetch_all(&self.pool) + .await?; + /* + let usernames = result + .into_iter() + .map(|row| Username::from(row.column(0))) + .collect::>(); + */ + + Ok(usernames) + } + async fn auth_token(&self, _token: AuthToken) -> Result { unimplemented!() } async fn auth_session(&self, _token: SessionToken) -> Result { - /* - let conn = self.pool.acquire().await.map_err(|_| AuthError::SqlError)?; - conn.transaction(|tr| {}) - */ unimplemented!() } } @@ -417,3 +464,34 @@ mod test { }); } } + +#[cfg(test)] +mod authdb_test { + use super::*; + use cool_asserts::assert_matches; + + #[tokio::test] + async fn can_create_and_list_users() { + let db = AuthDB::new(PathBuf::from(":memory:")) + .await + .expect("a memory-only database will be created"); + let _ = db + .add_user(Username::from("savanni")) + .await + .expect("user to be created"); + assert_matches!(db.list_users().await, Ok(names) => { + let names = names.into_iter().collect::>(); + assert!(names.contains(&Username::from("savanni"))); + }) + } + + #[test] + fn can_authenticate_token() { + unimplemented!() + } + + #[test] + fn can_validate_session_token() { + unimplemented!() + } +} -- 2.44.1 From 5e4db0032b8f76632926de85a35f96f39fb69c20 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 3 Oct 2023 13:56:55 -0400 Subject: [PATCH 27/36] Add session checks --- .../20231003154201_initial_auth_db.sql | 6 +- file-service/src/store/mod.rs | 122 +++++++++++++++--- 2 files changed, 104 insertions(+), 24 deletions(-) diff --git a/file-service/migrations/20231003154201_initial_auth_db.sql b/file-service/migrations/20231003154201_initial_auth_db.sql index f049c62..46a16bb 100644 --- a/file-service/migrations/20231003154201_initial_auth_db.sql +++ b/file-service/migrations/20231003154201_initial_auth_db.sql @@ -5,8 +5,8 @@ CREATE TABLE IF NOT EXISTS users ( token TEXT NOT NULL ); -CREATE TABLE IF NOT EXISTS session_tokens ( +CREATE TABLE IF NOT EXISTS sessions ( token TEXT NOT NULL, - user INTEGER, - FOREIGN KEY(user) REFERENCES user(id) + user_id INTEGER, + FOREIGN KEY(user_id) REFERENCES users(id) ); diff --git a/file-service/src/store/mod.rs b/file-service/src/store/mod.rs index 96445f0..7a60a0e 100644 --- a/file-service/src/store/mod.rs +++ b/file-service/src/store/mod.rs @@ -67,6 +67,12 @@ pub enum ReadFileError { #[derive(Debug, Error)] pub enum AuthError { + #[error("authentication token is duplicated")] + DuplicateAuthToken, + + #[error("session token is duplicated")] + DuplicateSessionToken, + #[error("database failed")] SqlError(sqlx::Error), } @@ -250,11 +256,11 @@ impl App { } } - pub async fn auth_token(&self, token: AuthToken) -> Result { + pub async fn auth_token(&self, token: AuthToken) -> Result, AuthError> { self.authdb.read().await.auth_token(token).await } - pub async fn auth_session(&self, token: SessionToken) -> Result { + pub async fn auth_session(&self, token: SessionToken) -> Result, AuthError> { self.authdb.read().await.auth_session(token).await } @@ -289,10 +295,9 @@ impl AuthDB { } async fn add_user(&self, username: Username) -> Result { - let auth_plaintext = format!("{}:{}", Uuid::new_v4(), username.to_string()); - let mut hasher = Sha256::new(); - hasher.update(auth_plaintext); + hasher.update(Uuid::new_v4().hyphenated().to_string()); + hasher.update(username.to_string()); let auth_token = Base64::encode_string(&hasher.finalize()); let _ = sqlx::query("INSERT INTO users (username, token) VALUES ($1, $2)") @@ -308,22 +313,57 @@ impl AuthDB { let usernames = sqlx::query_as::<_, Username>("SELECT (username) FROM users") .fetch_all(&self.pool) .await?; - /* - let usernames = result - .into_iter() - .map(|row| Username::from(row.column(0))) - .collect::>(); - */ Ok(usernames) } - async fn auth_token(&self, _token: AuthToken) -> Result { - unimplemented!() + async fn auth_token(&self, token: AuthToken) -> Result, AuthError> { + let results = sqlx::query("SELECT * FROM users WHERE token = $1") + .bind(token.to_string()) + .fetch_all(&self.pool) + .await?; + + if results.len() > 1 { + return Err(AuthError::DuplicateAuthToken); + } + + if results.len() == 0 { + return Ok(None); + } + + let user_id: i64 = results[0].try_get("id")?; + + let mut hasher = Sha256::new(); + hasher.update(Uuid::new_v4().hyphenated().to_string()); + hasher.update(token.to_string()); + let session_token = Base64::encode_string(&hasher.finalize()); + + let _ = sqlx::query("INSERT INTO sessions (token, user_id) VALUES ($1, $2)") + .bind(session_token.clone()) + .bind(user_id) + .execute(&self.pool) + .await?; + + Ok(Some(SessionToken::from(session_token))) } - async fn auth_session(&self, _token: SessionToken) -> Result { - unimplemented!() + async fn auth_session(&self, token: SessionToken) -> Result, AuthError> { + let rows = sqlx::query( + "SELECT users.username FROM sessions INNER JOIN users ON sessions.user_id = users.id WHERE sessions.token = $1", + ) + .bind(token.to_string()) + .fetch_all(&self.pool) + .await?; + if rows.len() > 1 { + return Err(AuthError::DuplicateSessionToken); + } + + if rows.len() == 0 { + return Ok(None); + } + + let username: String = rows[0].try_get("username")?; + Ok(Some(Username::from(username))) } } @@ -485,13 +525,53 @@ mod authdb_test { }) } - #[test] - fn can_authenticate_token() { - unimplemented!() + #[tokio::test] + async fn unknown_auth_token_returns_nothing() { + let db = AuthDB::new(PathBuf::from(":memory:")) + .await + .expect("a memory-only database will be created"); + let _ = db + .add_user(Username::from("savanni")) + .await + .expect("user to be created"); + + let token = AuthToken::from("0000000000"); + + assert_matches!(db.auth_token(token).await, Ok(None)); } - #[test] - fn can_validate_session_token() { - unimplemented!() + #[tokio::test] + async fn auth_token_becomes_session_token() { + let db = AuthDB::new(PathBuf::from(":memory:")) + .await + .expect("a memory-only database will be created"); + let token = db + .add_user(Username::from("savanni")) + .await + .expect("user to be created"); + + assert_matches!(db.auth_token(token).await, Ok(_)); + } + + #[tokio::test] + async fn can_validate_session_token() { + let db = AuthDB::new(PathBuf::from(":memory:")) + .await + .expect("a memory-only database will be created"); + let token = db + .add_user(Username::from("savanni")) + .await + .expect("user to be created"); + let session = db + .auth_token(token) + .await + .expect("token authentication should succeed") + .expect("session token should be found"); + + assert_matches!( + db.auth_session(session).await, + Ok(Some(username)) => { + assert_eq!(username, Username::from("savanni")); + }); } } -- 2.44.1 From 491c80b42b18b0eae7db300a0125a9d1be5fd3f1 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 3 Oct 2023 16:18:19 -0400 Subject: [PATCH 28/36] Split out a support library --- file-service/Cargo.toml | 11 ++++++++ file-service/src/handlers.rs | 4 +-- file-service/src/lib.rs | 6 ++++ file-service/src/main.rs | 53 ++++++++++++++++++++++++++++++----- file-service/src/pages.rs | 6 ++-- file-service/src/store/mod.rs | 47 +++---------------------------- 6 files changed, 71 insertions(+), 56 deletions(-) create mode 100644 file-service/src/lib.rs diff --git a/file-service/Cargo.toml b/file-service/Cargo.toml index e026c4d..0aabb3c 100644 --- a/file-service/Cargo.toml +++ b/file-service/Cargo.toml @@ -6,6 +6,16 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +auth-cli = [ "clap" ] + +[[bin]] +name = "auth-cli" +path = "src/bin/cli.rs" +required-features = [ "auth-cli" ] + +[target.auth-cli.dependencies] + [dependencies] build_html = { version = "2" } bytes = { version = "1" } @@ -28,6 +38,7 @@ tokio = { version = "1", features = [ "full" ] } uuid = { version = "0.4", features = [ "serde", "v4" ] } warp = { version = "0.3" } base64ct = { version = "1", features = [ "alloc" ] } +clap = { version = "4", features = [ "derive" ], optional = true } [dev-dependencies] cool_asserts = { version = "2" } diff --git a/file-service/src/handlers.rs b/file-service/src/handlers.rs index 7d8fb22..824d1da 100644 --- a/file-service/src/handlers.rs +++ b/file-service/src/handlers.rs @@ -1,9 +1,9 @@ use build_html::Html; use http::{Error, StatusCode}; -use std::{collections::HashMap, future::Future}; +use std::collections::HashMap; use warp::http::Response; -use crate::{pages, App, AuthToken, FileHandle, FileId, FileInfo, ReadFileError, SessionToken}; +use crate::{pages, App, AuthToken, FileId, FileInfo, ReadFileError, SessionToken}; pub async fn handle_index( app: App, diff --git a/file-service/src/lib.rs b/file-service/src/lib.rs new file mode 100644 index 0000000..a19f54c --- /dev/null +++ b/file-service/src/lib.rs @@ -0,0 +1,6 @@ +mod store; + +pub use store::{ + AuthDB, AuthError, AuthToken, FileHandle, FileId, FileInfo, ReadFileError, SessionToken, Store, + Username, WriteFileError, +}; diff --git a/file-service/src/main.rs b/file-service/src/main.rs index 6dd12f5..8c885de 100644 --- a/file-service/src/main.rs +++ b/file-service/src/main.rs @@ -8,25 +8,25 @@ use http::status::StatusCode; use bytes::Buf; use futures_util::StreamExt; use std::{ - collections::HashMap, + collections::HashSet, convert::Infallible, io::Read, net::{IpAddr, Ipv4Addr, SocketAddr}, path::PathBuf, - sync::{Arc, RwLock}, + sync::Arc, }; -use store::Username; +use tokio::sync::RwLock; use warp::{filters::multipart::Part, Filter, Rejection}; mod handlers; mod html; mod pages; -mod store; -pub use handlers::handle_index; -pub use store::{ - App, AuthDB, AuthToken, FileHandle, FileId, FileInfo, ReadFileError, SessionToken, Store, +pub use file_service::{ + AuthDB, AuthError, AuthToken, FileHandle, FileId, FileInfo, ReadFileError, SessionToken, Store, + Username, WriteFileError, }; +pub use handlers::handle_index; /* async fn authenticate_user(app: App, auth_token: String) -> Result { @@ -112,6 +112,45 @@ async fn handle_upload( } */ +#[derive(Clone)] +pub struct App { + authdb: Arc>, + store: Arc>, +} + +impl App { + pub fn new(authdb: AuthDB, store: Store) -> Self { + Self { + authdb: Arc::new(RwLock::new(authdb)), + store: Arc::new(RwLock::new(store)), + } + } + + pub async fn auth_token(&self, token: AuthToken) -> Result, AuthError> { + self.authdb.read().await.auth_token(token).await + } + + pub async fn auth_session(&self, token: SessionToken) -> Result, AuthError> { + self.authdb.read().await.auth_session(token).await + } + + pub async fn list_files(&self) -> Result, ReadFileError> { + self.store.read().await.list_files() + } + + pub async fn get_file(&self, id: &FileId) -> Result { + self.store.read().await.get_file(id) + } + + pub async fn add_file( + &self, + filename: String, + content: Vec, + ) -> Result { + self.store.write().await.add_file(filename, content) + } +} + fn with_app(app: App) -> impl Filter + Clone { warp::any().map(move || app.clone()) } diff --git a/file-service/src/pages.rs b/file-service/src/pages.rs index 45d5fdf..ba1b586 100644 --- a/file-service/src/pages.rs +++ b/file-service/src/pages.rs @@ -1,8 +1,6 @@ -use crate::{ - html::*, - store::{FileHandle, FileId, ReadFileError}, -}; +use crate::html::*; use build_html::{self, Container, ContainerType, Html, HtmlContainer}; +use file_service::{FileHandle, FileId, ReadFileError}; pub fn auth(_message: Option) -> build_html::HtmlPage { build_html::HtmlPage::new() diff --git a/file-service/src/store/mod.rs b/file-service/src/store/mod.rs index 7a60a0e..b5a1eeb 100644 --- a/file-service/src/store/mod.rs +++ b/file-service/src/store/mod.rs @@ -242,45 +242,6 @@ impl FileRoot for Context { } } -#[derive(Clone)] -pub struct App { - authdb: Arc>, - store: Arc>, -} - -impl App { - pub fn new(authdb: AuthDB, store: Store) -> Self { - Self { - authdb: Arc::new(RwLock::new(authdb)), - store: Arc::new(RwLock::new(store)), - } - } - - pub async fn auth_token(&self, token: AuthToken) -> Result, AuthError> { - self.authdb.read().await.auth_token(token).await - } - - pub async fn auth_session(&self, token: SessionToken) -> Result, AuthError> { - self.authdb.read().await.auth_session(token).await - } - - pub async fn list_files(&self) -> Result, ReadFileError> { - self.store.read().await.list_files() - } - - pub async fn get_file(&self, id: &FileId) -> Result { - self.store.read().await.get_file(id) - } - - pub async fn add_file( - &self, - filename: String, - content: Vec, - ) -> Result { - self.store.write().await.add_file(filename, content) - } -} - #[derive(Clone)] pub struct AuthDB { pool: SqlitePool, @@ -294,7 +255,7 @@ impl AuthDB { Ok(Self { pool }) } - async fn add_user(&self, username: Username) -> Result { + pub async fn add_user(&self, username: Username) -> Result { let mut hasher = Sha256::new(); hasher.update(Uuid::new_v4().hyphenated().to_string()); hasher.update(username.to_string()); @@ -309,7 +270,7 @@ impl AuthDB { Ok(AuthToken::from(auth_token)) } - async fn list_users(&self) -> Result, AuthError> { + pub async fn list_users(&self) -> Result, AuthError> { let usernames = sqlx::query_as::<_, Username>("SELECT (username) FROM users") .fetch_all(&self.pool) .await?; @@ -317,7 +278,7 @@ impl AuthDB { Ok(usernames) } - async fn auth_token(&self, token: AuthToken) -> Result, AuthError> { + pub async fn auth_token(&self, token: AuthToken) -> Result, AuthError> { let results = sqlx::query("SELECT * FROM users WHERE token = $1") .bind(token.to_string()) .fetch_all(&self.pool) @@ -347,7 +308,7 @@ impl AuthDB { Ok(Some(SessionToken::from(session_token))) } - async fn auth_session(&self, token: SessionToken) -> Result, AuthError> { + pub async fn auth_session(&self, token: SessionToken) -> Result, AuthError> { let rows = sqlx::query( "SELECT users.username FROM sessions INNER JOIN users ON sessions.user_id = users.id WHERE sessions.token = $1", ) -- 2.44.1 From f53c7200e644441f65d5222b752d7af19b0bbdc4 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 3 Oct 2023 16:32:00 -0400 Subject: [PATCH 29/36] Add a CLI application for user management --- .gitignore | 1 + Cargo.lock | 111 +++++++++++++++++++++++++++++++++++- file-service/Cargo.toml | 8 +++ file-service/src/bin/cli.rs | 44 ++++++++++++++ file-service/src/main.rs | 4 +- 5 files changed, 165 insertions(+), 3 deletions(-) create mode 100644 file-service/src/bin/cli.rs diff --git a/.gitignore b/.gitignore index 26ce53f..021583e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ node_modules dist result *.tgz +file-service/auth.sqlite diff --git a/Cargo.lock b/Cargo.lock index f3f6976..39219d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,6 +74,54 @@ dependencies = [ "winapi", ] +[[package]] +name = "anstream" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +dependencies = [ + "anstyle", + "windows-sys", +] + [[package]] name = "anyhow" version = "1.0.75" @@ -331,12 +379,52 @@ dependencies = [ "ansi_term", "atty", "bitflags 1.3.2", - "strsim", + "strsim 0.8.0", "textwrap", "unicode-width", "vec_map", ] +[[package]] +name = "clap" +version = "4.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim 0.10.0", +] + +[[package]] +name = "clap_derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.29", +] + +[[package]] +name = "clap_lex" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" + [[package]] name = "cloudabi" version = "0.0.3" @@ -352,6 +440,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "config" version = "0.1.0" @@ -747,6 +841,7 @@ dependencies = [ "build_html", "bytes", "chrono", + "clap 4.4.6", "cool_asserts", "futures-util", "hex-string", @@ -2421,7 +2516,7 @@ name = "orizentic" version = "1.0.1" dependencies = [ "chrono", - "clap", + "clap 2.34.0", "itertools 0.10.5", "jsonwebtoken", "serde 1.0.188", @@ -3679,6 +3774,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "subtle" version = "2.5.0" @@ -4233,6 +4334,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uuid" version = "0.4.0" diff --git a/file-service/Cargo.toml b/file-service/Cargo.toml index 0aabb3c..0766c1e 100644 --- a/file-service/Cargo.toml +++ b/file-service/Cargo.toml @@ -9,6 +9,14 @@ edition = "2018" [features] auth-cli = [ "clap" ] +[lib] +name = "file_service" +path = "src/lib.rs" + +[[bin]] +name = "file-service" +path = "src/main.rs" + [[bin]] name = "auth-cli" path = "src/bin/cli.rs" diff --git a/file-service/src/bin/cli.rs b/file-service/src/bin/cli.rs new file mode 100644 index 0000000..5c1d3b5 --- /dev/null +++ b/file-service/src/bin/cli.rs @@ -0,0 +1,44 @@ +use clap::{Parser, Subcommand}; +use file_service::{AuthDB, Username}; +use std::path::PathBuf; + +#[derive(Subcommand, Debug)] +enum Commands { + AddUser { username: String }, + DeleteUser { username: String }, + ListUsers, +} + +#[derive(Parser, Debug)] +struct Args { + #[command(subcommand)] + command: Commands, +} + +#[tokio::main] +pub async fn main() { + let args = Args::parse(); + let authdb = AuthDB::new(PathBuf::from(&std::env::var("AUTHDB").unwrap())) + .await + .expect("to be able to open the database"); + + match args.command { + Commands::AddUser { username } => { + match authdb.add_user(Username::from(username.clone())).await { + Ok(token) => { + println!( + "User {} created. Auth token: {}", + username, + token.to_string() + ); + } + Err(err) => { + println!("Could not create user {}", username); + println!("\tError: {:?}", err); + } + } + } + Commands::DeleteUser { username } => {} + Commands::ListUsers => {} + } +} diff --git a/file-service/src/main.rs b/file-service/src/main.rs index 8c885de..7f5a3db 100644 --- a/file-service/src/main.rs +++ b/file-service/src/main.rs @@ -175,7 +175,9 @@ fn with_session() -> impl Filter + pub async fn main() { pretty_env_logger::init(); - let authdb = AuthDB::new(PathBuf::from(":memory:")).await.unwrap(); + let authdb = AuthDB::new(PathBuf::from(&std::env::var("AUTHDB").unwrap())) + .await + .unwrap(); let store = Store::new(PathBuf::from(&std::env::var("FILE_SHARE_DIR").unwrap())); let app = App::new(authdb, store); -- 2.44.1 From b3bfa846912ff6f0e0cc1cbd95653f28e597dd9b Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 3 Oct 2023 17:04:35 -0400 Subject: [PATCH 30/36] Validate the session token A previous commit added authentication token checks. Auth tokens are replaced with session tokens, which can (and should) expire. This commit validates sessions, which now allows access to gated operations. --- .gitignore | 4 ++- Cargo.lock | 51 ++++++++++++++++++++++++++++++++--- file-service/Cargo.toml | 5 ++-- file-service/src/handlers.rs | 19 ++++++++++--- file-service/src/main.rs | 34 +++++++++++++++++------ file-service/src/store/mod.rs | 15 ++++++----- 6 files changed, 104 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index 021583e..4539abe 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ node_modules dist result *.tgz -file-service/auth.sqlite +file-service/*.sqlite +file-service/*.sqlite-shm +file-service/*.sqlite-wal diff --git a/Cargo.lock b/Cargo.lock index 39219d7..519c7fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -416,7 +416,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -471,6 +471,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" +[[package]] +name = "cookie" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" +dependencies = [ + "time 0.3.29", + "version_check 0.9.4", +] + [[package]] name = "cool_asserts" version = "2.0.3" @@ -661,6 +671,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" + [[package]] name = "digest" version = "0.10.7" @@ -842,6 +858,7 @@ dependencies = [ "bytes", "chrono", "clap 4.4.6", + "cookie", "cool_asserts", "futures-util", "hex-string", @@ -1744,7 +1761,7 @@ dependencies = [ "log 0.3.9", "mime 0.2.6", "num_cpus", - "time", + "time 0.1.45", "traitobject", "typeable", "unicase 1.4.2", @@ -2178,7 +2195,7 @@ checksum = "6c9172cb4c2f6c52117e25570983edcbb322f130b1031ae5d5d6b1abe7eeb493" dependencies = [ "iron", "log 0.3.9", - "time", + "time 0.1.45", ] [[package]] @@ -3942,6 +3959,34 @@ dependencies = [ "winapi", ] +[[package]] +name = "time" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe" +dependencies = [ + "deranged", + "itoa", + "serde 1.0.188", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +dependencies = [ + "time-core", +] + [[package]] name = "tinystr" version = "0.7.4" diff --git a/file-service/Cargo.toml b/file-service/Cargo.toml index 0766c1e..8238be0 100644 --- a/file-service/Cargo.toml +++ b/file-service/Cargo.toml @@ -25,9 +25,12 @@ required-features = [ "auth-cli" ] [target.auth-cli.dependencies] [dependencies] +base64ct = { version = "1", features = [ "alloc" ] } build_html = { version = "2" } bytes = { version = "1" } chrono = { version = "0.4", features = ["serde"] } +clap = { version = "4", features = [ "derive" ], optional = true } +cookie = { version = "0.17" } futures-util = { version = "0.3" } hex-string = "0.1.0" http = { version = "0.2" } @@ -45,8 +48,6 @@ thiserror = "1.0.20" tokio = { version = "1", features = [ "full" ] } uuid = { version = "0.4", features = [ "serde", "v4" ] } warp = { version = "0.3" } -base64ct = { version = "1", features = [ "alloc" ] } -clap = { version = "4", features = [ "derive" ], optional = true } [dev-dependencies] cool_asserts = { version = "2" } diff --git a/file-service/src/handlers.rs b/file-service/src/handlers.rs index 824d1da..6f3fdc7 100644 --- a/file-service/src/handlers.rs +++ b/file-service/src/handlers.rs @@ -10,9 +10,9 @@ pub async fn handle_index( token: Option, ) -> Result, Error> { match token { - Some(token) => match app.auth_session(token).await { + Some(token) => match app.validate_session(token).await { Ok(_) => render_gallery_page(app).await, - Err(err) => render_auth_page(Some(format!("authentication refused: {:?}", err))), + Err(err) => render_auth_page(Some(format!("session expired: {:?}", err))), }, None => render_auth_page(None), } @@ -76,10 +76,21 @@ pub async fn handle_auth( ) -> Result, Error> { match form.get("token") { Some(token) => match app - .auth_token(AuthToken::from(AuthToken::from(token.clone()))) + .authenticate(AuthToken::from(AuthToken::from(token.clone()))) .await { - Ok(_session_token) => render_gallery_page(app).await, + Ok(Some(session_token)) => Response::builder() + .header("location", "/") + .header( + "set-cookie", + format!( + "session={}; Secure; HttpOnly; SameSite=Strict", + session_token.to_string() + ), + ) + .status(StatusCode::SEE_OTHER) + .body("".to_owned()), + Ok(None) => render_auth_page(Some(format!("no user found"))), Err(_) => render_auth_page(Some(format!("invalid auth token"))), }, None => render_auth_page(Some(format!("no token available"))), diff --git a/file-service/src/main.rs b/file-service/src/main.rs index 7f5a3db..6918cbc 100644 --- a/file-service/src/main.rs +++ b/file-service/src/main.rs @@ -6,9 +6,10 @@ use http::status::StatusCode; // use mustache::{compile_path, Template}; // use orizentic::{Permissions, ResourceName, Secret}; use bytes::Buf; +use cookie::Cookie; use futures_util::StreamExt; use std::{ - collections::HashSet, + collections::{HashMap, HashSet}, convert::Infallible, io::Read, net::{IpAddr, Ipv4Addr, SocketAddr}, @@ -126,12 +127,15 @@ impl App { } } - pub async fn auth_token(&self, token: AuthToken) -> Result, AuthError> { - self.authdb.read().await.auth_token(token).await + pub async fn authenticate(&self, token: AuthToken) -> Result, AuthError> { + self.authdb.read().await.authenticate(token).await } - pub async fn auth_session(&self, token: SessionToken) -> Result, AuthError> { - self.authdb.read().await.auth_session(token).await + pub async fn validate_session( + &self, + token: SessionToken, + ) -> Result, AuthError> { + self.authdb.read().await.validate_session(token).await } pub async fn list_files(&self) -> Result, ReadFileError> { @@ -159,9 +163,23 @@ fn maybe_with_session() -> impl Filter,), Error { warp::any() .and(warp::header::optional::("cookie")) - .map(|cookies| { - println!("cookies retrieved: {:?}", cookies); - None + .map(|cookies| match cookies { + Some(cookies) => { + let c = Cookie::split_parse(cookies) + .collect::, cookie::ParseError>>(); + match c { + Ok(cookies) => { + for c in cookies { + if c.name() == "session" { + return Some(SessionToken::from(c.value())); + } + } + None + } + Err(_) => None, + } + } + None => None, }) } diff --git a/file-service/src/store/mod.rs b/file-service/src/store/mod.rs index b5a1eeb..119f02a 100644 --- a/file-service/src/store/mod.rs +++ b/file-service/src/store/mod.rs @@ -278,7 +278,7 @@ impl AuthDB { Ok(usernames) } - pub async fn auth_token(&self, token: AuthToken) -> Result, AuthError> { + pub async fn authenticate(&self, token: AuthToken) -> Result, AuthError> { let results = sqlx::query("SELECT * FROM users WHERE token = $1") .bind(token.to_string()) .fetch_all(&self.pool) @@ -308,7 +308,10 @@ impl AuthDB { Ok(Some(SessionToken::from(session_token))) } - pub async fn auth_session(&self, token: SessionToken) -> Result, AuthError> { + pub async fn validate_session( + &self, + token: SessionToken, + ) -> Result, AuthError> { let rows = sqlx::query( "SELECT users.username FROM sessions INNER JOIN users ON sessions.user_id = users.id WHERE sessions.token = $1", ) @@ -498,7 +501,7 @@ mod authdb_test { let token = AuthToken::from("0000000000"); - assert_matches!(db.auth_token(token).await, Ok(None)); + assert_matches!(db.authenticate(token).await, Ok(None)); } #[tokio::test] @@ -511,7 +514,7 @@ mod authdb_test { .await .expect("user to be created"); - assert_matches!(db.auth_token(token).await, Ok(_)); + assert_matches!(db.authenticate(token).await, Ok(_)); } #[tokio::test] @@ -524,13 +527,13 @@ mod authdb_test { .await .expect("user to be created"); let session = db - .auth_token(token) + .authenticate(token) .await .expect("token authentication should succeed") .expect("session token should be found"); assert_matches!( - db.auth_session(session).await, + db.validate_session(session).await, Ok(Some(username)) => { assert_eq!(username, Username::from("savanni")); }); -- 2.44.1 From 9bb32a378cf7f5a73f94f063d17f0b71a28361c2 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 3 Oct 2023 17:30:43 -0400 Subject: [PATCH 31/36] Validate the session token with file uploads File uploads now check the session token before continuing. Resolves: https://www.pivotaltracker.com/story/show/186174680 --- file-service/src/handlers.rs | 17 ++++++------ file-service/src/main.rs | 54 +++++++++++++++++++----------------- 2 files changed, 38 insertions(+), 33 deletions(-) diff --git a/file-service/src/handlers.rs b/file-service/src/handlers.rs index 6f3fdc7..b9004ee 100644 --- a/file-service/src/handlers.rs +++ b/file-service/src/handlers.rs @@ -97,14 +97,15 @@ pub async fn handle_auth( } } -pub async fn handle_upload( - _app: App, - _token: SessionToken, -) -> Result, Error> { - println!("handle_upload"); - Response::builder() - .status(StatusCode::NOT_IMPLEMENTED) - .body("".to_owned()) +pub async fn handle_upload(app: App, token: SessionToken) -> Result, Error> { + match app.validate_session(token).await { + Ok(Some(_)) => Response::builder() + .status(StatusCode::NOT_IMPLEMENTED) + .body("".to_owned()), + _ => Response::builder() + .status(StatusCode::UNAUTHORIZED) + .body("".to_owned()), + } } fn serve_file( diff --git a/file-service/src/main.rs b/file-service/src/main.rs index 6918cbc..7cc8711 100644 --- a/file-service/src/main.rs +++ b/file-service/src/main.rs @@ -159,26 +159,25 @@ fn with_app(app: App) -> impl Filter + Clo warp::any().map(move || app.clone()) } +fn parse_cookies(cookie_str: &str) -> Result, cookie::ParseError> { + Cookie::split_parse(cookie_str) + .map(|c| c.map(|c| (c.name().to_owned(), c.value().to_owned()))) + .collect::, cookie::ParseError>>() +} + +fn get_session_token(cookies: HashMap) -> Option { + cookies + .get("session") + .cloned() + .and_then(|session| Some(SessionToken::from(session))) +} + fn maybe_with_session() -> impl Filter,), Error = Rejection> + Copy { warp::any() .and(warp::header::optional::("cookie")) - .map(|cookies| match cookies { - Some(cookies) => { - let c = Cookie::split_parse(cookies) - .collect::, cookie::ParseError>>(); - match c { - Ok(cookies) => { - for c in cookies { - if c.name() == "session" { - return Some(SessionToken::from(c.value())); - } - } - None - } - Err(_) => None, - } - } + .map(|cookie_str: Option| match cookie_str { + Some(cookie_str) => parse_cookies(&cookie_str).ok().and_then(get_session_token), None => None, }) } @@ -186,7 +185,12 @@ fn maybe_with_session() -> impl Filter,), Error fn with_session() -> impl Filter + Copy { warp::any() .and(warp::header::("cookie")) - .map(|token: String| SessionToken::from(token)) + .and_then(|cookie_str: String| async move { + match parse_cookies(&cookie_str).ok().and_then(get_session_token) { + Some(session_token) => Ok(session_token), + None => Err(warp::reject()), + } + }) } #[tokio::main] @@ -220,7 +224,7 @@ pub async fn main() { .and(warp::filters::body::form()) .then(handle_auth); - let upload_handler = warp::path!("upload") + let upload_via_form = warp::path!("upload") .and(warp::post()) .and(with_app(app.clone())) .and(with_session()) @@ -239,13 +243,13 @@ pub async fn main() { .then(move |id, old_etags, app: App| file(app, id, old_etags)); let server = warp::serve( - root.or(auth).with(log), /* - root.or(auth) - .or(thumbnail) - .or(file) - .or(upload_handler) - .with(log), - */ + root.or(auth).or(upload_via_form).with(log), /* + root.or(auth) + .or(thumbnail) + .or(file) + .or(upload_handler) + .with(log), + */ ); server -- 2.44.1 From c38d680e5723eef0d77191628c7f9d8bdeade04d Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 3 Oct 2023 17:47:54 -0400 Subject: [PATCH 32/36] Handle file uploads with a validated session --- .gitignore | 1 + file-service/src/handlers.rs | 130 +++++++++++++++++++++++++++++++++-- file-service/src/main.rs | 83 ++-------------------- 3 files changed, 132 insertions(+), 82 deletions(-) diff --git a/.gitignore b/.gitignore index 4539abe..f8e5e3f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ result file-service/*.sqlite file-service/*.sqlite-shm file-service/*.sqlite-wal +file-service/var diff --git a/file-service/src/handlers.rs b/file-service/src/handlers.rs index b9004ee..afc0cbd 100644 --- a/file-service/src/handlers.rs +++ b/file-service/src/handlers.rs @@ -1,7 +1,12 @@ use build_html::Html; +use bytes::Buf; +use cookie::time::error::Format; +use file_service::WriteFileError; +use futures_util::StreamExt; use http::{Error, StatusCode}; use std::collections::HashMap; -use warp::http::Response; +use std::io::Read; +use warp::{filters::multipart::FormData, http::Response, multipart::Part}; use crate::{pages, App, AuthToken, FileId, FileInfo, ReadFileError, SessionToken}; @@ -97,11 +102,27 @@ pub async fn handle_auth( } } -pub async fn handle_upload(app: App, token: SessionToken) -> Result, Error> { +pub async fn handle_upload( + app: App, + token: SessionToken, + form: FormData, +) -> Result, Error> { match app.validate_session(token).await { - Ok(Some(_)) => Response::builder() - .status(StatusCode::NOT_IMPLEMENTED) - .body("".to_owned()), + Ok(Some(_)) => match process_file_upload(app, form).await { + Ok(_) => Response::builder() + .header("location", "/") + .status(StatusCode::SEE_OTHER) + .body("".to_owned()), + Err(UploadError::FilenameMissing) => Response::builder() + .status(StatusCode::BAD_REQUEST) + .body("filename is required for all files".to_owned()), + Err(UploadError::WriteFileError(err)) => Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(format!("could not write to the file system: {:?}", err)), + Err(UploadError::WarpError(err)) => Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(format!("error with the app framework: {:?}", err)), + }, _ => Response::builder() .status(StatusCode::UNAUTHORIZED) .body("".to_owned()), @@ -133,3 +154,102 @@ where }, } } + +async fn collect_multipart( + mut stream: warp::filters::multipart::FormData, +) -> Result, Option, Vec)>, warp::Error> { + let mut content: Vec<(Option, Option, Vec)> = Vec::new(); + + while let Some(part) = stream.next().await { + match part { + Ok(part) => content.push(collect_content(part).await.unwrap()), + Err(err) => return Err(err), + } + } + + Ok(content) +} + +async fn collect_content( + mut part: Part, +) -> Result<(Option, Option, Vec), String> { + let mut content: Vec = Vec::new(); + + while let Some(Ok(data)) = part.data().await { + let mut reader = data.reader(); + reader.read_to_end(&mut content).unwrap(); + } + + Ok(( + part.content_type().map(|s| s.to_owned()), + part.filename().map(|s| s.to_owned()), + content, + )) +} + +/* +async fn handle_upload( + form: warp::filters::multipart::FormData, + app: App, +) -> warp::http::Result> { + let files = collect_multipart(form).await; + match files { + Ok(files) => { + for (_, filename, content) in files { + match filename { + Some(filename) => { + app.add_file(filename, content).unwrap(); + } + None => { + return warp::http::Response::builder() + .status(StatusCode::BAD_REQUEST) + .body("".to_owned()) + } + } + } + } + Err(_err) => { + return warp::http::Response::builder() + .status(StatusCode::BAD_REQUEST) + .body("".to_owned()) + } + } + + // println!("file length: {:?}", files.map(|f| f.len())); + warp::http::Response::builder() + .header("location", "/") + .status(StatusCode::SEE_OTHER) + .body("".to_owned()) +} +*/ + +enum UploadError { + FilenameMissing, + WriteFileError(WriteFileError), + WarpError(warp::Error), +} + +impl From for UploadError { + fn from(err: WriteFileError) -> Self { + Self::WriteFileError(err) + } +} + +impl From for UploadError { + fn from(err: warp::Error) -> Self { + Self::WarpError(err) + } +} + +async fn process_file_upload(app: App, form: FormData) -> Result<(), UploadError> { + let files = collect_multipart(form).await?; + for (_, filename, content) in files { + match filename { + Some(filename) => { + app.add_file(filename, content).await?; + } + None => return Err(UploadError::FilenameMissing), + } + } + Ok(()) +} diff --git a/file-service/src/main.rs b/file-service/src/main.rs index 7cc8711..237abf8 100644 --- a/file-service/src/main.rs +++ b/file-service/src/main.rs @@ -42,75 +42,12 @@ async fn authenticate_user(app: App, auth_token: String) -> Result Result<(Option, Option, Vec), String> { - let mut content: Vec = Vec::new(); - - while let Some(Ok(data)) = part.data().await { - let mut reader = data.reader(); - reader.read_to_end(&mut content).unwrap(); - } - - Ok(( - part.content_type().map(|s| s.to_owned()), - part.filename().map(|s| s.to_owned()), - content, - )) -} */ /* -async fn collect_multipart( - mut stream: warp::filters::multipart::FormData, -) -> Result, Option, Vec)>, warp::Error> { - let mut content: Vec<(Option, Option, Vec)> = Vec::new(); - - while let Some(part) = stream.next().await { - match part { - Ok(part) => content.push(collect_content(part).await.unwrap()), - Err(err) => return Err(err), - } - } - - Ok(content) -} */ /* -async fn handle_upload( - form: warp::filters::multipart::FormData, - app: App, -) -> warp::http::Result> { - let files = collect_multipart(form).await; - match files { - Ok(files) => { - for (_, filename, content) in files { - match filename { - Some(filename) => { - app.add_file(filename, content).unwrap(); - } - None => { - return warp::http::Response::builder() - .status(StatusCode::BAD_REQUEST) - .body("".to_owned()) - } - } - } - } - Err(_err) => { - return warp::http::Response::builder() - .status(StatusCode::BAD_REQUEST) - .body("".to_owned()) - } - } - - // println!("file length: {:?}", files.map(|f| f.len())); - warp::http::Response::builder() - .header("location", "/") - .status(StatusCode::SEE_OTHER) - .body("".to_owned()) -} */ #[derive(Clone)] @@ -204,13 +141,6 @@ pub async fn main() { let app = App::new(authdb, store); - /* - let with_app = { - let app = app.clone(); - warp::any().map(move || app.clone()) - }; - */ - let log = warp::log("file_service"); let root = warp::path!() .and(warp::get()) @@ -228,6 +158,7 @@ pub async fn main() { .and(warp::post()) .and(with_app(app.clone())) .and(with_session()) + .and(warp::multipart::form()) .then(handle_upload); let thumbnail = warp::path!(String / "tn") @@ -243,13 +174,11 @@ pub async fn main() { .then(move |id, old_etags, app: App| file(app, id, old_etags)); let server = warp::serve( - root.or(auth).or(upload_via_form).with(log), /* - root.or(auth) - .or(thumbnail) - .or(file) - .or(upload_handler) - .with(log), - */ + root.or(auth) + .or(upload_via_form) + .or(thumbnail) + .or(file) + .with(log), ); server -- 2.44.1 From 1e110692829c997ded9b9a4b38509f95ae8d0751 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 3 Oct 2023 17:54:37 -0400 Subject: [PATCH 33/36] Remove old placeholder directories --- file-service/fixtures/.metadata/.placeholder | 0 file-service/fixtures/.thumbnails/.placeholder | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 file-service/fixtures/.metadata/.placeholder delete mode 100644 file-service/fixtures/.thumbnails/.placeholder diff --git a/file-service/fixtures/.metadata/.placeholder b/file-service/fixtures/.metadata/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/file-service/fixtures/.thumbnails/.placeholder b/file-service/fixtures/.thumbnails/.placeholder deleted file mode 100644 index e69de29..0000000 -- 2.44.1 From 2e7e159325a922b34d1347aa5c8cf3cb478e669f Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 3 Oct 2023 17:55:05 -0400 Subject: [PATCH 34/36] Remove an excess comment --- file-service/migrations/20231003154201_initial_auth_db.sql | 1 - 1 file changed, 1 deletion(-) diff --git a/file-service/migrations/20231003154201_initial_auth_db.sql b/file-service/migrations/20231003154201_initial_auth_db.sql index 46a16bb..509609e 100644 --- a/file-service/migrations/20231003154201_initial_auth_db.sql +++ b/file-service/migrations/20231003154201_initial_auth_db.sql @@ -1,4 +1,3 @@ --- Add migration script here CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY NOT NULL, username TEXT NOT NULL, -- 2.44.1 From f7403b43a3a5ed7958a31f60129b96171fe7a9c7 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 3 Oct 2023 17:56:17 -0400 Subject: [PATCH 35/36] Remove a legacy file --- file-service/src/cookies.rs | 61 ------------------------------------- 1 file changed, 61 deletions(-) delete mode 100644 file-service/src/cookies.rs diff --git a/file-service/src/cookies.rs b/file-service/src/cookies.rs deleted file mode 100644 index 3e5c45b..0000000 --- a/file-service/src/cookies.rs +++ /dev/null @@ -1,61 +0,0 @@ -use iron::headers; -use std::collections::HashMap; - -#[derive(Clone, Debug)] -pub struct Cookie { - pub name: String, - pub value: String, -} - -impl From<&str> for Cookie { - fn from(s: &str) -> Cookie { - let parts: Vec<&str> = s.split("=").collect(); - Cookie { - name: String::from(parts[0]), - value: String::from(parts[1]), - } - } -} - -impl From<&String> for Cookie { - fn from(s: &String) -> Cookie { - Cookie::from(s.as_str()) - } -} - -impl From for Cookie { - fn from(s: String) -> Cookie { - Cookie::from(s.as_str()) - } -} - -#[derive(Debug)] -pub struct CookieJar(HashMap); - -impl CookieJar { - pub fn new() -> CookieJar { - CookieJar(HashMap::new()) - } - - pub fn add_cookie(&mut self, name: String, value: Cookie) { - self.0.insert(name, value); - } - - pub fn lookup(&self, name: &str) -> Option<&Cookie> { - self.0.get(name) - } -} - -// Some(Cookie(["auth=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJhYzNhNDZjNi0zZmExLTRkMGEtYWYxMi1lN2QzZmVmZGM4NzgiLCJhdWQiOiJzYXZhbm5pIiwiZXhwIjoxNjIxMzUxNDM2LCJpc3MiOiJzYXZhbm5pIiwiaWF0IjoxNTg5NzI5MDM2LCJzdWIiOiJodHRwczovL3NhdmFubmkubHVtaW5lc2NlbnQtZHJlYW1zLmNvbS9maWxlLXNlcnZpY2UvIiwicGVybXMiOlsiYWRtaW4iXX0.8zjAbZ7Ut0d6EcDeyik39GKhXvH4qkMDdaiQVNKWiuM"])) -impl From<&headers::Cookie> for CookieJar { - fn from(c: &headers::Cookie) -> CookieJar { - let jar = CookieJar::new(); - - let headers::Cookie(cs) = c; - cs.iter().fold(jar, |mut jar, c_| { - let cookie = Cookie::from(c_); - jar.add_cookie(cookie.name.clone(), cookie); - jar - }) - } -} -- 2.44.1 From 2f6be84a4375f698fdd59230f751e38d5eac85d0 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 3 Oct 2023 17:56:38 -0400 Subject: [PATCH 36/36] Remove dead comments --- file-service/src/main.rs | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/file-service/src/main.rs b/file-service/src/main.rs index 237abf8..0489779 100644 --- a/file-service/src/main.rs +++ b/file-service/src/main.rs @@ -29,27 +29,6 @@ pub use file_service::{ }; pub use handlers::handle_index; -/* -async fn authenticate_user(app: App, auth_token: String) -> Result { - match app.auth_session(SessionToken::from(auth_token)).await { - Ok(username) => Ok(username), - Err(_) => Err(warp::reject::not_found()), - } -} -*/ - -/* -*/ - -/* -*/ - -/* -*/ - -/* -*/ - #[derive(Clone)] pub struct App { authdb: Arc>, -- 2.44.1