baccy
baccy is a permanent backup library and macOS command-line application for
audio files, photos, and other assets. It copies configured local directories,
memory cards, and already-mounted network shares to one central backup root.
It can run once from a terminal or continuously as a per-user LaunchAgent.
Baccy never deletes backups because a source disappears. A changed source replaces its previous backup copy; baccy does not retain overwritten versions.
Install
Use uv from a checkout:
uv sync
The baccy command is then available through uv run baccy.
Configuration
By default baccy looks for:
~/Library/Application Support/baccy/config.toml
Without an installed daemon, the default configuration would use automatic removable-drive discovery and write selected backups to:
~/baccy
CLI commands use the installed daemon's recorded configuration by default.
Use --config PATH to run without an installed daemon. A missing path supplied
explicitly with --config is an error.
Pass --config PATH before the command to use a different file.
--daemon explicitly selects the default daemon configuration. It cannot be
combined with --config. Global flags must precede the command.
backup_root = "/path/to/baccy"
discover_removable = true
poll_seconds = 60
stability_seconds = 60
s3_max_bandwidth = 1_000_000
verbose = true
[[uploads]]
name = "main-mp3"
match = "main and duration > 120"
encoding = { format = "mp3", bitrate_kbps = 128 }
destination = "ssh:user@example.org:/srv/shows"
[[uploads]]
name = "channel-archive"
match = "True"
encoding = { format = "flac" }
destination = "s3:show-recordings"
[[sources]]
kind = "path"
name = "recs"
path = "/Volumes/Recordings/recs"
exclude = [".DS_Store"]
[[sources]]
kind = "volume"
name = "camera-card"
uuid = "F0B5CF26-1C8C-4E33-B36D-4D4F84C2F685"
relative_path = "DCIM"
expected_name = "CAMERA"
Path sources are fixed local directories or network shares that macOS has
already mounted. Volume sources are found below /Volumes by volume UUID, not
by their display names. Find the UUID for a mounted card with:
diskutil info -plist /Volumes/CAMERA
With discover_removable = true, the default, baccy also examines newly
mounted removable or ejectable volumes that are not already configured. It
selects content only when either:
- the volume root contains a
DCIMdirectory, identifying a camera card; or - any directory contains a valid recs
recording.tomlorsession-record.jsonl, identifying recs sessions.
For a camera volume, only recognized photo files below DCIM are backed up;
videos, sidecars, manuals, and unrelated files are ignored. Supported photo
families include JPEG, HEIC/HEIF, PNG, TIFF, DNG, and common camera RAW formats.
For a recs volume, every file inside each detected session directory is backed
up, preserving the portable session; unrelated files elsewhere on the volume
are ignored.
Other unconfigured removable drives are ignored. Automatically discovered
volumes use their filesystem UUID as part of the backup source identity, so a
renamed volume continues in the same destination. Volumes without a UUID and
the volume containing backup_root are ignored. Set
discover_removable = false to use configured sources only.
include and exclude are optional lists of path-match patterns. Sources use
include = ["**"] and no exclusions by default. Source names must be unique,
and neither a source nor the backup root may contain the other.
Set verbose = true to include unchanged files in command output. When the
installed service is running, it also sends macOS notifications when it
recognizes a backup disk and when a disk or network-machine backup pass
finishes. Machine recognition is logged without a notification. The current
default includes unchanged individual results while
retaining the unchanged count.
Project publication
Projects are the first directory below any recs source baccy has backed up.
Each uploads rule independently selects finalized audio segments, so one
recording can create several artifacts. Publication always reads the completed
copy in the backup root, never an active recorder or removable drive.
match is a restricted expression over duration, main, device,
channels, track, format, player, and has_player. It supports
comparisons, membership, and, or, and not, but no calls, attributes,
subscripts, arithmetic, or Python evaluation. The default main tracks are
the two highest numbered channels of the device with the greatest observed
channel number. If devices tie, rules referring to main are deferred.
Rules encode source, flac, or mp3. MP3 rules require bitrate_kbps.
Derived FLAC files are written atomically to artifacts/ under the backup
root, keyed by source content and the complete rule definition. MP3 files are
encoded in /tmp and deleted after each upload attempt. The permanent source
backup remains authoritative.
Uploads preserve their session-relative paths. Encoded derivatives change only the file extension. Baccy rejects targets that collide among the artifacts and landing pages planned in the same pass before it starts an encoder or upload.
SSH destinations use the existing non-interactive SSH configuration and keys.
S3 destinations use boto3 and the host's normal AWS credential chain. Baccy
accepts ssh:USER@[IPv6]:/absolute/path for IPv6 hosts and
s3:BUCKET/PREFIX for keys under a bucket prefix. An S3 endpoint override
comes from the default profile in ~/.aws/config (endpoint_url), not from
the destination string. Baccy records an artifact identity in S3 object
metadata and skips an object with the same identity. s3_max_bandwidth limits
all S3 uploads in bytes per second.
Credentials never appear in the TOML or event log.
Run once
Run one complete scan and exit:
uv run baccy backup
uv run baccy --config /path/to/baccy.toml backup
uv run baccy --dry-run backup
The first form uses the installed daemon's recorded configuration. To run
without an installed daemon, pass --config PATH before the command.
The command prints one path per completed file, with status and detail for failed or deferred work. It exits with status 1 for failures or unavailable sources, 3 for deferred work without failures, and 0 for a complete pass. A missing removable card or mounted share does not delete prior backups.
For recs sessions, baccy appends only newly completed lines from
session-record.jsonl after verifying its already backed-up prefix. WAV and
FLAC files named by an unfinished file_started record are deferred. They are
copied atomically once recs writes a matching file_finished record.
Use -d or --dry-run before the command to preview would_copy and
would_upload results without creating the backup root, lock, event log,
artifact cache, temporary files, or network connections.
baccy -d watch repeatedly performs the same non-writing preview.
Repair zero frame counts
Repair historical completed-audio records whose frame_count is zero from the
FLAC or WAV file headers:
uv run python scripts/repair_zero_frame_counts.py ~/baccy/audio/totm
Import recs sessions
Import one or more recs project directories or individual session directories:
uv run baccy --config /path/to/baccy.toml import /Volumes/Recordings/concert
uv run baccy --config /path/to/baccy.toml import /Volumes/Recordings/2026/09/24/20-00-00 --project concert
uv run baccy --config /path/to/baccy.toml import /Volumes/Recordings/concert --copy
By default import moves each session into its canonical location under the
backup root. --copy leaves the original directory unchanged. It uses a
session header's project_name first; --project NAME overrides it. For a
project directory whose session headers omit the project, its directory name is
used. An individual session without a header project requires --project.
For a project directory, the existing YEAR/MONTH/DAY/TIMESTAMP layout is
preserved. For an individual session, baccy derives YEAR/MONTH/DAY from the
session's started_at header and keeps its timestamp directory name. Import
does not publish anything. Run baccy sync after import to apply configured
upload rules.
Sync publication
Reconcile every publishable session, or only selected project/directory paths below the backup root:
uv run baccy sync
uv run baccy sync concert
uv run baccy sync concert/2026/09
uv run baccy sync --verify
sync lists each configured remote destination once and compares remote file
sizes with the local source files or recorded artifact sizes when available.
For S3 objects with a matching catalog record, it also checks the remote
baccy-identity metadata. It does not download or hash remote files. An SSH
file replaced with different content of the same size can still go undetected;
derived files without a recorded size can only be checked for nonzero size.
Normal publication trusts the local catalog and can therefore skip a remote file
that was deleted outside baccy. Run sync to repair missing remote files.
Use sync --verify to compare SHA-256 hashes of local artifacts and remote
content that would otherwise be considered unchanged, including same-size SSH
replacements. This reads those remote files and may take a long time or incur
transfer charges. It runs in the foreground,
including when --daemon supplies the configuration; ordinary sync still
schedules a daemon pass and returns promptly.
Rename direct S3 backups
baccy rename PATTERN REPLACEMENT previews direct-source audio renames and
asks for confirmation. Use --re for a regular-expression pattern and put
--dry-run before rename for a preview without changes. The command copies
and verifies all new S3 objects before renaming local files and rewriting
session journals; it deletes the old S3 objects last. Progress is saved in
BACKUP_ROOT/rename-progress.json. If interrupted or failed, rerun the same
command and arguments to resume. Backups, imports, and syncs will not modify
the backup root while a rename is pending. Rename events are written as
timestamped JSON lines in BACKUP_ROOT/events.jsonl.
Test upload access
Test authentication and access to every configured SSH and S3 destination without scanning or uploading files:
uv run baccy test
uv run baccy --config /path/to/baccy.toml test
It prints ok on success. On failure, it prints each inaccessible destination
and its error to standard error, then exits with status -1 (reported by macOS
as 255).
Watch in the foreground
Run repeated scans in the current terminal:
uv run baccy watch
SIGINT and SIGTERM stop the watcher after its current copy operation. The
watcher calls the same one-pass backup engine as baccy backup; it is not a
second backup implementation.
Every 10 seconds, watch reads the local ARP table for newly visible systems. It
uses non-interactive SSH and requires the host's verified key to be in the
user's SSH known_hosts file. Unknown or changed keys are rejected. Verify
each recording machine's host-key fingerprint independently before trusting
it; an unverified ssh-keyscan result alone is not proof of identity. The
machine must also accept the user's existing SSH credentials. Configured
kind = "network" sources are rejected because they were never resolved; use
trusted-host automatic discovery instead.
Each newly seen MAC address gets an immediate SSH attempt, with at most eight
hosts probed concurrently and, for connection failures, one retry after two
seconds and another after four seconds. A previously discovered recording
source that disappears from the ARP table is reported as unavailable.
Authentication rejections and hosts without a ~/recs directory are not
retried until baccy restarts. A host that accepts SSH but does not yet have
~/recs is checked again on each later ARP scan. With verbose = true, baccy
logs newly recognized SSH-capable machines without sending a notification.
Multicast and broadcast ARP entries are ignored.
Qualifying ~/recs directories are copied as network sources, with the catalog
skipping files whose remote size and modification time have not changed.
With verbose = true, the service log records each newly observed network
host, whether SSH failed or ~/recs was absent, and recognized-source backup
start and completion. Watch it with:
tail -f ~/Library/Logs/baccy/baccy.log
When running as the installed service, baccy sends a macOS notification for a new copy failure. It does not notify for unavailable configured sources, unplugged removable media, or rejected network hosts. Repeated identical failures are reported once until they recover or change.
Start automatically after login
Install the per-user LaunchAgent:
uv run baccy --config /path/to/baccy.toml service install
uv run baccy service status
baccy install is shorthand for baccy service install and accepts the same
flags, including --no-sync.
Installation waits for the daemon to start, then schedules a sync. Pass
--no-sync to install without scheduling that initial sync.
On success, installation prints ok. Pass the global --verbose or -v flag
before the command to print the full service status instead. Build and install
output is shown only if installation fails.
The agent uses launchd with RunAtLoad and KeepAlive. Installation builds
an isolated release environment under ~/Library/Application Support/baccy/,
so the service does not run code from this checkout. It starts after the user
logs in after a restart and restarts after an unexpected exit. It has the same
user access to mounted volumes and network shares as baccy watch.
uv run baccy service stop
uv run baccy service start
uv run baccy service restart
uv run baccy service uninstall
Run uv run baccy service install again to explicitly deploy a newer checkout.
service restart only restarts the installed release.
This is intentionally a per-user LaunchAgent, not a privileged LaunchDaemon. It does not run before login. Pre-login backups would require a separate system-wide deployment and credentials policy.
recs sessions
baccy treats a recs session as ordinary portable files and never imports recs,
rewrites its files, or attempts finalization or migration. It preserves the
whole session-relative layout, so paths in recording.toml and
session-record.jsonl continue to be meaningful in a restored session.
Within a scan baccy copies TOML before JSONL, then copies other files. That
means recording.toml, session-record.jsonl, native MIDI, OSC, and key event
streams are backed up before large audio assets.
JSONL is handled as an append-only snapshot: baccy copies the complete prefix
that existed at the start of the copy, requires it to end on a newline, and
verifies that prefix again before committing it. This permits backing up an
active recs journal without waiting for recording to stop. TOML, audio, photos,
and other ordinary files must remain unchanged for stability_seconds before
they are copied.
Backup layout and recovery
Project sessions are stored in their one canonical location:
BACKUP_ROOT/audio/PROJECT/SESSION/RELATIVE_PATH
New recs sessions are copied directly to the canonical project directory.
Non-project assets, such as photos, remain source-specific:
BACKUP_ROOT/photo/SOURCE_NAME/RELATIVE_PATH
At the backup root:
events.jsonlrecords copied, uploaded, failed, and deferred files. A deferred file is recorded once until it is successfully copied..lockprevents concurrent backup passes against one backup root.
Each new file is copied to a temporary file beside its destination, flushed to disk, and atomically renamed only after the source has passed its snapshot checks. A partial copy is never promoted to the visible destination. When a file changes, baccy atomically replaces the existing destination.
To restore a project session, copy it from PROJECT/SESSION/.
Version-one boundaries
baccy does not mount shares, connect to remote hosts, manage credentials, or publish to a public server. Configure a mounted share as a path source instead. It also does not preserve ownership or extended attributes in this version.
License
baccy is licensed under the MIT License.
Metadata
Release files for baccy 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| baccy-0.2.0.tar.gz | 48.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| baccy-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 107.8 kB
Release files / baccy-0.2.0.tar.gz
| Download URL | baccy-0.2.0.tar.gz |
|---|---|
| Size | 48.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
afe6aefe3535d84da03617de30a973eeca88711ae1232677fc803335c54d0a6f
|
|
BLAKE2b-256 checksum How to use checksums |
ab02f61a740e88391198e1addfbb7119b180f8133a22c8f17bf2630decc89fed
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.10.12 {"installer":{"name":"uv","version":"0.10.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / baccy-0.2.0-py3-none-any.whl
| Download URL | baccy-0.2.0-py3-none-any.whl |
|---|---|
| Size | 59.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5741ed85b95dcb2226d4d32d26bf484d895bdc38597120e96f57faefa0489609
|
|
BLAKE2b-256 checksum How to use checksums |
5373294686468ee72eb734c4ef203d87df8b8fe544d22f9c4517ac1499b19469
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.10.12 {"installer":{"name":"uv","version":"0.10.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|