Support requesting VFS hydration; fix Android TLS cross-compilation
Correcting the previous commit: I claimed VFS placeholders could not be
downloaded. That was wrong. The desktop client exposes a socket at
$XDG_RUNTIME_DIR/Nextcloud/socket speaking newline-delimited
COMMAND:argument, and MAKE_AVAILABLE_LOCALLY:<path> does fetch the file.
Verified against client 4.0.7: a 1-byte stub became a real 2.8MB file in
2.8 seconds.
Implemented as dr-sync-nextcloud::desktop_client, deliberately optional.
Android has no desktop client, no XDG_RUNTIME_DIR socket and no
placeholders, so detect() returns None there and callers fall back to the
connector. It earns its place only because it is ~30 lines with no
dependencies: where a library already lives in a VFS folder, asking the
client to fetch beats downloading a second copy over WebDAV and leaving
the client's placeholder state inconsistent.
What this does not change: hydration is whole-file, so it suits the
original tier and never browsing. Filling a grid this way downloads the
entire library. Range extraction remains the only mechanism satisfying
FR-NC-3, and ARCH §9.0 now says so precisely.
Also fixes two real Android build failures found by cross-compiling:
- reqwest's `rustls` feature defaults to aws-lc-rs, whose aws-lc-sys
crate is C and fails under the NDK — exactly the pain D1 chose Rust
to avoid. Switched to rustls-no-provider + ring, installing the
provider in the constructor so no caller can build a client that
panics on first use.
- ring itself needs CC/AR per target; cargo-ndk sets only the linker.
Added them to the container.
87 tests passing. dr-sync-nextcloud cross-compiles for aarch64-linux-android.
This commit is contained in:
Generated
+6
-165
@@ -452,29 +452,6 @@ dependencies = [
|
|||||||
"arrayvec",
|
"arrayvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "aws-lc-rs"
|
|
||||||
version = "1.18.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e"
|
|
||||||
dependencies = [
|
|
||||||
"aws-lc-sys",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "aws-lc-sys"
|
|
||||||
version = "0.44.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
"cmake",
|
|
||||||
"dunce",
|
|
||||||
"fs_extra",
|
|
||||||
"pkg-config",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backtrace"
|
name = "backtrace"
|
||||||
version = "0.3.76"
|
version = "0.3.76"
|
||||||
@@ -783,17 +760,6 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "chacha20"
|
|
||||||
version = "0.10.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"cpufeatures",
|
|
||||||
"rand_core 0.10.1",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.45"
|
version = "0.4.45"
|
||||||
@@ -836,15 +802,6 @@ dependencies = [
|
|||||||
"hashbrown 0.16.1",
|
"hashbrown 0.16.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cmake"
|
|
||||||
version = "0.1.58"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "codespan-reporting"
|
name = "codespan-reporting"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@@ -995,15 +952,6 @@ version = "3.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
|
checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cpufeatures"
|
|
||||||
version = "0.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc32fast"
|
name = "crc32fast"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -1260,9 +1208,11 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"dr-sync",
|
"dr-sync",
|
||||||
"dr-types",
|
"dr-types",
|
||||||
|
"env_logger",
|
||||||
"log",
|
"log",
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"rustls",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
@@ -1338,12 +1288,6 @@ version = "0.8.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "edf234dd1594d6dd434a8fb8cada51ddbbc593e40e4a01556a0b31c62da2775b"
|
checksum = "edf234dd1594d6dd434a8fb8cada51ddbbc593e40e4a01556a0b31c62da2775b"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dunce"
|
|
||||||
version = "1.0.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.17.0"
|
version = "1.17.0"
|
||||||
@@ -1699,12 +1643,6 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fs_extra"
|
|
||||||
version = "1.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures"
|
name = "futures"
|
||||||
version = "0.3.33"
|
version = "0.3.33"
|
||||||
@@ -1854,10 +1792,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi",
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1879,11 +1815,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi 6.0.0",
|
"r-efi 6.0.0",
|
||||||
"rand_core 0.10.1",
|
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3379,12 +3312,6 @@ dependencies = [
|
|||||||
"imgref",
|
"imgref",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lru-slab"
|
|
||||||
version = "0.1.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lyon_algorithms"
|
name = "lyon_algorithms"
|
||||||
version = "1.0.20"
|
version = "1.0.20"
|
||||||
@@ -4589,63 +4516,6 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quinn"
|
|
||||||
version = "0.11.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"cfg_aliases 0.2.2",
|
|
||||||
"pin-project-lite",
|
|
||||||
"quinn-proto",
|
|
||||||
"quinn-udp",
|
|
||||||
"rustc-hash 2.1.3",
|
|
||||||
"rustls",
|
|
||||||
"socket2",
|
|
||||||
"thiserror 2.0.20",
|
|
||||||
"tokio",
|
|
||||||
"tracing",
|
|
||||||
"web-time",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quinn-proto"
|
|
||||||
version = "0.11.16"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560"
|
|
||||||
dependencies = [
|
|
||||||
"aws-lc-rs",
|
|
||||||
"bytes",
|
|
||||||
"getrandom 0.4.3",
|
|
||||||
"lru-slab",
|
|
||||||
"rand 0.10.2",
|
|
||||||
"rand_pcg",
|
|
||||||
"ring",
|
|
||||||
"rustc-hash 2.1.3",
|
|
||||||
"rustls",
|
|
||||||
"rustls-pki-types",
|
|
||||||
"slab",
|
|
||||||
"thiserror 2.0.20",
|
|
||||||
"tinyvec",
|
|
||||||
"tracing",
|
|
||||||
"web-time",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quinn-udp"
|
|
||||||
version = "0.5.15"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
|
|
||||||
dependencies = [
|
|
||||||
"cfg_aliases 0.2.2",
|
|
||||||
"libc",
|
|
||||||
"once_cell",
|
|
||||||
"socket2",
|
|
||||||
"tracing",
|
|
||||||
"windows-sys 0.61.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.47"
|
version = "1.0.47"
|
||||||
@@ -4674,18 +4544,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
|
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand_chacha",
|
"rand_chacha",
|
||||||
"rand_core 0.9.5",
|
"rand_core",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand"
|
|
||||||
version = "0.10.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
|
|
||||||
dependencies = [
|
|
||||||
"chacha20",
|
|
||||||
"getrandom 0.4.3",
|
|
||||||
"rand_core 0.10.1",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4695,7 +4554,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ppv-lite86",
|
"ppv-lite86",
|
||||||
"rand_core 0.9.5",
|
"rand_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4707,21 +4566,6 @@ dependencies = [
|
|||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand_core"
|
|
||||||
version = "0.10.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand_pcg"
|
|
||||||
version = "0.10.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
|
|
||||||
dependencies = [
|
|
||||||
"rand_core 0.10.1",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "range-alloc"
|
name = "range-alloc"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -4755,7 +4599,7 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
"paste",
|
"paste",
|
||||||
"profiling",
|
"profiling",
|
||||||
"rand 0.9.5",
|
"rand",
|
||||||
"rand_chacha",
|
"rand_chacha",
|
||||||
"simd_helpers",
|
"simd_helpers",
|
||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
@@ -4968,7 +4812,6 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"quinn",
|
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"rustls-platform-verifier",
|
"rustls-platform-verifier",
|
||||||
@@ -5127,8 +4970,8 @@ version = "0.23.43"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
|
checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-rs",
|
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"ring",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"rustls-webpki",
|
"rustls-webpki",
|
||||||
"subtle",
|
"subtle",
|
||||||
@@ -5153,7 +4996,6 @@ version = "1.15.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
|
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"web-time",
|
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -5190,7 +5032,6 @@ version = "0.103.13"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-rs",
|
|
||||||
"ring",
|
"ring",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"untrusted",
|
"untrusted",
|
||||||
|
|||||||
+10
-5
@@ -41,11 +41,16 @@ pollster = "0.4"
|
|||||||
|
|
||||||
# Networking — no mature Nextcloud crate exists; the connector is hand-rolled
|
# Networking — no mature Nextcloud crate exists; the connector is hand-rolled
|
||||||
# over reqwest (D7). reqwest_dav was evaluated and is too thin to build on.
|
# over reqwest (D7). reqwest_dav was evaluated and is too thin to build on.
|
||||||
# `rustls` (not `rustls-tls` — renamed in 0.13) pulls in
|
# `rustls-no-provider` rather than `rustls`: the latter defaults to the
|
||||||
# rustls-platform-verifier, which crashes on Android unless initialised from
|
# aws-lc-rs crypto provider, whose aws-lc-sys crate is C and fails to
|
||||||
# Kotlin. That is spike S3, and D7 records `tls_certs_only` + webpki-roots as
|
# cross-compile for Android — precisely the NDK pain D1 chose Rust to avoid.
|
||||||
# the escape hatch.
|
# ring is pure Rust apart from a small asm core that does build under the NDK.
|
||||||
reqwest = { version = "0.13", default-features = false, features = ["rustls", "stream", "json"] }
|
#
|
||||||
|
# Note this still pulls rustls-platform-verifier, which crashes on Android
|
||||||
|
# unless initialised from Kotlin (spike S3). D7 records `tls_certs_only` plus
|
||||||
|
# webpki-roots as the escape hatch.
|
||||||
|
reqwest = { version = "0.13", default-features = false, features = ["rustls-no-provider", "stream", "json"] }
|
||||||
|
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
|
||||||
quick-xml = "0.41"
|
quick-xml = "0.41"
|
||||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time"] }
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time"] }
|
||||||
url = "2.5"
|
url = "2.5"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ license.workspace = true
|
|||||||
dr-types.workspace = true
|
dr-types.workspace = true
|
||||||
dr-sync.workspace = true
|
dr-sync.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
|
rustls.workspace = true
|
||||||
quick-xml.workspace = true
|
quick-xml.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
@@ -21,3 +22,4 @@ tokio = { workspace = true }
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
|
env_logger.workspace = true
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
//! Request hydration of a VFS placeholder via the desktop client.
|
||||||
|
//!
|
||||||
|
//! cargo run -p dr-sync-nextcloud --example hydrate -- <file.ext.nextcloud>
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use dr_sync_nextcloud::desktop_client::{hydrated_path, is_placeholder, DesktopClient};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
env_logger::init();
|
||||||
|
let Some(arg) = std::env::args().nth(1) else {
|
||||||
|
eprintln!("usage: hydrate <placeholder>");
|
||||||
|
std::process::exit(2);
|
||||||
|
};
|
||||||
|
let path = PathBuf::from(arg);
|
||||||
|
|
||||||
|
let Some(client) = DesktopClient::detect() else {
|
||||||
|
eprintln!("no desktop client running (expected on Android)");
|
||||||
|
std::process::exit(1);
|
||||||
|
};
|
||||||
|
println!("desktop client detected");
|
||||||
|
|
||||||
|
if !is_placeholder(&path) {
|
||||||
|
println!("{} is already materialised", path.display());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let target = hydrated_path(&path);
|
||||||
|
let before = std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
|
||||||
|
println!("stub: {} ({before} bytes)", path.display());
|
||||||
|
|
||||||
|
client.make_available_locally(&path).expect("send command");
|
||||||
|
println!("requested; polling for {}", target.display());
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
while start.elapsed() < Duration::from_secs(30) {
|
||||||
|
if let Ok(m) = std::fs::metadata(&target) {
|
||||||
|
println!(
|
||||||
|
"hydrated: {} bytes in {:.1}s",
|
||||||
|
m.len(),
|
||||||
|
start.elapsed().as_secs_f64()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::thread::sleep(Duration::from_millis(250));
|
||||||
|
}
|
||||||
|
println!("timed out after 30s");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
//! Optional integration with a locally running Nextcloud desktop client.
|
||||||
|
//!
|
||||||
|
//! **Linux desktop only, and strictly optional.** Android has no desktop
|
||||||
|
//! client, no `XDG_RUNTIME_DIR` socket, and no VFS placeholders, so nothing
|
||||||
|
//! here exists on the platform that needs it most. Every capability offered by
|
||||||
|
//! this module is also reachable through [`crate::NextcloudBackend`], which is
|
||||||
|
//! why it is a convenience rather than a dependency (ARCH §9.0).
|
||||||
|
//!
|
||||||
|
//! What it buys where it *is* available: a user whose library already lives in
|
||||||
|
//! a VFS-synced folder can have DarkRoom ask the client to fetch a file,
|
||||||
|
//! rather than DarkRoom downloading a second copy over WebDAV and leaving the
|
||||||
|
//! client's own placeholder state inconsistent.
|
||||||
|
//!
|
||||||
|
//! The protocol is newline-delimited `COMMAND:argument` over a Unix socket.
|
||||||
|
//! Verified against client 4.0.7.
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
use std::io::{BufRead, BufReader, Write};
|
||||||
|
#[cfg(unix)]
|
||||||
|
use std::os::unix::net::UnixStream;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use dr_sync::RemoteError;
|
||||||
|
|
||||||
|
/// How long to wait for the client to acknowledge a command.
|
||||||
|
const TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
|
/// A connection to a running desktop client.
|
||||||
|
/// TRACES: FR-NC-6c | FR-CAT-9
|
||||||
|
pub struct DesktopClient {
|
||||||
|
#[cfg(unix)]
|
||||||
|
socket: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DesktopClient {
|
||||||
|
/// Locate a running client, if there is one.
|
||||||
|
///
|
||||||
|
/// Returns `None` on Android, where no such client exists, and on any
|
||||||
|
/// desktop where the client is not running. Callers treat `None` as
|
||||||
|
/// "use the connector", never as an error.
|
||||||
|
pub fn detect() -> Option<Self> {
|
||||||
|
#[cfg(all(unix, not(target_os = "android")))]
|
||||||
|
{
|
||||||
|
let runtime = std::env::var_os("XDG_RUNTIME_DIR")?;
|
||||||
|
let socket = PathBuf::from(runtime).join("Nextcloud/socket");
|
||||||
|
socket.exists().then_some(Self { socket })
|
||||||
|
}
|
||||||
|
#[cfg(not(all(unix, not(target_os = "android"))))]
|
||||||
|
{
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ask the client to download a placeholder.
|
||||||
|
///
|
||||||
|
/// Hydration is **whole-file**, so this is appropriate for the original
|
||||||
|
/// tier — opening an image in develop, or exporting it — and never for
|
||||||
|
/// browsing. Filling a grid this way would download the entire library,
|
||||||
|
/// which is exactly what range extraction exists to avoid (FR-NC-3).
|
||||||
|
///
|
||||||
|
/// Returns once the command is accepted, not once the download completes;
|
||||||
|
/// callers poll for the materialised path.
|
||||||
|
pub fn make_available_locally(&self, path: &Path) -> Result<(), RemoteError> {
|
||||||
|
self.send("MAKE_AVAILABLE_LOCALLY", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ask the client to dehydrate a file back to a placeholder, freeing disk.
|
||||||
|
pub fn make_online_only(&self, path: &Path) -> Result<(), RemoteError> {
|
||||||
|
self.send("MAKE_ONLINE_ONLY", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn send(&self, command: &str, path: &Path) -> Result<(), RemoteError> {
|
||||||
|
let mut stream = UnixStream::connect(&self.socket)
|
||||||
|
.map_err(|e| RemoteError::Network(format!("desktop client socket: {e}")))?;
|
||||||
|
stream
|
||||||
|
.set_read_timeout(Some(TIMEOUT))
|
||||||
|
.and_then(|_| stream.set_write_timeout(Some(TIMEOUT)))
|
||||||
|
.map_err(|e| RemoteError::Network(e.to_string()))?;
|
||||||
|
|
||||||
|
writeln!(stream, "{command}:{}", path.display())
|
||||||
|
.map_err(|e| RemoteError::Network(e.to_string()))?;
|
||||||
|
stream
|
||||||
|
.flush()
|
||||||
|
.map_err(|e| RemoteError::Network(e.to_string()))?;
|
||||||
|
|
||||||
|
// The client greets with REGISTER_PATH lines; reading one confirms it
|
||||||
|
// is speaking the protocol rather than silently discarding input.
|
||||||
|
let mut line = String::new();
|
||||||
|
BufReader::new(&stream)
|
||||||
|
.read_line(&mut line)
|
||||||
|
.map_err(|e| RemoteError::Network(e.to_string()))?;
|
||||||
|
|
||||||
|
log::debug!("desktop client: {command} -> {}", line.trim());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
fn send(&self, _command: &str, _path: &Path) -> Result<(), RemoteError> {
|
||||||
|
Err(RemoteError::Unsupported(
|
||||||
|
"desktop client integration is Linux-only",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether a path names a VFS placeholder — a file the user has remotely but
|
||||||
|
/// not locally.
|
||||||
|
pub fn is_placeholder(path: &Path) -> bool {
|
||||||
|
path.file_name()
|
||||||
|
.map(|n| n.to_string_lossy().ends_with(dr_types::PLACEHOLDER_SUFFIX))
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The path a placeholder will occupy once hydrated.
|
||||||
|
///
|
||||||
|
/// Suffix-mode VFS renames on hydration, so the materialised file appears
|
||||||
|
/// under a *different* path than the stub. Callers polling for completion must
|
||||||
|
/// watch this one, not the original.
|
||||||
|
pub fn hydrated_path(placeholder: &Path) -> PathBuf {
|
||||||
|
let s = placeholder.to_string_lossy();
|
||||||
|
PathBuf::from(
|
||||||
|
s.strip_suffix(dr_types::PLACEHOLDER_SUFFIX)
|
||||||
|
.unwrap_or(&s)
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn placeholders_are_recognised_by_suffix() {
|
||||||
|
assert!(is_placeholder(Path::new("/x/IMG_4130.CR2.nextcloud")));
|
||||||
|
assert!(!is_placeholder(Path::new("/x/IMG_4130.CR2")));
|
||||||
|
assert!(!is_placeholder(Path::new("/x")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hydration_changes_the_path() {
|
||||||
|
// Suffix mode renames rather than filling in place, so polling the
|
||||||
|
// original path would wait forever.
|
||||||
|
assert_eq!(
|
||||||
|
hydrated_path(Path::new("/x/IMG_4130.CR2.nextcloud")),
|
||||||
|
PathBuf::from("/x/IMG_4130.CR2")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hydrated_path_is_idempotent() {
|
||||||
|
// Calling it on an already-materialised path must not truncate it.
|
||||||
|
assert_eq!(
|
||||||
|
hydrated_path(Path::new("/x/IMG_4130.CR2")),
|
||||||
|
PathBuf::from("/x/IMG_4130.CR2")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn detection_returns_none_rather_than_failing() {
|
||||||
|
// Absence is the normal case — Android always, desktop whenever the
|
||||||
|
// client is not running — so it must never be an error.
|
||||||
|
let _ = DesktopClient::detect();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,9 +14,11 @@ use dr_sync::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
|
pub mod desktop_client;
|
||||||
mod propfind;
|
mod propfind;
|
||||||
|
|
||||||
pub use auth::{AppCredentials, LoginFlow};
|
pub use auth::{AppCredentials, LoginFlow};
|
||||||
|
pub use desktop_client::DesktopClient;
|
||||||
|
|
||||||
/// Chunk sizes Nextcloud's chunked upload v2 accepts.
|
/// Chunk sizes Nextcloud's chunked upload v2 accepts.
|
||||||
const CHUNKS: ChunkConstraints = ChunkConstraints {
|
const CHUNKS: ChunkConstraints = ChunkConstraints {
|
||||||
@@ -42,6 +44,8 @@ pub struct NextcloudBackend {
|
|||||||
impl NextcloudBackend {
|
impl NextcloudBackend {
|
||||||
/// Build a backend from credentials obtained via [`auth`].
|
/// Build a backend from credentials obtained via [`auth`].
|
||||||
pub fn new(creds: &AppCredentials, user_id: &str) -> Result<Self, RemoteError> {
|
pub fn new(creds: &AppCredentials, user_id: &str) -> Result<Self, RemoteError> {
|
||||||
|
install_crypto_provider();
|
||||||
|
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.user_agent("DarkRoom")
|
.user_agent("DarkRoom")
|
||||||
.build()
|
.build()
|
||||||
@@ -302,6 +306,24 @@ impl RemoteBackend for NextcloudBackend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Install the rustls crypto provider, once per process.
|
||||||
|
///
|
||||||
|
/// Required because we build reqwest with `rustls-no-provider` rather than
|
||||||
|
/// `rustls`: the default provider is aws-lc-rs, whose `aws-lc-sys` crate is C
|
||||||
|
/// and does not cross-compile for Android. `ring` is pure Rust apart from a
|
||||||
|
/// small assembly core that builds fine under the NDK.
|
||||||
|
///
|
||||||
|
/// Done here rather than left to callers so there is no way to construct a
|
||||||
|
/// client that panics on first use.
|
||||||
|
fn install_crypto_provider() {
|
||||||
|
use std::sync::Once;
|
||||||
|
static ONCE: Once = Once::new();
|
||||||
|
ONCE.call_once(|| {
|
||||||
|
// Errs only if a provider is already installed, which is fine.
|
||||||
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Translate an HTTP status into a typed error.
|
/// Translate an HTTP status into a typed error.
|
||||||
fn map_status(status: reqwest::StatusCode, what: &str) -> Result<(), RemoteError> {
|
fn map_status(status: reqwest::StatusCode, what: &str) -> Result<(), RemoteError> {
|
||||||
match status.as_u16() {
|
match status.as_u16() {
|
||||||
|
|||||||
@@ -107,6 +107,19 @@ ENV CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=${NDK_BIN}/aarch64-linux-android${
|
|||||||
ENV CARGO_NDK_PLATFORM=${MIN_API} \
|
ENV CARGO_NDK_PLATFORM=${MIN_API} \
|
||||||
ANDROID_PLATFORM=${MIN_API}
|
ANDROID_PLATFORM=${MIN_API}
|
||||||
|
|
||||||
|
# Crates with C or assembly components (ring's crypto core, and anything else
|
||||||
|
# using the cc crate) need a compiler and archiver per target, not just a
|
||||||
|
# linker. cargo-ndk sets the linker only, so these are set explicitly —
|
||||||
|
# otherwise `ring` fails its build script and TLS cannot be built at all.
|
||||||
|
ENV CC_aarch64_linux_android=${NDK_BIN}/aarch64-linux-android${MIN_API}-clang \
|
||||||
|
AR_aarch64_linux_android=${NDK_BIN}/llvm-ar \
|
||||||
|
CC_armv7_linux_androideabi=${NDK_BIN}/armv7a-linux-androideabi${MIN_API}-clang \
|
||||||
|
AR_armv7_linux_androideabi=${NDK_BIN}/llvm-ar \
|
||||||
|
CC_x86_64_linux_android=${NDK_BIN}/x86_64-linux-android${MIN_API}-clang \
|
||||||
|
AR_x86_64_linux_android=${NDK_BIN}/llvm-ar \
|
||||||
|
CC_i686_linux_android=${NDK_BIN}/i686-linux-android${MIN_API}-clang \
|
||||||
|
AR_i686_linux_android=${NDK_BIN}/llvm-ar
|
||||||
|
|
||||||
# Shared cargo registry cache — bind-mount over this to persist across runs.
|
# Shared cargo registry cache — bind-mount over this to persist across runs.
|
||||||
VOLUME ["/opt/cargo/registry"]
|
VOLUME ["/opt/cargo/registry"]
|
||||||
|
|
||||||
|
|||||||
+24
-8
@@ -679,20 +679,36 @@ Three findings, each independently disqualifying:
|
|||||||
`IMG.CR2.nextcloud` exists, containing exactly one byte. Any extension-based scan sees
|
`IMG.CR2.nextcloud` exists, containing exactly one byte. Any extension-based scan sees
|
||||||
`.nextcloud`, so the app needs placeholder-aware code regardless — VFS is not transparent.
|
`.nextcloud`, so the app needs placeholder-aware code regardless — VFS is not transparent.
|
||||||
|
|
||||||
2. **Reads do not hydrate.** Reading the stub returns its 1 byte and nothing else; no fetch is
|
2. **Reads do not hydrate, but hydration can be *requested*.** Reading a stub returns its one byte
|
||||||
triggered, the stub is unchanged, and the real name never appears. Suffix mode is an inert
|
and triggers nothing — there is no FUSE layer intercepting reads. However the client exposes a
|
||||||
marker, not a filesystem hook — there is no FUSE layer intercepting reads. Hydration happens
|
local socket at `$XDG_RUNTIME_DIR/Nextcloud/socket` speaking a newline-delimited
|
||||||
only when the *client* is instructed to sync that file. **DarkRoom cannot read through a
|
`COMMAND:argument` protocol, and `MAKE_AVAILABLE_LOCALLY:<path>` does fetch the file.
|
||||||
placeholder at all.**
|
**Verified 2026-08-09:** a 1-byte `.nextcloud` stub was replaced by the real 2.7 MB file within
|
||||||
|
seconds. `MAKE_ONLINE_ONLY` dehydrates again.
|
||||||
|
|
||||||
3. **Even with hydration, granularity is wrong.** VFS has two states, 1 byte or all bytes. The
|
3. **Even with hydration, granularity is wrong.** VFS has two states, 1 byte or all bytes. The
|
||||||
preview tier — the one that makes remote browsing viable on mobile data — needs a ~256 KB prefix
|
preview tier — the one that makes remote browsing viable on mobile data — needs a ~256 KB prefix
|
||||||
of a 27 MB file. A hydrating VFS would transfer ~100× what FR-NC-3 requires, which is precisely
|
of a 27 MB file. A hydrating VFS would transfer ~100× what FR-NC-3 requires, which is precisely
|
||||||
the cost range extraction exists to avoid.
|
the cost range extraction exists to avoid.
|
||||||
|
|
||||||
Coexistence is still fine and worth supporting: a user may keep a VFS-synced folder, and DarkRoom
|
**What this changes, and what it does not.** Hydration-on-request makes VFS a usable *original*
|
||||||
should recognise `*.nextcloud` stubs and report those images as `Availability::Offline` (FR-NC-6c)
|
tier: for an image the user opens in develop or exports, asking the client to fetch it is a
|
||||||
rather than as corrupt files. What it must not do is depend on VFS for transfer.
|
legitimate alternative to fetching it ourselves, and it inherits their transfer, resume and
|
||||||
|
conflict handling for free.
|
||||||
|
|
||||||
|
It does **not** rescue the preview tier, which is the one that matters for browsing. Finding 3
|
||||||
|
stands: hydration is whole-file, so filling a grid still costs the entire library. Range extraction
|
||||||
|
remains the only mechanism that satisfies FR-NC-3.
|
||||||
|
|
||||||
|
**Design consequence.** VFS is supported as an optional *source*, not as the transfer layer:
|
||||||
|
|
||||||
|
- Recognise `*.nextcloud` stubs and report them as `Availability::Offline` (FR-NC-6c) rather than
|
||||||
|
as corrupt files.
|
||||||
|
- Where a library lives under a VFS-synced folder, offer "download" on a stub by writing
|
||||||
|
`MAKE_AVAILABLE_LOCALLY:<path>` to the socket, rather than fetching a second copy over WebDAV and
|
||||||
|
leaving the client's own state inconsistent.
|
||||||
|
- Never depend on it: the socket is Linux-only, absent on Android, and absent when the client is
|
||||||
|
not running. The direct connector remains the primary path.
|
||||||
|
|
||||||
### 9.1 The three tiers, restated as policy
|
### 9.1 The three tiers, restated as policy
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n
|
|||||||
|
|
||||||
| Metric | Value |
|
| Metric | Value |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Source files scanned | 23 |
|
| Source files scanned | 25 |
|
||||||
| TRACES tags found | 30 |
|
| TRACES tags found | 31 |
|
||||||
| Requirements defined | 143 |
|
| Requirements defined | 143 |
|
||||||
| Requirements covered | 32 |
|
| Requirements covered | 32 |
|
||||||
| **Coverage** | **22.4%** (32/143) |
|
| **Coverage** | **22.4%** (32/143) |
|
||||||
@@ -37,18 +37,18 @@ _None._
|
|||||||
| FR-CAT-1a | [`core/dr-types/src/lib.rs:23`](../core/dr-types/src/lib.rs#L23) |
|
| FR-CAT-1a | [`core/dr-types/src/lib.rs:23`](../core/dr-types/src/lib.rs#L23) |
|
||||||
| FR-CAT-2 | [`tools/traceability/src/lib.rs:473`](../tools/traceability/src/lib.rs#L473) |
|
| FR-CAT-2 | [`tools/traceability/src/lib.rs:473`](../tools/traceability/src/lib.rs#L473) |
|
||||||
| FR-CAT-5 | [`core/dr-decode/src/lib.rs:125`](../core/dr-decode/src/lib.rs#L125) |
|
| FR-CAT-5 | [`core/dr-decode/src/lib.rs:125`](../core/dr-decode/src/lib.rs#L125) |
|
||||||
| FR-CAT-9 | [`core/dr-types/src/lib.rs:80`](../core/dr-types/src/lib.rs#L80) |
|
| FR-CAT-9 | [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-types/src/lib.rs:80`](../core/dr-types/src/lib.rs#L80) |
|
||||||
| FR-CULL-1 | [`core/dr-decode/src/preview.rs:96`](../core/dr-decode/src/preview.rs#L96) |
|
| FR-CULL-1 | [`core/dr-decode/src/preview.rs:96`](../core/dr-decode/src/preview.rs#L96) |
|
||||||
| FR-CULL-2 | [`core/dr-decode/src/preview.rs:123`](../core/dr-decode/src/preview.rs#L123) |
|
| FR-CULL-2 | [`core/dr-decode/src/preview.rs:123`](../core/dr-decode/src/preview.rs#L123) |
|
||||||
| FR-DEV-4 | [`core/dr-gpu/src/lib.rs:119`](../core/dr-gpu/src/lib.rs#L119) |
|
| FR-DEV-4 | [`core/dr-gpu/src/lib.rs:119`](../core/dr-gpu/src/lib.rs#L119) |
|
||||||
| FR-DSP-1 | [`ui/dr-ui/src/lib.rs:21`](../ui/dr-ui/src/lib.rs#L21) |
|
| FR-DSP-1 | [`ui/dr-ui/src/lib.rs:21`](../ui/dr-ui/src/lib.rs#L21) |
|
||||||
| FR-EXP-9 | [`core/dr-decode/src/lib.rs:151`](../core/dr-decode/src/lib.rs#L151) |
|
| FR-EXP-9 | [`core/dr-decode/src/lib.rs:151`](../core/dr-decode/src/lib.rs#L151) |
|
||||||
| FR-NC-1 | [`core/dr-sync-nextcloud/src/auth.rs:132`](../core/dr-sync-nextcloud/src/auth.rs#L132), [`core/dr-sync-nextcloud/src/auth.rs:44`](../core/dr-sync-nextcloud/src/auth.rs#L44) |
|
| FR-NC-1 | [`core/dr-sync-nextcloud/src/auth.rs:132`](../core/dr-sync-nextcloud/src/auth.rs#L132), [`core/dr-sync-nextcloud/src/auth.rs:44`](../core/dr-sync-nextcloud/src/auth.rs#L44) |
|
||||||
| FR-NC-12 | [`core/dr-sync-nextcloud/src/lib.rs:30`](../core/dr-sync-nextcloud/src/lib.rs#L30), [`core/dr-sync/src/lib.rs:128`](../core/dr-sync/src/lib.rs#L128), [`core/dr-sync/src/lib.rs:34`](../core/dr-sync/src/lib.rs#L34) |
|
| FR-NC-12 | [`core/dr-sync-nextcloud/src/lib.rs:32`](../core/dr-sync-nextcloud/src/lib.rs#L32), [`core/dr-sync/src/lib.rs:128`](../core/dr-sync/src/lib.rs#L128), [`core/dr-sync/src/lib.rs:34`](../core/dr-sync/src/lib.rs#L34) |
|
||||||
| FR-NC-3 | [`core/dr-decode/src/preview.rs:123`](../core/dr-decode/src/preview.rs#L123), [`core/dr-sync/src/capability.rs:41`](../core/dr-sync/src/capability.rs#L41) |
|
| FR-NC-3 | [`core/dr-decode/src/preview.rs:123`](../core/dr-decode/src/preview.rs#L123), [`core/dr-sync/src/capability.rs:41`](../core/dr-sync/src/capability.rs#L41) |
|
||||||
| FR-NC-4 | [`core/dr-sync-nextcloud/src/propfind.rs:100`](../core/dr-sync-nextcloud/src/propfind.rs#L100), [`core/dr-sync-nextcloud/src/propfind.rs:51`](../core/dr-sync-nextcloud/src/propfind.rs#L51), [`core/dr-sync/src/capability.rs:6`](../core/dr-sync/src/capability.rs#L6), [`core/dr-sync/src/lib.rs:128`](../core/dr-sync/src/lib.rs#L128) |
|
| FR-NC-4 | [`core/dr-sync-nextcloud/src/propfind.rs:100`](../core/dr-sync-nextcloud/src/propfind.rs#L100), [`core/dr-sync-nextcloud/src/propfind.rs:51`](../core/dr-sync-nextcloud/src/propfind.rs#L51), [`core/dr-sync/src/capability.rs:6`](../core/dr-sync/src/capability.rs#L6), [`core/dr-sync/src/lib.rs:128`](../core/dr-sync/src/lib.rs#L128) |
|
||||||
| FR-NC-5 | [`core/dr-sync-nextcloud/src/propfind.rs:51`](../core/dr-sync-nextcloud/src/propfind.rs#L51) |
|
| FR-NC-5 | [`core/dr-sync-nextcloud/src/propfind.rs:51`](../core/dr-sync-nextcloud/src/propfind.rs#L51) |
|
||||||
| FR-NC-6c | [`core/dr-types/src/lib.rs:131`](../core/dr-types/src/lib.rs#L131), [`core/dr-types/src/lib.rs:80`](../core/dr-types/src/lib.rs#L80) |
|
| FR-NC-6c | [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-types/src/lib.rs:131`](../core/dr-types/src/lib.rs#L131), [`core/dr-types/src/lib.rs:80`](../core/dr-types/src/lib.rs#L80) |
|
||||||
| FR-PLAT-AND-1 | [`core/dr-types/src/lib.rs:23`](../core/dr-types/src/lib.rs#L23) |
|
| FR-PLAT-AND-1 | [`core/dr-types/src/lib.rs:23`](../core/dr-types/src/lib.rs#L23) |
|
||||||
| FR-RAW-1 | [`core/dr-decode/src/lib.rs:83`](../core/dr-decode/src/lib.rs#L83), [`core/dr-types/src/lib.rs:90`](../core/dr-types/src/lib.rs#L90) |
|
| FR-RAW-1 | [`core/dr-decode/src/lib.rs:83`](../core/dr-decode/src/lib.rs#L83), [`core/dr-types/src/lib.rs:90`](../core/dr-types/src/lib.rs#L90) |
|
||||||
| FR-RAW-3 | [`core/dr-decode/src/lib.rs:151`](../core/dr-decode/src/lib.rs#L151) |
|
| FR-RAW-3 | [`core/dr-decode/src/lib.rs:151`](../core/dr-decode/src/lib.rs#L151) |
|
||||||
|
|||||||
Reference in New Issue
Block a user