Skip to main content

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 DCIM directory, identifying a camera card; or
  • any directory contains a valid recs recording.toml or session-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.jsonl records copied, uploaded, failed, and deferred files. A deferred file is recorded once until it is successfully copied.
  • .lock prevents 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)

Source distribution for baccy 0.2.0
File Size Uploaded
baccy-0.2.0.tar.gz 48.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for baccy 0.2.0
File Interpreter ABI Platform
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}

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page