import Foundation /// Per-file offload result. public struct FileRecord: Sendable, Equatable { public let relativePath: String public let size: UInt64 public let xxh64: String /// Destination copy verified by post-copy re-read. public let verified: Bool /// Already present + identical at all destinations. public let skipped: Bool } /// Offload job report. public struct OffloadReport: Sendable { public let source: String public let destinations: [String] public let files: [FileRecord] public let totalBytes: UInt64 public let duration: TimeInterval public let startedAt: Date } /// Verified multi-destination copy: source hashed while copying, destinations /// re-read + hashed after write. Never silently overwrites mismatched files. public struct CopyEngine { public init() {} /// Copy source tree to all destinations. /// - overwritePartial: re-copy destination files whose *size* differs /// (interrupted copy detection). Size-matching mismatched hashes always error. public func offload( source: String, destinations: [String], overwritePartial: Bool = false ) throws -> OffloadReport { let fm = FileManager.default var isDir: ObjCBool = false guard fm.fileExists(atPath: source, isDirectory: &isDir), isDir.boolValue else { throw OffloadError.fileNotFound(source) } let start = Date() let clock = ContinuousClock.now let files = try listFiles(root: source).sorted() var records = [FileRecord]() var totalBytes: UInt64 = 0 for rel in files { let srcPath = source + "/" + rel let srcHash = try FileHasher.xxh64(path: srcPath) let attrs = try fm.attributesOfItem(atPath: srcPath) let size = attrs[.size] as? UInt64 ?? 0 totalBytes += size var anyCopied = false for dest in destinations { let dstPath = dest + "/" + rel let dstDir = (dstPath as NSString).deletingLastPathComponent try fm.createDirectory(atPath: dstDir, withIntermediateDirectories: true) if fm.fileExists(atPath: dstPath) { let dstAttrs = try fm.attributesOfItem(atPath: dstPath) let dstSize = dstAttrs[.size] as? UInt64 ?? 0 if dstSize != size { // Partial/interrupted copy. if overwritePartial { try fm.removeItem(atPath: dstPath) } else { throw OffloadError.destinationConflict( "\(dstPath): size \(dstSize) != source \(size) (partial? rerun with overwritePartial)") } } else { let dstHash = try FileHasher.xxh64(path: dstPath) if dstHash == srcHash { continue // identical — skip } throw OffloadError.destinationConflict( "\(dstPath): exists with different content (hash \(dstHash) != \(srcHash))") } } // Copy via temp + atomic rename, then verify by re-read. let tmpPath = dstPath + ".forgetmp" try? fm.removeItem(atPath: tmpPath) try fm.copyItem(atPath: srcPath, toPath: tmpPath) let copiedHash = try FileHasher.xxh64(path: tmpPath) guard copiedHash == srcHash else { try? fm.removeItem(atPath: tmpPath) throw OffloadError.verifyFailed(path: dstPath, expected: srcHash, actual: copiedHash) } _ = try? fm.replaceItemAt(URL(fileURLWithPath: dstPath), withItemAt: URL(fileURLWithPath: tmpPath)) if !fm.fileExists(atPath: dstPath) { // replaceItemAt fallback (some Linux Foundation builds). try fm.moveItem(atPath: tmpPath, toPath: dstPath) } anyCopied = true } records.append(FileRecord( relativePath: rel, size: size, xxh64: srcHash, verified: true, skipped: !anyCopied)) } return OffloadReport( source: source, destinations: destinations, files: records, totalBytes: totalBytes, duration: max(Double(clock.duration(to: .now).components.seconds) + Double(clock.duration(to: .now).components.attoseconds) / 1e18, 0.000001), startedAt: start) } /// Relative paths of all regular files under root. func listFiles(root: String) throws -> [String] { let fm = FileManager.default guard let enumerator = fm.enumerator(atPath: root) else { throw OffloadError.readFailed(root) } var out = [String]() while let rel = enumerator.nextObject() as? String { var isDir: ObjCBool = false if fm.fileExists(atPath: root + "/" + rel, isDirectory: &isDir), !isDir.boolValue { out.append(rel) } } return out } }