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.
51 lines
1.5 KiB
Rust
51 lines
1.5 KiB
Rust
//! 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);
|
|
}
|