diff --git a/Examples/FileBrowser/FileBrowser (macOS)/AppDelegate.swift b/Examples/FileBrowser/FileBrowser (macOS)/AppDelegate.swift index 0002760..0eba8d4 100644 --- a/Examples/FileBrowser/FileBrowser (macOS)/AppDelegate.swift +++ b/Examples/FileBrowser/FileBrowser (macOS)/AppDelegate.swift @@ -90,7 +90,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { } let filesViewController = FilesViewController.instantiate( - client: foregroundViewController.client, + accessor: foregroundViewController.treeAccessor, serverNode: foregroundViewController.serverNode, share: foregroundViewController.share, path: foregroundViewController.path, diff --git a/Examples/FileBrowser/FileBrowser (macOS)/Base.lproj/Main.storyboard b/Examples/FileBrowser/FileBrowser (macOS)/Base.lproj/Main.storyboard index 91a56a0..a84080e 100644 --- a/Examples/FileBrowser/FileBrowser (macOS)/Base.lproj/Main.storyboard +++ b/Examples/FileBrowser/FileBrowser (macOS)/Base.lproj/Main.storyboard @@ -1,8 +1,8 @@ - + - + @@ -327,7 +327,7 @@ CA - + @@ -356,7 +356,7 @@ CA - + diff --git a/Examples/FileBrowser/FileBrowser (macOS)/DirectoryStructure.swift b/Examples/FileBrowser/FileBrowser (macOS)/DirectoryStructure.swift index 309ac25..36ed9aa 100644 --- a/Examples/FileBrowser/FileBrowser (macOS)/DirectoryStructure.swift +++ b/Examples/FileBrowser/FileBrowser (macOS)/DirectoryStructure.swift @@ -5,6 +5,7 @@ import SMBClient class DirectoryStructure { private let server: String private let path: String + private let treeAccessor: TreeAccessor private var tree = Tree() private var viewTree = Tree() @@ -12,8 +13,6 @@ class DirectoryStructure { private var searchText = "" private var sortDescriptor = NSSortDescriptor(key: "NameColumn", ascending: true) - private let client: SMBClient - var useCache = false { didSet { if !useCache { @@ -23,10 +22,10 @@ class DirectoryStructure { } private var cache = [FileNode: [FileNode]]() - init(server: String, path: String, client: SMBClient) { + init(server: String, path: String, accessor: TreeAccessor) { self.server = server self.path = path - self.client = client + treeAccessor = accessor } func viewTree(_ tree: Tree) -> Tree { @@ -46,18 +45,18 @@ class DirectoryStructure { return viewTree } - func reload() async { - let nodes = await listDirectory(path: path, parent: nil) + func reload() async throws { + let nodes = try await listDirectory(path: path, parent: nil) tree.nodes = nodes viewTree = viewTree(tree) } - func reload(directory path: String, _ outlineView: NSOutlineView) async { + func reload(directory path: String, _ outlineView: NSOutlineView) async throws { if let fileNode = node(ID(path)) { - await expand(fileNode, outlineView) + try await expand(fileNode, outlineView) } else { - let nodes = await listDirectory(path: path, parent: nil) + let nodes = try await listDirectory(path: path, parent: nil) tree.nodes = Array( Set(nodes) @@ -71,10 +70,10 @@ class DirectoryStructure { } } - func expand(_ fileNode: FileNode, _ outlineView: NSOutlineView) async { + func expand(_ fileNode: FileNode, _ outlineView: NSOutlineView) async throws { let path = resolvePath(fileNode) - let nodes = await listDirectory(path: path, parent: fileNode) + let nodes = try await listDirectory(path: path, parent: fileNode) let children = children(of: fileNode) let (deleted, inserted) = nodeDelta(oldNodes: children, newNodes: nodes) @@ -144,6 +143,10 @@ class DirectoryStructure { viewTree.nodes.count } + func availableSpace() async throws -> UInt64 { + return try await treeAccessor.availableSpace() + } + func rootNodes() -> [FileNode] { viewTree.rootNodes() } @@ -201,22 +204,16 @@ class DirectoryStructure { return false } - private func listDirectory(path: String, parent: FileNode?) async -> [FileNode] { - do { - let files = try await client.listDirectory(path: path) - .filter { $0.name != "." && $0.name != ".." && !$0.isHidden } - .sorted { $0.name.localizedStandardCompare($1.name) == .orderedAscending } - let nodes = files - .map { FileNode(path: join(path, $0.name), file: $0, parent: parent?.id) } + private func listDirectory(path: String, parent: FileNode?) async throws -> [FileNode] { + let files = try await treeAccessor.listDirectory(path: path) + .filter { $0.name != "." && $0.name != ".." && !$0.isHidden } + .sorted { $0.name.localizedStandardCompare($1.name) == .orderedAscending } + let nodes = files + .map { FileNode(path: join(path, $0.name), file: $0, parent: parent?.id) } - DataRepository.shared.set(join(server, path), nodes: nodes) + DataRepository.shared.set(join(server, path), nodes: nodes) - return nodes - } catch { - NSAlert(error: error).runModal() - } - - return DataRepository.shared.nodes(join(server, path)) ?? [] + return nodes } private func nodeDelta(oldNodes: [FileNode], newNodes: [FileNode]) -> (deleted: [Int], inserted: [Int]) { diff --git a/Examples/FileBrowser/FileBrowser (macOS)/DocumentWindowController.swift b/Examples/FileBrowser/FileBrowser (macOS)/DocumentWindowController.swift index 9d552c5..7bebb13 100644 --- a/Examples/FileBrowser/FileBrowser (macOS)/DocumentWindowController.swift +++ b/Examples/FileBrowser/FileBrowser (macOS)/DocumentWindowController.swift @@ -9,7 +9,6 @@ private let storyboardID = "DocumentWindowController" class DocumentWindowController: NSWindowController, NSWindowDelegate { private let path: String - private let client: SMBClient private let fileReader: FileReader private let server: HTTPServer @@ -17,17 +16,16 @@ class DocumentWindowController: NSWindowController, NSWindowDelegate { private var task: Task<(), any Error>? private let semaphore = Semaphore(value: 1) - static func instantiate(path: String, client: SMBClient) -> Self { + static func instantiate(path: String, accessor: TreeAccessor) -> Self { let storyboard = NSStoryboard(name: storyboardID, bundle: nil) return storyboard.instantiateController(identifier: storyboardID) { (coder) in - Self(coder: coder, path: path, client: client) + Self(coder: coder, path: path, accessor: accessor) } } - required init?(coder: NSCoder, path: String, client: SMBClient) { + required init?(coder: NSCoder, path: String, accessor: TreeAccessor) { self.path = path - self.client = client - fileReader = client.fileReader(path: path) + fileReader = accessor.fileReader(path: path) port = UInt16(42000 + NSApp.windows.count) server = HTTPServer(port: port, logger: .disabled) diff --git a/Examples/FileBrowser/FileBrowser (macOS)/FileTransfer.swift b/Examples/FileBrowser/FileBrowser (macOS)/FileTransfer.swift index 1afca52..ea47258 100644 --- a/Examples/FileBrowser/FileBrowser (macOS)/FileTransfer.swift +++ b/Examples/FileBrowser/FileBrowser (macOS)/FileTransfer.swift @@ -32,13 +32,13 @@ class FileUpload: FileTransfer { let id: UUID private let source: URL private let destination: String - private let client: SMBClient + private let treeAccessor: TreeAccessor - init(source: URL, destination: String, client: SMBClient) { + init(source: URL, destination: String, accessor: TreeAccessor) { id = UUID() self.source = source self.destination = destination - self.client = client + treeAccessor = accessor displayName = source.lastPathComponent state = .queued @@ -60,7 +60,7 @@ class FileUpload: FileTransfer { state = .started(transferProgress) progressHandler(state) - try await client.upload(localPath: source, remotePath: destination) { (completedFiles, fileBeingTransferred, bytesSent) in + try await treeAccessor.upload(localPath: source, remotePath: destination) { (completedFiles, fileBeingTransferred, bytesSent) in transferProgress = .directory(completedFiles: completedFiles, fileBeingTransferred: fileBeingTransferred, bytesSent: bytesSent) state = .started(transferProgress) progressHandler(state) @@ -78,7 +78,7 @@ class FileUpload: FileTransfer { state = .started(transferProgress) progressHandler(state) - try await client.upload(fileHandle: fileHandle, path: destination) { (progress) in + try await treeAccessor.upload(fileHandle: fileHandle, path: destination) { (progress) in transferProgress = .file(progress: progress, numberOfBytes: numberOfBytes) state = .started(.file(progress: progress, numberOfBytes: numberOfBytes)) progressHandler(state) @@ -98,7 +98,7 @@ class FileUpload: FileTransfer { name: Self.didFinish, object: self, userInfo: [ - FileUploadUserInfoKey.share: client.share ?? "", + FileUploadUserInfoKey.share: treeAccessor.share ?? "", FileUploadUserInfoKey.path: destination, ] ) diff --git a/Examples/FileBrowser/FileBrowser (macOS)/FilesViewController.swift b/Examples/FileBrowser/FileBrowser (macOS)/FilesViewController.swift index 47e7030..643ddad 100644 --- a/Examples/FileBrowser/FileBrowser (macOS)/FilesViewController.swift +++ b/Examples/FileBrowser/FileBrowser (macOS)/FilesViewController.swift @@ -9,18 +9,19 @@ class FilesViewController: NSViewController { @IBOutlet private var pathBarView: PathBarView! @IBOutlet private var statusBarView: StatusBarView! - let client: SMBClient + let treeAccessor: TreeAccessor let serverNode: ServerNode let share: String let path: String let rootPath: String - private lazy var dirTree = DirectoryStructure(server: serverNode.path, path: path, client: client) + private lazy var dirTree = DirectoryStructure(server: serverNode.path, path: path, accessor: treeAccessor) private let semaphore = Semaphore(value: 1) private var tabGroupObserving: NSKeyValueObservation? private var scrollViewObserving: NSKeyValueObservation? + private var availableSpace: UInt64? private var dateFormatter: DateFormatter = { let dateFormatter = DateFormatter() dateFormatter.dateStyle = .medium @@ -28,10 +29,10 @@ class FilesViewController: NSViewController { return dateFormatter }() - static func instantiate(client: SMBClient, serverNode: ServerNode, share: String, path: String, rootPath: String) -> Self { + static func instantiate(accessor: TreeAccessor, serverNode: ServerNode, share: String, path: String, rootPath: String) -> Self { let storyboard = NSStoryboard(name: "FilesViewController", bundle: nil) return storyboard.instantiateController(identifier: "FilesViewController") { (coder) in - Self(coder: coder, client: client, serverNode: serverNode, share: share, path: path, rootPath: rootPath) + Self(coder: coder, accessor: accessor, serverNode: serverNode, share: share, path: path, rootPath: rootPath) } } @@ -39,8 +40,8 @@ class FilesViewController: NSViewController { return nil } - required init?(coder: NSCoder, client: SMBClient, serverNode: ServerNode, share: String, path: String, rootPath: String) { - self.client = client + required init?(coder: NSCoder, accessor: TreeAccessor, serverNode: ServerNode, share: String, path: String, rootPath: String) { + self.treeAccessor = accessor self.serverNode = serverNode self.share = share self.path = path @@ -81,16 +82,10 @@ class FilesViewController: NSViewController { Task { @MainActor in do { - if client.share != share { - if let _ = client.share { - try await client.disconnectShare() - } - try await client.connectShare(share) - } - - await dirTree.reload() + try await dirTree.reload() outlineView.reloadData() + availableSpace = try await dirTree.availableSpace() updateItemCount() } catch { NSAlert(error: error).runModal() @@ -152,7 +147,11 @@ class FilesViewController: NSViewController { } private func updateItemCount() { - statusBarView.label.stringValue = NSLocalizedString("\(outlineView.numberOfRows) items", comment: "") + if let availableSpace { + statusBarView.label.stringValue = NSLocalizedString("\(outlineView.numberOfRows) items, \(ByteCountFormatter.string(fromByteCount: Int64(availableSpace), countStyle: .file)) available", comment: "") + } else { + statusBarView.label.stringValue = NSLocalizedString("\(outlineView.numberOfRows) items", comment: "") + } } @objc @@ -206,8 +205,12 @@ class FilesViewController: NSViewController { } Task { @MainActor in - await dirTree.reload(directory: dirname, outlineView) - updateItemCount() + do { + try await dirTree.reload(directory: dirname, outlineView) + updateItemCount() + } catch { + NSAlert(error: error).runModal() + } } } @@ -232,8 +235,13 @@ class FilesViewController: NSViewController { } }() Task { - try await client.createDirectory(path: join(path, filename)) - await dirTree.reload(directory: path, outlineView) + do { + try await treeAccessor.createDirectory(share: share, path: join(path, filename)) + try await dirTree.reload(directory: path, outlineView) + updateItemCount() + } catch { + NSAlert(error: error).runModal() + } } } @@ -261,28 +269,28 @@ class FilesViewController: NSViewController { if fileNode.isDirectory { guard let navigationController = navigationController() else { return } - let client = self.client + let treeAccessor = self.treeAccessor let path = dirTree.resolvePath(fileNode) let rootPath = self.rootPath let filesViewController = FilesViewController.instantiate( - client: client, serverNode: serverNode, share: share, path: path, rootPath: rootPath + accessor: treeAccessor, serverNode: serverNode, share: share, path: path, rootPath: rootPath ) filesViewController.title = fileNode.name navigationController.push(filesViewController) } else { - let client = self.client + let treeAccessor = self.treeAccessor let path = dirTree.resolvePath(fileNode) let windowController: NSWindowController let pathExtension = URL(fileURLWithPath: path).pathExtension if MediaPlayerWindowController.supportedExtensions.contains(pathExtension) { - windowController = MediaPlayerWindowController.instantiate(path: path, client: client) + windowController = MediaPlayerWindowController.instantiate(path: path, accessor: treeAccessor) windowController.showWindow(nil) } else { - windowController = DocumentWindowController.instantiate(path: path, client: client) + windowController = DocumentWindowController.instantiate(path: path, accessor: treeAccessor) windowController.showWindow(nil) } } @@ -332,17 +340,18 @@ class FilesViewController: NSViewController { let path = fileNode.path if fileNode.isDirectory { - try await client.deleteDirectory(path: path) + try await treeAccessor.deleteDirectory(share: share, path: path) } else { - try await client.deleteFile(path: path) + try await treeAccessor.deleteFile(share: share, path: path) } reloadPaths.insert(dirname(path)) } for reloadPath in reloadPaths { - await dirTree.reload(directory: reloadPath, outlineView) + try await dirTree.reload(directory: reloadPath, outlineView) } + updateItemCount() } catch { NSAlert(error: error).runModal() } @@ -354,7 +363,7 @@ class FilesViewController: NSViewController { guard let navigationController = navigationController() else { return } guard let absolutePath = pathItem.url?.path else { return } - let client = self.client + let treeAccessor = self.treeAccessor let rootPath = self.rootPath if absolutePath.hasPrefix(rootPath) { @@ -363,7 +372,7 @@ class FilesViewController: NSViewController { guard relativePath != path else { return } let filesViewController = FilesViewController.instantiate( - client: client, serverNode: serverNode, share: share, path: relativePath, rootPath: rootPath + accessor: treeAccessor, serverNode: serverNode, share: share, path: relativePath, rootPath: rootPath ) filesViewController.title = pathItem.title @@ -516,10 +525,12 @@ extension FilesViewController: NSOutlineViewDataSource { func moveFile(from: String, to: String) async { do { - try await client.move(from: from, to: to) + try await treeAccessor.move(share: share, from: from, to: to) + + try await dirTree.reload(directory: dirname(from), outlineView) + try await dirTree.reload(directory: dirname(to), outlineView) - await dirTree.reload(directory: dirname(from), outlineView) - await dirTree.reload(directory: dirname(to), outlineView) + updateItemCount() } catch { NSAlert(error: error).runModal() } @@ -544,12 +555,12 @@ extension FilesViewController: NSOutlineViewDataSource { if let fileNode = item as? FileNode { let destination = join(fileNode.path, basename) queue.addFileTransfer( - FileUpload(source: fileURL, destination: destination, client: client) + FileUpload(source: fileURL, destination: destination, accessor: treeAccessor) ) } else { let destination = join(path, basename) queue.addFileTransfer( - FileUpload(source: fileURL, destination: destination, client: client) + FileUpload(source: fileURL, destination: destination, accessor: treeAccessor) ) } } @@ -624,8 +635,12 @@ extension FilesViewController: NSOutlineViewDelegate { await semaphore.wait() defer { Task { await semaphore.signal() } } - await dirTree.expand(fileNode, outlineView) - updateItemCount() + do { + try await dirTree.expand(fileNode, outlineView) + updateItemCount() + } catch { + NSAlert(error: error).runModal() + } } } @@ -701,8 +716,8 @@ extension FilesViewController: NSTextFieldDelegate { Task { @MainActor in do { - try await client.rename(from: node.path, to: join(dirname(node.path), textField.stringValue)) - await dirTree.reload(directory: dirname(node.path), outlineView) + try await treeAccessor.rename(share: share, from: node.path, to: join(dirname(node.path), textField.stringValue)) + try await dirTree.reload(directory: dirname(node.path), outlineView) } catch { textField.stringValue = node.name NSAlert(error: error).runModal() diff --git a/Examples/FileBrowser/FileBrowser (macOS)/MediaPlayerWindowController.swift b/Examples/FileBrowser/FileBrowser (macOS)/MediaPlayerWindowController.swift index 1681f53..5f60a92 100644 --- a/Examples/FileBrowser/FileBrowser (macOS)/MediaPlayerWindowController.swift +++ b/Examples/FileBrowser/FileBrowser (macOS)/MediaPlayerWindowController.swift @@ -12,23 +12,23 @@ class MediaPlayerWindowController: NSWindowController, NSWindowDelegate { return extensions }() - private let client: SMBClient + private let treeAccessor: TreeAccessor private let path: String - private lazy var asset = SMBAVAsset(client: client, path: path) + private lazy var asset = SMBAVAsset(accessor: treeAccessor, path: path) private var observation: NSKeyValueObservation? private var windowController: NSWindowController? - static func instantiate(path: String, client: SMBClient) -> Self { + static func instantiate(path: String, accessor: TreeAccessor) -> Self { let storyboard = NSStoryboard(name: storyboardID, bundle: nil) return storyboard.instantiateController(identifier: storyboardID) { (coder) in - Self(coder: coder, path: path, client: client) + Self(coder: coder, path: path, accessor: accessor) } } - required init?(coder: NSCoder, path: String, client: SMBClient) { + required init?(coder: NSCoder, path: String, accessor: TreeAccessor) { self.path = path - self.client = client + self.treeAccessor = accessor super.init(coder: coder) } diff --git a/Examples/FileBrowser/FileBrowser (macOS)/SharesViewController.swift b/Examples/FileBrowser/FileBrowser (macOS)/SharesViewController.swift index de06db8..0784b46 100644 --- a/Examples/FileBrowser/FileBrowser (macOS)/SharesViewController.swift +++ b/Examples/FileBrowser/FileBrowser (macOS)/SharesViewController.swift @@ -104,19 +104,18 @@ class SharesViewController: NSViewController { guard let session = SessionManager.shared.session(for: serverNode.id) else { return } Task { @MainActor in - do { - let client = session.client - _ = try await client.treeConnect(path: shareNode.name) - - let filesViewController = FilesViewController.instantiate( - client: client, serverNode: serverNode, share: shareNode.name, path: "", rootPath: "/\(server)/\(shareNode.name)" - ) - filesViewController.title = shareNode.name - - navigationController.push(filesViewController) - } catch { - NSAlert(error: error).runModal() - } + let treeAccessor = session.client.treeAccessor(share: shareNode.name) + + let filesViewController = FilesViewController.instantiate( + accessor: treeAccessor, + serverNode: serverNode, + share: shareNode.name, + path: "", + rootPath: "/\(server)/\(shareNode.name)" + ) + filesViewController.title = shareNode.name + + navigationController.push(filesViewController) } } } diff --git a/Examples/FileBrowser/FileBrowser (macOS)/SidebarViewController.swift b/Examples/FileBrowser/FileBrowser (macOS)/SidebarViewController.swift index 6b6ef45..3ba7db7 100644 --- a/Examples/FileBrowser/FileBrowser (macOS)/SidebarViewController.swift +++ b/Examples/FileBrowser/FileBrowser (macOS)/SidebarViewController.swift @@ -146,23 +146,18 @@ extension SidebarViewController: NSOutlineViewDelegate { guard let session = sessionManager.session(for: serverNode.id) else { return } Task { @MainActor in - do { - let client = session.client - _ = try await client.treeConnect(path: shareNode.name) - - let filesViewController = FilesViewController.instantiate( - client: client, - serverNode: serverNode, - share: shareNode.name, - path: "", - rootPath: "/\(shareNode.device)/\(shareNode.name)" - ) - filesViewController.title = shareNode.name - - navigationController()?.push(filesViewController) - } catch { - NSAlert(error: error).runModal() - } + let treeAccessor = session.client.treeAccessor(share: shareNode.name) + + let filesViewController = FilesViewController.instantiate( + accessor: treeAccessor, + serverNode: serverNode, + share: shareNode.name, + path: "", + rootPath: "/\(shareNode.device)/\(shareNode.name)" + ) + filesViewController.title = shareNode.name + + navigationController()?.push(filesViewController) } default: break diff --git a/Examples/FileBrowser/FileBrowser (macOS)/WindowController.swift b/Examples/FileBrowser/FileBrowser (macOS)/WindowController.swift index 4bea653..726e212 100644 --- a/Examples/FileBrowser/FileBrowser (macOS)/WindowController.swift +++ b/Examples/FileBrowser/FileBrowser (macOS)/WindowController.swift @@ -140,7 +140,7 @@ class WindowController: NSWindowController { return } - let client = topViewController.client + let treeAccessor = topViewController.treeAccessor let serverNode = topViewController.serverNode let share = topViewController.share let rootPath = topViewController.rootPath @@ -166,7 +166,7 @@ class WindowController: NSWindowController { } let filesViewController = FilesViewController.instantiate( - client: client, serverNode: serverNode, share: share, path: path, rootPath: rootPath + accessor: treeAccessor, serverNode: serverNode, share: share, path: path, rootPath: rootPath ) filesViewController.title = sender.title diff --git a/Examples/FileBrowser/FileBrowser.xcodeproj/project.pbxproj b/Examples/FileBrowser/FileBrowser.xcodeproj/project.pbxproj index 73fba85..4dae290 100644 --- a/Examples/FileBrowser/FileBrowser.xcodeproj/project.pbxproj +++ b/Examples/FileBrowser/FileBrowser.xcodeproj/project.pbxproj @@ -784,7 +784,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/kishikawakatsumi/SMBClient.git"; requirement = { - branch = main; + branch = switchtree; kind = branch; }; }; diff --git a/Examples/FileBrowser/FileBrowser.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/FileBrowser/FileBrowser.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a2625be..b66d6da 100644 --- a/Examples/FileBrowser/FileBrowser.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Examples/FileBrowser/FileBrowser.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swhitty/FlyingFox.git", "state" : { - "revision" : "e7f38fe316d815053f0c5fdc973391b333754b11", - "version" : "0.17.0" + "revision" : "0482ba51ff2d6d91a7ea449c3dbdce2a78802b85", + "version" : "0.19.0" } }, { @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/kishikawakatsumi/SMBClient.git", "state" : { - "branch" : "main", - "revision" : "1a2683c12ff2a050f500db293ce0378bdb056411" + "branch" : "switchtree", + "revision" : "3534f14f951bb2ffe6353e065dc8e9cd576fc017" } } ], diff --git a/Examples/FileBrowser/Shared/SMBAVAsset.swift b/Examples/FileBrowser/Shared/SMBAVAsset.swift index 8e0add9..2af8bbf 100644 --- a/Examples/FileBrowser/Shared/SMBAVAsset.swift +++ b/Examples/FileBrowser/Shared/SMBAVAsset.swift @@ -7,11 +7,11 @@ private let readSize = UInt32(1024 * 1024 * 10) class SMBAVAsset: AVURLAsset { private let resourceLoaderDelegate: AssetResourceLoaderDelegate - init(client: SMBClient, path: String) { + init(accessor: TreeAccessor, path: String) { let url = URL(string: "smb:///\(path)")! self.resourceLoaderDelegate = AssetResourceLoaderDelegate( - client: client, + accessor: accessor, path: path, contentType: url.pathExtension ) @@ -27,22 +27,21 @@ class SMBAVAsset: AVURLAsset { } private class AssetResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate { - private let client: SMBClient private let fileReader: FileReader private let path: String private let contentType: String? private let queue = TaskQueue() - init(client: SMBClient, path: String, contentType: String?) { - self.client = client - fileReader = client.fileReader(path: path) + init(accessor: TreeAccessor, path: String, contentType: String?) { + fileReader = accessor.fileReader(path: path) self.path = path self.contentType = contentType } func close() { - Task { + let fileReader = self.fileReader + queue.dispatch { try await fileReader.close() } } diff --git a/Sources/SMBClient/Messages/Header+CustomDebugStringConvertible.swift b/Sources/SMBClient/Messages/Header+CustomDebugStringConvertible.swift index c16bc37..6b9e5cb 100644 --- a/Sources/SMBClient/Messages/Header+CustomDebugStringConvertible.swift +++ b/Sources/SMBClient/Messages/Header+CustomDebugStringConvertible.swift @@ -14,7 +14,7 @@ extension Header: CustomDebugStringConvertible { Credits granted: \(creditRequestResponse) Flags: \(flags) Chain Offset: \(nextCommand) - Message ID: \(String(messageId, radix: 16)) + Message ID: \(messageId) Process Id: \(String(format: "0x%08x", reserved)) Tree Id: \(String(format: "0x%08x", treeId)) Session Id: \(String(format: "0x%016llx", sessionId)) @@ -33,7 +33,7 @@ extension Header: CustomDebugStringConvertible { Credits requested: \(creditRequestResponse) Flags: \(flags) Chain Offset: \(nextCommand) - Message ID: \(String(messageId, radix: 16)) + Message ID: \(messageId) Process Id: \(String(format: "0x%08x", reserved)) Tree Id: \(String(format: "0x%08x", treeId)) Session Id: \(String(format: "0x%016llx", sessionId)) diff --git a/Sources/SMBClient/SMBClient.swift b/Sources/SMBClient/SMBClient.swift index d429704..4d1a410 100644 --- a/Sources/SMBClient/SMBClient.swift +++ b/Sources/SMBClient/SMBClient.swift @@ -181,6 +181,10 @@ public class SMBClient { FileWriter(session: session, path: Pathname.normalize(path)) } + public func treeAccessor(share: String) -> TreeAccessor { + session.treeAccessor(share: share) + } + public func availableSpace() async throws -> UInt64 { let response = try await session.queryInfo(path: "", infoType: .fileSystem, fileInfoClass: .fileFsSizeInformation) diff --git a/Sources/SMBClient/Session.swift b/Sources/SMBClient/Session.swift index bd7ae0c..f12ba3d 100644 --- a/Sources/SMBClient/Session.swift +++ b/Sources/SMBClient/Session.swift @@ -3,7 +3,7 @@ import Foundation public class Session { private var messageId = SequenceNumber() private var sessionId: UInt64 = 0 - private var treeId: UInt32 = 0 + private(set) var treeId: UInt32 = 0 private var signingKey: Data? @@ -22,16 +22,39 @@ public class Session { private let connection: Connection - public init(host: String) { - connection = Connection(host: host) - onDisconnected = { _ in } + public convenience init(host: String) { + self.init(Connection(host: host)) + } + + public convenience init(host: String, port: Int) { + self.init(Connection(host: host, port: port)) } - public init(host: String, port: Int) { - connection = Connection(host: host, port: port) + private init(_ connection: Connection) { + self.connection = connection onDisconnected = { _ in } } + func newSession() -> Session { + let session = Session(connection) + + session.messageId = messageId + session.sessionId = sessionId + session.treeId = 0 + + session.signingKey = signingKey + + session.maxTransactSize = maxTransactSize + session.maxReadSize = maxReadSize + session.maxWriteSize = maxWriteSize + + return session + } + + func treeAccessor(share: String) -> TreeAccessor { + TreeAccessor(session: self, share: share) + } + public func connect() async throws { try await connection.connect() } @@ -135,9 +158,10 @@ public class Session { } public func enumShareAll() async throws -> [Share] { - try await treeConnect(path: "IPC$") + let treeAccessor = treeAccessor(share: "IPC$") + let session = try await treeAccessor.session() - let createResponse = try await create( + let createResponse = try await session.create( desiredAccess: [.readData, .writeData, .appendData, .readAttributes], fileAttributes: [.normal], shareAccess: [.read, .write], @@ -145,15 +169,16 @@ public class Session { createOptions: [.nonDirectoryFile], name: "srvsvc" ) - try await bind(fileId: createResponse.fileId) - let ioCtlResponse = try await netShareEnum(fileId: createResponse.fileId) + + try await session.bind(fileId: createResponse.fileId) + let ioCtlResponse = try await session.netShareEnum(fileId: createResponse.fileId) let rpcResponse = DCERPC.Response(data: ioCtlResponse.buffer) let netShareEnumResponse = NetShareEnumResponse(data: rpcResponse.stub) let shares = netShareEnumResponse.shareInfo1.shareInfo - try await close(fileId: createResponse.fileId) + try await session.close(fileId: createResponse.fileId) return shares.compactMap { var type = Share.ShareType(rawValue: $0.type & 0x0FFFFFFF) @@ -182,6 +207,7 @@ public class Session { treeId = response.header.treeId connectedTree = path + return response } diff --git a/Sources/SMBClient/TreeAccessor.swift b/Sources/SMBClient/TreeAccessor.swift new file mode 100644 index 0000000..33492b3 --- /dev/null +++ b/Sources/SMBClient/TreeAccessor.swift @@ -0,0 +1,141 @@ +import Foundation + +public class TreeAccessor { + public let share: String + private let session: Session + + init(session: Session, share: String) { + self.session = session.newSession() + self.share = share + } + + deinit { + let session = self.session + Task { + try await session.treeDisconnect() + } + } + + public func listDirectory(path: String, pattern: String = "*") async throws -> [File] { + let files = try await session().queryDirectory(path: Pathname.normalize(path), pattern: pattern) + return files.map { File(fileInfo: $0) } + } + + public func createDirectory(share: String? = nil, path: String) async throws { + try await session().createDirectory(path: Pathname.normalize(path.precomposedStringWithCanonicalMapping)) + } + + public func rename(share: String? = nil, from: String, to: String) async throws { + try await move(share: share, from: Pathname.normalize(from), to: Pathname.normalize(to)) + } + + public func move(share: String? = nil, from: String, to: String) async throws { + try await session().move(from: Pathname.normalize(from), to: Pathname.normalize(to.precomposedStringWithCanonicalMapping)) + } + + public func deleteDirectory(share: String? = nil, path: String) async throws { + try await session().deleteDirectory(path: Pathname.normalize(path)) + } + + public func deleteFile(share: String? = nil, path: String) async throws { + try await session().deleteFile(path: Pathname.normalize(path)) + } + + public func fileStat(share: String? = nil, path: String) async throws -> FileStat { + let response = try await session().fileStat(path: Pathname.normalize(path)) + return FileStat(response) + } + + public func existFile(share: String? = nil, path: String) async throws -> Bool { + try await session().existFile(path: Pathname.normalize(path)) + } + + public func existDirectory(share: String? = nil, path: String) async throws -> Bool { + try await session().existDirectory(path: Pathname.normalize(path)) + } + + public func fileInfo(share: String? = nil, path: String) async throws -> FileAllInformation { + let response = try await session().queryInfo(path: Pathname.normalize(path)) + return FileAllInformation(data: response.buffer) + } + + public func download(share: String? = nil, path: String) async throws -> Data { + let fileReader = fileReader(path: Pathname.normalize(path)) + + let data = try await fileReader.download() + try await fileReader.close() + + return data + } + + public func upload(share: String? = nil, content: Data, path: String) async throws { + try await upload(share: share, content: content, path: Pathname.normalize(path), progressHandler: { _ in }) + } + + public func upload(share: String? = nil, content: Data, path: String, progressHandler: (_ progress: Double) -> Void) async throws { + let fileWriter = fileWriter(share: share, path: Pathname.normalize(path)) + + try await fileWriter.upload(data: content, progressHandler: progressHandler) + try await fileWriter.close() + } + + public func upload(share: String? = nil, fileHandle: FileHandle, path: String) async throws { + try await upload(share: share, fileHandle: fileHandle, path: path, progressHandler: { _ in }) + } + + public func upload(share: String? = nil, fileHandle: FileHandle, path: String, progressHandler: (_ progress: Double) -> Void) async throws { + let fileWriter = fileWriter(share: share, path: Pathname.normalize(path)) + + try await fileWriter.upload(fileHandle: fileHandle, progressHandler: progressHandler) + try await fileWriter.close() + } + + public func upload(share: String? = nil, localPath: URL, remotePath path: String) async throws { + try await upload(share: share, localPath: localPath, remotePath: path, progressHandler: { _, _, _ in }) + } + + public func upload( + share: String? = nil, + localPath: URL, + remotePath path: String, + progressHandler: (_ completedFiles: Int, _ fileBeingTransferred: URL, _ bytesSent: Int64) -> Void + ) async throws { + let fileWriter = fileWriter(share: share, path: Pathname.normalize(path)) + + try await fileWriter.upload(localPath: localPath, progressHandler: progressHandler) + try await fileWriter.close() + } + + public func fileReader(share: String? = nil, path: String) -> FileReader { + FileReader(session: session, path: Pathname.normalize(path)) + } + + public func fileWriter(share: String? = nil, path: String) -> FileWriter { + FileWriter(session: session, path: Pathname.normalize(path)) + } + + public func availableSpace() async throws -> UInt64 { + let response = try await session().queryInfo(path: "", infoType: .fileSystem, fileInfoClass: .fileFsSizeInformation) + + let sizeInformation = FileFsSizeInformation(data: response.buffer) + let availableAllocationUnits = sizeInformation.availableAllocationUnits + let sectorsPerAllocationUnit = sizeInformation.sectorsPerAllocationUnit + let bytesPerSector = sizeInformation.bytesPerSector + + let bytesPerAllocationUnit = UInt64(sectorsPerAllocationUnit * bytesPerSector) + let availableSpaceBytes = availableAllocationUnits * bytesPerAllocationUnit + + return availableSpaceBytes + } + + public func keepAlive() async throws -> Echo.Response { + try await session().echo() + } + + func session() async throws -> Session { + if session.treeId == 0 { + try await session.treeConnect(path: share) + } + return session + } +}