StorageLocationManager

Manages user-selected storage location with security-scoped bookmark persistence.

Overview

Located in Minuta/Sources/Services/StorageLocationManager.swift.

This is a @MainActor ObservableObject that handles:

  • User folder selection via system picker
  • Security-scoped bookmark creation and restoration
  • Persistence across app launches
  • Fallback to default location

Properties

@Published private(set) var currentURL: URL?        // Selected folder URL (nil = default)
@Published private(set) var hasSelectedLocation: Bool  // Whether user has made a selection
var storageURL: URL                                  // Resolved URL (selected or default)

Key Methods

saveBookmark(for:)

Saves a security-scoped bookmark for the selected URL:

func saveBookmark(for url: URL) throws
  • Starts accessing security-scoped resource
  • Creates bookmark with appropriate options (Mac Catalyst vs iOS)
  • Stores bookmark data in UserDefaults
  • Updates published properties

restoreBookmark()

Restores URL from saved bookmark:

func restoreBookmark() throws -> URL
  • Called on init if user has previously selected location
  • Handles stale bookmarks by attempting refresh
  • Starts accessing security-scoped resource

useDefaultLocation()

Uses the default ~/Documents/Minuta location:

func useDefaultLocation()
  • Clears stored bookmark
  • Marks location as selected (to skip first-launch prompt)

Default Location

storageURL falls back to <Documents>/Minuta when no folder is selected. What <Documents> means differs per platform:

  • Mac Catalyst (Homebrew build, not sandboxed): the real ~/Documents, so the folder sits next to the user’s other documents and is picked up by iCloud Drive “Desktop & Documents” sync if that is on.

  • Mac App Store build (sandboxed): the container’s Documents, invisible in Finder. Planned, see 917-plan-mac-testflight. This build also does not publish the CLI discovery pointer, see CLI discovery pointer.

  • iOS: the app container’s Documents. It is exposed to the user through two Info.plist keys (Minuta/Sources/Info.plist):

    • UIFileSharingEnabled publishes the container’s Documents in the Files app under On My iPhone > Minuta (the data lives in the Minuta subfolder there) and in Finder’s file-sharing pane when the device is connected.
    • LSSupportsOpeningDocumentsInPlace lets Files and other apps open those files where they are instead of copying them.

    Without both keys the default folder is invisible on the phone, and users cannot find, back up, or move their data. The folder is still local-only: it does not sync to iCloud Drive. Users who want cloud sync pick an iCloud Drive / Dropbox folder in Settings, or wait for the iCloud default described in 918-plan-icloud-default.

    Files edited from the Files app while Minuta is open are picked up by ExternalChangeMonitor (mtime polling), same as a sync-service drop.

resetToDefault()

Resets to show first-launch folder selection:

func resetToDefault()
  • Clears stored bookmark
  • Marks location as not selected

isCurrentLocationAccessible()

Checks if current storage location is writable:

func isCurrentLocationAccessible() -> Bool

Security-Scoped Bookmarks

Different bookmark options for Mac Catalyst vs iOS:

Mac Catalyst

url.bookmarkData(options: .withSecurityScope, ...)
URL(resolvingBookmarkData: data, options: .withSecurityScope, ...)

iOS

url.bookmarkData(options: .minimalBookmark, ...)
URL(resolvingBookmarkData: data, options: [], ...)

Error Handling

enum StorageLocationError: Error {
    case noBookmarkFound
    case bookmarkCreationFailed(Error)
    case bookmarkResolutionFailed(Error)
    case accessDenied
    case staleBookmark
}

Usage Flow

First Launch

  1. App checks hasSelectedLocation
  2. If false, shows StorageSetupView
  3. User picks folder or uses default
  4. saveBookmark() or useDefaultLocation() called

Subsequent Launches

  1. Init calls restoreBookmark() if hasSelectedLocation
  2. If restoration fails, resets to show folder picker
  3. storageURL property provides current location

Changing Folder

  1. User opens Settings, taps “Data Folder”
  2. FolderPickerView shown
  3. On selection, saveBookmark(for:) called
  4. AppState.reinitializeStorage() called to reload data

Integration with AppState

AppState uses StorageLocationManager’s storageURL:

let url = storageLocationManager.storageURL
storage = LocalFileStorageService(baseURL: url)

CLI discovery pointer

publishResolvedStoragePath() runs on init(), saveBookmark(for:) and useDefaultLocation(). It writes two things through the injected StoragePathPublishing:

  • the CLI pointer <realHome>/Library/Application Support/minuta/storage-path.txt, holding the resolved storage folder, so the minuta CLI can find a user-picked folder;
  • the .minuta-version marker inside the storage folder.

The pointer is skipped in two cases; the version marker is written in both:

  • App Sandbox (Mac App Store build): isSandboxed is true when APP_SANDBOX_CONTAINER_ID is present in the environment. The pointer path is outside the container, so the write is not attempted. The CLI then never learns the app’s folder: it falls back to the real ~/Documents/Minuta (not the container’s Documents this build defaults to) unless the user passes --storage or sets MINUTA_STORAGE. A pointer left by an earlier non-sandboxed build is not updated either.
  • UI-test launches (--test-storage=): a throwaway test folder must not be advertised to the CLI.

Discovery order on the CLI side: CLI Reference. Mechanics: Cross-Process Safety.