From 70932900cde31a0eec9068da33e86b49e417a517 Mon Sep 17 00:00:00 2001 From: lonelyhentxi Date: Wed, 1 Jan 2025 07:10:18 +0800 Subject: [PATCH] fix: fix qbit torrent test --- .gitignore | 5 +- .vscode/settings.json | 3 +- Cargo.lock | 271 +++++++++++++++--- Cargo.toml | 8 +- apps/recorder/Cargo.toml | 30 +- apps/recorder/src/extract/mikan/constants.rs | 1 - apps/recorder/src/extract/mikan/mod.rs | 2 +- apps/recorder/src/extract/mikan/rss_parser.rs | 26 +- apps/recorder/src/fetch/client.rs | 6 + apps/recorder/src/lib.rs | 5 +- .../src => apps/recorder/src/sync}/core.rs | 42 +-- .../src => apps/recorder/src/sync}/error.rs | 0 .../lib.rs => apps/recorder/src/sync/mod.rs | 5 +- .../src => apps/recorder/src/sync}/qbit.rs | 207 ++++++++----- apps/recorder/src/test_utils.rs | 107 +++++++ packages/dlsignal/.gitignore | 17 -- packages/dlsignal/Cargo.toml | 38 --- packages/quirks-path/Cargo.toml | 4 + packages/quirks-path/src/lib.rs | 4 +- packages/quirks-path/src/url.rs | 54 +++- patches/.gitkeep | 0 21 files changed, 625 insertions(+), 210 deletions(-) rename {packages/dlsignal/src => apps/recorder/src/sync}/core.rs (87%) rename {packages/dlsignal/src => apps/recorder/src/sync}/error.rs (100%) rename packages/dlsignal/src/lib.rs => apps/recorder/src/sync/mod.rs (64%) rename {packages/dlsignal/src => apps/recorder/src/sync}/qbit.rs (75%) create mode 100644 apps/recorder/src/test_utils.rs delete mode 100644 packages/dlsignal/.gitignore delete mode 100644 packages/dlsignal/Cargo.toml create mode 100644 patches/.gitkeep diff --git a/.gitignore b/.gitignore index 9bcd4eb..85a95f3 100644 --- a/.gitignore +++ b/.gitignore @@ -255,4 +255,7 @@ public/robots.txt public/sitemap*.xml # Custom -/data \ No newline at end of file +/data + +patches/* +!patches/.gitkeep \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 05e54d8..0c63a11 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -28,5 +28,6 @@ "emmet.showExpandedAbbreviation": "never", "prettier.enable": false, "tailwindCSS.experimental.configFile": "./packages/tailwind-config/config.ts", - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.tsdk": "node_modules/typescript/lib", + "rust-analyzer.cargo.features": ["testcontainers"] } diff --git a/Cargo.lock b/Cargo.lock index 590de9f..dda531b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -559,9 +559,9 @@ dependencies = [ [[package]] name = "bollard" -version = "0.17.1" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d41711ad46fda47cd701f6908e59d1bd6b9a2b7464c0d0aeab95c6d37096ff8a" +checksum = "97ccca1260af6a459d75994ad5acc1651bcabcbdbc41467cc9786519ab854c30" dependencies = [ "base64 0.22.1", "bollard-stubs", @@ -588,7 +588,7 @@ dependencies = [ "serde_json", "serde_repr", "serde_urlencoded", - "thiserror 1.0.69", + "thiserror 2.0.9", "tokio", "tokio-util", "tower-service", @@ -598,9 +598,9 @@ dependencies = [ [[package]] name = "bollard-stubs" -version = "1.45.0-rc.26.0.1" +version = "1.47.1-rc.27.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7c5415e3a6bc6d3e99eff6268e488fd4ee25e7b28c10f08fa6760bd9de16e4" +checksum = "3f179cfbddb6e77a5472703d4b30436bff32929c0aa8a9008ecf23d1d3cdd0da" dependencies = [ "serde", "serde_repr", @@ -896,6 +896,21 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "conquer-once" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d008a441c0f269f36ca13712528069a86a3e60dffee1d98b976eb3b0b2160b4" +dependencies = [ + "conquer-util", +] + +[[package]] +name = "conquer-util" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e763eef8846b13b380f37dfecda401770b0ca4e56e95170237bd7c25c7db3582" + [[package]] name = "console" version = "0.15.10" @@ -965,6 +980,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -1371,30 +1396,6 @@ dependencies = [ "syn 2.0.92", ] -[[package]] -name = "dlsignal" -version = "0.1.0" -dependencies = [ - "async-trait", - "bytes", - "chrono", - "eyre", - "futures", - "itertools 0.13.0", - "lazy_static", - "librqbit-core", - "qbit-rs", - "quirks_path", - "regex", - "reqwest", - "serde", - "testcontainers", - "testcontainers-modules", - "thiserror 2.0.9", - "tokio", - "url", -] - [[package]] name = "docker_credential" version = "1.3.1" @@ -1504,6 +1505,18 @@ dependencies = [ "regex", ] +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.92", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -1985,6 +1998,51 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hickory-proto" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447afdcdb8afb9d0a852af6dc65d9b285ce720ed7a59e42a8bf2e931c67bc1b5" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna 1.0.3", + "ipnet", + "once_cell", + "rand", + "thiserror 1.0.69", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2e2aba9c389ce5267d31cf1e4dace82390ae276b0b364ea55630b1fa1b44b4" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "lru-cache", + "once_cell", + "parking_lot 0.12.3", + "rand", + "resolv-conf", + "smallvec", + "thiserror 1.0.69", + "tokio", + "tracing", +] + [[package]] name = "hkdf" version = "0.12.4" @@ -2012,6 +2070,17 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + [[package]] name = "hostname" version = "0.4.0" @@ -2171,6 +2240,7 @@ dependencies = [ "hyper", "hyper-util", "rustls", + "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls", @@ -2514,6 +2584,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2", + "widestring", + "windows-sys 0.48.0", + "winreg", +] + [[package]] name = "ipnet" version = "2.10.1" @@ -2579,6 +2661,17 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +[[package]] +name = "java-properties" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37bf6f484471c451f2b51eabd9e66b3fa7274550c5ec4b6c3d6070840945117f" +dependencies = [ + "encoding_rs", + "lazy_static", + "regex", +] + [[package]] name = "jobserver" version = "0.1.32" @@ -2808,7 +2901,7 @@ dependencies = [ "fastrand", "futures-io", "futures-util", - "hostname", + "hostname 0.4.0", "httpdate", "idna 1.0.3", "mime", @@ -3079,6 +3172,15 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "mac" version = "0.1.1" @@ -3105,6 +3207,12 @@ dependencies = [ "tendril", ] +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + [[package]] name = "matchers" version = "0.1.0" @@ -3251,7 +3359,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -4054,7 +4162,7 @@ dependencies = [ [[package]] name = "qbit-rs" version = "0.4.6" -source = "git+https://github.com/lonelyhentxi/qbit.git?rev=a2c70aa#a2c70aa391d5edc2ab79c92fa8dcfec00d0d714b" +source = "git+https://github.com/lonelyhentxi/qbit.git?rev=72d53138ebe#72d53138ebe1e3de49be46edc213ea9cb7345e55" dependencies = [ "bytes", "mod_use", @@ -4086,6 +4194,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quick-xml" version = "0.36.2" @@ -4166,6 +4280,7 @@ dependencies = [ "percent-encoding", "serde", "thiserror 1.0.69", + "tracing", "url", ] @@ -4253,26 +4368,31 @@ dependencies = [ name = "recorder" version = "0.1.0" dependencies = [ + "anyhow", "async-trait", "axum", "axum-auth", + "bollard", "bytes", "chrono", - "dlsignal", "eyre", "fancy-regex", "figment", + "futures", "html-escape", "insta", "itertools 0.13.0", "jwt-authorizer", "lazy_static", "leaky-bucket", + "librqbit-core", "lightningcss", "loco-rs", + "log", "maplit", "once_cell", "opendal", + "qbit-rs", "quirks_path", "regex", "reqwest", @@ -4287,6 +4407,8 @@ dependencies = [ "serde_json", "serde_with", "serial_test", + "testcontainers", + "testcontainers-modules", "thiserror 2.0.9", "tokio", "tracing", @@ -4419,6 +4541,7 @@ dependencies = [ "futures-core", "futures-util", "h2", + "hickory-resolver", "http 1.2.0", "http-body", "http-body-util", @@ -4437,6 +4560,7 @@ dependencies = [ "pin-project-lite", "quinn", "rustls", + "rustls-native-certs", "rustls-pemfile", "rustls-pki-types", "serde", @@ -4522,6 +4646,16 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "resolv-conf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +dependencies = [ + "hostname 0.3.1", + "quick-error", +] + [[package]] name = "retry-policies" version = "0.4.0" @@ -4709,15 +4843,14 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.7.3" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" dependencies = [ "openssl-probe", - "rustls-pemfile", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 3.1.0", ] [[package]] @@ -5015,7 +5148,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.6.0", - "core-foundation", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81d3f8c9bfcc3cbb6b0179eb57042d75b1582bdc65c3cb95f3fa999509c03cbc" +dependencies = [ + "bitflags 2.6.0", + "core-foundation 0.10.0", "core-foundation-sys", "libc", "security-framework-sys", @@ -5065,6 +5211,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-java-properties" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8b5db85b934578ef8a8acc8ef7956b313d9e920d4d4160ef7862bd4c85d4bc7" +dependencies = [ + "encoding_rs", + "java-properties", + "serde", +] + [[package]] name = "serde-value" version = "0.7.0" @@ -5295,6 +5452,16 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -5834,7 +6001,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.6.0", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -5920,13 +6087,13 @@ dependencies = [ [[package]] name = "testcontainers" version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f40cc2bd72e17f328faf8ca7687fe337e61bccd8acf9674fa78dd3792b045e1" +source = "git+https://github.com/testcontainers/testcontainers-rs.git?rev=af21727#af2172714bbb79c6ce648b699135922f85cafc0c" dependencies = [ "async-trait", "bollard", "bollard-stubs", "bytes", + "conquer-once", "docker_credential", "either", "etcetera", @@ -5935,14 +6102,18 @@ dependencies = [ "memchr", "parse-display", "pin-project-lite", + "reqwest", "serde", + "serde-java-properties", "serde_json", "serde_with", - "thiserror 1.0.69", + "signal-hook", + "thiserror 2.0.9", "tokio", "tokio-stream", "tokio-tar", "tokio-util", + "ulid", "url", ] @@ -6831,6 +7002,12 @@ dependencies = [ "wasite", ] +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + [[package]] name = "winapi" version = "0.3.9" @@ -7068,6 +7245,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "write16" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index 9492ba0..859e345 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,9 @@ [workspace] -members = ["apps/recorder", "packages/quirks-path", "packages/dlsignal"] +members = ["apps/recorder", "packages/quirks-path"] resolver = "2" + +[patch.crates-io] +testcontainers = { git = "https://github.com/testcontainers/testcontainers-rs.git", rev = "af21727" } + +# [patch."https://github.com/lonelyhentxi/qbit.git"] +# qbit-rs = { path = "./patches/qbit-rs" } diff --git a/apps/recorder/Cargo.toml b/apps/recorder/Cargo.toml index 8336fa5..a2eb6ce 100644 --- a/apps/recorder/Cargo.toml +++ b/apps/recorder/Cargo.toml @@ -13,14 +13,21 @@ name = "recorder_cli" path = "src/bin/main.rs" required-features = [] +[features] +default = [] +testcontainers = [ + "dep:testcontainers", + "dep:testcontainers-modules", + "dep:bollard", +] + [dependencies] quirks_path = { path = "../../packages/quirks-path" } -dlsignal = { path = "../../packages/dlsignal" } loco-rs = { version = "0.13" } serde = { version = "1", features = ["derive"] } serde_json = "1" eyre = "0.6" -tokio = { version = "1.42", default-features = false } +tokio = { version = "1.42", features = ["macros", "fs", "rt-multi-thread"] } async-trait = "0.1.83" tracing = "0.1" chrono = "0.4" @@ -60,6 +67,25 @@ leaky-bucket = "1.1.2" serde_with = "3" jwt-authorizer = "0.15.0" axum-auth = "0.7.0" +futures = "0.3.31" +librqbit-core = "4" +qbit-rs = { git = "https://github.com/lonelyhentxi/qbit.git", rev = "72d53138ebe", features = [ + "default", + "builder", +] } +testcontainers = { version = "0.23.1", features = [ + "default", + "properties-config", + "watchdog", + "http_wait", + "reusable-containers", +], optional = true } +testcontainers-modules = { version = "0.11.4", optional = true } + +log = "0.4.22" +anyhow = "1.0.95" +bollard = { version = "0.18", optional = true } + [dev-dependencies] serial_test = "3" diff --git a/apps/recorder/src/extract/mikan/constants.rs b/apps/recorder/src/extract/mikan/constants.rs index b8fb392..35ceb3d 100644 --- a/apps/recorder/src/extract/mikan/constants.rs +++ b/apps/recorder/src/extract/mikan/constants.rs @@ -1,4 +1,3 @@ pub const MIKAN_BUCKET_KEY: &str = "mikan"; -pub const MIKAN_BASE_URL: &str = "https://mikanani.me/"; pub const MIKAN_UNKNOWN_FANSUB_NAME: &str = "生肉/不明字幕"; pub const MIKAN_UNKNOWN_FANSUB_ID: &str = "202"; diff --git a/apps/recorder/src/extract/mikan/mod.rs b/apps/recorder/src/extract/mikan/mod.rs index 43e9f08..cdad84d 100644 --- a/apps/recorder/src/extract/mikan/mod.rs +++ b/apps/recorder/src/extract/mikan/mod.rs @@ -6,7 +6,7 @@ pub mod web_parser; pub use client::{AppMikanClient, AppMikanClientInitializer}; pub use config::AppMikanConfig; -pub use constants::{MIKAN_BASE_URL, MIKAN_BUCKET_KEY}; +pub use constants::MIKAN_BUCKET_KEY; pub use rss_parser::{ build_mikan_bangumi_rss_link, build_mikan_subscriber_aggregation_rss_link, parse_mikan_bangumi_id_from_rss_link, parse_mikan_rss_channel_from_rss_link, diff --git a/apps/recorder/src/extract/mikan/rss_parser.rs b/apps/recorder/src/extract/mikan/rss_parser.rs index 0123ad1..13513af 100644 --- a/apps/recorder/src/extract/mikan/rss_parser.rs +++ b/apps/recorder/src/extract/mikan/rss_parser.rs @@ -1,17 +1,22 @@ use std::ops::Deref; use chrono::DateTime; -use dlsignal::core::BITTORRENT_MIME_TYPE; use itertools::Itertools; use reqwest::IntoUrl; use serde::{Deserialize, Serialize}; use url::Url; -use super::{ - web_parser::{parse_mikan_episode_id_from_homepage, MikanEpisodeHomepage}, - AppMikanClient, +use crate::{ + extract::{ + errors::ParseError, + mikan::{ + web_parser::{parse_mikan_episode_id_from_homepage, MikanEpisodeHomepage}, + AppMikanClient, + }, + }, + fetch::bytes::fetch_bytes, + sync::core::BITTORRENT_MIME_TYPE, }; -use crate::{extract::errors::ParseError, fetch::bytes::fetch_bytes}; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct MikanRssItem { @@ -297,11 +302,12 @@ pub async fn parse_mikan_rss_channel_from_rss_link( mod tests { use std::assert_matches::assert_matches; - use dlsignal::core::BITTORRENT_MIME_TYPE; - - use crate::extract::mikan::{ - parse_mikan_rss_channel_from_rss_link, MikanBangumiAggregationRssChannel, - MikanBangumiRssChannel, MikanRssChannel, + use crate::{ + extract::mikan::{ + parse_mikan_rss_channel_from_rss_link, MikanBangumiAggregationRssChannel, + MikanBangumiRssChannel, MikanRssChannel, + }, + sync::core::BITTORRENT_MIME_TYPE, }; #[tokio::test] diff --git a/apps/recorder/src/fetch/client.rs b/apps/recorder/src/fetch/client.rs index 51031a5..dd2e6b7 100644 --- a/apps/recorder/src/fetch/client.rs +++ b/apps/recorder/src/fetch/client.rs @@ -31,6 +31,12 @@ pub struct HttpClient { pub config: HttpClientConfig, } +impl Into for HttpClient { + fn into(self) -> ClientWithMiddleware { + self.client + } +} + impl Deref for HttpClient { type Target = ClientWithMiddleware; diff --git a/apps/recorder/src/lib.rs b/apps/recorder/src/lib.rs index 59e37f1..d780562 100644 --- a/apps/recorder/src/lib.rs +++ b/apps/recorder/src/lib.rs @@ -1,6 +1,7 @@ #![feature(duration_constructors, assert_matches, unboxed_closures)] pub mod app; +pub mod auth; pub mod config; pub mod controllers; pub mod dal; @@ -8,7 +9,9 @@ pub mod extract; pub mod fetch; pub mod migrations; pub mod models; +pub mod sync; pub mod tasks; +#[cfg(test)] +pub mod test_utils; pub mod views; pub mod workers; -pub mod auth; diff --git a/packages/dlsignal/src/core.rs b/apps/recorder/src/sync/core.rs similarity index 87% rename from packages/dlsignal/src/core.rs rename to apps/recorder/src/sync/core.rs index 215af24..98cdeca 100644 --- a/packages/dlsignal/src/core.rs +++ b/apps/recorder/src/sync/core.rs @@ -1,4 +1,5 @@ -use bytes::Bytes; +use std::fmt::Debug; + use itertools::Itertools; use lazy_static::lazy_static; use librqbit_core::{ @@ -7,23 +8,14 @@ use librqbit_core::{ }; use quirks_path::{Path, PathBuf}; use regex::Regex; -use reqwest::IntoUrl; use serde::{Deserialize, Serialize}; use url::Url; -use crate::{QbitTorrent, QbitTorrentContent, TorrentDownloadError}; +use super::{QbitTorrent, QbitTorrentContent, TorrentDownloadError}; +use crate::fetch::{fetch_bytes, HttpClient}; pub const BITTORRENT_MIME_TYPE: &str = "application/x-bittorrent"; pub const MAGNET_SCHEMA: &str = "magnet"; -pub const DEFAULT_TORRENT_USER_AGENT: &str = "Wget/1.13.4 (linux-gnu)"; - -async fn download_torrent_file(url: T) -> eyre::Result { - let request_client = reqwest::Client::builder() - .user_agent(DEFAULT_TORRENT_USER_AGENT) - .build()?; - let bytes = request_client.get(url).send().await?.bytes().await?; - Ok(bytes) -} #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -46,7 +38,7 @@ lazy_static! { static ref TORRENT_EXT_RE: Regex = Regex::new(r"\.torrent$").unwrap(); } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Clone, PartialEq, Eq)] pub enum TorrentSource { MagnetUrl { url: Url, @@ -64,7 +56,7 @@ pub enum TorrentSource { } impl TorrentSource { - pub async fn parse(url: &str) -> eyre::Result { + pub async fn parse(client: Option<&HttpClient>, url: &str) -> eyre::Result { let url = Url::parse(url)?; let source = if url.scheme() == MAGNET_SCHEMA { TorrentSource::from_magnet_url(url)? @@ -79,11 +71,11 @@ impl TorrentSource { ) { TorrentSource::from_torrent_url(url, match_hash.as_str().to_string())? } else { - let contents = download_torrent_file(url).await?; + let contents = fetch_bytes(client, url).await?; TorrentSource::from_torrent_file(contents.to_vec(), Some(basename.to_string()))? } } else { - let contents = download_torrent_file(url).await?; + let contents = fetch_bytes(client, url).await?; TorrentSource::from_torrent_file(contents.to_vec(), None)? }; Ok(source) @@ -137,6 +129,24 @@ impl TorrentSource { } } +impl Debug for TorrentSource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TorrentSource::MagnetUrl { url, .. } => { + write!(f, "MagnetUrl {{ url: {} }}", url.as_str()) + } + TorrentSource::TorrentUrl { url, .. } => { + write!(f, "TorrentUrl {{ url: {} }}", url.as_str()) + } + TorrentSource::TorrentFile { name, hash, .. } => write!( + f, + "TorrentFile {{ name: \"{}\", hash: \"{hash}\" }}", + name.as_deref().unwrap_or_default() + ), + } + } +} + pub trait TorrentContent { fn get_name(&self) -> &str; diff --git a/packages/dlsignal/src/error.rs b/apps/recorder/src/sync/error.rs similarity index 100% rename from packages/dlsignal/src/error.rs rename to apps/recorder/src/sync/error.rs diff --git a/packages/dlsignal/src/lib.rs b/apps/recorder/src/sync/mod.rs similarity index 64% rename from packages/dlsignal/src/lib.rs rename to apps/recorder/src/sync/mod.rs index e324d1a..ba69ae7 100644 --- a/packages/dlsignal/src/lib.rs +++ b/apps/recorder/src/sync/mod.rs @@ -2,7 +2,10 @@ pub mod core; pub mod error; pub mod qbit; -pub use core::{Torrent, TorrentContent, TorrentDownloader, TorrentFilter, TorrentSource}; +pub use core::{ + Torrent, TorrentContent, TorrentDownloader, TorrentFilter, TorrentSource, BITTORRENT_MIME_TYPE, + MAGNET_SCHEMA, +}; pub use error::TorrentDownloadError; pub use qbit::{ diff --git a/packages/dlsignal/src/qbit.rs b/apps/recorder/src/sync/qbit.rs similarity index 75% rename from packages/dlsignal/src/qbit.rs rename to apps/recorder/src/sync/qbit.rs index 1a48397..d72d83b 100644 --- a/packages/dlsignal/src/qbit.rs +++ b/apps/recorder/src/sync/qbit.rs @@ -14,9 +14,10 @@ use qbit_rs::{ }; use quirks_path::{path_equals_as_file_url, Path, PathBuf}; use tokio::time::sleep; +use tracing::instrument; use url::Url; -use crate::{Torrent, TorrentDownloadError, TorrentDownloader, TorrentFilter, TorrentSource}; +use super::{Torrent, TorrentDownloadError, TorrentDownloader, TorrentFilter, TorrentSource}; impl From for QbitTorrentSource { fn from(value: TorrentSource) -> Self { @@ -81,7 +82,9 @@ impl QBittorrentDownloader { ) -> Result { let endpoint_url = Url::parse(&creation.endpoint).map_err(TorrentDownloadError::InvalidUrlParse)?; + let credential = Credential::new(creation.username, creation.password); + let client = Qbit::new(endpoint_url.clone(), credential); client @@ -100,6 +103,7 @@ impl QBittorrentDownloader { }) } + #[instrument(level = "debug")] pub async fn api_version(&self) -> eyre::Result { let result = self.client.get_webapi_version().await?; Ok(result) @@ -116,8 +120,9 @@ impl QBittorrentDownloader { H: FnOnce() -> E, G: Fn(Arc, E) -> Fut, Fut: Future>, - F: FnMut(D) -> bool, + F: FnMut(&D) -> bool, E: Clone, + D: Debug + serde::Serialize, { let mut next_wait_ms = 32u64; let mut all_wait_ms = 0u64; @@ -129,9 +134,10 @@ impl QBittorrentDownloader { if all_wait_ms >= timeout.as_millis() as u64 { // full update let sync_data = fetch_data_fn(self.client.clone(), env.clone()).await?; - if stop_wait_fn(sync_data) { + if stop_wait_fn(&sync_data) { break; } else { + tracing::warn!(name = "wait_until timeout", sync_data = serde_json::to_string(&sync_data).unwrap(), timeout = ?timeout); return Err(TorrentDownloadError::TimeoutError { action: Cow::Borrowed("QBittorrentDownloader::wait_unit"), timeout, @@ -140,7 +146,7 @@ impl QBittorrentDownloader { } } let sync_data = fetch_data_fn(self.client.clone(), env.clone()).await?; - if stop_wait_fn(sync_data) { + if stop_wait_fn(&sync_data) { break; } next_wait_ms *= 2; @@ -148,6 +154,7 @@ impl QBittorrentDownloader { Ok(()) } + #[instrument(level = "trace", skip(self, stop_wait_fn))] pub async fn wait_torrents_until( &self, arg: GetTorrentListArg, @@ -155,7 +162,7 @@ impl QBittorrentDownloader { timeout: Option, ) -> eyre::Result<()> where - F: FnMut(Vec) -> bool, + F: FnMut(&Vec) -> bool, { self.wait_until( || arg, @@ -171,7 +178,8 @@ impl QBittorrentDownloader { .await } - pub async fn wait_sync_until bool>( + #[instrument(level = "debug", skip(self, stop_wait_fn))] + pub async fn wait_sync_until bool>( &self, stop_wait_fn: F, timeout: Option, @@ -188,7 +196,8 @@ impl QBittorrentDownloader { .await } - async fn wait_torrent_contents_until) -> bool>( + #[instrument(level = "debug", skip(self, stop_wait_fn))] + async fn wait_torrent_contents_until) -> bool>( &self, hash: &str, stop_wait_fn: F, @@ -211,6 +220,7 @@ impl QBittorrentDownloader { #[async_trait::async_trait] impl TorrentDownloader for QBittorrentDownloader { + #[instrument(level = "debug", skip(self))] async fn get_torrents_info( &self, status_filter: TorrentFilter, @@ -239,6 +249,7 @@ impl TorrentDownloader for QBittorrentDownloader { .collect::>()) } + #[instrument(level = "debug", skip(self))] async fn add_torrents( &self, source: TorrentSource, @@ -268,6 +279,7 @@ impl TorrentDownloader for QBittorrentDownloader { |sync_data| { sync_data .torrents + .as_ref() .is_some_and(|t| t.contains_key(source_hash)) }, None, @@ -276,6 +288,7 @@ impl TorrentDownloader for QBittorrentDownloader { Ok(()) } + #[instrument(level = "debug", skip(self))] async fn delete_torrents(&self, hashes: Vec) -> eyre::Result<()> { self.client .delete_torrents(hashes.clone(), Some(true)) @@ -291,6 +304,7 @@ impl TorrentDownloader for QBittorrentDownloader { Ok(()) } + #[instrument(level = "debug", skip(self))] async fn rename_torrent_file( &self, hash: &str, @@ -304,7 +318,11 @@ impl TorrentDownloader for QBittorrentDownloader { hash, |contents| -> bool { contents.iter().any(|c| { - path_equals_as_file_url(save_path.join(&c.name), &new_path).unwrap_or(false) + path_equals_as_file_url(save_path.join(&c.name), &new_path) + .inspect_err(|error| { + tracing::warn!(name = "path_equals_as_file_url", error = ?error); + }) + .unwrap_or(false) }) }, None, @@ -313,19 +331,23 @@ impl TorrentDownloader for QBittorrentDownloader { Ok(()) } + #[instrument(level = "debug", skip(self))] async fn move_torrents(&self, hashes: Vec, new_path: &str) -> eyre::Result<()> { self.client .set_torrent_location(hashes.clone(), new_path) .await?; + self.wait_torrents_until( GetTorrentListArg::builder() .hashes(hashes.join("|")) .build(), |torrents| -> bool { - torrents.iter().all(|t| { - t.save_path - .as_ref() - .is_some_and(|p| path_equals_as_file_url(p, new_path).unwrap_or(false)) + torrents.iter().flat_map(|t| t.save_path.as_ref()).any(|p| { + path_equals_as_file_url(p, new_path) + .inspect_err(|error| { + tracing::warn!(name = "path_equals_as_file_url", error = ?error); + }) + .unwrap_or(false) }) }, None, @@ -346,11 +368,13 @@ impl TorrentDownloader for QBittorrentDownloader { Ok(torrent.save_path.take()) } + #[instrument(level = "debug", skip(self))] async fn check_connection(&self) -> eyre::Result<()> { self.api_version().await?; Ok(()) } + #[instrument(level = "debug", skip(self))] async fn set_torrents_category(&self, hashes: Vec, category: &str) -> eyre::Result<()> { let result = self .client @@ -379,6 +403,7 @@ impl TorrentDownloader for QBittorrentDownloader { Ok(()) } + #[instrument(level = "debug", skip(self))] async fn add_torrent_tags(&self, hashes: Vec, tags: Vec) -> eyre::Result<()> { if tags.is_empty() { return Err(eyre::eyre!("add torrent tags can not be empty")); @@ -408,6 +433,7 @@ impl TorrentDownloader for QBittorrentDownloader { Ok(()) } + #[instrument(level = "debug", skip(self))] async fn add_category(&self, category: &str) -> eyre::Result<()> { self.client .add_category( @@ -419,6 +445,7 @@ impl TorrentDownloader for QBittorrentDownloader { |sync_data| { sync_data .categories + .as_ref() .is_some_and(|s| s.contains_key(category)) }, None, @@ -445,11 +472,12 @@ impl Debug for QBittorrentDownloader { #[cfg(test)] pub mod tests { use itertools::Itertools; + use testcontainers_modules::testcontainers::ImageExt; use super::*; fn get_tmp_qbit_test_folder() -> &'static str { - if cfg!(windows) { + if cfg!(all(windows, not(feature = "testcontainers"))) { "C:\\Windows\\Temp\\konobangu\\qbit" } else { "/tmp/konobangu/qbit" @@ -457,65 +485,117 @@ pub mod tests { } #[cfg(feature = "testcontainers")] - pub fn create_qbit_testcontainer() -> testcontainers::RunnableImage - { - let image = testcontainers::RunnableImage::from( - testcontainers::GenericImage::new("linuxserver/qbittorrent", "latest") - .with_wait_for(testcontainers::core::WaitFor::message_on_stderr( - "Connection to localhost", - )) - .with_env_var("WEBUI_PORT", "8080") - .with_env_var("TZ", "Asia/Singapore") - .with_env_var("TORRENTING_PORT", "6881") - .with_exposed_port(8080) - .with_exposed_port(6881), - ); + pub async fn create_qbit_testcontainer( + ) -> eyre::Result> { + use testcontainers::{ + core::{ + ContainerPort, + // ReuseDirective, + WaitFor, + }, + GenericImage, + }; - image + use crate::test_utils::testcontainers::ContainerRequestEnhancedExt; + + let container = GenericImage::new("linuxserver/qbittorrent", "latest") + .with_wait_for(WaitFor::message_on_stderr("Connection to localhost")) + .with_env_var("WEBUI_PORT", "8080") + .with_env_var("TZ", "Asia/Singapore") + .with_env_var("TORRENTING_PORT", "6881") + .with_mapped_port(6881, ContainerPort::Tcp(6881)) + .with_mapped_port(8080, ContainerPort::Tcp(8080)) + // .with_reuse(ReuseDirective::Always) + .with_default_log_consumer() + .with_prune_existed_label("qbit-downloader", true, true) + .await?; + + Ok(container) } #[cfg(not(feature = "testcontainers"))] #[tokio::test] async fn test_qbittorrent_downloader() { - test_qbittorrent_downloader_impl().await; + test_qbittorrent_downloader_impl(None, None).await; } - // @TODO: not support now, testcontainers crate not support to read logs to get - // password #[cfg(feature = "testcontainers")] - #[tokio::test] - async fn test_qbittorrent_downloader() { - let docker = testcontainers::clients::Cli::default(); - let image = create_qbit_testcontainer(); + #[tokio::test(flavor = "multi_thread")] + async fn test_qbittorrent_downloader() -> eyre::Result<()> { + use testcontainers::runners::AsyncRunner; + use tokio::io::AsyncReadExt; - let _container = docker.run(image); + tracing_subscriber::fmt() + .with_max_level(tracing::Level::DEBUG) + .with_test_writer() + .init(); - test_qbittorrent_downloader_impl().await; + let image = create_qbit_testcontainer().await?; + + let container = image.start().await?; + + let mut logs = String::new(); + + container.stdout(false).read_to_string(&mut logs).await?; + + let username = logs + .lines() + .find_map(|line| { + if line.contains("The WebUI administrator username is") { + line.split_whitespace().last() + } else { + None + } + }) + .expect("should have username") + .trim(); + + let password = logs + .lines() + .find_map(|line| { + if line.contains("A temporary password is provided for this session") { + line.split_whitespace().last() + } else { + None + } + }) + .expect("should have password") + .trim(); + + tracing::info!(username, password); + + test_qbittorrent_downloader_impl(Some(username), Some(password)).await?; + + Ok(()) } - async fn test_qbittorrent_downloader_impl() { + async fn test_qbittorrent_downloader_impl( + username: Option<&str>, + password: Option<&str>, + ) -> eyre::Result<()> { let base_save_path = Path::new(get_tmp_qbit_test_folder()); - let downloader = QBittorrentDownloader::from_creation(QBittorrentDownloaderCreation { - endpoint: "http://localhost:8080".to_string(), - password: "".to_string(), - username: "".to_string(), + let mut downloader = QBittorrentDownloader::from_creation(QBittorrentDownloaderCreation { + endpoint: "http://127.0.0.1:8080".to_string(), + password: password.unwrap_or_default().to_string(), + username: username.unwrap_or_default().to_string(), subscriber_id: 0, save_path: base_save_path.to_string(), }) - .await - .unwrap(); + .await?; - downloader.check_connection().await.unwrap(); + downloader.wait_sync_timeout = Duration::from_secs(3); + + downloader.check_connection().await?; downloader .delete_torrents(vec!["47ee2d69e7f19af783ad896541a07b012676f858".to_string()]) - .await - .unwrap(); + .await?; let torrent_source = TorrentSource::parse( + None, "https://mikanani.me/Download/20240301/47ee2d69e7f19af783ad896541a07b012676f858.torrent" - ).await.unwrap(); + ).await?; let save_path = base_save_path.join(format!( "test_add_torrents_{}", @@ -524,8 +604,7 @@ pub mod tests { downloader .add_torrents(torrent_source, save_path.to_string(), Some("bangumi")) - .await - .unwrap(); + .await?; let get_torrent = async || -> eyre::Result { let torrent_infos = downloader @@ -540,7 +619,7 @@ pub mod tests { Ok(result) }; - let target_torrent = get_torrent().await.unwrap(); + let target_torrent = get_torrent().await?; let files = target_torrent.iter_files().collect_vec(); assert!(!files.is_empty()); @@ -558,10 +637,9 @@ pub mod tests { vec!["47ee2d69e7f19af783ad896541a07b012676f858".to_string()], vec![test_tag.clone()], ) - .await - .unwrap(); + .await?; - let target_torrent = get_torrent().await.unwrap(); + let target_torrent = get_torrent().await?; assert!(target_torrent.get_tags().iter().any(|s| s == &test_tag)); @@ -572,10 +650,9 @@ pub mod tests { vec!["47ee2d69e7f19af783ad896541a07b012676f858".to_string()], &test_category, ) - .await - .unwrap(); + .await?; - let target_torrent = get_torrent().await.unwrap(); + let target_torrent = get_torrent().await?; assert_eq!(Some(test_category.as_str()), target_torrent.get_category()); @@ -589,10 +666,9 @@ pub mod tests { vec!["47ee2d69e7f19af783ad896541a07b012676f858".to_string()], moved_save_path.as_str(), ) - .await - .unwrap(); + .await?; - let target_torrent = get_torrent().await.unwrap(); + let target_torrent = get_torrent().await?; let content_path = target_torrent.iter_files().next().unwrap().get_name(); @@ -604,10 +680,9 @@ pub mod tests { content_path, new_content_path, ) - .await - .unwrap(); + .await?; - let target_torrent = get_torrent().await.unwrap(); + let target_torrent = get_torrent().await?; let content_path = target_torrent.iter_files().next().unwrap().get_name(); @@ -615,14 +690,14 @@ pub mod tests { downloader .delete_torrents(vec!["47ee2d69e7f19af783ad896541a07b012676f858".to_string()]) - .await - .unwrap(); + .await?; let torrent_infos1 = downloader .get_torrents_info(TorrentFilter::All, None, None) - .await - .unwrap(); + .await?; assert!(torrent_infos1.is_empty()); + + Ok(()) } } diff --git a/apps/recorder/src/test_utils.rs b/apps/recorder/src/test_utils.rs new file mode 100644 index 0000000..dccecf4 --- /dev/null +++ b/apps/recorder/src/test_utils.rs @@ -0,0 +1,107 @@ +#[cfg(feature = "testcontainers")] +pub mod testcontainers { + use bollard::container::ListContainersOptions; + use itertools::Itertools; + use testcontainers::{ + core::logs::consumer::logging_consumer::LoggingConsumer, ContainerRequest, Image, ImageExt, + }; + + pub const TESTCONTAINERS_PROJECT_KEY: &str = "tech.enfw.testcontainers.project"; + pub const TESTCONTAINERS_CONTAINER_KEY: &str = "tech.enfw.testcontainers.container"; + pub const TESTCONTAINERS_PRUNE_KEY: &str = "tech.enfw.testcontainers.prune"; + + #[async_trait::async_trait] + pub trait ContainerRequestEnhancedExt: Sized + ImageExt + where + I: Image, + { + async fn with_prune_existed_label( + self, + container_label: &str, + prune: bool, + force: bool, + ) -> eyre::Result; + + fn with_default_log_consumer(self) -> Self; + } + + #[async_trait::async_trait] + impl ContainerRequestEnhancedExt for ContainerRequest + where + I: Image, + { + async fn with_prune_existed_label( + self, + container_label: &str, + prune: bool, + force: bool, + ) -> eyre::Result { + use std::collections::HashMap; + + use bollard::container::PruneContainersOptions; + use testcontainers::core::client::docker_client_instance; + + if prune { + let client = docker_client_instance().await?; + + let mut filters = HashMap::>::new(); + + filters.insert( + String::from("label"), + vec![ + format!("{TESTCONTAINERS_PRUNE_KEY}=true"), + format!("{}={}", TESTCONTAINERS_PROJECT_KEY, "konobangu"), + format!("{}={}", TESTCONTAINERS_CONTAINER_KEY, container_label), + ], + ); + + if force { + let result = client + .list_containers(Some(ListContainersOptions { + all: false, + filters: filters.clone(), + ..Default::default() + })) + .await?; + + let remove_containers = result + .iter() + .filter(|c| matches!(c.state.as_deref(), Some("running"))) + .flat_map(|c| c.id.as_deref()) + .collect_vec(); + + futures::future::try_join_all( + remove_containers + .iter() + .map(|c| client.stop_container(c, None)), + ) + .await?; + + tracing::warn!(name = "stop running containers", result = ?remove_containers); + } + + let result = client + .prune_containers(Some(PruneContainersOptions { filters })) + .await?; + + tracing::warn!(name = "prune existed containers", result = ?result); + } + + let result = self.with_labels([ + (TESTCONTAINERS_PRUNE_KEY, "true"), + (TESTCONTAINERS_PROJECT_KEY, "konobangu"), + (TESTCONTAINERS_CONTAINER_KEY, container_label), + ]); + + Ok(result) + } + + fn with_default_log_consumer(self) -> Self { + self.with_log_consumer( + LoggingConsumer::new() + .with_stdout_level(log::Level::Info) + .with_stderr_level(log::Level::Error), + ) + } + } +} diff --git a/packages/dlsignal/.gitignore b/packages/dlsignal/.gitignore deleted file mode 100644 index 8861634..0000000 --- a/packages/dlsignal/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -**/config/local.yaml -**/config/*.local.yaml - -# Generated by Cargo -# will have compiled files and executables -debug/ -target/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk - -# MSVC Windows builds of rustc generate these, which store debugging information -*.pdb diff --git a/packages/dlsignal/Cargo.toml b/packages/dlsignal/Cargo.toml deleted file mode 100644 index bd3f3e1..0000000 --- a/packages/dlsignal/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -[package] -name = "dlsignal" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[lib] -name = "dlsignal" -path = "src/lib.rs" - -[features] -default = [] -testcontainers = [] - -[dependencies] -quirks_path = { path = "../quirks-path" } -async-trait = "0.1.83" -chrono = "0.4.39" -eyre = "0.6.12" -futures = "0.3.31" -itertools = "0.13.0" -lazy_static = "1.5.0" -librqbit-core = "4" -qbit-rs = { git = "https://github.com/lonelyhentxi/qbit.git", rev = "a2c70aa", features = [ - "default", - "builder", -] } -regex = "1.11.1" -serde = "1.0.216" -thiserror = "2.0.9" -tokio = "1.42.0" -url = "2.5.4" -reqwest = "0.12.11" -bytes = "1.9.0" - -[dev-dependencies] -testcontainers = { version = "0.23.1" } -testcontainers-modules = { version = "0.11.4" } diff --git a/packages/quirks-path/Cargo.toml b/packages/quirks-path/Cargo.toml index 6e434d8..da4b7b3 100644 --- a/packages/quirks-path/Cargo.toml +++ b/packages/quirks-path/Cargo.toml @@ -8,4 +8,8 @@ nom = "7.1.3" percent-encoding = "2.3.1" serde = { version = "1.0.197", features = ["derive"] } thiserror = "1.0.57" +tracing = "0.1.41" url = "2.5.0" + +[dev-dependencies] +tracing = "0.1.41" diff --git a/packages/quirks-path/src/lib.rs b/packages/quirks-path/src/lib.rs index c8d7406..f5d9040 100644 --- a/packages/quirks-path/src/lib.rs +++ b/packages/quirks-path/src/lib.rs @@ -1702,11 +1702,11 @@ mod tests { ); assert!(matches!( test_fn(r"\\?\abc\path"), - Err(PathToUrlError::UrlNotSupportedPrefix { .. }) + Err(PathToUrlError::NotSupportedPrefixError { .. }) )); assert!(matches!( test_fn(r"\\.\device\path"), - Err(PathToUrlError::UrlNotSupportedPrefix { .. }) + Err(PathToUrlError::NotSupportedPrefixError { .. }) )); assert!(matches!( test_fn(r"~/a"), diff --git a/packages/quirks-path/src/url.rs b/packages/quirks-path/src/url.rs index 6128124..801385a 100644 --- a/packages/quirks-path/src/url.rs +++ b/packages/quirks-path/src/url.rs @@ -10,12 +10,20 @@ const URL_PATH_SEGMENT: &AsciiSet = &URL_PATH.add(b'/').add(b'%'); #[derive(thiserror::Error, Debug)] pub enum PathToUrlError { - #[error("Path not absolute: {path}")] + #[error(transparent)] + UrlParseError(#[from] url::ParseError), + #[error("PathNotAbsoluteError {{ path = {path} }}")] PathNotAbsoluteError { path: Cow<'static, str> }, - #[error("Invalid UNC path")] - ParseUrlError(#[from] ::url::ParseError), - #[error("Path prefix can not be a url: {path}")] - UrlNotSupportedPrefix { path: Cow<'static, str> }, + #[error("NotSupportedPrefixError {{ path = {path}, prefix = {prefix} }}")] + NotSupportedPrefixError { + path: Cow<'static, str>, + prefix: Cow<'static, str>, + }, + #[error("NotSupportedFirstComponentError {{ path = {path}, comp = {comp} }}")] + NotSupportedFirstComponentError { + path: Cow<'static, str>, + comp: Cow<'static, str>, + }, } #[inline] @@ -60,15 +68,41 @@ pub(crate) fn path_to_file_url_segments( serialization.extend(percent_encode(share.as_bytes(), URL_PATH_SEGMENT)); } _ => { - return Err(PathToUrlError::UrlNotSupportedPrefix { + return Err(PathToUrlError::NotSupportedPrefixError { path: Cow::Owned(path.as_str().to_string()), - }) + prefix: Cow::Owned(p.as_str().to_string()), + }); } }, - _ => { - return Err(PathToUrlError::UrlNotSupportedPrefix { + Some(Component::RootDir(_)) => { + let host_end = to_u32(serialization.len()).unwrap(); + let mut empty = true; + for component in components { + empty = false; + serialization.push('/'); + + serialization.extend(percent_encode( + component.as_str().as_bytes(), + URL_PATH_SEGMENT, + )); + } + + if empty { + serialization.push('/'); + } + return Ok((host_end, None)); + } + Some(comp) => { + return Err(PathToUrlError::NotSupportedFirstComponentError { path: Cow::Owned(path.as_str().to_string()), - }) + comp: Cow::Owned(comp.as_str().to_string()), + }); + } + None => { + return Err(PathToUrlError::NotSupportedFirstComponentError { + path: Cow::Owned(path.as_str().to_string()), + comp: Cow::Borrowed("null"), + }); } } diff --git a/patches/.gitkeep b/patches/.gitkeep new file mode 100644 index 0000000..e69de29