Skip to content

Repository files navigation

logmv

CI crates.io docs.rs

Logged atomic file move and trash, with an append-only JSON-Lines audit trail.

What it is

logmv moves a file with mv-style ergonomics, but every operation is recorded as one compact JSON line in a log you choose. Instead of an untraceable mv or rm, you get a durable, machine-readable history of what moved where and when.

It solves the "where did that file go?" problem for scripts, agents, and cleanup jobs: each move, trash, directory creation, and directory removal is appended to a JSON-Lines log, so the filesystem's history is auditable after the fact.

Guarantees it holds:

  • Atomic move only. A single rename syscall; never a copy-then-delete fallback. A cross-volume move fails loudly (EXDEV) rather than silently degrading.
  • Never overwrite. If the destination already exists, the move is refused and nothing is logged.
  • Trash never unlinks. --trash relocates into ~/.Trash, disambiguating the name on collision; it never deletes.
  • No silent drift. If a rename succeeds but the log append then fails, the error is loud so the filesystem and log are never quietly out of sync.

Platform support

logmv supports only macOS and Linux. The crate bakes in macOS/Linux-specific assumptions: EXDEV == raw OS error 18 (the cross-volume rename signal), '/' path separators, and the ~/.Trash move model. Other platforms differ on all three, so a non-macOS/Linux build is rejected at compile time rather than silently misbehaving.

Install

From crates.io (once published):

$ cargo install logmv

From source:

$ git clone https://github.com/johanthoren/logmv
$ cd logmv
$ cargo install --path .

Usage

The first positional argument is always the log file to append to. Flags (--trash, --mkdir, --rmdir) must precede the source/destination.

Move SRC DST

$ logmv ops.log report.txt archive.txt

Appends one move line (source and destination canonicalized to absolute paths):

{"ts":"2026-07-02T22:00:40+08:00","act":"move","src":"/Users/alice/work/report.txt","dst":"/Users/alice/work/archive.txt"}

If DST is an existing directory (or ends with /), SRC is moved into it as DST/basename(SRC).

Trash --trash

Moves the path into ~/.Trash, disambiguating the name if one already exists (e.g. obsolete-1.txt); it never unlinks.

$ logmv ops.log --trash obsolete.txt
{"ts":"2026-07-02T22:00:49+08:00","act":"trash","src":"/Users/alice/work/obsolete.txt","dst":"/Users/alice/.Trash/obsolete.txt"}

Create missing parents --mkdir

Creates the destination's missing parent directories (like mkdir -p) and logs one mkdir line per directory actually created, parent to child, before the move.

$ logmv ops.log --mkdir deep.txt archive/2026/reports/deep.txt
{"ts":"2026-07-02T22:00:40+08:00","act":"mkdir","src":"-","dst":"/Users/alice/work/archive/2026"}
{"ts":"2026-07-02T22:00:40+08:00","act":"mkdir","src":"-","dst":"/Users/alice/work/archive/2026/reports"}
{"ts":"2026-07-02T22:00:40+08:00","act":"move","src":"/Users/alice/work/deep.txt","dst":"/Users/alice/work/archive/2026/reports/deep.txt"}

Remove emptied parents --rmdir

After a successful, logged move, removes the source's now-empty parent directories, cascading upward and stopping at the first non-empty one (like rmdir -p). Only truly-empty directories are removed.

$ logmv ops.log --rmdir project/tmp/cache.txt out
{"ts":"2026-07-02T22:00:49+08:00","act":"move","src":"/Users/alice/work/project/tmp/cache.txt","dst":"/Users/alice/work/out/cache.txt"}
{"ts":"2026-07-02T22:00:49+08:00","act":"rmdir","src":"/Users/alice/work/project/tmp","dst":"-"}
{"ts":"2026-07-02T22:00:49+08:00","act":"rmdir","src":"/Users/alice/work/project","dst":"-"}

Trailing metadata [K V]

Any trailing K V pairs are recorded on the log line after the canonical keys. They must come in complete pairs (an odd number of trailing arguments is a usage error), and a key may not shadow a canonical key (ts/act/src/dst).

$ logmv ops.log q3.txt q3-archived.txt by cc reason quarterly-cleanup
{"ts":"2026-07-02T22:00:40+08:00","act":"move","src":"/Users/alice/work/q3.txt","dst":"/Users/alice/work/q3-archived.txt","by":"cc","reason":"quarterly-cleanup"}

Log schema

The log is JSON Lines: one compact JSON object per line, appended, never rewritten. Each line begins with four canonical keys in a fixed order, followed by any free K V metadata pairs in the order given:

Key Meaning
ts RFC 3339 timestamp, second precision, with the local UTC offset
act one of move, trash, mkdir, rmdir
src canonical absolute source path ("-" for mkdir)
dst canonical absolute destination path ("-" for rmdir)

Every metadata value is written as a JSON string (a numeric-looking "42" stays a string). serde owns all key/value escaping, so arbitrary characters in metadata never corrupt the line.

The move itself is byte-faithful, but the log is not a byte-exact record for non-UTF-8 paths: on Linux each invalid byte in src or dst is written as the Unicode replacement character (U+FFFD), so a line containing it is not reliably reversible. macOS enforces UTF-8, so the case cannot arise there.

Exit codes

  • 0: success. stdout is empty.
  • 1: failure. A single logmv: <error> line is written to stderr.

For example, refusing to overwrite an existing destination:

$ logmv ops.log s.txt d.txt
logmv: destination already exists: d.txt
$ echo $?
1

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

About

Logged atomic file move and trash with an append-only JSON-Lines audit trail

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages