No description
Find a file
Pandi 9c65e5b321 fix(cli): expand the env-file placeholder in --help
The help printed the placeholder name literally. Same replace pattern the base URL already used; a test now asserts no brace survives in --help or in a usage error.
2026-11-05 09:24:13 +01:00
src fix(cli): expand the env-file placeholder in --help 2026-11-05 09:24:13 +01:00
tests fix(cli): expand the env-file placeholder in --help 2026-11-05 09:24:13 +01:00
.gitignore nc-put: upload one file to WebDAV and verify the remote size 2026-10-30 18:27:52 +01:00
Cargo.lock nc-put: upload one file to WebDAV and verify the remote size 2026-10-30 18:27:52 +01:00
Cargo.toml nc-put: upload one file to WebDAV and verify the remote size 2026-10-30 18:27:52 +01:00
README.md nc-put: upload one file to WebDAV and verify the remote size 2026-10-30 18:27:52 +01:00

nc-put (Rust)

Upload one file to a WebDAV share (Nextcloud) and prove the upload landed completely: MKCOL the missing parent directories, stream the file with PUT, then compare the remote getcontentlength against the local size.

Written for the git bundles in /home/pandi/backups/git/, which until now only existed on the same disk as the repositories they backup.

Build and install

cargo build --release
install -m 755 target/release/nc-put ~/bin/nc-put

Usage

nc-put [--base-url URL] [--dry-run] [--quiet] QUELLDATEI REMOTE-PFAD

REMOTE-PFAD is relative to the user's WebDAV root, e.g. hermes-backups/git/2026-10-30/OakSeerr.bundle. Path segments are percent-encoded individually.

  • --base-url URL / NC_BASE_URL — WebDAV root of the user, e.g. https://host/remote.php/dav/files/USER/. A literal {user} in the value is replaced by the username from the env file, so the shell command needs no username.
  • --dry-run — print target URL, directories and byte count; no requests.
  • --quiet — suppress progress lines.

Credentials come from NEXTCLOUD_WEBDAV_USER / NEXTCLOUD_WEBDAV_PASS in the env file (HERMES_ENV, default /home/pandi/.hermes/.env). The parser is line-based: KEY=VALUE, # comments and blank lines ignored, = allowed in the value, one layer of matching quotes stripped, nothing shell-interpreted. The password is never printed, not even truncated.

Exit codes: 0 ok, 1 upload or verification failed, 2 bad invocation or missing credentials. Failures name the HTTP status and a snippet of the server body (413, 507, timeouts included).

Behaviour

  1. MKCOL each missing directory segment in order; HTTP 405 (already exists) is accepted.
  2. PUT the file with Content-Length set and the file reader streamed straight into the request — the file never sits in memory as a whole.
  3. PROPFIND Depth: 0 on the target and compare getcontentlength with the local size; a mismatch exits 1.

User-Agent is curl/8.5.0, same as nc-inventar: the server rejects unknown agents with 403.

Differences to a plain curl -T

  • One command does directories, upload and size check, and fails loudly with the HTTP status when any step is short.
  • The size check is a real PROPFIND against the server, not a guess from the PUT status code.

Tests

cargo test
  • 4 unit tests: env parser (comments, = in value, quotes, export, junk lines), missing key returns an error instead of panicking, getcontentlength parsing regardless of namespace prefix, per-segment percent-encoding.
  • 2 integration tests against a local std::net::TcpListener stub (no network): (a) the three missing directories are created one by one with MKCOL, an existing directory answered with 405 is tolerated, and the PUT body is byte-identical to the file; (b) a reported getcontentlength one byte short of the local size yields exit code 1 with a clear message.