Skip to content

feat(storage): choose where the container runtime keeps its data - #39

Open
djpfs wants to merge 6 commits into
mainfrom
feat/storage-location
Open

djpfs wants to merge 6 commits into
mainfrom
feat/storage-location

Conversation

@djpfs

@djpfs djpfs commented Oct 21, 2026

Copy link
Copy Markdown
Owner

Summary

  • Settings > Storage > Location: shows the current folder and the size of each part, and moves the whole store to a folder the user picks.
  • The move stops the daemon, copies with ditto, checks the size, and replaces the default path with a symlink. The daemon restarts with CONTAINER_APP_ROOT set to the new path.
  • The original stays as a backup until the user deletes it, with confirmation.
  • Failures roll back: the original is restored and the daemon starts on it again.

Test plan

  • swift test: copy and verify, rollback on copy failure, rollback on size mismatch, refusal of a missing destination and of an already-moved store, restore of the backup, and folder sizes (temporary folders only)
  • Real move on a Mac with a running store (not done here: it moves the live data)

Not included

  • Logs (paths.logRoot) are not moved.
  • Moving back to the default location is not a button yet; the backup can be restored by hand.

djpfs added 6 commits October 21, 2026 12:14
Settings > Storage gets a Location section. Move storage… copies the
runtime store to a folder the user picks, checks the copy's size, and
replaces the default path with a symlink to the copy. The original is kept
as a backup until the user deletes it.

- StorageLocation moves and restores the store with a verified copy
  (ditto). Failures put the original back and remove the partial copy.
- The move stops the daemon and restarts it on the new path with
  CONTAINER_APP_ROOT set. If the restart fails, the original is restored.
- LaunchdManager.start accepts the environment for the daemon.
- The symlink is followed by the CLI and the daemon, so no shell variable
  is needed.

Logs are not moved yet.
- The Location section shows what the move is doing (stopping, copying,
  starting) and the result or the error. Before, a failed or skipped move
  showed nothing.
- Folder sizes use allocated blocks. Logical sizes counted sparse disk
  images at their full length (4.41 TB for a store that uses about 500 MB).
  The copy check still compares logical sizes on both sides.
Sparse disk images report their full length as their size. The space check
used that length, so a store of about 520 MB was reported as needing 4.8 TB.
ditto keeps sparse files sparse, so the copy needs only the allocated bytes.
The copy is still verified by logical size on both sides.
… the main thread

- The copy uses rsync --sparse, which keeps holes in sparse disk images.
  ditto wrote a 550 GB sparse image out in full and filled the destination.
- The copy is watched: if the space it occupies passes 1.5x the source's
  allocated bytes plus 64 MB, rsync is stopped and the move rolls back.
- The copy result is checked against the same limit after it finishes.
- The move runs on a background task, so the window stays responsive during
  a long copy.
rsync reads every byte of a sparse file to find its holes. The store has
nine snapshot images of 513 GB each, holding about 40 MB of data apiece, so
the copy ran for half an hour and copied 211 MB of 649 MB.

bsdtar finds holes with SEEK_HOLE and does not read them. A real 513 GB
snapshot copies in seconds with the same allocated size as the source. The
copy is still watched against the allocation limit, and stopping it also
stops the tar writing to that destination.
When the daemon failed to start on the new location, the original was put
back but the copy stayed on the target disk. The next move then refused with
"something already exists". The unused copy is now removed in the rollback.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant