Look before overwriting the file we are asking about
The probe PUT its bytes first and read the outcome, which answers the question by destroying the evidence: pointed at a real sidecar it would replace an edit with the word "probe", and on success delete it outright. PROPFIND first. Permissions and status usually settle create-versus-update on their own, and a path that already exists is now reported and left alone. `--write` still forces the update test for a file worth losing, and the cleanup DELETE fires only for a path the probe itself created. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,16 +9,24 @@
|
||||
//! an access-control rule from a lock.
|
||||
//!
|
||||
//! Reads the stored session and its keyring credential, so it exercises the
|
||||
//! same account the app does. It writes a few bytes and deletes them again.
|
||||
//! same account the app does.
|
||||
//!
|
||||
//! It PROPFINDs before it writes. A path that already exists is reported and
|
||||
//! left alone: overwriting a real sidecar to learn whether we may overwrite it
|
||||
//! is a poor trade. Pass `--write` to test an update anyway, which is only
|
||||
//! sensible against a file you are willing to lose. Absent paths are written
|
||||
//! and deleted again, which tests creation and costs nothing.
|
||||
|
||||
use dr_plat::PlatformSecretStore;
|
||||
use dr_sync_nextcloud::session::SessionStore;
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() {
|
||||
let mut args = std::env::args().skip(1);
|
||||
let (Some(server), Some(path)) = (args.next(), args.next()) else {
|
||||
eprintln!("usage: put_probe <server-url> <remote/path>");
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let force = args.iter().any(|a| a == "--write");
|
||||
let mut positional = args.iter().filter(|a| !a.starts_with("--"));
|
||||
let (Some(server), Some(path)) = (positional.next(), positional.next()) else {
|
||||
eprintln!("usage: put_probe [--write] <server-url> <remote/path>");
|
||||
std::process::exit(2);
|
||||
};
|
||||
|
||||
@@ -44,7 +52,7 @@ async fn main() {
|
||||
session.user_id,
|
||||
path
|
||||
);
|
||||
println!("PUT {url}");
|
||||
println!("{url}");
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let send = |method: reqwest::Method, body: Vec<u8>| {
|
||||
@@ -58,12 +66,49 @@ async fn main() {
|
||||
client
|
||||
.request(method, &url)
|
||||
.basic_auth(user, Some(pass))
|
||||
// Ignored by PUT and DELETE; required by PROPFIND.
|
||||
.header("Depth", "0")
|
||||
.body(body)
|
||||
.send()
|
||||
.await
|
||||
}
|
||||
};
|
||||
|
||||
// What the server thinks of the path, before we touch it. `oc:permissions`
|
||||
// on a file carries `W` when it may be updated, and the status separates
|
||||
// "exists and refuses updates" from "does not exist yet".
|
||||
let propfind = send(
|
||||
reqwest::Method::from_bytes(b"PROPFIND").expect("valid method"),
|
||||
br#"<?xml version="1.0"?><d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns"><d:prop><oc:permissions/></d:prop></d:propfind>"#.to_vec(),
|
||||
)
|
||||
.await;
|
||||
let exists = match propfind {
|
||||
Ok(resp) => {
|
||||
let status = resp.status();
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
let perms = body
|
||||
.split("<oc:permissions>")
|
||||
.nth(1)
|
||||
.and_then(|t| t.split('<').next())
|
||||
.unwrap_or("(not reported)");
|
||||
println!("PROPFIND -> {status}, permissions: {perms}");
|
||||
if status.is_success() && !perms.contains('W') {
|
||||
println!(" no W: the server will not let this account update it");
|
||||
}
|
||||
status.is_success()
|
||||
}
|
||||
Err(e) => {
|
||||
println!("PROPFIND failed: {e}");
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if exists && !force {
|
||||
println!("exists already; not overwriting it. Re-run with --write to test an update.");
|
||||
return;
|
||||
}
|
||||
|
||||
println!("PUT (probe bytes)");
|
||||
match send(reqwest::Method::PUT, b"probe".to_vec()).await {
|
||||
Ok(resp) => {
|
||||
let status = resp.status();
|
||||
@@ -76,7 +121,9 @@ async fn main() {
|
||||
{
|
||||
println!(" {}", line.trim());
|
||||
}
|
||||
if status.is_success() {
|
||||
// Only tidy up what we brought into being. Deleting a path that
|
||||
// was already there would turn a diagnostic into data loss.
|
||||
if status.is_success() && !exists {
|
||||
let _ = send(reqwest::Method::DELETE, Vec::new()).await;
|
||||
println!("(probe file removed)");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user