diff --git a/Sources/SMBClient/SMBClient.swift b/Sources/SMBClient/SMBClient.swift index bb7cc74..b3513ce 100644 --- a/Sources/SMBClient/SMBClient.swift +++ b/Sources/SMBClient/SMBClient.swift @@ -170,7 +170,11 @@ public class SMBClient { FileWriter(session: session, path: Pathname.normalize(path)) } - public func availableSpace() async throws -> UInt64 { + public func treeAccessor(share: String) -> TreeAccessor { + session.treeAccessor(share: share) + } + + public func availableSpace(share: String? = nil) async throws -> UInt64 { let response = try await session.queryInfo(path: "", infoType: .fileSystem, fileInfoClass: .fileFsSizeInformation) let sizeInformation = FileFsSizeInformation(data: response.buffer) diff --git a/Sources/SMBClient/Session.swift b/Sources/SMBClient/Session.swift index bd7ae0c..7f7acb6 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,9 @@ public class Session { } public func enumShareAll() async throws -> [Share] { - try await treeConnect(path: "IPC$") + let treeAccessor = treeAccessor(share: "IPC$") - let createResponse = try await create( + let createResponse = try await treeAccessor.session.create( desiredAccess: [.readData, .writeData, .appendData, .readAttributes], fileAttributes: [.normal], shareAccess: [.read, .write], @@ -145,6 +168,7 @@ public class Session { createOptions: [.nonDirectoryFile], name: "srvsvc" ) + try await bind(fileId: createResponse.fileId) let ioCtlResponse = try await netShareEnum(fileId: createResponse.fileId) @@ -182,6 +206,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..bbd34a7 --- /dev/null +++ b/Sources/SMBClient/TreeAccessor.swift @@ -0,0 +1,141 @@ +import Foundation + +public class TreeAccessor { + let session: Session + let share: String + + init(session: Session, share: String) { + self.session = session.newSession() + self.share = share + } + + deinit { + let this = self + Task { + try await this.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(share: String? = nil) 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() + } + + private func session() async throws -> Session { + if session.treeId == 0 { + try await session.treeConnect(path: share) + } + return session + } +}