FS Router

FS Router is a macOS menu-bar application for routing selected local cache directories to a mounted external volume. When a mapping is active, its local path is an owned symbolic link to the corresponding directory on the volume. When routing is disabled or the volume is unavailable, FS Router restores a real local directory without copying the external cache back.

Stop builds, package installers, and other cache users before switching or cleaning caches. Unplugging a drive can interrupt active work. FS Router cannot detect every open file or guarantee uninterrupted access.

Requirements and build

  • macOS 13 or later
  • Swift 6 and a working Xcode or Command Line Tools installation
  • A writable mounted volume for external caches

From the repository root:

swift test
bash scripts/build-app.sh
open "dist/FS Router.app"

The build script creates an ad-hoc-signed app bundle. FS Router runs in the menu bar and does not require administrator privileges or a background helper.

Configuration

The default configuration is ~/.config/fs-router/config.json. The same directory holds the ownership registry, transaction journals, and an instance lock. Set FS_ROUTER_CONFIG_DIR to use a different directory for all of these files, for example when testing:

FS_ROUTER_CONFIG_DIR="$HOME/.config/fs-router-test" \
  "dist/FS Router.app/Contents/MacOS/FSRouter"

FS Router creates a default configuration if none exists. All default mappings start disabled. Open and edit the JSON using Open Config, then select Reload Config in the popover. The app rejects invalid documents and refuses to overwrite external edits until they have been reloaded.

A portable version-1 example:

{
  "version": 1,
  "enabled": true,
  "volume": {
    "mountPath": "/Volumes/data",
    "uuid": null,
    "cacheRoot": "fs-router-caches"
  },
  "entries": [
    {
      "id": "go-build",
      "name": "Go build cache",
      "localPath": "~/Library/Caches/go-build",
      "externalPath": "go-build",
      "enabled": false,
      "onAttach": "ask",
      "pruneAfterDays": 30
    },
    {
      "id": "gradle",
      "name": "Gradle caches",
      "localPath": "~/.gradle/caches",
      "externalPath": "gradle",
      "enabled": false,
      "onAttach": "ask",
      "pruneAfterDays": 30
    }
  ]
}
  • mountPath is the initial volume-binding hint. The app records a mounted volume's UUID and subsequently matches that UUID, even if the mount path changes. To use a different disk, explicitly edit the UUID in JSON.
  • cacheRoot is a relative directory on the volume. Use "." to address existing cache directories directly beneath the volume; each externalPath must still identify a non-root relative path.
  • localPath must be inside your home directory. Local and external mappings must not overlap other mappings. Missing cache directories can be created when routing is enabled.
  • onAttach is one of ask, copyLocal, replaceExternal, dropLocal, or useLocal. pruneAfterDays must be a positive whole number.
  • Tool-specific environment variables and settings can override the example paths. Verify the effective paths for Go, Gradle, Xcode, package managers, and other tools before enabling their mappings. Do not map entire user homes, SDKs, project directories, simulator devices, or the volume root as disposable caches.

The app's Open Config and Reveal Config actions locate the actual active configuration. The configuration is machine-specific and is not included in the repository.

Attachment choices

When an enabled mapping uses Ask, choose one action for the current attachment. Always for this mapping saves the choice to JSON.

Action Effect
Copy local to SSD Merge local and external trees. Identical files are retained; for different entries, the newer modification time wins. Equal timestamps keep the external entry.
Replace SSD with local Replace the entire mapped external cache with the local tree. External-only files are removed; an empty local cache replaces the external cache with an empty one.
Drop local Discard the local cache after switching to the existing external cache, or an empty external cache if none exists.
Use local Keep the local directory and leave the external cache untouched for this attachment.

Successful external routing removes the original local cache after the link is committed; it is not a permanent duplicate. Disable an entry or global routing to return to an empty local directory while leaving the external cache intact. Normal Quit also attempts to restore owned links to local directories.

Maintenance and recovery

The popover provides local size, Refresh Size, Clear Cache, and Remove Old Files. Cleanup is for real local directories, not externally routed caches. Clear permanently deletes the local cache's contents while retaining the root directory. Age-based removal previews the count and bytes first; a file is eligible only when both access and modification times are older than the chosen cutoff. Access timestamps may be incomplete and do not prove a file is unused.

If a transaction cannot safely finish or recover, FS Router reports an error and may retain a backup or journal. Keep the drive connected, preserve the paths shown in the error, and use Reveal Recovery or Retry Recovery. If a local directory or foreign link conflicts with a retained backup, do not delete either copy; consolidate them manually before retrying.

The application uses no-follow filesystem operations and checks recorded identities for owned links and transaction backups. These safeguards do not eliminate races with other programs writing to a cache. Power loss, forced termination, or disconnecting a drive during a transaction can require recovery; use disposable cache data and maintain backups of anything important.

S
Description
No description provided
Readme
82 KiB
0 Stars 1 Watchers 0 Forks
Languages
Swift 99.8%
Shell 0.2%