// // FileBookmarkService.swift // KeeVault // import Foundation /// Persists a security-scoped bookmark for the user's KDBX file in the shared App Group. public final class FileBookmarkService { private static let key = "kdbxBookmark" private let defaults: UserDefaults public init(appGroup: String = "group.org.antiloop222.keevault") { defaults = UserDefaults(suiteName: appGroup) ?? .standard } // `.withSecurityScope` is a macOS-only bookmark-creation option; iOS grants scoped // access automatically once the user picks a file, without that flag. private static var creationOptions: URL.BookmarkCreationOptions { #if os(macOS) return .withSecurityScope #else return [] #endif } private static var resolutionOptions: URL.BookmarkResolutionOptions { #if os(macOS) return .withSecurityScope #else return [] #endif } public func save(url: URL) throws { // For File Provider-backed URLs (e.g. iCloud Drive), creating a bookmark without // an active security scope produces one that resolves later with // NSCocoaErrorDomain Code=4 ("The file doesn't exist"), even immediately after picking. let accessing = url.startAccessingSecurityScopedResource() defer { if accessing { url.stopAccessingSecurityScopedResource() } } let data = try url.bookmarkData( options: Self.creationOptions, includingResourceValuesForKeys: nil, relativeTo: nil ) defaults.set(data, forKey: Self.key) } /// Returns the resolved URL, starting security access. Caller must call `stopAccess(url:)` when done. public func resolveURL() throws -> URL { guard let data = defaults.data(forKey: Self.key) else { throw BookmarkError.notFound } var isStale = false let url = try URL( resolvingBookmarkData: data, options: Self.resolutionOptions, relativeTo: nil, bookmarkDataIsStale: &isStale ) if isStale { let fresh = try url.bookmarkData(options: Self.creationOptions, includingResourceValuesForKeys: nil, relativeTo: nil) defaults.set(fresh, forKey: Self.key) } guard url.startAccessingSecurityScopedResource() else { throw BookmarkError.accessDenied } return url } public func stopAccess(url: URL) { url.stopAccessingSecurityScopedResource() } public func clear() { defaults.removeObject(forKey: Self.key) } public var hasBookmark: Bool { defaults.data(forKey: Self.key) != nil } } public enum BookmarkError: Error { case notFound case accessDenied }