From 72b0f5a8894cef5ae66f4785240c7d4a004eac49 Mon Sep 17 00:00:00 2001 From: Jason Morley Date: Fri, 5 Jul 2024 16:44:25 -1000 Subject: [PATCH] =?UTF-8?q?fix:=20=F0=9F=93=81=20Download=20folders=20(#89?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Reconnect/Extensions/String.swift | 2 +- Reconnect/Extensions/URL.swift | 6 + Reconnect/Model/BrowserModel.swift | 149 ++++++++++++++---------- Reconnect/PLP/FileServer.swift | 23 +++- Reconnect/Views/BrowserDetailView.swift | 6 +- 5 files changed, 114 insertions(+), 72 deletions(-) diff --git a/Reconnect/Extensions/String.swift b/Reconnect/Extensions/String.swift index b8dba68..3451879 100644 --- a/Reconnect/Extensions/String.swift +++ b/Reconnect/Extensions/String.swift @@ -36,7 +36,7 @@ extension String { return hasSuffix(.windowsPathSeparator) } - var windowsLastPathComponent: String { + var lastWindowsPathComponent: String { return windowsPathComponents.last ?? "" } diff --git a/Reconnect/Extensions/URL.swift b/Reconnect/Extensions/URL.swift index 5f8de2c..d0604ed 100644 --- a/Reconnect/Extensions/URL.swift +++ b/Reconnect/Extensions/URL.swift @@ -23,4 +23,10 @@ extension URL { static let about = URL(string: "x-reconnect://about")! static let browser = URL(string: "x-reconnect://browser")! + func appendingPathComponents(_ pathComponents: [String]) -> URL { + return pathComponents.reduce(self) { url, pathComponent in + return url.appendingPathComponent(pathComponent) + } + } + } diff --git a/Reconnect/Model/BrowserModel.swift b/Reconnect/Model/BrowserModel.swift index c7a4922..644cdd4 100644 --- a/Reconnect/Model/BrowserModel.swift +++ b/Reconnect/Model/BrowserModel.swift @@ -28,6 +28,18 @@ class BrowserModel { return name(for: path) } + var nextItems: [NavigationStack.Item] { + return navigationStack.nextItems + } + + var path: String? { + return navigationStack.path + } + + var previousItems: [NavigationStack.Item] { + return navigationStack.previousItems.reversed() + } + var transfersModel = TransfersModel() let fileServer: FileServer @@ -66,7 +78,7 @@ class BrowserModel { if path.isRoot, let drive = drives.first(where: { path.hasPrefix($0.drive) }) { return drive.displayName } - return path.windowsLastPathComponent + return path.lastWindowsPathComponent } func image(for path: String) -> String { @@ -106,16 +118,16 @@ class BrowserModel { } } - var path: String? { - return navigationStack.path - } - - var previousItems: [NavigationStack.Item] { - return navigationStack.previousItems.reversed() - } - - var nextItems: [NavigationStack.Item] { - return navigationStack.nextItems + private func runAsync(task: @escaping () async throws -> Void) { + Task { + do { + try await task() + } catch { + await MainActor.run { + lastError = error + } + } + } } func canGoBack() -> Bool { @@ -137,84 +149,93 @@ class BrowserModel { } func newFolder() { - guard let path = navigationStack.path else { - return - } - Task { - do { - let folderPath = path + "untitled folder" - try await fileServer.mkdir(path: folderPath) - let files = try await fileServer.dir(path: path) - .sorted { $0.name.localizedStandardCompare($1.name) == .orderedAscending } - self.files = files - fileSelection = Set([folderPath + "\\"]) - } catch { - print("Failed to create new folder with error \(error).") - lastError = error + runAsync { + guard let path = self.path else { + throw ReconnectError.invalidFilePath } + let folderPath = path + "untitled folder" + try await self.fileServer.mkdir(path: folderPath) + let files = try await self.fileServer.dir(path: path) + .sorted { $0.name.localizedStandardCompare($1.name) == .orderedAscending } + self.files = files + self.fileSelection = Set([folderPath + "\\"]) } } func delete(path: String) { - Task { - do { - if path.isWindowsDirectory { - try await fileServer.rmdir(path: path) - } else { - try await fileServer.remove(path: path) - } - update() - } catch { - print("Failed to delete item at path '\(path)' with error \(error).") - lastError = error + runAsync { + if path.isWindowsDirectory { + try await self.fileServer.rmdir(path: path) + } else { + try await self.fileServer.remove(path: path) } + self.files.removeAll { $0.path == path } } } - func download(path: String) { + func download(from path: String) { + if path.isWindowsDirectory { + downloadDirectory(path: path) + } else { + downloadFile(from: path) + } + } + + private func downloadFile(from path: String, to destinationURL: URL? = nil) { Task { let fileManager = FileManager.default - let downloadsUrl = fileManager.urls(for: .downloadsDirectory, in: .userDomainMask)[0] - - let filename = path.windowsLastPathComponent - let destinationURL = downloadsUrl.appendingPathComponent(filename) - - print("Downloading file at path '\(path)' to destination path '\(destinationURL.path)'...") + let downloadsURL = fileManager.urls(for: .downloadsDirectory, in: .userDomainMask)[0] + let filename = path.lastWindowsPathComponent + let downloadURL = destinationURL ?? downloadsURL.appendingPathComponent(filename) + print("Downloading file at path '\(path)' to destination path '\(downloadURL.path)'...") transfersModel.add(filename) { transfer in - try await self.fileServer.copyFile(fromRemotePath: path, toLocalPath: destinationURL.path) { progress, size in + try await self.fileServer.copyFile(fromRemotePath: path, toLocalPath: downloadURL.path) { progress, size in transfer.setStatus(.active(Float(progress) / Float(size))) return .continue } transfer.setStatus(.complete) } + } + } - do { - if let directoryUrls = try? FileManager.default.contentsOfDirectory(at: downloadsUrl, includingPropertiesForKeys: nil, options: FileManager.DirectoryEnumerationOptions.skipsSubdirectoryDescendants) { - print(directoryUrls) + private func downloadDirectory(path: String) { + runAsync { + let fileManager = FileManager.default + let downloadsURL = fileManager.urls(for: .downloadsDirectory, in: .userDomainMask)[0] + let parentPath = path.deletingLastWindowsPathComponent.ensuringTrailingWindowsPathSeparator(isPresent: true) + + // Here we know we're downloading a directory, so we make sure the destination exists. + try fileManager.createDirectory(at: downloadsURL.appendingPathComponent(path.lastWindowsPathComponent), + withIntermediateDirectories: true) + + // Iterate over the recursive directory listing creating directories where necessary and downloading files. + let files = try await self.fileServer.dir(path: path, recursive: true) + for file in files { + let relativePath = String(file.path.dropFirst(parentPath.count)) + let destinationURL = downloadsURL.appendingPathComponents(relativePath.windowsPathComponents) + if file.isDirectory { + try fileManager.createDirectory(at: destinationURL, withIntermediateDirectories: true) + } else { + self.downloadFile(from: file.path, to: destinationURL) } } } } func upload(url: URL) { - Task { - do { - guard let path else { - throw ReconnectError.invalidFilePath - } - let destinationPath = path + url.lastPathComponent - print("Uploading file at path '\(url.path)' to destination path '\(destinationPath)'...") - transfersModel.add(url.lastPathComponent) { transfer in - try await self.fileServer.copyFile(fromLocalPath: url.path, toRemotePath: destinationPath) { progress, size in - transfer.setStatus(.active(Float(progress) / Float(size))) - return .continue - } - transfer.setStatus(.complete) - self.update() + runAsync { + guard let path = self.path else { + throw ReconnectError.invalidFilePath + } + let destinationPath = path + url.lastPathComponent + print("Uploading file at path '\(url.path)' to destination path '\(destinationPath)'...") + self.transfersModel.add(url.lastPathComponent) { transfer in + try await self.fileServer.copyFile(fromLocalPath: url.path, toRemotePath: destinationPath) { progress, size in + transfer.setStatus(.active(Float(progress) / Float(size))) + return .continue } - } catch { - print("Failed to upload file with error \(error).") - lastError = error + transfer.setStatus(.complete) + self.update() } } } diff --git a/Reconnect/PLP/FileServer.swift b/Reconnect/PLP/FileServer.swift index 2645e28..f769caa 100644 --- a/Reconnect/PLP/FileServer.swift +++ b/Reconnect/PLP/FileServer.swift @@ -81,6 +81,10 @@ class FileServer { return path } + var isDirectory: Bool { + return attributes.contains(.directory) + } + let path: String let name: String let size: UInt32 @@ -164,7 +168,7 @@ class FileServer { } } - private func syncQueue_dir(path: String) throws -> [DirectoryEntry] { + private func syncQueue_dir(path: String, recursive: Bool) throws -> [DirectoryEntry] { dispatchPrecondition(condition: .onQueue(workQueue)) try syncQueue_connect() var details = PlpDir() @@ -174,7 +178,18 @@ class FileServer { for i in 0.. DirectoryEntry { @@ -274,9 +289,9 @@ class FileServer { name: String(cString: name)) } - func dir(path: String) async throws -> [DirectoryEntry] { + func dir(path: String, recursive: Bool = false) async throws -> [DirectoryEntry] { return try await perform { - return try self.syncQueue_dir(path: path) + return try self.syncQueue_dir(path: path, recursive: recursive) } } diff --git a/Reconnect/Views/BrowserDetailView.swift b/Reconnect/Views/BrowserDetailView.swift index f400ec8..bdedb6c 100644 --- a/Reconnect/Views/BrowserDetailView.swift +++ b/Reconnect/Views/BrowserDetailView.swift @@ -29,7 +29,7 @@ struct BrowserDetailView: View { ZStack { Table(model.files, selection: $model.fileSelection) { TableColumn("") { file in - if file.attributes.contains(.directory) { + if file.isDirectory { Image("Folder16") } else { switch file.uid3 { @@ -61,7 +61,7 @@ struct BrowserDetailView: View { .foregroundStyle(.secondary) } TableColumn("Size") { file in - if file.attributes.contains(.directory) { + if file.isDirectory { Text("--") .foregroundStyle(.secondary) } else { @@ -80,7 +80,7 @@ struct BrowserDetailView: View { Button("Download") { for item in items { - model.download(path: item) + model.download(from: item) } }