53 lines
2.3 KiB
Swift
53 lines
2.3 KiB
Swift
import AppKit
|
|
import Foundation
|
|
|
|
@MainActor
|
|
enum SFTPLocalCopyExporter {
|
|
static func present(for source: URL, failed: @escaping (any Error) -> Void) {
|
|
let panel = NSSavePanel()
|
|
panel.title = String(localized: "Keep Local Copy")
|
|
panel.nameFieldStringValue = source.lastPathComponent
|
|
panel.canCreateDirectories = true
|
|
panel.begin { response in
|
|
guard response == .OK, let destination = panel.url else { return }
|
|
Task {
|
|
do {
|
|
try await Task.detached(priority: .userInitiated) {
|
|
try copy(from: source, to: destination)
|
|
}.value
|
|
} catch { failed(error) }
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Prepare the complete copy before replacing a user-selected existing file.
|
|
nonisolated static func copy(from source: URL, to destination: URL) throws {
|
|
guard source.standardizedFileURL != destination.standardizedFileURL else { return }
|
|
let scoped = destination.startAccessingSecurityScopedResource()
|
|
defer { if scoped { destination.stopAccessingSecurityScopedResource() } }
|
|
var coordinationError: NSError?
|
|
var copyError: (any Error)?
|
|
NSFileCoordinator().coordinate(
|
|
readingItemAt: source, options: [], writingItemAt: destination, options: .forReplacing,
|
|
error: &coordinationError
|
|
) { readingURL, writingURL in
|
|
do {
|
|
let files = FileManager.default
|
|
let directory = try files.url(
|
|
for: .itemReplacementDirectory, in: .userDomainMask,
|
|
appropriateFor: writingURL, create: true)
|
|
defer { try? files.removeItem(at: directory) }
|
|
let prepared = directory.appendingPathComponent(source.lastPathComponent)
|
|
try files.copyItem(at: readingURL, to: prepared)
|
|
if files.fileExists(atPath: writingURL.path) {
|
|
_ = try files.replaceItemAt(writingURL, withItemAt: prepared)
|
|
} else {
|
|
try files.moveItem(at: prepared, to: writingURL)
|
|
}
|
|
} catch { copyError = error }
|
|
}
|
|
if let coordinationError { throw coordinationError }
|
|
if let copyError { throw copyError }
|
|
}
|
|
}
|