Prometheus

NAS Workflow — mount, auto-sort Downloads, archive code, direct-save screenshots

Everything that touches /Volumes/media — how it gets mounted, what auto-sorts onto it, how stale code projects get archived, and where screenshots land.

Everything that touches the TrueNAS share at /Volumes/media — how it gets mounted, what auto-sorts onto it, how stale code projects get archived to it, and where screenshots land. For the Time Machine deep-dive, see Backup.

Mounting the share

The share smb://<USER>@<NAS-IP>/media is mounted at /Volumes/media by a hidden Finder Login Item. install.sh renders nas/truenas-media.inetloc.template with your real NAS_HOST / NAS_USER from ~/.config/dotfiles/local.env, drops it under ~/Library/Application Support/dotfiles/, and registers it as a Login Item. Credentials come from the macOS Keychain, saved on the first manual Finder mount.

re-mount by hand (if Finder ever drops it)
osascript -e 'mount volume "smb://<USER>@<NAS-IP>/media"'

Retry layer — nas-mount + LaunchAgent

The .inetloc Login Item fires the moment you log in — often before Wi-Fi has associated. When that happens Finder errors out and never retries, leaving you staring at an unmounted share until you click media in the Finder sidebar manually. bin/nas-mount is the fix.

It checks whether /Volumes/media is already mounted (instant early-out). If not, it asks Finder to mount the share via osascript — the same path the .inetloc uses, so the saved Keychain credential just works. No mount_smbfs, no Auth-77, no mkdir race on /Volumes. Six attempts with 15 s between, ~90 s total budget — comfortably covering Wi-Fi association + DHCP + mDNS at login.

A LaunchAgent (com.prometheus.nas-mount.plist) wraps the script and fires it at every login. Both the .inetloc and the agent run on login; whichever wins the race, the other no-ops.

nas-mount usage
nas-mount               # retry until mounted, ~90 s budget
nas-mount -1            # one attempt, no retry loop
nas-mount -v            # verbose: tee log to stdout

# fires automatically at login via com.prometheus.nas-mount LaunchAgent
launchctl start com.prometheus.nas-mount   # kick by hand
tail -f ~/.nas-mount/nas-mount.log         # watch the chain

Exit codes: 0 = mounted (already or just now), 1 = NAS not configured in local.env, 2 = retries exhausted. Installer wires both pieces only when HAS_NAS=true.

Downloads auto-sort — sort-downloads

A LaunchAgent watches ~/Downloads via WatchPaths and fires bin/sort-downloads whenever a file appears. The script classifies each file by extension (or name, for Screenshot *) and routes it to the matching category folder on the NAS. Plist keys: WatchPaths, ThrottleInterval=30, RunAtLoad=true, StartOnMount=true, StartInterval=300.

category routing
screenshots   · PNG / JPEG named "Screenshot *" or "Screen Shot *"
Pictures      · jpg jpeg png gif webp heic heif tiff bmp svg raw
PDFs          · pdf
Documents     · doc docx xls xlsx ppt pptx odt txt md rtf csv epub
Installers    · dmg pkg iso mpkg
Movies        · mp4 mov mkv webm avi m4v flv wmv
Music         · mp3 m4a wav flac ogg aac aiff
Archives      · zip tar gz tgz bz2 7z rar xz zst
Other         · everything else

Safety: bails cleanly if the NAS isn’t mounted, skips partial downloads (.crdownload, .part, …), waits 30 s for in-flight files to settle, silently ignores macOS metadata (.DS_Store, .localized, AppleDouble shadows). Cross-volume copy uses cp -Xp into a .partial-$$ temp file, then renames to final, then unlinks source — a NAS dropout mid-copy never strands a file half-way.

Reconnect handling. At boot the agent’s RunAtLoad fires before the NAS has come up, so it bails (“NAS not mounted — skipping sweep”) and leaves files in place. The chain is restarted by nas-mount: once it successfully mounts the share, it directly execs sort-downloads to clear the backlog. We don’t rely on the plist’s StartOnMount for this — verified empirically that the disk-arbitration event launchd watches isn’t always emitted for SMB shares mounted via Finder + osascript. The key stays on the plist as belt-and-suspenders (it’s idempotent), but the deterministic chain is the direct exec from nas-mount.

manual usage
sort-downloads -n     # dry-run, just print decisions
sort-downloads -v     # verbose: log skip reasons
sort-downloads        # one sweep (what launchd calls)
tail -f ~/.sort-download/sort-downloads.log

Screenshots direct to NAS

