Files
Allofit/sources/ui/AccessManager.swift
Bitsy 35ffc86e46 Fix idle CPU drain from open path queries, general code cleanup
Fix
- An open window with a path query (term containing "/") re-ran the search
  over the whole index on every FSEvents update, keeping several cores busy
  while files changed. Index-driven refilters are now throttled to one per
  second, skipped while the window is closed, minimized or covered, and
  replayed once when it becomes visible again. Typing stays instant.
- Path terms no longer build and lowercase the full path of every record:
  the folded folder path is precomputed once per folder (interned like
  parentPath) and matched together with the folded name without allocating
  the joined string. Path-only queries are about 3x faster.

Cleanup
- Remove dead code: FSChange.isDir, FileWatcher.latestEventId,
  IndexState.record(atPath:), the never-set lastQuery preference,
  AccessManager.reset(), SearchField.onSubmit, an unreachable return.
- Merge duplicates: the two Settings size formatters become
  Formatters.sizeOrDash, the shellQuote wrapper is replaced by
  AdminShell.quote.
- Naming conventions: "in"-prefixed parameter names everywhere (delegate
  methods, binary helpers, initializers), camelCase impl helpers.
- Comment every function and type; fix stale comments.
- Debug and release builds are warning-free.
2026-10-02 09:29:18 +02:00

72 lines
2.9 KiB
Swift

import Foundation
import SwiftUI
// AccessManager holds the in-memory mapping from FileRecord.ID to the
// staged user-readable copy produced by an "Authorize" tap. Both the
// Table (which shows a lock badge on the selected row when the preview
// pane is closed) and the PreviewPane (which gates the QLPreviewView
// behind the same badge) observe this so the badge disappears and the
// preview switches to the staged URL as soon as the sudo copy lands.
//
// All published mutations happen on the main actor; the actual sudo cp
// runs inside a Task.detached spawned by `authorize(_:)` so the AppleScript
// password prompt doesn't block the main runloop.
@MainActor
final class AccessManager: ObservableObject {
// id -> URL of the user-readable copy in ~/Library/Caches/Allofit/elevated
@Published private(set) var stagedURLs: [FileRecord.ID: URL] = [:]
// ids currently being authorized (admin script in flight); used to
// render a small spinner inside the lock badge
@Published private(set) var authorizingIds: Set<FileRecord.ID> = []
// most recent authorization error (cancelled prompt, sudo failure,
// etc); surfaced as the badge's accessibility / tooltip text
@Published private(set) var lastError: String?
// returns the URL to use when previewing / opening the file: the
// staged copy if we have one, otherwise the original path
func effectiveURL(for inRecord: FileRecord) -> URL {
if let vStaged = stagedURLs[inRecord.id] {
return vStaged
}
return URL(fileURLWithPath: inRecord.fullPath)
}
// true if the effective URL (staged or original) isn't readable by
// the current user - this is what drives the lock-badge visibility
func needsAuthorization(for inRecord: FileRecord) -> Bool {
let vUrl = effectiveURL(for: inRecord)
return !ElevatedAccess.canRead(path: vUrl.path)
}
// true while an authorize task is in flight for the given record id
func isAuthorizing(_ inId: FileRecord.ID) -> Bool {
return authorizingIds.contains(inId)
}
// runs the sudo cp + chown flow for one record. The first call inside
// the system's admin-auth-cache window (~5 min) prompts for the
// password; subsequent calls within that window are silent.
func authorize(_ inRecord: FileRecord) async {
let vId = inRecord.id
// idempotency: a double-tap on the badge shouldn't kick off two
// concurrent admin scripts for the same file
guard !authorizingIds.contains(vId) else { return }
authorizingIds.insert(vId)
lastError = nil
let vOriginalUrl = URL(fileURLWithPath: inRecord.fullPath)
do {
// Task.detached so the synchronous NSAppleScript admin prompt
// runs on a background thread; the prompt itself is shown on
// main by AppKit regardless of where we invoke it from
let vStaged = try await Task.detached(priority: .userInitiated) {
try ElevatedAccess.stage(vOriginalUrl)
}.value
stagedURLs[vId] = vStaged
} catch {
lastError = error.localizedDescription
}
authorizingIds.remove(vId)
}
}