From 8beedae4cd4d2019c4253595a8af7f279fdced7e Mon Sep 17 00:00:00 2001 From: Amir Abbas Date: Sat, 27 Apr 2019 02:16:38 +0430 Subject: [PATCH] - contents(atPath:) to use Range instead of offset-length pair - write(data:) parameter is DataProtocol - Updated Readme - Refactoring obj-c layer --- AMSMB2.xcodeproj/project.pbxproj | 4 + AMSMB2/AMSMB2.swift | 185 ++++++++----------------------- AMSMB2/ObjCCompat.swift | 178 +++++++++++++++++++++++++++++ AMSMB2Tests/AMSMB2Tests.swift | 28 ++++- README.md | 95 ++++++++-------- 5 files changed, 300 insertions(+), 190 deletions(-) create mode 100644 AMSMB2/ObjCCompat.swift diff --git a/AMSMB2.xcodeproj/project.pbxproj b/AMSMB2.xcodeproj/project.pbxproj index 2bd7f74..24dd8f2 100644 --- a/AMSMB2.xcodeproj/project.pbxproj +++ b/AMSMB2.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 790FA33920B2CC5600578C8A /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 790FA33820B2CC5600578C8A /* Extensions.swift */; }; 7957E1F220F1EEB100F96CE4 /* Fsctl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7957E1F120F1EEB100F96CE4 /* Fsctl.swift */; }; + 7966DC4D2273A5CA00BBC39D /* ObjCCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7966DC4C2273A5CA00BBC39D /* ObjCCompat.swift */; }; 79B5EDBD210FE1DB00449EF6 /* MSRPC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79B5EDBC210FE1DB00449EF6 /* MSRPC.swift */; }; 79FDFE5E20AD439F00749FB5 /* AMSMB2.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79FDFE5420AD439F00749FB5 /* AMSMB2.framework */; }; 79FDFE6320AD439F00749FB5 /* AMSMB2Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79FDFE6220AD439F00749FB5 /* AMSMB2Tests.swift */; }; @@ -33,6 +34,7 @@ /* Begin PBXFileReference section */ 790FA33820B2CC5600578C8A /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; 7957E1F120F1EEB100F96CE4 /* Fsctl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fsctl.swift; sourceTree = ""; }; + 7966DC4C2273A5CA00BBC39D /* ObjCCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjCCompat.swift; sourceTree = ""; }; 79B5EDBC210FE1DB00449EF6 /* MSRPC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSRPC.swift; sourceTree = ""; }; 79FDFE5420AD439F00749FB5 /* AMSMB2.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AMSMB2.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 79FDFE5720AD439F00749FB5 /* AMSMB2.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AMSMB2.h; sourceTree = ""; }; @@ -92,6 +94,7 @@ 79FDFE5820AD439F00749FB5 /* Info.plist */, 79FDFE5720AD439F00749FB5 /* AMSMB2.h */, 79FDFE6E20AD45C100749FB5 /* AMSMB2.swift */, + 7966DC4C2273A5CA00BBC39D /* ObjCCompat.swift */, 79FDFE7B20AD545D00749FB5 /* Context.swift */, 79B5EDBC210FE1DB00449EF6 /* MSRPC.swift */, 7957E1F120F1EEB100F96CE4 /* Fsctl.swift */, @@ -235,6 +238,7 @@ 790FA33920B2CC5600578C8A /* Extensions.swift in Sources */, 79FDFE6F20AD45C100749FB5 /* AMSMB2.swift in Sources */, 79B5EDBD210FE1DB00449EF6 /* MSRPC.swift in Sources */, + 7966DC4D2273A5CA00BBC39D /* ObjCCompat.swift in Sources */, 7957E1F220F1EEB100F96CE4 /* Fsctl.swift in Sources */, 79FDFE7A20AD541500749FB5 /* URL.swift in Sources */, 79FDFF0120AD852200749FB5 /* FileHandle.swift in Sources */, diff --git a/AMSMB2/AMSMB2.swift b/AMSMB2/AMSMB2.swift index df6a083..91e4ad3 100644 --- a/AMSMB2/AMSMB2.swift +++ b/AMSMB2/AMSMB2.swift @@ -234,6 +234,8 @@ public class AMSMB2: NSObject, NSSecureCoding, Codable, NSCopying, CustomReflect /** Disconnects from a share. + + - Important: Disconnecting when an operation is in progress may cause disgraceful termination of operation. */ @objc(disconnectShare) open func disconnectShare() { @@ -242,11 +244,15 @@ public class AMSMB2: NSObject, NSSecureCoding, Codable, NSCopying, CustomReflect /** Disconnects from a share. + + - Important: Disconnecting when an operation is in progress may cause disgraceful termination of operation. */ @objc(disconnectShareWithCompletionHandler:) open func disconnectShare(completionHandler: SimpleCompletionHandler) { q.async { do { + self.connectLock.lock() + defer { self.connectLock.unlock() } try self.context?.disconnect() self.context = nil self.connectedShare = nil @@ -310,52 +316,6 @@ public class AMSMB2: NSObject, NSSecureCoding, Codable, NSCopying, CustomReflect } } - /** - Enumerates shares' list on server. - - - Parameters: - - completionHandler: closure will be run after enumerating is completed. - - names: An array of share names. Can be passed to `connectShare()` function. - - comments: An array of share remark name, related to names array with same index. Suitable for displaying shares to user. - - error: `Error` if any occured during enumeration. - */ - @available(swift, obsoleted: 1.0) - @objc(listSharesWithCompletionHandler:) - public func __listShares(completionHandler: @escaping (_ names: [String], _ comments: [String], _ error: Error?) -> Void) { - listShares(enumerateHidden: false) { (result) in - switch result { - case .success(let shares): - completionHandler(shares.map({ $0.name }), shares.map({ $0.comment }), nil) - case .failure(let error): - completionHandler([], [], error) - } - } - } - - /** - Enumerates shares' list on server. - - - Parameters: - - enumerateHidden: enumrating special/administrative e.g. user directory in macOS or - shares usually ends with `$`, e.g. `C$` or `admin$`. - - completionHandler: closure will be run after enumerating is completed. - - names: An array of share names. Can be passed to `connectShare()` function. - - comments: An array of share remark name, related to names array with same index. Suitable for displaying shares to user. - - error: `Error` if any occured during enumeration. - */ - @available(swift, obsoleted: 1.0) - @objc(listSharesWithEnumerateHidden:completionHandler:) - public func __listShares(enumerateHidden: Bool, completionHandler: @escaping (_ names: [String], _ comments: [String], _ error: Error?) -> Void) { - listShares(enumerateHidden: enumerateHidden) { (result) in - switch result { - case .success(let shares): - completionHandler(shares.map({ $0.name }), shares.map({ $0.comment }), nil) - case .failure(let error): - completionHandler([], [], error) - } - } - } - /** Enumerates directory contents in the give path. @@ -374,23 +334,6 @@ public class AMSMB2: NSObject, NSSecureCoding, Codable, NSCopying, CustomReflect } } - /** - Enumerates directory contents in the give path. - - - Parameters: - - atPath: path of directory to be enumerated. - - completionHandler: closure will be run after enumerating is completed. - - recursive: subdirectories will enumerated if `true`. - - contents: An array of `[URLResourceKey: Any]` which holds files' attributes. file name is stored in `.nameKey`. - - error: `Error` if any occured during enumeration. - */ - @available(swift, obsoleted: 1.0) - @objc(contentsOfDirectoryAtPath:recursive:completionHandler:) - public func __contentsOfDirectory(atPath path: String, recursive: Bool = false, - completionHandler: @escaping (_ contents: [[URLResourceKey: Any]]?, _ error: Error?) -> Void) { - contentsOfDirectory(atPath: path, recursive: recursive, completionHandler: convert(completionHandler)) - } - /** Returns a dictionary that describes the attributes of the mounted file system on which a given path resides. @@ -421,23 +364,6 @@ public class AMSMB2: NSObject, NSSecureCoding, Codable, NSCopying, CustomReflect } } - /** - Returns a dictionary that describes the attributes of the mounted file system on which a given path resides. - - - Parameters: - - forPath: Any pathname within the mounted file system. - - completionHandler: closure will be run after fetching attributes is completed. - - attrubutes: A dictionary object that describes the attributes of the mounted file system on which path resides. - See _File-System Attribute Keys_ for a description of the keys available in the dictionary. - - error: `Error` if any occured during enumeration. - */ - @available(swift, obsoleted: 1.0) - @objc(attributesOfFileSystemForPath:completionHandler:) - public func __attributesOfFileSystem(forPath path: String, - completionHandler: @escaping (_ attrubutes: [FileAttributeKey: Any]?, _ error: Error?) -> Void) { - attributesOfFileSystem(forPath: path, completionHandler: convert(completionHandler)) - } - /** Returns the attributes of the item at given path. @@ -462,22 +388,6 @@ public class AMSMB2: NSObject, NSSecureCoding, Codable, NSCopying, CustomReflect } } - /** - Returns the attributes of the item at given path. - - - Parameters: - - atPath: path of file to be enumerated. - - completionHandler: closure will be run after enumerating is completed. - - file: An dictionary with `URLResourceKey` as key which holds file's attributes. - - error: `Error` if any occured during enumeration. - */ - @available(swift, obsoleted: 1.0) - @objc(attributesOfItemAtPath:completionHandler:) - public func __attributesOfItem(atPath path: String, - completionHandler: @escaping (_ file: [URLResourceKey: Any]?, _ error: Error?) -> Void) { - attributesOfItem(atPath: path, completionHandler: convert(completionHandler)) - } - /** Creates a new directory at given path. @@ -610,51 +520,58 @@ public class AMSMB2: NSObject, NSSecureCoding, Codable, NSCopying, CustomReflect } } + /** + Fetches whole data contents of a file. With reporting progress on about every 1MiB. + + - Parameters: + - atPath: path of file to be fetched. + - progress: reports progress of recieved bytes count read and expected content length. + User must return `true` if they want to continuing or `false` to abort reading. + - bytes: recieved bytes count. + - total: expected content length. + - completionHandler: closure will be run after reading data is completed. + - result: a `Data` object which contains file contents. + */ + open func contents(atPath path: String, progress: SMB2ReadProgressHandler, + completionHandler: @escaping (_ result: Result) -> Void) { + contents(atPath: path, range: 0.. Void) { + open func contents(atPath path: String, range: R? = nil, progress: SMB2ReadProgressHandler, + completionHandler: @escaping (_ result: Result) -> Void) + where R.Bound: FixedWidthInteger + { + let range: Range = range?.relative(to: 0..= 0 else { - throw POSIXError(.EINVAL, description: "Invalid content offset.") - } - + completionHandler(.init(catching: { () -> Data in let stream = OutputStream.toMemory() - if length > 0 { - try self.read(path: path, range: offset..<(offset + Int64(length)), to: stream, progress: progress) - } else if length < 0 { - try self.read(path: path, range: offset..(data: DataType, toPath path: String, progress: SMB2WriteProgressHandler, + completionHandler: SimpleCompletionHandler) { q.async { do { - let stream = InputStream(data: data) + let stream = InputStream(data: Data(data)) try self.write(from: stream, toPath: path, chunkSize: 0, progress: progress) completionHandler?(nil) } catch { @@ -1165,16 +1081,3 @@ extension AMSMB2 { try file.fsync() } } - -extension AMSMB2 { - func convert(_ resultCompletion: @escaping (T?, Error?) -> Void) -> ((Result) -> Void) { - return { result in - switch result { - case .success(let val): - resultCompletion(val, nil) - case .failure(let error): - resultCompletion(nil, error) - } - } - } -} diff --git a/AMSMB2/ObjCCompat.swift b/AMSMB2/ObjCCompat.swift new file mode 100644 index 0000000..4ae4f0b --- /dev/null +++ b/AMSMB2/ObjCCompat.swift @@ -0,0 +1,178 @@ +// +// ObjCCompat.swift +// AMSMB2 +// +// Created by Amir Abbas on 2/7/1398 AP. +// Copyright © 1398 AP Mousavian. All rights reserved. +// + +import Foundation + +extension AMSMB2 { + /** + Enumerates shares' list on server. + + - Parameters: + - completionHandler: closure will be run after enumerating is completed. + - names: An array of share names. Can be passed to `connectShare()` function. + - comments: An array of share remark name, related to names array with same index. Suitable for displaying shares to user. + - error: `Error` if any occured during enumeration. + */ + @available(swift, obsoleted: 1.0) + @objc(listSharesWithCompletionHandler:) + public func __listShares(completionHandler: @escaping (_ names: [String], _ comments: [String], _ error: Error?) -> Void) { + listShares(enumerateHidden: false) { (result) in + switch result { + case .success(let shares): + completionHandler(shares.map({ $0.name }), shares.map({ $0.comment }), nil) + case .failure(let error): + completionHandler([], [], error) + } + } + } + + /** + Enumerates shares' list on server. + + - Parameters: + - enumerateHidden: enumrating special/administrative e.g. user directory in macOS or + shares usually ends with `$`, e.g. `C$` or `admin$`. + - completionHandler: closure will be run after enumerating is completed. + - names: An array of share names. Can be passed to `connectShare()` function. + - comments: An array of share remark name, related to names array with same index. Suitable for displaying shares to user. + - error: `Error` if any occured during enumeration. + */ + @available(swift, obsoleted: 1.0) + @objc(listSharesWithEnumerateHidden:completionHandler:) + public func __listShares(enumerateHidden: Bool, completionHandler: @escaping (_ names: [String], _ comments: [String], _ error: Error?) -> Void) { + listShares(enumerateHidden: enumerateHidden) { (result) in + switch result { + case .success(let shares): + completionHandler(shares.map({ $0.name }), shares.map({ $0.comment }), nil) + case .failure(let error): + completionHandler([], [], error) + } + } + } + + /** + Enumerates directory contents in the give path. + + - Parameters: + - atPath: path of directory to be enumerated. + - completionHandler: closure will be run after enumerating is completed. + - recursive: subdirectories will enumerated if `true`. + - contents: An array of `[URLResourceKey: Any]` which holds files' attributes. file name is stored in `.nameKey`. + - error: `Error` if any occured during enumeration. + */ + @available(swift, obsoleted: 1.0) + @objc(contentsOfDirectoryAtPath:recursive:completionHandler:) + public func __contentsOfDirectory(atPath path: String, recursive: Bool = false, + completionHandler: @escaping (_ contents: [[URLResourceKey: Any]]?, _ error: Error?) -> Void) { + contentsOfDirectory(atPath: path, recursive: recursive, completionHandler: convert(completionHandler)) + } + + /** + Returns a dictionary that describes the attributes of the mounted file system on which a given path resides. + + - Parameters: + - forPath: Any pathname within the mounted file system. + - completionHandler: closure will be run after fetching attributes is completed. + - attrubutes: A dictionary object that describes the attributes of the mounted file system on which path resides. + See _File-System Attribute Keys_ for a description of the keys available in the dictionary. + - error: `Error` if any occured during enumeration. + */ + @available(swift, obsoleted: 1.0) + @objc(attributesOfFileSystemForPath:completionHandler:) + public func __attributesOfFileSystem(forPath path: String, + completionHandler: @escaping (_ attrubutes: [FileAttributeKey: Any]?, _ error: Error?) -> Void) { + attributesOfFileSystem(forPath: path, completionHandler: convert(completionHandler)) + } + + /** + Returns the attributes of the item at given path. + + - Parameters: + - atPath: path of file to be enumerated. + - completionHandler: closure will be run after enumerating is completed. + - file: An dictionary with `URLResourceKey` as key which holds file's attributes. + - error: `Error` if any occured during enumeration. + */ + @available(swift, obsoleted: 1.0) + @objc(attributesOfItemAtPath:completionHandler:) + public func __attributesOfItem(atPath path: String, + completionHandler: @escaping (_ file: [URLResourceKey: Any]?, _ error: Error?) -> Void) { + attributesOfItem(atPath: path, completionHandler: convert(completionHandler)) + } + + /** + Fetches data contents of a file from an offset with specified length. With reporting progress + on about every 1MiB. + + - Note: If offset is bigger than file's size, an empty `Data` will be returned. If length exceeds file, returned data + will be truncated to entire file content from given offset. + + - Parameters: + - atPath: path of file to be fetched. + - offset: first byte of file to be read, starting from zero. + - length: length of bytes should be read from offset. If a value. + - progress: reports progress of recieved bytes count read and expected content length. + User must return `true` if they want to continuing or `false` to abort reading. + - bytes: recieved bytes count. + - total: expected content length. + - completionHandler: closure will be run after reading data is completed. + - contents: a `Data` object which contains file contents. + - error: `Error` if any occured during reading. + */ + @available(swift, obsoleted: 1.0) + @objc(contentsAtPath:fromOffset:toLength:progress:completionHandler:) + open func __contents(atPath path: String, offset: Int64 = 0, length: Int = -1, progress: SMB2ReadProgressHandler, + completionHandler: @escaping (_ contents: Data?, _ error: Error?) -> Void) { + guard offset >= 0 else { + let error = POSIXError(.EINVAL, description: "Invalid content offset.") + completionHandler(nil, error) + return + } + + if length > 0 { + contents(atPath: path, range: offset..<(offset + Int64(length)), progress: progress, completionHandler: convert(completionHandler)) + } else if length < 0 { + contents(atPath: path, range: offset..(_ resultCompletion: @escaping (T?, Error?) -> Void) -> ((Result) -> Void) { + return { result in + switch result { + case .success(let val): + resultCompletion(val, nil) + case .failure(let error): + resultCompletion(nil, error) + } + } + } +} diff --git a/AMSMB2Tests/AMSMB2Tests.swift b/AMSMB2Tests/AMSMB2Tests.swift index dd3f882..7e903d9 100644 --- a/AMSMB2Tests/AMSMB2Tests.swift +++ b/AMSMB2Tests/AMSMB2Tests.swift @@ -179,10 +179,10 @@ class AMSMB2Tests: XCTestCase { private func readWrite(size: Int, checkLeak: Bool = false, function: String) { let expectation = self.expectation(description: function) - expectation.expectedFulfillmentCount = 2 + expectation.expectedFulfillmentCount = 3 let smb = AMSMB2(url: server, credential: credential)! - print(#function, "Large test size:", size) + print(#function, "test size:", size) let data = randomData(size: size) let baseMemUsage = report_memory() @@ -203,17 +203,35 @@ class AMSMB2Tests: XCTestCase { if checkLeak { XCTAssertLessThan(self.report_memory() - baseMemUsage, 2 * size) } + smb.contents(atPath: "writetest.dat", progress: { (progress, total) -> Bool in XCTAssertGreaterThan(progress, 0) XCTAssertEqual(total, Int64(data.count)) print(function, "downloaded:", progress, "of", total) return true - }, completionHandler: { (rdata, error) in + }, completionHandler: { result in if checkLeak { XCTAssertLessThan(self.report_memory() - baseMemUsage, 2 * size) } - XCTAssertNil(error) - XCTAssertEqual(data, rdata) + switch result { + case .success(let rdata): + XCTAssertEqual(data, rdata) + case .failure: + XCTAssert(false) + } + expectation.fulfill() + }) + + smb.contents(atPath: "writetest.dat", range: .. Void) { - let client = AMSMB2(url: self.serverURL, credential: self.credential)! + lazy private var client = AMSMB2(url: self.serverURL, credential: self.credential)! + + private func connect(handler: @escaping (Result) -> Void) { + // AMSMB2 can handle queueing connection requests client.connectShare(name: self.share) { error in - handler(client, error) + if let error = error { + handler(.failure(error)) + } else { + handler(.success(self.client)) + } } } func listDirectory(path: String) { - connect { (client, error) in - if let error = error { - print(error) - return - } - - client?.contentOfDirectory(atPath: path, - completionHandler: { (files, error) in - if let error = error { - print(error) - return + connect { result in + switch result { + case .success(let client): + client.contentsOfDirectory(atPath: path) { result in + switch result { + case .success(let files): + for entry in files { + print("name:", entry[.nameKey] as! String, + ", path:", entry[.pathKey] as! String, + ", type:", entry[.fileResourceTypeKey] as! URLFileResourceType, + ", size:", entry[.fileSizeKey] as! Int64, + ", modified:", entry[.contentModificationDateKey] as! Date, + ", created:", entry[.creationDateKey] as! Date) + } + + case .failure(let error): + print(error) + } } - for entry in files { - print("name: ", entry[.nameKey] as! String, - ", path: ", entry[.pathKey], - ", type: ", entry[.fileResourceTypeKey] as? URLFileResourceType, - ", size: ", entry[.fileSizeKey] as? Int64, - ", modified: ", entry[.contentModificationDateKey], - ", created: ", entry[.creationDateKey] - ) - } - }) + case .failure(let error): + print(error) + } } } func moveItem(path: String, to toPath: String) { - self.connect { (client, error) in - if let error = error { - print(error) - return - } - - client?.moveItem(atPath: path, toPath: toPath) { error in - if let error = error { - print(error) - } else { - print("\(path) moved successfully.") - } - - // Disconnecting is optional, it will be called eventually - // when `AMSMB2` object is freed. - // You may call it explicitly to detect errors. - client?.disconnectShare(completionHandler: { (error) in + self.connect { result in + switch result { + case .success(let client): + client.moveItem(atPath: path, toPath: toPath) { error in if let error = error { print(error) + } else { + print("\(path) moved successfully.") } - }) + + // Disconnecting is optional, it will be called eventually + // when `AMSMB2` object is freed. + // You may call it explicitly to detect errors. + client.disconnectShare(completionHandler: { (error) in + if let error = error { + print(error) + } + }) + } + + case .failure(let error): + print(error) } } }