Configure your screenshot tool (Shottr, the built-in ⌘⇧4, CleanShot, whatever) to save directly to /Volumes/media/screenshots/. Bypasses the LaunchAgent permission problem entirely, and every capture is on the NAS the moment you press the shutter.

macOS built-in screenshot tool — set save location
defaults write com.apple.screencapture location /Volumes/media/screenshots/
killall SystemUIServer

For Shottr: Preferences → General → Save to → /Volumes/media/screenshots/. The folder must exist (one-time mkdir -p /Volumes/media/screenshots if it doesn’t).

Code archive — archive-project

Manual tool that recursively scans ~/Code/ for git repos and offers to move stale ones to /Volumes/media/code/archived/, preserving the original folder layout. Run when you want to free SSD space without losing finished work.

Decision logic

A project is offered as a candidate only when all three of these are true:

  1. It’s a regular git repo (.git/ directory). Worktrees, submodules, and repos nested inside another repo are skipped.
  2. The working tree is fully clean — git status --porcelain prints nothing. Any uncommitted change — staged, unstaged, or untracked — means the project stays put, no matter how old.
  3. The last commit is older than ARCHIVE_AFTER_MONTHS months (default 1; tweakable at the top of bin/archive-project or via --months=N).
common runs
archive-project              # interactive, asks Y/N/q per candidate
archive-project -n           # dry-run (preview only)
archive-project --months=3   # stricter: only 3+ months old
archive-project -y           # yes to all candidates
archive-project --keep-deps  # don't strip node_modules etc.
archive-project --man        # open the man page
archive-project -h           # short help

What gets stripped before the move

By default the script blows away universally regeneratable build output anywhere in the project tree — before moving anything. Saves multiple GB of SMB transfer on RN / Flutter / Node monorepos. --keep-deps disables this.

STRIP_DIRS (depth-agnostic, matched by name)
# JS / Node
node_modules .next .nuxt .svelte-kit .astro .turbo .vite .parcel-cache .cache

# iOS / Xcode / CocoaPods

Pods DerivedData

# Android / Gradle (RN + Flutter both use this)

.gradle

# React Native / Expo

.expo .expo-shared

# Flutter / Dart

.dart_tool

# Python

**pycache** .pytest_cache .mypy_cache .ruff_cache

Deliberately left alone: build/, dist/, out/, target/, venv/ — their names are too generic to delete safely by default. Run clean-node-modules or strip them by hand first if you want them gone too.

Atomic move semantics

The “move” is copy-then-remove, never mv:

  1. cp -RXp src dst — recursive, no extended attributes (so SMB doesn’t choke on com.apple.provenance / com.apple.quarantine), preserves mode + mtime.
  2. If the copy completes end-to-end, the SSD source is removed with rm -rf.
  3. If the copy fails, the partial destination is wiped and the SSD source is kept for the next try.

A NAS dropout or full-disk error mid-move cannot strand you with half a project on each side.

Where to tweak

bin/archive-project — config block at the top
ARCHIVE_AFTER_MONTHS=1
CODE_DIR="$HOME/Code"
ARCHIVE_DIR="/Volumes/media/code/archived"
NAS_MOUNT="/Volumes/media"
LOG="$HOME/.archive-project/archive.log"

Time Machine to TrueNAS

Monthly Time Machine backups to a separate SMB share (smb://<USER>@<NAS-IP>/timemachine) are managed by a root LaunchDaemon, not the user-scoped tools on this page. The full setup — share creation on TrueNAS, replacing the hourly schedule, helper commands (tm-status, tm-backup), exclusion rules — lives on the dedicated Backup page.

Cheat sheet

at a glance
cd /Volumes/media                                # jump in (also: alias `media`)
open /Volumes/media                              # open in Finder

nas-mount                                        # re-mount with retries
nas-mount -1                                     # one attempt, no retry
sort-downloads -n                                # preview Downloads auto-sort
archive-project -n                               # preview code archive candidates
archive-project --months=6                       # stricter one-off threshold

tail -f ~/.nas-mount/nas-mount.log               # mount + chain history
tail -f ~/.sort-download/sort-downloads.log      # auto-sort activity
tail -f ~/.archive-project/archive.log           # archive history

osascript -e 'mount volume "smb://&lt;USER&gt;@&lt;NAS-IP&gt;/media"' # re-mount by hand
On this page
Edit this page on GitHub Last updated

Built by rafay99-epic · MIT License

Tokyo Night · macOS · SketchyBar · AeroSpace · Ghostty · Zsh · Fish