import Foundation // 接收落盘管理器(对应桌面 desktop/platform/download.go):把接收到的字节写入 app 沙盒 // Documents。小文件整文件写(saveWhole);大文件流式 begin / append / finalize;abort // 丢弃临时文件。线程:由 EngineController 在主线程调用,文件 IO 同步执行(首版求简单可 // 验,后续可挪后台队列)。 final class DownloadManager { private struct Active { let handle: FileHandle let tmpURL: URL } private var active: [String: Active] = [:] private func downloadDir() -> URL { return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] } // 列出已落盘的接收文件(Documents 顶层,排除流式临时 .part / 隐藏文件),最近的在前。 // 供应用内「收到的文件」视图浏览 / 打开 / 转发,也是 Files app 可见的同一批文件。 func receivedFiles() -> [URL] { let urls = (try? FileManager.default.contentsOfDirectory( at: downloadDir(), includingPropertiesForKeys: [ .contentModificationDateKey ], options: [ .skipsHiddenFiles ])) ?? [] return urls .filter { $0.pathExtension != "part" && !$0.lastPathComponent.hasPrefix(".cdrop-") } .sorted { a, b in modDate(a) > modDate(b) } } func deleteReceivedFile(_ url: URL) { try? FileManager.default.removeItem(at: url) } private func modDate(_ url: URL) -> Date { return (try? url.resourceValues(forKeys: [ .contentModificationDateKey ]))? .contentModificationDate ?? .distantPast } // 整文件写入(接收端小文件路径,对应桥 saveDownload)。返回最终绝对路径。 func saveWhole(name: String, data: Data) throws -> String { let url = uniqueURL(for: sanitize(name)) try data.write(to: url) return url.path } func begin(sessionId: String) throws { cleanup(sessionId) // re-begin 安全:先清旧条目再开新文件 let tmp = downloadDir().appendingPathComponent(".cdrop-\(sanitize(sessionId)).part") FileManager.default.createFile(atPath: tmp.path, contents: nil) let handle = try FileHandle(forWritingTo: tmp) active[sessionId] = Active(handle: handle, tmpURL: tmp) } func append(sessionId: String, data: Data) throws { guard let a = active[sessionId] else { throw Err.unknownSession } try a.handle.write(contentsOf: data) } func finalize(sessionId: String, name: String) throws -> String { guard let a = active[sessionId] else { throw Err.unknownSession } try a.handle.close() active[sessionId] = nil let dest = uniqueURL(for: sanitize(name)) try FileManager.default.moveItem(at: a.tmpURL, to: dest) return dest.path } func abort(sessionId: String) { cleanup(sessionId) } private func cleanup(_ sessionId: String) { guard let a = active[sessionId] else { return } try? a.handle.close() try? FileManager.default.removeItem(at: a.tmpURL) active[sessionId] = nil } // 唯一化:若已存在则插入 (1) (2)…(对应桌面 uniquePath)。 private func uniqueURL(for name: String) -> URL { let dir = downloadDir() let base = (name as NSString).deletingPathExtension let ext = (name as NSString).pathExtension var candidate = dir.appendingPathComponent(name) var i = 1 while FileManager.default.fileExists(atPath: candidate.path) { let next = ext.isEmpty ? "\(base) (\(i))" : "\(base) (\(i)).\(ext)" candidate = dir.appendingPathComponent(next) i += 1 } return candidate } // 文件名 sanitize:只取末段、剥路径分隔符,防目录穿越(对应桌面 sanitizeFileName)。 private func sanitize(_ name: String) -> String { let last = (name as NSString).lastPathComponent let cleaned = last.replacingOccurrences(of: "/", with: "_") return cleaned.isEmpty ? "download" : cleaned } enum Err: Error { case unknownSession } }