Commit Graph

14 Commits

Author SHA1 Message Date
orrisroot 3f2ca938bd perf(transfer): parallelize folder traversal API calls
Use a shared API request limiter across recursive upload and
download traversal so folder detail fetches, file listings,
folder auth, and transfers can run concurrently under one
budget.

Refactor the traversal loops into task-driven pipelines while
preserving skip-if-exists, excludes, cleanup, and current
output behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-07 15:49:02 +09:00
orrisroot 4e73766732 perf(cache): reuse auth state in memory
Cache parsed auth state per remote and validate it with on-disk
file metadata so repeated authenticated API calls can skip
redundant open/read/JSON parse work within one process.

Centralize cache load, persist, and removal helpers in the cache
module, reuse them from login, logout, and whoami, and update
the refresh path to persist structured cache data directly.

Add targeted cache tests for memory reuse, invalidation after
external writes, persist updates, and cache removal.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-07 15:49:01 +09:00
orrisroot a67f9a72a6 fix(auth): refresh tokens before authenticated requests
Move token refresh checks into the shared Rust connection/API path so long-running authenticated operations stop reusing stale access tokens. This covers recursive download and upload traversal, recursive ls via the shared APIs, and direct authenticated commands such as cp, mv, rm, and chacl.

Also surface HTTP failures earlier in the affected API methods instead of failing later during response parsing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-20 16:26:47 +09:00
orrisroot d05bd8a08d chore(rust): update lockfile and format sources
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-20 15:59:28 +09:00
orrisroot 769a5a68e2 refactor: unify error handling with anyhow and add From conversions
Phase 5: Replace all Box<dyn Error> return types with anyhow::Result<T>
throughout the codebase. Replace string-based Err("msg".into()) and
format!().into() patterns with bail!() and anyhow!() macros. Fix
dirs::home_dir().unwrap() in settings.rs to use a fallback path instead
of panicking when HOME is unset. Remove stray use std::error::Error
imports no longer needed.

Phase 6: Add From<&User> for CacheUser in models/user.rs and
From<&Laboratory>/From<&Laboratories> for CacheLaboratory/CacheLabsWrapper
in models/laboratory.rs. Simplify commands/login.rs to use .into()
conversions, removing the redundant to_cache_user() and to_cache_labs()
helper functions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-20 14:19:10 +09:00
orrisroot ecd244491b fix(upload,download): refresh access token before each parallel transfer task
The access token obtained at command startup could expire during a long
transfer session (e.g. uploading thousands of files or large files),
causing subsequent requests to fail with 401 Unauthorized.

Root cause: load_cache_with_token_refresh was called only once, and the
resulting MDRSConnection — including its now-stale token — was shared
across all parallel tasks via Arc.  There was no mechanism to update the
token in the shared instance after creation.

Fix:
- Add MDRSConnection::with_token(&self, token) that creates a new
  connection struct reusing the caller's HTTP client (cheap Arc clone,
  shares the connection pool) but carrying a fresh Bearer token.
- In upload.rs and download.rs, call load_cache_with_token_refresh
  inside each tokio::spawn task body, then create a task-local
  connection via conn.with_token(fresh_token) before transferring the
  file.  The shared reqwest::Client (connection pool) is preserved.

cp.rs is not changed: it uses only short server-side API calls with no
parallel tasks, so token expiry during a cp operation is negligible.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-20 13:29:14 +09:00
orrisroot d58acd70e5 fix(token): use dedicated .lock file to guard entire refresh critical section
The previous implementation had two correctness issues:

1. flock on .tmp was ineffective for cross-process exclusion.
   After fs::rename(), the .tmp inode disappears.  A second process
   opening .tmp gets a brand-new inode, so both processes hold flocks
   on different inodes simultaneously — no mutual exclusion occurs.

2. The critical section was too narrow.  The in-process tokio::Mutex
   only serializes tasks within the same process.  Two separate mdrs
   processes could both read the cache, both decide a refresh was
   needed, and both call the token-refresh endpoint before either had
   written the new token back — risking double-refresh and potential
   failures on servers that use refresh-token rotation.

