Commit Graph

18 Commits

Author SHA1 Message Date
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 723017a11c docs(agents): add Rust project guidance
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-20 15:54:24 +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 e8fd359f54 chore(scripts): add Linux release build script
Add scripts/build-release-linux.sh for building musl-linked release
archives (x86_64 and aarch64) on Linux. Uses `cross` for cross-
compilation so no host toolchain beyond Docker/Podman is required.

Also add mdrs-*.tar.gz and mdrs-*.zip to .gitignore to prevent
generated release archives from being tracked.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-20 13:50:08 +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 f0671c06ad feat: add manual release build scripts for macOS and Windows
CI does not have macOS or Windows runners, so provide scripts to
build and optionally upload release archives locally.

- scripts/build-release-macos.sh  — builds x86_64 and aarch64-apple-darwin
- scripts/build-release-windows.ps1 — builds x86_64-pc-windows-msvc
- scripts/.env.example             — template for Gitea credentials

Both scripts read GITEA_TOKEN, GITEA_SERVER_URL, and GITEA_REPOSITORY
from the environment or from scripts/.env (which is gitignored).
Upload to Gitea is skipped when GITEA_TOKEN is not set.

Use curl.exe for multipart upload in the PowerShell script to support
Windows PowerShell 5.1 (Invoke-RestMethod -Form requires PS 6.1+).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-20 11:44:22 +09:00
orrisroot e7692c109d ci: replace cross with cargo-zigbuild for aarch64 cross-compilation
Release / build-linux-x86_64 (push) Successful in 1m35s
Release / build-linux-aarch64 (push) Successful in 2m5s
cross runs builds inside a Docker container, so the host-installed
Rust toolchain is not visible. Replace with cargo-zigbuild which
uses Zig as a cross-linker and runs directly on the host.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
v0.1.1
2026-04-17 21:31:50 +09:00
orrisroot 3a3186b24a ci: install Rust via rustup installer script
Release / build-linux-x86_64 (push) Successful in 1m34s
Release / build-linux-aarch64 (push) Failing after 1m39s
The act-runner environment does not have rustup pre-installed.
Install it using the official sh.rustup.rs script and append
~/.cargo/bin to GITHUB_PATH so subsequent steps can find the tools.

Node.js-based actions (actions/checkout, gitea-release-action) run
on the host runner where Node.js is available, so no container image
is needed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 21:13:51 +09:00
orrisroot 032f066307 ci: simplify release workflow using gitea-release-action
Release / build-linux-x86_64 (push) Failing after 15s
Release / build-linux-aarch64 (push) Failing after 6s
Replace manual curl-based release creation and asset upload with
akkuman/gitea-release-action@v1. Each build job now independently
creates/updates the release and uploads its asset in a single step.

Also remove macOS and Windows jobs as those runners are not available.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 20:43:02 +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 v0.1.0 2026-04-17 16:52:04 +09:00