Logged atomic file move and trash, with an append-only JSON-Lines audit trail.
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
renamesyscall; 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.
--trashrelocates 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.
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.
From crates.io (once published):
$ cargo install logmvFrom source:
$ git clone https://github.com/johanthoren/logmv
$ cd logmv
$ cargo install --path .The first positional argument is always the log file to append to. Flags
(--trash, --mkdir, --rmdir) must precede the source/destination.
$ logmv ops.log report.txt archive.txtAppends 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).
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"}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"}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":"-"}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"}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.
- 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 $?
1Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
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.