diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 75cdd725..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,60 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Build/Lint/Test Commands - -### Building -- Build debug version: `cargo build` -- Build release version: `cargo build --release` -- Run the application: `cargo run` - -### Testing -- Run all tests: `cargo test` -- Run a specific test: `cargo test test_name` -- Run tests for a specific module: `cargo test module_name` -- Run tests with feature flags: `cargo test --features test_utils` - -### Linting -- Run clippy linting: `cargo clippy` -- Format code: `cargo fmt` - -## Code Style Guidelines - -### Architecture -- This project follows a hexagonal/clean architecture pattern: - - `application`: Contains services (use cases), DTOs, and ports - - `domain`: Contains core entities, repositories (interfaces), and domain services - - `infrastructure`: Contains concrete implementations of repository interfaces - - `interfaces`: Contains HTTP/API handlers and routes - -### Error Handling -- Use the `DomainError` type for domain-level errors -- Use the `AppError` type for API/HTTP-level errors -- Use the `ErrorContext` trait to add context to errors from external crates -- Follow the error factory pattern for creating common error types - -### Naming Conventions -- Types and structs: PascalCase -- Functions and methods: snake_case -- Constants and statics: SCREAMING_SNAKE_CASE -- Modules and files: snake_case -- Use descriptive names that express intent - -### Testing -- Use mock objects for dependencies in unit tests -- Use the `#[tokio::test]` attribute for async tests -- Include both positive and negative test cases -- Follow the Arrange-Act-Assert pattern in tests - -### Imports -- Group imports by source: - 1. Standard library imports - 2. External crate imports - 3. Local crate imports (with `crate::` prefix) -- Use explicit imports (no glob imports except in tests) - -### Documentation -- Document public API functions and types with doc comments -- Include examples where helpful -- Document error cases and conditions \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 9eb082a6..97958621 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,20 +2,11 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aes" @@ -30,9 +21,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -43,12 +34,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -60,21 +45,21 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" dependencies = [ "derive_arbitrary", ] @@ -93,13 +78,12 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.22" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a194f9d963d8099596278594b3107448656ba73831c9d8c783e613ce86da64" +checksum = "d10e4f991a553474232bc0a31799f6d24b034a84c0971d80d2e2f78b2e576e40" dependencies = [ - "flate2", - "futures-core", - "memchr", + "compression-codecs", + "compression-core", "pin-project-lite", "tokio", ] @@ -128,9 +112,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", @@ -154,15 +138,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "axum" -version = "0.8.3" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de45108900e1f9b9242f7f2e254aa3e2c029c921c258fe9e6b4217eeebd54288" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" dependencies = [ "axum-core", "axum-macros", @@ -181,8 +165,7 @@ dependencies = [ "multer", "percent-encoding", "pin-project-lite", - "rustversion", - "serde", + "serde_core", "serde_json", "serde_path_to_error", "serde_urlencoded", @@ -196,9 +179,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.2" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", @@ -207,7 +190,6 @@ dependencies = [ "http-body-util", "mime", "pin-project-lite", - "rustversion", "sync_wrapper", "tower-layer", "tower-service", @@ -227,9 +209,9 @@ dependencies = [ [[package]] name = "axum-server" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "495c05f60d6df0093e8fb6e74aa5846a0ad06abaf96d76166283720bf740f8ab" +checksum = "c1ab4a3ec9ea8a657c72d99a03a824af695bd0fb5ec639ccbd9cd3543b41a5f9" dependencies = [ "bytes", "fs-err", @@ -241,21 +223,6 @@ dependencies = [ "tower-service", ] -[[package]] -name = "backtrace" -version = "0.3.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] - [[package]] name = "base64" version = "0.22.1" @@ -264,17 +231,17 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.7.3" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -297,9 +264,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "byteorder" @@ -309,9 +276,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "bzip2" @@ -334,10 +301,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.19" +version = "1.2.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -345,17 +313,16 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.40" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", @@ -374,6 +341,23 @@ dependencies = [ "inout", ] +[[package]] +name = "compression-codecs" +version = "0.4.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00828ba6fd27b45a448e57dbfe84f1029d4c9f26b368157e9a448a5f49a2ec2a" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -422,9 +406,9 @@ dependencies = [ [[package]] name = "crc" -version = "3.2.1" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ "crc-catalog", ] @@ -437,9 +421,9 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -461,9 +445,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -471,15 +455,15 @@ dependencies = [ [[package]] name = "deflate64" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" +checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" [[package]] name = "der" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", "pem-rfc7468", @@ -488,18 +472,18 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", ] [[package]] name = "derive_arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", @@ -573,12 +557,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.11" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -594,9 +578,9 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.4.0" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ "concurrent-queue", "parking", @@ -610,10 +594,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] -name = "flate2" -version = "1.1.1" +name = "find-msvc-tools" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" dependencies = [ "crc32fast", "miniz_oxide", @@ -659,9 +649,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -674,9 +664,9 @@ checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" [[package]] name = "fs-err" -version = "3.1.0" +version = "3.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f89bda4c2a21204059a977ed3bfe746677dfd137b83c339e702b0ac91d482aa" +checksum = "baf68cef89750956493a66a10f512b9e58d9db21f2a573c079c0bdf1207a54a7" dependencies = [ "autocfg", "tokio", @@ -794,42 +784,36 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasip2", "wasm-bindgen", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - [[package]] name = "h2" -version = "0.4.8" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", @@ -846,22 +830,28 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "hashlink" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown", + "hashbrown 0.15.5", ] [[package]] @@ -896,21 +886,20 @@ dependencies = [ [[package]] name = "home" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -957,13 +946,14 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2", "http", "http-body", @@ -971,6 +961,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -978,11 +969,10 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.5" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "futures-util", "http", "hyper", "hyper-util", @@ -1011,29 +1001,34 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.11" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ + "base64", "bytes", "futures-channel", "futures-util", "http", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1055,9 +1050,9 @@ dependencies = [ [[package]] name = "icalendar" -version = "0.16.13" +version = "0.17.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab45baec83621017cbc28632fbc4afdb7fb4ee59bdefc7e42eccdf3f9b02c2e4" +checksum = "cc3b69b799a03e059f6dc984c25a8bf847d8ca4cbddb079c39ede7b3d24854c3" dependencies = [ "chrono", "iso8601", @@ -1068,21 +1063,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -1091,104 +1087,66 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ - "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", + "icu_locale_core", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -1197,9 +1155,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -1207,12 +1165,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", ] [[package]] @@ -1231,35 +1189,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] -name = "iso8601" -version = "0.6.2" +name = "iri-string" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5c177cff824ab21a6f41079a4c401241c4e8be14f316c4c6b07d5fca351c98d" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "iso8601" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1082f0c48f143442a1ac6122f67e360ceee130b967af4d50996e5154a45df46" dependencies = [ "nom", ] [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.4", "libc", ] [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -1291,15 +1259,26 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.171" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libm" -version = "0.2.11" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags", + "libc", + "redox_syscall 0.7.0", +] [[package]] name = "libsqlite3-sys" @@ -1313,37 +1292,30 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] -[[package]] -name = "lockfree-object-pool" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" - [[package]] name = "log" -version = "0.4.27" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lzma-rs" @@ -1368,11 +1340,11 @@ dependencies = [ [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -1393,9 +1365,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "mime" @@ -1415,29 +1387,30 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] name = "mio" -version = "1.0.3" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] name = "mockall" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2" +checksum = "f58d964098a5f9c6b63d0798e5372fd04708193510a7af313c22e9f29b7b620b" dependencies = [ "cfg-if", "downcast", @@ -1449,9 +1422,9 @@ dependencies = [ [[package]] name = "mockall_derive" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898" +checksum = "ca41ce716dda6a9be188b385aa78ee5260fc25cd3802cb2a8afdc6afbe6b6dbf" dependencies = [ "cfg-if", "proc-macro2", @@ -1513,12 +1486,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "overload", - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -1533,11 +1505,10 @@ dependencies = [ [[package]] name = "num-bigint-dig" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" dependencies = [ - "byteorder", "lazy_static", "libm", "num-integer", @@ -1550,9 +1521,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-integer" @@ -1584,15 +1555,6 @@ dependencies = [ "libm", ] -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -1601,9 +1563,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "openssl" -version = "0.10.72" +version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ "bitflags", "cfg-if", @@ -1633,18 +1595,18 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-src" -version = "300.5.0+3.5.0" +version = "300.5.5+3.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8ce546f549326b0e6052b649198487d91320875da901e7bd11a06d1ee3f9c2f" +checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.107" +version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", @@ -1653,12 +1615,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "oxicloud" version = "0.1.0" @@ -1684,7 +1640,7 @@ dependencies = [ "openssl", "pin-project-lite", "quick-xml", - "rand 0.9.0", + "rand 0.9.2", "rand_core 0.6.4", "reqwest", "serde", @@ -1713,9 +1669,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -1723,15 +1679,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -1757,12 +1713,12 @@ dependencies = [ [[package]] name = "pem" -version = "3.0.5" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ "base64", - "serde", + "serde_core", ] [[package]] @@ -1776,9 +1732,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" @@ -1819,6 +1775,15 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1862,36 +1827,36 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quick-xml" -version = "0.37.4" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ce8c88de324ff838700f36fb6ab86c96df0e3c4ab6ef3a9b2044465cce1369" +checksum = "f2e3bf4aa9d243beeb01a7b3bc30b77cfe2c44e24ec02d751a7104a53c2c49a1" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" @@ -1906,13 +1871,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", - "zerocopy", + "rand_core 0.9.5", ] [[package]] @@ -1932,7 +1896,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -1941,76 +1905,58 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.4", ] [[package]] name = "redox_syscall" -version = "0.5.11" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] [[package]] -name = "regex" -version = "1.11.1" +name = "redox_syscall" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" dependencies = [ - "aho-corasick", - "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "bitflags", ] [[package]] name = "regex-automata" -version = "0.1.10" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.29" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.12.15" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", @@ -2025,30 +1971,27 @@ dependencies = [ "hyper-rustls", "hyper-tls", "hyper-util", - "ipnet", "js-sys", "log", "mime", "mime_guess", "native-tls", - "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", - "system-configuration", "tokio", "tokio-native-tls", "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows-registry", ] [[package]] @@ -2059,7 +2002,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -2067,9 +2010,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.8" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ "const-oid", "digest", @@ -2085,30 +2028,24 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - [[package]] name = "rustix" -version = "1.0.5" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.26" +version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ "once_cell", "ring", @@ -2119,25 +2056,19 @@ dependencies = [ ] [[package]] -name = "rustls-pemfile" -version = "2.2.0" +name = "rustls-pki-types" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ - "rustls-pki-types", + "zeroize", ] -[[package]] -name = "rustls-pki-types" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" - [[package]] name = "rustls-webpki" -version = "0.103.1" +version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ "ring", "rustls-pki-types", @@ -2146,23 +2077,23 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2186,9 +2117,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -2196,18 +2127,28 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -2216,24 +2157,26 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] name = "serde_path_to_error" -version = "0.1.17" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" dependencies = [ "itoa", "serde", + "serde_core", ] [[package]] @@ -2261,9 +2204,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -2287,10 +2230,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -2306,9 +2250,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "simple_asn1" @@ -2324,30 +2268,27 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.9" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" dependencies = [ "serde", ] [[package]] name = "socket2" -version = "0.5.9" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -2371,9 +2312,9 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" dependencies = [ "sqlx-core", "sqlx-macros", @@ -2384,10 +2325,11 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ + "base64", "bytes", "chrono", "crc", @@ -2398,7 +2340,7 @@ dependencies = [ "futures-intrusive", "futures-io", "futures-util", - "hashbrown", + "hashbrown 0.15.5", "hashlink", "indexmap", "log", @@ -2406,7 +2348,6 @@ dependencies = [ "once_cell", "percent-encoding", "rustls", - "rustls-pemfile", "serde", "serde_json", "sha2", @@ -2417,14 +2358,14 @@ dependencies = [ "tracing", "url", "uuid", - "webpki-roots", + "webpki-roots 0.26.11", ] [[package]] name = "sqlx-macros" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3112e2ad78643fef903618d78cf0aec1cb3134b019730edb039b69eaf531f310" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" dependencies = [ "proc-macro2", "quote", @@ -2435,9 +2376,9 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e9f90acc5ab146a99bf5061a7eb4976b573f560bc898ef3bf8435448dd5e7ad" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" dependencies = [ "dotenvy", "either", @@ -2454,16 +2395,15 @@ dependencies = [ "sqlx-postgres", "sqlx-sqlite", "syn", - "tempfile", "tokio", "url", ] [[package]] name = "sqlx-mysql" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64", @@ -2505,9 +2445,9 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64", @@ -2544,9 +2484,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" dependencies = [ "atoi", "chrono", @@ -2562,6 +2502,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", + "thiserror", "tracing", "url", "uuid", @@ -2569,9 +2510,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stringprep" @@ -2592,9 +2533,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.100" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -2612,9 +2553,9 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", @@ -2623,9 +2564,9 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ "bitflags", "core-foundation", @@ -2644,15 +2585,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.19.1" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", - "getrandom 0.3.2", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2663,18 +2604,18 @@ checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -2683,40 +2624,39 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", ] [[package]] name = "time" -version = "0.3.41" +version = "0.3.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4" dependencies = [ "num-conv", "time-core", @@ -2724,9 +2664,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -2734,9 +2674,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -2749,11 +2689,10 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.44.2" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ - "backtrace", "bytes", "libc", "mio", @@ -2762,14 +2701,14 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", @@ -2788,9 +2727,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -2798,9 +2737,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -2809,9 +2748,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.14" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -2822,9 +2761,9 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -2838,9 +2777,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.2" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "async-compression", "bitflags", @@ -2852,12 +2791,14 @@ dependencies = [ "http-body-util", "http-range-header", "httpdate", + "iri-string", "mime", "mime_guess", "percent-encoding", "pin-project-lite", "tokio", "tokio-util", + "tower", "tower-layer", "tower-service", "tracing", @@ -2878,9 +2819,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -2890,9 +2831,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -2901,9 +2842,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -2922,14 +2863,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "sharded-slab", "smallvec", "thread_local", @@ -2946,15 +2887,15 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicase" -version = "2.8.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-bidi" @@ -2964,24 +2905,24 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-normalization" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] [[package]] name = "unicode-properties" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "untrusted" @@ -2991,21 +2932,16 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -3014,13 +2950,13 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.16.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.4", "js-sys", - "serde", + "serde_core", "wasm-bindgen", ] @@ -3053,17 +2989,17 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] @@ -3074,37 +3010,25 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -3113,9 +3037,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3123,31 +3047,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", @@ -3155,63 +3079,50 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.8" +version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.5", +] + +[[package]] +name = "webpki-roots" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" dependencies = [ "rustls-pki-types", ] [[package]] name = "whoami" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" dependencies = [ - "redox_syscall", + "libredox", "wasite", ] -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-core" -version = "0.61.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", "windows-link", "windows-result", - "windows-strings 0.4.0", + "windows-strings", ] [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -3220,9 +3131,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -3231,44 +3142,35 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.1.1" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-registry" -version = "0.4.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ + "windows-link", "windows-result", - "windows-strings 0.3.1", - "windows-targets 0.53.0", + "windows-strings", ] [[package]] name = "windows-result" -version = "0.3.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.3.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link", ] @@ -3293,11 +3195,20 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.59.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.52.6", + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", ] [[package]] @@ -3333,18 +3244,19 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.0" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -3361,9 +3273,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -3379,9 +3291,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -3397,9 +3309,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -3409,9 +3321,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -3427,9 +3339,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -3445,9 +3357,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -3463,9 +3375,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -3481,30 +3393,21 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags", -] - -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "xz2" @@ -3517,11 +3420,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -3529,9 +3431,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", @@ -3541,18 +3443,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.24" +version = "0.8.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +checksum = "7456cf00f0685ad319c5b1693f291a650eaf345e941d082fc4e03df8a03996ac" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.24" +version = "0.8.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +checksum = "1328722bbf2115db7e19d69ebcc15e795719e2d66b60827c6a69a117365e37a0" dependencies = [ "proc-macro2", "quote", @@ -3582,18 +3484,18 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", @@ -3601,10 +3503,21 @@ dependencies = [ ] [[package]] -name = "zerovec" -version = "0.10.4" +name = "zerotrie" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -3613,9 +3526,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", @@ -3624,9 +3537,9 @@ dependencies = [ [[package]] name = "zip" -version = "2.6.1" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dcb24d0152526ae49b9b96c1dcf71850ca1e0b882e4e28ed898a93c41334744" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" dependencies = [ "aes", "arbitrary", @@ -3635,14 +3548,16 @@ dependencies = [ "crc32fast", "crossbeam-utils", "deflate64", + "displaydoc", "flate2", - "getrandom 0.3.2", + "getrandom 0.3.4", "hmac", "indexmap", "lzma-rs", "memchr", "pbkdf2", "sha1", + "thiserror", "time", "xz2", "zeroize", @@ -3651,16 +3566,20 @@ dependencies = [ ] [[package]] -name = "zopfli" -version = "0.8.1" +name = "zmij" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" dependencies = [ "bumpalo", "crc32fast", - "lockfree-object-pool", "log", - "once_cell", "simd-adler32", ] @@ -3684,9 +3603,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.15+zstd.1.5.7" +version = "2.0.16+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 1583f3b0..08a34977 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,45 +5,45 @@ edition = "2021" [dependencies] -axum = { version = "0.8.3", features = ["multipart", "http1", "tokio", "macros"] } -tokio = { version = "1.44.2", features = ["full"] } -tokio-util = { version = "0.7.14", features = ["io", "codec"] } -tokio-stream = { version = "0.1.17", features = ["fs"] } -bytes = "1.10.1" -tempfile = "3.19.1" -tower = "0.5.2" -tower-http = { version = "0.6.2", features = ["fs", "compression-gzip", "trace", "cors", "add-extension", "request-id"] } -flate2 = "1.1.1" -zip = "2.6.1" -tracing = "0.1.41" -tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } -chrono = { version = "0.4.40", features = ["serde"] } +axum = { version = "0.8.8", features = ["multipart", "http1", "tokio", "macros"] } +tokio = { version = "1.49.0", features = ["full"] } +tokio-util = { version = "0.7.18", features = ["io", "codec"] } +tokio-stream = { version = "0.1.18", features = ["fs"] } +bytes = "1.11.0" +tempfile = "3.24.0" +tower = "0.5.3" +tower-http = { version = "0.6.8", features = ["fs", "compression-gzip", "trace", "cors", "add-extension", "request-id"] } +flate2 = "1.1.8" +zip = "2.1.0" +tracing = "0.1.44" +tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } +chrono = { version = "0.4.43", features = ["serde"] } http-body = "1.0.1" -serde = { version = "1.0.219", features = ["derive"] } -serde_json = "1.0.140" +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.149" futures = "0.3.31" async-stream = "0.3.6" mime_guess = "2.0.5" -uuid = { version = "1.16.0", features = ["v4", "serde"] } -async-trait = "0.1.88" -thiserror = "2.0.12" -reqwest = { version = "0.12.15", features = ["json", "multipart"] } -mockall = { version = "0.13.1", optional = true } -rand = "0.9.0" +uuid = { version = "1.20.0", features = ["v4", "serde"] } +async-trait = "0.1.89" +thiserror = "2.0.18" +reqwest = { version = "0.12.18", features = ["json", "multipart"] } +mockall = { version = "0.14.0", optional = true } +rand = "0.9.1" pin-project-lite = "0.2.16" -sqlx = { version = "0.8.3", features = ["postgres", "runtime-tokio", "tls-rustls", "chrono", "uuid", "json"] } -anyhow = "1.0.97" +sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio", "tls-rustls", "chrono", "uuid", "json"] } +anyhow = "1.0.100" jsonwebtoken = "9.3.1" argon2 = "0.5.3" rand_core = { version = "0.6.4", features = ["std"] } -time = "0.3.41" +time = "0.3.46" axum-server = "0.7.2" -hyper = { version = "1.6.0", features = ["full"] } -url = "2.5.4" -quick-xml = "0.37.4" +hyper = { version = "1.8.1", features = ["full"] } +url = "2.5.8" +quick-xml = "0.39.0" http-body-util = "0.1.3" -openssl = { version = "0.10.72", features = ["vendored"] } -icalendar = "0.16.13" +openssl = { version = "0.10.75", features = ["vendored"] } +icalendar = "0.17.6" dotenv = "0.15.0" [features] diff --git a/Dockerfile b/Dockerfile index 39431960..855f0f52 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Stage 1: Cache dependencies -FROM rust:1.85-alpine AS cacher +FROM rust:1.93.0-alpine3.23 AS cacher WORKDIR /app RUN apk --no-cache upgrade && \ apk add --no-cache musl-dev pkgconfig postgresql-dev gcc perl make @@ -10,7 +10,7 @@ RUN mkdir -p src && \ cargo build --release && \ rm -rf src target/release/deps/oxicloud* # Stage 2: Build the application -FROM rust:1.85-alpine AS builder +FROM rust:1.93.0-alpine3.23 AS builder WORKDIR /app RUN apk --no-cache upgrade && \ apk add --no-cache musl-dev pkgconfig postgresql-dev gcc perl make diff --git a/ISSUE_45_SOLUTION.md b/ISSUE_45_SOLUTION.md deleted file mode 100644 index d7abd381..00000000 --- a/ISSUE_45_SOLUTION.md +++ /dev/null @@ -1,103 +0,0 @@ -# Solución para el error "El usuario 'admin' ya existe" - -## Problema - -Cuando se intenta crear un usuario administrador en una instalación nueva de OxiCloud, aparece el siguiente error: - -``` -oxicloud-1 | 2025-04-12T10:47:26.643669Z ERROR oxicloud::interfaces::api::handlers::auth_handler: Registration failed for user admin: Already Exists: El usuario 'admin' ya existe -``` - -Este error ocurre porque las migraciones de la base de datos ya crean un usuario administrador por defecto como parte del proceso de inicialización. - -## Solución implementada - -Hemos mejorado el sistema para que maneje mejor el registro de usuarios administradores: - -1. **En una instalación nueva**: - - Si registras cualquier usuario como administrador (sea cual sea su nombre), el sistema detectará que es una instalación nueva y eliminará automáticamente el usuario admin predeterminado. - - Esto te permite crear tu propio usuario administrador con el nombre que prefieras desde el principio. - -2. **En un sistema en uso**: - - No se permite crear nuevos usuarios administradores desde la página de registro una vez que ya existe un administrador en el sistema - - Esto previene la creación no autorizada de usuarios con permisos elevados - - El sistema tampoco permite tener múltiples usuarios con el mismo nombre (incluido "admin") - -### Detalles técnicos - -La solución implementa: - -1. Detección inteligente de instalaciones nuevas basada en: - - Verificación del número total de usuarios en el sistema - - Verificación del número de usuarios administradores - -2. Reconocimiento de usuarios administradores: - - Un usuario es administrador si su nombre es "admin" - - Un usuario es administrador si se proporciona un rol "admin" explícitamente - - Los administradores reciben automáticamente una cuota de 100GB - -3. Eliminación segura del usuario admin predeterminado: - - Se detecta al inicio del registro si es una instalación nueva - - Se elimina el admin predeterminado antes de continuar con el registro - -4. Prevención de creación de múltiples administradores: - - Una vez que existe un usuario administrador en el sistema, no se permite crear más administradores desde la página de registro - - Solo se puede crear un administrador desde la página de registro durante la instalación inicial - - Esto protege el sistema contra la creación no autorizada de usuarios con privilegios elevados - -## Cómo usar esta funcionalidad - -### En una instalación nueva: - -1. Inicia OxiCloud por primera vez (las migraciones crearán automáticamente un usuario admin predeterminado) -2. Ve a la pantalla de registro y crea un usuario con: - - **Nombre de usuario**: Cualquier nombre que prefieras (por ejemplo, "torrefacto") - - **Contraseña**: La que tú quieras - - **Email**: Tu correo electrónico -3. El sistema detectará automáticamente que se trata de una instalación nueva -4. Si es un usuario administrador (porque el nombre es "admin" o porque explícitamente quieres que sea admin), el sistema eliminará el admin predeterminado antes de continuar -5. Tu nuevo usuario se creará y podrás iniciar sesión con él - -### Si necesitas restablecer el usuario administrador: - -Si ya tienes un sistema en uso y necesitas restablecer el usuario administrador: - -#### Opción 1: Usar el script proporcionado -```bash -cat scripts/reset_admin.sql | docker exec -i oxicloud-postgres-1 psql -U postgres -d oxicloud -``` - -#### Opción 2: Hacerlo manualmente -```bash -docker exec -it oxicloud-postgres-1 psql -U postgres -d oxicloud -``` - -```sql -SET search_path TO auth; -DELETE FROM auth.users WHERE username = 'admin'; -SELECT username, email, role FROM auth.users; -``` - -Luego registra un nuevo usuario admin a través de la interfaz web. - -## Nota técnica - -El usuario administrador predeterminado se crea durante las migraciones con estos valores: - -```sql -INSERT INTO auth.users ( - id, - username, - email, - password_hash, - role, - storage_quota_bytes -) VALUES ( - '00000000-0000-0000-0000-000000000000', - 'admin', - 'admin@oxicloud.local', - '$argon2id$v=19$m=65536,t=3,p=4$c2FsdHNhbHRzYWx0c2FsdA$H3VxE8LL2qPT31DM3loTg6D+O4MSc2sD7GjlQ5h7Jkw', -- Admin123! - 'admin', - 107374182400 -- 100GB for admin -); -``` diff --git a/REPO-DOCUMENTATION.md b/REPO-DOCUMENTATION.md deleted file mode 100644 index 4d9665bf..00000000 --- a/REPO-DOCUMENTATION.md +++ /dev/null @@ -1,708 +0,0 @@ -# OxiCloud Code Documentation - -This document provides a detailed description of each file in the OxiCloud codebase, organized by architectural layers according to the clean hexagonal architecture pattern. - -## Domain Layer - -The domain layer forms the core of the application, containing business entities and repository interfaces. - -### Entities - -**File Entity (`src/domain/entities/file.rs`)** -- Core domain entity representing files in the system -- Implements an immutable design pattern for file operations -- Provides validation, creation, and manipulation methods for files -- Maintains both physical storage information and logical metadata -- Includes error handling via `FileError` for validation failures - -**Folder Entity (`src/domain/entities/folder.rs`)** -- Represents folders/directories in the domain model -- Supports hierarchical structure with parent-child relationships -- Provides validation, creation, and update operations -- Implements immutable pattern with methods returning new instances -- Handles path resolution for proper folder hierarchy - -**User Entity (`src/domain/entities/user.rs`)** -- Manages user accounts and authentication -- Provides secure password handling with Argon2 hashing -- Supports roles (Admin, User) with appropriate permissions -- Tracks storage usage and quotas -- Includes account management functions (activation, deactivation, login tracking) - -**Share Entity (`src/domain/entities/share.rs`)** -- Implements file and folder sharing functionality -- Supports various permission levels (read, write, reshare) -- Provides password protection for shared resources -- Handles expiration dates for temporary sharing -- Tracks access statistics for shared resources - -**Calendar Entity (`src/domain/entities/calendar.rs`)** -- Supports calendar functionality for CalDAV integration -- Manages calendar properties like name, color, and description -- Provides ownership and access control -- Supports custom properties for extended CalDAV compatibility -- Handles validation of calendar data - -**Calendar Event Entity (`src/domain/entities/calendar_event.rs`)** -- Represents calendar events with properties like title, description, location -- Handles date/time management with recurrence rules -- Supports reminders and notifications -- Provides validation for event data -- Implements custom properties for CalDAV compatibility - -**Trashed Item Entity (`src/domain/entities/trashed_item.rs`)** -- Manages files and folders in the trash -- Tracks original locations for restoration -- Implements automatic cleanup based on retention policies -- Provides restoration and permanent deletion functionality - -### Repositories (Interfaces) - -**File Repository (`src/domain/repositories/file_repository.rs`)** -- Defines the contract for file storage operations -- Abstracts storage implementation details from the domain -- Supports file creation, retrieval, updating, and deletion -- Provides methods for content streaming and file movement -- Includes trash functionality for file lifecycle management - -**Folder Repository (`src/domain/repositories/folder_repository.rs`)** -- Defines the interface for folder manipulation -- Abstracts storage implementation details for directories -- Handles folder creation, listing, and hierarchy management -- Supports moving folders and retrieving path information -- Includes trash operations for folders - -**User Repository (`src/domain/repositories/user_repository.rs`)** -- Defines the interface for user data persistence -- Supports user creation, retrieval, and management -- Provides authentication and session management -- Handles user preferences and settings storage -- Manages user quotas and storage usage tracking - -**Share Repository (`src/domain/repositories/share_repository.rs`)** -- Defines the interface for share record management -- Handles creation and validation of share records -- Tracks permissions and expiration settings -- Provides access verification for shared resources -- Manages share revocation and updates - -**Trash Repository (`src/domain/repositories/trash_repository.rs`)** -- Defines the interface for trash operations -- Manages soft deletion and restoration of resources -- Handles retention policies and automatic cleanup -- Provides listing of trashed items with metadata -- Supports permanent deletion operations - -### Domain Services - -**Auth Service (`src/domain/services/auth_service.rs`)** -- Provides domain-level authentication logic -- Implements password validation and hashing -- Defines authentication policies and rules -- Manages token generation and validation -- Handles security-related domain operations - -**I18n Service (`src/domain/services/i18n_service.rs`)** -- Defines domain-level internationalization interface -- Provides translation lookup capabilities -- Manages localization strategies -- Supports multiple languages and fallbacks -- Handles format localization for dates, numbers, etc. - -**Path Service (`src/domain/services/path_service.rs`)** -- Manages domain-level path abstractions -- Provides path validation and normalization -- Handles path traversal and resolution -- Implements path security measures -- Supports different path formats and conventions - -## Application Layer - -The application layer orchestrates use cases by coordinating domain objects and providing services to the interfaces layer. - -### Services - -**File Service (`src/application/services/file_service.rs`)** -- Implements file-related use cases -- Coordinates between repositories for file operations -- Provides file upload, download, and listing functionality -- Handles error translation between layers -- Contains business logic for file operations - -**Folder Service (`src/application/services/folder_service.rs`)** -- Implements folder management use cases -- Manages folder creation, listing, and hierarchy -- Coordinates between repositories for folder operations -- Maintains folder structure integrity -- Handles error translation for folder operations - -**Auth Application Service (`src/application/services/auth_application_service.rs`)** -- Manages user authentication flows -- Implements login, logout, and session management -- Handles token generation and validation -- Coordinates with user repository for verification -- Manages password reset and account recovery - -**File Management Service (`src/application/services/file_management_service.rs`)** -- Provides higher-level file operations -- Manages file uploads, versions, and metadata -- Handles file operations across repositories -- Coordinates transactional file operations -- Provides advanced file searching and filtering - -**File Retrieval Service (`src/application/services/file_retrieval_service.rs`)** -- Specialized service for file content retrieval -- Optimizes file reading operations -- Provides streaming and download functionality -- Implements read-specific error handling -- Supports different retrieval patterns (whole file, ranges) - -**File Upload Service (`src/application/services/file_upload_service.rs`)** -- Specialized service for handling file uploads -- Manages chunked and multipart uploads -- Provides validation during upload -- Handles large file uploads efficiently -- Supports upload resumption and integrity verification - -**Search Service (`src/application/services/search_service.rs`)** -- Implements file and folder search functionality -- Provides text-based content searching -- Handles metadata-based filtering -- Supports sorting and pagination of results -- Optimizes search operations for performance - -**Share Service (`src/application/services/share_service.rs`)** -- Implements file and folder sharing functionality -- Creates and manages share links -- Handles permission checking for shared resources -- Manages password protection for shares -- Processes access requests for shared content - -**Trash Service (`src/application/services/trash_service.rs`)** -- Implements trash can functionality -- Manages moving items to trash and restoration -- Handles automatic cleanup of expired trash -- Coordinates with repositories for trash operations -- Maintains metadata for trashed items - -**Recent Service (`src/application/services/recent_service.rs`)** -- Tracks recently accessed files -- Manages user-specific recent file lists -- Handles expiration of old entries -- Provides sorting and filtering of recent files -- Coordinates with file repository for metadata - -**Favorites Service (`src/application/services/favorites_service.rs`)** -- Manages user favorite files and folders -- Provides adding and removing favorites -- Handles listing and sorting of favorites -- Coordinates with repositories for data consistency -- Maintains user-specific favorite lists - -**I18n Application Service (`src/application/services/i18n_application_service.rs`)** -- Handles internationalization and localization -- Provides translation lookups for UI components -- Manages locale detection and setting -- Coordinates with i18n domain service -- Supports dynamic language switching - -**Storage Mediator (`src/application/services/storage_mediator.rs`)** -- Coordinates between different storage repositories -- Manages transaction coordination -- Handles path resolution between storage layers -- Provides unified view of storage subsystems -- Optimizes operations across storage types - -**Batch Operations (`src/application/services/batch_operations.rs`)** -- Implements batch processing for file operations -- Handles atomic multi-file operations -- Provides transaction support for batch operations -- Manages failure handling and partial success -- Optimizes performance for bulk operations - -### Ports - -**Inbound Ports (`src/application/ports/inbound.rs`)** -- Defines interfaces for external systems to use -- Contains use case interfaces for application services -- Specifies contracts for UI and API interactions -- Provides clear boundaries for application functionality -- Forms the primary API for interfaces layer - -**Outbound Ports (`src/application/ports/outbound.rs`)** -- Defines interfaces used by application services -- Specifies contracts that infrastructure must implement -- Allows swapping infrastructure implementations -- Maintains dependency inversion principle -- Protects application layer from external dependencies - -**Auth Ports (`src/application/ports/auth_ports.rs`)** -- Defines interfaces for authentication operations -- Specifies contracts for login, validation, and sessions -- Handles token generation and verification -- Provides user identity management -- Supports different authentication methods - -**Storage Ports (`src/application/ports/storage_ports.rs`)** -- Defines interfaces for storage operations -- Specifies contracts for accessing persistent storage -- Handles file system and database interactions -- Provides transaction support for storage operations -- Supports different storage backends - -**File Ports (`src/application/ports/file_ports.rs`)** -- Defines interfaces for file operations -- Contains file upload and retrieval use cases -- Specifies contracts for file management -- Handles file-specific error conditions -- Supports various file operation patterns - -**Favorites Ports (`src/application/ports/favorites_ports.rs`)** -- Defines interfaces for favorites functionality -- Specifies contracts for favorite management -- Handles favorite-specific operations -- Provides user-specific favorite management -- Supports different favorite organization structures - -**Recent Ports (`src/application/ports/recent_ports.rs`)** -- Defines interfaces for recent files functionality -- Specifies contracts for recent file tracking -- Handles history and access patterns -- Provides user-specific recent file handling -- Supports different recency algorithms - -**Share Ports (`src/application/ports/share_ports.rs`)** -- Defines interfaces for sharing functionality -- Specifies contracts for share creation and access -- Handles permission verification for shares -- Provides link generation and management -- Supports different sharing models - -**Trash Ports (`src/application/ports/trash_ports.rs`)** -- Defines interfaces for trash functionality -- Specifies contracts for trash operations -- Handles trash-specific workflows -- Provides retention and cleanup interfaces -- Supports different trash implementation strategies - -### DTOs - -**File DTO (`src/application/dtos/file_dto.rs`)** -- Data transfer object for file entities -- Provides serialization and API representation -- Translates between domain model and external interfaces -- Includes conversions to/from domain entities -- Contains file metadata for API responses - -**Folder DTO (`src/application/dtos/folder_dto.rs`)** -- Data transfer object for folder entities -- Provides folder data for API responses -- Handles serialization and API representation -- Includes conversions to/from domain entities -- Contains folder structure information - -**User DTO (`src/application/dtos/user_dto.rs`)** -- Data transfer object for user information -- Provides user data for API responses -- Handles serialization with sensitive data protection -- Includes conversions to/from domain entities -- Contains user profile information - -**Share DTO (`src/application/dtos/share_dto.rs`)** -- Data transfer object for share information -- Provides share data for API responses -- Handles serialization of sharing details -- Includes conversions to/from domain entities -- Contains share link and permission data - -**Trash DTO (`src/application/dtos/trash_dto.rs`)** -- Data transfer object for trashed items -- Provides trash information for API responses -- Handles serialization of trash metadata -- Includes conversions to/from domain entities -- Contains restoration information - -**Pagination DTO (`src/application/dtos/pagination.rs`)** -- Handles pagination for list responses -- Provides page size and number information -- Supports offset and cursor-based pagination -- Includes metadata for total items and pages -- Facilitates consistent pagination across APIs - -**Favorites DTO (`src/application/dtos/favorites_dto.rs`)** -- Data transfer object for favorites -- Provides favorites data for API responses -- Handles serialization of favorite items -- Includes conversions to/from domain entities -- Contains favorite metadata and organization - -**Recent DTO (`src/application/dtos/recent_dto.rs`)** -- Data transfer object for recent files -- Provides recent items data for API responses -- Handles serialization of access history -- Includes conversions to/from domain entities -- Contains timing and access metadata - -**Search DTO (`src/application/dtos/search_dto.rs`)** -- Data transfer object for search results -- Provides search data for API responses -- Handles serialization of search results -- Includes query and result metadata -- Contains relevance and ranking information - -**I18n DTO (`src/application/dtos/i18n_dto.rs`)** -- Data transfer object for internationalization -- Provides language and translation data -- Handles serialization of language resources -- Includes locale and preference information -- Contains translation bundle structures - -### Adapters - -**WebDAV Adapter (`src/application/adapters/webdav_adapter.rs`)** -- Adapts between OxiCloud domain models and WebDAV protocol -- Handles XML parsing and generation for WebDAV operations -- Implements property handling for WebDAV (PROPFIND, PROPPATCH) -- Provides WebDAV-specific error handling -- Translates between file operations and WebDAV methods - -### Transactions - -**Storage Transaction (`src/application/transactions/storage_transaction.rs`)** -- Manages transactional operations for storage -- Implements transaction boundaries and commits -- Provides rollback capabilities on failure -- Ensures consistency across multiple operations -- Handles transaction isolation levels - -## Infrastructure Layer - -This layer provides concrete implementations of repository interfaces and technical services. - -### Repositories (Implementations) - -**File FS Repository (`src/infrastructure/repositories/file_fs_repository.rs`)** -- Implements FileRepository interface for filesystem storage -- Manages physical file operations on disk -- Handles file content reading and writing -- Implements optimized large file handling -- Provides metadata caching for performance - -**File FS Read Repository (`src/infrastructure/repositories/file_fs_read_repository.rs`)** -- Specialized repository for read-only file operations -- Optimized for high-performance file retrieval -- Implements caching for frequently accessed files -- Supports streaming of large files -- Handles content type detection and verification - -**File FS Write Repository (`src/infrastructure/repositories/file_fs_write_repository.rs`)** -- Specialized repository for file write operations -- Handles atomic file writes with transaction support -- Implements optimized large file writes -- Manages file locking for concurrent writes -- Provides integrity verification for written files - -**File FS Repository Trash (`src/infrastructure/repositories/file_fs_repository_trash.rs`)** -- Extends file repository with trash functionality -- Implements soft delete operations for files -- Manages restoration from trash -- Handles automatic cleanup of expired trash -- Maintains metadata for trashed files - -**Folder FS Repository (`src/infrastructure/repositories/folder_fs_repository.rs`)** -- Implements FolderRepository interface for filesystem -- Creates and manages directory structures -- Handles folder listing and hierarchy traversal -- Implements folder permissions and ownership -- Provides optimization for deep folder structures - -**Folder FS Repository Trash (`src/infrastructure/repositories/folder_fs_repository_trash.rs`)** -- Extends folder repository with trash functionality -- Implements soft delete for directories -- Handles recursive trash operations for folders -- Manages restoration of folder hierarchies -- Maintains metadata for trashed folders - -**Share FS Repository (`src/infrastructure/repositories/share_fs_repository.rs`)** -- Implements ShareRepository for filesystem-based sharing -- Manages share records and permissions -- Handles link generation and validation -- Provides access control for shared resources -- Supports share expiration and revocation - -**Trash FS Repository (`src/infrastructure/repositories/trash_fs_repository.rs`)** -- Implements TrashRepository for filesystem -- Manages trash directory structure -- Handles metadata for trashed items -- Implements cleanup policies for expired trash -- Supports permanent deletion operations - -**Session PG Repository (`src/infrastructure/repositories/pg/session_pg_repository.rs`)** -- Implements session storage using PostgreSQL -- Manages user sessions and authentication state -- Handles session creation, validation, and expiration -- Provides secure token management -- Supports multiple concurrent sessions - -**User PG Repository (`src/infrastructure/repositories/pg/user_pg_repository.rs`)** -- Implements UserRepository with PostgreSQL -- Stores user accounts and profile information -- Handles user queries and updates -- Manages user roles and permissions -- Supports user search and filtering - -**File Metadata Manager (`src/infrastructure/repositories/file_metadata_manager.rs`)** -- Manages file metadata independently of content -- Handles extended attributes for files -- Provides caching for frequently accessed metadata -- Optimizes metadata operations -- Supports custom metadata fields - -**File Path Resolver (`src/infrastructure/repositories/file_path_resolver.rs`)** -- Resolves logical paths to physical storage locations -- Handles path normalization and validation -- Provides path translation between different systems -- Supports virtual paths and redirections -- Optimizes path resolution for nested structures - -**Parallel File Processor (`src/infrastructure/repositories/parallel_file_processor.rs`)** -- Implements parallel processing for large files -- Optimizes file operations with multi-threading -- Provides chunked reading and writing -- Handles load balancing for file operations -- Implements backpressure mechanisms - -### Services - -**ID Mapping Service (`src/infrastructure/services/id_mapping_service.rs`)** -- Manages mapping between UUIDs and filesystem paths -- Provides persistent ID generation and lookup -- Handles path changes while maintaining stable IDs -- Implements caching for frequently accessed mappings -- Ensures consistency between IDs and paths - -**Buffer Pool (`src/infrastructure/services/buffer_pool.rs`)** -- Manages memory buffers for file operations -- Implements pooling for optimal memory usage -- Provides buffer recycling to reduce allocations -- Handles buffer sizing for different operations -- Implements thread-safe buffer management - -**Cache Manager (`src/infrastructure/services/cache_manager.rs`)** -- Provides application-wide caching services -- Implements multiple cache levels (memory, disk) -- Handles cache invalidation and consistency -- Manages cache size limits and eviction -- Provides statistics for cache performance - -**Compression Service (`src/infrastructure/services/compression_service.rs`)** -- Implements data compression for files and responses -- Supports multiple compression algorithms -- Provides on-the-fly compression for API responses -- Handles selective compression based on file types -- Optimizes compression levels for different content - -**File System I18n Service (`src/infrastructure/services/file_system_i18n_service.rs`)** -- Implements I18n service using filesystem storage -- Loads translations from JSON files -- Handles language detection and fallbacks -- Provides translation lookups for UI components -- Supports dynamic language switching - -**File Metadata Cache (`src/infrastructure/services/file_metadata_cache.rs`)** -- Provides caching for file metadata -- Optimizes repeated metadata access -- Implements cache invalidation strategies -- Handles concurrent access to metadata -- Supports different cache levels (memory, persistent) - -**ID Mapping Optimizer (`src/infrastructure/services/id_mapping_optimizer.rs`)** -- Optimizes ID-to-path mapping operations -- Implements batch processing for mapping updates -- Provides preloading for frequently accessed mappings -- Handles compaction of mapping storage -- Optimizes lookup performance for large mappings - -**Zip Service (`src/infrastructure/services/zip_service.rs`)** -- Provides ZIP archive creation and extraction -- Supports on-the-fly compression for downloads -- Handles large directory archiving -- Implements streaming ZIP generation -- Provides progress tracking for large operations - -**Trash Cleanup Service (`src/infrastructure/services/trash_cleanup_service.rs`)** -- Manages automatic cleanup of expired trash items -- Implements retention policy enforcement -- Provides scheduled cleanup operations -- Handles graceful cleanup with resource limits -- Supports custom cleanup rules - -## Interfaces Layer - -This layer handles external communication, including API endpoints and web interfaces. - -### API Handlers - -**File Handler (`src/interfaces/api/handlers/file_handler.rs`)** -- Handles HTTP requests for file operations -- Processes file uploads with multipart support -- Provides file downloads with optional compression -- Implements CRUD operations for files -- Manages error responses and status codes - -**Folder Handler (`src/interfaces/api/handlers/folder_handler.rs`)** -- Handles HTTP requests for folder operations -- Processes folder creation and listing -- Implements CRUD operations for directories -- Provides folder hierarchy navigation -- Manages error responses for folder operations - -**Auth Handler (`src/interfaces/api/handlers/auth_handler.rs`)** -- Handles authentication-related API endpoints -- Processes login, logout, and registration -- Manages session tokens and refresh -- Implements password reset functionality -- Provides authentication status information - -**Share Handler (`src/interfaces/api/handlers/share_handler.rs`)** -- Handles file and folder sharing endpoints -- Processes share creation and management -- Provides access to shared resources -- Handles permission verification -- Manages share links and passwords - -**Trash Handler (`src/interfaces/api/handlers/trash_handler.rs`)** -- Handles trash-related API endpoints -- Processes moving items to trash -- Provides trash listing and filtering -- Handles restoration from trash -- Manages permanent deletion operations - -**Search Handler (`src/interfaces/api/handlers/search_handler.rs`)** -- Handles search-related API endpoints -- Processes text search queries -- Provides filtering and sorting options -- Handles pagination for search results -- Manages relevance scoring for results - -**Recent Handler (`src/interfaces/api/handlers/recent_handler.rs`)** -- Handles recently accessed files endpoints -- Provides listing and filtering of recent files -- Manages user-specific recent history -- Handles pagination for recent items -- Provides sorting options for recent files - -**Favorites Handler (`src/interfaces/api/handlers/favorites_handler.rs`)** -- Handles user favorites endpoints -- Processes adding and removing favorites -- Provides listing and filtering of favorites -- Manages user-specific favorite collections -- Handles sorting and organization of favorites - -**I18n Handler (`src/interfaces/api/handlers/i18n_handler.rs`)** -- Handles internationalization endpoints -- Provides language selection and detection -- Serves translation resources -- Manages locale settings -- Handles language preference persistence - -**Batch Handler (`src/interfaces/api/handlers/batch_handler.rs`)** -- Handles batch operation endpoints -- Processes multiple operations in a single request -- Provides transaction support for batches -- Handles partial success scenarios -- Manages comprehensive error reporting - -**WebDAV Handler (`src/interfaces/api/handlers/webdav_handler.rs`)** -- Implements WebDAV protocol (RFC 4918) endpoints -- Handles WebDAV methods (PROPFIND, PROPPATCH, etc.) -- Provides file system access via HTTP -- Manages WebDAV properties and locks -- Supports third-party WebDAV clients - -### API Routes - -**Routes (`src/interfaces/api/routes.rs`)** -- Defines API routes and URL structure -- Maps endpoints to appropriate handlers -- Configures middleware for routes -- Handles versioning for API endpoints -- Provides documentation integration - -### Middleware - -**Auth Middleware (`src/interfaces/middleware/auth.rs`)** -- Handles authentication for API requests -- Verifies tokens and sessions -- Provides user context for handlers -- Manages authentication errors -- Supports different authentication methods - -**Cache Middleware (`src/interfaces/middleware/cache.rs`)** -- Implements response caching -- Handles cache headers and validation -- Provides conditional request processing -- Manages cache invalidation -- Optimizes for different content types - -**Redirect Middleware (`src/interfaces/middleware/redirect.rs`)** -- Handles HTTP redirects -- Manages URL normalization -- Provides permanent and temporary redirects -- Handles protocol upgrades (HTTP to HTTPS) -- Supports path-based redirections - -### Web Interface - -**Web Module (`src/interfaces/web/mod.rs`)** -- Coordinates web interface components -- Manages static file serving -- Provides web application integration -- Handles web-specific middleware -- Supports single-page application routing - -## Common Layer - -This layer provides shared utilities and configurations used across the application. - -**Config (`src/common/config.rs`)** -- Manages application configuration -- Loads settings from environment and files -- Provides typed configuration access -- Handles configuration validation -- Supports different environments (dev, prod) - -**Errors (`src/common/errors.rs`)** -- Defines error types and handling -- Provides consistent error formatting -- Implements error context and wrapping -- Handles error translation between layers -- Supports error categorization and logging - -**DI (`src/common/di.rs`)** -- Implements dependency injection -- Manages service lifecycles -- Provides application state container -- Handles service resolution and registration -- Supports scoped service instances - -**DB (`src/common/db.rs`)** -- Manages database connections -- Provides connection pooling -- Handles database migrations -- Implements query helpers -- Supports transaction management - -**Cache (`src/common/cache.rs`)** -- Provides generic caching facilities -- Implements different cache strategies -- Handles cache key generation -- Manages cache invalidation -- Supports distributed caching - -**Auth Factory (`src/common/auth_factory.rs`)** -- Creates authentication components -- Configures auth providers based on settings -- Provides factory methods for auth services -- Handles auth strategy selection -- Supports multiple authentication methods \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index ea80a92e..00000000 --- a/SECURITY.md +++ /dev/null @@ -1,44 +0,0 @@ -# Security Policy - -## Supported Versions - -The following versions of OxiCloud are currently supported with security updates: - -| Version | Supported | -| ------- | ------------------ | -| Latest | :white_check_mark: | - -## Reporting a Vulnerability - -The OxiCloud team takes security issues seriously. We appreciate your efforts to responsibly disclose your findings and will make every effort to acknowledge your contributions. - -To report a security vulnerability, please follow these steps: - -1. **DO NOT** disclose the vulnerability publicly (e.g., in GitHub issues) -2. Email details of the vulnerability to the project maintainers -3. Include as much information as possible, such as: - - A clear description of the vulnerability - - Steps to reproduce the issue - - Potential impact - - Suggested fixes if available - -## What to Expect - -After submitting a vulnerability report, you can expect the following: - -1. **Acknowledgment**: The team will acknowledge receipt of your report within 3 business days -2. **Assessment**: We'll evaluate the vulnerability and determine its impact -3. **Plan**: We'll develop a plan to address the vulnerability -4. **Fix & Release**: Once fixed, we'll release an update -5. **Recognition**: With your permission, we'll acknowledge your contribution in the release notes - -## Security Best Practices for OxiCloud Users - -- Keep your OxiCloud installation updated to the latest version -- Use strong, unique passwords for all user accounts -- Configure proper file permissions -- Regularly back up your data -- Consider running OxiCloud behind a reverse proxy with HTTPS -- Implement IP restrictions where appropriate - -Thank you for helping keep OxiCloud and its users secure! \ No newline at end of file diff --git a/src/application/adapters/caldav_adapter.rs b/src/application/adapters/caldav_adapter.rs index 4b998218..ecd3776c 100644 --- a/src/application/adapters/caldav_adapter.rs +++ b/src/application/adapters/caldav_adapter.rs @@ -48,7 +48,6 @@ impl CalDavAdapter { let mut in_sync_collection = false; let mut in_prop = false; let mut in_filter = false; - let mut in_time_range = false; let mut start_time: Option> = None; let mut end_time: Option> = None; let mut props = Vec::new(); @@ -68,8 +67,6 @@ impl CalDavAdapter { s if s == "prop" || s.ends_with(":prop") => in_prop = true, s if s == "filter" || s.ends_with(":filter") => in_filter = true, s if s == "time-range" || s.ends_with(":time-range") => { - in_time_range = true; - // Parse time-range attributes for attr in e.attributes() { if let Ok(attr) = attr { @@ -106,7 +103,7 @@ impl CalDavAdapter { } }, Ok(Event::Text(e)) => { - let text = e.unescape().unwrap_or_default(); + let text = e.decode().unwrap_or_default(); // Check if we're in sync-token element if in_sync_collection && !in_prop && !in_filter { @@ -128,7 +125,7 @@ impl CalDavAdapter { s if s == "sync-collection" || s.ends_with(":sync-collection") => in_sync_collection = false, s if s == "prop" || s.ends_with(":prop") => in_prop = false, s if s == "filter" || s.ends_with(":filter") => in_filter = false, - s if s == "time-range" || s.ends_with(":time-range") => in_time_range = false, + s if s == "time-range" || s.ends_with(":time-range") => { /* time-range end, attributes already parsed */ }, _ => () } }, @@ -771,7 +768,7 @@ impl CalDavAdapter { } }, Ok(Event::Text(e)) => { - let text = e.unescape().unwrap_or_default(); + let text = e.decode().unwrap_or_default(); if in_displayname { displayname = text.to_string(); diff --git a/src/application/adapters/webdav_adapter.rs b/src/application/adapters/webdav_adapter.rs index e830e827..b7d7c21a 100644 --- a/src/application/adapters/webdav_adapter.rs +++ b/src/application/adapters/webdav_adapter.rs @@ -685,7 +685,7 @@ impl WebDavAdapter { }, Ok(Event::Text(e)) => { if current_prop.is_some() { - current_text.push_str(&e.unescape().unwrap_or_default()); + current_text.push_str(&e.decode().unwrap_or_default()); } }, Ok(Event::End(ref e)) => { @@ -879,7 +879,7 @@ impl WebDavAdapter { }, Ok(Event::Text(e)) => { if in_owner { - owner_text.push_str(&e.unescape().unwrap_or_default()); + owner_text.push_str(&e.decode().unwrap_or_default()); } }, Ok(Event::End(ref e)) => { diff --git a/src/application/dtos/calendar_dto.rs b/src/application/dtos/calendar_dto.rs index 31525025..314e1e48 100644 --- a/src/application/dtos/calendar_dto.rs +++ b/src/application/dtos/calendar_dto.rs @@ -1,6 +1,5 @@ use serde::{Serialize, Deserialize}; use chrono::{DateTime, Utc}; -use uuid::Uuid; use std::collections::HashMap; use crate::domain::entities::calendar::Calendar; use crate::domain::entities::calendar_event::CalendarEvent; diff --git a/src/application/dtos/user_dto.rs b/src/application/dtos/user_dto.rs index 4f03256c..fa2010d8 100644 --- a/src/application/dtos/user_dto.rs +++ b/src/application/dtos/user_dto.rs @@ -65,4 +65,13 @@ pub struct ChangePasswordDto { #[derive(Debug, Serialize, Deserialize)] pub struct RefreshTokenDto { pub refresh_token: String, +} + +/// Datos del usuario autenticado actual (para uso en servicios de application) +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CurrentUser { + pub id: String, + pub username: String, + pub email: String, + pub role: String, } \ No newline at end of file diff --git a/src/application/ports/auth_ports.rs b/src/application/ports/auth_ports.rs index 7c854d29..0150240d 100644 --- a/src/application/ports/auth_ports.rs +++ b/src/application/ports/auth_ports.rs @@ -3,6 +3,66 @@ use crate::domain::entities::user::User; use crate::domain::entities::session::Session; use crate::common::errors::DomainError; +// ============================================================================ +// Cryptography Ports - Extracted from Domain to maintain Clean Architecture +// ============================================================================ + +/// Port for password hashing operations. +/// +/// This trait abstracts cryptographic password operations, allowing the domain +/// layer to remain independent of specific hashing implementations (argon2, bcrypt, etc.) +pub trait PasswordHasherPort: Send + Sync + 'static { + /// Hash a plain text password + fn hash_password(&self, password: &str) -> Result; + + /// Verify a plain text password against a hash + fn verify_password(&self, password: &str, hash: &str) -> Result; +} + +/// Claims contained in a JWT token +#[derive(Debug, Clone)] +pub struct TokenClaims { + /// Subject identifier (user ID) + pub sub: String, + /// Expiration timestamp (seconds since Unix epoch) + pub exp: i64, + /// Issued at timestamp (seconds since Unix epoch) + pub iat: i64, + /// JWT unique ID + pub jti: String, + /// Username + pub username: String, + /// User email + pub email: String, + /// User role + pub role: String, +} + +/// Port for JWT token operations. +/// +/// This trait abstracts token generation and validation, allowing the domain +/// layer to remain independent of specific JWT implementations. +pub trait TokenServicePort: Send + Sync + 'static { + /// Generate an access token for a user + fn generate_access_token(&self, user: &User) -> Result; + + /// Validate a token and extract its claims + fn validate_token(&self, token: &str) -> Result; + + /// Generate a refresh token + fn generate_refresh_token(&self) -> String; + + /// Get refresh token expiry in seconds + fn refresh_token_expiry_secs(&self) -> i64; + + /// Get refresh token expiry in days + fn refresh_token_expiry_days(&self) -> i64; +} + +// ============================================================================ +// Storage Ports +// ============================================================================ + #[async_trait] pub trait UserStoragePort: Send + Sync + 'static { /// Crea un nuevo usuario diff --git a/src/application/services/auth_application_service.rs b/src/application/services/auth_application_service.rs index 3ed15e9d..b8bd8d7e 100644 --- a/src/application/services/auth_application_service.rs +++ b/src/application/services/auth_application_service.rs @@ -1,8 +1,7 @@ use std::sync::Arc; use crate::domain::entities::user::{User, UserRole}; use crate::domain::entities::session::Session; -use crate::domain::services::auth_service::AuthService; -use crate::application::ports::auth_ports::{UserStoragePort, SessionStoragePort}; +use crate::application::ports::auth_ports::{UserStoragePort, SessionStoragePort, PasswordHasherPort, TokenServicePort}; use crate::application::dtos::user_dto::{UserDto, RegisterDto, LoginDto, AuthResponseDto, ChangePasswordDto, RefreshTokenDto}; use crate::application::dtos::folder_dto::CreateFolderDto; use crate::application::ports::inbound::FolderUseCase; @@ -11,7 +10,8 @@ use crate::common::errors::{DomainError, ErrorKind}; pub struct AuthApplicationService { user_storage: Arc, session_storage: Arc, - auth_service: Arc, + password_hasher: Arc, + token_service: Arc, folder_service: Option>, } @@ -19,12 +19,14 @@ impl AuthApplicationService { pub fn new( user_storage: Arc, session_storage: Arc, - auth_service: Arc, + password_hasher: Arc, + token_service: Arc, ) -> Self { Self { user_storage, session_storage, - auth_service, + password_hasher, + token_service, folder_service: None, } } @@ -127,11 +129,23 @@ impl AuthApplicationService { 1024 * 1024 * 1024 // 1GB para usuarios normales }; - // Crear usuario + // Validar longitud de password antes de hashear + if dto.password.len() < 8 { + return Err(DomainError::new( + ErrorKind::InvalidInput, + "User", + "Password debe tener al menos 8 caracteres" + )); + } + + // Hashear el password usando el servicio de infraestructura + let password_hash = self.password_hasher.hash_password(&dto.password)?; + + // Crear usuario con el hash pre-generado let user = User::new( dto.username.clone(), dto.email, - dto.password, + password_hash, role, quota, ).map_err(|e| DomainError::new( @@ -203,13 +217,8 @@ impl AuthApplicationService { )); } - // Verificar contraseña - let is_valid = user.verify_password(&dto.password) - .map_err(|_| DomainError::new( - ErrorKind::AccessDenied, - "Auth", - "Credenciales inválidas" - ))?; + // Verificar contraseña usando el hasher inyectado + let is_valid = self.password_hasher.verify_password(&dto.password, user.password_hash())?; if !is_valid { return Err(DomainError::new( @@ -223,11 +232,10 @@ impl AuthApplicationService { user.register_login(); self.user_storage.update_user(user.clone()).await?; - // Generar tokens - let access_token = self.auth_service.generate_access_token(&user) - .map_err(DomainError::from)?; + // Generar tokens usando el servicio de tokens inyectado + let access_token = self.token_service.generate_access_token(&user)?; - let refresh_token = self.auth_service.generate_refresh_token(); + let refresh_token = self.token_service.generate_refresh_token(); // Guardar sesión let session = Session::new( @@ -235,7 +243,7 @@ impl AuthApplicationService { refresh_token.clone(), None, // IP (se puede añadir desde la capa HTTP) None, // User-Agent (se puede añadir desde la capa HTTP) - self.auth_service.refresh_token_expiry_days(), + self.token_service.refresh_token_expiry_days(), ); self.session_storage.create_session(session).await?; @@ -246,7 +254,7 @@ impl AuthApplicationService { access_token, refresh_token, token_type: "Bearer".to_string(), - expires_in: self.auth_service.refresh_token_expiry_secs(), + expires_in: self.token_service.refresh_token_expiry_secs(), }) } @@ -283,10 +291,9 @@ impl AuthApplicationService { self.session_storage.revoke_session(session.id()).await?; // Generar nuevos tokens - let access_token = self.auth_service.generate_access_token(&user) - .map_err(DomainError::from)?; + let access_token = self.token_service.generate_access_token(&user)?; - let new_refresh_token = self.auth_service.generate_refresh_token(); + let new_refresh_token = self.token_service.generate_refresh_token(); // Crear nueva sesión let new_session = Session::new( @@ -294,7 +301,7 @@ impl AuthApplicationService { new_refresh_token.clone(), None, None, - self.auth_service.refresh_token_expiry_days(), + self.token_service.refresh_token_expiry_days(), ); self.session_storage.create_session(new_session).await?; @@ -304,7 +311,7 @@ impl AuthApplicationService { access_token, refresh_token: new_refresh_token, token_type: "Bearer".to_string(), - expires_in: self.auth_service.refresh_token_expiry_secs(), + expires_in: self.token_service.refresh_token_expiry_secs(), }) } @@ -342,13 +349,8 @@ impl AuthApplicationService { // Obtener usuario let mut user = self.user_storage.get_user_by_id(user_id).await?; - // Verificar contraseña actual - let is_valid = user.verify_password(&dto.current_password) - .map_err(|_| DomainError::new( - ErrorKind::AccessDenied, - "Auth", - "Contraseña actual incorrecta" - ))?; + // Verificar contraseña actual usando el hasher inyectado + let is_valid = self.password_hasher.verify_password(&dto.current_password, user.password_hash())?; if !is_valid { return Err(DomainError::new( @@ -358,13 +360,18 @@ impl AuthApplicationService { )); } - // Actualizar contraseña - user.update_password(dto.new_password.clone()) - .map_err(|e| DomainError::new( + // Validar nueva contraseña + if dto.new_password.len() < 8 { + return Err(DomainError::new( ErrorKind::InvalidInput, "User", - format!("Error al cambiar contraseña: {}", e) - ))?; + "Password debe tener al menos 8 caracteres" + )); + } + + // Hashear nueva contraseña y actualizar usuario + let new_hash = self.password_hasher.hash_password(&dto.new_password)?; + user.update_password_hash(new_hash); // Guardar usuario actualizado self.user_storage.update_user(user).await?; diff --git a/src/application/services/batch_operations.rs b/src/application/services/batch_operations.rs index c07fbc58..16fec11a 100644 --- a/src/application/services/batch_operations.rs +++ b/src/application/services/batch_operations.rs @@ -1,12 +1,11 @@ use std::sync::Arc; -use thiserror::Error; use futures::{future::join_all, Future}; use tokio::sync::Semaphore; -use tracing::{info, error}; +use tracing::info; +use thiserror::Error; use crate::application::services::file_service::FileService; use crate::application::services::folder_service::FolderService; -use crate::domain::services::path_service::StoragePath; use crate::common::errors::DomainError; use crate::common::config::AppConfig; use crate::application::ports::inbound::FolderUseCase; @@ -15,7 +14,6 @@ use crate::application::dtos::folder_dto::FolderDto; /// Errores específicos para operaciones por lotes #[derive(Debug, Error)] -#[allow(dead_code)] pub enum BatchOperationError { #[error("Error de dominio: {0}")] Domain(#[from] DomainError), @@ -59,52 +57,6 @@ pub struct BatchStats { pub max_concurrency: usize, } -/// Tipo de entidad para operaciones por lotes -#[derive(Debug, Clone, PartialEq, Eq)] -#[allow(dead_code)] -pub enum EntityType { - File, - Folder, -} - -/// Tipo de operación por lotes -#[derive(Debug, Clone, PartialEq, Eq)] -#[allow(dead_code)] -pub enum BatchOperationType { - Create, - Read, - Update, - Delete, - Copy, - Move, -} - -/// Identificador para una entidad (ID o ruta) -#[derive(Debug, Clone)] -#[allow(dead_code)] -pub enum EntityIdentifier { - Id(String), - Path(StoragePath), -} - -impl EntityIdentifier { - #[allow(dead_code)] - pub fn as_id(&self) -> Option<&str> { - match self { - EntityIdentifier::Id(id) => Some(id), - _ => None, - } - } - - #[allow(dead_code)] - pub fn as_path(&self) -> Option<&StoragePath> { - match self { - EntityIdentifier::Path(path) => Some(path), - _ => None, - } - } -} - /// Servicio de operaciones por lotes pub struct BatchOperationService { file_service: Arc, @@ -494,7 +446,6 @@ impl BatchOperationService { } /// Operación genérica de lote para cualquier tipo de función asíncrona - #[allow(dead_code)] pub async fn generic_batch_operation( &self, items: Vec, diff --git a/src/application/services/calendar_service.rs b/src/application/services/calendar_service.rs index 195b5598..ff7be3fc 100644 --- a/src/application/services/calendar_service.rs +++ b/src/application/services/calendar_service.rs @@ -1,14 +1,12 @@ use std::sync::Arc; use async_trait::async_trait; use chrono::{DateTime, Utc}; -use uuid::Uuid; use crate::application::dtos::calendar_dto::{ CalendarDto, CalendarEventDto, CreateCalendarDto, UpdateCalendarDto, CreateEventDto, UpdateEventDto, CreateEventICalDto }; use crate::application::ports::calendar_ports::{CalendarStoragePort, CalendarUseCase}; -use crate::interfaces::middleware::auth::CurrentUser; use crate::common::errors::{DomainError, ErrorKind}; pub struct CalendarService { diff --git a/src/application/services/contact_service.rs b/src/application/services/contact_service.rs index 495556e8..bbef000e 100644 --- a/src/application/services/contact_service.rs +++ b/src/application/services/contact_service.rs @@ -9,12 +9,11 @@ use crate::application::dtos::address_book_dto::{ }; use crate::application::dtos::contact_dto::{ ContactDto, CreateContactDto, UpdateContactDto, CreateContactVCardDto, - ContactGroupDto, CreateContactGroupDto, UpdateContactGroupDto, GroupMembershipDto, - EmailDto, PhoneDto, AddressDto + ContactGroupDto, CreateContactGroupDto, UpdateContactGroupDto, GroupMembershipDto }; use crate::application::ports::carddav_ports::{AddressBookUseCase, ContactUseCase}; use crate::application::ports::storage_ports::StorageUseCase; -use crate::common::errors::{DomainError, ErrorContext}; +use crate::common::errors::DomainError; use crate::domain::entities::contact::{AddressBook, Contact, ContactGroup, Email, Phone, Address}; use crate::domain::repositories::address_book_repository::AddressBookRepository; use crate::domain::repositories::contact_repository::{ContactRepository, ContactGroupRepository}; diff --git a/src/application/services/file_management_service.rs b/src/application/services/file_management_service.rs index f29b81ff..19c08649 100644 --- a/src/application/services/file_management_service.rs +++ b/src/application/services/file_management_service.rs @@ -16,13 +16,6 @@ impl FileManagementService { pub fn new(file_repository: Arc) -> Self { Self { file_repository } } - - /// Creates a stub for testing - pub fn default_stub() -> Self { - Self { - file_repository: Arc::new(crate::infrastructure::repositories::FileFsWriteRepository::default_stub()) - } - } } #[async_trait] diff --git a/src/application/services/file_retrieval_service.rs b/src/application/services/file_retrieval_service.rs index f8f7da69..55bdd5c8 100644 --- a/src/application/services/file_retrieval_service.rs +++ b/src/application/services/file_retrieval_service.rs @@ -18,13 +18,6 @@ impl FileRetrievalService { pub fn new(file_repository: Arc) -> Self { Self { file_repository } } - - /// Crea un stub para pruebas - pub fn default_stub() -> Self { - Self { - file_repository: Arc::new(crate::infrastructure::repositories::FileFsReadRepository::default_stub()) - } - } } #[async_trait] diff --git a/src/application/services/file_upload_service.rs b/src/application/services/file_upload_service.rs index 2ed71a06..ea15bd23 100644 --- a/src/application/services/file_upload_service.rs +++ b/src/application/services/file_upload_service.rs @@ -48,14 +48,6 @@ impl FileUploadService { self.storage_usage_service = Some(storage_usage_service); self } - - /// Crea un stub para pruebas - pub fn default_stub() -> Self { - Self { - file_repository: Arc::new(crate::infrastructure::repositories::FileFsWriteRepository::default_stub()), - storage_usage_service: None, - } - } } #[async_trait] diff --git a/src/application/services/file_use_case_factory.rs b/src/application/services/file_use_case_factory.rs index f4efca8d..c26f5b17 100644 --- a/src/application/services/file_use_case_factory.rs +++ b/src/application/services/file_use_case_factory.rs @@ -23,14 +23,6 @@ impl AppFileUseCaseFactory { file_write_repository, } } - - /// Crea un stub para pruebas - pub fn default_stub() -> Self { - Self { - file_read_repository: Arc::new(crate::infrastructure::repositories::FileFsReadRepository::default_stub()), - file_write_repository: Arc::new(crate::infrastructure::repositories::FileFsWriteRepository::default_stub()), - } - } } impl FileUseCaseFactory for AppFileUseCaseFactory { diff --git a/src/application/services/i18n_application_service.rs b/src/application/services/i18n_application_service.rs index 6a05606c..3a4b8817 100644 --- a/src/application/services/i18n_application_service.rs +++ b/src/application/services/i18n_application_service.rs @@ -51,7 +51,6 @@ impl I18nApplicationService { } /// Load translations for all available locales - #[allow(dead_code)] pub async fn load_all_translations(&self) -> Vec<(Locale, I18nResult<()>)> { let locales = self.i18n_service.available_locales().await; let mut results = Vec::new(); @@ -70,7 +69,6 @@ impl I18nApplicationService { } /// Check if a locale is supported - #[allow(dead_code)] pub async fn is_supported(&self, locale: Locale) -> bool { self.i18n_service.is_supported(locale).await } diff --git a/src/application/services/storage_mediator.rs b/src/application/services/storage_mediator.rs index 33188e2c..3df8e135 100644 --- a/src/application/services/storage_mediator.rs +++ b/src/application/services/storage_mediator.rs @@ -7,8 +7,8 @@ use thiserror::Error; use crate::domain::entities::folder::Folder; use crate::domain::repositories::folder_repository::{FolderRepository, FolderRepositoryError}; use crate::domain::repositories::file_repository::FileRepositoryError; -use crate::domain::services::path_service::{PathService, StoragePath}; -use crate::application::ports::outbound::IdMappingPort; +use crate::domain::services::path_service::StoragePath; +use crate::application::ports::outbound::{IdMappingPort, StoragePort}; /// Errores específicos del mediador de almacenamiento #[derive(Debug, Error)] @@ -99,12 +99,12 @@ pub trait StorageMediator: Send + Sync + 'static { /// Implementación concreta del mediador de almacenamiento pub struct FileSystemStorageMediator { pub folder_repository: Arc, - pub path_service: Arc, + pub path_service: Arc, pub id_mapping: Arc, } impl FileSystemStorageMediator { - pub fn new(folder_repository: Arc, path_service: Arc, id_mapping: Arc) -> Self { + pub fn new(folder_repository: Arc, path_service: Arc, id_mapping: Arc) -> Self { Self { folder_repository, path_service, id_mapping } } @@ -116,7 +116,7 @@ impl FileSystemStorageMediator { /// Overload para implementar inicialización diferida con repository placeholder pub fn new_with_lazy_folder( _folder_repository: Arc>>>, - path_service: Arc, + path_service: Arc, id_mapping: Arc ) -> Self { // Create temporary stub repository @@ -208,16 +208,18 @@ impl FolderRepository for FolderRepositoryStub { } /// Stub implementation for initialization dependency issues -pub struct StubStorageMediator { - #[allow(dead_code)] - _path_service: Arc, -} +/// This is a minimal stub that doesn't require any infrastructure dependencies +pub struct StubStorageMediator; impl StubStorageMediator { pub fn new() -> Self { - let root_path = PathBuf::from("/tmp"); - let path_service = Arc::new(PathService::new(root_path)); - Self { _path_service: path_service } + Self + } +} + +impl Default for StubStorageMediator { + fn default() -> Self { + Self::new() } } diff --git a/src/application/services/storage_usage_service.rs b/src/application/services/storage_usage_service.rs index 83c2e8fe..47472d03 100644 --- a/src/application/services/storage_usage_service.rs +++ b/src/application/services/storage_usage_service.rs @@ -3,7 +3,7 @@ use async_trait::async_trait; use tokio::task; use crate::common::errors::DomainError; use crate::application::ports::auth_ports::UserStoragePort; -use crate::domain::repositories::file_repository::FileRepository; +use crate::application::ports::outbound::FileStoragePort; use crate::application::ports::storage_ports::StorageUsagePort; use tracing::{info, error, debug}; @@ -14,14 +14,14 @@ use tracing::{info, error, debug}; * is using and updating this information in the user records. */ pub struct StorageUsageService { - file_repository: Arc, + file_repository: Arc, user_repository: Arc, } impl StorageUsageService { /// Creates a new storage usage service pub fn new( - file_repository: Arc, + file_repository: Arc, user_repository: Arc, ) -> Self { Self { @@ -90,7 +90,7 @@ impl StorageUsageService { async fn calculate_folder_size(&self, folder_id: &str) -> Result { // Implementation with explicit boxing to handle recursion in async functions async fn inner_calculate_size( - repo: Arc, + repo: Arc, folder_id: &str, ) -> Result { let mut total_size: i64 = 0; diff --git a/src/application/transactions/storage_transaction.rs b/src/application/transactions/storage_transaction.rs index df39336c..06782cb0 100644 --- a/src/application/transactions/storage_transaction.rs +++ b/src/application/transactions/storage_transaction.rs @@ -37,7 +37,6 @@ impl StorageTransaction { } /// Añade una operación sin rollback (para limpieza o logging) - #[allow(dead_code)] pub fn add_finalizer(&mut self, finalizer: F) where F: Future> + Send + 'static, diff --git a/src/common/adapters.rs b/src/common/adapters.rs new file mode 100644 index 00000000..92fdec5a --- /dev/null +++ b/src/common/adapters.rs @@ -0,0 +1,263 @@ +//! Adaptadores para convertir entre interfaces de dominio y aplicación +//! +//! Este módulo contiene adaptadores que permiten usar repositorios que implementan +//! `FileStoragePort` y `FolderStoragePort` donde se espera `FileRepository` y `FolderRepository`. + +use std::sync::Arc; +use async_trait::async_trait; + +use crate::application::ports::outbound::{FileStoragePort, FolderStoragePort}; +use crate::domain::entities::file::File; +use crate::domain::entities::folder::Folder; +use crate::domain::repositories::file_repository::{FileRepository, FileRepositoryResult, FileRepositoryError}; +use crate::domain::repositories::folder_repository::{FolderRepository, FolderRepositoryResult, FolderRepositoryError}; +use crate::domain::services::path_service::StoragePath; + +/// Adaptador que convierte FileStoragePort a FileRepository +pub struct DomainFileRepoAdapter { + repo: Arc, +} + +impl DomainFileRepoAdapter { + pub fn new(repo: Arc) -> Self { + Self { repo } + } +} + +#[async_trait] +impl FileRepository for DomainFileRepoAdapter { + async fn save_file_from_bytes( + &self, + name: String, + folder_id: Option, + content_type: String, + content: Vec, + ) -> FileRepositoryResult { + self.repo.save_file(name, folder_id, content_type, content) + .await + .map_err(|e| FileRepositoryError::Other(format!("{}", e))) + } + + async fn save_file_with_id( + &self, + _id: String, + _name: String, + _folder_id: Option, + _content_type: String, + _content: Vec, + ) -> FileRepositoryResult { + Err(FileRepositoryError::Other("Not implemented".to_string())) + } + + async fn get_file_by_id(&self, id: &str) -> FileRepositoryResult { + self.repo.get_file(id) + .await + .map_err(|e| FileRepositoryError::Other(format!("{}", e))) + } + + async fn list_files(&self, folder_id: Option<&str>) -> FileRepositoryResult> { + self.repo.list_files(folder_id) + .await + .map_err(|e| FileRepositoryError::Other(format!("{}", e))) + } + + async fn delete_file(&self, id: &str) -> FileRepositoryResult<()> { + self.repo.delete_file(id) + .await + .map_err(|e| FileRepositoryError::Other(format!("{}", e))) + } + + async fn delete_file_entry(&self, id: &str) -> FileRepositoryResult<()> { + self.delete_file(id).await + } + + async fn get_file_content(&self, id: &str) -> FileRepositoryResult> { + self.repo.get_file_content(id) + .await + .map_err(|e| FileRepositoryError::Other(format!("{}", e))) + } + + async fn get_file_stream(&self, id: &str) -> FileRepositoryResult> + Send>> { + self.repo.get_file_stream(id) + .await + .map_err(|e| FileRepositoryError::Other(format!("{}", e))) + } + + async fn move_file(&self, id: &str, target_folder_id: Option) -> FileRepositoryResult { + self.repo.move_file(id, target_folder_id) + .await + .map_err(|e| FileRepositoryError::Other(format!("{}", e))) + } + + async fn get_file_path(&self, id: &str) -> FileRepositoryResult { + self.repo.get_file_path(id) + .await + .map_err(|e| FileRepositoryError::Other(format!("{}", e))) + } + + async fn move_to_trash(&self, file_id: &str) -> FileRepositoryResult<()> { + self.repo.delete_file(file_id) + .await + .map_err(|e| FileRepositoryError::Other(format!("{}", e))) + } + + async fn restore_from_trash(&self, file_id: &str, original_path: &str) -> FileRepositoryResult<()> { + tracing::info!("Restoring file from trash: {} to {}", file_id, original_path); + + match self.repo.get_file(file_id).await { + Ok(_) => { + let path_components: Vec<&str> = original_path.split('/').collect(); + let parent_folder: Option = if path_components.len() > 1 { + tracing::info!("Attempting to restore to parent folder from path: {}", original_path); + None + } else { + None + }; + + match self.repo.move_file(file_id, parent_folder).await { + Ok(_) => { + tracing::info!("Successfully restored file from trash: {}", file_id); + Ok(()) + }, + Err(e) => { + tracing::error!("Failed to restore file from trash: {}", e); + Err(FileRepositoryError::Other(format!("Failed to restore file: {}", e))) + } + } + }, + Err(e) => { + tracing::error!("File not found in trash: {}", e); + Err(FileRepositoryError::NotFound(file_id.to_string())) + } + } + } + + async fn delete_file_permanently(&self, file_id: &str) -> FileRepositoryResult<()> { + tracing::info!("Permanently deleting file: {}", file_id); + + match self.repo.delete_file(file_id).await { + Ok(_) => { + tracing::info!("Successfully deleted file permanently: {}", file_id); + Ok(()) + }, + Err(e) => { + tracing::error!("Failed to permanently delete file: {}", e); + Err(FileRepositoryError::Other(format!("Failed to delete file permanently: {}", e))) + } + } + } + + async fn update_file_content(&self, file_id: &str, content: Vec) -> FileRepositoryResult<()> { + tracing::info!("Updating content for file: {}", file_id); + + self.repo.update_file_content(file_id, content) + .await + .map_err(|e| { + tracing::error!("Failed to update file content: {}", e); + FileRepositoryError::Other(format!("Failed to update file content: {}", e)) + }) + } +} + +/// Adaptador que convierte FolderStoragePort a FolderRepository +pub struct DomainFolderRepoAdapter { + repo: Arc, +} + +impl DomainFolderRepoAdapter { + pub fn new(repo: Arc) -> Self { + Self { repo } + } +} + +#[async_trait] +impl FolderRepository for DomainFolderRepoAdapter { + async fn create_folder(&self, name: String, parent_id: Option) -> FolderRepositoryResult { + self.repo.create_folder(name, parent_id) + .await + .map_err(|e| FolderRepositoryError::Other(format!("{}", e))) + } + + async fn get_folder_by_id(&self, id: &str) -> FolderRepositoryResult { + self.repo.get_folder(id) + .await + .map_err(|e| FolderRepositoryError::Other(format!("{}", e))) + } + + async fn get_folder_by_storage_path(&self, storage_path: &StoragePath) -> FolderRepositoryResult { + self.repo.get_folder_by_path(storage_path) + .await + .map_err(|e| FolderRepositoryError::Other(format!("{}", e))) + } + + async fn list_folders(&self, parent_id: Option<&str>) -> FolderRepositoryResult> { + self.repo.list_folders(parent_id) + .await + .map_err(|e| FolderRepositoryError::Other(format!("{}", e))) + } + + async fn list_folders_paginated( + &self, + parent_id: Option<&str>, + offset: usize, + limit: usize, + include_total: bool + ) -> FolderRepositoryResult<(Vec, Option)> { + self.repo.list_folders_paginated(parent_id, offset, limit, include_total) + .await + .map_err(|e| FolderRepositoryError::Other(format!("{}", e))) + } + + async fn rename_folder(&self, id: &str, new_name: String) -> FolderRepositoryResult { + self.repo.rename_folder(id, new_name) + .await + .map_err(|e| FolderRepositoryError::Other(format!("{}", e))) + } + + async fn move_folder(&self, id: &str, new_parent_id: Option<&str>) -> FolderRepositoryResult { + self.repo.move_folder(id, new_parent_id) + .await + .map_err(|e| FolderRepositoryError::Other(format!("{}", e))) + } + + async fn delete_folder(&self, id: &str) -> FolderRepositoryResult<()> { + self.repo.delete_folder(id) + .await + .map_err(|e| FolderRepositoryError::Other(format!("{}", e))) + } + + async fn folder_exists_at_storage_path(&self, storage_path: &StoragePath) -> FolderRepositoryResult { + self.repo.folder_exists(storage_path) + .await + .map_err(|e| FolderRepositoryError::Other(format!("{}", e))) + } + + async fn get_folder_storage_path(&self, id: &str) -> FolderRepositoryResult { + self.repo.get_folder_path(id) + .await + .map_err(|e| FolderRepositoryError::Other(format!("{}", e))) + } + + async fn folder_exists(&self, _path: &std::path::PathBuf) -> FolderRepositoryResult { + Err(FolderRepositoryError::Other("Not implemented".to_string())) + } + + async fn get_folder_by_path(&self, _path: &std::path::PathBuf) -> FolderRepositoryResult { + Err(FolderRepositoryError::Other("Not implemented".to_string())) + } + + async fn move_to_trash(&self, folder_id: &str) -> FolderRepositoryResult<()> { + self.repo.delete_folder(folder_id) + .await + .map_err(|e| FolderRepositoryError::Other(format!("{}", e))) + } + + async fn restore_from_trash(&self, _folder_id: &str, _original_path: &str) -> FolderRepositoryResult<()> { + Err(FolderRepositoryError::Other( + "Restore from trash should be handled by TrashService, not through this adapter".to_string())) + } + + async fn delete_folder_permanently(&self, folder_id: &str) -> FolderRepositoryResult<()> { + self.delete_folder(folder_id).await + } +} diff --git a/src/common/auth_factory.rs b/src/common/auth_factory.rs index c010b35f..06a407e5 100644 --- a/src/common/auth_factory.rs +++ b/src/common/auth_factory.rs @@ -2,10 +2,12 @@ use std::sync::Arc; use anyhow::Result; use sqlx::PgPool; -use crate::domain::services::auth_service::AuthService; +use crate::application::ports::auth_ports::TokenServicePort; use crate::application::services::auth_application_service::AuthApplicationService; use crate::application::services::folder_service::FolderService; use crate::infrastructure::repositories::{UserPgRepository, SessionPgRepository}; +use crate::infrastructure::services::password_hasher::Argon2PasswordHasher; +use crate::infrastructure::services::jwt_service::JwtTokenService; use crate::common::config::AppConfig; use crate::common::di::AuthServices; @@ -14,13 +16,16 @@ pub async fn create_auth_services( pool: Arc, folder_service: Option> ) -> Result { - // Crear servicio de dominio de autenticación - let auth_service = Arc::new(AuthService::new( + // Crear servicio de tokens JWT (implementación de TokenServicePort) + let token_service: Arc = Arc::new(JwtTokenService::new( config.auth.jwt_secret.clone(), config.auth.access_token_expiry_secs, config.auth.refresh_token_expiry_secs, )); + // Crear servicio de hashing de contraseñas + let password_hasher = Arc::new(Argon2PasswordHasher::new()); + // Crear repositorios PostgreSQL let user_repository = Arc::new(UserPgRepository::new(pool.clone())); let session_repository = Arc::new(SessionPgRepository::new(pool.clone())); @@ -29,7 +34,8 @@ pub async fn create_auth_services( let mut auth_app_service = AuthApplicationService::new( user_repository, session_repository, - auth_service.clone(), + password_hasher, + token_service.clone(), ); // Configurar servicio de carpetas si está disponible @@ -41,7 +47,7 @@ pub async fn create_auth_services( let auth_application_service = Arc::new(auth_app_service); Ok(AuthServices { - auth_service, + token_service, auth_application_service, }) } \ No newline at end of file diff --git a/src/common/cache.rs b/src/common/cache.rs deleted file mode 100644 index 543a0e61..00000000 --- a/src/common/cache.rs +++ /dev/null @@ -1,208 +0,0 @@ -use std::collections::HashMap; -use std::hash::Hash; -use std::sync::Arc; -use std::time::{Duration, Instant}; -use tokio::sync::RwLock; -use crate::common::errors::{DomainError, ErrorKind}; - -/// Entrada de caché con tiempo de expiración -#[derive(Debug, Clone)] -#[allow(dead_code)] -struct CacheEntry { - value: V, - expiry: Instant, -} - -impl CacheEntry { - /// Crea una nueva entrada en la caché - #[allow(dead_code)] - fn new(value: V, ttl: Duration) -> Self { - Self { - value, - expiry: Instant::now() + ttl, - } - } - - /// Verifica si la entrada ha expirado - #[allow(dead_code)] - fn is_expired(&self) -> bool { - Instant::now() > self.expiry - } -} - -/// Servicio genérico de caché con TTL -#[allow(dead_code)] -pub struct CacheService { - cache: Arc>>>, - ttl: Duration, - max_entries: usize, -} - -impl CacheService -where - K: Hash + Eq + Clone + Send + Sync + 'static + std::fmt::Debug, - V: Clone + Send + Sync + 'static, -{ - /// Crea un nuevo servicio de caché - #[allow(dead_code)] - pub fn new(ttl: Duration, max_entries: usize) -> Self { - Self { - cache: Arc::new(RwLock::new(HashMap::new())), - ttl, - max_entries, - } - } - - /// Obtiene un valor de la caché o lo inserta si no existe - #[allow(dead_code)] - pub async fn get_or_insert(&self, key: K, loader: F) -> Result - where - F: FnOnce() -> Result, - E: std::error::Error + Send + Sync + 'static, - { - // Intentar leer de la caché primero - { - let cache = self.cache.read().await; - if let Some(entry) = cache.get(&key) { - if !entry.is_expired() { - tracing::debug!("Cache hit for key: {:?}", key); - return Ok(entry.value.clone()); - } - } - } - - // Cache miss o entrada expirada, obtener valor y actualizar - let value = loader().map_err(|e| { - DomainError::new( - ErrorKind::InternalError, - "Cache", - format!("Failed to load value for cache: {}", e), - ) - .with_source(e) - })?; - - // Insertar en la caché - { - let mut cache = self.cache.write().await; - - // Si alcanzamos el límite, eliminar una entrada aleatoria - if cache.len() >= self.max_entries { - if let Some(expired_key) = cache - .iter() - .find(|(_, v)| v.is_expired()) - .map(|(k, _)| k.clone()) - { - cache.remove(&expired_key); - } else if let Some(random_key) = cache.keys().next().cloned() { - cache.remove(&random_key); - } - } - - cache.insert(key.clone(), CacheEntry::new(value.clone(), self.ttl)); - } - - tracing::debug!("Cache miss for key: {:?}, value loaded and cached", key); - Ok(value) - } - - /// Invalida una entrada específica de la caché - #[allow(dead_code)] - pub async fn invalidate(&self, key: &K) { - let mut cache = self.cache.write().await; - cache.remove(key); - tracing::debug!("Cache entry invalidated for key: {:?}", key); - } - - /// Invalida todas las entradas de la caché - #[allow(dead_code)] - pub async fn invalidate_all(&self) { - let mut cache = self.cache.write().await; - cache.clear(); - tracing::debug!("Cache fully invalidated"); - } - - /// Obtiene el número de entradas en la caché - #[allow(dead_code)] - pub async fn len(&self) -> usize { - self.cache.read().await.len() - } - - /// Limpia las entradas expiradas de la caché - #[allow(dead_code)] - pub async fn cleanup_expired(&self) -> usize { - let mut cache = self.cache.write().await; - let initial_len = cache.len(); - - cache.retain(|_, v| !v.is_expired()); - - let removed = initial_len - cache.len(); - if removed > 0 { - tracing::debug!("Removed {} expired cache entries", removed); - } - - removed - } -} - -/// Caché específica para metadatos de archivos -#[derive(Debug, Clone)] -#[allow(dead_code)] -pub struct FileMetadata { - pub size: u64, - pub created_at: u64, - pub modified_at: u64, - pub is_dir: bool, -} - -/// Gestor de caché para operaciones comunes de almacenamiento -#[allow(dead_code)] -pub struct CacheManager { - /// Caché para metadatos de archivos/carpetas - metadata_cache: CacheService, - /// Caché para verificación de existencia de archivos - existence_cache: CacheService, -} - -impl CacheManager { - /// Crea un nuevo gestor de caché - #[allow(dead_code)] - pub fn new(metadata_ttl: Duration, existence_ttl: Duration) -> Self { - Self { - metadata_cache: CacheService::new(metadata_ttl, 10000), // Caché para 10,000 elementos - existence_cache: CacheService::new(existence_ttl, 20000), // Caché para 20,000 elementos - } - } - - /// Obtiene o carga los metadatos de un archivo/carpeta - #[allow(dead_code)] - pub async fn get_metadata(&self, path: std::path::PathBuf, loader: F) -> Result - where - F: FnOnce() -> Result, - { - self.metadata_cache.get_or_insert(path, loader).await - } - - /// Verifica o determina si un archivo/carpeta existe - #[allow(dead_code)] - pub async fn check_exists(&self, path: std::path::PathBuf, checker: F) -> Result - where - F: FnOnce() -> Result, - { - self.existence_cache.get_or_insert(path, checker).await - } - - /// Invalida la caché para una ruta específica - #[allow(dead_code)] - pub async fn invalidate_path(&self, path: &std::path::Path) { - self.metadata_cache.invalidate(&path.to_path_buf()).await; - self.existence_cache.invalidate(&path.to_path_buf()).await; - } - - /// Limpia todas las entradas expiradas - #[allow(dead_code)] - pub async fn cleanup(&self) -> (usize, usize) { - let metadata_cleaned = self.metadata_cache.cleanup_expired().await; - let existence_cleaned = self.existence_cache.cleanup_expired().await; - (metadata_cleaned, existence_cleaned) - } -} \ No newline at end of file diff --git a/src/common/config.rs b/src/common/config.rs index e4961772..04b1bf0b 100644 --- a/src/common/config.rs +++ b/src/common/config.rs @@ -33,7 +33,6 @@ pub struct TimeoutConfig { /// Timeout para adquisición de locks (ms) pub lock_acquisition_ms: u64, /// Timeout para operaciones de red (ms) - #[allow(dead_code)] pub network_operation_ms: u64, } @@ -80,7 +79,6 @@ impl TimeoutConfig { } /// Obtiene un Duration para operaciones de red - #[allow(dead_code)] pub fn network_timeout(&self) -> Duration { Duration::from_millis(self.network_operation_ms) } @@ -92,7 +90,6 @@ pub struct ResourceConfig { /// Umbral en MB para considerar un archivo como grande pub large_file_threshold_mb: u64, /// Umbral de entradas para considerar un directorio como grande - #[allow(dead_code)] pub large_dir_threshold_entries: usize, /// Tamaño de chunk para procesamiento de archivos grandes (bytes) pub chunk_size_bytes: usize, @@ -133,7 +130,6 @@ impl ResourceConfig { } /// Determina si un directorio es considerado grande - #[allow(dead_code)] pub fn is_large_directory(&self, entry_count: usize) -> bool { entry_count >= self.large_dir_threshold_entries } @@ -170,7 +166,6 @@ pub struct ConcurrencyConfig { /// Máximo de tareas de archivo concurrentes pub max_concurrent_files: usize, /// Máximo de tareas de directorio concurrentes - #[allow(dead_code)] pub max_concurrent_dirs: usize, /// Máximo de operaciones de IO concurrentes pub max_concurrent_io: usize, @@ -451,7 +446,6 @@ impl AppConfig { } /// Obtenemos una configuración global por defecto -#[allow(dead_code)] pub fn default_config() -> AppConfig { AppConfig::default() } \ No newline at end of file diff --git a/src/common/di.rs b/src/common/di.rs index 59a447eb..8ace65a3 100644 --- a/src/common/di.rs +++ b/src/common/di.rs @@ -3,20 +3,29 @@ use std::sync::Arc; use std::sync::RwLock; use sqlx::PgPool; -use crate::domain::services::auth_service::AuthService; use crate::application::services::auth_application_service::AuthApplicationService; -use crate::domain::services::path_service::PathService; +use crate::infrastructure::services::path_service::PathService; use crate::infrastructure::repositories::folder_fs_repository::FolderFsRepository; use crate::infrastructure::repositories::file_fs_repository::FileFsRepository; use crate::infrastructure::repositories::trash_fs_repository::TrashFsRepository; +use crate::infrastructure::repositories::share_fs_repository::ShareFsRepository; +use crate::infrastructure::repositories::parallel_file_processor::ParallelFileProcessor; use crate::infrastructure::services::file_system_i18n_service::FileSystemI18nService; use crate::infrastructure::services::id_mapping_service::IdMappingService; +use crate::infrastructure::services::id_mapping_optimizer::IdMappingOptimizer; use crate::infrastructure::services::cache_manager::StorageCacheManager; use crate::infrastructure::services::file_metadata_cache::FileMetadataCache; +use crate::infrastructure::services::buffer_pool::BufferPool; +use crate::infrastructure::services::trash_cleanup_service::TrashCleanupService; use crate::application::services::folder_service::FolderService; use crate::application::services::file_service::FileService; use crate::application::services::i18n_application_service::I18nApplicationService; +use crate::application::services::trash_service::TrashService; +use crate::application::services::search_service::SearchService; +use crate::application::services::share_service::ShareService; +use crate::application::services::favorites_service::FavoritesService; +use crate::application::services::recent_service::RecentService; use crate::application::ports::trash_ports::TrashUseCase; use crate::application::services::storage_mediator::{StorageMediator, FileSystemStorageMediator}; use crate::application::ports::inbound::{FileUseCase, FolderUseCase, SearchUseCase}; @@ -28,11 +37,14 @@ use crate::application::ports::storage_ports::{FileReadPort, FileWritePort}; use crate::infrastructure::repositories::{FileMetadataManager, FilePathResolver, FileFsReadRepository, FileFsWriteRepository}; use crate::application::services::{FileUploadService, FileRetrievalService, FileManagementService, AppFileUseCaseFactory}; use crate::common::errors::DomainError; +use crate::common::adapters::{DomainFileRepoAdapter, DomainFolderRepoAdapter}; use crate::domain::services::i18n_service::I18nService; use crate::common::config::AppConfig; /// Fábrica para los diferentes componentes de la aplicación -#[allow(dead_code)] +/// +/// Esta fábrica centraliza la creación de todos los servicios de la aplicación, +/// garantizando el orden correcto de inicialización y resolviendo dependencias circulares. pub struct AppServiceFactory { storage_path: PathBuf, locales_path: PathBuf, @@ -41,7 +53,6 @@ pub struct AppServiceFactory { impl AppServiceFactory { /// Crea una nueva fábrica de servicios - #[allow(dead_code)] pub fn new(storage_path: PathBuf, locales_path: PathBuf) -> Self { Self { storage_path, @@ -51,7 +62,6 @@ impl AppServiceFactory { } /// Crea una nueva fábrica de servicios con configuración personalizada - #[allow(dead_code)] pub fn with_config(storage_path: PathBuf, locales_path: PathBuf, config: AppConfig) -> Self { Self { storage_path, @@ -60,17 +70,25 @@ impl AppServiceFactory { } } + /// Obtiene la configuración + pub fn config(&self) -> &AppConfig { + &self.config + } + + /// Obtiene la ruta de almacenamiento + pub fn storage_path(&self) -> &PathBuf { + &self.storage_path + } + /// Inicializa los servicios base del sistema - #[allow(dead_code)] pub async fn create_core_services(&self) -> Result { // Path service let path_service = Arc::new(PathService::new(self.storage_path.clone())); // Cache manager - // TTL values in milliseconds and max entries for cache - let file_ttl_ms = 60_000; // 1 minute for files - let dir_ttl_ms = 120_000; // 2 minutes for directories - let max_entries = 10_000; // Maximum cache entries + let file_ttl_ms = self.config.cache.file_ttl_ms; + let dir_ttl_ms = self.config.cache.directory_ttl_ms; + let max_entries = self.config.cache.max_entries; let cache_manager = Arc::new(StorageCacheManager::new(file_ttl_ms, dir_ttl_ms, max_entries)); // Iniciar tarea de limpieza de caché en segundo plano @@ -79,22 +97,39 @@ impl AppServiceFactory { StorageCacheManager::start_cleanup_task(cache_manager_clone).await; }); - // ID mapping service - let id_mapping_path = self.storage_path.join("folder_ids.json"); - let id_mapping_service = Arc::new( - IdMappingService::new(id_mapping_path).await? + // ID mapping service para carpetas + let folder_id_mapping_path = self.storage_path.join("folder_ids.json"); + let folder_id_mapping_service = Arc::new( + IdMappingService::new(folder_id_mapping_path).await? ); + // ID mapping service para archivos + let file_id_mapping_path = self.storage_path.join("file_ids.json"); + let file_id_mapping_service = Arc::new( + IdMappingService::new(file_id_mapping_path).await? + ); + + // Optimizer con batch processing y caching + let id_mapping_optimizer = Arc::new( + IdMappingOptimizer::new(folder_id_mapping_service.clone()) + ); + + // Iniciar tarea de limpieza del optimizer + IdMappingOptimizer::start_cleanup_task(id_mapping_optimizer.clone()); + + tracing::info!("Core services initialized: path service, cache manager, ID mapping"); + Ok(CoreServices { path_service, cache_manager, - id_mapping_service, + id_mapping_service: folder_id_mapping_service, + file_id_mapping_service, + id_mapping_optimizer, config: self.config.clone(), }) } - /// Inicializa los servicios de repositorio utilizando el patrón Builder mejorado - #[allow(dead_code)] + /// Inicializa los servicios de repositorio pub fn create_repository_services(&self, core: &CoreServices) -> RepositoryServices { // Storage mediator - con inicialización diferida para folder repository let folder_repository_holder = Arc::new(RwLock::new(None)); @@ -102,7 +137,7 @@ impl AppServiceFactory { let storage_mediator = Arc::new(FileSystemStorageMediator::new_with_lazy_folder( folder_repository_holder.clone(), core.path_service.clone(), - core.id_mapping_service.clone() + core.id_mapping_optimizer.clone() )); // Folder repository @@ -113,7 +148,7 @@ impl AppServiceFactory { core.path_service.clone(), )); - // Actualizar el holder para el mediador una vez que el repository está creado + // Actualizar el holder para el mediador if let Ok(mut holder) = folder_repository_holder.write() { *holder = Some(folder_repository.clone()); } @@ -123,6 +158,22 @@ impl AppServiceFactory { FileMetadataCache::default_with_config(core.config.clone()) ); + // Iniciar tarea de limpieza de metadata cache + let cache_clone = metadata_cache.clone(); + tokio::spawn(async move { + FileMetadataCache::start_cleanup_task(cache_clone).await; + }); + + // Buffer pool para optimización de memoria + let buffer_pool = BufferPool::new(256 * 1024, 50, 120); // 256KB buffers, 50 max, 2 min TTL + BufferPool::start_cleaner(buffer_pool.clone()); + + // Parallel file processor + let parallel_processor = Arc::new(ParallelFileProcessor::new_with_buffer_pool( + core.config.clone(), + buffer_pool.clone() + )); + // Componentes refactorizados let metadata_manager = Arc::new(FileMetadataManager::new( metadata_cache.clone(), @@ -141,7 +192,7 @@ impl AppServiceFactory { metadata_manager.clone(), path_resolver.clone(), core.config.clone(), - None // processor will be added later if needed + Some(parallel_processor.clone()) )); let file_write_repository = Arc::new(FileFsWriteRepository::new( @@ -150,16 +201,17 @@ impl AppServiceFactory { path_resolver.clone(), storage_mediator.clone(), core.config.clone(), - None // processor will be added later if needed + Some(parallel_processor.clone()) )); - // Legacy file repository - mantenido por compatibilidad - let file_repository = Arc::new(FileFsRepository::new( + // File repository con procesamiento paralelo + let file_repository = Arc::new(FileFsRepository::new_with_processor( self.storage_path.clone(), storage_mediator.clone(), - core.id_mapping_service.clone(), + core.file_id_mapping_service.clone(), core.path_service.clone(), - metadata_cache, + metadata_cache.clone(), + parallel_processor )); // I18n repository @@ -177,6 +229,8 @@ impl AppServiceFactory { None }; + tracing::info!("Repository services initialized with parallel processing and buffer pool"); + RepositoryServices { folder_repository, file_repository, @@ -186,24 +240,23 @@ impl AppServiceFactory { storage_mediator, metadata_manager, path_resolver, + metadata_cache, trash_repository, } } /// Inicializa los servicios de aplicación - #[allow(dead_code)] pub fn create_application_services(&self, repos: &RepositoryServices) -> ApplicationServices { // Servicios principales let folder_service = Arc::new(FolderService::new( repos.folder_repository.clone() )); - // Antiguo servicio único let file_service = Arc::new(FileService::new( repos.file_repository.clone() )); - // Nuevos servicios refactorizados + // Servicios refactorizados let file_upload_service = Arc::new(FileUploadService::new( repos.file_write_repository.clone() )); @@ -225,13 +278,21 @@ impl AppServiceFactory { repos.i18n_repository.clone() )); - // Servicio de papelera (deshabilitado temporalmente) - let trash_service = None; // La función de papelera está deshabilitada por defecto + // Search service con caché + let search_service: Option> = Some(Arc::new(SearchService::new( + repos.file_repository.clone(), + repos.folder_repository.clone(), + 300, // Cache TTL in seconds (5 minutes) + 1000, // Maximum cache entries + ))); - // Servicio de búsqueda (deshabilitado por defecto) - let search_service = None; // La función de búsqueda se activa según la configuración + tracing::info!("Application services initialized"); ApplicationServices { + // Tipos concretos para handlers que los necesitan + folder_service_concrete: folder_service.clone(), + file_service_concrete: file_service.clone(), + // Traits para abstracción folder_service, file_service, file_upload_service, @@ -239,27 +300,132 @@ impl AppServiceFactory { file_management_service, file_use_case_factory, i18n_service, - trash_service, + trash_service: None, // Se configura después con create_trash_service search_service, - share_service: None, // No share service by default - favorites_service: None, // No favorites service by default - recent_service: None // No recent service by default + share_service: None, // Se configura después con create_share_service + favorites_service: None, // Se configura después con create_favorites_service + recent_service: None, // Se configura después con create_recent_service + } + } + + /// Crea el servicio de papelera + pub async fn create_trash_service( + &self, + repos: &RepositoryServices, + ) -> Option> { + if !self.config.features.enable_trash { + tracing::info!("Trash service is disabled in configuration"); + return None; + } + + let trash_repo = repos.trash_repository.as_ref()?; + + // Crear adaptadores + let file_repo_adapter = Arc::new(DomainFileRepoAdapter::new(repos.file_repository.clone())); + let folder_repo_adapter = Arc::new(DomainFolderRepoAdapter::new(repos.folder_repository.clone())); + + let service = Arc::new(TrashService::new( + trash_repo.clone(), + file_repo_adapter, + folder_repo_adapter, + self.config.storage.trash_retention_days, + )); + + // Inicializar servicio de limpieza + let cleanup_service = TrashCleanupService::new( + service.clone(), + trash_repo.clone(), + 24, // Run cleanup every 24 hours + ); + + cleanup_service.start_cleanup_job().await; + tracing::info!("Trash service initialized with daily cleanup schedule"); + + Some(service as Arc) + } + + /// Crea el servicio de compartición + pub fn create_share_service( + &self, + repos: &RepositoryServices, + ) -> Option> { + if !self.config.features.enable_file_sharing { + tracing::info!("File sharing service is disabled in configuration"); + return None; + } + + let share_repository = Arc::new(ShareFsRepository::new( + Arc::new(self.config.clone()) + )); + + let service = Arc::new(ShareService::new( + Arc::new(self.config.clone()), + share_repository, + repos.file_repository.clone(), + repos.folder_repository.clone() + )); + + tracing::info!("File sharing service initialized"); + Some(service) + } + + /// Crea el servicio de favoritos (requiere base de datos) + pub fn create_favorites_service( + &self, + db_pool: &Arc, + ) -> Arc { + let service = Arc::new(FavoritesService::new(db_pool.clone())); + tracing::info!("Favorites service initialized"); + service + } + + /// Crea el servicio de elementos recientes (requiere base de datos) + pub fn create_recent_service( + &self, + db_pool: &Arc, + ) -> Arc { + let service = Arc::new(RecentService::new( + db_pool.clone(), + 50 // Maximum recent items per user + )); + tracing::info!("Recent items service initialized"); + service + } + + /// Precarga traducciones + pub async fn preload_translations(&self, i18n_service: &I18nApplicationService) { + use crate::domain::services::i18n_service::Locale; + + if let Err(e) = i18n_service.load_translations(Locale::English).await { + tracing::warn!("Failed to load English translations: {}", e); + } + if let Err(e) = i18n_service.load_translations(Locale::Spanish).await { + tracing::warn!("Failed to load Spanish translations: {}", e); + } + tracing::info!("Translations preloaded"); + } + + /// Precarga directorios en caché + pub async fn preload_cache(&self, metadata_cache: &FileMetadataCache) { + tracing::info!("Preloading common directories to warm up cache..."); + if let Ok(count) = metadata_cache.preload_directory(&self.storage_path, true, 1).await { + tracing::info!("Preloaded {} directory entries into cache", count); } } } /// Contenedor para servicios base -#[allow(dead_code)] #[derive(Clone)] pub struct CoreServices { pub path_service: Arc, pub cache_manager: Arc, pub id_mapping_service: Arc, + pub file_id_mapping_service: Arc, + pub id_mapping_optimizer: Arc, pub config: AppConfig, } /// Contenedor para servicios de repositorio -#[allow(dead_code)] #[derive(Clone)] pub struct RepositoryServices { pub folder_repository: Arc, @@ -270,13 +436,17 @@ pub struct RepositoryServices { pub storage_mediator: Arc, pub metadata_manager: Arc, pub path_resolver: Arc, + pub metadata_cache: Arc, pub trash_repository: Option>, } /// Contenedor para servicios de aplicación -#[allow(dead_code)] #[derive(Clone)] pub struct ApplicationServices { + // Tipos concretos para compatibilidad con handlers existentes + pub folder_service_concrete: Arc, + pub file_service_concrete: Arc, + // Traits para abstracción pub folder_service: Arc, pub file_service: Arc, pub file_upload_service: Arc, @@ -292,10 +462,9 @@ pub struct ApplicationServices { } /// Contenedor para servicios de autenticación -#[allow(dead_code)] #[derive(Clone)] pub struct AuthServices { - pub auth_service: Arc, + pub token_service: Arc, pub auth_application_service: Arc, } @@ -323,7 +492,7 @@ impl Default for AppState { let config = crate::common::config::AppConfig::default(); let path_service = Arc::new( - crate::domain::services::path_service::PathService::new( + crate::infrastructure::services::path_service::PathService::new( std::path::PathBuf::from("./storage") ) ); @@ -758,14 +927,23 @@ impl Default for AppState { let file_management_service = Arc::new(DummyFileManagementUseCase) as Arc; let file_use_case_factory = Arc::new(DummyFileUseCaseFactory) as Arc; + // Create dummy ID mapping service for files + let dummy_file_id_mapping = Arc::new(IdMappingService::dummy()); + let dummy_id_optimizer = Arc::new(IdMappingOptimizer::new(dummy_file_id_mapping.clone())); + // This creates the core services needed for basic functionality let core_services = CoreServices { path_service: path_service.clone(), cache_manager: Arc::new(crate::infrastructure::services::cache_manager::StorageCacheManager::default()), id_mapping_service: id_mapping_service.clone(), + file_id_mapping_service: dummy_file_id_mapping, + id_mapping_optimizer: dummy_id_optimizer, config: config.clone(), }; + // Create dummy metadata cache + let dummy_metadata_cache = Arc::new(FileMetadataCache::default_with_config(config.clone())); + // Create empty repository implementations let repository_services = RepositoryServices { folder_repository: Arc::new(DummyFolderStoragePort) as Arc, @@ -780,6 +958,7 @@ impl Default for AppState { storage_mediator.clone(), id_mapping_service.clone() )), + metadata_cache: dummy_metadata_cache, trash_repository: None, // No trash repository in minimal mode }; @@ -798,9 +977,17 @@ impl Default for AppState { Ok(()) } } + + // Create dummy concrete services for compatibility + let dummy_folder_storage = Arc::new(DummyFolderStoragePort) as Arc; + let dummy_file_storage = Arc::new(DummyFileStoragePort) as Arc; + let folder_service_concrete = Arc::new(FolderService::new(dummy_folder_storage)); + let file_service_concrete = Arc::new(FileService::new(dummy_file_storage)); // Create application services let application_services = ApplicationServices { + folder_service_concrete: folder_service_concrete.clone(), + file_service_concrete: file_service_concrete.clone(), folder_service, file_service, file_upload_service, diff --git a/src/common/errors.rs b/src/common/errors.rs index 673526b0..251a4ff3 100644 --- a/src/common/errors.rs +++ b/src/common/errors.rs @@ -1,255 +1,22 @@ -use std::fmt::{Display, Formatter, Result as FmtResult}; -use std::error::Error as StdError; -use thiserror::Error; +//! Errores de la aplicación +//! +//! Este módulo re-exporta los errores del dominio y define utilidades +//! para conversión de errores de infraestructura. -/// Tipo Result común para la aplicación con DomainError como error estándar -pub type Result = std::result::Result; +// Re-exportar errores del dominio para compatibilidad +pub use crate::domain::errors::{DomainError, ErrorKind, Result}; -/// Tipos de errores comunes en toda la aplicación -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ErrorKind { - /// Entidad no encontrada - NotFound, - /// Entidad ya existe - AlreadyExists, - /// Entrada inválida o validación fallida - InvalidInput, - /// Error de acceso o permisos - AccessDenied, - /// Tiempo de espera agotado - Timeout, - /// Error interno del sistema - InternalError, - /// Funcionalidad no implementada - NotImplemented, - /// Operación no soportada - UnsupportedOperation, - /// Error de base de datos - DatabaseError, -} +// Re-exportar AppError desde interfaces para compatibilidad hacia atrás +// NOTA: El lugar canónico de AppError es ahora crate::interfaces::errors -impl Display for ErrorKind { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - match self { - ErrorKind::NotFound => write!(f, "Not Found"), - ErrorKind::AlreadyExists => write!(f, "Already Exists"), - ErrorKind::InvalidInput => write!(f, "Invalid Input"), - ErrorKind::AccessDenied => write!(f, "Access Denied"), - ErrorKind::Timeout => write!(f, "Timeout"), - ErrorKind::InternalError => write!(f, "Internal Error"), - ErrorKind::NotImplemented => write!(f, "Not Implemented"), - ErrorKind::UnsupportedOperation => write!(f, "Unsupported Operation"), - ErrorKind::DatabaseError => write!(f, "Database Error"), - } - } -} - -/// Error base de dominio que proporciona contexto detallado -#[derive(Error, Debug)] -#[error("{kind}: {message}")] -pub struct DomainError { - /// Tipo de error - pub kind: ErrorKind, - /// Tipo de entidad afectada (ej: "File", "Folder") - pub entity_type: &'static str, - /// Identificador de la entidad si está disponible - pub entity_id: Option, - /// Mensaje descriptivo del error - pub message: String, - /// Error fuente (opcional) - #[source] - pub source: Option>, -} - -impl DomainError { - /// Crea un nuevo error de dominio - pub fn new>( - kind: ErrorKind, - entity_type: &'static str, - message: S, - ) -> Self { - Self { - kind, - entity_type, - entity_id: None, - message: message.into(), - source: None, - } - } - - /// Crea un error de entidad no encontrada - pub fn not_found>(entity_type: &'static str, entity_id: S) -> Self { - let id = entity_id.into(); - Self { - kind: ErrorKind::NotFound, - entity_type, - entity_id: Some(id.clone()), - message: format!("{} not found: {}", entity_type, id), - source: None, - } - } - - /// Crea un error de entidad ya existente - pub fn already_exists>(entity_type: &'static str, entity_id: S) -> Self { - let id = entity_id.into(); - Self { - kind: ErrorKind::AlreadyExists, - entity_type, - entity_id: Some(id.clone()), - message: format!("{} already exists: {}", entity_type, id), - source: None, - } - } - - /// Crea un error para operaciones no soportadas - pub fn operation_not_supported>(entity_type: &'static str, message: S) -> Self { - Self::new( - ErrorKind::UnsupportedOperation, - entity_type, - message, - ) - } - - /// Crea un error de tiempo agotado - pub fn timeout>(entity_type: &'static str, message: S) -> Self { - Self { - kind: ErrorKind::Timeout, - entity_type, - entity_id: None, - message: message.into(), - source: None, - } - } - - /// Crea un error interno - pub fn internal_error>(entity_type: &'static str, message: S) -> Self { - Self { - kind: ErrorKind::InternalError, - entity_type, - entity_id: None, - message: message.into(), - source: None, - } - } - - /// Crea un error de acceso denegado - pub fn access_denied>(entity_type: &'static str, message: S) -> Self { - Self { - kind: ErrorKind::AccessDenied, - entity_type, - entity_id: None, - message: message.into(), - source: None, - } - } - - /// Alias for access_denied to maintain compatibility - pub fn unauthorized>(message: S) -> Self { - Self { - kind: ErrorKind::AccessDenied, - entity_type: "Authorization", - entity_id: None, - message: message.into(), - source: None, - } - } - - /// Crea un error de base de datos - pub fn database_error>(message: S) -> Self { - Self { - kind: ErrorKind::DatabaseError, - entity_type: "Database", - entity_id: None, - message: message.into(), - source: None, - } - } - - /// Crea un error de validación - pub fn validation_error>(message: S) -> Self { - Self { - kind: ErrorKind::InvalidInput, - entity_type: "Validation", - entity_id: None, - message: message.into(), - source: None, - } - } - - /// Crea un error de funcionalidad no implementada - pub fn not_implemented>(entity_type: &'static str, message: S) -> Self { - Self { - kind: ErrorKind::NotImplemented, - entity_type, - entity_id: None, - message: message.into(), - source: None, - } - } - - /// Establece el ID de la entidad - #[allow(dead_code)] - pub fn with_id>(mut self, entity_id: S) -> Self { - self.entity_id = Some(entity_id.into()); - self - } - - /// Establece el error fuente - pub fn with_source(mut self, source: E) -> Self { - self.source = Some(Box::new(source)); - self - } -} - -/// Trait para añadir contexto a los errores -pub trait ErrorContext { - fn with_context(self, context: F) -> std::result::Result - where - C: Into, - F: FnOnce() -> C; - - #[allow(dead_code)] - fn with_error_kind(self, kind: ErrorKind, entity_type: &'static str) -> std::result::Result; -} - -impl ErrorContext for std::result::Result { - fn with_context(self, context: F) -> std::result::Result - where - C: Into, - F: FnOnce() -> C, - { - self.map_err(|e| { - DomainError { - kind: ErrorKind::InternalError, - entity_type: "Unknown", - entity_id: None, - message: context().into(), - source: Some(Box::new(e)), - } - }) - } - - fn with_error_kind(self, kind: ErrorKind, entity_type: &'static str) -> std::result::Result { - self.map_err(|e| { - DomainError { - kind, - entity_type, - entity_id: None, - message: format!("{}", e), - source: Some(Box::new(e)), - } - }) - } -} - -/// Macro para convertir errores específicos a DomainError +// Macro para convertir errores específicos de infraestructura a DomainError #[macro_export] macro_rules! impl_from_error { ($error_type:ty, $entity_type:expr) => { - impl From<$error_type> for DomainError { + impl From<$error_type> for crate::domain::errors::DomainError { fn from(err: $error_type) -> Self { - DomainError { - kind: ErrorKind::InternalError, + crate::domain::errors::DomainError { + kind: crate::domain::errors::ErrorKind::InternalError, entity_type: $entity_type, entity_id: None, message: format!("{}", err), @@ -260,102 +27,6 @@ macro_rules! impl_from_error { }; } -// Implementación para errores estándar comunes -impl_from_error!(std::io::Error, "IO"); +// Implementaciones para errores de infraestructura (sqlx, serde_json) impl_from_error!(serde_json::Error, "Serialization"); impl_from_error!(sqlx::Error, "Database"); -impl_from_error!(uuid::Error, "UUID"); - -// Error para capas HTTP/API -#[derive(Debug)] -pub struct AppError { - pub status_code: axum::http::StatusCode, - pub message: String, - pub error_type: String, -} - -// Estructura de respuesta de error -#[derive(serde::Serialize)] -pub struct ErrorResponse { - pub status: String, - pub message: String, - pub error_type: String, -} - -impl AppError { - pub fn new(status_code: axum::http::StatusCode, message: impl Into, error_type: impl Into) -> Self { - Self { - status_code, - message: message.into(), - error_type: error_type.into(), - } - } - - pub fn bad_request(message: impl Into) -> Self { - Self::new(axum::http::StatusCode::BAD_REQUEST, message, "BadRequest") - } - - pub fn unauthorized(message: impl Into) -> Self { - Self::new(axum::http::StatusCode::UNAUTHORIZED, message, "Unauthorized") - } - - pub fn forbidden(message: impl Into) -> Self { - Self::new(axum::http::StatusCode::FORBIDDEN, message, "Forbidden") - } - - pub fn not_found(message: impl Into) -> Self { - Self::new(axum::http::StatusCode::NOT_FOUND, message, "NotFound") - } - - pub fn internal_error(message: impl Into) -> Self { - Self::new(axum::http::StatusCode::INTERNAL_SERVER_ERROR, message, "InternalError") - } - - pub fn method_not_allowed(message: impl Into) -> Self { - Self::new(axum::http::StatusCode::METHOD_NOT_ALLOWED, message, "MethodNotAllowed") - } - - pub fn conflict(message: impl Into) -> Self { - Self::new(axum::http::StatusCode::CONFLICT, message, "Conflict") - } - - pub fn unsupported_media_type(message: impl Into) -> Self { - Self::new(axum::http::StatusCode::UNSUPPORTED_MEDIA_TYPE, message, "UnsupportedMediaType") - } -} - -impl From for AppError { - fn from(err: DomainError) -> Self { - let status_code = match err.kind { - ErrorKind::NotFound => axum::http::StatusCode::NOT_FOUND, - ErrorKind::AlreadyExists => axum::http::StatusCode::CONFLICT, - ErrorKind::InvalidInput => axum::http::StatusCode::BAD_REQUEST, - ErrorKind::AccessDenied => axum::http::StatusCode::FORBIDDEN, - ErrorKind::Timeout => axum::http::StatusCode::REQUEST_TIMEOUT, - ErrorKind::InternalError => axum::http::StatusCode::INTERNAL_SERVER_ERROR, - ErrorKind::NotImplemented => axum::http::StatusCode::NOT_IMPLEMENTED, - ErrorKind::UnsupportedOperation => axum::http::StatusCode::METHOD_NOT_ALLOWED, - ErrorKind::DatabaseError => axum::http::StatusCode::INTERNAL_SERVER_ERROR, - }; - - Self { - status_code, - message: err.message, - error_type: err.kind.to_string(), - } - } -} - -impl axum::response::IntoResponse for AppError { - fn into_response(self) -> axum::response::Response { - let status = self.status_code; - let error_response = ErrorResponse { - status: status.to_string(), - message: self.message, - error_type: self.error_type, - }; - - let body = axum::Json(error_response); - (status, body).into_response() - } -} \ No newline at end of file diff --git a/src/common/mod.rs b/src/common/mod.rs index b31f022f..1073261f 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -1,6 +1,6 @@ pub mod errors; pub mod config; -pub mod cache; pub mod di; pub mod db; -pub mod auth_factory; \ No newline at end of file +pub mod auth_factory; +pub mod adapters; \ No newline at end of file diff --git a/src/domain/entities/contact.rs b/src/domain/entities/contact.rs index 270a8247..11232d6f 100644 --- a/src/domain/entities/contact.rs +++ b/src/domain/entities/contact.rs @@ -1,8 +1,7 @@ use chrono::{DateTime, NaiveDate, Utc}; -use serde::{Deserialize, Serialize}; -use sqlx::types::Uuid; +use uuid::Uuid; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct AddressBook { pub id: Uuid, pub name: String, @@ -29,21 +28,21 @@ impl Default for AddressBook { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct Email { pub email: String, pub r#type: String, // home, work, other pub is_primary: bool, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct Phone { pub number: String, pub r#type: String, // mobile, home, work, fax, other pub is_primary: bool, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct Address { pub street: Option, pub city: Option, @@ -54,7 +53,7 @@ pub struct Address { pub is_primary: bool, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct Contact { pub id: Uuid, pub address_book_id: Uuid, @@ -105,7 +104,7 @@ impl Default for Contact { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct ContactGroup { pub id: Uuid, pub address_book_id: Uuid, diff --git a/src/domain/entities/file.rs b/src/domain/entities/file.rs index cabed47f..4493c6d6 100644 --- a/src/domain/entities/file.rs +++ b/src/domain/entities/file.rs @@ -1,4 +1,3 @@ -use serde::{Serialize, Deserialize}; use crate::domain::services::path_service::StoragePath; /** @@ -15,7 +14,6 @@ pub enum FileError { /// Occurs when validation fails for any file entity attribute. #[error("Validation error: {0}")] - #[allow(dead_code)] ValidationError(String), } @@ -36,7 +34,7 @@ pub type FileResult = Result; * This entity maintains both physical storage information and logical metadata about files, * serving as the bridge between the storage system and the application. */ -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct File { /// Unique identifier for the file - used throughout the system for file operations id: String, @@ -44,12 +42,10 @@ pub struct File { /// Name of the file including extension name: String, - /// Path to the file in the domain model - not serialized as it contains internal representation - #[serde(skip_serializing, skip_deserializing)] + /// Path to the file in the domain model storage_path: StoragePath, - /// String representation of the path for serialization and API compatibility - #[serde(rename = "path")] + /// String representation of the path for API compatibility path_string: String, /// Size of the file in bytes @@ -253,7 +249,6 @@ impl File { // Methods to create new versions of the file (immutable) /// Creates a new version of the file with updated name - #[allow(dead_code)] pub fn with_name(&self, new_name: String) -> FileResult { // Validate file name if new_name.is_empty() || new_name.contains('/') || new_name.contains('\\') { @@ -318,7 +313,6 @@ impl File { } /// Creates a new version of the file with updated size - #[allow(dead_code)] pub fn with_size(&self, new_size: u64) -> Self { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) diff --git a/src/domain/entities/folder.rs b/src/domain/entities/folder.rs index f5dd9b93..e7a53270 100644 --- a/src/domain/entities/folder.rs +++ b/src/domain/entities/folder.rs @@ -1,4 +1,3 @@ -use serde::{Serialize, Deserialize}; use crate::domain::services::path_service::StoragePath; /// Error in the creation or manipulation of folder entities @@ -8,7 +7,6 @@ pub enum FolderError { InvalidFolderName(String), #[error("Validation error: {0}")] - #[allow(dead_code)] ValidationError(String), } @@ -16,7 +14,7 @@ pub enum FolderError { pub type FolderResult = Result; /// Represents a folder entity in the domain -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Folder { /// Unique identifier for the folder id: String, @@ -25,11 +23,9 @@ pub struct Folder { name: String, /// Path to the folder in the domain model - #[serde(skip_serializing, skip_deserializing)] storage_path: StoragePath, - /// String representation of the path (for serialization compatibility) - #[serde(rename = "path")] + /// String representation of the path (for API compatibility) path_string: String, /// Parent folder ID (None if it's a root folder) @@ -235,7 +231,6 @@ impl Folder { } /// Returns an absolute path for this folder - #[allow(dead_code)] pub fn get_absolute_path>(&self, root_path: P) -> std::path::PathBuf { let mut result = std::path::PathBuf::from(root_path.as_ref()); diff --git a/src/domain/entities/session.rs b/src/domain/entities/session.rs index 167ef59e..ab688497 100644 --- a/src/domain/entities/session.rs +++ b/src/domain/entities/session.rs @@ -1,8 +1,7 @@ -use serde::{Serialize, Deserialize}; use uuid::Uuid; use chrono::{DateTime, Utc, Duration}; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct Session { pub id: String, pub user_id: String, diff --git a/src/domain/entities/user.rs b/src/domain/entities/user.rs index fb5aa252..26bc1ed8 100644 --- a/src/domain/entities/user.rs +++ b/src/domain/entities/user.rs @@ -1,7 +1,3 @@ -use serde::{Serialize, Deserialize}; -use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; -use argon2::password_hash::SaltString; -use rand_core::OsRng; use uuid::Uuid; use chrono::{DateTime, Utc}; @@ -22,7 +18,7 @@ pub enum UserError { pub type UserResult = Result; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] // We'll handle conversion manually for now until the type is properly set up in the database pub enum UserRole { Admin, @@ -38,12 +34,11 @@ impl std::fmt::Display for UserRole { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct User { id: String, username: String, email: String, - #[serde(skip_serializing)] password_hash: String, role: UserRole, storage_quota_bytes: i64, @@ -55,10 +50,22 @@ pub struct User { } impl User { + /// Create a new user with a pre-hashed password. + /// + /// The password hashing should be done externally using PasswordHasherPort + /// to maintain clean architecture and keep cryptographic dependencies + /// out of the domain layer. + /// + /// # Arguments + /// * `username` - User's username (3-32 characters) + /// * `email` - User's email address + /// * `password_hash` - Pre-hashed password (from PasswordHasherPort) + /// * `role` - User's role + /// * `storage_quota_bytes` - Storage quota in bytes pub fn new( username: String, email: String, - password: String, + password_hash: String, role: UserRole, storage_quota_bytes: i64, ) -> UserResult { @@ -75,19 +82,12 @@ impl User { ))); } - if password.len() < 8 { + if password_hash.is_empty() { return Err(UserError::InvalidPassword(format!( - "Password debe tener al menos 8 caracteres" + "Password hash no puede estar vacío" ))); } - // Generar hash con Argon2id (recomendado para 2023+) - let salt = SaltString::generate(&mut OsRng); - let argon2 = Argon2::default(); - let password_hash = argon2.hash_password(password.as_bytes(), &salt) - .map_err(|e| UserError::ValidationError(format!("Error al generar hash: {}", e)))? - .to_string(); - let now = Utc::now(); Ok(Self { @@ -179,30 +179,13 @@ impl User { &self.password_hash } - // Verificación de password - pub fn verify_password(&self, password: &str) -> UserResult { - let parsed_hash = PasswordHash::new(&self.password_hash) - .map_err(|e| UserError::AuthenticationError(format!("Error al procesar hash: {}", e)))?; - - Ok(Argon2::default().verify_password(password.as_bytes(), &parsed_hash).is_ok()) - } - - // Cambiar contraseña - pub fn update_password(&mut self, new_password: String) -> UserResult<()> { - if new_password.len() < 8 { - return Err(UserError::InvalidPassword(format!( - "Password debe tener al menos 8 caracteres" - ))); - } - - let salt = SaltString::generate(&mut OsRng); - let argon2 = Argon2::default(); - self.password_hash = argon2.hash_password(new_password.as_bytes(), &salt) - .map_err(|e| UserError::ValidationError(format!("Error al generar hash: {}", e)))? - .to_string(); - + /// Update the password hash. + /// + /// The new password should be hashed externally using PasswordHasherPort + /// before calling this method. + pub fn update_password_hash(&mut self, new_hash: String) { + self.password_hash = new_hash; self.updated_at = Utc::now(); - Ok(()) } // Actualizar uso de almacenamiento diff --git a/src/domain/errors.rs b/src/domain/errors.rs new file mode 100644 index 00000000..71b6ab9d --- /dev/null +++ b/src/domain/errors.rs @@ -0,0 +1,271 @@ +//! Errores del dominio +//! +//! Este módulo contiene los tipos de error propios del dominio. +//! DomainError es el error base que se usa en toda la capa de dominio. + +use std::fmt::{Display, Formatter, Result as FmtResult}; +use std::error::Error as StdError; +use thiserror::Error; + +/// Tipo Result común para el dominio con DomainError como error estándar +pub type Result = std::result::Result; + +/// Tipos de errores del dominio +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ErrorKind { + /// Entidad no encontrada + NotFound, + /// Entidad ya existe + AlreadyExists, + /// Entrada inválida o validación fallida + InvalidInput, + /// Error de acceso o permisos + AccessDenied, + /// Tiempo de espera agotado + Timeout, + /// Error interno del sistema + InternalError, + /// Funcionalidad no implementada + NotImplemented, + /// Operación no soportada + UnsupportedOperation, + /// Error de base de datos + DatabaseError, +} + +impl Display for ErrorKind { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + ErrorKind::NotFound => write!(f, "Not Found"), + ErrorKind::AlreadyExists => write!(f, "Already Exists"), + ErrorKind::InvalidInput => write!(f, "Invalid Input"), + ErrorKind::AccessDenied => write!(f, "Access Denied"), + ErrorKind::Timeout => write!(f, "Timeout"), + ErrorKind::InternalError => write!(f, "Internal Error"), + ErrorKind::NotImplemented => write!(f, "Not Implemented"), + ErrorKind::UnsupportedOperation => write!(f, "Unsupported Operation"), + ErrorKind::DatabaseError => write!(f, "Database Error"), + } + } +} + +/// Error base de dominio que proporciona contexto detallado +#[derive(Error, Debug)] +#[error("{kind}: {message}")] +pub struct DomainError { + /// Tipo de error + pub kind: ErrorKind, + /// Tipo de entidad afectada (ej: "File", "Folder") + pub entity_type: &'static str, + /// Identificador de la entidad si está disponible + pub entity_id: Option, + /// Mensaje descriptivo del error + pub message: String, + /// Error fuente (opcional) + #[source] + pub source: Option>, +} + +impl DomainError { + /// Crea un nuevo error de dominio + pub fn new>( + kind: ErrorKind, + entity_type: &'static str, + message: S, + ) -> Self { + Self { + kind, + entity_type, + entity_id: None, + message: message.into(), + source: None, + } + } + + /// Crea un error de entidad no encontrada + pub fn not_found>(entity_type: &'static str, entity_id: S) -> Self { + let id = entity_id.into(); + Self { + kind: ErrorKind::NotFound, + entity_type, + entity_id: Some(id.clone()), + message: format!("{} not found: {}", entity_type, id), + source: None, + } + } + + /// Crea un error de entidad ya existente + pub fn already_exists>(entity_type: &'static str, entity_id: S) -> Self { + let id = entity_id.into(); + Self { + kind: ErrorKind::AlreadyExists, + entity_type, + entity_id: Some(id.clone()), + message: format!("{} already exists: {}", entity_type, id), + source: None, + } + } + + /// Crea un error para operaciones no soportadas + pub fn operation_not_supported>(entity_type: &'static str, message: S) -> Self { + Self::new( + ErrorKind::UnsupportedOperation, + entity_type, + message, + ) + } + + /// Crea un error de tiempo agotado + pub fn timeout>(entity_type: &'static str, message: S) -> Self { + Self { + kind: ErrorKind::Timeout, + entity_type, + entity_id: None, + message: message.into(), + source: None, + } + } + + /// Crea un error interno + pub fn internal_error>(entity_type: &'static str, message: S) -> Self { + Self { + kind: ErrorKind::InternalError, + entity_type, + entity_id: None, + message: message.into(), + source: None, + } + } + + /// Crea un error de acceso denegado + pub fn access_denied>(entity_type: &'static str, message: S) -> Self { + Self { + kind: ErrorKind::AccessDenied, + entity_type, + entity_id: None, + message: message.into(), + source: None, + } + } + + /// Alias for access_denied to maintain compatibility + pub fn unauthorized>(message: S) -> Self { + Self { + kind: ErrorKind::AccessDenied, + entity_type: "Authorization", + entity_id: None, + message: message.into(), + source: None, + } + } + + /// Crea un error de base de datos + pub fn database_error>(message: S) -> Self { + Self { + kind: ErrorKind::DatabaseError, + entity_type: "Database", + entity_id: None, + message: message.into(), + source: None, + } + } + + /// Crea un error de validación + pub fn validation_error>(message: S) -> Self { + Self { + kind: ErrorKind::InvalidInput, + entity_type: "Validation", + entity_id: None, + message: message.into(), + source: None, + } + } + + /// Crea un error de funcionalidad no implementada + pub fn not_implemented>(entity_type: &'static str, message: S) -> Self { + Self { + kind: ErrorKind::NotImplemented, + entity_type, + entity_id: None, + message: message.into(), + source: None, + } + } + + /// Establece el ID de la entidad + pub fn with_id>(mut self, entity_id: S) -> Self { + self.entity_id = Some(entity_id.into()); + self + } + + /// Establece el error fuente + pub fn with_source(mut self, source: E) -> Self { + self.source = Some(Box::new(source)); + self + } +} + +/// Trait para añadir contexto a los errores +pub trait ErrorContext { + fn with_context(self, context: F) -> std::result::Result + where + C: Into, + F: FnOnce() -> C; + + fn with_error_kind(self, kind: ErrorKind, entity_type: &'static str) -> std::result::Result; +} + +impl ErrorContext for std::result::Result { + fn with_context(self, context: F) -> std::result::Result + where + C: Into, + F: FnOnce() -> C, + { + self.map_err(|e| { + DomainError { + kind: ErrorKind::InternalError, + entity_type: "Unknown", + entity_id: None, + message: context().into(), + source: Some(Box::new(e)), + } + }) + } + + fn with_error_kind(self, kind: ErrorKind, entity_type: &'static str) -> std::result::Result { + self.map_err(|e| { + DomainError { + kind, + entity_type, + entity_id: None, + message: format!("{}", e), + source: Some(Box::new(e)), + } + }) + } +} + +// Implementaciones From para errores estándar (sin dependencias externas de infra) +impl From for DomainError { + fn from(err: std::io::Error) -> Self { + DomainError { + kind: ErrorKind::InternalError, + entity_type: "IO", + entity_id: None, + message: format!("{}", err), + source: Some(Box::new(err)), + } + } +} + +impl From for DomainError { + fn from(err: uuid::Error) -> Self { + DomainError { + kind: ErrorKind::InvalidInput, + entity_type: "UUID", + entity_id: None, + message: format!("{}", err), + source: Some(Box::new(err)), + } + } +} diff --git a/src/domain/mod.rs b/src/domain/mod.rs index 6bd8a8f1..eef62350 100644 --- a/src/domain/mod.rs +++ b/src/domain/mod.rs @@ -1,3 +1,6 @@ pub mod entities; +pub mod errors; pub mod repositories; -pub mod services; \ No newline at end of file +pub mod services; + +// Re-export common types diff --git a/src/domain/repositories/address_book_repository.rs b/src/domain/repositories/address_book_repository.rs index d5e628e0..92e0cca3 100644 --- a/src/domain/repositories/address_book_repository.rs +++ b/src/domain/repositories/address_book_repository.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use sqlx::types::Uuid; +use uuid::Uuid; use std::result::Result; use crate::common::errors::DomainError; diff --git a/src/domain/repositories/contact_repository.rs b/src/domain/repositories/contact_repository.rs index 88222e2d..810dcbd5 100644 --- a/src/domain/repositories/contact_repository.rs +++ b/src/domain/repositories/contact_repository.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use sqlx::types::Uuid; +use uuid::Uuid; use std::result::Result; use crate::common::errors::DomainError; diff --git a/src/domain/repositories/file_repository.rs b/src/domain/repositories/file_repository.rs index 2934d24d..eb021294 100644 --- a/src/domain/repositories/file_repository.rs +++ b/src/domain/repositories/file_repository.rs @@ -12,7 +12,6 @@ use bytes::Bytes; * operations, providing detailed context for error handling across the application. */ #[derive(Debug, thiserror::Error)] -#[allow(dead_code)] pub enum FileRepositoryError { /// Returned when a requested file cannot be found by ID or path #[error("File not found: {0}")] @@ -109,7 +108,6 @@ pub trait FileRepository: Send + Sync + 'static { * @param content Binary data of the file * @return The created File entity on success, error otherwise */ - #[allow(dead_code)] async fn save_file_with_id( &self, id: String, @@ -152,7 +150,6 @@ pub trait FileRepository: Send + Sync + 'static { * @param id The unique identifier of the file to delete * @return Success or error */ - #[allow(dead_code)] async fn delete_file_entry(&self, id: &str) -> FileRepositoryResult<()>; /** diff --git a/src/domain/repositories/folder_repository.rs b/src/domain/repositories/folder_repository.rs index 141e5c44..dce7ab03 100644 --- a/src/domain/repositories/folder_repository.rs +++ b/src/domain/repositories/folder_repository.rs @@ -5,7 +5,6 @@ use crate::common::errors::DomainError; /// Error types for folder repository operations #[derive(Debug, thiserror::Error)] -#[allow(dead_code)] pub enum FolderRepositoryError { #[error("Folder not found: {0}")] NotFound(String), @@ -84,12 +83,10 @@ pub trait FolderRepository: Send + Sync + 'static { /// Legacy method - checks if a folder exists at the given PathBuf path #[deprecated(note = "Use folder_exists_at_storage_path instead")] - #[allow(dead_code)] async fn folder_exists(&self, path: &std::path::PathBuf) -> FolderRepositoryResult; /// Legacy method - gets a folder by its PathBuf path #[deprecated(note = "Use get_folder_by_storage_path instead")] - #[allow(dead_code)] async fn get_folder_by_path(&self, path: &std::path::PathBuf) -> FolderRepositoryResult; /// Moves a folder to trash diff --git a/src/domain/services/i18n_service.rs b/src/domain/services/i18n_service.rs index 5fa5555e..6b9a88db 100644 --- a/src/domain/services/i18n_service.rs +++ b/src/domain/services/i18n_service.rs @@ -61,6 +61,5 @@ pub trait I18nService: Send + Sync + 'static { async fn available_locales(&self) -> Vec; /// Check if a locale is supported - #[allow(dead_code)] async fn is_supported(&self, locale: Locale) -> bool; } \ No newline at end of file diff --git a/src/domain/services/mod.rs b/src/domain/services/mod.rs index 72c01c62..b201ea54 100644 --- a/src/domain/services/mod.rs +++ b/src/domain/services/mod.rs @@ -1,3 +1,5 @@ pub mod i18n_service; pub mod path_service; -pub mod auth_service; \ No newline at end of file + +// NOTE: auth_service has been moved to infrastructure/services/jwt_service.rs +// The functionality is now exposed through application/ports/auth_ports.rs (TokenServicePort) \ No newline at end of file diff --git a/src/domain/services/path_service.rs b/src/domain/services/path_service.rs index 2f67f7ef..0dd74534 100644 --- a/src/domain/services/path_service.rs +++ b/src/domain/services/path_service.rs @@ -1,5 +1,12 @@ -/// Abstracto servicio de dominio para rutas, sin dependencias de sistema de archivos -/// Representa una ruta de almacenamiento en el dominio +//! StoragePath - Value Object del dominio para representar rutas de almacenamiento +//! +//! Este módulo contiene solo el Value Object StoragePath que es parte del dominio puro. +//! PathService (que implementa StoragePort y StorageMediator) fue movido a +//! infrastructure/services/path_service.rs porque tiene dependencias de sistema de archivos. + +use std::path::PathBuf; + +/// Representa una ruta de almacenamiento en el dominio (Value Object) #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct StoragePath { segments: Vec, @@ -7,7 +14,6 @@ pub struct StoragePath { impl StoragePath { /// Crea una nueva ruta de almacenamiento - #[allow(dead_code)] pub fn new(segments: Vec) -> Self { Self { segments } } @@ -89,301 +95,41 @@ impl StoragePath { } } -use std::path::{Path, PathBuf}; -use async_trait::async_trait; -use tokio::fs; - -use crate::common::errors::{DomainError, ErrorKind}; -use crate::application::ports::outbound::StoragePort; -use crate::application::services::storage_mediator::{StorageMediator, StorageMediatorResult, StorageMediatorError}; -use crate::domain::entities::folder::Folder; - -/// Servicio de dominio para manejar operaciones con rutas de almacenamiento -pub struct PathService { - root_path: PathBuf, // Necesario para la implementación -} - -impl PathService { - /// Crea un nuevo servicio de rutas con una raíz específica - pub fn new(root_path: PathBuf) -> Self { - Self { root_path } - } - - /// Convierte una ruta del dominio a una ruta física absoluta - pub fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf { - let mut path = self.root_path.clone(); - for segment in storage_path.segments() { - path.push(segment); - } - path - } - - /// Convierte una ruta física a una ruta de dominio - #[allow(dead_code)] - pub fn to_storage_path(&self, physical_path: &Path) -> Option { - physical_path.strip_prefix(&self.root_path).ok().map(|rel_path| { - let segments = rel_path - .components() - .filter_map(|c| match c { - std::path::Component::Normal(os_str) => Some(os_str.to_string_lossy().to_string()), - _ => None, - }) - .collect(); - StoragePath { segments } - }) - } - - /// Crea una ruta de archivo dentro de una carpeta - #[allow(dead_code)] - pub fn create_file_path(&self, folder_path: &StoragePath, file_name: &str) -> StoragePath { - folder_path.join(file_name) - } - - /// Verifica si una ruta es directamente hija de otra - #[allow(dead_code)] - pub fn is_direct_child(&self, parent_path: &StoragePath, potential_child: &StoragePath) -> bool { - if let Some(child_parent) = potential_child.parent() { - &child_parent == parent_path - } else { - parent_path.is_empty() - } - } - - /// Verifica si una ruta está en la raíz - #[allow(dead_code)] - pub fn is_in_root(&self, path: &StoragePath) -> bool { - path.parent().map_or(true, |p| p.is_empty()) - } - - /// Gets the root path used by this service - #[allow(dead_code)] - pub fn get_root_path(&self) -> &Path { - &self.root_path - } - - /// Valida una ruta para asegurar que no contiene componentes peligrosos - pub fn validate_path(&self, path: &StoragePath) -> Result<(), DomainError> { - // Verificar que no haya segmentos vacíos - if path.segments().iter().any(|s| s.is_empty()) { - return Err(DomainError::new( - ErrorKind::InvalidInput, - "Path", - format!("Path contains empty segments: {}", path.to_string()) - )); - } - - // Verificar que no haya caracteres peligrosos - let dangerous_chars = ['\\', ':', '*', '?', '"', '<', '>', '|']; - for segment in path.segments() { - if segment.contains(&dangerous_chars[..]) { - return Err(DomainError::new( - ErrorKind::InvalidInput, - "Path", - format!("Path contains dangerous characters: {}", segment) - )); - } - - // Verificar que no empiece con . (oculto en Unix) - if segment.starts_with('.') && segment != ".well-known" { - return Err(DomainError::new( - ErrorKind::InvalidInput, - "Path", - format!("Path segments cannot start with dot: {}", segment) - )); - } - } - - Ok(()) - } -} - -#[async_trait] -impl StoragePort for PathService { - fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf { - let mut path = self.root_path.clone(); - for segment in storage_path.segments() { - path.push(segment); - } - path - } - - async fn ensure_directory(&self, storage_path: &StoragePath) -> Result<(), DomainError> { - // Primero validar la ruta - self.validate_path(storage_path)?; - - // Resolver a ruta física - let physical_path = self.resolve_path(storage_path); - - // Crear directorios si no existen - if !physical_path.exists() { - fs::create_dir_all(&physical_path).await - .map_err(|e| DomainError::new( - ErrorKind::AccessDenied, - "Storage", - format!("Failed to create directory: {}", physical_path.display()) - ).with_source(e))?; - - tracing::debug!("Created directory: {}", physical_path.display()); - } else if !physical_path.is_dir() { - return Err(DomainError::new( - ErrorKind::InvalidInput, - "Storage", - format!("Path exists but is not a directory: {}", physical_path.display()) - )); - } - - Ok(()) - } - - async fn file_exists(&self, storage_path: &StoragePath) -> Result { - let physical_path = self.resolve_path(storage_path); - - let exists = physical_path.exists() && physical_path.is_file(); - Ok(exists) - } - - async fn directory_exists(&self, storage_path: &StoragePath) -> Result { - let physical_path = self.resolve_path(storage_path); - - let exists = physical_path.exists() && physical_path.is_dir(); - Ok(exists) - } -} - -#[async_trait] -impl StorageMediator for PathService { - async fn get_folder_path(&self, folder_id: &str) -> StorageMediatorResult { - // This is a simplified implementation since PathService doesn't have direct - // access to folder repository. It's typically used through a proxy. - Err(StorageMediatorError::NotFound(format!("Folder with ID {} not found", folder_id))) - } - - async fn get_folder_storage_path(&self, folder_id: &str) -> StorageMediatorResult { - // Simplified implementation - should be overridden by actual implementations - Err(StorageMediatorError::NotFound(format!("Folder with ID {} not found", folder_id))) - } - - async fn get_folder(&self, folder_id: &str) -> StorageMediatorResult { - // Simplified implementation - should be overridden by actual implementations - Err(StorageMediatorError::NotFound(format!("Folder with ID {} not found", folder_id))) - } - - async fn file_exists_at_path(&self, path: &Path) -> StorageMediatorResult { - let abs_path = self.resolve_path(&StoragePath::from_string(&path.to_string_lossy())); - Ok(abs_path.exists() && abs_path.is_file()) - } - - async fn file_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult { - let abs_path = self.resolve_path(storage_path); - Ok(abs_path.exists() && abs_path.is_file()) - } - - async fn folder_exists_at_path(&self, path: &Path) -> StorageMediatorResult { - let abs_path = self.resolve_path(&StoragePath::from_string(&path.to_string_lossy())); - Ok(abs_path.exists() && abs_path.is_dir()) - } - - async fn folder_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult { - let abs_path = self.resolve_path(storage_path); - Ok(abs_path.exists() && abs_path.is_dir()) - } - - fn resolve_path(&self, relative_path: &Path) -> PathBuf { - // Convert path to storage path then resolve - let path_str = relative_path.to_string_lossy().to_string(); - let storage_path = StoragePath::from_string(&path_str); - self.resolve_path(&storage_path) - } - - fn resolve_storage_path(&self, storage_path: &StoragePath) -> PathBuf { - self.resolve_path(storage_path) - } - - async fn ensure_directory(&self, path: &Path) -> StorageMediatorResult<()> { - let abs_path = self.resolve_path(&StoragePath::from_string(&path.to_string_lossy())); - - if !abs_path.exists() { - fs::create_dir_all(&abs_path).await - .map_err(|e| StorageMediatorError::AccessError(format!("Failed to create directory: {}", e)))?; - } else if !abs_path.is_dir() { - return Err(StorageMediatorError::InvalidPath( - format!("Path exists but is not a directory: {}", abs_path.display()) - )); - } - - Ok(()) - } - - async fn ensure_storage_directory(&self, storage_path: &StoragePath) -> StorageMediatorResult<()> { - let abs_path = self.resolve_path(storage_path); - - if !abs_path.exists() { - fs::create_dir_all(&abs_path).await - .map_err(|e| StorageMediatorError::AccessError(format!("Failed to create directory: {}", e)))?; - } else if !abs_path.is_dir() { - return Err(StorageMediatorError::InvalidPath( - format!("Path exists but is not a directory: {}", abs_path.display()) - )); - } - - Ok(()) - } -} - #[cfg(test)] mod tests { use super::*; #[test] - fn test_resolve_path() { - let service = PathService::new(PathBuf::from("/storage")); - - let storage_path = StoragePath::from_string("test/file.txt"); - let absolute = service.resolve_path(&storage_path); - - assert_eq!(absolute, PathBuf::from("/storage/test/file.txt")); + fn test_storage_path_from_string() { + let path = StoragePath::from_string("folder/subfolder/file.txt"); + assert_eq!(path.segments(), &["folder", "subfolder", "file.txt"]); + assert_eq!(path.to_string(), "/folder/subfolder/file.txt"); } #[test] - fn test_to_storage_path() { - let service = PathService::new(PathBuf::from("/storage")); - - let physical_path = PathBuf::from("/storage/folder/file.txt"); - let storage_path = service.to_storage_path(&physical_path).unwrap(); - - assert_eq!(storage_path.to_string(), "/folder/file.txt"); + fn test_storage_path_join() { + let path = StoragePath::from_string("folder"); + let joined = path.join("file.txt"); + assert_eq!(joined.to_string(), "/folder/file.txt"); } #[test] - fn test_is_in_root() { - let service = PathService::new(PathBuf::from("/storage")); - - let root_path = StoragePath::from_string("file.txt"); - let nested_path = StoragePath::from_string("folder/file.txt"); - - assert!(service.is_in_root(&root_path)); - assert!(!service.is_in_root(&nested_path)); + fn test_storage_path_parent() { + let path = StoragePath::from_string("folder/file.txt"); + let parent = path.parent().unwrap(); + assert_eq!(parent.to_string(), "/folder"); } #[test] - fn test_is_direct_child() { - let service = PathService::new(PathBuf::from("/storage")); - - let parent = StoragePath::from_string("folder"); - let child = StoragePath::from_string("folder/file.txt"); - let not_child = StoragePath::from_string("folder2/file.txt"); - - assert!(service.is_direct_child(&parent, &child)); - assert!(!service.is_direct_child(&parent, ¬_child)); + fn test_storage_path_root() { + let root = StoragePath::root(); + assert!(root.is_empty()); + assert_eq!(root.to_string(), "/"); } #[test] - fn test_create_file_path() { - let service = PathService::new(PathBuf::from("/storage")); - - let folder_path = StoragePath::from_string("folder"); - let file_path = service.create_file_path(&folder_path, "file.txt"); - - assert_eq!(file_path.to_string(), "/folder/file.txt"); + fn test_storage_path_file_name() { + let path = StoragePath::from_string("folder/file.txt"); + assert_eq!(path.file_name(), Some("file.txt".to_string())); } } \ No newline at end of file diff --git a/src/infrastructure/adapters/calendar_storage_adapter.rs b/src/infrastructure/adapters/calendar_storage_adapter.rs new file mode 100644 index 00000000..316ccf35 --- /dev/null +++ b/src/infrastructure/adapters/calendar_storage_adapter.rs @@ -0,0 +1,300 @@ +//! Calendar Storage Adapter +//! +//! This adapter implements the `CalendarStoragePort` application port using +//! the `CalendarRepository` and `CalendarEventRepository` domain repositories. +//! It bridges the gap between the application layer and the infrastructure layer. + +use std::sync::Arc; +use std::collections::HashMap; +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use uuid::Uuid; + +use crate::application::dtos::calendar_dto::{ + CalendarDto, CalendarEventDto, CreateCalendarDto, UpdateCalendarDto, + CreateEventDto, UpdateEventDto, CreateEventICalDto +}; +use crate::application::ports::calendar_ports::CalendarStoragePort; +use crate::common::errors::{DomainError, ErrorKind}; +use crate::domain::entities::calendar::Calendar; +use crate::domain::entities::calendar_event::CalendarEvent; +use crate::domain::repositories::calendar_repository::CalendarRepository; +use crate::domain::repositories::calendar_event_repository::CalendarEventRepository; + +/// Adapter that implements CalendarStoragePort using domain repositories +pub struct CalendarStorageAdapter { + calendar_repository: Arc, + event_repository: Arc, +} + +impl CalendarStorageAdapter { + /// Creates a new CalendarStorageAdapter with the given repositories + pub fn new( + calendar_repository: Arc, + event_repository: Arc, + ) -> Self { + Self { + calendar_repository, + event_repository, + } + } +} + +#[async_trait] +impl CalendarStoragePort for CalendarStorageAdapter { + // Calendar operations + + async fn create_calendar(&self, dto: CreateCalendarDto, owner_id: &str) -> Result { + let calendar = Calendar::new( + dto.name, + owner_id.to_string(), + dto.description, + dto.color, + )?; + + let created = self.calendar_repository.create_calendar(calendar).await?; + Ok(CalendarDto::from(created)) + } + + async fn update_calendar(&self, calendar_id: &str, update: UpdateCalendarDto) -> Result { + let uuid = Uuid::parse_str(calendar_id) + .map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?; + + let mut calendar = self.calendar_repository.find_calendar_by_id(&uuid).await?; + + if let Some(name) = update.name { + calendar.update_name(name)?; + } + if let Some(description) = update.description { + calendar.update_description(Some(description)); + } + if let Some(color) = update.color { + calendar.update_color(Some(color))?; + } + + let updated = self.calendar_repository.update_calendar(calendar).await?; + Ok(CalendarDto::from(updated)) + } + + async fn delete_calendar(&self, calendar_id: &str) -> Result<(), DomainError> { + let uuid = Uuid::parse_str(calendar_id) + .map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?; + + // First delete all events in the calendar + self.event_repository.delete_all_events_in_calendar(&uuid).await?; + + // Then delete the calendar itself + self.calendar_repository.delete_calendar(&uuid).await + } + + async fn get_calendar(&self, calendar_id: &str) -> Result { + let uuid = Uuid::parse_str(calendar_id) + .map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?; + + let calendar = self.calendar_repository.find_calendar_by_id(&uuid).await?; + Ok(CalendarDto::from(calendar)) + } + + async fn list_calendars_by_owner(&self, owner_id: &str) -> Result, DomainError> { + let calendars = self.calendar_repository.list_calendars_by_owner(owner_id).await?; + Ok(calendars.into_iter().map(CalendarDto::from).collect()) + } + + async fn list_calendars_shared_with_user(&self, user_id: &str) -> Result, DomainError> { + let calendars = self.calendar_repository.list_calendars_shared_with_user(user_id).await?; + Ok(calendars.into_iter().map(CalendarDto::from).collect()) + } + + async fn list_public_calendars(&self, limit: i64, offset: i64) -> Result, DomainError> { + let calendars = self.calendar_repository.list_public_calendars(limit, offset).await?; + Ok(calendars.into_iter().map(CalendarDto::from).collect()) + } + + async fn check_calendar_access(&self, calendar_id: &str, user_id: &str) -> Result { + let uuid = Uuid::parse_str(calendar_id) + .map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?; + + self.calendar_repository.user_has_calendar_access(&uuid, user_id).await + } + + // Calendar sharing + + async fn share_calendar(&self, calendar_id: &str, user_id: &str, access_level: &str) -> Result<(), DomainError> { + let uuid = Uuid::parse_str(calendar_id) + .map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?; + + self.calendar_repository.share_calendar(&uuid, user_id, access_level).await + } + + async fn remove_calendar_sharing(&self, calendar_id: &str, user_id: &str) -> Result<(), DomainError> { + let uuid = Uuid::parse_str(calendar_id) + .map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?; + + self.calendar_repository.remove_calendar_sharing(&uuid, user_id).await + } + + async fn get_calendar_shares(&self, calendar_id: &str) -> Result, DomainError> { + let uuid = Uuid::parse_str(calendar_id) + .map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?; + + self.calendar_repository.get_calendar_shares(&uuid).await + } + + // Calendar properties + + async fn set_calendar_property(&self, calendar_id: &str, property_name: &str, property_value: &str) -> Result<(), DomainError> { + let uuid = Uuid::parse_str(calendar_id) + .map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?; + + self.calendar_repository.set_calendar_property(&uuid, property_name, property_value).await + } + + async fn get_calendar_property(&self, calendar_id: &str, property_name: &str) -> Result, DomainError> { + let uuid = Uuid::parse_str(calendar_id) + .map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?; + + self.calendar_repository.get_calendar_property(&uuid, property_name).await + } + + async fn get_calendar_properties(&self, calendar_id: &str) -> Result, DomainError> { + let uuid = Uuid::parse_str(calendar_id) + .map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?; + + self.calendar_repository.get_calendar_properties(&uuid).await + } + + // Event operations + + async fn create_event(&self, dto: CreateEventDto) -> Result { + let calendar_id = Uuid::parse_str(&dto.calendar_id) + .map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Event", "Invalid calendar ID format"))?; + + // Verify calendar exists and user has access + let _calendar = self.calendar_repository.find_calendar_by_id(&calendar_id).await?; + + // Generate basic iCal data + let ical_data = format!( + "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//OxiCloud//EN\nBEGIN:VEVENT\nUID:{}@oxicloud\nDTSTAMP:{}\nDTSTART:{}\nDTEND:{}\nSUMMARY:{}\nEND:VEVENT\nEND:VCALENDAR", + uuid::Uuid::new_v4(), + chrono::Utc::now().format("%Y%m%dT%H%M%SZ"), + dto.start_time.format("%Y%m%dT%H%M%SZ"), + dto.end_time.format("%Y%m%dT%H%M%SZ"), + dto.summary + ); + + let event = CalendarEvent::new( + calendar_id, + dto.summary, + dto.description, + dto.location, + dto.start_time, + dto.end_time, + dto.all_day.unwrap_or(false), + dto.rrule, + ical_data, + )?; + + let created = self.event_repository.create_event(event).await?; + Ok(CalendarEventDto::from(created)) + } + + async fn create_event_from_ical(&self, dto: CreateEventICalDto) -> Result { + let calendar_id = Uuid::parse_str(&dto.calendar_id) + .map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Event", "Invalid calendar ID format"))?; + + // Verify calendar exists + let _calendar = self.calendar_repository.find_calendar_by_id(&calendar_id).await?; + + // Parse iCal data and create event + let event = CalendarEvent::from_ical(calendar_id, dto.ical_data.clone())?; + + let created = self.event_repository.create_event(event).await?; + Ok(CalendarEventDto::from(created)) + } + + async fn update_event(&self, event_id: &str, update: UpdateEventDto) -> Result { + let uuid = Uuid::parse_str(event_id) + .map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Event", "Invalid event ID format"))?; + + let mut event = self.event_repository.find_event_by_id(&uuid).await?; + + if let Some(summary) = update.summary { + event.update_summary(summary)?; + } + if let Some(description) = update.description { + event.update_description(Some(description)); + } + if let Some(location) = update.location { + event.update_location(Some(location)); + } + if let Some(start_time) = update.start_time { + if let Some(end_time) = update.end_time { + event.update_time_range(start_time, end_time)?; + } else { + event.update_time_range(start_time, *event.end_time())?; + } + } else if let Some(end_time) = update.end_time { + event.update_time_range(*event.start_time(), end_time)?; + } + if let Some(all_day) = update.all_day { + event.update_all_day(all_day); + } + if let Some(rrule) = update.rrule { + event.update_rrule(Some(rrule))?; + } + + let updated = self.event_repository.update_event(event).await?; + Ok(CalendarEventDto::from(updated)) + } + + async fn delete_event(&self, event_id: &str) -> Result<(), DomainError> { + let uuid = Uuid::parse_str(event_id) + .map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Event", "Invalid event ID format"))?; + + self.event_repository.delete_event(&uuid).await + } + + async fn get_event(&self, event_id: &str) -> Result { + let uuid = Uuid::parse_str(event_id) + .map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Event", "Invalid event ID format"))?; + + let event = self.event_repository.find_event_by_id(&uuid).await?; + Ok(CalendarEventDto::from(event)) + } + + async fn list_events_by_calendar(&self, calendar_id: &str) -> Result, DomainError> { + let uuid = Uuid::parse_str(calendar_id) + .map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?; + + let events = self.event_repository.list_events_by_calendar(&uuid).await?; + Ok(events.into_iter().map(CalendarEventDto::from).collect()) + } + + async fn list_events_by_calendar_paginated(&self, calendar_id: &str, limit: i64, offset: i64) -> Result, DomainError> { + let uuid = Uuid::parse_str(calendar_id) + .map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?; + + let events = self.event_repository.list_events_by_calendar_paginated(&uuid, limit, offset).await?; + Ok(events.into_iter().map(CalendarEventDto::from).collect()) + } + + async fn get_events_in_time_range( + &self, + calendar_id: &str, + start: &DateTime, + end: &DateTime + ) -> Result, DomainError> { + let uuid = Uuid::parse_str(calendar_id) + .map_err(|_| DomainError::new(ErrorKind::InvalidInput, "Calendar", "Invalid calendar ID format"))?; + + let events = self.event_repository.get_events_in_time_range(&uuid, start, end).await?; + Ok(events.into_iter().map(CalendarEventDto::from).collect()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // Tests would go here using mock repositories +} diff --git a/src/infrastructure/adapters/contact_storage_adapter.rs b/src/infrastructure/adapters/contact_storage_adapter.rs new file mode 100644 index 00000000..ca43eb05 --- /dev/null +++ b/src/infrastructure/adapters/contact_storage_adapter.rs @@ -0,0 +1,645 @@ +//! Contact Storage Adapter +//! +//! This adapter implements the `AddressBookUseCase` and `ContactUseCase` application ports +//! using the domain repositories. It bridges the gap between the application layer +//! and the infrastructure layer for CardDAV functionality. + +use std::sync::Arc; +use async_trait::async_trait; +use uuid::Uuid; + +use crate::application::dtos::address_book_dto::{ + AddressBookDto, CreateAddressBookDto, UpdateAddressBookDto, + ShareAddressBookDto, UnshareAddressBookDto +}; +use crate::application::dtos::contact_dto::{ + ContactDto, CreateContactDto, UpdateContactDto, CreateContactVCardDto, + ContactGroupDto, CreateContactGroupDto, UpdateContactGroupDto, GroupMembershipDto, + EmailDto, PhoneDto, AddressDto +}; +use crate::application::ports::carddav_ports::{AddressBookUseCase, ContactUseCase}; +use crate::common::errors::{DomainError, ErrorKind}; +use crate::domain::entities::contact::{AddressBook, Contact, ContactGroup, Email, Phone, Address}; +use crate::domain::repositories::address_book_repository::AddressBookRepository; +use crate::domain::repositories::contact_repository::{ContactRepository, ContactGroupRepository}; + +/// Adapter that implements AddressBookUseCase and ContactUseCase using domain repositories +pub struct ContactStorageAdapter { + address_book_repository: Arc, + contact_repository: Arc, + group_repository: Arc, +} + +impl ContactStorageAdapter { + /// Creates a new ContactStorageAdapter with the given repositories + pub fn new( + address_book_repository: Arc, + contact_repository: Arc, + group_repository: Arc, + ) -> Self { + Self { + address_book_repository, + contact_repository, + group_repository, + } + } + + /// Helper to parse UUID from string + fn parse_uuid(id: &str, entity_name: &'static str) -> Result { + Uuid::parse_str(id) + .map_err(|_| DomainError::new(ErrorKind::InvalidInput, entity_name, format!("Invalid {} ID format", entity_name))) + } + + /// Helper to check if user has access to an address book + async fn check_address_book_access(&self, address_book_id: &Uuid, user_id: &str) -> Result { + let address_book = self.address_book_repository + .get_address_book_by_id(address_book_id) + .await? + .ok_or_else(|| DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found"))?; + + // Check if user is owner + if address_book.owner_id == user_id { + return Ok(address_book); + } + + // Check if address book is public + if address_book.is_public { + return Ok(address_book); + } + + // Check if address book is shared with user + let shares = self.address_book_repository.get_address_book_shares(address_book_id).await?; + if shares.iter().any(|(shared_user, _)| shared_user == user_id) { + return Ok(address_book); + } + + Err(DomainError::new(ErrorKind::AccessDenied, "AddressBook", "Access denied to address book")) + } + + /// Helper to check write access + async fn check_write_access(&self, address_book_id: &Uuid, user_id: &str) -> Result { + let address_book = self.address_book_repository + .get_address_book_by_id(address_book_id) + .await? + .ok_or_else(|| DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found"))?; + + // Owner always has write access + if address_book.owner_id == user_id { + return Ok(address_book); + } + + // Check shares for write permission + let shares = self.address_book_repository.get_address_book_shares(address_book_id).await?; + if shares.iter().any(|(shared_user, can_write)| shared_user == user_id && *can_write) { + return Ok(address_book); + } + + Err(DomainError::new(ErrorKind::AccessDenied, "AddressBook", "Write access denied")) + } + + /// Convert EmailDto to domain Email + fn dto_to_email(dto: EmailDto) -> Email { + Email { + email: dto.email, + r#type: dto.r#type, + is_primary: dto.is_primary, + } + } + + /// Convert PhoneDto to domain Phone + fn dto_to_phone(dto: PhoneDto) -> Phone { + Phone { + number: dto.number, + r#type: dto.r#type, + is_primary: dto.is_primary, + } + } + + /// Convert AddressDto to domain Address + fn dto_to_address(dto: AddressDto) -> Address { + Address { + street: dto.street, + city: dto.city, + state: dto.state, + postal_code: dto.postal_code, + country: dto.country, + r#type: dto.r#type, + is_primary: dto.is_primary, + } + } + + /// Generate vCard from contact data + fn generate_vcard(contact: &Contact) -> String { + let mut vcard = String::from("BEGIN:VCARD\nVERSION:3.0\n"); + + if let Some(ref full_name) = contact.full_name { + vcard.push_str(&format!("FN:{}\n", full_name)); + } + + if contact.first_name.is_some() || contact.last_name.is_some() { + let last = contact.last_name.as_deref().unwrap_or(""); + let first = contact.first_name.as_deref().unwrap_or(""); + vcard.push_str(&format!("N:{};{};;;\n", last, first)); + } + + if let Some(ref nickname) = contact.nickname { + vcard.push_str(&format!("NICKNAME:{}\n", nickname)); + } + + for email in &contact.email { + vcard.push_str(&format!("EMAIL;TYPE={}:{}\n", email.r#type.to_uppercase(), email.email)); + } + + for phone in &contact.phone { + vcard.push_str(&format!("TEL;TYPE={}:{}\n", phone.r#type.to_uppercase(), phone.number)); + } + + if let Some(ref org) = contact.organization { + vcard.push_str(&format!("ORG:{}\n", org)); + } + + if let Some(ref title) = contact.title { + vcard.push_str(&format!("TITLE:{}\n", title)); + } + + if let Some(ref notes) = contact.notes { + vcard.push_str(&format!("NOTE:{}\n", notes)); + } + + vcard.push_str(&format!("UID:{}\n", contact.uid)); + vcard.push_str("END:VCARD\n"); + + vcard + } +} + +#[async_trait] +impl AddressBookUseCase for ContactStorageAdapter { + async fn create_address_book(&self, dto: CreateAddressBookDto) -> Result { + let address_book = AddressBook { + id: Uuid::new_v4(), + name: dto.name, + owner_id: dto.owner_id, + description: dto.description, + color: dto.color, + is_public: dto.is_public.unwrap_or(false), + created_at: chrono::Utc::now(), + updated_at: chrono::Utc::now(), + }; + + let created = self.address_book_repository.create_address_book(address_book).await?; + Ok(AddressBookDto::from(created)) + } + + async fn update_address_book(&self, address_book_id: &str, update: UpdateAddressBookDto) -> Result { + let uuid = Self::parse_uuid(address_book_id, "AddressBook")?; + + // Check write access + let mut address_book = self.check_write_access(&uuid, &update.user_id).await?; + + if let Some(name) = update.name { + address_book.name = name; + } + if let Some(description) = update.description { + address_book.description = Some(description); + } + if let Some(color) = update.color { + address_book.color = Some(color); + } + if let Some(is_public) = update.is_public { + address_book.is_public = is_public; + } + address_book.updated_at = chrono::Utc::now(); + + let updated = self.address_book_repository.update_address_book(address_book).await?; + Ok(AddressBookDto::from(updated)) + } + + async fn delete_address_book(&self, address_book_id: &str, user_id: &str) -> Result<(), DomainError> { + let uuid = Self::parse_uuid(address_book_id, "AddressBook")?; + + // Only owner can delete + let address_book = self.address_book_repository + .get_address_book_by_id(&uuid) + .await? + .ok_or_else(|| DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found"))?; + + if address_book.owner_id != user_id { + return Err(DomainError::new(ErrorKind::AccessDenied, "AddressBook", "Only owner can delete address book")); + } + + self.address_book_repository.delete_address_book(&uuid).await + } + + async fn get_address_book(&self, address_book_id: &str, user_id: &str) -> Result { + let uuid = Self::parse_uuid(address_book_id, "AddressBook")?; + let address_book = self.check_address_book_access(&uuid, user_id).await?; + Ok(AddressBookDto::from(address_book)) + } + + async fn list_user_address_books(&self, user_id: &str) -> Result, DomainError> { + let owned = self.address_book_repository.get_address_books_by_owner(user_id).await?; + let shared = self.address_book_repository.get_shared_address_books(user_id).await?; + + let mut all_books: Vec = owned; + all_books.extend(shared); + + Ok(all_books.into_iter().map(AddressBookDto::from).collect()) + } + + async fn list_public_address_books(&self) -> Result, DomainError> { + let public = self.address_book_repository.get_public_address_books().await?; + Ok(public.into_iter().map(AddressBookDto::from).collect()) + } + + async fn share_address_book(&self, dto: ShareAddressBookDto, user_id: &str) -> Result<(), DomainError> { + let uuid = Self::parse_uuid(&dto.address_book_id, "AddressBook")?; + + // Only owner can share + let address_book = self.address_book_repository + .get_address_book_by_id(&uuid) + .await? + .ok_or_else(|| DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found"))?; + + if address_book.owner_id != user_id { + return Err(DomainError::new(ErrorKind::AccessDenied, "AddressBook", "Only owner can share")); + } + + self.address_book_repository.share_address_book(&uuid, &dto.user_id, dto.can_write).await + } + + async fn unshare_address_book(&self, dto: UnshareAddressBookDto, user_id: &str) -> Result<(), DomainError> { + let uuid = Self::parse_uuid(&dto.address_book_id, "AddressBook")?; + + // Only owner can unshare + let address_book = self.address_book_repository + .get_address_book_by_id(&uuid) + .await? + .ok_or_else(|| DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found"))?; + + if address_book.owner_id != user_id { + return Err(DomainError::new(ErrorKind::AccessDenied, "AddressBook", "Only owner can unshare")); + } + + self.address_book_repository.unshare_address_book(&uuid, &dto.user_id).await + } + + async fn get_address_book_shares(&self, address_book_id: &str, user_id: &str) -> Result, DomainError> { + let uuid = Self::parse_uuid(address_book_id, "AddressBook")?; + + // Only owner can view shares + let address_book = self.address_book_repository + .get_address_book_by_id(&uuid) + .await? + .ok_or_else(|| DomainError::new(ErrorKind::NotFound, "AddressBook", "Address book not found"))?; + + if address_book.owner_id != user_id { + return Err(DomainError::new(ErrorKind::AccessDenied, "AddressBook", "Only owner can view shares")); + } + + self.address_book_repository.get_address_book_shares(&uuid).await + } +} + +#[async_trait] +impl ContactUseCase for ContactStorageAdapter { + async fn create_contact(&self, dto: CreateContactDto) -> Result { + let address_book_id = Self::parse_uuid(&dto.address_book_id, "AddressBook")?; + + // Check write access + self.check_write_access(&address_book_id, &dto.user_id).await?; + + let contact = Contact { + id: Uuid::new_v4(), + address_book_id, + uid: format!("{}@oxicloud", Uuid::new_v4()), + full_name: dto.full_name, + first_name: dto.first_name, + last_name: dto.last_name, + nickname: dto.nickname, + email: dto.email.into_iter().map(Self::dto_to_email).collect(), + phone: dto.phone.into_iter().map(Self::dto_to_phone).collect(), + address: dto.address.into_iter().map(Self::dto_to_address).collect(), + organization: dto.organization, + title: dto.title, + notes: dto.notes, + photo_url: dto.photo_url, + birthday: dto.birthday, + anniversary: dto.anniversary, + vcard: String::new(), + etag: Uuid::new_v4().to_string(), + created_at: chrono::Utc::now(), + updated_at: chrono::Utc::now(), + }; + + // Generate vCard + let mut contact_with_vcard = contact; + contact_with_vcard.vcard = Self::generate_vcard(&contact_with_vcard); + + let created = self.contact_repository.create_contact(contact_with_vcard).await?; + Ok(ContactDto::from(created)) + } + + async fn create_contact_from_vcard(&self, dto: CreateContactVCardDto) -> Result { + let address_book_id = Self::parse_uuid(&dto.address_book_id, "AddressBook")?; + + // Check write access + self.check_write_access(&address_book_id, &dto.user_id).await?; + + // Parse vCard - for now, create a basic contact with the raw vCard + let contact = Contact { + id: Uuid::new_v4(), + address_book_id, + uid: format!("{}@oxicloud", Uuid::new_v4()), + full_name: Some("Imported Contact".to_string()), + first_name: None, + last_name: None, + nickname: None, + email: Vec::new(), + phone: Vec::new(), + address: Vec::new(), + organization: None, + title: None, + notes: None, + photo_url: None, + birthday: None, + anniversary: None, + vcard: dto.vcard, + etag: Uuid::new_v4().to_string(), + created_at: chrono::Utc::now(), + updated_at: chrono::Utc::now(), + }; + + let created = self.contact_repository.create_contact(contact).await?; + Ok(ContactDto::from(created)) + } + + async fn update_contact(&self, contact_id: &str, update: UpdateContactDto) -> Result { + let uuid = Self::parse_uuid(contact_id, "Contact")?; + + let mut contact = self.contact_repository + .get_contact_by_id(&uuid) + .await? + .ok_or_else(|| DomainError::new(ErrorKind::NotFound, "Contact", "Contact not found"))?; + + // Check write access to the address book + self.check_write_access(&contact.address_book_id, &update.user_id).await?; + + if let Some(full_name) = update.full_name { + contact.full_name = Some(full_name); + } + if let Some(first_name) = update.first_name { + contact.first_name = Some(first_name); + } + if let Some(last_name) = update.last_name { + contact.last_name = Some(last_name); + } + if let Some(nickname) = update.nickname { + contact.nickname = Some(nickname); + } + if let Some(emails) = update.email { + contact.email = emails.into_iter().map(Self::dto_to_email).collect(); + } + if let Some(phones) = update.phone { + contact.phone = phones.into_iter().map(Self::dto_to_phone).collect(); + } + if let Some(addresses) = update.address { + contact.address = addresses.into_iter().map(Self::dto_to_address).collect(); + } + if let Some(organization) = update.organization { + contact.organization = Some(organization); + } + if let Some(title) = update.title { + contact.title = Some(title); + } + if let Some(notes) = update.notes { + contact.notes = Some(notes); + } + if let Some(photo_url) = update.photo_url { + contact.photo_url = Some(photo_url); + } + if let Some(birthday) = update.birthday { + contact.birthday = Some(birthday); + } + if let Some(anniversary) = update.anniversary { + contact.anniversary = Some(anniversary); + } + + contact.updated_at = chrono::Utc::now(); + contact.etag = Uuid::new_v4().to_string(); + contact.vcard = Self::generate_vcard(&contact); + + let updated = self.contact_repository.update_contact(contact).await?; + Ok(ContactDto::from(updated)) + } + + async fn delete_contact(&self, contact_id: &str, user_id: &str) -> Result<(), DomainError> { + let uuid = Self::parse_uuid(contact_id, "Contact")?; + + let contact = self.contact_repository + .get_contact_by_id(&uuid) + .await? + .ok_or_else(|| DomainError::new(ErrorKind::NotFound, "Contact", "Contact not found"))?; + + // Check write access + self.check_write_access(&contact.address_book_id, user_id).await?; + + self.contact_repository.delete_contact(&uuid).await + } + + async fn get_contact(&self, contact_id: &str, user_id: &str) -> Result { + let uuid = Self::parse_uuid(contact_id, "Contact")?; + + let contact = self.contact_repository + .get_contact_by_id(&uuid) + .await? + .ok_or_else(|| DomainError::new(ErrorKind::NotFound, "Contact", "Contact not found"))?; + + // Check read access + self.check_address_book_access(&contact.address_book_id, user_id).await?; + + Ok(ContactDto::from(contact)) + } + + async fn list_contacts(&self, address_book_id: &str, user_id: &str) -> Result, DomainError> { + let uuid = Self::parse_uuid(address_book_id, "AddressBook")?; + + // Check read access + self.check_address_book_access(&uuid, user_id).await?; + + let contacts = self.contact_repository.get_contacts_by_address_book(&uuid).await?; + Ok(contacts.into_iter().map(ContactDto::from).collect()) + } + + async fn search_contacts(&self, address_book_id: &str, query: &str, user_id: &str) -> Result, DomainError> { + let uuid = Self::parse_uuid(address_book_id, "AddressBook")?; + + // Check read access + self.check_address_book_access(&uuid, user_id).await?; + + let contacts = self.contact_repository.search_contacts(&uuid, query).await?; + Ok(contacts.into_iter().map(ContactDto::from).collect()) + } + + async fn create_group(&self, dto: CreateContactGroupDto) -> Result { + let address_book_id = Self::parse_uuid(&dto.address_book_id, "AddressBook")?; + + // Check write access + self.check_write_access(&address_book_id, &dto.user_id).await?; + + let group = ContactGroup { + id: Uuid::new_v4(), + address_book_id, + name: dto.name, + created_at: chrono::Utc::now(), + updated_at: chrono::Utc::now(), + }; + + let created = self.group_repository.create_group(group).await?; + Ok(ContactGroupDto::from(created)) + } + + async fn update_group(&self, group_id: &str, update: UpdateContactGroupDto) -> Result { + let uuid = Self::parse_uuid(group_id, "ContactGroup")?; + + let mut group = self.group_repository + .get_group_by_id(&uuid) + .await? + .ok_or_else(|| DomainError::new(ErrorKind::NotFound, "ContactGroup", "Group not found"))?; + + // Check write access + self.check_write_access(&group.address_book_id, &update.user_id).await?; + + group.name = update.name; + group.updated_at = chrono::Utc::now(); + + let updated = self.group_repository.update_group(group).await?; + Ok(ContactGroupDto::from(updated)) + } + + async fn delete_group(&self, group_id: &str, user_id: &str) -> Result<(), DomainError> { + let uuid = Self::parse_uuid(group_id, "ContactGroup")?; + + let group = self.group_repository + .get_group_by_id(&uuid) + .await? + .ok_or_else(|| DomainError::new(ErrorKind::NotFound, "ContactGroup", "Group not found"))?; + + // Check write access + self.check_write_access(&group.address_book_id, user_id).await?; + + self.group_repository.delete_group(&uuid).await + } + + async fn get_group(&self, group_id: &str, user_id: &str) -> Result { + let uuid = Self::parse_uuid(group_id, "ContactGroup")?; + + let group = self.group_repository + .get_group_by_id(&uuid) + .await? + .ok_or_else(|| DomainError::new(ErrorKind::NotFound, "ContactGroup", "Group not found"))?; + + // Check read access + self.check_address_book_access(&group.address_book_id, user_id).await?; + + Ok(ContactGroupDto::from(group)) + } + + async fn list_groups(&self, address_book_id: &str, user_id: &str) -> Result, DomainError> { + let uuid = Self::parse_uuid(address_book_id, "AddressBook")?; + + // Check read access + self.check_address_book_access(&uuid, user_id).await?; + + let groups = self.group_repository.get_groups_by_address_book(&uuid).await?; + Ok(groups.into_iter().map(ContactGroupDto::from).collect()) + } + + async fn add_contact_to_group(&self, dto: GroupMembershipDto, user_id: &str) -> Result<(), DomainError> { + let group_id = Self::parse_uuid(&dto.group_id, "ContactGroup")?; + let contact_id = Self::parse_uuid(&dto.contact_id, "Contact")?; + + let group = self.group_repository + .get_group_by_id(&group_id) + .await? + .ok_or_else(|| DomainError::new(ErrorKind::NotFound, "ContactGroup", "Group not found"))?; + + // Check write access + self.check_write_access(&group.address_book_id, user_id).await?; + + self.group_repository.add_contact_to_group(&group_id, &contact_id).await + } + + async fn remove_contact_from_group(&self, dto: GroupMembershipDto, user_id: &str) -> Result<(), DomainError> { + let group_id = Self::parse_uuid(&dto.group_id, "ContactGroup")?; + let contact_id = Self::parse_uuid(&dto.contact_id, "Contact")?; + + let group = self.group_repository + .get_group_by_id(&group_id) + .await? + .ok_or_else(|| DomainError::new(ErrorKind::NotFound, "ContactGroup", "Group not found"))?; + + // Check write access + self.check_write_access(&group.address_book_id, user_id).await?; + + self.group_repository.remove_contact_from_group(&group_id, &contact_id).await + } + + async fn list_contacts_in_group(&self, group_id: &str, user_id: &str) -> Result, DomainError> { + let uuid = Self::parse_uuid(group_id, "ContactGroup")?; + + let group = self.group_repository + .get_group_by_id(&uuid) + .await? + .ok_or_else(|| DomainError::new(ErrorKind::NotFound, "ContactGroup", "Group not found"))?; + + // Check read access + self.check_address_book_access(&group.address_book_id, user_id).await?; + + let contacts = self.group_repository.get_contacts_in_group(&uuid).await?; + Ok(contacts.into_iter().map(ContactDto::from).collect()) + } + + async fn list_groups_for_contact(&self, contact_id: &str, user_id: &str) -> Result, DomainError> { + let uuid = Self::parse_uuid(contact_id, "Contact")?; + + let contact = self.contact_repository + .get_contact_by_id(&uuid) + .await? + .ok_or_else(|| DomainError::new(ErrorKind::NotFound, "Contact", "Contact not found"))?; + + // Check read access + self.check_address_book_access(&contact.address_book_id, user_id).await?; + + let groups = self.group_repository.get_groups_for_contact(&uuid).await?; + Ok(groups.into_iter().map(ContactGroupDto::from).collect()) + } + + async fn get_contact_vcard(&self, contact_id: &str, user_id: &str) -> Result { + let uuid = Self::parse_uuid(contact_id, "Contact")?; + + let contact = self.contact_repository + .get_contact_by_id(&uuid) + .await? + .ok_or_else(|| DomainError::new(ErrorKind::NotFound, "Contact", "Contact not found"))?; + + // Check read access + self.check_address_book_access(&contact.address_book_id, user_id).await?; + + Ok(contact.vcard) + } + + async fn get_contacts_as_vcards(&self, address_book_id: &str, user_id: &str) -> Result, DomainError> { + let uuid = Self::parse_uuid(address_book_id, "AddressBook")?; + + // Check read access + self.check_address_book_access(&uuid, user_id).await?; + + let contacts = self.contact_repository.get_contacts_by_address_book(&uuid).await?; + + Ok(contacts + .into_iter() + .map(|c| (c.id.to_string(), c.vcard)) + .collect()) + } +} diff --git a/src/infrastructure/adapters/mod.rs b/src/infrastructure/adapters/mod.rs new file mode 100644 index 00000000..0311cc94 --- /dev/null +++ b/src/infrastructure/adapters/mod.rs @@ -0,0 +1,11 @@ +//! Infrastructure Adapters +//! +//! This module contains adapters that bridge the gap between domain repositories +//! and application ports. These adapters implement the application layer ports +//! using the infrastructure layer repositories. + +pub mod calendar_storage_adapter; +pub mod contact_storage_adapter; + +pub use calendar_storage_adapter::CalendarStorageAdapter; +pub use contact_storage_adapter::ContactStorageAdapter; diff --git a/src/infrastructure/mod.rs b/src/infrastructure/mod.rs index e14f8fad..a8330fa3 100644 --- a/src/infrastructure/mod.rs +++ b/src/infrastructure/mod.rs @@ -1,3 +1,4 @@ +pub mod adapters; pub mod repositories; pub mod services; diff --git a/src/infrastructure/repositories/file_fs_read_repository.rs b/src/infrastructure/repositories/file_fs_read_repository.rs index 33b2aec3..d8fde6a5 100644 --- a/src/infrastructure/repositories/file_fs_read_repository.rs +++ b/src/infrastructure/repositories/file_fs_read_repository.rs @@ -16,39 +16,30 @@ use crate::common::config::AppConfig; /// Implementación de repositorio para operaciones de lectura de archivos pub struct FileFsReadRepository { - root_path: PathBuf, metadata_manager: Arc, path_resolver: Arc, - config: AppConfig, - parallel_processor: Option>, } impl FileFsReadRepository { /// Crea un nuevo repositorio de lectura de archivos pub fn new( - root_path: PathBuf, + _root_path: PathBuf, metadata_manager: Arc, path_resolver: Arc, - config: AppConfig, - parallel_processor: Option>, + _config: AppConfig, + _parallel_processor: Option>, ) -> Self { Self { - root_path, metadata_manager, path_resolver, - config, - parallel_processor, } } /// Crea un stub para pruebas pub fn default_stub() -> Self { Self { - root_path: PathBuf::from("./storage"), metadata_manager: Arc::new(FileMetadataManager::default()), path_resolver: Arc::new(FilePathResolver::default_stub()), - config: AppConfig::default(), - parallel_processor: None, } } diff --git a/src/infrastructure/repositories/file_fs_repository.rs b/src/infrastructure/repositories/file_fs_repository.rs index 2638ff10..5ed1237e 100644 --- a/src/infrastructure/repositories/file_fs_repository.rs +++ b/src/infrastructure/repositories/file_fs_repository.rs @@ -22,7 +22,8 @@ use crate::application::services::storage_mediator::StorageMediator; // use crate::application::ports::outbound::IdMappingPort; use crate::infrastructure::services::id_mapping_service::IdMappingError; use crate::infrastructure::services::file_metadata_cache::{FileMetadataCache, CacheEntryType}; -use crate::domain::services::path_service::{StoragePath, PathService}; +use crate::domain::services::path_service::StoragePath; +use crate::infrastructure::services::path_service::PathService; use crate::common::errors::DomainError; use crate::common::config::AppConfig; use crate::application::ports::outbound::FileStoragePort; @@ -63,7 +64,6 @@ pub struct FileFsRepository { impl FileFsRepository { /// Creates a new filesystem-based file repository - #[allow(dead_code)] pub fn new( root_path: PathBuf, storage_mediator: Arc, @@ -389,14 +389,6 @@ impl From for FileRepositoryError { } } -// Add Timeout variant to FileRepositoryError -impl FileRepositoryError { - #[allow(dead_code)] - fn timeout(message: impl Into) -> Self { - FileRepositoryError::Timeout(message.into()) - } -} - // Errors are already defined by the FileRepositoryError interface // Enable cloning for concurrent operations diff --git a/src/infrastructure/repositories/file_fs_write_repository.rs b/src/infrastructure/repositories/file_fs_write_repository.rs index deded060..413c54a3 100644 --- a/src/infrastructure/repositories/file_fs_write_repository.rs +++ b/src/infrastructure/repositories/file_fs_write_repository.rs @@ -16,43 +16,34 @@ use crate::infrastructure::services::file_system_utils::FileSystemUtils; /// Implementación de repositorio para operaciones de escritura de archivos pub struct FileFsWriteRepository { - root_path: PathBuf, metadata_manager: Arc, path_resolver: Arc, - storage_mediator: Arc, config: AppConfig, - parallel_processor: Option>, } impl FileFsWriteRepository { /// Crea un nuevo repositorio de escritura de archivos pub fn new( - root_path: PathBuf, + _root_path: PathBuf, metadata_manager: Arc, path_resolver: Arc, - storage_mediator: Arc, + _storage_mediator: Arc, config: AppConfig, - parallel_processor: Option>, + _parallel_processor: Option>, ) -> Self { Self { - root_path, metadata_manager, path_resolver, - storage_mediator, config, - parallel_processor, } } /// Crea un stub para pruebas pub fn default_stub() -> Self { Self { - root_path: PathBuf::from("./storage"), metadata_manager: Arc::new(FileMetadataManager::default()), path_resolver: Arc::new(FilePathResolver::default_stub()), - storage_mediator: Arc::new(crate::application::services::storage_mediator::FileSystemStorageMediator::new_stub()), config: AppConfig::default(), - parallel_processor: None, } } @@ -108,13 +99,6 @@ impl FileFsWriteRepository { .map_err(|e| crate::domain::repositories::file_repository::FileRepositoryError::Other(e.to_string())) } } - - /// Elimina un archivo de forma no bloqueante - async fn delete_file_non_blocking(&self, _abs_path: PathBuf) -> FileRepositoryResult<()> { - // Implementación real debe eliminar el archivo - // Por ahora, devolvemos OK - Ok(()) - } } #[async_trait] diff --git a/src/infrastructure/repositories/file_path_resolver.rs b/src/infrastructure/repositories/file_path_resolver.rs index c55bb125..1ab681ed 100644 --- a/src/infrastructure/repositories/file_path_resolver.rs +++ b/src/infrastructure/repositories/file_path_resolver.rs @@ -2,7 +2,8 @@ use std::path::PathBuf; use std::sync::Arc; use async_trait::async_trait; -use crate::domain::services::path_service::{PathService, StoragePath}; +use crate::domain::services::path_service::StoragePath; +use crate::infrastructure::services::path_service::PathService; use crate::application::services::storage_mediator::StorageMediator; // use crate::application::ports::outbound::IdMappingPort; use crate::domain::repositories::file_repository::FileRepositoryError; diff --git a/src/infrastructure/repositories/folder_fs_repository.rs b/src/infrastructure/repositories/folder_fs_repository.rs index 869966a4..b8248f4c 100644 --- a/src/infrastructure/repositories/folder_fs_repository.rs +++ b/src/infrastructure/repositories/folder_fs_repository.rs @@ -10,7 +10,8 @@ use crate::domain::entities::folder::{Folder, FolderError}; use crate::domain::repositories::folder_repository::{ FolderRepository, FolderRepositoryError, FolderRepositoryResult }; -use crate::domain::services::path_service::{StoragePath, PathService}; +use crate::domain::services::path_service::StoragePath; +use crate::infrastructure::services::path_service::PathService; // use crate::application::ports::outbound::IdMappingPort; use crate::infrastructure::services::id_mapping_service::{IdMappingService, IdMappingError}; use crate::application::services::storage_mediator::StorageMediator; @@ -51,7 +52,6 @@ impl FolderFsRepository { /// Creates a stub repository for initialization purposes /// This is used temporarily during dependency injection setup - #[allow(dead_code)] pub fn new_stub() -> Self { let root_path = PathBuf::from("/tmp"); let path_service = Arc::new(PathService::new(root_path.clone())); diff --git a/src/infrastructure/repositories/parallel_file_processor.rs b/src/infrastructure/repositories/parallel_file_processor.rs index ba85c2e4..6fd125b1 100644 --- a/src/infrastructure/repositories/parallel_file_processor.rs +++ b/src/infrastructure/repositories/parallel_file_processor.rs @@ -422,22 +422,6 @@ impl ParallelFileProcessor { info!("Successfully wrote file of {}MB in parallel with optimized Bytes", file_size / (1024 * 1024)); Ok(()) } - - /// Writes a chunk to a file at a specific position - #[allow(dead_code)] - async fn write_chunk_optimized( - file: &mut File, - offset: u64, - data: Bytes - ) -> Result<(), std::io::Error> { - // Prepare writing at the correct position - file.seek(SeekFrom::Start(offset)).await?; - - // Write data without additional copies - file.write_all(&data).await?; - - Ok(()) - } } #[cfg(test)] diff --git a/src/infrastructure/repositories/pg/address_book_pg_repository.rs b/src/infrastructure/repositories/pg/address_book_pg_repository.rs index adfccee6..4bae9803 100644 --- a/src/infrastructure/repositories/pg/address_book_pg_repository.rs +++ b/src/infrastructure/repositories/pg/address_book_pg_repository.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use crate::domain::entities::contact::AddressBook; use crate::domain::repositories::address_book_repository::{AddressBookRepository, AddressBookRepositoryResult}; -use crate::common::errors::{DomainError, ErrorContext}; +use crate::common::errors::DomainError; pub struct AddressBookPgRepository { pool: Arc, @@ -15,11 +15,6 @@ impl AddressBookPgRepository { pub fn new(pool: Arc) -> Self { Self { pool } } - - // Método auxiliar para mapear errores SQL - fn map_error(err: sqlx::Error) -> Result { - Err(DomainError::database_error(err.to_string())) - } } #[async_trait] diff --git a/src/infrastructure/repositories/pg/calendar_event_pg_repository.rs b/src/infrastructure/repositories/pg/calendar_event_pg_repository.rs index 279cccb1..96b46a72 100644 --- a/src/infrastructure/repositories/pg/calendar_event_pg_repository.rs +++ b/src/infrastructure/repositories/pg/calendar_event_pg_repository.rs @@ -409,242 +409,4 @@ impl CalendarEventRepository for CalendarEventPgRepository { Ok(events) } -} - -// Additional methods not part of the trait -impl CalendarEventPgRepository { - // Helper method to get event by ID - async fn get_event_by_id(&self, id: &Uuid) -> CalendarEventRepositoryResult> { - let row_opt = sqlx::query( - r#" - SELECT - id, calendar_id, summary, description, location, - start_time, end_time, all_day, rrule, - created_at, updated_at, ical_uid, ical_data - FROM caldav.calendar_events - WHERE id = $1 - "# - ) - .bind(id) - .fetch_optional(&*self.pool) - .await - .map_err(|e| DomainError::database_error(format!("Failed to get calendar event by id: {}", e)))?; - - if let Some(row) = row_opt { - // En una implementación real, construiríamos un objeto CalendarEvent completo - // Este es un ejemplo simplificado - let event = CalendarEvent::with_id( - row.get("id"), - row.get("calendar_id"), - row.get("summary"), - row.get::, _>("description"), - row.get::, _>("location"), - row.get("start_time"), - row.get("end_time"), - row.get("all_day"), - row.get::, _>("rrule"), - row.get("ical_uid"), - row.get("ical_data"), - row.get("created_at"), - row.get("updated_at") - ).map_err(|e| DomainError::database_error(format!("Error creating calendar event: {}", e)))?; - - return Ok(Some(event)); - } - - Ok(None) - } - - // Helper method to get event by UID - async fn get_event_by_uid(&self, calendar_id: &Uuid, uid: &str) -> CalendarEventRepositoryResult> { - let row_opt = sqlx::query( - r#" - SELECT - id, calendar_id, summary, description, location, - start_time, end_time, all_day, rrule, - created_at, updated_at, ical_uid, ical_data - FROM caldav.calendar_events - WHERE calendar_id = $1 AND ical_uid = $2 - "# - ) - .bind(calendar_id) - .bind(uid) - .fetch_optional(&*self.pool) - .await - .map_err(|e| DomainError::database_error(format!("Failed to get calendar event by UID: {}", e)))?; - - if let Some(_row) = row_opt { - // En una implementación real, construiríamos un objeto CalendarEvent a partir de la fila - // Por simplicidad, devolvemos None como ejemplo - return Ok(None); - } - - Ok(None) - } - - // Helper method to get events by calendar - async fn get_events_by_calendar(&self, calendar_id: &Uuid) -> CalendarEventRepositoryResult> { - let _rows = sqlx::query( - r#" - SELECT - id, calendar_id, summary, description, location, - start_time, end_time, all_day, rrule, - created_at, updated_at, ical_uid, ical_data - FROM caldav.calendar_events - WHERE calendar_id = $1 - ORDER BY start_time - "# - ) - .bind(calendar_id) - .fetch_all(&*self.pool) - .await - .map_err(|e| DomainError::database_error(format!("Failed to get events by calendar: {}", e)))?; - - // En una implementación real, mapearíamos cada fila a un objeto CalendarEvent - // Este es un ejemplo simplificado que devuelve una lista vacía - let events = Vec::new(); - - // Ejemplo de cómo sería el mapeo real: - // for row in rows { - // let event = CalendarEvent::with_id( - // row.get("id"), - // row.get("calendar_id"), - // row.get("summary"), - // // ... otros campos - // ); - // events.push(event); - // } - - Ok(events) - } - - // Helper method to get changed events - async fn get_changed_events( - &self, - calendar_id: &Uuid, - since: &DateTime - ) -> CalendarEventRepositoryResult> { - let _rows = sqlx::query( - r#" - SELECT - id, calendar_id, summary, description, location, - start_time, end_time, all_day, rrule, - created_at, updated_at, ical_uid, ical_data - FROM caldav.calendar_events - WHERE calendar_id = $1 AND updated_at > $2 - ORDER BY updated_at - "# - ) - .bind(calendar_id) - .bind(since) - .fetch_all(&*self.pool) - .await - .map_err(|e| DomainError::database_error(format!("Failed to get changed events: {}", e)))?; - - // En una implementación real, mapearíamos cada fila a un objeto CalendarEvent - // Este es un ejemplo simplificado que devuelve una lista vacía - let events = Vec::new(); - - // Ejemplo de cómo sería el mapeo real: - // for row in rows { - // let event = CalendarEvent::with_id( - // row.get("id"), - // row.get("calendar_id"), - // row.get("summary"), - // row.get::, _>("description"), - // row.get::, _>("location"), - // row.get("start_time"), - // row.get("end_time"), - // row.get("all_day"), - // row.get::, _>("rrule"), - // row.get("ical_uid"), - // row.get("ical_data"), - // row.get("created_at"), - // row.get("updated_at") - // ).unwrap(); - // events.push(event); - // } - - Ok(events) - } - - // Helper method to add an attendee to an event - async fn add_event_attendee( - &self, - event_id: &Uuid, - email: &str, - name: Option<&str>, - role: &str, - status: &str - ) -> CalendarEventRepositoryResult<()> { - sqlx::query( - r#" - INSERT INTO caldav.calendar_event_attendees (event_id, email, name, role, status) - VALUES ($1, $2, $3, $4, $5) - ON CONFLICT (event_id, email) DO UPDATE - SET name = $3, role = $4, status = $5 - "# - ) - .bind(event_id) - .bind(email) - .bind(name) - .bind(role) - .bind(status) - .execute(&*self.pool) - .await - .map_err(|e| DomainError::database_error(format!("Failed to add event attendee: {}", e)))?; - - Ok(()) - } - - // Helper method to remove an attendee from an event - async fn remove_event_attendee( - &self, - event_id: &Uuid, - email: &str - ) -> CalendarEventRepositoryResult<()> { - sqlx::query( - r#" - DELETE FROM caldav.calendar_event_attendees - WHERE event_id = $1 AND email = $2 - "# - ) - .bind(event_id) - .bind(email) - .execute(&*self.pool) - .await - .map_err(|e| DomainError::database_error(format!("Failed to remove event attendee: {}", e)))?; - - Ok(()) - } - - // Helper method to get all attendees for an event - async fn get_event_attendees( - &self, - event_id: &Uuid - ) -> CalendarEventRepositoryResult, String, String)>> { - let rows = sqlx::query( - r#" - SELECT email, name, role, status - FROM caldav.calendar_event_attendees - WHERE event_id = $1 - ORDER BY email - "# - ) - .bind(event_id) - .fetch_all(&*self.pool) - .await - .map_err(|e| DomainError::database_error(format!("Failed to get event attendees: {}", e)))?; - - let mut attendees = Vec::new(); - for row in rows { - let email: String = row.get("email"); - let name: Option = row.get("name"); - let role: String = row.get("role"); - let status: String = row.get("status"); - attendees.push((email, name, role, status)); - } - - Ok(attendees) - } } \ No newline at end of file diff --git a/src/infrastructure/repositories/pg/calendar_pg_repository.rs b/src/infrastructure/repositories/pg/calendar_pg_repository.rs index 2f7944cf..4690196f 100644 --- a/src/infrastructure/repositories/pg/calendar_pg_repository.rs +++ b/src/infrastructure/repositories/pg/calendar_pg_repository.rs @@ -1,12 +1,11 @@ use async_trait::async_trait; use chrono::Utc; -use sqlx::{PgPool, query, query_as, Row, types::Uuid}; +use sqlx::{PgPool, Row, types::Uuid}; use std::sync::Arc; use crate::domain::entities::calendar::Calendar; use crate::domain::repositories::calendar_repository::{CalendarRepository, CalendarRepositoryResult}; -use crate::common::errors::{DomainError, ErrorContext}; -use sqlx::Transaction; +use crate::common::errors::DomainError; pub struct CalendarPgRepository { pool: Arc, diff --git a/src/infrastructure/repositories/pg/contact_group_pg_repository.rs b/src/infrastructure/repositories/pg/contact_group_pg_repository.rs deleted file mode 100644 index 9d0a0454..00000000 --- a/src/infrastructure/repositories/pg/contact_group_pg_repository.rs +++ /dev/null @@ -1,224 +0,0 @@ -use async_trait::async_trait; -use sqlx::{PgPool, types::Uuid}; -use std::sync::Arc; - -use crate::common::errors::DomainError; -use crate::domain::entities::contact::{ContactGroup, Contact}; -use crate::domain::repositories::contact_repository::{ContactGroupRepository, ContactRepositoryResult}; - -pub struct ContactGroupPgRepository { - pool: Arc, -} - -impl ContactGroupPgRepository { - pub fn new(pool: Arc) -> Self { - Self { pool } - } -} - -#[async_trait] -impl ContactGroupRepository for ContactGroupPgRepository { - async fn create_group(&self, group: ContactGroup) -> ContactRepositoryResult { - let _row = sqlx::query( - r#" - INSERT INTO carddav.contact_groups (id, address_book_id, name, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5) - RETURNING id, address_book_id, name, created_at, updated_at - "# - ) - .bind(group.id) - .bind(group.address_book_id) - .bind(&group.name) - .bind(group.created_at) - .bind(group.updated_at) - .fetch_one(&*self.pool) - .await - .map_err(|e| DomainError::database_error(format!("Failed to create contact group: {}", e)))?; - - // En una implementación real, construiríamos un objeto ContactGroup a partir de la fila - // Por simplicidad, devolvemos el grupo original - Ok(group) - } - - async fn update_group(&self, group: ContactGroup) -> ContactRepositoryResult { - let _row = sqlx::query( - r#" - UPDATE carddav.contact_groups - SET name = $3, updated_at = $4 - WHERE id = $1 AND address_book_id = $2 - RETURNING id, address_book_id, name, created_at, updated_at - "# - ) - .bind(group.id) - .bind(group.address_book_id) - .bind(&group.name) - .bind(group.updated_at) - .fetch_one(&*self.pool) - .await - .map_err(|e| match e { - sqlx::Error::RowNotFound => DomainError::not_found("Contact group", group.id.to_string()), - _ => DomainError::database_error(format!("Failed to update contact group: {}", e)), - })?; - - // En una implementación real, construiríamos un objeto ContactGroup a partir de la fila - // Por simplicidad, devolvemos el grupo original - Ok(group) - } - - async fn delete_group(&self, id: &Uuid) -> ContactRepositoryResult<()> { - // Begin transaction - let mut tx = self.pool.begin().await - .map_err(|e| DomainError::database_error(format!("Failed to begin transaction: {}", e)))?; - - // Delete group memberships - sqlx::query( - r#"DELETE FROM carddav.contact_group_members WHERE group_id = $1"# - ) - .bind(id) - .execute(&mut *tx) - .await - .map_err(|e| DomainError::database_error(format!("Failed to delete group memberships: {}", e)))?; - - // Delete the group - sqlx::query( - r#"DELETE FROM carddav.contact_groups WHERE id = $1"# - ) - .bind(id) - .execute(&mut *tx) - .await - .map_err(|e| DomainError::database_error(format!("Failed to delete contact group: {}", e)))?; - - // Commit transaction - tx.commit().await - .map_err(|e| DomainError::database_error(format!("Failed to commit transaction: {}", e)))?; - - Ok(()) - } - - async fn get_group_by_id(&self, id: &Uuid) -> ContactRepositoryResult> { - let row_opt = sqlx::query( - r#" - SELECT id, address_book_id, name, created_at, updated_at - FROM carddav.contact_groups - WHERE id = $1 - "# - ) - .bind(id) - .fetch_optional(&*self.pool) - .await - .map_err(|e| DomainError::database_error(format!("Failed to get contact group: {}", e)))?; - - if let Some(row) = row_opt { - // En una implementación real, construiríamos un objeto ContactGroup a partir de la fila - // Para esta demostración, devolvemos un grupo predeterminado con el ID correcto - let mut group = ContactGroup::default(); - group.id = id.clone(); - return Ok(Some(group)); - } - - Ok(None) - } - - async fn get_groups_by_address_book(&self, address_book_id: &Uuid) -> ContactRepositoryResult> { - let _rows = sqlx::query( - r#" - SELECT id, address_book_id, name, created_at, updated_at - FROM carddav.contact_groups - WHERE address_book_id = $1 - ORDER BY name - "# - ) - .bind(address_book_id) - .fetch_all(&*self.pool) - .await - .map_err(|e| DomainError::database_error(format!("Failed to get contact groups by address book: {}", e)))?; - - // En una implementación real, construiríamos objetos ContactGroup a partir de las filas - // Por simplicidad, devolvemos una lista vacía - let groups = Vec::new(); - - Ok(groups) - } - - async fn add_contact_to_group(&self, group_id: &Uuid, contact_id: &Uuid) -> ContactRepositoryResult<()> { - // Check if the membership already exists - let row_opt = sqlx::query( - r#" - SELECT 1 FROM carddav.contact_group_members - WHERE group_id = $1 AND contact_id = $2 - "# - ) - .bind(group_id) - .bind(contact_id) - .fetch_optional(&*self.pool) - .await - .map_err(|e| DomainError::database_error(format!("Failed to check group membership: {}", e)))?; - - let exists = row_opt.is_some(); - - if !exists { - sqlx::query( - r#" - INSERT INTO carddav.contact_group_members (group_id, contact_id) - VALUES ($1, $2) - "# - ) - .bind(group_id) - .bind(contact_id) - .execute(&*self.pool) - .await - .map_err(|e| DomainError::database_error(format!("Failed to add contact to group: {}", e)))?; - } - - Ok(()) - } - - async fn remove_contact_from_group(&self, group_id: &Uuid, contact_id: &Uuid) -> ContactRepositoryResult<()> { - sqlx::query( - r#" - DELETE FROM carddav.contact_group_members - WHERE group_id = $1 AND contact_id = $2 - "# - ) - .bind(group_id) - .bind(contact_id) - .execute(&*self.pool) - .await - .map_err(|e| DomainError::database_error(format!("Failed to remove contact from group: {}", e)))?; - - Ok(()) - } - - async fn get_contacts_in_group(&self, group_id: &Uuid) -> ContactRepositoryResult> { - // En lugar de implementar toda la lógica compleja que requiere query!, simplificamos - // Devolvemos una lista vacía por simplicidad para evitar el uso de macros SQLx - - // Para una implementación real, deberíamos convertir cada query! a sqlx::query - // y manejar la conversión de resultados manualmente - - Ok(Vec::new()) - } - - async fn get_groups_for_contact(&self, contact_id: &Uuid) -> ContactRepositoryResult> { - let _rows = sqlx::query( - r#" - SELECT - g.id, g.address_book_id, g.name, g.created_at, g.updated_at - FROM carddav.contact_groups g - JOIN carddav.contact_group_members m ON g.id = m.group_id - WHERE m.contact_id = $1 - ORDER BY g.name - "# - ) - .bind(contact_id) - .fetch_all(&*self.pool) - .await - .map_err(|e| DomainError::database_error(format!("Failed to get groups for contact: {}", e)))?; - - // En una implementación real, construiríamos objetos ContactGroup a partir de las filas - // Por simplicidad y demostración, devolvemos una lista vacía - let groups = Vec::new(); - - Ok(groups) - } -} \ No newline at end of file diff --git a/src/infrastructure/repositories/pg/contact_persistence_dto.rs b/src/infrastructure/repositories/pg/contact_persistence_dto.rs new file mode 100644 index 00000000..0ce545d3 --- /dev/null +++ b/src/infrastructure/repositories/pg/contact_persistence_dto.rs @@ -0,0 +1,129 @@ +//! Persistence DTOs for Contact entities +//! +//! These DTOs are used for JSONB serialization/deserialization in PostgreSQL. +//! They mirror the domain entities but include serde traits required for persistence. +//! This keeps the domain layer free of infrastructure concerns (serde dependency). + +use serde::{Deserialize, Serialize}; +use crate::domain::entities::contact::{Email, Phone, Address}; + +/// Persistence DTO for Email - used for JSONB serialization +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EmailPersistenceDto { + pub email: String, + pub r#type: String, + pub is_primary: bool, +} + +impl From<&Email> for EmailPersistenceDto { + fn from(email: &Email) -> Self { + Self { + email: email.email.clone(), + r#type: email.r#type.clone(), + is_primary: email.is_primary, + } + } +} + +impl From for Email { + fn from(dto: EmailPersistenceDto) -> Self { + Self { + email: dto.email, + r#type: dto.r#type, + is_primary: dto.is_primary, + } + } +} + +/// Persistence DTO for Phone - used for JSONB serialization +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PhonePersistenceDto { + pub number: String, + pub r#type: String, + pub is_primary: bool, +} + +impl From<&Phone> for PhonePersistenceDto { + fn from(phone: &Phone) -> Self { + Self { + number: phone.number.clone(), + r#type: phone.r#type.clone(), + is_primary: phone.is_primary, + } + } +} + +impl From for Phone { + fn from(dto: PhonePersistenceDto) -> Self { + Self { + number: dto.number, + r#type: dto.r#type, + is_primary: dto.is_primary, + } + } +} + +/// Persistence DTO for Address - used for JSONB serialization +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AddressPersistenceDto { + pub street: Option, + pub city: Option, + pub state: Option, + pub postal_code: Option, + pub country: Option, + pub r#type: String, + pub is_primary: bool, +} + +impl From<&Address> for AddressPersistenceDto { + fn from(addr: &Address) -> Self { + Self { + street: addr.street.clone(), + city: addr.city.clone(), + state: addr.state.clone(), + postal_code: addr.postal_code.clone(), + country: addr.country.clone(), + r#type: addr.r#type.clone(), + is_primary: addr.is_primary, + } + } +} + +impl From for Address { + fn from(dto: AddressPersistenceDto) -> Self { + Self { + street: dto.street, + city: dto.city, + state: dto.state, + postal_code: dto.postal_code, + country: dto.country, + r#type: dto.r#type, + is_primary: dto.is_primary, + } + } +} + +/// Helper functions to convert collections +pub fn emails_to_persistence(emails: &[Email]) -> Vec { + emails.iter().map(EmailPersistenceDto::from).collect() +} + +pub fn emails_from_persistence(dtos: Vec) -> Vec { + dtos.into_iter().map(Email::from).collect() +} + +pub fn phones_to_persistence(phones: &[Phone]) -> Vec { + phones.iter().map(PhonePersistenceDto::from).collect() +} + +pub fn phones_from_persistence(dtos: Vec) -> Vec { + dtos.into_iter().map(Phone::from).collect() +} + +pub fn addresses_to_persistence(addresses: &[Address]) -> Vec { + addresses.iter().map(AddressPersistenceDto::from).collect() +} + +pub fn addresses_from_persistence(dtos: Vec) -> Vec
{ + dtos.into_iter().map(Address::from).collect() +} diff --git a/src/infrastructure/repositories/pg/contact_pg_repository.rs b/src/infrastructure/repositories/pg/contact_pg_repository.rs index ba286e1a..8190dbed 100644 --- a/src/infrastructure/repositories/pg/contact_pg_repository.rs +++ b/src/infrastructure/repositories/pg/contact_pg_repository.rs @@ -1,12 +1,13 @@ use async_trait::async_trait; use chrono::Utc; -use sqlx::{PgPool, query, query_as, types::Uuid}; +use sqlx::{PgPool, types::Uuid}; use std::sync::Arc; use serde_json::Value as JsonValue; -use crate::domain::entities::contact::{Contact, ContactGroup}; -use crate::domain::repositories::contact_repository::{ContactRepository, ContactGroupRepository, ContactRepositoryResult}; -use crate::common::errors::{DomainError, ErrorContext}; +use crate::domain::entities::contact::Contact; +use crate::domain::repositories::contact_repository::{ContactRepository, ContactRepositoryResult}; +use crate::common::errors::DomainError; +use super::contact_persistence_dto::{emails_to_persistence, phones_to_persistence, addresses_to_persistence}; pub struct ContactPgRepository { pool: Arc, @@ -21,12 +22,16 @@ impl ContactPgRepository { #[async_trait] impl ContactRepository for ContactPgRepository { async fn create_contact(&self, contact: Contact) -> ContactRepositoryResult { - // Convert complex fields to JSON - let email_json = serde_json::to_value(&contact.email).unwrap_or(JsonValue::Null); - let phone_json = serde_json::to_value(&contact.phone).unwrap_or(JsonValue::Null); - let address_json = serde_json::to_value(&contact.address).unwrap_or(JsonValue::Null); + // Convert domain entities to persistence DTOs for JSONB serialization + let email_dtos = emails_to_persistence(&contact.email); + let phone_dtos = phones_to_persistence(&contact.phone); + let address_dtos = addresses_to_persistence(&contact.address); - let row = sqlx::query( + let email_json = serde_json::to_value(&email_dtos).unwrap_or(JsonValue::Null); + let phone_json = serde_json::to_value(&phone_dtos).unwrap_or(JsonValue::Null); + let address_json = serde_json::to_value(&address_dtos).unwrap_or(JsonValue::Null); + + let _row = sqlx::query( r#" INSERT INTO carddav.contacts ( id, address_book_id, uid, full_name, first_name, last_name, nickname, @@ -74,16 +79,20 @@ impl ContactRepository for ContactPgRepository { async fn update_contact(&self, contact: Contact) -> ContactRepositoryResult { let now = Utc::now(); - // Convert complex fields to JSON - let email_json = serde_json::to_value(&contact.email).unwrap_or(JsonValue::Null); - let phone_json = serde_json::to_value(&contact.phone).unwrap_or(JsonValue::Null); - let address_json = serde_json::to_value(&contact.address).unwrap_or(JsonValue::Null); + // Convert domain entities to persistence DTOs for JSONB serialization + let email_dtos = emails_to_persistence(&contact.email); + let phone_dtos = phones_to_persistence(&contact.phone); + let address_dtos = addresses_to_persistence(&contact.address); + + let email_json = serde_json::to_value(&email_dtos).unwrap_or(JsonValue::Null); + let phone_json = serde_json::to_value(&phone_dtos).unwrap_or(JsonValue::Null); + let address_json = serde_json::to_value(&address_dtos).unwrap_or(JsonValue::Null); // Create a clone of the contact with the updated timestamp let mut updated_contact = contact.clone(); updated_contact.updated_at = now; - let row = sqlx::query( + let _row = sqlx::query( r#" UPDATE carddav.contacts SET @@ -312,205 +321,4 @@ impl ContactRepository for ContactPgRepository { Ok(contacts) } -} - -pub struct ContactGroupPgRepository { - pool: Arc, -} - -impl ContactGroupPgRepository { - pub fn new(pool: Arc) -> Self { - Self { pool } - } -} - -#[async_trait] -impl ContactGroupRepository for ContactGroupPgRepository { - async fn create_group(&self, group: ContactGroup) -> ContactRepositoryResult { - let _row = sqlx::query( - r#" - INSERT INTO carddav.contact_groups (id, address_book_id, name, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5) - RETURNING id, address_book_id, name, created_at, updated_at - "# - ) - .bind(group.id) - .bind(group.address_book_id) - .bind(&group.name) - .bind(group.created_at) - .bind(group.updated_at) - .fetch_one(&*self.pool) - .await - .map_err(|e| DomainError::database_error(format!("Failed to create contact group: {}", e)))?; - - // En una implementación real, construiríamos un objeto ContactGroup a partir de la fila - // Por simplicidad, devolvemos el grupo original - Ok(group) - } - - async fn update_group(&self, group: ContactGroup) -> ContactRepositoryResult { - let now = Utc::now(); - - // Create a clone of the group with updated timestamp - let mut updated_group = group.clone(); - updated_group.updated_at = now; - - let _row = sqlx::query( - r#" - UPDATE carddav.contact_groups - SET name = $1, updated_at = $2 - WHERE id = $3 - RETURNING id, address_book_id, name, created_at, updated_at - "# - ) - .bind(&updated_group.name) - .bind(now) - .bind(updated_group.id) - .fetch_one(&*self.pool) - .await - .map_err(|e| DomainError::database_error(format!("Failed to update contact group: {}", e)))?; - - // En una implementación real, construiríamos un objeto ContactGroup a partir de la fila - // Por simplicidad, devolvemos el grupo con el timestamp actualizado - Ok(updated_group) - } - - async fn delete_group(&self, id: &Uuid) -> ContactRepositoryResult<()> { - sqlx::query( - r#" - DELETE FROM carddav.contact_groups - WHERE id = $1 - "# - ) - .bind(id) - .execute(&*self.pool) - .await - .map_err(|e| DomainError::database_error(format!("Failed to delete contact group: {}", e)))?; - - Ok(()) - } - - async fn get_group_by_id(&self, id: &Uuid) -> ContactRepositoryResult> { - let row_opt = sqlx::query( - r#" - SELECT id, address_book_id, name, created_at, updated_at - FROM carddav.contact_groups - WHERE id = $1 - "# - ) - .bind(id) - .fetch_optional(&*self.pool) - .await - .map_err(|e| DomainError::database_error(format!("Failed to get contact group by id: {}", e)))?; - - if let Some(_row) = row_opt { - // En una implementación real, construiríamos un objeto ContactGroup a partir de la fila - // Por simplicidad y demostración, devolvemos una instancia predeterminada - return Ok(Some(ContactGroup::default())); - } - - Ok(None) - } - - async fn get_groups_by_address_book(&self, address_book_id: &Uuid) -> ContactRepositoryResult> { - let _rows = sqlx::query( - r#" - SELECT id, address_book_id, name, created_at, updated_at - FROM carddav.contact_groups - WHERE address_book_id = $1 - ORDER BY name - "# - ) - .bind(address_book_id) - .fetch_all(&*self.pool) - .await - .map_err(|e| DomainError::database_error(format!("Failed to get contact groups by address book: {}", e)))?; - - // En una implementación real, construiríamos objetos ContactGroup a partir de las filas - // Por simplicidad y demostración, devolvemos una lista vacía - let groups = Vec::new(); - - Ok(groups) - } - - async fn add_contact_to_group(&self, group_id: &Uuid, contact_id: &Uuid) -> ContactRepositoryResult<()> { - sqlx::query( - r#" - INSERT INTO carddav.group_memberships (group_id, contact_id) - VALUES ($1, $2) - ON CONFLICT (group_id, contact_id) DO NOTHING - "# - ) - .bind(group_id) - .bind(contact_id) - .execute(&*self.pool) - .await - .map_err(|e| DomainError::database_error(format!("Failed to add contact to group: {}", e)))?; - - Ok(()) - } - - async fn remove_contact_from_group(&self, group_id: &Uuid, contact_id: &Uuid) -> ContactRepositoryResult<()> { - sqlx::query( - r#" - DELETE FROM carddav.group_memberships - WHERE group_id = $1 AND contact_id = $2 - "# - ) - .bind(group_id) - .bind(contact_id) - .execute(&*self.pool) - .await - .map_err(|e| DomainError::database_error(format!("Failed to remove contact from group: {}", e)))?; - - Ok(()) - } - - async fn get_contacts_in_group(&self, group_id: &Uuid) -> ContactRepositoryResult> { - let _rows = sqlx::query( - r#" - SELECT - c.id, c.address_book_id, c.uid, c.full_name, c.first_name, c.last_name, c.nickname, - c.email, c.phone, c.address, c.organization, c.title, c.notes, c.photo_url, - c.birthday, c.anniversary, c.vcard, c.etag, c.created_at, c.updated_at - FROM carddav.contacts c - INNER JOIN carddav.group_memberships m ON c.id = m.contact_id - WHERE m.group_id = $1 - ORDER BY c.full_name, c.first_name, c.last_name - "# - ) - .bind(group_id) - .fetch_all(&*self.pool) - .await - .map_err(|e| DomainError::database_error(format!("Failed to get contacts in group: {}", e)))?; - - // En una implementación real, construiríamos objetos Contact a partir de las filas - // Por simplicidad y demostración, devolvemos una lista vacía - let contacts = Vec::new(); - - Ok(contacts) - } - - async fn get_groups_for_contact(&self, contact_id: &Uuid) -> ContactRepositoryResult> { - let _rows = sqlx::query( - r#" - SELECT - g.id, g.address_book_id, g.name, g.created_at, g.updated_at - FROM carddav.contact_groups g - INNER JOIN carddav.group_memberships m ON g.id = m.group_id - WHERE m.contact_id = $1 - ORDER BY g.name - "# - ) - .bind(contact_id) - .fetch_all(&*self.pool) - .await - .map_err(|e| DomainError::database_error(format!("Failed to get groups for contact: {}", e)))?; - - // En una implementación real, construiríamos objetos ContactGroup a partir de las filas - // Por simplicidad y demostración, devolvemos una lista vacía - let groups = Vec::new(); - - Ok(groups) - } } \ No newline at end of file diff --git a/src/infrastructure/repositories/pg/mod.rs b/src/infrastructure/repositories/pg/mod.rs index e870932f..8b899e40 100644 --- a/src/infrastructure/repositories/pg/mod.rs +++ b/src/infrastructure/repositories/pg/mod.rs @@ -2,7 +2,7 @@ mod address_book_pg_repository; mod calendar_pg_repository; mod calendar_event_pg_repository; mod contact_pg_repository; -mod contact_group_pg_repository; +mod contact_persistence_dto; mod session_pg_repository; mod transaction_utils; mod user_pg_repository; @@ -11,6 +11,6 @@ pub use address_book_pg_repository::AddressBookPgRepository; pub use calendar_pg_repository::CalendarPgRepository; pub use calendar_event_pg_repository::CalendarEventPgRepository; pub use contact_pg_repository::ContactPgRepository; -pub use contact_group_pg_repository::ContactGroupPgRepository; +pub use contact_persistence_dto::*; pub use session_pg_repository::SessionPgRepository; pub use user_pg_repository::UserPgRepository; diff --git a/src/infrastructure/repositories/pg/transaction_utils.rs b/src/infrastructure/repositories/pg/transaction_utils.rs index 53df6a38..63e33bd4 100644 --- a/src/infrastructure/repositories/pg/transaction_utils.rs +++ b/src/infrastructure/repositories/pg/transaction_utils.rs @@ -1,4 +1,4 @@ -use sqlx::{PgPool, Transaction, Postgres, Error as SqlxError, Executor}; +use sqlx::{PgPool, Transaction, Postgres, Error as SqlxError}; use std::sync::Arc; use tracing::{debug, error, info}; @@ -50,81 +50,4 @@ where Err(e) } } -} - -/// Variant that accepts a transaction isolation level -pub async fn with_transaction_isolation( - pool: &Arc, - operation_name: &str, - isolation_level: TransactionIsolationLevel, - operation: F, -) -> Result -where - F: for<'c> FnOnce(&'c mut Transaction<'_, Postgres>) -> futures::future::BoxFuture<'c, Result>, - E: From + std::fmt::Display, -{ - debug!("Starting database transaction with isolation level {:?} for: {}", - isolation_level, operation_name); - - // Begin transaction with specific isolation level - let mut tx = pool.begin().await.map_err(|e| { - error!("Failed to begin transaction for {}: {}", operation_name, e); - E::from(e) - })?; - - // Set isolation level - tx.execute(&format!("SET TRANSACTION ISOLATION LEVEL {}", isolation_level.to_string())[..]) - .await - .map_err(|e| { - error!("Failed to set isolation level for {}: {}", operation_name, e); - E::from(e) - })?; - - // Execute the operation within the transaction - match operation(&mut tx).await { - Ok(result) => { - // If operation succeeds, commit the transaction - match tx.commit().await { - Ok(_) => { - debug!("Transaction committed successfully for: {}", operation_name); - Ok(result) - }, - Err(e) => { - error!("Failed to commit transaction for {}: {}", operation_name, e); - Err(E::from(e)) - } - } - }, - Err(e) => { - // If operation fails, rollback the transaction - if let Err(rollback_err) = tx.rollback().await { - error!("Failed to rollback transaction for {}: {}", operation_name, rollback_err); - // Still return the original error - } else { - info!("Transaction rolled back for {}: {}", operation_name, e); - } - Err(e) - } - } -} - -/// Transaction isolation levels from SQL standard -#[derive(Debug)] -pub enum TransactionIsolationLevel { - /// Read committed isolation level - ReadCommitted, - /// Repeatable read isolation level - RepeatableRead, - /// Serializable isolation level - Serializable, -} - -impl ToString for TransactionIsolationLevel { - fn to_string(&self) -> String { - match self { - TransactionIsolationLevel::ReadCommitted => "READ COMMITTED".to_string(), - TransactionIsolationLevel::RepeatableRead => "REPEATABLE READ".to_string(), - TransactionIsolationLevel::Serializable => "SERIALIZABLE".to_string(), - } - } } \ No newline at end of file diff --git a/src/infrastructure/repositories/trash_fs_repository.rs b/src/infrastructure/repositories/trash_fs_repository.rs index efd2b55f..3d41fbe6 100644 --- a/src/infrastructure/repositories/trash_fs_repository.rs +++ b/src/infrastructure/repositories/trash_fs_repository.rs @@ -29,13 +29,12 @@ struct TrashedItemEntry { pub struct TrashFsRepository { trash_dir: PathBuf, trash_index_path: PathBuf, - id_mapping_service: Arc, } impl TrashFsRepository { pub fn new( storage_root: impl AsRef, - id_mapping_service: Arc, + _id_mapping_service: Arc, ) -> Self { let trash_dir = storage_root.as_ref().join(".trash"); let trash_index_path = trash_dir.join("trash_index.json"); @@ -43,7 +42,6 @@ impl TrashFsRepository { Self { trash_dir, trash_index_path, - id_mapping_service, } } @@ -224,14 +222,6 @@ impl TrashFsRepository { deletion_date: item.deletion_date.to_rfc3339(), } } - - /// Obtiene la ruta de un elemento en la papelera - fn get_trash_path_for_item(&self, user_id: &Uuid, item_id: &Uuid) -> PathBuf { - self.trash_dir - .join("files") - .join(user_id.to_string()) - .join(item_id.to_string()) - } } #[async_trait] diff --git a/src/infrastructure/services/buffer_pool.rs b/src/infrastructure/services/buffer_pool.rs index 9e4f4e3d..7c84fb2c 100644 --- a/src/infrastructure/services/buffer_pool.rs +++ b/src/infrastructure/services/buffer_pool.rs @@ -9,11 +9,9 @@ use tracing::debug; pub const DEFAULT_BUFFER_SIZE: usize = 64 * 1024; // 64KB /// Número máximo por defecto de buffers en el pool -#[allow(dead_code)] pub const DEFAULT_MAX_BUFFERS: usize = 100; /// Tiempo de vida por defecto de un buffer inactivo (en segundos) -#[allow(dead_code)] pub const DEFAULT_BUFFER_TTL: u64 = 60; /// Buffer pooling para optimizar operaciones de lectura/escritura @@ -83,7 +81,6 @@ impl BufferPool { } /// Crea un pool con configuración por defecto - #[allow(dead_code)] pub fn default() -> Arc { Self::new( DEFAULT_BUFFER_SIZE, @@ -282,7 +279,6 @@ impl BorrowedBuffer { } /// Obtiene una referencia a los datos utilizados - #[allow(dead_code)] pub fn as_slice(&self) -> &[u8] { &self.buffer[..self.used_size] } @@ -302,7 +298,6 @@ impl BorrowedBuffer { } /// Copia datos a este buffer y actualiza el tamaño usado - #[allow(dead_code)] pub fn copy_from_slice(&mut self, data: &[u8]) -> usize { let copy_size = min(data.len(), self.buffer.len()); self.buffer[..copy_size].copy_from_slice(&data[..copy_size]); @@ -311,7 +306,6 @@ impl BorrowedBuffer { } /// Impide que el buffer se devuelva al pool al destruirse - #[allow(dead_code)] pub fn do_not_return(mut self) -> Self { self.return_to_pool = false; self @@ -323,7 +317,6 @@ impl BorrowedBuffer { } /// Obtiene el tamaño usado del buffer - #[allow(dead_code)] pub fn used_size(&self) -> usize { self.used_size } diff --git a/src/infrastructure/services/cache_manager.rs b/src/infrastructure/services/cache_manager.rs index a5f41f29..2d078831 100644 --- a/src/infrastructure/services/cache_manager.rs +++ b/src/infrastructure/services/cache_manager.rs @@ -8,7 +8,6 @@ use tokio::sync::RwLock; /// Representación de metadatos en caché #[derive(Debug, Clone)] -#[allow(dead_code)] pub struct CachedMetadata { /// Si el archivo o directorio existe pub exists: bool, @@ -23,7 +22,6 @@ pub struct CachedMetadata { } /// Estructura para gestionar la caché de metadatos de archivos y directorios -#[allow(dead_code)] pub struct StorageCacheManager { /// Caché de existencia y metadatos cache: RwLock>, @@ -37,7 +35,6 @@ pub struct StorageCacheManager { impl StorageCacheManager { /// Crea una nueva instancia del gestor de caché - #[allow(dead_code)] pub fn new(file_ttl_ms: u64, dir_ttl_ms: u64, max_entries: usize) -> Self { Self { cache: RwLock::new(HashMap::with_capacity(max_entries)), @@ -48,7 +45,6 @@ impl StorageCacheManager { } /// Crea una instancia por defecto del gestor de caché - #[allow(dead_code)] pub fn default() -> Self { Self::new( 60_000, // 1 minuto para archivos @@ -58,7 +54,6 @@ impl StorageCacheManager { } /// Verifica si un archivo o directorio existe en caché - #[allow(dead_code)] pub async fn check_exists(&self, path: &PathBuf, _is_dir: bool) -> Result { // Intentar obtener de la caché if let Some(metadata) = self.get_cached_metadata(path).await { @@ -70,7 +65,6 @@ impl StorageCacheManager { } /// Obtiene los metadatos de un path desde la caché - #[allow(dead_code)] async fn get_cached_metadata(&self, path: &PathBuf) -> Option { let cache = self.cache.read().await; @@ -85,7 +79,6 @@ impl StorageCacheManager { } /// Actualiza la caché con los metadatos de un path - #[allow(dead_code)] pub async fn update_cache(&self, path: &PathBuf, exists: bool, size: Option, created_at: Option, modified_at: Option, is_dir: bool) { let mut cache = self.cache.write().await; @@ -115,7 +108,6 @@ impl StorageCacheManager { } /// Elimina entradas aleatorias de la caché cuando está llena - #[allow(dead_code)] async fn evict_entries(&self, cache: &mut HashMap, count: usize) { // Obtener las entradas más antiguas para eliminar let mut entries: Vec<_> = cache.keys().cloned().collect(); @@ -136,7 +128,6 @@ impl StorageCacheManager { } /// Inicia una tarea de limpieza periódica - #[allow(dead_code)] pub fn start_cleanup_task(cache_manager: Arc) -> BoxFuture<'static, ()> { Box::pin(async move { let interval = Duration::from_secs(60); // Ejecutar cada minuto @@ -170,14 +161,12 @@ impl StorageCacheManager { } /// Invalida una entrada específica de la caché - #[allow(dead_code)] pub async fn invalidate(&self, path: &PathBuf) { let mut cache = self.cache.write().await; cache.remove(path); } /// Invalida todas las entradas de la caché relacionadas con una carpeta - #[allow(dead_code)] pub async fn invalidate_folder(&self, folder_path: &PathBuf) { let mut cache = self.cache.write().await; @@ -204,7 +193,6 @@ impl StorageCacheManager { } /// Obtiene el número actual de entradas en la caché - #[allow(dead_code)] pub async fn cache_size(&self) -> usize { let cache = self.cache.read().await; cache.len() diff --git a/src/infrastructure/services/compression_service.rs b/src/infrastructure/services/compression_service.rs index aa14d626..94851e3e 100644 --- a/src/infrastructure/services/compression_service.rs +++ b/src/infrastructure/services/compression_service.rs @@ -48,14 +48,12 @@ pub trait CompressionService: Send + Sync { async fn decompress_data(&self, compressed_data: &[u8]) -> io::Result>; /// Comprime un stream de datos - #[allow(dead_code)] fn compress_stream(&self, stream: S, level: CompressionLevel) -> impl Stream> + Send where S: Stream> + Send + 'static + Unpin; /// Descomprime un stream de datos - #[allow(dead_code)] fn decompress_stream(&self, compressed_stream: S) -> impl Stream> + Send where diff --git a/src/infrastructure/services/file_metadata_cache.rs b/src/infrastructure/services/file_metadata_cache.rs index d48ce17f..f7b36ae7 100644 --- a/src/infrastructure/services/file_metadata_cache.rs +++ b/src/infrastructure/services/file_metadata_cache.rs @@ -47,14 +47,12 @@ pub struct FileMetadata { /// Ruta absoluta del archivo pub path: PathBuf, /// Si el archivo existe físicamente - #[allow(dead_code)] pub exists: bool, /// Tipo de entrada (archivo, directorio) pub entry_type: CacheEntryType, /// Tamaño en bytes (para archivos) pub size: Option, /// Tipo MIME (para archivos) - #[allow(dead_code)] pub mime_type: Option, /// Timestamp de creación (UNIX epoch seconds) pub created_at: Option, @@ -259,7 +257,6 @@ impl FileMetadataCache { } /// Verifica si un archivo existe - #[allow(dead_code)] pub async fn exists(&self, path: &Path) -> Option { if let Some(metadata) = self.get_metadata(path).await { return Some(metadata.exists); @@ -269,7 +266,6 @@ impl FileMetadataCache { } /// Verifica si un path es un directorio - #[allow(dead_code)] pub async fn is_dir(&self, path: &Path) -> Option { if let Some(metadata) = self.get_metadata(path).await { return Some(metadata.entry_type == CacheEntryType::Directory); @@ -288,7 +284,6 @@ impl FileMetadataCache { } /// Obtiene el tamaño de un archivo - #[allow(dead_code)] pub async fn get_size(&self, path: &Path) -> Option { if let Some(metadata) = self.get_metadata(path).await { return metadata.size; @@ -298,7 +293,6 @@ impl FileMetadataCache { } /// Obtiene el tipo MIME de un archivo - #[allow(dead_code)] pub async fn get_mime_type(&self, path: &Path) -> Option { if let Some(metadata) = self.get_metadata(path).await { return metadata.mime_type; diff --git a/src/infrastructure/services/id_mapping_optimizer.rs b/src/infrastructure/services/id_mapping_optimizer.rs index fb342f80..5713111a 100644 --- a/src/infrastructure/services/id_mapping_optimizer.rs +++ b/src/infrastructure/services/id_mapping_optimizer.rs @@ -301,7 +301,6 @@ impl IdMappingOptimizer { } /// Precargar un conjunto de rutas para obtener sus IDs en batch - #[allow(dead_code)] pub async fn preload_paths(&self, paths: Vec) -> Result<(), IdMappingError> { // Solo proceder si hay rutas para cargar if paths.is_empty() { @@ -342,7 +341,6 @@ impl IdMappingOptimizer { } /// Precargar un conjunto de IDs para obtener sus rutas en batch - #[allow(dead_code)] pub async fn preload_ids(&self, ids: Vec) -> Result<(), IdMappingError> { // Solo proceder si hay IDs para cargar if ids.is_empty() { diff --git a/src/infrastructure/services/id_mapping_service.rs b/src/infrastructure/services/id_mapping_service.rs index d3395b7a..199efafd 100644 --- a/src/infrastructure/services/id_mapping_service.rs +++ b/src/infrastructure/services/id_mapping_service.rs @@ -1,6 +1,5 @@ use std::path::PathBuf; use std::collections::HashMap; -use std::time::Duration; use tokio::sync::{RwLock, Mutex}; use tokio::fs; use tokio::time; @@ -29,7 +28,6 @@ pub enum IdMappingError { SerializationError(#[from] serde_json::Error), #[error("Other error: {0}")] - #[allow(dead_code)] Other(String), } @@ -69,9 +67,6 @@ struct IdMap { version: u32, // Versión para detectar cambios } -/// Constantes para configuración -const SAVE_DEBOUNCE_MS: u64 = 0; // Sin debounce para garantizar guardado inmediato - /// Servicio para gestionar mapeos entre rutas y IDs únicos pub struct IdMappingService { map_path: PathBuf, @@ -490,7 +485,6 @@ impl IdMappingPort for IdMappingService { /// Synchronous helper for contexts where we can't use async impl IdMappingService { /// Create a new service synchronously (only for stubs and initialization) - #[allow(dead_code)] pub fn new_sync(map_path: PathBuf) -> Self { // Create a minimal implementation for initialization purposes Self { diff --git a/src/infrastructure/services/jwt_service.rs b/src/infrastructure/services/jwt_service.rs new file mode 100644 index 00000000..3f0a7aee --- /dev/null +++ b/src/infrastructure/services/jwt_service.rs @@ -0,0 +1,210 @@ +//! JWT-based token service implementation. +//! +//! This module provides JWT token generation and validation functionality, +//! implementing the TokenServicePort trait defined in the application layer. + +use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey, Algorithm}; +use serde::{Serialize, Deserialize}; +use uuid::Uuid; +use chrono::Utc; + +use crate::application::ports::auth_ports::{TokenServicePort, TokenClaims}; +use crate::domain::entities::user::User; +use crate::common::errors::{DomainError, ErrorKind}; + +/// Internal JWT claims structure for serialization. +/// This is the actual JWT payload structure used by jsonwebtoken crate. +#[derive(Debug, Serialize, Deserialize)] +struct JwtClaims { + /// Subject identifier - contains the user ID + pub sub: String, + /// Expiration timestamp (seconds since Unix epoch) + pub exp: i64, + /// Issued at timestamp (seconds since Unix epoch) + pub iat: i64, + /// JWT unique ID for token tracking and revocation + pub jti: String, + /// Username for display and identification purposes + pub username: String, + /// User email for communication and identification + pub email: String, + /// User role for authorization checks + pub role: String, +} + +impl From for TokenClaims { + fn from(claims: JwtClaims) -> Self { + TokenClaims { + sub: claims.sub, + exp: claims.exp, + iat: claims.iat, + jti: claims.jti, + username: claims.username, + email: claims.email, + role: claims.role, + } + } +} + +/// JWT-based implementation of the TokenServicePort. +/// +/// This service handles JWT token generation and validation for user authentication. +/// It uses HS256 algorithm for signing tokens. +pub struct JwtTokenService { + /// Secret key used for signing JWT tokens + jwt_secret: String, + /// Expiration time for access tokens in seconds + access_token_expiry: i64, + /// Expiration time for refresh tokens in seconds + refresh_token_expiry: i64, +} + +impl JwtTokenService { + /// Create a new JwtTokenService with the specified configuration. + /// + /// # Arguments + /// * `jwt_secret` - Secret key for signing tokens (should be at least 32 bytes) + /// * `access_token_expiry_secs` - Lifetime of access tokens in seconds + /// * `refresh_token_expiry_secs` - Lifetime of refresh tokens in seconds + pub fn new(jwt_secret: String, access_token_expiry_secs: i64, refresh_token_expiry_secs: i64) -> Self { + Self { + jwt_secret, + access_token_expiry: access_token_expiry_secs, + refresh_token_expiry: refresh_token_expiry_secs, + } + } +} + +impl TokenServicePort for JwtTokenService { + fn generate_access_token(&self, user: &User) -> Result { + let now = Utc::now().timestamp(); + + // Log information for debugging + tracing::debug!( + "Generating token for user: {}, id: {}, role: {}", + user.username(), + user.id(), + user.role() + ); + + let claims = JwtClaims { + sub: user.id().to_string(), + exp: now + self.access_token_expiry, + iat: now, + jti: Uuid::new_v4().to_string(), + username: user.username().to_string(), + email: user.email().to_string(), + role: format!("{}", user.role()), + }; + + // Log JWT claims for debugging + tracing::debug!("JWT claims: sub={}, exp={}, iat={}", claims.sub, claims.exp, claims.iat); + + encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(self.jwt_secret.as_bytes()) + ) + .map_err(|e| { + tracing::error!("Error generating token: {}", e); + DomainError::new( + ErrorKind::InternalError, + "TokenService", + format!("Error al generar token: {}", e) + ) + }) + } + + fn validate_token(&self, token: &str) -> Result { + let validation = Validation::new(Algorithm::HS256); + + let token_data = decode::( + token, + &DecodingKey::from_secret(self.jwt_secret.as_bytes()), + &validation + ) + .map_err(|e| { + match e.kind() { + jsonwebtoken::errors::ErrorKind::ExpiredSignature => { + DomainError::new(ErrorKind::AccessDenied, "TokenService", "Token expirado") + }, + _ => DomainError::new( + ErrorKind::AccessDenied, + "TokenService", + format!("Token inválido: {}", e) + ), + } + })?; + + Ok(token_data.claims.into()) + } + + fn generate_refresh_token(&self) -> String { + Uuid::new_v4().to_string() + } + + fn refresh_token_expiry_secs(&self) -> i64 { + self.refresh_token_expiry + } + + fn refresh_token_expiry_days(&self) -> i64 { + self.refresh_token_expiry / (24 * 3600) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::domain::entities::user::{User, UserRole}; + + fn create_test_user() -> User { + User::from_data( + "test-user-id".to_string(), + "testuser".to_string(), + "test@example.com".to_string(), + "hashed_password".to_string(), + UserRole::User, + 1024 * 1024 * 1024, // 1GB + 0, + chrono::Utc::now(), + chrono::Utc::now(), + None, + true, + ) + } + + #[test] + fn test_generate_and_validate_token() { + let service = JwtTokenService::new( + "test_secret_key_at_least_32_bytes_long".to_string(), + 3600, // 1 hour + 86400, // 1 day + ); + + let user = create_test_user(); + let token = service.generate_access_token(&user).expect("Should generate token"); + + let claims = service.validate_token(&token).expect("Should validate token"); + assert_eq!(claims.sub, user.id()); + assert_eq!(claims.username, user.username()); + assert_eq!(claims.email, user.email()); + } + + #[test] + fn test_refresh_token_is_unique() { + let service = JwtTokenService::new("secret".to_string(), 3600, 86400); + + let token1 = service.generate_refresh_token(); + let token2 = service.generate_refresh_token(); + + assert_ne!(token1, token2); + } + + #[test] + fn test_invalid_token() { + let service = JwtTokenService::new("secret".to_string(), 3600, 86400); + + let result = service.validate_token("invalid_token"); + assert!(result.is_err()); + } +} diff --git a/src/infrastructure/services/mod.rs b/src/infrastructure/services/mod.rs index 0a01075a..e670be38 100644 --- a/src/infrastructure/services/mod.rs +++ b/src/infrastructure/services/mod.rs @@ -7,4 +7,7 @@ pub mod file_metadata_cache; pub mod compression_service; pub mod buffer_pool; pub mod trash_cleanup_service; -pub mod zip_service; \ No newline at end of file +pub mod zip_service; +pub mod path_service; +pub mod password_hasher; +pub mod jwt_service; \ No newline at end of file diff --git a/src/infrastructure/services/password_hasher.rs b/src/infrastructure/services/password_hasher.rs new file mode 100644 index 00000000..593171c1 --- /dev/null +++ b/src/infrastructure/services/password_hasher.rs @@ -0,0 +1,91 @@ +//! Argon2-based password hasher implementation. +//! +//! This module provides a secure password hashing implementation using the Argon2id +//! algorithm, which is the recommended choice for password hashing as of 2023+. + +use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; +use argon2::password_hash::SaltString; +use rand_core::OsRng; + +use crate::application::ports::auth_ports::PasswordHasherPort; +use crate::common::errors::{DomainError, ErrorKind}; + +/// Argon2-based implementation of the PasswordHasherPort. +/// +/// Uses Argon2id algorithm which provides resistance against both side-channel +/// and GPU-based attacks. This is the recommended algorithm for password hashing. +#[derive(Debug, Clone)] +pub struct Argon2PasswordHasher { + /// Argon2 hasher instance - uses default secure parameters + _private: (), +} + +impl Argon2PasswordHasher { + /// Create a new Argon2PasswordHasher with default secure parameters. + pub fn new() -> Self { + Self { _private: () } + } +} + +impl Default for Argon2PasswordHasher { + fn default() -> Self { + Self::new() + } +} + +impl PasswordHasherPort for Argon2PasswordHasher { + fn hash_password(&self, password: &str) -> Result { + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + + argon2.hash_password(password.as_bytes(), &salt) + .map(|hash| hash.to_string()) + .map_err(|e| DomainError::new( + ErrorKind::InternalError, + "PasswordHasher", + format!("Error al generar hash de password: {}", e) + )) + } + + fn verify_password(&self, password: &str, hash: &str) -> Result { + let parsed_hash = PasswordHash::new(hash) + .map_err(|e| DomainError::new( + ErrorKind::InternalError, + "PasswordHasher", + format!("Error al procesar hash: {}", e) + ))?; + + Ok(Argon2::default().verify_password(password.as_bytes(), &parsed_hash).is_ok()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_hash_and_verify_password() { + let hasher = Argon2PasswordHasher::new(); + let password = "test_password_123"; + + let hash = hasher.hash_password(password).expect("Should hash password"); + assert!(hasher.verify_password(password, &hash).expect("Should verify")); + assert!(!hasher.verify_password("wrong_password", &hash).expect("Should verify")); + } + + #[test] + fn test_different_hashes_for_same_password() { + let hasher = Argon2PasswordHasher::new(); + let password = "same_password"; + + let hash1 = hasher.hash_password(password).expect("Should hash"); + let hash2 = hasher.hash_password(password).expect("Should hash"); + + // Hashes should be different due to random salt + assert_ne!(hash1, hash2); + + // But both should verify correctly + assert!(hasher.verify_password(password, &hash1).expect("Should verify")); + assert!(hasher.verify_password(password, &hash2).expect("Should verify")); + } +} diff --git a/src/infrastructure/services/path_service.rs b/src/infrastructure/services/path_service.rs new file mode 100644 index 00000000..b661b743 --- /dev/null +++ b/src/infrastructure/services/path_service.rs @@ -0,0 +1,301 @@ +//! PathService - Servicio de infraestructura para manejo de rutas de almacenamiento +//! +//! Este servicio fue movido desde domain/services porque implementa traits de application +//! (StoragePort, StorageMediator) y tiene dependencias de sistema de archivos (tokio::fs). +//! +//! StoragePath (Value Object) permanece en domain/services/path_service.rs + +use std::path::{Path, PathBuf}; +use async_trait::async_trait; +use tokio::fs; + +use crate::common::errors::{DomainError, ErrorKind}; +use crate::application::ports::outbound::StoragePort; +use crate::application::services::storage_mediator::{StorageMediator, StorageMediatorResult, StorageMediatorError}; +use crate::domain::entities::folder::Folder; +use crate::domain::services::path_service::StoragePath; + +/// Servicio de infraestructura para manejar operaciones con rutas de almacenamiento +pub struct PathService { + root_path: PathBuf, +} + +impl PathService { + /// Crea un nuevo servicio de rutas con una raíz específica + pub fn new(root_path: PathBuf) -> Self { + Self { root_path } + } + + /// Convierte una ruta del dominio a una ruta física absoluta + pub fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf { + let mut path = self.root_path.clone(); + for segment in storage_path.segments() { + path.push(segment); + } + path + } + + /// Convierte una ruta física a una ruta de dominio + pub fn to_storage_path(&self, physical_path: &Path) -> Option { + physical_path.strip_prefix(&self.root_path).ok().map(|rel_path| { + let segments: Vec = rel_path + .components() + .filter_map(|c| match c { + std::path::Component::Normal(os_str) => Some(os_str.to_string_lossy().to_string()), + _ => None, + }) + .collect(); + StoragePath::new(segments) + }) + } + + /// Crea una ruta de archivo dentro de una carpeta + pub fn create_file_path(&self, folder_path: &StoragePath, file_name: &str) -> StoragePath { + folder_path.join(file_name) + } + + /// Verifica si una ruta es directamente hija de otra + pub fn is_direct_child(&self, parent_path: &StoragePath, potential_child: &StoragePath) -> bool { + if let Some(child_parent) = potential_child.parent() { + &child_parent == parent_path + } else { + parent_path.is_empty() + } + } + + /// Verifica si una ruta está en la raíz + pub fn is_in_root(&self, path: &StoragePath) -> bool { + path.parent().map_or(true, |p| p.is_empty()) + } + + /// Gets the root path used by this service + pub fn get_root_path(&self) -> &Path { + &self.root_path + } + + /// Valida una ruta para asegurar que no contiene componentes peligrosos + pub fn validate_path(&self, path: &StoragePath) -> Result<(), DomainError> { + // Verificar que no haya segmentos vacíos + if path.segments().iter().any(|s| s.is_empty()) { + return Err(DomainError::new( + ErrorKind::InvalidInput, + "Path", + format!("Path contains empty segments: {}", path.to_string()) + )); + } + + // Verificar que no haya caracteres peligrosos + let dangerous_chars = ['\\', ':', '*', '?', '"', '<', '>', '|']; + for segment in path.segments() { + if segment.contains(&dangerous_chars[..]) { + return Err(DomainError::new( + ErrorKind::InvalidInput, + "Path", + format!("Path contains dangerous characters: {}", segment) + )); + } + + // Verificar que no empiece con . (oculto en Unix) + if segment.starts_with('.') && segment != ".well-known" { + return Err(DomainError::new( + ErrorKind::InvalidInput, + "Path", + format!("Path segments cannot start with dot: {}", segment) + )); + } + } + + Ok(()) + } +} + +#[async_trait] +impl StoragePort for PathService { + fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf { + let mut path = self.root_path.clone(); + for segment in storage_path.segments() { + path.push(segment); + } + path + } + + async fn ensure_directory(&self, storage_path: &StoragePath) -> Result<(), DomainError> { + // Primero validar la ruta + self.validate_path(storage_path)?; + + // Resolver a ruta física + let physical_path = self.resolve_path(storage_path); + + // Crear directorios si no existen + if !physical_path.exists() { + fs::create_dir_all(&physical_path).await + .map_err(|e| DomainError::new( + ErrorKind::AccessDenied, + "Storage", + format!("Failed to create directory: {}", physical_path.display()) + ).with_source(e))?; + + tracing::debug!("Created directory: {}", physical_path.display()); + } else if !physical_path.is_dir() { + return Err(DomainError::new( + ErrorKind::InvalidInput, + "Storage", + format!("Path exists but is not a directory: {}", physical_path.display()) + )); + } + + Ok(()) + } + + async fn file_exists(&self, storage_path: &StoragePath) -> Result { + let physical_path = self.resolve_path(storage_path); + + let exists = physical_path.exists() && physical_path.is_file(); + Ok(exists) + } + + async fn directory_exists(&self, storage_path: &StoragePath) -> Result { + let physical_path = self.resolve_path(storage_path); + + let exists = physical_path.exists() && physical_path.is_dir(); + Ok(exists) + } +} + +#[async_trait] +impl StorageMediator for PathService { + async fn get_folder_path(&self, folder_id: &str) -> StorageMediatorResult { + // This is a simplified implementation since PathService doesn't have direct + // access to folder repository. It's typically used through a proxy. + Err(StorageMediatorError::NotFound(format!("Folder with ID {} not found", folder_id))) + } + + async fn get_folder_storage_path(&self, folder_id: &str) -> StorageMediatorResult { + // Simplified implementation - should be overridden by actual implementations + Err(StorageMediatorError::NotFound(format!("Folder with ID {} not found", folder_id))) + } + + async fn get_folder(&self, folder_id: &str) -> StorageMediatorResult { + // Simplified implementation - should be overridden by actual implementations + Err(StorageMediatorError::NotFound(format!("Folder with ID {} not found", folder_id))) + } + + async fn file_exists_at_path(&self, path: &Path) -> StorageMediatorResult { + let abs_path = self.resolve_path(&StoragePath::from_string(&path.to_string_lossy())); + Ok(abs_path.exists() && abs_path.is_file()) + } + + async fn file_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult { + let abs_path = self.resolve_path(storage_path); + Ok(abs_path.exists() && abs_path.is_file()) + } + + async fn folder_exists_at_path(&self, path: &Path) -> StorageMediatorResult { + let abs_path = self.resolve_path(&StoragePath::from_string(&path.to_string_lossy())); + Ok(abs_path.exists() && abs_path.is_dir()) + } + + async fn folder_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult { + let abs_path = self.resolve_path(storage_path); + Ok(abs_path.exists() && abs_path.is_dir()) + } + + fn resolve_path(&self, relative_path: &Path) -> PathBuf { + // Convert path to storage path then resolve + let path_str = relative_path.to_string_lossy().to_string(); + let storage_path = StoragePath::from_string(&path_str); + PathService::resolve_path(self, &storage_path) + } + + fn resolve_storage_path(&self, storage_path: &StoragePath) -> PathBuf { + PathService::resolve_path(self, storage_path) + } + + async fn ensure_directory(&self, path: &Path) -> StorageMediatorResult<()> { + let abs_path = PathService::resolve_path(self, &StoragePath::from_string(&path.to_string_lossy())); + + if !abs_path.exists() { + fs::create_dir_all(&abs_path).await + .map_err(|e| StorageMediatorError::AccessError(format!("Failed to create directory: {}", e)))?; + } else if !abs_path.is_dir() { + return Err(StorageMediatorError::InvalidPath( + format!("Path exists but is not a directory: {}", abs_path.display()) + )); + } + + Ok(()) + } + + async fn ensure_storage_directory(&self, storage_path: &StoragePath) -> StorageMediatorResult<()> { + let abs_path = PathService::resolve_path(self, storage_path); + + if !abs_path.exists() { + fs::create_dir_all(&abs_path).await + .map_err(|e| StorageMediatorError::AccessError(format!("Failed to create directory: {}", e)))?; + } else if !abs_path.is_dir() { + return Err(StorageMediatorError::InvalidPath( + format!("Path exists but is not a directory: {}", abs_path.display()) + )); + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_resolve_path() { + let service = PathService::new(PathBuf::from("/storage")); + + let storage_path = StoragePath::from_string("test/file.txt"); + let absolute = service.resolve_path(&storage_path); + + assert_eq!(absolute, PathBuf::from("/storage/test/file.txt")); + } + + #[test] + fn test_to_storage_path() { + let service = PathService::new(PathBuf::from("/storage")); + + let physical_path = PathBuf::from("/storage/folder/file.txt"); + let storage_path = service.to_storage_path(&physical_path).unwrap(); + + assert_eq!(storage_path.to_string(), "/folder/file.txt"); + } + + #[test] + fn test_is_in_root() { + let service = PathService::new(PathBuf::from("/storage")); + + let root_path = StoragePath::from_string("file.txt"); + let nested_path = StoragePath::from_string("folder/file.txt"); + + assert!(service.is_in_root(&root_path)); + assert!(!service.is_in_root(&nested_path)); + } + + #[test] + fn test_is_direct_child() { + let service = PathService::new(PathBuf::from("/storage")); + + let parent = StoragePath::from_string("folder"); + let child = StoragePath::from_string("folder/file.txt"); + let not_child = StoragePath::from_string("folder2/file.txt"); + + assert!(service.is_direct_child(&parent, &child)); + assert!(!service.is_direct_child(&parent, ¬_child)); + } + + #[test] + fn test_create_file_path() { + let service = PathService::new(PathBuf::from("/storage")); + + let folder_path = StoragePath::from_string("folder"); + let file_path = service.create_file_path(&folder_path, "file.txt"); + + assert_eq!(file_path.to_string(), "/folder/file.txt"); + } +} diff --git a/src/interfaces/api/handlers/auth_handler.rs b/src/interfaces/api/handlers/auth_handler.rs index 6de4301a..a39e6ecd 100644 --- a/src/interfaces/api/handlers/auth_handler.rs +++ b/src/interfaces/api/handlers/auth_handler.rs @@ -12,7 +12,7 @@ use crate::application::dtos::user_dto::{ LoginDto, RegisterDto, UserDto, ChangePasswordDto, RefreshTokenDto, AuthResponseDto }; use crate::interfaces::middleware::auth::CurrentUser; -use crate::common::errors::AppError; +use crate::interfaces::errors::AppError; pub fn auth_routes() -> Router> { Router::new() diff --git a/src/interfaces/api/handlers/batch_handler.rs b/src/interfaces/api/handlers/batch_handler.rs index 969cc1ea..134b8b9b 100644 --- a/src/interfaces/api/handlers/batch_handler.rs +++ b/src/interfaces/api/handlers/batch_handler.rs @@ -39,7 +39,6 @@ pub struct BatchFolderOperationRequest { pub recursive: bool, /// ID de la carpeta destino (opcional) #[serde(skip_serializing_if = "Option::is_none")] - #[allow(dead_code)] pub target_folder_id: Option, } diff --git a/src/interfaces/api/handlers/caldav_handler.rs b/src/interfaces/api/handlers/caldav_handler.rs index fdddc53e..9fa2edeb 100644 --- a/src/interfaces/api/handlers/caldav_handler.rs +++ b/src/interfaces/api/handlers/caldav_handler.rs @@ -5,7 +5,6 @@ use axum::{ response::IntoResponse, Json, }; -use std::sync::Arc; use serde_json::json; use crate::common::di::AppState; diff --git a/src/interfaces/api/handlers/caldav_handler.rs.orig b/src/interfaces/api/handlers/caldav_handler.rs.orig deleted file mode 100644 index 0bb51152..00000000 --- a/src/interfaces/api/handlers/caldav_handler.rs.orig +++ /dev/null @@ -1,362 +0,0 @@ -use axum::{ - Router, - routing::{get, put, delete, any}, - extract::{Path, State, Request}, - http::{StatusCode, HeaderMap}, - response::{IntoResponse, Response}, - body::Body, - Json, -}; -use tracing::error; -use std::sync::Arc; -use serde_json::json; - -use crate::common::di::AppState; -use crate::application::dtos::calendar_dto::{ - CalendarDto, CreateCalendarDto, UpdateCalendarDto, - CalendarEventDto, CreateEventDto as CreateCalendarEventDto, - UpdateEventDto as UpdateCalendarEventDto -}; - -// CalDAV handler implementation -pub fn caldav_routes() -> Router { - Router::new() - // Calendar operations - .route("/calendars", get(list_calendars)) - .route("/calendars/:calendar_id", - get(get_calendar) - .put(update_calendar) - .delete(delete_calendar) - ) - .route("/calendars/:calendar_id/events", - get(list_events) - .post(create_event) - ) - .route("/calendars/:calendar_id/events/:event_id", - get(get_event) - .put(update_event) - .delete(delete_event) - ) -} - -async fn list_calendars( - State(state): State, -) -> impl IntoResponse { - let user_id = "default_user"; // In production, get this from auth middleware - - match &state.calendar_service { - Some(calendar_service) => { - let params = json!({ - "user_id": user_id - }); - - match calendar_service.handle_request("list_user_calendars", params).await { - Ok(result) => { - let calendars: Vec = serde_json::from_value(result) - .unwrap_or_else(|_| Vec::new()); - (StatusCode::OK, Json(calendars)) - }, - Err(e) => { - (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ - "error": format!("Failed to list calendars: {}", e) - }))) - } - } - }, - None => { - (StatusCode::NOT_IMPLEMENTED, Json(json!({ - "error": "Calendar service not available" - }))) - } - } -} - -async fn get_calendar( - State(state): State, - Path(calendar_id): Path, -) -> impl IntoResponse { - let user_id = "default_user"; // In production, get this from auth middleware - - match &state.calendar_service { - Some(calendar_service) => { - let params = json!({ - "calendar_id": calendar_id, - "user_id": user_id - }); - - match calendar_service.handle_request("get_calendar", params).await { - Ok(result) => { - let calendar: CalendarDto = serde_json::from_value(result) - .unwrap_or_else(|_| CalendarDto::default()); - (StatusCode::OK, Json(calendar)) - }, - Err(e) => { - (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ - "error": format!("Failed to get calendar: {}", e) - }))) - } - } - }, - None => { - (StatusCode::NOT_IMPLEMENTED, Json(json!({ - "error": "Calendar service not available" - }))) - } - } -} - -async fn update_calendar( - State(state): State, - Path(calendar_id): Path, - Json(update): Json, -) -> impl IntoResponse { - let user_id = "default_user"; // In production, get this from auth middleware - - // Set the user ID in the update - let mut update_with_user = update; - update_with_user.user_id = user_id.to_string(); - - match &state.calendar_service { - Some(calendar_service) => { - match calendar_service.handle_request("update_calendar", json!({ - "calendar_id": calendar_id, - "name": update_with_user.name, - "description": update_with_user.description, - "color": update_with_user.color, - "is_public": update_with_user.is_public, - "user_id": update_with_user.user_id - })).await { - Ok(result) => { - let calendar: CalendarDto = serde_json::from_value(result) - .unwrap_or_else(|_| CalendarDto::default()); - (StatusCode::OK, Json(calendar)) - }, - Err(e) => { - (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ - "error": format!("Failed to update calendar: {}", e) - }))) - } - } - }, - None => { - (StatusCode::NOT_IMPLEMENTED, Json(json!({ - "error": "Calendar service not available" - }))) - } - } -} - -async fn delete_calendar( - State(state): State, - Path(calendar_id): Path, -) -> impl IntoResponse { - let user_id = "default_user"; // In production, get this from auth middleware - - match &state.calendar_service { - Some(calendar_service) => { - let params = json!({ - "calendar_id": calendar_id, - "user_id": user_id - }); - - match calendar_service.handle_request("delete_calendar", params).await { - Ok(_) => { - (StatusCode::NO_CONTENT, Json(json!({}))) - }, - Err(e) => { - (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ - "error": format!("Failed to delete calendar: {}", e) - }))) - } - } - }, - None => { - (StatusCode::NOT_IMPLEMENTED, Json(json!({ - "error": "Calendar service not available" - }))) - } - } -} - -async fn list_events( - State(state): State, - Path(calendar_id): Path, -) -> impl IntoResponse { - let user_id = "default_user"; // In production, get this from auth middleware - - match &state.calendar_service { - Some(calendar_service) => { - let params = json!({ - "calendar_id": calendar_id, - "user_id": user_id - }); - - match calendar_service.handle_request("list_events", params).await { - Ok(result) => { - let events: Vec = serde_json::from_value(result) - .unwrap_or_else(|_| Vec::new()); - (StatusCode::OK, Json(events)) - }, - Err(e) => { - (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ - "error": format!("Failed to list events: {}", e) - }))) - } - } - }, - None => { - (StatusCode::NOT_IMPLEMENTED, Json(json!({ - "error": "Calendar service not available" - }))) - } - } -} - -async fn create_event( - State(state): State, - Path(calendar_id): Path, - Json(mut event): Json, -) -> impl IntoResponse { - let user_id = "default_user"; // In production, get this from auth middleware - - // Set the calendar ID and user ID in the event - event.calendar_id = calendar_id; - event.user_id = user_id.to_string(); - - match &state.calendar_service { - Some(calendar_service) => { - match calendar_service.handle_request("create_event", serde_json::to_value(event).unwrap()).await { - Ok(result) => { - let event: CalendarEventDto = serde_json::from_value(result) - .unwrap_or_else(|_| CalendarEventDto::default()); - (StatusCode::CREATED, Json(event)) - }, - Err(e) => { - let error_dto = CalendarEventDto::default(); - error!( - "Failed to create event: {}", - e - ); - (StatusCode::INTERNAL_SERVER_ERROR, Json(error_dto)) - } - } - }, - None => { - (StatusCode::NOT_IMPLEMENTED, Json(json!({ - "error": "Calendar service not available" - }))) - } - } -} - -async fn get_event( - State(state): State, - Path((calendar_id, event_id)): Path<(String, String)>, -) -> impl IntoResponse { - let user_id = "default_user"; // In production, get this from auth middleware - - match &state.calendar_service { - Some(calendar_service) => { - let params = json!({ - "event_id": event_id, - "user_id": user_id - }); - - match calendar_service.handle_request("get_event", params).await { - Ok(result) => { - let event: CalendarEventDto = serde_json::from_value(result) - .unwrap_or_else(|_| CalendarEventDto::default()); - (StatusCode::OK, Json(event)) - }, - Err(e) => { - let error_dto = CalendarEventDto::default(); - error!( - "Failed to get event: {}", - e - ); - (StatusCode::INTERNAL_SERVER_ERROR, Json(error_dto)) - } - } - }, - None => { - (StatusCode::NOT_IMPLEMENTED, Json(json!({ - "error": "Calendar service not available" - }))) - } - } -} - -async fn update_event( - State(state): State, - Path((calendar_id, event_id)): Path<(String, String)>, - Json(mut update): Json, -) -> impl IntoResponse { - let user_id = "default_user"; // In production, get this from auth middleware - - // Set the user ID in the update - update.user_id = user_id.to_string(); - - match &state.calendar_service { - Some(calendar_service) => { - let mut params = serde_json::to_value(update).unwrap(); - - // Add event_id to the params - if let serde_json::Value::Object(ref mut map) = params { - map.insert("event_id".to_string(), serde_json::Value::String(event_id)); - } - - match calendar_service.handle_request("update_event", params).await { - Ok(result) => { - let event: CalendarEventDto = serde_json::from_value(result) - .unwrap_or_else(|_| CalendarEventDto::default()); - (StatusCode::OK, Json(event)) - }, - Err(e) => { - let error_dto = CalendarEventDto::default(); - error!( - "Failed to update event: {}", - e - ); - (StatusCode::INTERNAL_SERVER_ERROR, Json(error_dto)) - } - } - }, - None => { - (StatusCode::NOT_IMPLEMENTED, Json(json!({ - "error": "Calendar service not available" - }))) - } - } -} - -async fn delete_event( - State(state): State, - Path((calendar_id, event_id)): Path<(String, String)>, -) -> impl IntoResponse { - let user_id = "default_user"; // In production, get this from auth middleware - - match &state.calendar_service { - Some(calendar_service) => { - let params = json!({ - "event_id": event_id, - "user_id": user_id - }); - - match calendar_service.handle_request("delete_event", params).await { - Ok(_) => { - (StatusCode::NO_CONTENT, Json(json!({}))) - }, - Err(e) => { - (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ - "error": format!("Failed to delete event: {}", e) - }))) - } - } - }, - None => { - (StatusCode::NOT_IMPLEMENTED, Json(json!({ - "error": "Calendar service not available" - }))) - } - } -} \ No newline at end of file diff --git a/src/interfaces/api/handlers/caldav_handler.rs.rej b/src/interfaces/api/handlers/caldav_handler.rs.rej deleted file mode 100644 index 93142400..00000000 --- a/src/interfaces/api/handlers/caldav_handler.rs.rej +++ /dev/null @@ -1,41 +0,0 @@ ---- caldav_handler.rs -+++ caldav_handler.rs -@@ -242,9 +242,9 @@ - } - }, - None => { -- (StatusCode::NOT_IMPLEMENTED, Json(json\!({ -- "error": "Calendar service not available" -- }))) -+ let error_dto = CalendarEventDto::default(); -+ error\!("Calendar service not available"); -+ (StatusCode::NOT_IMPLEMENTED, Json(error_dto)) - } - } - } -@@ -277,9 +277,9 @@ - } - }, - None => { -- (StatusCode::NOT_IMPLEMENTED, Json(json\!({ -- "error": "Calendar service not available" -- }))) -+ let error_dto = CalendarEventDto::default(); -+ error\!("Calendar service not available"); -+ (StatusCode::NOT_IMPLEMENTED, Json(error_dto)) - } - } - } -@@ -320,9 +320,9 @@ - } - }, - None => { -- (StatusCode::NOT_IMPLEMENTED, Json(json\!({ -- "error": "Calendar service not available" -- }))) -+ let error_dto = CalendarEventDto::default(); -+ error\!("Calendar service not available"); -+ (StatusCode::NOT_IMPLEMENTED, Json(error_dto)) - } - } - } diff --git a/src/interfaces/api/handlers/file_handler.rs b/src/interfaces/api/handlers/file_handler.rs index 119ad3ee..04ce3c13 100644 --- a/src/interfaces/api/handlers/file_handler.rs +++ b/src/interfaces/api/handlers/file_handler.rs @@ -7,10 +7,6 @@ use axum::{ }; use serde::Deserialize; use std::collections::HashMap; -use futures::Stream; -// use futures::StreamExt; -use std::task::{Context, Poll}; -use std::pin::Pin; use crate::application::services::file_service::{FileService, FileServiceError}; use crate::infrastructure::services::compression_service::{ @@ -46,35 +42,6 @@ type GlobalState = AppState; */ pub struct FileHandler; -// Simpler approach to make streams Unpin - use Pin> directly -struct BoxedStream { - inner: Pin + Send + 'static>>, -} - -impl Stream for BoxedStream { - type Item = T; - - fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - // Accessing the field directly is safe because BoxedStream is not a structural pinning type - unsafe { self.get_unchecked_mut().inner.as_mut().poll_next(cx) } - } -} - -// This is safe because BoxedStream's inner field is already Pin> -impl Unpin for BoxedStream {} - -impl BoxedStream { - #[allow(dead_code)] - fn new(stream: S) -> Self - where - S: Stream + Send + 'static, - { - BoxedStream { - inner: Box::pin(stream), - } - } -} - impl FileHandler { /// Uploads a file pub async fn upload_file( diff --git a/src/interfaces/api/handlers/webdav_handler.rs b/src/interfaces/api/handlers/webdav_handler.rs index 98bf8bfc..d92a1c08 100644 --- a/src/interfaces/api/handlers/webdav_handler.rs +++ b/src/interfaces/api/handlers/webdav_handler.rs @@ -22,7 +22,7 @@ use crate::common::di::AppState; use crate::application::adapters::webdav_adapter::{WebDavAdapter, PropFindRequest, LockInfo, LockScope, LockType}; use crate::interfaces::middleware::auth::CurrentUser; use crate::application::dtos::folder_dto::FolderDto; -use crate::common::errors::AppError; +use crate::interfaces::errors::AppError; // Create a custom DAV header since it's not in the standard headers const HEADER_DAV: HeaderName = HeaderName::from_static("dav"); diff --git a/src/interfaces/api/routes.rs b/src/interfaces/api/routes.rs index c02c5192..fba8dbfe 100644 --- a/src/interfaces/api/routes.rs +++ b/src/interfaces/api/routes.rs @@ -50,7 +50,7 @@ pub fn create_api_routes( ) -> Router { // Create a simplified AppState for the trash view // Setup required components for repository construction - let path_service = Arc::new(crate::domain::services::path_service::PathService::new(std::path::PathBuf::from("./storage"))); + let path_service = Arc::new(crate::infrastructure::services::path_service::PathService::new(std::path::PathBuf::from("./storage"))); let storage_mediator = Arc::new(crate::application::services::storage_mediator::FileSystemStorageMediator::new_stub()); let id_mapping_service = Arc::new(crate::infrastructure::services::id_mapping_service::IdMappingService::dummy()); let path_resolver = Arc::new(crate::infrastructure::repositories::file_path_resolver::FilePathResolver::new( @@ -79,11 +79,17 @@ pub fn create_api_routes( path_service.clone(), )); + // Create concrete id_mapping_service for optimizer + let id_mapping_service_concrete = Arc::new(crate::infrastructure::services::id_mapping_service::IdMappingService::dummy()); + let id_mapping_optimizer = Arc::new(crate::infrastructure::services::id_mapping_optimizer::IdMappingOptimizer::new(id_mapping_service_concrete.clone())); + let mut app_state = crate::common::di::AppState { core: crate::common::di::CoreServices { path_service: path_service.clone(), cache_manager: Arc::new(crate::infrastructure::services::cache_manager::StorageCacheManager::default()), id_mapping_service: id_mapping_service.clone(), + file_id_mapping_service: id_mapping_service_concrete.clone(), + id_mapping_optimizer: id_mapping_optimizer.clone(), config: crate::common::config::AppConfig::default(), }, repositories: crate::common::di::RepositoryServices { @@ -95,16 +101,28 @@ pub fn create_api_routes( storage_mediator: storage_mediator.clone(), metadata_manager: Arc::new(crate::infrastructure::repositories::FileMetadataManager::default()), path_resolver: path_resolver.clone(), + metadata_cache: metadata_cache.clone(), trash_repository: None, // This is OK to be None since we use the trash_service directly }, storage_usage_service: None, applications: crate::common::di::ApplicationServices { + folder_service_concrete: folder_service.clone(), + file_service_concrete: file_service.clone(), folder_service: folder_service.clone(), file_service: file_service.clone(), - file_upload_service: Arc::new(crate::application::services::file_upload_service::FileUploadService::default_stub()), - file_retrieval_service: Arc::new(crate::application::services::file_retrieval_service::FileRetrievalService::default_stub()), - file_management_service: Arc::new(crate::application::services::file_management_service::FileManagementService::default_stub()), - file_use_case_factory: Arc::new(crate::application::services::file_use_case_factory::AppFileUseCaseFactory::default_stub()), + file_upload_service: Arc::new(crate::application::services::file_upload_service::FileUploadService::new( + Arc::new(crate::infrastructure::repositories::FileFsWriteRepository::default_stub()) + )), + file_retrieval_service: Arc::new(crate::application::services::file_retrieval_service::FileRetrievalService::new( + Arc::new(crate::infrastructure::repositories::FileFsReadRepository::default_stub()) + )), + file_management_service: Arc::new(crate::application::services::file_management_service::FileManagementService::new( + Arc::new(crate::infrastructure::repositories::FileFsWriteRepository::default_stub()) + )), + file_use_case_factory: Arc::new(crate::application::services::file_use_case_factory::AppFileUseCaseFactory::new( + Arc::new(crate::infrastructure::repositories::FileFsReadRepository::default_stub()), + Arc::new(crate::infrastructure::repositories::FileFsWriteRepository::default_stub()) + )), i18n_service: i18n_service.clone().unwrap_or_else(|| Arc::new(crate::application::services::i18n_application_service::I18nApplicationService::dummy()) ), diff --git a/src/interfaces/errors.rs b/src/interfaces/errors.rs new file mode 100644 index 00000000..cc20b277 --- /dev/null +++ b/src/interfaces/errors.rs @@ -0,0 +1,117 @@ +//! HTTP/API Error types for the interfaces layer. +//! +//! This module contains error types specific to the HTTP/API layer. +//! These errors handle the conversion from domain errors to HTTP responses. + +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use axum::Json; +use serde::Serialize; + +use crate::domain::errors::{DomainError, ErrorKind}; + +/// Error type for HTTP/API responses. +/// +/// This struct represents errors that will be returned to HTTP clients. +/// It contains the HTTP status code, a user-friendly message, and an error type identifier. +#[derive(Debug)] +pub struct AppError { + pub status_code: StatusCode, + pub message: String, + pub error_type: String, +} + +/// JSON response structure for errors. +#[derive(Serialize)] +pub struct ErrorResponse { + pub status: String, + pub message: String, + pub error_type: String, +} + +impl AppError { + /// Create a new AppError with custom status code, message and error type. + pub fn new(status_code: StatusCode, message: impl Into, error_type: impl Into) -> Self { + Self { + status_code, + message: message.into(), + error_type: error_type.into(), + } + } + + /// Create a 400 Bad Request error. + pub fn bad_request(message: impl Into) -> Self { + Self::new(StatusCode::BAD_REQUEST, message, "BadRequest") + } + + /// Create a 401 Unauthorized error. + pub fn unauthorized(message: impl Into) -> Self { + Self::new(StatusCode::UNAUTHORIZED, message, "Unauthorized") + } + + /// Create a 403 Forbidden error. + pub fn forbidden(message: impl Into) -> Self { + Self::new(StatusCode::FORBIDDEN, message, "Forbidden") + } + + /// Create a 404 Not Found error. + pub fn not_found(message: impl Into) -> Self { + Self::new(StatusCode::NOT_FOUND, message, "NotFound") + } + + /// Create a 500 Internal Server Error. + pub fn internal_error(message: impl Into) -> Self { + Self::new(StatusCode::INTERNAL_SERVER_ERROR, message, "InternalError") + } + + /// Create a 405 Method Not Allowed error. + pub fn method_not_allowed(message: impl Into) -> Self { + Self::new(StatusCode::METHOD_NOT_ALLOWED, message, "MethodNotAllowed") + } + + /// Create a 409 Conflict error. + pub fn conflict(message: impl Into) -> Self { + Self::new(StatusCode::CONFLICT, message, "Conflict") + } + + /// Create a 415 Unsupported Media Type error. + pub fn unsupported_media_type(message: impl Into) -> Self { + Self::new(StatusCode::UNSUPPORTED_MEDIA_TYPE, message, "UnsupportedMediaType") + } +} + +impl From for AppError { + fn from(err: DomainError) -> Self { + let status_code = match err.kind { + ErrorKind::NotFound => StatusCode::NOT_FOUND, + ErrorKind::AlreadyExists => StatusCode::CONFLICT, + ErrorKind::InvalidInput => StatusCode::BAD_REQUEST, + ErrorKind::AccessDenied => StatusCode::FORBIDDEN, + ErrorKind::Timeout => StatusCode::REQUEST_TIMEOUT, + ErrorKind::InternalError => StatusCode::INTERNAL_SERVER_ERROR, + ErrorKind::NotImplemented => StatusCode::NOT_IMPLEMENTED, + ErrorKind::UnsupportedOperation => StatusCode::METHOD_NOT_ALLOWED, + ErrorKind::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, + }; + + Self { + status_code, + message: err.message, + error_type: err.kind.to_string(), + } + } +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + let status = self.status_code; + let error_response = ErrorResponse { + status: status.to_string(), + message: self.message, + error_type: self.error_type, + }; + + let body = Json(error_response); + (status, body).into_response() + } +} diff --git a/src/interfaces/middleware/auth.rs b/src/interfaces/middleware/auth.rs index 29b4d34a..d0416a68 100644 --- a/src/interfaces/middleware/auth.rs +++ b/src/interfaces/middleware/auth.rs @@ -9,14 +9,8 @@ use axum::{ use crate::common::di::AppState; -// Extensión para almacenar datos del usuario autenticado -#[derive(Clone, Debug)] -pub struct CurrentUser { - pub id: String, - pub username: String, - pub email: String, - pub role: String, -} +// Re-export CurrentUser from application layer for use in handlers +pub use crate::application::dtos::user_dto::CurrentUser; // Estructura para usar en extractores de Axum #[derive(Clone, Debug)] diff --git a/src/interfaces/middleware/cache.rs b/src/interfaces/middleware/cache.rs index 0aace573..e1448dff 100644 --- a/src/interfaces/middleware/cache.rs +++ b/src/interfaces/middleware/cache.rs @@ -58,7 +58,6 @@ impl HttpCache { } /// Crea una nueva instancia con un tiempo de vida especificado - #[allow(dead_code)] pub fn with_max_age(max_age: u64) -> Self { Self { cache: Arc::new(Mutex::new(HashMap::with_capacity(100))), @@ -163,20 +162,6 @@ impl HttpCache { None } - /// Calcula el ETag para una respuesta - #[allow(dead_code)] - fn calculate_etag(&self, response: &T) -> EntityTag { - // Serializar la respuesta - let json = serde_json::to_string(response).unwrap_or_default(); - - // Calcular hash - let mut hasher = DefaultHasher::new(); - json.hash(&mut hasher); - let hash = hasher.finish(); - - format!("\"{}\"", hash) - } - /// Genera un ETag simple para un bloque de bytes fn calculate_etag_for_bytes(&self, bytes: &[u8]) -> EntityTag { // Calcular hash @@ -189,7 +174,6 @@ impl HttpCache { } /// Middleware de caché HTTP -#[allow(dead_code)] pub async fn cache_middleware( cache: HttpCache, cache_key: &str, @@ -328,7 +312,6 @@ pub struct HttpCacheLayer { impl HttpCacheLayer { /// Crea una nueva capa de caché - #[allow(dead_code)] pub fn new(cache: HttpCache) -> Self { Self { cache, @@ -337,7 +320,6 @@ impl HttpCacheLayer { } /// Establece el tiempo de vida máximo - #[allow(dead_code)] pub fn with_max_age(mut self, max_age: u64) -> Self { self.max_age = Some(max_age); self diff --git a/src/interfaces/mod.rs b/src/interfaces/mod.rs index fc1dae1b..e91f832f 100644 --- a/src/interfaces/mod.rs +++ b/src/interfaces/mod.rs @@ -1,5 +1,6 @@ pub mod api; pub mod web; pub mod middleware; +pub mod errors; -pub use api::create_api_routes; \ No newline at end of file +pub use api::create_api_routes; diff --git a/src/lib.rs b/src/lib.rs index 683bedd9..9b78d543 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,8 @@ pub use application::services::folder_service::FolderService; pub use application::services::file_service::FileService; pub use application::services::i18n_application_service::I18nApplicationService; pub use application::services::storage_mediator::{StorageMediator, FileSystemStorageMediator}; -pub use domain::services::path_service::PathService; +pub use infrastructure::services::path_service::PathService; +pub use domain::services::path_service::StoragePath; pub use infrastructure::repositories::folder_fs_repository::FolderFsRepository; pub use infrastructure::repositories::file_fs_repository::FileFsRepository; pub use infrastructure::repositories::parallel_file_processor::ParallelFileProcessor; diff --git a/src/main.rs b/src/main.rs index c75faf01..2ee5da15 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,791 +1,803 @@ -use std::net::SocketAddr; -use std::path::PathBuf; -use std::sync::Arc; - -use axum::Router; -use tower_http::trace::TraceLayer; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; - -/// OxiCloud - Cloud Storage Platform -/// -/// OxiCloud is a NextCloud-like file storage system built in Rust with a focus on -/// performance, security, and clean architecture. The system provides: -/// -/// - File and folder management with rich metadata -/// - User authentication and authorization -/// - File trash system with automatic cleanup -/// - Efficient handling of large files through parallel processing -/// - Compression capabilities for bandwidth optimization -/// - RESTful API and web interface -/// -/// The architecture follows the Clean/Hexagonal Architecture pattern with: -/// -/// - Domain Layer: Core business entities and repository interfaces (domain/*) -/// - Application Layer: Use cases and service orchestration (application/*) -/// - Infrastructure Layer: Technical implementations of repositories (infrastructure/*) -/// - Interface Layer: API endpoints and web controllers (interfaces/*) -/// -/// Dependencies are managed through dependency inversion, with high-level modules -/// defining interfaces (ports) that low-level modules implement (adapters). -/// -/// @author OxiCloud Development Team - -/// Common utilities, configuration, and error handling -mod common; -/// Core domain model, entities, and business rules -mod domain; -/// Application services, use cases, and DTOs -mod application; -/// Technical implementations of repositories and services -mod infrastructure; -/// External interfaces like API endpoints and web controllers -mod interfaces; - -use application::services::folder_service::FolderService; -use application::services::file_service::FileService; -use application::services::i18n_application_service::I18nApplicationService; -use application::services::storage_mediator::FileSystemStorageMediator; -use application::services::share_service::ShareService; -use application::services::favorites_service::FavoritesService; -use domain::services::path_service::PathService; -use infrastructure::repositories::folder_fs_repository::FolderFsRepository; -use infrastructure::repositories::file_fs_repository::FileFsRepository; -use infrastructure::repositories::parallel_file_processor::ParallelFileProcessor; -use infrastructure::repositories::share_fs_repository::ShareFsRepository; -use infrastructure::services::file_system_i18n_service::FileSystemI18nService; -use infrastructure::services::id_mapping_service::IdMappingService; -use infrastructure::services::id_mapping_optimizer::IdMappingOptimizer; -use infrastructure::services::file_metadata_cache::FileMetadataCache; -use infrastructure::services::buffer_pool::BufferPool; -use infrastructure::services::compression_service::GzipCompressionService; -use interfaces::{create_api_routes, web::create_web_routes}; -use application::services::trash_service::TrashService; -use infrastructure::repositories::trash_fs_repository::TrashFsRepository; -use infrastructure::services::trash_cleanup_service::TrashCleanupService; -use common::db::create_database_pool; -use common::auth_factory::create_auth_services; -use common::di::AppState; - -#[tokio::main] -async fn main() -> Result<(), Box> { - // Initialize tracing - tracing_subscriber::registry() - .with(tracing_subscriber::EnvFilter::new( - std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into()), - )) - .with(tracing_subscriber::fmt::layer()) - .init(); - - // Load configuration from environment variables - let config = common::config::AppConfig::from_env(); - - // Set up storage directory - let storage_path = config.storage_path.clone(); - if !storage_path.exists() { - std::fs::create_dir_all(&storage_path).expect("Failed to create storage directory"); - } - - // Set up locales directory - let locales_path = PathBuf::from("./static/locales"); - if !locales_path.exists() { - std::fs::create_dir_all(&locales_path).expect("Failed to create locales directory"); - } - - // Initialize database if auth is enabled - let db_pool = if config.features.enable_auth { - match create_database_pool(&config).await { - Ok(pool) => { - tracing::info!("PostgreSQL database pool initialized successfully"); - Some(Arc::new(pool)) - }, - Err(e) => { - tracing::error!("Failed to initialize database pool: {}", e); - None - } - } - } else { - None - }; - - // Create a reference to db_pool for use throughout the code - let db_pool_ref = db_pool.as_ref(); - - // Initialize path service - let path_service = Arc::new(PathService::new(storage_path.clone())); - - // Initialize ID mapping service for folders - let folder_id_mapping_path = storage_path.join("folder_ids.json"); - let folder_id_mapping_service = Arc::new( - IdMappingService::new(folder_id_mapping_path).await - .expect("Failed to initialize folder ID mapping service") - ); - - // Initialize ID mapping service for files - let file_id_mapping_path = storage_path.join("file_ids.json"); - let file_id_mapping_service = Arc::new( - IdMappingService::new(file_id_mapping_path).await - .expect("Failed to initialize file ID mapping service") - ); - - // For backward compatibility, use folder ID service as the base ID mapping service - let base_id_mapping_service = folder_id_mapping_service.clone(); - - // Create optimized ID mapping service with batch processing and caching - let id_mapping_optimizer = Arc::new( - IdMappingOptimizer::new(base_id_mapping_service.clone()) - ); - - // Initialize folder repository with all required components - let folder_repository = Arc::new(FolderFsRepository::new( - storage_path.clone(), - Arc::new(FileSystemStorageMediator::new_stub()), // Temporary stub (will be replaced) - base_id_mapping_service.clone(), - path_service.clone() - )); - - // Initialize storage mediator - let storage_mediator = Arc::new(FileSystemStorageMediator::new( - folder_repository.clone(), - path_service.clone(), - id_mapping_optimizer.clone() - )); - - // Update folder repository with proper storage mediator - // This replaces the stub we initialized it with - let folder_repository = Arc::new(FolderFsRepository::new( - storage_path.clone(), - storage_mediator.clone(), - base_id_mapping_service.clone(), - path_service.clone() - )); - - // Start cleanup task for ID mapping optimizer - IdMappingOptimizer::start_cleanup_task(id_mapping_optimizer.clone()); - - tracing::info!("ID mapping optimizer initialized with batch processing and caching"); - - // Initialize the metadata cache - let config = common::config::AppConfig::default(); - let metadata_cache = Arc::new(FileMetadataCache::default_with_config(config.clone())); - - // Start the periodic cleanup task for cache maintenance - let cache_clone = metadata_cache.clone(); - tokio::spawn(async move { - FileMetadataCache::start_cleanup_task(cache_clone).await; - }); - - // Initialize the buffer pool for memory optimization - // Use larger buffer size for better performance with large files - let buffer_pool = BufferPool::new(256 * 1024, 50, 120); // 256KB buffers, 50 max, 2 min TTL - - // Start the buffer pool cleanup task - BufferPool::start_cleaner(buffer_pool.clone()); - - tracing::info!("Buffer pool initialized with 50 buffers of 256KB each"); - - // Initialize parallel file processor with buffer pool - let parallel_processor = Arc::new(ParallelFileProcessor::new_with_buffer_pool( - config.clone(), - buffer_pool.clone() - )); - - // Initialize compression service with buffer pool - let _compression_service = Arc::new(GzipCompressionService::new_with_buffer_pool( - buffer_pool.clone() - )); - - // Initialize file repository with mediator, ID mapping service, metadata cache, and parallel processor - let file_repository = Arc::new(FileFsRepository::new_with_processor( - storage_path.clone(), - storage_mediator, - file_id_mapping_service.clone(), // Use the file-specific ID mapping service - path_service.clone(), - metadata_cache.clone(), // Clone to keep a reference for later use - parallel_processor - )); - - // Initialize application services - let folder_service = Arc::new(FolderService::new(folder_repository.clone())); - let file_service = Arc::new(FileService::new(file_repository.clone())); - - // Initialize trash service if enabled - let trash_repository = if config.features.enable_trash { - Some(Arc::new(TrashFsRepository::new( - storage_path.as_path(), - base_id_mapping_service.clone(), - ))) - } else { - None - }; - - // Create adapters for repositories (using domain interfaces instead of ports) - struct DomainFileRepoAdapter { - repo: Arc - } - - impl DomainFileRepoAdapter { - fn new(repo: Arc) -> Self { - Self { repo } - } - } - - #[async_trait::async_trait] - impl domain::repositories::file_repository::FileRepository for DomainFileRepoAdapter { - async fn save_file_from_bytes( - &self, - name: String, - folder_id: Option, - content_type: String, - content: Vec, - ) -> domain::repositories::file_repository::FileRepositoryResult { - self.repo.save_file(name, folder_id, content_type, content) - .await - .map_err(|e| domain::repositories::file_repository::FileRepositoryError::Other(format!("{}", e))) - } - - async fn save_file_with_id( - &self, - id: String, - name: String, - folder_id: Option, - content_type: String, - content: Vec, - ) -> domain::repositories::file_repository::FileRepositoryResult { - Err(domain::repositories::file_repository::FileRepositoryError::Other("Not implemented".to_string())) - } - - async fn get_file_by_id(&self, id: &str) -> domain::repositories::file_repository::FileRepositoryResult { - self.repo.get_file(id) - .await - .map_err(|e| domain::repositories::file_repository::FileRepositoryError::Other(format!("{}", e))) - } - - async fn list_files(&self, folder_id: Option<&str>) -> domain::repositories::file_repository::FileRepositoryResult> { - self.repo.list_files(folder_id) - .await - .map_err(|e| domain::repositories::file_repository::FileRepositoryError::Other(format!("{}", e))) - } - - async fn delete_file(&self, id: &str) -> domain::repositories::file_repository::FileRepositoryResult<()> { - self.repo.delete_file(id) - .await - .map_err(|e| domain::repositories::file_repository::FileRepositoryError::Other(format!("{}", e))) - } - - async fn delete_file_entry(&self, id: &str) -> domain::repositories::file_repository::FileRepositoryResult<()> { - self.delete_file(id).await - } - - async fn get_file_content(&self, id: &str) -> domain::repositories::file_repository::FileRepositoryResult> { - self.repo.get_file_content(id) - .await - .map_err(|e| domain::repositories::file_repository::FileRepositoryError::Other(format!("{}", e))) - } - - async fn get_file_stream(&self, id: &str) -> domain::repositories::file_repository::FileRepositoryResult> + Send>> { - self.repo.get_file_stream(id) - .await - .map_err(|e| domain::repositories::file_repository::FileRepositoryError::Other(format!("{}", e))) - } - - async fn move_file(&self, id: &str, target_folder_id: Option) -> domain::repositories::file_repository::FileRepositoryResult { - self.repo.move_file(id, target_folder_id) - .await - .map_err(|e| domain::repositories::file_repository::FileRepositoryError::Other(format!("{}", e))) - } - - async fn get_file_path(&self, id: &str) -> domain::repositories::file_repository::FileRepositoryResult { - self.repo.get_file_path(id) - .await - .map_err(|e| domain::repositories::file_repository::FileRepositoryError::Other(format!("{}", e))) - } - - async fn move_to_trash(&self, file_id: &str) -> domain::repositories::file_repository::FileRepositoryResult<()> { - // Since we're using TrashService to handle trashing, this method is not directly used - // but we'll implement it by delegating to the repository's delete_file method - self.repo.delete_file(file_id) - .await - .map_err(|e| domain::repositories::file_repository::FileRepositoryError::Other(format!("{}", e))) - } - - async fn restore_from_trash(&self, file_id: &str, original_path: &str) -> domain::repositories::file_repository::FileRepositoryResult<()> { - - - tracing::info!("Restoring file from trash: {} to {}", file_id, original_path); - - // We need to get the file from trash first to ensure it exists - match self.repo.get_file(file_id).await { - Ok(_) => { - // Extract the parent folder ID from the original path if available - let path_components: Vec<&str> = original_path.split('/').collect(); - let parent_folder: Option = if path_components.len() > 1 { - // Try to extract folder ID from path, but this is just a simplified approach - // In a real implementation, we would need to find or create the folder - tracing::info!("Attempting to restore to parent folder from path: {}", original_path); - None // No folder ID for now, will go to root - } else { - None // No parent folder, go to root - }; - - // Use move_file to attempt to restore the file to its original location or root - match self.repo.move_file(file_id, parent_folder).await { - Ok(_) => { - tracing::info!("Successfully restored file from trash: {}", file_id); - Ok(()) - }, - Err(e) => { - tracing::error!("Failed to restore file from trash: {}", e); - Err(domain::repositories::file_repository::FileRepositoryError::Other(format!("Failed to restore file: {}", e))) - } - } - }, - Err(e) => { - tracing::error!("File not found in trash: {}", e); - Err(domain::repositories::file_repository::FileRepositoryError::NotFound(file_id.to_string())) - } - } - } - - async fn delete_file_permanently(&self, file_id: &str) -> domain::repositories::file_repository::FileRepositoryResult<()> { - tracing::info!("Permanently deleting file: {}", file_id); - - // Directly attempt to delete the file using the file service - match self.repo.delete_file(file_id).await { - Ok(_) => { - tracing::info!("Successfully deleted file permanently: {}", file_id); - Ok(()) - }, - Err(e) => { - tracing::error!("Failed to permanently delete file: {}", e); - Err(domain::repositories::file_repository::FileRepositoryError::Other(format!("Failed to delete file permanently: {}", e))) - } - } - } - - async fn update_file_content(&self, file_id: &str, content: Vec) -> domain::repositories::file_repository::FileRepositoryResult<()> { - tracing::info!("Updating content for file: {}", file_id); - - self.repo.update_file_content(file_id, content) - .await - .map_err(|e| { - tracing::error!("Failed to update file content: {}", e); - domain::repositories::file_repository::FileRepositoryError::Other(format!("Failed to update file content: {}", e)) - }) - } - } - - struct DomainFolderRepoAdapter { - repo: Arc - } - - impl DomainFolderRepoAdapter { - fn new(repo: Arc) -> Self { - Self { repo } - } - } - - #[async_trait::async_trait] - impl domain::repositories::folder_repository::FolderRepository for DomainFolderRepoAdapter { - async fn create_folder(&self, name: String, parent_id: Option) -> domain::repositories::folder_repository::FolderRepositoryResult { - self.repo.create_folder(name, parent_id) - .await - .map_err(|e| domain::repositories::folder_repository::FolderRepositoryError::Other(format!("{}", e))) - } - - async fn get_folder_by_id(&self, id: &str) -> domain::repositories::folder_repository::FolderRepositoryResult { - self.repo.get_folder(id) - .await - .map_err(|e| domain::repositories::folder_repository::FolderRepositoryError::Other(format!("{}", e))) - } - - async fn get_folder_by_storage_path(&self, storage_path: &domain::services::path_service::StoragePath) -> domain::repositories::folder_repository::FolderRepositoryResult { - self.repo.get_folder_by_path(storage_path) - .await - .map_err(|e| domain::repositories::folder_repository::FolderRepositoryError::Other(format!("{}", e))) - } - - async fn list_folders(&self, parent_id: Option<&str>) -> domain::repositories::folder_repository::FolderRepositoryResult> { - self.repo.list_folders(parent_id) - .await - .map_err(|e| domain::repositories::folder_repository::FolderRepositoryError::Other(format!("{}", e))) - } - - async fn list_folders_paginated( - &self, - parent_id: Option<&str>, - offset: usize, - limit: usize, - include_total: bool - ) -> domain::repositories::folder_repository::FolderRepositoryResult<(Vec, Option)> { - self.repo.list_folders_paginated(parent_id, offset, limit, include_total) - .await - .map_err(|e| domain::repositories::folder_repository::FolderRepositoryError::Other(format!("{}", e))) - } - - async fn rename_folder(&self, id: &str, new_name: String) -> domain::repositories::folder_repository::FolderRepositoryResult { - self.repo.rename_folder(id, new_name) - .await - .map_err(|e| domain::repositories::folder_repository::FolderRepositoryError::Other(format!("{}", e))) - } - - async fn move_folder(&self, id: &str, new_parent_id: Option<&str>) -> domain::repositories::folder_repository::FolderRepositoryResult { - self.repo.move_folder(id, new_parent_id) - .await - .map_err(|e| domain::repositories::folder_repository::FolderRepositoryError::Other(format!("{}", e))) - } - - async fn delete_folder(&self, id: &str) -> domain::repositories::folder_repository::FolderRepositoryResult<()> { - self.repo.delete_folder(id) - .await - .map_err(|e| domain::repositories::folder_repository::FolderRepositoryError::Other(format!("{}", e))) - } - - async fn folder_exists_at_storage_path(&self, storage_path: &domain::services::path_service::StoragePath) -> domain::repositories::folder_repository::FolderRepositoryResult { - self.repo.folder_exists(storage_path) - .await - .map_err(|e| domain::repositories::folder_repository::FolderRepositoryError::Other(format!("{}", e))) - } - - async fn get_folder_storage_path(&self, id: &str) -> domain::repositories::folder_repository::FolderRepositoryResult { - self.repo.get_folder_path(id) - .await - .map_err(|e| domain::repositories::folder_repository::FolderRepositoryError::Other(format!("{}", e))) - } - - async fn folder_exists(&self, path: &std::path::PathBuf) -> domain::repositories::folder_repository::FolderRepositoryResult { - Err(domain::repositories::folder_repository::FolderRepositoryError::Other("Not implemented".to_string())) - } - - async fn get_folder_by_path(&self, path: &std::path::PathBuf) -> domain::repositories::folder_repository::FolderRepositoryResult { - Err(domain::repositories::folder_repository::FolderRepositoryError::Other("Not implemented".to_string())) - } - - async fn move_to_trash(&self, folder_id: &str) -> domain::repositories::folder_repository::FolderRepositoryResult<()> { - // Since we're using TrashService to handle trashing, this method is not directly used - // but we'll still use delete_folder since the underlying repository has proper trash support - self.repo.delete_folder(folder_id) - .await - .map_err(|e| domain::repositories::folder_repository::FolderRepositoryError::Other(format!("{}", e))) - } - - async fn restore_from_trash(&self, folder_id: &str, original_path: &str) -> domain::repositories::folder_repository::FolderRepositoryResult<()> { - // Convert the original_path to a StoragePath for the repository - use crate::domain::services::path_service::StoragePath; - let storage_path = StoragePath::from_string(original_path); - let _ = storage_path; // Prevent unused variable warning - - // The underlying repo doesn't have a direct API for this, but the implementation exists - // in the folder repository through TrashService - // This should be coordinated through TrashService instead - Err(domain::repositories::folder_repository::FolderRepositoryError::Other( - "Restore from trash should be handled by TrashService, not through this adapter".to_string())) - } - - async fn delete_folder_permanently(&self, folder_id: &str) -> domain::repositories::folder_repository::FolderRepositoryResult<()> { - // The repository now has proper implementation for permanent deletion - // But we still use delete_folder since that's the method available on FolderStoragePort - self.delete_folder(folder_id).await - } - } - - // Create repository adapters - let file_repo_adapter = Arc::new(DomainFileRepoAdapter::new(file_repository.clone())); - let folder_repo_adapter = Arc::new(DomainFolderRepoAdapter::new(folder_repository.clone())); - - // Create the trash service with properly typed adapters - let trash_service = if let Some(ref trash_repo) = trash_repository { - let service = Arc::new(TrashService::new( - trash_repo.clone(), - file_repo_adapter, - folder_repo_adapter, - config.storage.trash_retention_days, - )); - - // Initialize trash cleanup service - let cleanup_service = TrashCleanupService::new( - service.clone(), - trash_repo.clone(), - 24, // Run cleanup every 24 hours - ); - - // Start cleanup job if trash is enabled - if config.features.enable_trash { - cleanup_service.start_cleanup_job().await; - tracing::info!("Trash cleanup service started with daily schedule"); - } - - Some(service as Arc) - } else { - None - }; - - // Initialize i18n service - let i18n_repository = Arc::new(FileSystemI18nService::new(locales_path.clone())); - let i18n_service = Arc::new(I18nApplicationService::new(i18n_repository.clone())); - - // Preload translations - if let Err(e) = i18n_service.load_translations(domain::services::i18n_service::Locale::English).await { - tracing::warn!("Failed to load English translations: {}", e); - } - if let Err(e) = i18n_service.load_translations(domain::services::i18n_service::Locale::Spanish).await { - tracing::warn!("Failed to load Spanish translations: {}", e); - } - - tracing::info!("Compression service initialized with buffer pool support"); - - // Initialize auth services if enabled and database connection is available - let auth_services = if config.features.enable_auth && db_pool_ref.is_some() { - match create_auth_services( - &config, - db_pool_ref.unwrap().clone(), - Some(folder_service.clone()) // Pasar el servicio de carpetas para creación automática de carpetas de usuario - ).await { - Ok(services) => { - tracing::info!("Authentication services initialized successfully with folder service"); - Some(services) - }, - Err(e) => { - tracing::error!("Failed to initialize authentication services: {}", e); - None - } - } - } else { - None - }; - - // Create AppState for DI container - let core_services = common::di::CoreServices { - path_service: path_service.clone(), - cache_manager: Arc::new(infrastructure::services::cache_manager::StorageCacheManager::default()), - id_mapping_service: base_id_mapping_service.clone(), // We keep using the folder ID mapping service for core services - config: config.clone(), - }; - - // Crear stubs para los repositorios - let file_read_stub = Arc::new(infrastructure::repositories::FileFsReadRepository::default_stub()); - let file_write_stub = Arc::new(infrastructure::repositories::FileFsWriteRepository::default_stub()); - let storage_mediator_stub = Arc::new(application::services::storage_mediator::FileSystemStorageMediator::new_stub()); - let metadata_manager = Arc::new(infrastructure::repositories::FileMetadataManager::default()); - let path_resolver_stub = Arc::new(infrastructure::repositories::FilePathResolver::default_stub()); - - let repository_services = common::di::RepositoryServices { - folder_repository: Arc::new(FolderFsRepository::new( - storage_path.clone(), - storage_mediator_stub.clone(), - folder_id_mapping_service.clone(), - path_service.clone() - )), - file_repository: Arc::new(FileFsRepository::new( - storage_path.clone(), - storage_mediator_stub.clone(), - file_id_mapping_service.clone(), - path_service.clone(), - metadata_cache.clone(), - )), - file_read_repository: file_read_stub, - file_write_repository: file_write_stub, - i18n_repository: i18n_repository.clone(), - storage_mediator: storage_mediator_stub, - metadata_manager, - path_resolver: path_resolver_stub, - trash_repository: trash_repository.clone().map(|repo| { - // Convert Arc to Arc - let repo: Arc = repo; - repo - }), - }; - - // Create the search service - let search_service: Option> = { - // Create the search service with caching - let search_service = Arc::new(application::services::search_service::SearchService::new( - file_repository.clone(), - folder_repository.clone(), - 300, // Cache TTL in seconds (5 minutes) - 1000, // Maximum cache entries - )); - - tracing::info!("Search service initialized with caching (TTL: 300s, max entries: 1000)"); - Some(search_service) - }; - - // Initialize share repository and service if enabled - let share_service: Option> = if config.features.enable_file_sharing { - let share_repository = Arc::new(ShareFsRepository::new( - Arc::new(config.clone()) - )); - - let share_service = Arc::new(ShareService::new( - Arc::new(config.clone()), - share_repository, - file_repository.clone(), - folder_repository.clone() - )); - - tracing::info!("File sharing service initialized successfully"); - Some(share_service) - } else { - tracing::info!("File sharing service is disabled in configuration"); - None - }; - - // Initialize favorites service if database is available - let favorites_service: Option> = - if let Some(pool) = db_pool_ref { - // Create a new favorites service with the database pool - let favorites_service = Arc::new(FavoritesService::new( - pool.clone() - )); - - tracing::info!("Favorites service initialized successfully"); - Some(favorites_service) - } else { - tracing::info!("Favorites service is disabled (requires database connection)"); - None - }; - - // Initialize recent items service if database is available - let recent_service: Option> = - if let Some(pool) = db_pool_ref { - // Create a new service with the database pool - let service = Arc::new(application::services::recent_service::RecentService::new( - pool.clone(), - 50 // Maximum recent items per user - )); - - tracing::info!("Recent items service initialized successfully"); - Some(service) - } else { - tracing::info!("Recent items service is disabled (requires database connection)"); - None - }; - - // For now, we'll use a placeholder for the contact service - // Instead of using the real PostgreSQL repositories, we'll create a dummy implementation - // This makes the code compile, and we can replace it with the real implementation later - let contact_service: Option> = None; - - let application_services = common::di::ApplicationServices { - folder_service: folder_service.clone(), - file_service: file_service.clone(), - file_upload_service: Arc::new(application::services::file_upload_service::FileUploadService::default_stub()), - file_retrieval_service: Arc::new(application::services::file_retrieval_service::FileRetrievalService::default_stub()), - file_management_service: Arc::new(application::services::file_management_service::FileManagementService::default_stub()), - file_use_case_factory: Arc::new(application::services::file_use_case_factory::AppFileUseCaseFactory::default_stub()), - i18n_service: i18n_service.clone(), - trash_service: trash_service.clone(), - search_service: search_service.clone(), - share_service: share_service.clone(), - favorites_service: favorites_service.clone(), - recent_service: recent_service.clone() - }; - - // Create the AppState without Arc first - let calendar_service_option = None; - - let mut app_state = AppState { - core: core_services, - repositories: repository_services, - applications: application_services, - db_pool: db_pool.clone(), - auth_service: auth_services.clone(), - trash_service: trash_service.clone(), - share_service: share_service.clone(), - favorites_service: favorites_service.clone(), - recent_service: recent_service.clone(), - storage_usage_service: None, - calendar_service: calendar_service_option, - contact_service: contact_service.clone(), - }; - - // Initialize storage usage service - let _storage_usage_service = if let Some(pool) = db_pool_ref { - // Create a user repository that implements UserStoragePort - let user_repository = Arc::new( - infrastructure::repositories::pg::UserPgRepository::new(pool.clone()) - ); - - // Create storage usage service that uses database for user information - // and file repository for storage calculation - let service = Arc::new(application::services::storage_usage_service::StorageUsageService::new( - file_repository.clone(), - user_repository, - )); - - tracing::info!("Storage usage service initialized successfully"); - - // Add the service to the app state - app_state = app_state.with_storage_usage_service(service.clone()); - - Some(service) - } else { - tracing::info!("Storage usage service is disabled (requires database connection)"); - None - }; - - // Wrap in Arc after all modifications - let app_state = Arc::new(app_state); - - // Build application router - let api_routes = create_api_routes(folder_service, file_service, Some(i18n_service), trash_service, search_service, share_service, favorites_service, recent_service); - let web_routes = create_web_routes(); - - // Build the app router - // Import auth handler - use interfaces::api::handlers::auth_handler::auth_routes; - - // Create basic app router - let mut app = Router::new() - .nest("/api", api_routes) - .merge(web_routes) - .layer(TraceLayer::new_for_http()); - - // Add auth routes if auth is enabled - if config.features.enable_auth && auth_services.is_some() { - // Create auth routes with app state - let auth_router = auth_routes().with_state(app_state.clone()); - - // Add auth routes at /api/auth - app = app.nest("/api/auth", auth_router); - } - - // Preload common directories to warm the cache - tracing::info!("Preloading common directories to warm up cache..."); - if let Ok(count) = metadata_cache.preload_directory(&storage_path, true, 1).await { - tracing::info!("Preloaded {} directory entries into cache", count); - } - - // Start server with clear message - let addr = SocketAddr::from(([0, 0, 0, 0], 8086)); - tracing::info!("Starting OxiCloud server on http://{}", addr); - - // Start the server - tracing::info!("Authentication system initialized successfully"); - - // Import the redirect middleware - use crate::interfaces::middleware::redirect::redirect_middleware; - - // Apply the redirect middleware to handle legacy routes - app = app.layer(axum::middleware::from_fn(redirect_middleware)); - - // Create a standard TCP listener - let listener = tokio::net::TcpListener::bind(addr).await?; - tracing::info!("Server binding to http://{}", addr); - tracing::info!("Starting server with Axum routes..."); - - // Axum 0.8 requires the state to match the expected type - // Extract the state from Arc so we can pass it to the router - let app_state_inner = Arc::try_unwrap(app_state) - .unwrap_or_else(|arc| (*arc).clone()); - - // Add global state to the router - let app = app.with_state(app_state_inner); - - // Use axum's serve function with the router with state - axum::serve(listener, app).await?; - - tracing::info!("Server shutdown completed"); - - Ok(()) -} - +use std::net::SocketAddr; +use std::path::PathBuf; +use std::sync::Arc; + +use axum::Router; +use tower_http::trace::TraceLayer; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + +/// OxiCloud - Cloud Storage Platform +/// +/// OxiCloud is a NextCloud-like file storage system built in Rust with a focus on +/// performance, security, and clean architecture. The system provides: +/// +/// - File and folder management with rich metadata +/// - User authentication and authorization +/// - File trash system with automatic cleanup +/// - Efficient handling of large files through parallel processing +/// - Compression capabilities for bandwidth optimization +/// - RESTful API and web interface +/// +/// The architecture follows the Clean/Hexagonal Architecture pattern with: +/// +/// - Domain Layer: Core business entities and repository interfaces (domain/*) +/// - Application Layer: Use cases and service orchestration (application/*) +/// - Infrastructure Layer: Technical implementations of repositories (infrastructure/*) +/// - Interface Layer: API endpoints and web controllers (interfaces/*) +/// +/// Dependencies are managed through dependency inversion, with high-level modules +/// defining interfaces (ports) that low-level modules implement (adapters). +/// +/// @author OxiCloud Development Team + +// Use the library crate instead of redefining modules +use oxicloud::application; +use oxicloud::common; +use oxicloud::domain; +use oxicloud::infrastructure; +use oxicloud::interfaces; + +use application::services::folder_service::FolderService; +use application::services::file_service::FileService; +use application::services::i18n_application_service::I18nApplicationService; +use application::services::storage_mediator::FileSystemStorageMediator; +use application::services::share_service::ShareService; +use application::services::favorites_service::FavoritesService; +use infrastructure::services::path_service::PathService; +use infrastructure::repositories::folder_fs_repository::FolderFsRepository; +use infrastructure::repositories::file_fs_repository::FileFsRepository; +use infrastructure::repositories::parallel_file_processor::ParallelFileProcessor; +use infrastructure::repositories::share_fs_repository::ShareFsRepository; +use infrastructure::services::file_system_i18n_service::FileSystemI18nService; +use infrastructure::services::id_mapping_service::IdMappingService; +use infrastructure::services::id_mapping_optimizer::IdMappingOptimizer; +use infrastructure::services::file_metadata_cache::FileMetadataCache; +use infrastructure::services::buffer_pool::BufferPool; +use infrastructure::services::compression_service::GzipCompressionService; +use interfaces::{create_api_routes, web::create_web_routes}; +use application::services::trash_service::TrashService; +use infrastructure::repositories::trash_fs_repository::TrashFsRepository; +use infrastructure::services::trash_cleanup_service::TrashCleanupService; +use common::db::create_database_pool; +use common::auth_factory::create_auth_services; +use common::di::AppState; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize tracing + tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::new( + std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into()), + )) + .with(tracing_subscriber::fmt::layer()) + .init(); + + // Load configuration from environment variables + let config = common::config::AppConfig::from_env(); + + // Set up storage directory + let storage_path = config.storage_path.clone(); + if !storage_path.exists() { + std::fs::create_dir_all(&storage_path).expect("Failed to create storage directory"); + } + + // Set up locales directory + let locales_path = PathBuf::from("./static/locales"); + if !locales_path.exists() { + std::fs::create_dir_all(&locales_path).expect("Failed to create locales directory"); + } + + // Initialize database if auth is enabled + let db_pool = if config.features.enable_auth { + match create_database_pool(&config).await { + Ok(pool) => { + tracing::info!("PostgreSQL database pool initialized successfully"); + Some(Arc::new(pool)) + }, + Err(e) => { + tracing::error!("Failed to initialize database pool: {}", e); + None + } + } + } else { + None + }; + + // Create a reference to db_pool for use throughout the code + let db_pool_ref = db_pool.as_ref(); + + // Initialize path service + let path_service = Arc::new(PathService::new(storage_path.clone())); + + // Initialize ID mapping service for folders + let folder_id_mapping_path = storage_path.join("folder_ids.json"); + let folder_id_mapping_service = Arc::new( + IdMappingService::new(folder_id_mapping_path).await + .expect("Failed to initialize folder ID mapping service") + ); + + // Initialize ID mapping service for files + let file_id_mapping_path = storage_path.join("file_ids.json"); + let file_id_mapping_service = Arc::new( + IdMappingService::new(file_id_mapping_path).await + .expect("Failed to initialize file ID mapping service") + ); + + // For backward compatibility, use folder ID service as the base ID mapping service + let base_id_mapping_service = folder_id_mapping_service.clone(); + + // Create optimized ID mapping service with batch processing and caching + let id_mapping_optimizer = Arc::new( + IdMappingOptimizer::new(base_id_mapping_service.clone()) + ); + + // Initialize folder repository with all required components + let folder_repository = Arc::new(FolderFsRepository::new( + storage_path.clone(), + Arc::new(FileSystemStorageMediator::new_stub()), // Temporary stub (will be replaced) + base_id_mapping_service.clone(), + path_service.clone() + )); + + // Initialize storage mediator + let storage_mediator = Arc::new(FileSystemStorageMediator::new( + folder_repository.clone(), + path_service.clone(), + id_mapping_optimizer.clone() + )); + + // Update folder repository with proper storage mediator + // This replaces the stub we initialized it with + let folder_repository = Arc::new(FolderFsRepository::new( + storage_path.clone(), + storage_mediator.clone(), + base_id_mapping_service.clone(), + path_service.clone() + )); + + // Start cleanup task for ID mapping optimizer + IdMappingOptimizer::start_cleanup_task(id_mapping_optimizer.clone()); + + tracing::info!("ID mapping optimizer initialized with batch processing and caching"); + + // Initialize the metadata cache + let config = common::config::AppConfig::default(); + let metadata_cache = Arc::new(FileMetadataCache::default_with_config(config.clone())); + + // Start the periodic cleanup task for cache maintenance + let cache_clone = metadata_cache.clone(); + tokio::spawn(async move { + FileMetadataCache::start_cleanup_task(cache_clone).await; + }); + + // Initialize the buffer pool for memory optimization + // Use larger buffer size for better performance with large files + let buffer_pool = BufferPool::new(256 * 1024, 50, 120); // 256KB buffers, 50 max, 2 min TTL + + // Start the buffer pool cleanup task + BufferPool::start_cleaner(buffer_pool.clone()); + + tracing::info!("Buffer pool initialized with 50 buffers of 256KB each"); + + // Initialize parallel file processor with buffer pool + let parallel_processor = Arc::new(ParallelFileProcessor::new_with_buffer_pool( + config.clone(), + buffer_pool.clone() + )); + + // Initialize compression service with buffer pool + let _compression_service = Arc::new(GzipCompressionService::new_with_buffer_pool( + buffer_pool.clone() + )); + + // Initialize file repository with mediator, ID mapping service, metadata cache, and parallel processor + let file_repository = Arc::new(FileFsRepository::new_with_processor( + storage_path.clone(), + storage_mediator, + file_id_mapping_service.clone(), // Use the file-specific ID mapping service + path_service.clone(), + metadata_cache.clone(), // Clone to keep a reference for later use + parallel_processor + )); + + // Initialize application services + let folder_service = Arc::new(FolderService::new(folder_repository.clone())); + let file_service = Arc::new(FileService::new(file_repository.clone())); + + // Initialize trash service if enabled + let trash_repository = if config.features.enable_trash { + Some(Arc::new(TrashFsRepository::new( + storage_path.as_path(), + base_id_mapping_service.clone(), + ))) + } else { + None + }; + + // Create adapters for repositories (using domain interfaces instead of ports) + struct DomainFileRepoAdapter { + repo: Arc + } + + impl DomainFileRepoAdapter { + fn new(repo: Arc) -> Self { + Self { repo } + } + } + + #[async_trait::async_trait] + impl domain::repositories::file_repository::FileRepository for DomainFileRepoAdapter { + async fn save_file_from_bytes( + &self, + name: String, + folder_id: Option, + content_type: String, + content: Vec, + ) -> domain::repositories::file_repository::FileRepositoryResult { + self.repo.save_file(name, folder_id, content_type, content) + .await + .map_err(|e| domain::repositories::file_repository::FileRepositoryError::Other(format!("{}", e))) + } + + async fn save_file_with_id( + &self, + _id: String, + _name: String, + _folder_id: Option, + _content_type: String, + _content: Vec, + ) -> domain::repositories::file_repository::FileRepositoryResult { + Err(domain::repositories::file_repository::FileRepositoryError::Other("Not implemented".to_string())) + } + + async fn get_file_by_id(&self, id: &str) -> domain::repositories::file_repository::FileRepositoryResult { + self.repo.get_file(id) + .await + .map_err(|e| domain::repositories::file_repository::FileRepositoryError::Other(format!("{}", e))) + } + + async fn list_files(&self, folder_id: Option<&str>) -> domain::repositories::file_repository::FileRepositoryResult> { + self.repo.list_files(folder_id) + .await + .map_err(|e| domain::repositories::file_repository::FileRepositoryError::Other(format!("{}", e))) + } + + async fn delete_file(&self, id: &str) -> domain::repositories::file_repository::FileRepositoryResult<()> { + self.repo.delete_file(id) + .await + .map_err(|e| domain::repositories::file_repository::FileRepositoryError::Other(format!("{}", e))) + } + + async fn delete_file_entry(&self, id: &str) -> domain::repositories::file_repository::FileRepositoryResult<()> { + self.delete_file(id).await + } + + async fn get_file_content(&self, id: &str) -> domain::repositories::file_repository::FileRepositoryResult> { + self.repo.get_file_content(id) + .await + .map_err(|e| domain::repositories::file_repository::FileRepositoryError::Other(format!("{}", e))) + } + + async fn get_file_stream(&self, id: &str) -> domain::repositories::file_repository::FileRepositoryResult> + Send>> { + self.repo.get_file_stream(id) + .await + .map_err(|e| domain::repositories::file_repository::FileRepositoryError::Other(format!("{}", e))) + } + + async fn move_file(&self, id: &str, target_folder_id: Option) -> domain::repositories::file_repository::FileRepositoryResult { + self.repo.move_file(id, target_folder_id) + .await + .map_err(|e| domain::repositories::file_repository::FileRepositoryError::Other(format!("{}", e))) + } + + async fn get_file_path(&self, id: &str) -> domain::repositories::file_repository::FileRepositoryResult { + self.repo.get_file_path(id) + .await + .map_err(|e| domain::repositories::file_repository::FileRepositoryError::Other(format!("{}", e))) + } + + async fn move_to_trash(&self, file_id: &str) -> domain::repositories::file_repository::FileRepositoryResult<()> { + // Since we're using TrashService to handle trashing, this method is not directly used + // but we'll implement it by delegating to the repository's delete_file method + self.repo.delete_file(file_id) + .await + .map_err(|e| domain::repositories::file_repository::FileRepositoryError::Other(format!("{}", e))) + } + + async fn restore_from_trash(&self, file_id: &str, original_path: &str) -> domain::repositories::file_repository::FileRepositoryResult<()> { + + + tracing::info!("Restoring file from trash: {} to {}", file_id, original_path); + + // We need to get the file from trash first to ensure it exists + match self.repo.get_file(file_id).await { + Ok(_) => { + // Extract the parent folder ID from the original path if available + let path_components: Vec<&str> = original_path.split('/').collect(); + let parent_folder: Option = if path_components.len() > 1 { + // Try to extract folder ID from path, but this is just a simplified approach + // In a real implementation, we would need to find or create the folder + tracing::info!("Attempting to restore to parent folder from path: {}", original_path); + None // No folder ID for now, will go to root + } else { + None // No parent folder, go to root + }; + + // Use move_file to attempt to restore the file to its original location or root + match self.repo.move_file(file_id, parent_folder).await { + Ok(_) => { + tracing::info!("Successfully restored file from trash: {}", file_id); + Ok(()) + }, + Err(e) => { + tracing::error!("Failed to restore file from trash: {}", e); + Err(domain::repositories::file_repository::FileRepositoryError::Other(format!("Failed to restore file: {}", e))) + } + } + }, + Err(e) => { + tracing::error!("File not found in trash: {}", e); + Err(domain::repositories::file_repository::FileRepositoryError::NotFound(file_id.to_string())) + } + } + } + + async fn delete_file_permanently(&self, file_id: &str) -> domain::repositories::file_repository::FileRepositoryResult<()> { + tracing::info!("Permanently deleting file: {}", file_id); + + // Directly attempt to delete the file using the file service + match self.repo.delete_file(file_id).await { + Ok(_) => { + tracing::info!("Successfully deleted file permanently: {}", file_id); + Ok(()) + }, + Err(e) => { + tracing::error!("Failed to permanently delete file: {}", e); + Err(domain::repositories::file_repository::FileRepositoryError::Other(format!("Failed to delete file permanently: {}", e))) + } + } + } + + async fn update_file_content(&self, file_id: &str, content: Vec) -> domain::repositories::file_repository::FileRepositoryResult<()> { + tracing::info!("Updating content for file: {}", file_id); + + self.repo.update_file_content(file_id, content) + .await + .map_err(|e| { + tracing::error!("Failed to update file content: {}", e); + domain::repositories::file_repository::FileRepositoryError::Other(format!("Failed to update file content: {}", e)) + }) + } + } + + struct DomainFolderRepoAdapter { + repo: Arc + } + + impl DomainFolderRepoAdapter { + fn new(repo: Arc) -> Self { + Self { repo } + } + } + + #[async_trait::async_trait] + impl domain::repositories::folder_repository::FolderRepository for DomainFolderRepoAdapter { + async fn create_folder(&self, name: String, parent_id: Option) -> domain::repositories::folder_repository::FolderRepositoryResult { + self.repo.create_folder(name, parent_id) + .await + .map_err(|e| domain::repositories::folder_repository::FolderRepositoryError::Other(format!("{}", e))) + } + + async fn get_folder_by_id(&self, id: &str) -> domain::repositories::folder_repository::FolderRepositoryResult { + self.repo.get_folder(id) + .await + .map_err(|e| domain::repositories::folder_repository::FolderRepositoryError::Other(format!("{}", e))) + } + + async fn get_folder_by_storage_path(&self, storage_path: &domain::services::path_service::StoragePath) -> domain::repositories::folder_repository::FolderRepositoryResult { + self.repo.get_folder_by_path(storage_path) + .await + .map_err(|e| domain::repositories::folder_repository::FolderRepositoryError::Other(format!("{}", e))) + } + + async fn list_folders(&self, parent_id: Option<&str>) -> domain::repositories::folder_repository::FolderRepositoryResult> { + self.repo.list_folders(parent_id) + .await + .map_err(|e| domain::repositories::folder_repository::FolderRepositoryError::Other(format!("{}", e))) + } + + async fn list_folders_paginated( + &self, + parent_id: Option<&str>, + offset: usize, + limit: usize, + include_total: bool + ) -> domain::repositories::folder_repository::FolderRepositoryResult<(Vec, Option)> { + self.repo.list_folders_paginated(parent_id, offset, limit, include_total) + .await + .map_err(|e| domain::repositories::folder_repository::FolderRepositoryError::Other(format!("{}", e))) + } + + async fn rename_folder(&self, id: &str, new_name: String) -> domain::repositories::folder_repository::FolderRepositoryResult { + self.repo.rename_folder(id, new_name) + .await + .map_err(|e| domain::repositories::folder_repository::FolderRepositoryError::Other(format!("{}", e))) + } + + async fn move_folder(&self, id: &str, new_parent_id: Option<&str>) -> domain::repositories::folder_repository::FolderRepositoryResult { + self.repo.move_folder(id, new_parent_id) + .await + .map_err(|e| domain::repositories::folder_repository::FolderRepositoryError::Other(format!("{}", e))) + } + + async fn delete_folder(&self, id: &str) -> domain::repositories::folder_repository::FolderRepositoryResult<()> { + self.repo.delete_folder(id) + .await + .map_err(|e| domain::repositories::folder_repository::FolderRepositoryError::Other(format!("{}", e))) + } + + async fn folder_exists_at_storage_path(&self, storage_path: &domain::services::path_service::StoragePath) -> domain::repositories::folder_repository::FolderRepositoryResult { + self.repo.folder_exists(storage_path) + .await + .map_err(|e| domain::repositories::folder_repository::FolderRepositoryError::Other(format!("{}", e))) + } + + async fn get_folder_storage_path(&self, id: &str) -> domain::repositories::folder_repository::FolderRepositoryResult { + self.repo.get_folder_path(id) + .await + .map_err(|e| domain::repositories::folder_repository::FolderRepositoryError::Other(format!("{}", e))) + } + + async fn folder_exists(&self, _path: &std::path::PathBuf) -> domain::repositories::folder_repository::FolderRepositoryResult { + Err(domain::repositories::folder_repository::FolderRepositoryError::Other("Not implemented".to_string())) + } + + async fn get_folder_by_path(&self, _path: &std::path::PathBuf) -> domain::repositories::folder_repository::FolderRepositoryResult { + Err(domain::repositories::folder_repository::FolderRepositoryError::Other("Not implemented".to_string())) + } + + async fn move_to_trash(&self, folder_id: &str) -> domain::repositories::folder_repository::FolderRepositoryResult<()> { + // Since we're using TrashService to handle trashing, this method is not directly used + // but we'll still use delete_folder since the underlying repository has proper trash support + self.repo.delete_folder(folder_id) + .await + .map_err(|e| domain::repositories::folder_repository::FolderRepositoryError::Other(format!("{}", e))) + } + + async fn restore_from_trash(&self, _folder_id: &str, original_path: &str) -> domain::repositories::folder_repository::FolderRepositoryResult<()> { + // Convert the original_path to a StoragePath for the repository + use crate::domain::services::path_service::StoragePath; + let storage_path = StoragePath::from_string(original_path); + let _ = storage_path; // Prevent unused variable warning + + // The underlying repo doesn't have a direct API for this, but the implementation exists + // in the folder repository through TrashService + // This should be coordinated through TrashService instead + Err(domain::repositories::folder_repository::FolderRepositoryError::Other( + "Restore from trash should be handled by TrashService, not through this adapter".to_string())) + } + + async fn delete_folder_permanently(&self, folder_id: &str) -> domain::repositories::folder_repository::FolderRepositoryResult<()> { + // The repository now has proper implementation for permanent deletion + // But we still use delete_folder since that's the method available on FolderStoragePort + self.delete_folder(folder_id).await + } + } + + // Create repository adapters + let file_repo_adapter = Arc::new(DomainFileRepoAdapter::new(file_repository.clone())); + let folder_repo_adapter = Arc::new(DomainFolderRepoAdapter::new(folder_repository.clone())); + + // Create the trash service with properly typed adapters + let trash_service = if let Some(ref trash_repo) = trash_repository { + let service = Arc::new(TrashService::new( + trash_repo.clone(), + file_repo_adapter, + folder_repo_adapter, + config.storage.trash_retention_days, + )); + + // Initialize trash cleanup service + let cleanup_service = TrashCleanupService::new( + service.clone(), + trash_repo.clone(), + 24, // Run cleanup every 24 hours + ); + + // Start cleanup job if trash is enabled + if config.features.enable_trash { + cleanup_service.start_cleanup_job().await; + tracing::info!("Trash cleanup service started with daily schedule"); + } + + Some(service as Arc) + } else { + None + }; + + // Initialize i18n service + let i18n_repository = Arc::new(FileSystemI18nService::new(locales_path.clone())); + let i18n_service = Arc::new(I18nApplicationService::new(i18n_repository.clone())); + + // Preload translations + if let Err(e) = i18n_service.load_translations(domain::services::i18n_service::Locale::English).await { + tracing::warn!("Failed to load English translations: {}", e); + } + if let Err(e) = i18n_service.load_translations(domain::services::i18n_service::Locale::Spanish).await { + tracing::warn!("Failed to load Spanish translations: {}", e); + } + + tracing::info!("Compression service initialized with buffer pool support"); + + // Initialize auth services if enabled and database connection is available + let auth_services = if config.features.enable_auth && db_pool_ref.is_some() { + match create_auth_services( + &config, + db_pool_ref.unwrap().clone(), + Some(folder_service.clone()) // Pasar el servicio de carpetas para creación automática de carpetas de usuario + ).await { + Ok(services) => { + tracing::info!("Authentication services initialized successfully with folder service"); + Some(services) + }, + Err(e) => { + tracing::error!("Failed to initialize authentication services: {}", e); + None + } + } + } else { + None + }; + + // Create AppState for DI container + let core_services = common::di::CoreServices { + path_service: path_service.clone(), + cache_manager: Arc::new(infrastructure::services::cache_manager::StorageCacheManager::default()), + id_mapping_service: base_id_mapping_service.clone(), + file_id_mapping_service: file_id_mapping_service.clone(), + id_mapping_optimizer: id_mapping_optimizer.clone(), + config: config.clone(), + }; + + // Crear stubs para los repositorios + let file_read_stub = Arc::new(infrastructure::repositories::FileFsReadRepository::default_stub()); + let file_write_stub = Arc::new(infrastructure::repositories::FileFsWriteRepository::default_stub()); + let storage_mediator_stub = Arc::new(application::services::storage_mediator::FileSystemStorageMediator::new_stub()); + let metadata_manager = Arc::new(infrastructure::repositories::FileMetadataManager::default()); + let path_resolver_stub = Arc::new(infrastructure::repositories::FilePathResolver::default_stub()); + + let repository_services = common::di::RepositoryServices { + folder_repository: Arc::new(FolderFsRepository::new( + storage_path.clone(), + storage_mediator_stub.clone(), + folder_id_mapping_service.clone(), + path_service.clone() + )), + file_repository: Arc::new(FileFsRepository::new( + storage_path.clone(), + storage_mediator_stub.clone(), + file_id_mapping_service.clone(), + path_service.clone(), + metadata_cache.clone(), + )), + file_read_repository: file_read_stub, + file_write_repository: file_write_stub, + i18n_repository: i18n_repository.clone(), + storage_mediator: storage_mediator_stub, + metadata_manager, + path_resolver: path_resolver_stub, + metadata_cache: metadata_cache.clone(), + trash_repository: trash_repository.clone().map(|repo| { + // Convert Arc to Arc + let repo: Arc = repo; + repo + }), + }; + + // Create the search service + let search_service: Option> = { + // Create the search service with caching + let search_service = Arc::new(application::services::search_service::SearchService::new( + file_repository.clone(), + folder_repository.clone(), + 300, // Cache TTL in seconds (5 minutes) + 1000, // Maximum cache entries + )); + + tracing::info!("Search service initialized with caching (TTL: 300s, max entries: 1000)"); + Some(search_service) + }; + + // Initialize share repository and service if enabled + let share_service: Option> = if config.features.enable_file_sharing { + let share_repository = Arc::new(ShareFsRepository::new( + Arc::new(config.clone()) + )); + + let share_service = Arc::new(ShareService::new( + Arc::new(config.clone()), + share_repository, + file_repository.clone(), + folder_repository.clone() + )); + + tracing::info!("File sharing service initialized successfully"); + Some(share_service) + } else { + tracing::info!("File sharing service is disabled in configuration"); + None + }; + + // Initialize favorites service if database is available + let favorites_service: Option> = + if let Some(pool) = db_pool_ref { + // Create a new favorites service with the database pool + let favorites_service = Arc::new(FavoritesService::new( + pool.clone() + )); + + tracing::info!("Favorites service initialized successfully"); + Some(favorites_service) + } else { + tracing::info!("Favorites service is disabled (requires database connection)"); + None + }; + + // Initialize recent items service if database is available + let recent_service: Option> = + if let Some(pool) = db_pool_ref { + // Create a new service with the database pool + let service = Arc::new(application::services::recent_service::RecentService::new( + pool.clone(), + 50 // Maximum recent items per user + )); + + tracing::info!("Recent items service initialized successfully"); + Some(service) + } else { + tracing::info!("Recent items service is disabled (requires database connection)"); + None + }; + + // For now, we'll use a placeholder for the contact service + // Instead of using the real PostgreSQL repositories, we'll create a dummy implementation + // This makes the code compile, and we can replace it with the real implementation later + let contact_service: Option> = None; + + let application_services = common::di::ApplicationServices { + // Concrete types for backward compatibility with handlers + folder_service_concrete: folder_service.clone(), + file_service_concrete: file_service.clone(), + // Trait objects for abstraction + folder_service: folder_service.clone(), + file_service: file_service.clone(), + file_upload_service: Arc::new(application::services::file_upload_service::FileUploadService::new( + Arc::new(infrastructure::repositories::FileFsWriteRepository::default_stub()) + )), + file_retrieval_service: Arc::new(application::services::file_retrieval_service::FileRetrievalService::new( + Arc::new(infrastructure::repositories::FileFsReadRepository::default_stub()) + )), + file_management_service: Arc::new(application::services::file_management_service::FileManagementService::new( + Arc::new(infrastructure::repositories::FileFsWriteRepository::default_stub()) + )), + file_use_case_factory: Arc::new(application::services::file_use_case_factory::AppFileUseCaseFactory::new( + Arc::new(infrastructure::repositories::FileFsReadRepository::default_stub()), + Arc::new(infrastructure::repositories::FileFsWriteRepository::default_stub()) + )), + i18n_service: i18n_service.clone(), + trash_service: trash_service.clone(), + search_service: search_service.clone(), + share_service: share_service.clone(), + favorites_service: favorites_service.clone(), + recent_service: recent_service.clone() + }; + + // Create the AppState without Arc first + let calendar_service_option = None; + + let mut app_state = AppState { + core: core_services, + repositories: repository_services, + applications: application_services, + db_pool: db_pool.clone(), + auth_service: auth_services.clone(), + trash_service: trash_service.clone(), + share_service: share_service.clone(), + favorites_service: favorites_service.clone(), + recent_service: recent_service.clone(), + storage_usage_service: None, + calendar_service: calendar_service_option, + contact_service: contact_service.clone(), + }; + + // Initialize storage usage service + let _storage_usage_service = if let Some(pool) = db_pool_ref { + // Create a user repository that implements UserStoragePort + let user_repository = Arc::new( + infrastructure::repositories::pg::UserPgRepository::new(pool.clone()) + ); + + // Create storage usage service that uses database for user information + // and file repository for storage calculation + let service = Arc::new(application::services::storage_usage_service::StorageUsageService::new( + file_repository.clone(), + user_repository, + )); + + tracing::info!("Storage usage service initialized successfully"); + + // Add the service to the app state + app_state = app_state.with_storage_usage_service(service.clone()); + + Some(service) + } else { + tracing::info!("Storage usage service is disabled (requires database connection)"); + None + }; + + // Wrap in Arc after all modifications + let app_state = Arc::new(app_state); + + // Build application router + let api_routes = create_api_routes(folder_service, file_service, Some(i18n_service), trash_service, search_service, share_service, favorites_service, recent_service); + let web_routes = create_web_routes(); + + // Build the app router + // Import auth handler + use interfaces::api::handlers::auth_handler::auth_routes; + + // Create basic app router + let mut app = Router::new() + .nest("/api", api_routes) + .merge(web_routes) + .layer(TraceLayer::new_for_http()); + + // Add auth routes if auth is enabled + if config.features.enable_auth && auth_services.is_some() { + // Create auth routes with app state + let auth_router = auth_routes().with_state(app_state.clone()); + + // Add auth routes at /api/auth + app = app.nest("/api/auth", auth_router); + } + + // Preload common directories to warm the cache + tracing::info!("Preloading common directories to warm up cache..."); + if let Ok(count) = metadata_cache.preload_directory(&storage_path, true, 1).await { + tracing::info!("Preloaded {} directory entries into cache", count); + } + + // Start server with clear message + let addr = SocketAddr::from(([0, 0, 0, 0], 8086)); + tracing::info!("Starting OxiCloud server on http://{}", addr); + + // Start the server + tracing::info!("Authentication system initialized successfully"); + + // Import the redirect middleware + use crate::interfaces::middleware::redirect::redirect_middleware; + + // Apply the redirect middleware to handle legacy routes + app = app.layer(axum::middleware::from_fn(redirect_middleware)); + + // Create a standard TCP listener + let listener = tokio::net::TcpListener::bind(addr).await?; + tracing::info!("Server binding to http://{}", addr); + tracing::info!("Starting server with Axum routes..."); + + // Axum 0.8 requires the state to match the expected type + // Extract the state from Arc so we can pass it to the router + let app_state_inner = Arc::try_unwrap(app_state) + .unwrap_or_else(|arc| (*arc).clone()); + + // Add global state to the router + let app = app.with_state(app_state_inner); + + // Use axum's serve function with the router with state + axum::serve(listener, app).await?; + + tracing::info!("Server shutdown completed"); + + Ok(()) +} +