Run `filekeep` to keep versioned copies of individual files, the hosts file, and environment settings. Each entry chooses its own destination, filename template, and unchanged-content policy. There is no background service or built-in schedule.
Run filekeep to keep versioned copies of individual files, the hosts file, and
environment settings. Each entry chooses its own destination, filename template,
and unchanged-content policy. There is no background service or built-in schedule.
Build and run
Requires Rust 1.95 or newer. From PowerShell:
cd "$HOME\Projects\filekeep"
cargo build --release --locked
cargo run --release --locked -- --dry-run
cargo run --release --locked
cargo run --release --locked -- --config-file 'C:\path\other-settings.toml'
Cargo outputs are configured in .cargo/config.toml, normally under
C:/tmp/_rust/filekeep/target on Windows or /tmp/_rust/filekeep/target on Unix.
Use Cargo's evaluated target_directory; the executable is under its release
directory as filekeep.exe on Windows (filekeep elsewhere).
The included filekeep.yaml carries forward both older apps' destinations and
date-based filenames. Preview it with --dry-run before a normal run. On Unix,
change the environment entry's scopes to [process].
Self-extracting installer
cd "$HOME\Projects\doinstall"
.\build.cmd build
cd "$HOME\Projects\filekeep"
dotask create-filekeep-installer
The first command prepares matching doinstall tools after source updates; the
filekeep task only consumes prebuilt tools and never installs or builds doinstall
implicitly. It verifies/builds filekeep and produces one self-extracting setup
executable under artifacts/installers/, reporting its actual path. Runtime
installation needs neither dotask nor Cargo. Build and install are separate steps.
The installer includes the default YAML with hosts and machine/user environment
entries. Its interactive form lets the user select a backup folder and filename
template separately for each entry, plus ordinary application/PATH choices.
Both entries default to skip_if_unchanged: true. Filename templates remain
literal until filekeep runs. Installation does not run backups.
Settings are created at $XDG_CONFIG_HOME/filekeep/settings.yaml, defaulting to
~/.config/filekeep/settings.yaml, which normal configuration discovery finds.
The bundled example lives under defaults/, away from the executable so it cannot
shadow mutable user settings. Existing settings are preserved byte-for-byte on
install/upgrade; changing installer field values does not overwrite them. Edit the
existing YAML to change an established setup. Uninstall keeps settings by default;
actual backups are never installer-owned.
The task derives the version from Cargo. Use --app-version or --build-stamp
for an explicit package version/stamp. .dotasks.yaml selects the sibling doinstall
source checkout only to query Cargo's output for prebuilt native tools. Supply
--doinstall-tools C:/path/to/tools to use another matching tool set. Removing
settings.doinstall.source uses the installed builder's tool-path discovery.
Task adapters are tracked in .dotasks-lock.yaml for normal dotask synchronization.
To repeat Windows package acceptance without changing your active installation:
dotask verify-installer --installer 'C:\path\reported-filekeep-setup.exe'
This uses temporary application/configuration/backup directories, checks persistent PATH before and after, and exercises extraction, validation, custom outputs, installed command discovery, duplicate skips, lock cleanup, reinstall, a distinct upgrade package identity, and uninstall with retained settings/backups.
Unattended setup accepts the same values through doinstall's --set inputs:
hosts-output-directory, hosts-filename, environment-output-directory, and
environment-filename. To forward installer arguments through the self-extracting
launcher, place them after --. Pass --set add-to-path=false during isolated
acceptance, and use temporary application/configuration/command directories.
Settings
YAML (.yaml or .yml), JSON, XML, and TOML are built in. They share one schema;
equivalent examples are in filekeep.yaml and examples/. No settings are
generated, rewritten, or migrated automatically.
entries:
- id: editor-settings
source:
type: file
path: '${APPDATA}/Code/User/settings.json'
destination: '${HOME}/Backups/editor'
filename: '${HOST}-settings-${date:yyyyMMdd}.json'
skip_if_unchanged: true
| Field | Meaning |
|---|---|
id |
Required, nonblank, unique entry identity. Keep it stable across runs. |
source.type |
file, hosts_file, or environment. |
source.path |
Required only for file; one file, without globbing or directory recursion. |
source.scopes |
Only for environment: machine, user, and/or process. Defaults to [machine, user]. |
destination |
Backup directory; created when a backup is needed. |
filename |
A basename template, without directory separators. |
skip_if_unchanged |
Defaults to true. Set to false to retain every successful capture. |
hosts_file uses %SystemRoot%/System32/drivers/etc/hosts on Windows and
/etc/hosts elsewhere. Use type: file to supply a different location.
Relative source and destination paths resolve against the selected settings
file's directory, including when invoked from another working directory. A
leading ~/ or ~\ expands to the user's home directory. Invalid output names
are reported instead of silently being sanitized into a different name.
Malformed settings, missing required fields, unknown fields/types, and an empty
entry list fail configuration loading before any backup starts. Entry validation
and runtime failures are reported separately; all other entries still run. Every
entry is reported as created, skipped, planned, or failed, followed by a summary.
Exit status is 0 when all entries succeed or skip, and 1 if any entry fails.
CLI argument errors use status 2.
Configuration discovery
--config-file <path> uses exactly that file (relative to the working directory)
and never falls back if it is missing or invalid. Without it, search in order:
- Working directory:
<executable-stem>.yaml,.yml,.json,.xml,.toml. - Executable directory: the same names.
$XDG_CONFIG_HOME/filekeep/settings.yaml; when unset, empty, or relative, use<home>/.config/filekeep/settings.yaml.
This XDG convention applies on Windows too, with USERPROFILE as home; Unix uses
HOME. The app-specific fallback directory is always filekeep, even if the
executable is renamed. For example, work-settings.exe first searches for
work-settings.yaml and the other extensions.
If multiple supported formats exist in the first matching directory, select one
explicitly with --config-file. An invalid selected file is an error; the app
does not silently load a lower-priority file. Only settings.yaml is searched at
the XDG fallback; other formats there can be selected explicitly.
Templates
Environment tokens support ${VAR}, ${env:VAR}, $(VAR), and %VAR%.
HOME/USERPROFILE resolve to home; HOST/HOSTNAME/COMPUTERNAME resolve to
the machine name. These aliases are case-insensitive. Other environment variable
names follow the platform's case rules. Empty values are allowed; unknown tokens,
cycles, and expansions deeper than 32 levels are errors.
Date tokens support ${date:format}, $(date:format), and %date:format%.
Supported .NET-style components are yyyy, yy, MM, dd, HH, mm, ss,
and fff (milliseconds), with punctuation as separators. For example:
${date:yyyyMMdd-HHmmss-fff}. Unsupported date components are errors. This is a
documented subset of the older helpers' .NET date formatting, not strftime or the
entire .NET formatting language. Every entry uses the same local timestamp
captured at run start; templates are evaluated when preparing that run.
Duplicate detection and filename collisions
Comparison uses exact bytes: BOMs, line endings, trailing whitespace, and binary
content all matter. With skip_if_unchanged: true, compare the captured source
with that entry's previous successful backup, even when today's template gives a
different filename. A match creates no new backup and does not rotate files.
When a new backup would reuse an existing filename:
settings-001.json oldest archived version
settings-002.json next archived version
settings.json newest version, always the configured output name
Existing numbered archives stay in place. The displaced base gets one greater
than the highest existing suffix; gaps are not reused. Numbers have at least
three digits and continue beyond 999. The suffix goes before the final
extension (settings.tar-001.gz); extensionless names also work. Date-stamped
files keep their own numbering series. Nothing is automatically pruned.
Small state files live in <destination>/.filekeep/, keyed by configuration path,
entry ID, and machine. They identify the previous successful filename without
guessing from arbitrary filenames, timestamps, or directory-wide extension
matches. Changing a config path, ID, machine, or destination starts a new history
identity; existing backup content is preserved. Preexisting backups without state
are preserved through collision archiving rather than adopted as previous state.
If the recorded latest file has been removed, a new backup is created. Unknown or
corrupt state is reported and preserved.
File capture streams through a temporary file in the destination. The old base
is preserved under its numbered name before the new base is atomically published;
on supported filesystems a hard link avoids another copy, with an exclusive-copy
fallback elsewhere. This keeps the base present during publication and prevents
overwriting existing archives. A destination lock coordinates filekeep processes;
Windows uses an exclusive delete-on-close marker, and Unix locks the existing
.filekeep directory. No lock marker remains after the owning process exits,
including process termination. JSON state files intentionally remain.
A busy destination fails its entries so they can be retried later. Output paths
that alias this run's settings, another entry's output, or a configured file
source are rejected. Existing output symlinks and directories are rejected.
--dry-run reads sources and previous state to report skips, filenames, and
planned archives, without creating files, directories, or locks. It cannot verify
write permissions or reserve a filename against concurrent changes. File sources
preserve content bytes; timestamps, ACLs, alternate data streams, and application
transactions are outside this tool's file-copy contract. Avoid modifying a source
while it is being captured if a consistent application-level snapshot is required.
Environment captures
On Windows, machine and user read their registry environment keys without
expanding stored %VAR% references. process captures the inherited process
environment on every platform. Machine/user scopes are Windows-specific and
report an entry error on other platforms; no misleading process fallback is used.
Output is deterministic UTF-8 with LF endings, separate scope headings, sorted
variable names, quoted/escaped values, and readable semicolon-separated lists for
names containing path. A scope read failure fails the entire entry instead of
publishing an incomplete dump. Environment values are never printed to the
console, but the backup itself contains their values.
Modular structure
src/config.rs, src/sources/, and src/backup/ own the application schema,
source providers, backup state, orchestration, and error reporting. main.rs owns
the CLI and console output.
power_code/ is an app-independent local library crate for multi-format settings,
discovery, path handling, templates, exact comparison, and safe publication. It
has no dependency on filekeep, which keeps its files suitable for future
synchronization with PowerCode's Rust utilities. The workspace builds one CLI
executable; there is no plugin runtime.
Verification
cd "$HOME\Projects\filekeep"
cargo fmt --all -- --check
cargo clippy --workspace --all-targets --locked -- -D warnings
cargo test --workspace --locked
cargo build --release --locked
cargo run --release --locked -- --dry-run
Tests use temporary directories. They cover exact byte preservation, chronological rotation, numbering gaps, entries/configs with separate state, date changes, failure continuation, output/source conflicts, locks, corrupt/missing state, format equivalence, settings discovery, dry-run, and CLI exit behavior.
See docs/legacy-review.md for the original settings/behavior review.
