From b8728925b06d6a9e79b3572058af328db8993578 Mon Sep 17 00:00:00 2001 From: yoshikazu Date: Mon, 27 Jul 2026 15:41:19 +0000 Subject: [PATCH] Add sandboxed folder access --- Sources/Photochk/Services/FolderAccess.swift | 73 ++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 Sources/Photochk/Services/FolderAccess.swift diff --git a/Sources/Photochk/Services/FolderAccess.swift b/Sources/Photochk/Services/FolderAccess.swift new file mode 100644 index 0000000..5faecfd --- /dev/null +++ b/Sources/Photochk/Services/FolderAccess.swift @@ -0,0 +1,73 @@ +import AppKit +import Foundation + +/// フォルダの選択と、サンドボックス下でのアクセス権の保持を担う。 +/// +/// `NSOpenPanel` で選んだフォルダは配下ごとアクセスが許可される。security-scoped bookmark を +/// 保存しておくと、次回起動時に同じフォルダをそのまま開き直せる。 +@MainActor +final class FolderAccess { + private static let bookmarkKey = "lastFolderBookmark" + + private var accessedURL: URL? + + /// フォルダ選択パネルを出す。キャンセルされたら nil。 + func choose(startingAt current: URL?) -> URL? { + let panel = NSOpenPanel() + panel.canChooseFiles = false + panel.canChooseDirectories = true + panel.allowsMultipleSelection = false + panel.prompt = "このフォルダを調べる" + panel.message = "似た写真を探すフォルダを選んでください" + if let current { panel.directoryURL = current } + + guard panel.runModal() == .OK, let url = panel.url else { return nil } + beginAccess(to: url) + saveBookmark(for: url) + return url + } + + /// 前回のフォルダを復元する。無ければ nil。 + func restorePrevious() -> URL? { + guard let data = UserDefaults.standard.data(forKey: Self.bookmarkKey) else { return nil } + + var isStale = false + guard let url = try? URL( + resolvingBookmarkData: data, + options: .withSecurityScope, + relativeTo: nil, + bookmarkDataIsStale: &isStale + ) else { + UserDefaults.standard.removeObject(forKey: Self.bookmarkKey) + return nil + } + + guard FileManager.default.fileExists(atPath: url.path) else { return nil } + beginAccess(to: url) + if isStale { saveBookmark(for: url) } + return url + } + + func releaseAccess() { + accessedURL?.stopAccessingSecurityScopedResource() + accessedURL = nil + } + + // MARK: - 内部 + + private func beginAccess(to url: URL) { + releaseAccess() + if url.startAccessingSecurityScopedResource() { + accessedURL = url + } + } + + private func saveBookmark(for url: URL) { + guard let data = try? url.bookmarkData( + options: .withSecurityScope, + includingResourceValuesForKeys: nil, + relativeTo: nil + ) else { return } + UserDefaults.standard.set(data, forKey: Self.bookmarkKey) + } +}