mirror of
https://gitlab.com/ArkHost/HelixNotes.git
synced 2026-07-24 07:45:57 +02:00
313 lines
10 KiB
Swift
313 lines
10 KiB
Swift
import Foundation
|
|
import SwiftRs
|
|
import Tauri
|
|
import UIKit
|
|
import UniformTypeIdentifiers
|
|
|
|
private struct BookmarkRequest: Decodable {
|
|
let bookmarkId: String
|
|
}
|
|
|
|
final class IosVaultAccessPlugin: Plugin, UIDocumentPickerDelegate {
|
|
private let fileManager = FileManager.default
|
|
private let lock = NSLock()
|
|
private var pickerInvoke: Invoke?
|
|
private var activeBookmarkId: String?
|
|
private var activeURL: URL?
|
|
private var stagedBookmarkId: String?
|
|
private var stagedURL: URL?
|
|
|
|
deinit {
|
|
rollbackStagedScope()
|
|
releaseCurrentScope()
|
|
}
|
|
|
|
@objc func chooseFolder(_ invoke: Invoke) {
|
|
DispatchQueue.main.async {
|
|
guard self.pickerInvoke == nil else {
|
|
invoke.reject("A folder picker is already open.")
|
|
return
|
|
}
|
|
guard let viewController = self.manager.viewController else {
|
|
self.pickerInvoke = nil
|
|
invoke.reject("Files is unavailable right now. Try again.")
|
|
return
|
|
}
|
|
self.pickerInvoke = invoke
|
|
let picker = UIDocumentPickerViewController(
|
|
forOpeningContentTypes: [.folder],
|
|
asCopy: false
|
|
)
|
|
picker.delegate = self
|
|
picker.allowsMultipleSelection = false
|
|
viewController.present(picker, animated: true)
|
|
}
|
|
}
|
|
|
|
@objc func resolveFolder(_ invoke: Invoke) throws {
|
|
let request = try invoke.parseArgs(BookmarkRequest.self)
|
|
do {
|
|
invoke.resolve(try resolveBookmark(request.bookmarkId))
|
|
} catch {
|
|
invoke.reject(message(for: error))
|
|
}
|
|
}
|
|
|
|
@objc func releaseActive(_ invoke: Invoke) {
|
|
rollbackStagedScope()
|
|
releaseCurrentScope()
|
|
invoke.resolve()
|
|
}
|
|
|
|
@objc func commitStaged(_ invoke: Invoke) {
|
|
commitStagedScope()
|
|
invoke.resolve()
|
|
}
|
|
|
|
@objc func rollbackStaged(_ invoke: Invoke) {
|
|
rollbackStagedScope()
|
|
invoke.resolve()
|
|
}
|
|
|
|
@objc func forgetBookmark(_ invoke: Invoke) throws {
|
|
let request = try invoke.parseArgs(BookmarkRequest.self)
|
|
do {
|
|
let file = try bookmarkFile(for: request.bookmarkId, createDirectory: false)
|
|
lock.lock()
|
|
let isActive = activeBookmarkId == request.bookmarkId
|
|
let isStaged = stagedBookmarkId == request.bookmarkId
|
|
lock.unlock()
|
|
if fileManager.fileExists(atPath: file.path) {
|
|
try fileManager.removeItem(at: file)
|
|
}
|
|
if isActive {
|
|
releaseCurrentScope()
|
|
}
|
|
if isStaged {
|
|
rollbackStagedScope()
|
|
}
|
|
invoke.resolve()
|
|
} catch {
|
|
invoke.reject(message(for: error))
|
|
}
|
|
}
|
|
|
|
func documentPicker(
|
|
_ controller: UIDocumentPickerViewController,
|
|
didPickDocumentsAt urls: [URL]
|
|
) {
|
|
guard let invoke = takePickerInvoke() else { return }
|
|
guard let url = urls.first else {
|
|
invoke.resolve(cancelledResult())
|
|
return
|
|
}
|
|
|
|
do {
|
|
guard url.startAccessingSecurityScopedResource() else {
|
|
throw AccessError.denied
|
|
}
|
|
var adopted = false
|
|
defer {
|
|
if !adopted { url.stopAccessingSecurityScopedResource() }
|
|
}
|
|
|
|
let name = try validateFolder(url)
|
|
let bookmarkId = existingBookmarkId(for: url) ?? UUID().uuidString.lowercased()
|
|
let bookmark = try url.bookmarkData(
|
|
options: .minimalBookmark,
|
|
includingResourceValuesForKeys: nil,
|
|
relativeTo: nil
|
|
)
|
|
try bookmark.write(to: bookmarkFile(for: bookmarkId, createDirectory: true), options: .atomic)
|
|
stageScope(url: url, bookmarkId: bookmarkId)
|
|
adopted = true
|
|
invoke.resolve(result(bookmarkId: bookmarkId, url: url, name: name))
|
|
} catch {
|
|
invoke.reject(message(for: error))
|
|
}
|
|
}
|
|
|
|
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
|
takePickerInvoke()?.resolve(cancelledResult())
|
|
}
|
|
|
|
private func resolveBookmark(_ bookmarkId: String) throws -> [String: Any] {
|
|
guard UUID(uuidString: bookmarkId) != nil else { throw AccessError.missing }
|
|
|
|
lock.lock()
|
|
let currentURL = activeBookmarkId == bookmarkId ? activeURL : nil
|
|
lock.unlock()
|
|
if let currentURL {
|
|
return result(bookmarkId: bookmarkId, url: currentURL, name: try validateFolder(currentURL))
|
|
}
|
|
|
|
let data = try Data(contentsOf: bookmarkFile(for: bookmarkId, createDirectory: false))
|
|
var stale = false
|
|
let url = try URL(
|
|
resolvingBookmarkData: data,
|
|
options: [],
|
|
relativeTo: nil,
|
|
bookmarkDataIsStale: &stale
|
|
)
|
|
guard url.startAccessingSecurityScopedResource() else { throw AccessError.denied }
|
|
var adopted = false
|
|
defer {
|
|
if !adopted { url.stopAccessingSecurityScopedResource() }
|
|
}
|
|
|
|
let name = try validateFolder(url)
|
|
if stale {
|
|
let refreshed = try url.bookmarkData(
|
|
options: .minimalBookmark,
|
|
includingResourceValuesForKeys: nil,
|
|
relativeTo: nil
|
|
)
|
|
try refreshed.write(to: bookmarkFile(for: bookmarkId, createDirectory: true), options: .atomic)
|
|
}
|
|
stageScope(url: url, bookmarkId: bookmarkId)
|
|
adopted = true
|
|
return result(bookmarkId: bookmarkId, url: url, name: name)
|
|
}
|
|
|
|
private func validateFolder(_ url: URL) throws -> String {
|
|
let values = try url.resourceValues(forKeys: [.isDirectoryKey, .nameKey])
|
|
guard values.isDirectory == true else { throw AccessError.notFolder }
|
|
guard try url.checkResourceIsReachable() else { throw AccessError.unavailable }
|
|
return values.name ?? url.lastPathComponent
|
|
}
|
|
|
|
private func stageScope(url: URL, bookmarkId: String) {
|
|
lock.lock()
|
|
let previous = stagedURL
|
|
stagedURL = url
|
|
stagedBookmarkId = bookmarkId
|
|
lock.unlock()
|
|
previous?.stopAccessingSecurityScopedResource()
|
|
}
|
|
|
|
private func commitStagedScope() {
|
|
lock.lock()
|
|
guard let next = stagedURL else {
|
|
lock.unlock()
|
|
return
|
|
}
|
|
let previous = activeURL
|
|
activeURL = next
|
|
activeBookmarkId = stagedBookmarkId
|
|
stagedURL = nil
|
|
stagedBookmarkId = nil
|
|
lock.unlock()
|
|
previous?.stopAccessingSecurityScopedResource()
|
|
}
|
|
|
|
private func rollbackStagedScope() {
|
|
lock.lock()
|
|
let staged = stagedURL
|
|
stagedURL = nil
|
|
stagedBookmarkId = nil
|
|
lock.unlock()
|
|
staged?.stopAccessingSecurityScopedResource()
|
|
}
|
|
|
|
private func releaseCurrentScope() {
|
|
lock.lock()
|
|
let previous = activeURL
|
|
activeURL = nil
|
|
activeBookmarkId = nil
|
|
lock.unlock()
|
|
previous?.stopAccessingSecurityScopedResource()
|
|
}
|
|
|
|
private func takePickerInvoke() -> Invoke? {
|
|
let invoke = pickerInvoke
|
|
pickerInvoke = nil
|
|
return invoke
|
|
}
|
|
|
|
private func bookmarkDirectory(create: Bool) throws -> URL {
|
|
let base = try fileManager.url(
|
|
for: .applicationSupportDirectory,
|
|
in: .userDomainMask,
|
|
appropriateFor: nil,
|
|
create: true
|
|
)
|
|
let directory = base.appendingPathComponent("VaultBookmarks", isDirectory: true)
|
|
if create {
|
|
try fileManager.createDirectory(at: directory, withIntermediateDirectories: true)
|
|
}
|
|
return directory
|
|
}
|
|
|
|
private func bookmarkFile(for id: String, createDirectory: Bool) throws -> URL {
|
|
guard UUID(uuidString: id) != nil else { throw AccessError.missing }
|
|
let directory = try bookmarkDirectory(create: createDirectory)
|
|
return directory.appendingPathComponent("\(id).bookmark")
|
|
}
|
|
|
|
private func existingBookmarkId(for selectedURL: URL) -> String? {
|
|
guard
|
|
let directory = try? bookmarkDirectory(create: true),
|
|
let files = try? fileManager.contentsOfDirectory(
|
|
at: directory,
|
|
includingPropertiesForKeys: nil
|
|
)
|
|
else { return nil }
|
|
|
|
let selectedPath = selectedURL.standardizedFileURL.path
|
|
for file in files where file.pathExtension == "bookmark" {
|
|
guard let data = try? Data(contentsOf: file) else { continue }
|
|
var stale = false
|
|
guard
|
|
let resolved = try? URL(
|
|
resolvingBookmarkData: data,
|
|
options: [],
|
|
relativeTo: nil,
|
|
bookmarkDataIsStale: &stale
|
|
),
|
|
resolved.standardizedFileURL.path == selectedPath
|
|
else { continue }
|
|
let id = file.deletingPathExtension().lastPathComponent
|
|
if UUID(uuidString: id) != nil { return id }
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private func result(bookmarkId: String, url: URL, name: String) -> [String: Any] {
|
|
["bookmarkId": bookmarkId, "path": url.path, "name": name, "cancelled": false]
|
|
}
|
|
|
|
private func cancelledResult() -> [String: Any] {
|
|
["bookmarkId": "", "path": "", "name": "", "cancelled": true]
|
|
}
|
|
|
|
private func message(for error: Error) -> String {
|
|
if let accessError = error as? AccessError { return accessError.localizedDescription }
|
|
return "The selected folder is unavailable. Open Files and try again."
|
|
}
|
|
}
|
|
|
|
private enum AccessError: LocalizedError {
|
|
case denied
|
|
case missing
|
|
case notFolder
|
|
case unavailable
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .denied:
|
|
return "HelixNotes no longer has access to this folder. Choose it again in Files."
|
|
case .missing:
|
|
return "Saved access to this folder is missing. Choose it again in Files."
|
|
case .notFolder:
|
|
return "The selected item is not a folder."
|
|
case .unavailable:
|
|
return "The selected folder is unavailable. Open Files and try again."
|
|
}
|
|
}
|
|
}
|
|
|
|
@_cdecl("init_plugin_ios_vault_access")
|
|
func initPlugin() -> Plugin {
|
|
IosVaultAccessPlugin()
|
|
}
|