Fix: introduce a dedicated `cache/{remote}.lock` file as the cross-
process advisory lock target.  The lock file is never renamed, so its
inode remains stable for the entire critical section.  The flock now
wraps the complete read-check-refresh-write cycle in
load_cache_with_token_refresh(), and the redundant flock on .tmp in
refresh_and_persist() is removed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-20 13:05:21 +09:00
orrisroot 0da6a10898 fix: ensure NFC normalization is applied consistently
- api/files.rs: NFC-normalize filename before sending to server in
  upload_file(). On macOS, local filenames may be NFD-encoded, which
  would cause the server to store them as NFD instead of NFC.

- commands/download.rs: replace direct to_lowercase() subfolder
  comparison with find_subfolder_by_name() helper, which already
  applies NFC normalization on both sides.

- commands/cp.rs, mv.rs: apply nfc() to s_basename (source path
  component from user input) for consistency with d_basename, so
  the no-op identity check and find_*() calls use normalized strings.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-20 12:19:28 +09:00
orrisroot 1d81216c97 fix: use PathBuf::join for path construction in download command
On Windows, std::fs::canonicalize returns an extended-length path
with the \?\ prefix, which does not support forward slashes.
Using format!("{}/{}", ...) to join paths then caused os error 123
(ERROR_INVALID_NAME).

Replace all string-based path concatenation with PathBuf::join so
that the OS-appropriate separator is used on every platform.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-20 11:51:10 +09:00
orrisroot 3578a39d27 feat: implement selfupdate command
Release / create-release (push) Failing after 31s
Release / build-linux-x86_64 (push) Has been skipped
Release / build-linux-aarch64 (push) Has been skipped
Release / build-macos (aarch64-apple-darwin) (push) Has been skipped
Release / build-macos (x86_64-apple-darwin) (push) Has been skipped
Release / build-windows (push) Has been skipped
- Fetch latest release from Gitea API using existing reqwest client
- Match release asset by BUILD_TARGET triple (supports .tar.gz and .zip)
- Compare versions; show confirmation prompt (skippable with -y/--yes)
- Download archive, extract binary, atomically replace self via self-replace
- Support private repositories via GITEA_TOKEN environment variable
- Expose BUILD_TARGET in build.rs for compile-time target triple detection
- Add .gitea/workflows/release.yml for multi-platform release builds on tag push

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 20:15:19 +09:00
orrisroot 7947c3bae9 feat(config): simplify list command and add subcommand aliases
- config list: remove --long option, always display URL
- config list: add ls alias (#[command(alias = "ls")])
- config delete: add rm and remove aliases (#[command(aliases = ["remove", "rm"])])
- README: update config list and config delete examples

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 18:59:18 +09:00
orrisroot 0d474e7913 fix: align all command outputs with Python reference implementation
- fix(connection): fix create_folder API body (name, parent_id, description, template_id)
- feat(shared): add Unicode NFC normalization helper and find_subfolder_by_name()
- feat(Cargo): add unicode-normalization dependency
- fix(shared,mkdir,rm,cp,mv,upload): apply NFC normalization to path and name comparisons
- fix(labs): rewrite output as aligned table (Name/PI/Laboratory), remove cache fallback
- fix(mkdir): silent on success; align error message with Python
- fix(rm): silent on success; use find_subfolder_by_name for NFC-aware lookup
- fix(cp): silent on success; align all error messages; add no-op when src==dest
- fix(mv): silent on success; align all error messages; add no-op when src==dest
- fix(login): change output to 'Login Successful'
- fix(logout): remove all output (silent like Python)
- fix(chacl): remove success message (silent like Python)
- fix(metadata): use compact JSON output (to_string instead of to_string_pretty)
- fix(file_metadata): use compact JSON output
- fix(ls): use compact JSON output; add blank line after entries in recursive plain mode
- fix(config): silent on create/update/delete; add colon in list short format;
  remove empty-state messages; align error messages ('is already exists.' / 'is not exists.')

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 18:45:52 +09:00
orrisroot 65c0626910 fix(ls): rename --quick to --quiet; add version command; bump to 0.1.1
- Fix ls -q long option name: --quick → --quiet (typo fix)
- Bump version 0.1.0 → 0.1.1
- Add `version` subcommand (prints "mdrs <version>")
- Document `version` command in README

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 17:41:37 +09:00
orrisroot 872d27a4e4 first commit 2026-04-17 16:52:04 +09:00