72 lines
2.8 KiB
Swift
72 lines
2.8 KiB
Swift
import CryptoKit
|
|
import Foundation
|
|
import TermindApplication
|
|
|
|
/// Streams arbitrary regular files; never decodes content or filters filename extensions.
|
|
public actor SFTPEditLocalFiles {
|
|
public struct Snapshot: Sendable {
|
|
public let url: URL
|
|
public let digest: String
|
|
public let size: Int64
|
|
}
|
|
|
|
private struct Signature: Equatable {
|
|
let size: UInt64
|
|
let modified: Date?
|
|
let inode: UInt64
|
|
}
|
|
|
|
private var cache: [URL: (Signature, String)] = [:]
|
|
public init() {}
|
|
|
|
public func digest(_ url: URL, force: Bool = true) throws -> String {
|
|
let before = try signature(url)
|
|
if !force, let cached = cache[url], cached.0 == before { return cached.1 }
|
|
let file = try FileHandle(forReadingFrom: url)
|
|
defer { try? file.close() }
|
|
var hash = SHA256()
|
|
while let data = try file.read(upToCount: 64 * 1024), !data.isEmpty {
|
|
try Task.checkCancellation()
|
|
hash.update(data: data)
|
|
}
|
|
guard try signature(url) == before else { throw SFTPEditFailure.localChanged }
|
|
let digest = hash.finalize().map { String(format: "%02x", $0) }.joined()
|
|
cache[url] = (before, digest)
|
|
return digest
|
|
}
|
|
|
|
public func snapshot(_ url: URL) throws -> Snapshot {
|
|
let original = try digest(url)
|
|
let directory = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent("TermindEditUploads", isDirectory: true)
|
|
.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
|
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
|
let copy = directory.appendingPathComponent(url.lastPathComponent)
|
|
do {
|
|
try FileManager.default.copyItem(at: url, to: copy)
|
|
let copied = try digest(copy)
|
|
guard copied == original, try digest(url) == original else { throw SFTPEditFailure.localChanged }
|
|
return Snapshot(url: copy, digest: copied, size: Int64(try signature(copy).size))
|
|
} catch {
|
|
try? FileManager.default.removeItem(at: directory)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
public func removeSnapshot(_ snapshot: Snapshot) {
|
|
cache[snapshot.url] = nil
|
|
try? FileManager.default.removeItem(at: snapshot.url.deletingLastPathComponent())
|
|
}
|
|
|
|
public func forget(_ url: URL) { cache[url] = nil }
|
|
|
|
private func signature(_ url: URL) throws -> Signature {
|
|
let values = try FileManager.default.attributesOfItem(atPath: url.path)
|
|
guard values[.type] as? FileAttributeType == .typeRegular else { throw SFTPEditFailure.unsupported }
|
|
return Signature(
|
|
size: (values[.size] as? NSNumber)?.uint64Value ?? 0,
|
|
modified: values[.modificationDate] as? Date,
|
|
inode: (values[.systemFileNumber] as? NSNumber)?.uint64Value ?? 0)
|
|
}
|
|
}
|