diff --git a/.swift-version b/.swift-version deleted file mode 100644 index 389f774..0000000 --- a/.swift-version +++ /dev/null @@ -1 +0,0 @@ -4.0 \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index b1b891e..2c10fd4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: objective-c -osx_image: xcode9 +osx_image: xcode10.2 xcode_project: $PROJECTNAME.xcodeproj env: global: @@ -8,21 +8,19 @@ env: - IOS_FRAMEWORK_SCHEME="$PROJECTNAME iOS" - MACOS_FRAMEWORK_SCHEME="$PROJECTNAME OSX" - TVOS_FRAMEWORK_SCHEME="$PROJECTNAME tvOS" + - TEST_FRAMEWORK_SCHEME="$PROJECTNAMETests" - IOS_SDK=iphonesimulator - MACOS_SDK=macosx - TVOS_SDK=appletvsimulator matrix: - - DESTINATION="OS=11.0,name=iPad Pro (10.5-inch)" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="NO" CARTHAGEDEPLOY="YES" - - DESTINATION="OS=11.0,name=iPhone 8" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="YES" CARTHAGEDEPLOY="NO" - - DESTINATION="OS=11.0,name=iPhone 6" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="NO" CARTHAGEDEPLOY="NO" - - - DESTINATION="OS=11.0,name=Apple TV 1080p" SCHEME="$TVOS_FRAMEWORK_SCHEME" SDK="$TVOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="NO" CARTHAGEDEPLOY="NO" - # - DESTINATION="OS=10.0,name=Apple TV 1080p" SCHEME="$TVOS_FRAMEWORK_SCHEME" SDK="$TVOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="NO" CARTHAGEDEPLOY="NO" - - - DESTINATION="arch=x86_64" SCHEME="$MACOS_FRAMEWORK_SCHEME" SDK="$MACOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="NO" CARTHAGEDEPLOY="NO" + - DESTINATION="OS=12.2,name=iPad Pro (10.5-inch)" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" CARTHAGEDEPLOY="YES" + - DESTINATION="OS=12.2,name=iPhone 8" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" POD="YES" + - DESTINATION="arch=x86_64" SCHEME="$MACOS_FRAMEWORK_SCHEME" SDK="$MACOS_SDK" + - DESTINATION="arch=x86_64" SCHEME="FilesProviderTests" SDK="$MACOS_SDK" RUN_TESTS="YES" + - DESTINATION="OS=12.2,name=Apple TV 1080p" SCHEME="$TVOS_FRAMEWORK_SCHEME" SDK="$TVOS_SDK" before_install: - - gem install xcpretty --no-rdoc --no-ri --no-document --quiet - - gem install cocoapods --no-rdoc --no-ri --no-document --quiet + - gem install xcpretty --no-document --quiet + - gem install cocoapods --no-document --quiet # - gem install xcpretty-travis-formatter script: @@ -78,4 +76,4 @@ deploy: # repo: amosavian/$PROJECTNAME repo: amosavian/FileProvider tags: true - condition: "$CARTHAGEDEPLOY = YES" \ No newline at end of file + condition: "$CARTHAGEDEPLOY = YES" diff --git a/Docs/DESIGN.md b/Docs/DESIGN.md new file mode 100644 index 0000000..5a45afd --- /dev/null +++ b/Docs/DESIGN.md @@ -0,0 +1,273 @@ +![File Provider](fileprovider.png) + +# Concepts and Design + +## Protocols and base classes + +Every provider class conforms to some of the protocols each defines which operations are doable by a particular provider. +This allows developers to work with any provider class and to use a simple downcast to +intended protocol via `as?` optional keyword and call intended method. + +### FileProviderBasic + +This protocols consists basic variables necessary for all providers and functions to query contents and status of provider. + +`type` static property should return a string which is usually provider's name. +Value can be usedfor display purposes and to compare two different instances' type. + +`baseURL` determines root url of provider instance. +Some providers doesn't map url to files thus this variable is set to nil. `url(of:)` default implementation +uses this url to create a url which points to specified path. + +`dispatch_queue` and `operation_queue` are dispatch and operation queues used to have async operation. +As a rule of thumb, file operations are done in operation queue and querying are done by dispatch queue. +Some objects and classes like `NSFileCoordinator` uses OperationQueue for async operations. + +`delegate` is a used to inform controller about operation status/progress to update UI. +Avoid calling delegate methods directly and use `delegateNotify()` method in your implementation instead. + +`credential` property stores user and password necessary to access provider. +Local provider would ignore it. + +If listing query is paginated `contentsOfDirectory()` and `searchFiles()` may return both truncated +list array and error in case 2nd page can't be retrieved. +Thus it's safe to check `error` first to ensure list is complete. Truncated result is usable until full list is retrieved. + +`storageProperties()` returns `VolumeObject` see below for more information. + +When implementing `searchFiles()`, check `fileObject.mapPredicate()` using `query.evaluate(with:)`. +You may use `query` parameter to create a search string if provider supports search functionality. + +Practically, `searchFiles(path: path, recursive: false, query: NSPredicate(format: "TRUEPREDICATE"))` should be equal with `contentsOfDirectory()` method. +Consequently, if provider enlisting and search backend are same (e.g. iCloud, OneDrive or Google) +implement `contentsOfDirectory()` as a wrapper around `searchFiles()`, +otherwise implement'em independently for optimization reason. + +Avoid `isReachable()` to check connectivity and reachability. +Instead do operation and allow it o fail if there is a connection problem. +It may be deprecated at any time. + +`url(of:)` and `relativePathOf(url:)` have default implementation which build a url using `baseURL` property. +In case that `baseURL` is `nil`, it will wrap path inside a `URL` instance. +You may override them if more functionality is needed (e.g. OneDrive) or appending path to baseURL can't be mapped to file url directly. + +### FileProviderBasicRemote + +Adds a `session` and `cache` object for providers that need internet connection and `URLSession` object. +If your provider is a HTTP based api, subclass `HTTPFileProvider` class otherwise +(e.g. FTP or SMB) conform it to this protocol. + +### FileProviderOperations + +This protocol encapsulates methods for copying, moving, renaming, removing and downloading/uploading files. + +If your provider is a subclass of `HTTPFileProvider`, you may implement +`request(for:, overwrite:, attributes:)` abstract method which handles default implementation for these methods. +In this case, you need to create a `URLRequest` object based on `operation` parameter, +usually done a switch case statement. +See [`WebDAVFileProvder`](Sources/WebDAVFileProvider.swift) and [`OneDriveFileProvider`](Sources/OneDriveFileProvider) classes to see an example. + +### FileProviderReadWrite + +This protocol declares three methods to read and write files. + +You must care about memory when using these functions. If you must handle big data, +write it to a temporary file and use `copyItem(localFile:, to:)` or `copyItem(path:, toLocalURL:)` methods accordingly. + +### FileProviderMonitor + +This protocol allows developer to update UI when file list is changed. +It's not implemented for all providers and some providers like FTP and WebDAV don't support such functionality. + +If you are implementing it for a HTTP-based provider, +use `longpoolSession` to create a monitor request to avoid expiring requests frequently. + +Implementation details vary based on provider specifications. + +### FileProvideUndoable + +Implementing this protocol is a little hard as you must save operation inside `undoManager` for any operation. + + +### FileProviderSharing + +`publicLink(to:, completionHandler:)` method allows user to share file with other people. +Not all providers support this functionality and implementation details vary based on provider specification. + +### ExtendedFileProvider + +This protocol provides a way to fetch files' thumbnail and metadata. +Providers which have endpoint to get meta data (like Dropbox) implements this protocol. +Due to extensive network overload of fetching thumbnail, +It's recommended to check file using `thumbnailOfFileSupported(path)` method +to find out either provider supports thumbnail generation for specified file or not. +These methods only check file extension as a indicator and won't check file using provider. +A `true` result does not indicate that the file really has a thumbnail or not. + +Implementation of `thumbnailOfFile(path:, dimension:)` must provide a NSImage/UIImage with size +according to `dimension` parameter. If server supports requesting thumbnail with specified size, +implementation would pass requested dimension to server, +otherwise implementation must resize image using `ExtendedFileProvider.scaleDown(image:, toSize:)` method to resize image. + +`ExtendedFileProvider.convertToImage(pdfURL:, page:)` and `ExtendedFileProvider.convertToImage(pdfPage:)` methods can convert pdf file into an image. +Please note `pdfURL` parameter must be a local file url. + +`propertiesOfFile()` method will extract meta data of specified file and return it as a dictionary orders by `keys` parameter. +Keys may vary according to file type, e.g. EXIF data for an image or ID3 tags for a music file. + +To extend thumbnail generation behavior for local file to types which are not supported by Apple platform, +you may change `LocalFileInformationGenerator` struct static properties. +Some methods in this struct are unimplemented to allow developer to use third-party libraries +to provide meta data and thumbnail for other types of file. + +### FileProvider + +This protocol does not define any method, but indicates that class conforms to `FileProviderBasic `, +`FileProviderOperations`, `FileProviderReadWrite` and `NSCopying` protocols. + +### FileOperationType + +This enum holds operation type and associated information like source and destination path. +Developer is exposed to this enum in delegate methods. +Internally it's extensively used to refactor operation methods and to store operation info in +related `URLSessionTask` inside `taskDescription` property. + +As a associated enum, it can not be bridged to Objective-C. + +### FileObject + +`FileObject` class stores file properties in a dictionary with `URLResourceKey` as key type. +All other properties are computed variables and simply cast value to a strict swift type. + +Provider will create and return instance of `FileObject` class or its descendants in `contentsOfDirectory()`, `attributesOfItem()` and `searchFiles()` methods. +Provider **must** set `name` and `path` properties. + +`path`'s value can be a unix-style hierarchal path or other ways to point a file supported by server. +Some providers like Dropbox and OneDrive define accessing to file by id or revision. +As a convention, these alternative paths are structured like `type:identifier` e.g. `rev:abcd1234` or `id:abcd1234`. +Google only allows to address file with `id:abcd1234` and does not provide unix-style path. + +- **Important:** Never rely on `path` last component to extract file name, instead use `name` property. +Providers like `Google` have only file id in path thus using `path.lastPathComponent` to display file name may lead to confusion and improper result. + +### VolumeObject + +`VolumeObject` class is identical to `FileObject` structurally but only uses `URLResourceKey`'s keys which begin with `volume`. +An implementation of provider must return this enumerated in `storageProperties()` with properties like total size and free space of storage. + +There is not correspondent key in storage for `usage` property, +indeed it's calculated by subtracting available space from total space. + +## Progress handling + +Almost all methods return a `Progress` instance which encapsulates progress fraction and a way to cancel entire operation. +It's upon your provider's implementation to update progress `totalUnitCount` and `completedUnitCount` properties and assign a cancellation handler to Progress object. +Cancellation handler may call `Operation`'s or `URLSessionTask`'s `cancel()` method to interrupt operation. + +A typical progress handling in this library is like this: + +```swift +// totalUnitCount must be set to file size for downloading/uploading operation. +// totalUnitCount must be set to 1 for a simple remote file operation. +let progress = Progress(totalUnitCount: size) +// allow updating progress inside URLSession's delegate methods +progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey) +// kind must be set to .file for downloading/uploading operation. +progress.kind = .file +progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) +// progres.cancel() will call task.cancel() method. +progress.cancellationHandler = { [weak task] in + task?.cancel() +} +// Set .startingTimeKey to calculate estimated remaining time and speed in delegate. +progress.setUserInfoObject(Date(), forKey: .startingTimeKey) +``` + +Please note `.fileProvderOperationTypeKey` and `.startingTimeKey` are custom user info assigned to progress +to allow updating progress inside URLSession's delegate methods and calculating estimated remaining time and speed. +Your implementation must set these user info objects if you are using `SessionDelegate` object. + +You may update cancellation handler if another task is added. + +## Error handling + +This library doesn't manipulate errors returned by Foundation methods and uses `URLError` and `CococaError` to report error as far as there is a corresponding defined error. +You can use `urlError(_ :, code:)` and `cocoaError(_:, code:)` convenience methods to create error object. + +For HTTP providers, `FileProviderHTTPError` protocol defines a way to encapsulate HTTP status code and returned description by server. +You may declare a struct conforming to `FileProviderHTTPError` with an initializer to interpret server response. +`serverError(with:, path:, data:)` must be implemented in `HTTPFileProvider` subclasses and will be called by HTTP provider default implementation to digest error. + +**NEVER** define a custom enum for errors. Instead use Foundation errors like `URLError` and `CococaError` as +they provide comprehensive localized description for error. +Alternatively use `FileProviderHTTPError` conforming struct for HTTP providers. + +## Implementing HTTP-based Custom Provider + +This library provide `HTTPFileProvider` abstract class to easily implement provider which connects to a cloud/server that +uses http protocol for connection. +That's almost all REST and web based providers like Dropbox, Box, Google Drive, etc. + +`HTTPFileProvider` encapsulates much of downloading/uploading logic and provides `paginated` method +to allow enlisting/searching files in providers which return result in progressively. (e.g. Dropbox and OneDrive) + +By subclassing `HTTPFileProvider` class, you must override half a dozen of methods, mainly querying methods. +Your implementation may cause a **crash** if you fail to override these methods. + +### Methods and properties to override + +`type` static property, which returns name of provider. + +`init?(coder:)` decodes and initialize instance using `NSCoder`. +Your implementation must read `aDecoder` correspondent keys and initialize a new object using your provider's initilizer. + +`copy(with:)` method must create a new instance and assign properties from source (`self`) to copied object. + +`contentsOfDirectory()` and `searchFiles()` methods must send listing query to server and decode json/xml response into a `FileObject`. +Providers may subclass `FileObject` and implement an initializer to encapsulate decoding logic. +If server response is paginated, use `paginated()` method. See below and inline help to find how to use it. + +`attributesOfItem()` must send file attribute fetching query to server and decode response into a `FileObject` or its descendants instance. + +`storageProperties()` does querying account/cloud quota and encapsulates it into a `VolumeObject` instance. +You don't need to subclass `VolumeObject`. +If your server does not support such functionality, simply call completion handler with `nil` as result. + +`request(for:, overwrite:, attributes:)` creates a `URLRequest` for requested operation. +It will be called by create, copy/move and remove functions. +You may set `httpMethod` and `httpBody` and header values of request regarding operation type and associated variables. + +- **Important:** NEVER forget to call `urlrequest.setValue(authentication: credential, with:)` to set provider's credential +if server uses OAuth/OAuth2 authentication method. +You may need to set other http headers according to server specifications. + +- **Important**: `copyItem(path:, toLocalURL:)` and `copyItem(localFile:, to:)` methods will call `request(for:)` method with +source/destination property set to a local file url, begins with `file://`. +You must handle these separately. +See `WebDAVProvider` [source](Sources/WebDAVFileProvider.swift) as an example. + +`serverError(with:, path:, data:)` method will digest http status code and server response data to create an error conforming to `FileProviderHTTPError` protocol. + +### Optional overridable methods + +`isReachable()` default implementation tries to fetch storage properties and will return true if result is non-nil. +You may need to override this method if server does not support `storageProperties()` or there is a more optimized way to check reachability. + +`copyItem(localFile:, to:)` and `writeContents(path:, contents:)` can be overrided if server requires upload session. +See `OneDriveProvider`'s [source](Sources/OneDriveProvider) to see how create and handle an upload session. + +### Paginated enlisting + +`paginated()` method defines an easy way to communicate to servers which list responses are paginated. +Here is method signature: + +`pageHandler` closure gets server response as `Data`, decodes it and returns `FileObject` array or an error, +and if there is another sequel page, passes token of new page (a string tha can be a id or url) to `newToken`. +If there is no more sequel page, `newToken` must be nil. + +This token will be delivered to `requestHandler` closure which returns a `URLRequest` according to token. +A nil token indicates it's the first page. +`completionHandler` is the closure which user passed to `contentsOfDirectory()` or `searchFiles()` methods. + +`pageHandler` must update `progress` by adding the number of new files enlisted to `completedUnitCount` property. +This closure may filter results according to `query` parameter, using `query.evaluate(with:)`. diff --git a/Docs/OAuth.md b/Docs/OAuth.md new file mode 100644 index 0000000..c49fd6c --- /dev/null +++ b/Docs/OAuth.md @@ -0,0 +1,83 @@ +# Authentication + +Dropbox and OneDrive are using OAuth2 authentication method. Here is sample codes to get Bearer token using [OAuthSwift](https://github.com/OAuthSwift/OAuthSwift) + +## Handling url scheme in App delegate + +Add these lines to your application delegate: + +```swift +extension AppDelegate { + func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any]) -> Bool { + if url.host == "oauth-callback" { + OAuthSwift.handle(url: url) + } + + // HANDLING OTHER PATERNS + } +} +``` + +## Dropbox + +Your client id and secret must be given by Dropbox developer portal. Bearer tokens created by this method are permanent. + +```swift +let appScheme = "YOUR_APP_SCHEME" +oauth = OAuth2Swift(consumerKey: "CLIENT_ID", + consumerSecret: "CLIENT_SECRET", + authorizeUrl: "https://www.dropbox.com/oauth2/authorize", + responseType: "token")! +oauth.authorizeURLHandler = SafariURLHandler(viewController: self, oauthSwift: oauth) +_ = oauth.authorize(withCallbackURL: URL(string: "\(appScheme)://oauth-callback/dropbox")!, + scope: "", state:"DROPBOX", + success: { credential, response, parameters in + let urlcredential = URLCredential(user: user ?? "anonymous", password: credential.oauthToken, persistence: .permanent) + // TODO: Save credential in keychain + // TODO: Create Dropbox provider using urlcredential + }, failure: { error in + print(error.localizedDescription) + } +) +``` + +## OneDrive + +Your client id must be given by Microsoft developer portal. OneDrive doesn't need client secret for native apps, but will need to refresh token every one hour. + +We must save refresh token in adition to bearer token and use it when we get `.unauthorized` 401 HTTP error in completion handlers to get a new bearer token. + +```swift +let appScheme = "YOUR_APP_SCHEME" +oauth = OAuth2Swift.init(consumerKey: "CLIENT_ID", + consumerSecret: "", + authorizeUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", + accessTokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token", + responseType: "code")! +oauth.authorizeURLHandler = SafariURLHandler(viewController: self, oauthSwift: oauth) +if let refreshToken = {SAVED_REFRESH_TOKEN} { + oauth.renewAccessToken(withRefreshToken: token, + success: { credential, response, parameters in + let urlcredential = URLCredential(user: user ?? "anonymous", password: credential.oauthToken, persistence: .permanent) + let refreshToken = credential.oauthRefreshToken + // TODO: Save refreshToken in keychain + // TODO: Save credential in keychain + // TODO: Create OneDrive provider using urlcredential + }, failure: { error in + print(error.localizedDescription) + // TODO: Clear saved refresh token and call this method again to get authorization token + }) +} else { + _ = oauth.authorize( + withCallbackURL: URL(string: "\(appScheme)://oauth-callback/onedrive")!, + scope: "offline_access User.Read Files.ReadWrite.All", state: "ONEDRIVE", + success: { credential, response, parameters in + let credential = URLCredential(user: user ?? "anonymous", password: credential.oauthToken, persistence: .permanent) + // TODO: Save refreshToken in keychain + // TODO: Save credential in keychain + // TODO: Create OneDrive provider using credential + }, failure: { error in + print(error.localizedDescription) + }) +} +``` \ No newline at end of file diff --git a/Docs/Sample-iOS.swift b/Docs/Sample-iOS.swift new file mode 100644 index 0000000..7ea3f38 --- /dev/null +++ b/Docs/Sample-iOS.swift @@ -0,0 +1,117 @@ +// +// Sample-iOS.swift +// FileProvider +// +// Created by Amir Abbas Mousavian. +// Copyright © 2018 Mousavian. Distributed under MIT license. +// + +import UIKit +import FilesProvider + +class ViewController: UIViewController, FileProviderDelegate { + + let server: URL = URL(string: "https://server-webdav.com")! + let username = "username" + let password = "password" + + var webdav: WebDAVFileProvider? + + @IBOutlet weak var uploadProgressView: UIProgressView + @IBOutlet weak var downloadProgressView: UIProgressView + + override func viewDidLoad() { + + super.viewDidLoad() + + let credential = URLCredential(user: username, password: password, persistence: .permanent) + + webdav = WebDAVFileProvider(baseURL: server, credential: credential)! + webdav?.delegate = self as FileProviderDelegate + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + } + + @IBAction func createFolder(_ sender: Any) { + webdav?.create(folder: "new folder", at: "/", completionHandler: nil) + } + + @IBAction func createFile(_ sender: Any) { + let data = "Hello world from sample.txt!".data(encoding: .utf8) + webdav?.writeContents(path: "sample.txt", content: data, atomically: true, completionHandler: nil) + } + + @IBAction func getData(_ sender: Any) { + webdav?.contents(path: "sample.txt", completionHandler: { + contents, error in + if let contents = contents { + print(String(data: contents, encoding: .utf8)) + } + }) + } + + @IBAction func remove(_ sender: Any) { + webdav?.removeItem(path: "sample.txt", completionHandler: nil) + } + + @IBAction func download(_ sender: Any) { + let localURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("fileprovider.png") + let remotePath = "fileprovider.png" + + let progress = webdav?.copyItem(path: remotePath, toLocalURL: localURL, completionHandler: nil) + downloadProgressView.observedProgress = progress + } + + @IBAction func upload(_ sender: Any) { + let localURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("fileprovider.png") + let remotePath = "/fileprovider.png" + + let progress = webdav?.copyItem(localFile: localURL, to: remotePath, completionHandler: nil) + uploadProgressView.observedProgress = progress + } + + func fileproviderSucceed(_ fileProvider: FileProviderOperations, operation: FileOperationType) { + switch operation { + case .copy(source: let source, destination: let dest): + print("\(source) copied to \(dest).") + case .remove(path: let path): + print("\(path) has been deleted.") + default: + if let destination = operation.destination { + print("\(operation.actionDescription) from \(operation.source) to \(destination) succeed.") + } else { + print("\(operation.actionDescription) on \(operation.source) succeed.") + } + } + } + + func fileproviderFailed(_ fileProvider: FileProviderOperations, operation: FileOperationType, error: Error) { + switch operation { + case .copy(source: let source, destination: let dest): + print("copying \(source) to \(dest) has been failed.") + case .remove: + print("file can't be deleted.") + default: + if let destination = operation.destination { + print("\(operation.actionDescription) from \(operation.source) to \(destination) failed.") + } else { + print("\(operation.actionDescription) on \(operation.source) failed.") + } + } + } + + func fileproviderProgress(_ fileProvider: FileProviderOperations, operation: FileOperationType, progress: Float) { + switch operation { + case .copy(source: let source, destination: let dest) where dest.hasPrefix("file://"): + print("Downloading \(source) to \((dest as NSString).lastPathComponent): \(progress * 100) completed.") + case .copy(source: let source, destination: let dest) where source.hasPrefix("file://"): + print("Uploading \((source as NSString).lastPathComponent) to \(dest): \(progress * 100) completed.") + case .copy(source: let source, destination: let dest): + print("Copy \(source) to \(dest): \(progress * 100) completed.") + default: + break + } + } +} diff --git a/fileprovider.png b/Docs/fileprovider.png similarity index 100% rename from fileprovider.png rename to Docs/fileprovider.png diff --git a/FilesProvider.podspec b/FilesProvider.podspec index 4608066..e883679 100644 --- a/FilesProvider.podspec +++ b/FilesProvider.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| # s.name = "FilesProvider" - s.version = "0.21.0" + s.version = "0.26.0" s.summary = "FileManager replacement for Local and Remote (WebDAV/FTP/Dropbox/OneDrive/SMB2) files on iOS and macOS." # This description is used to generate tags and improve search results. @@ -55,9 +55,8 @@ Pod::Spec.new do |s| # profile URL. # - s.author = { "Amir Abbas Mousavian" => "a.mosavian@gmail.com" } # Or just: s.author = "Amir Abbas Mousavian" - # s.authors = { "Amir Abbas Mousavian" => "a.mosavian@gmail.com" } + s.authors = { "Amir Abbas Mousavian" => "a.mosavian@gmail.com" } s.social_media_url = "https://twitter.com/amosavian" # ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― # @@ -66,10 +65,7 @@ Pod::Spec.new do |s| # the deployment target. You can optionally include the target after the platform. # - # s.platform = :ios - # s.platform = :ios, "8.0" - - # When using multiple platforms + s.swift_version = "5.0" s.ios.deployment_target = "8.0" s.osx.deployment_target = "10.10" # s.watchos.deployment_target = "2.0" @@ -120,9 +116,12 @@ Pod::Spec.new do |s| # # s.framework = "SomeFramework" - # s.frameworks = "SomeFramework", "AnotherFramework" + s.frameworks = "AVFoundation", "ImageIO", "CoreGraphics" + s.ios.framework = "UIKit" + s.tvos.framework = "UIKit" + s.osx.framework = "AppKit" - # s.library = "iconv" + s.library = "xml2" # s.libraries = "iconv", "xml2" diff --git a/FilesProvider.xcodeproj/project.pbxproj b/FilesProvider.xcodeproj/project.pbxproj index 74b507e..85d6bca 100644 --- a/FilesProvider.xcodeproj/project.pbxproj +++ b/FilesProvider.xcodeproj/project.pbxproj @@ -60,6 +60,7 @@ 7958155B1F478ED9003344DD /* HTTPFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 795815591F478ED9003344DD /* HTTPFileProvider.swift */; }; 7958155C1F478ED9003344DD /* HTTPFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 795815591F478ED9003344DD /* HTTPFileProvider.swift */; }; 798654331E8874BC002FA550 /* FTPHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 798654321E8874BC002FA550 /* FTPHelper.swift */; }; + 798D394020D01DA100FFB9EF /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 798D393F20D01DA000FFB9EF /* CoreGraphics.framework */; }; 799396AA1D48C02300086753 /* DropboxFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396931D48C02300086753 /* DropboxFileProvider.swift */; }; 799396AB1D48C02300086753 /* DropboxFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396931D48C02300086753 /* DropboxFileProvider.swift */; }; 799396AC1D48C02300086753 /* DropboxFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396931D48C02300086753 /* DropboxFileProvider.swift */; }; @@ -72,9 +73,6 @@ 799396B91D48C02300086753 /* SMBFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396981D48C02300086753 /* SMBFileProvider.swift */; }; 799396BA1D48C02300086753 /* SMBFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396981D48C02300086753 /* SMBFileProvider.swift */; }; 799396BB1D48C02300086753 /* SMBFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396981D48C02300086753 /* SMBFileProvider.swift */; }; - 799396BC1D48C02300086753 /* CIFSTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7993969A1D48C02300086753 /* CIFSTypes.swift */; }; - 799396BD1D48C02300086753 /* CIFSTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7993969A1D48C02300086753 /* CIFSTypes.swift */; }; - 799396BE1D48C02300086753 /* CIFSTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7993969A1D48C02300086753 /* CIFSTypes.swift */; }; 799396BF1D48C02300086753 /* SMB2DataTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7993969B1D48C02300086753 /* SMB2DataTypes.swift */; }; 799396C01D48C02300086753 /* SMB2DataTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7993969B1D48C02300086753 /* SMB2DataTypes.swift */; }; 799396C11D48C02300086753 /* SMB2DataTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7993969B1D48C02300086753 /* SMB2DataTypes.swift */; }; @@ -116,7 +114,6 @@ 79BD63AC1E2CC2C20035128C /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63AB1E2CC2C20035128C /* ImageIO.framework */; }; 79BD63B01E2CC3300035128C /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63AF1E2CC3300035128C /* libxml2.tbd */; }; 79BD63B21E2CC3350035128C /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63B11E2CC3350035128C /* ImageIO.framework */; }; - 79BD63B61E2CC3860035128C /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63B51E2CC3860035128C /* CoreFoundation.framework */; }; 79BD63B81E2CC38D0035128C /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63B71E2CC38D0035128C /* AVFoundation.framework */; }; 79BD63BA1E2CC39B0035128C /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63B91E2CC39B0035128C /* libxml2.tbd */; }; 79BD63BE1E2CC3C20035128C /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63BD1E2CC3C20035128C /* ImageIO.framework */; }; @@ -128,13 +125,28 @@ 79BD63C81E2D17880035128C /* OneDriveHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C41E2D17880035128C /* OneDriveHelper.swift */; }; 79BD63C91E2D17880035128C /* OneDriveHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C41E2D17880035128C /* OneDriveHelper.swift */; }; 79BD63CA1E2D17880035128C /* OneDriveHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C41E2D17880035128C /* OneDriveHelper.swift */; }; + 79D903541FAB647400D61D31 /* FilesProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79D903531FAB647400D61D31 /* FilesProviderTests.swift */; }; + 79D903561FAB647400D61D31 /* FilesProvider.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 799396751D48B80D00086753 /* FilesProvider.framework */; }; 79F4678B1E8B80F200C91A85 /* FTPHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 798654321E8874BC002FA550 /* FTPHelper.swift */; }; 79F4678C1E8B80F200C91A85 /* FTPHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 798654321E8874BC002FA550 /* FTPHelper.swift */; }; 79F5745B1DFDB10B00179ABF /* FileObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79F5745A1DFDB10A00179ABF /* FileObject.swift */; }; 79F5745C1DFDB10B00179ABF /* FileObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79F5745A1DFDB10A00179ABF /* FileObject.swift */; }; 79F5745D1DFDB10B00179ABF /* FileObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79F5745A1DFDB10A00179ABF /* FileObject.swift */; }; + CE27C49B219BFFA0000BE23C /* ServerTrustPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE27C498219BFFA0000BE23C /* ServerTrustPolicy.swift */; }; + CE27C49C219C041E000BE23C /* ServerTrustPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE27C498219BFFA0000BE23C /* ServerTrustPolicy.swift */; }; + CE27C49D219C041E000BE23C /* ServerTrustPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE27C498219BFFA0000BE23C /* ServerTrustPolicy.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 79D903571FAB647400D61D31 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7993965C1D48B7BF00086753 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 799396741D48B80D00086753; + remoteInfo = "FilesProvider OSX"; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ 7902C0851D61B56D00564440 /* RemoteSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteSession.swift; sourceTree = ""; }; 791950F41DE58A5400B4426E /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.tbd; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.1.sdk/usr/lib/libxml2.tbd; sourceTree = DEVELOPER_DIR; }; @@ -154,6 +166,7 @@ 794C220D1D591A4B00EC49B8 /* SMB2QueryTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMB2QueryTypes.swift; sourceTree = ""; }; 795815591F478ED9003344DD /* HTTPFileProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPFileProvider.swift; sourceTree = ""; }; 798654321E8874BC002FA550 /* FTPHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTPHelper.swift; sourceTree = ""; }; + 798D393F20D01DA000FFB9EF /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 799396671D48B7F600086753 /* FilesProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FilesProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 799396751D48B80D00086753 /* FilesProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FilesProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 799396821D48B82700086753 /* FilesProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FilesProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -192,7 +205,11 @@ 79BD63C11E2CC3D30035128C /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/System/Library/Frameworks/AVFoundation.framework; sourceTree = DEVELOPER_DIR; }; 79BD63C31E2D17880035128C /* OneDriveFileProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OneDriveFileProvider.swift; sourceTree = ""; }; 79BD63C41E2D17880035128C /* OneDriveHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OneDriveHelper.swift; sourceTree = ""; }; + 79D903511FAB647400D61D31 /* FilesProviderTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FilesProviderTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 79D903531FAB647400D61D31 /* FilesProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesProviderTests.swift; sourceTree = ""; }; + 79D903551FAB647400D61D31 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 79F5745A1DFDB10A00179ABF /* FileObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileObject.swift; sourceTree = ""; }; + CE27C498219BFFA0000BE23C /* ServerTrustPolicy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerTrustPolicy.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -201,8 +218,8 @@ buildActionMask = 2147483647; files = ( 79BD63AA1E2CC2BB0035128C /* AVFoundation.framework in Frameworks */, - 79BD63AC1E2CC2C20035128C /* ImageIO.framework in Frameworks */, 79BD63A81E2CC2940035128C /* CoreGraphics.framework in Frameworks */, + 79BD63AC1E2CC2C20035128C /* ImageIO.framework in Frameworks */, 791950F51DE58A5400B4426E /* libxml2.tbd in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -212,7 +229,7 @@ buildActionMask = 2147483647; files = ( 79BD63B81E2CC38D0035128C /* AVFoundation.framework in Frameworks */, - 79BD63B61E2CC3860035128C /* CoreFoundation.framework in Frameworks */, + 798D394020D01DA100FFB9EF /* CoreGraphics.framework in Frameworks */, 79BD63B21E2CC3350035128C /* ImageIO.framework in Frameworks */, 79BD63B01E2CC3300035128C /* libxml2.tbd in Frameworks */, ); @@ -229,12 +246,21 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 79D9034E1FAB647400D61D31 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 79D903561FAB647400D61D31 /* FilesProvider.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 791950F31DE58A5300B4426E /* Frameworks */ = { isa = PBXGroup; children = ( + 798D393F20D01DA000FFB9EF /* CoreGraphics.framework */, 79BD63C11E2CC3D30035128C /* AVFoundation.framework */, 79BD63BF1E2CC3CD0035128C /* CoreGraphics.framework */, 79BD63BD1E2CC3C20035128C /* ImageIO.framework */, @@ -278,6 +304,7 @@ 79E34A101E2AC6C600E1293B /* Extra */, 799396911D48C02300086753 /* Sources */, 7993968A1D48B8C700086753 /* Pod */, + 79D903521FAB647400D61D31 /* Tests */, 799396681D48B7F600086753 /* Products */, 791950F31DE58A5300B4426E /* Frameworks */, ); @@ -289,6 +316,7 @@ 799396671D48B7F600086753 /* FilesProvider.framework */, 799396751D48B80D00086753 /* FilesProvider.framework */, 799396821D48B82700086753 /* FilesProvider.framework */, + 79D903511FAB647400D61D31 /* FilesProviderTests.xctest */, ); name = Products; sourceTree = ""; @@ -328,6 +356,7 @@ 798654321E8874BC002FA550 /* FTPHelper.swift */, 799396971D48C02300086753 /* SMBClient.swift */, 799396981D48C02300086753 /* SMBFileProvider.swift */, + CE27C498219BFFA0000BE23C /* ServerTrustPolicy.swift */, ); path = Sources; sourceTree = ""; @@ -352,6 +381,15 @@ path = SMBTypes; sourceTree = ""; }; + 79D903521FAB647400D61D31 /* Tests */ = { + isa = PBXGroup; + children = ( + 79D903531FAB647400D61D31 /* FilesProviderTests.swift */, + 79D903551FAB647400D61D31 /* Info.plist */, + ); + path = Tests; + sourceTree = ""; + }; 79E34A101E2AC6C600E1293B /* Extra */ = { isa = PBXGroup; children = ( @@ -440,13 +478,32 @@ productReference = 799396821D48B82700086753 /* FilesProvider.framework */; productType = "com.apple.product-type.framework"; }; + 79D903501FAB647400D61D31 /* FilesProviderTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 79D9035B1FAB647400D61D31 /* Build configuration list for PBXNativeTarget "FilesProviderTests" */; + buildPhases = ( + 79D9034D1FAB647400D61D31 /* Sources */, + 79D9034E1FAB647400D61D31 /* Frameworks */, + 79D9034F1FAB647400D61D31 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 79D903581FAB647400D61D31 /* PBXTargetDependency */, + ); + name = FilesProviderTests; + productName = FilesProviderTests; + productReference = 79D903511FAB647400D61D31 /* FilesProviderTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 7993965C1D48B7BF00086753 /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0900; + LastSwiftUpdateCheck = 0910; + LastUpgradeCheck = 0930; TargetAttributes = { 799396661D48B7F600086753 = { CreatedOnToolsVersion = 7.3.1; @@ -454,18 +511,25 @@ }; 799396741D48B80D00086753 = { CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1020; }; 799396811D48B82700086753 = { CreatedOnToolsVersion = 7.3.1; }; + 79D903501FAB647400D61D31 = { + CreatedOnToolsVersion = 9.1; + LastSwiftMigration = 1020; + ProvisioningStyle = Automatic; + }; }; }; buildConfigurationList = 7993965F1D48B7BF00086753 /* Build configuration list for PBXProject "FilesProvider" */; compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, + Base, ); mainGroup = 7993965B1D48B7BF00086753; productRefGroup = 799396681D48B7F600086753 /* Products */; @@ -475,6 +539,7 @@ 799396661D48B7F600086753 /* FilesProvider iOS */, 799396741D48B80D00086753 /* FilesProvider OSX */, 799396811D48B82700086753 /* FilesProvider tvOS */, + 79D903501FAB647400D61D31 /* FilesProviderTests */, ); }; /* End PBXProject section */ @@ -501,6 +566,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 79D9034F1FAB647400D61D31 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -511,6 +583,7 @@ 799396B31D48C02300086753 /* LocalFileProvider.swift in Sources */, 799396D41D48C02300086753 /* SMB2Tree.swift in Sources */, 7924B1A21D89DAE000589DB7 /* Options.swift in Sources */, + CE27C49D219C041E000BE23C /* ServerTrustPolicy.swift in Sources */, 792572411DF23BDA006A1526 /* LocalHelper.swift in Sources */, 7936BC121E880F5700A6C81C /* FTPFileProvider.swift in Sources */, 79480FF61E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */, @@ -528,7 +601,6 @@ 799396B91D48C02300086753 /* SMBFileProvider.swift in Sources */, 794C220E1D591A4B00EC49B8 /* SMB2QueryTypes.swift in Sources */, 794C21FE1D58912A00EC49B8 /* DropboxHelper.swift in Sources */, - 799396BC1D48C02300086753 /* CIFSTypes.swift in Sources */, 794C220A1D5893F800EC49B8 /* SMB2Notification.swift in Sources */, 799396D11D48C02300086753 /* SMB2SetInfo.swift in Sources */, 7924B1961D89DAE000589DB7 /* Document.swift in Sources */, @@ -556,6 +628,7 @@ 799396B41D48C02300086753 /* LocalFileProvider.swift in Sources */, 799396D51D48C02300086753 /* SMB2Tree.swift in Sources */, 7924B1A31D89DAE000589DB7 /* Options.swift in Sources */, + CE27C49C219C041E000BE23C /* ServerTrustPolicy.swift in Sources */, 792572421DF23BDA006A1526 /* LocalHelper.swift in Sources */, 7936BC131E880F5700A6C81C /* FTPFileProvider.swift in Sources */, 79480FF71E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */, @@ -573,7 +646,6 @@ 799396BA1D48C02300086753 /* SMBFileProvider.swift in Sources */, 794C220F1D591A4B00EC49B8 /* SMB2QueryTypes.swift in Sources */, 794C21FF1D58912A00EC49B8 /* DropboxHelper.swift in Sources */, - 799396BD1D48C02300086753 /* CIFSTypes.swift in Sources */, 794C220B1D5893F800EC49B8 /* SMB2Notification.swift in Sources */, 799396D21D48C02300086753 /* SMB2SetInfo.swift in Sources */, 7924B1971D89DAE000589DB7 /* Document.swift in Sources */, @@ -601,6 +673,7 @@ 799396B51D48C02300086753 /* LocalFileProvider.swift in Sources */, 799396D61D48C02300086753 /* SMB2Tree.swift in Sources */, 7924B1A41D89DAE000589DB7 /* Options.swift in Sources */, + CE27C49B219BFFA0000BE23C /* ServerTrustPolicy.swift in Sources */, 792572431DF23BDA006A1526 /* LocalHelper.swift in Sources */, 7936BC141E880F5700A6C81C /* FTPFileProvider.swift in Sources */, 79480FF81E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */, @@ -618,7 +691,6 @@ 799396BB1D48C02300086753 /* SMBFileProvider.swift in Sources */, 794C22101D591A4B00EC49B8 /* SMB2QueryTypes.swift in Sources */, 794C22001D58912A00EC49B8 /* DropboxHelper.swift in Sources */, - 799396BE1D48C02300086753 /* CIFSTypes.swift in Sources */, 794C220C1D5893F800EC49B8 /* SMB2Notification.swift in Sources */, 799396D31D48C02300086753 /* SMB2SetInfo.swift in Sources */, 7924B1981D89DAE000589DB7 /* Document.swift in Sources */, @@ -639,22 +711,40 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 79D9034D1FAB647400D61D31 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 79D903541FAB647400D61D31 /* FilesProviderTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 79D903581FAB647400D61D31 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 799396741D48B80D00086753 /* FilesProvider OSX */; + targetProxy = 79D903571FAB647400D61D31 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ 799396601D48B7BF00086753 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - BUNDLE_VERSION_STRING = 0.21.0; + BUNDLE_VERSION_STRING = 0.25.1; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; @@ -663,36 +753,38 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_BITCODE = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; ONLY_ACTIVE_ARCH = YES; PRODUCT_NAME = FilesProvider; - SWIFT_VERSION = 3.0; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.0; }; name = Debug; }; 799396611D48B7BF00086753 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - BUNDLE_VERSION_STRING = 0.21.0; + BUNDLE_VERSION_STRING = 0.25.1; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; @@ -700,18 +792,17 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_BITCODE = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; PRODUCT_NAME = FilesProvider; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 4.0; }; name = Release; }; @@ -723,15 +814,6 @@ CLANG_ANALYZER_NONNULL = YES; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; @@ -745,12 +827,6 @@ "DEBUG=1", "$(inherited)", ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = "Pod/Info-iOS.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; @@ -759,8 +835,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FilesProvider-iOS"; SDKROOT = iphoneos; SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -775,15 +850,6 @@ CLANG_ANALYZER_NONNULL = YES; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; @@ -792,12 +858,6 @@ ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = "Pod/Info-iOS.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; @@ -806,7 +866,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FilesProvider-iOS"; SDKROOT = iphoneos; SKIP_INSTALL = YES; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -824,22 +884,12 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; FRAMEWORK_VERSION = A; @@ -849,12 +899,6 @@ "DEBUG=1", "$(inherited)", ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = "Pod/Info-MacOS.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; @@ -863,7 +907,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FilesProvider-OSX"; SDKROOT = macosx; SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; @@ -879,32 +923,16 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_BITCODE = NO; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; FRAMEWORK_VERSION = A; GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = "Pod/Info-MacOS.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; @@ -913,6 +941,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FilesProvider-OSX"; SDKROOT = macosx; SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; @@ -928,15 +957,6 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; @@ -951,12 +971,6 @@ "DEBUG=1", "$(inherited)", ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = "Pod/Info-tvOS.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; @@ -964,7 +978,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FilesProvider-tvOS"; SDKROOT = appletvos; SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = 3; TVOS_DEPLOYMENT_TARGET = 10.0; VERSIONING_SYSTEM = "apple-generic"; @@ -982,15 +996,6 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; @@ -1000,12 +1005,6 @@ ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = "Pod/Info-tvOS.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; @@ -1013,6 +1012,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FilesProvider-tvOS"; SDKROOT = appletvos; SKIP_INSTALL = YES; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = 3; TVOS_DEPLOYMENT_TARGET = 10.0; VALIDATE_PRODUCT = YES; @@ -1021,6 +1021,76 @@ }; name = Release; }; + 79D903591FAB647400D61D31 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + INFOPLIST_FILE = Tests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.13; + MTL_ENABLE_DEBUG_INFO = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.mousavian.FilesProviderTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 79D9035A1FAB647400D61D31 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + ENABLE_NS_ASSERTIONS = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + INFOPLIST_FILE = Tests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.13; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = com.mousavian.FilesProviderTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -1060,6 +1130,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 79D9035B1FAB647400D61D31 /* Build configuration list for PBXNativeTarget "FilesProviderTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 79D903591FAB647400D61D31 /* Debug */, + 79D9035A1FAB647400D61D31 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 7993965C1D48B7BF00086753 /* Project object */; diff --git a/FilesProvider.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/FilesProvider.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/FilesProvider.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/FilesProvider.xcodeproj/xcshareddata/xcschemes/FilesProvider OSX.xcscheme b/FilesProvider.xcodeproj/xcshareddata/xcschemes/FilesProvider OSX.xcscheme index 8c547a5..d257538 100644 --- a/FilesProvider.xcodeproj/xcshareddata/xcschemes/FilesProvider OSX.xcscheme +++ b/FilesProvider.xcodeproj/xcshareddata/xcschemes/FilesProvider OSX.xcscheme @@ -1,6 +1,6 @@ + + + + + + + + @@ -37,7 +55,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/FilesProvider.xcodeproj/xcshareddata/xcschemes/FilesProvider iOS.xcscheme b/FilesProvider.xcodeproj/xcshareddata/xcschemes/FilesProvider iOS.xcscheme index f3e23bc..94148c0 100644 --- a/FilesProvider.xcodeproj/xcshareddata/xcschemes/FilesProvider iOS.xcscheme +++ b/FilesProvider.xcodeproj/xcshareddata/xcschemes/FilesProvider iOS.xcscheme @@ -1,6 +1,6 @@ @@ -37,7 +36,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/FilesProvider.xcodeproj/xcshareddata/xcschemes/FilesProvider tvOS.xcscheme b/FilesProvider.xcodeproj/xcshareddata/xcschemes/FilesProvider tvOS.xcscheme index d8cc7bb..f8aae99 100644 --- a/FilesProvider.xcodeproj/xcshareddata/xcschemes/FilesProvider tvOS.xcscheme +++ b/FilesProvider.xcodeproj/xcshareddata/xcschemes/FilesProvider tvOS.xcscheme @@ -1,6 +1,6 @@ @@ -37,7 +36,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/Package.swift b/Package.swift index ff5a542..ac21321 100644 --- a/Package.swift +++ b/Package.swift @@ -1,12 +1,26 @@ +// swift-tools-version:5.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + import PackageDescription let package = Package( name: "FilesProvider", products: [ - .library(name: "FilesProvider", targets: ["FilesProvider"]) + // Products define the executables and libraries produced by a package, and make them visible to other packages. + .library( + name: "FilesProvider", + targets: ["FilesProvider"] + ) ], dependencies: [], targets: [ - .target(name: "FilesProvider", path: "Sources") + .target(name: "FilesProvider", + dependencies: [], + path: "Sources" + ), + .testTarget(name: "FilesProviderTests", + dependencies: ["FilesProvider"], + path: "Tests" + ), ] ) diff --git a/README.md b/README.md index eab2219..b4b50d5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![File Provider](fileprovider.png) +![File Provider](Docs/fileprovider.png) > This Swift library provide a swifty way to deal with local and remote files and directories in a unified way. @@ -36,12 +36,11 @@ All functions do async calls and it wont block your main thread. - [x] **DropboxFileProvider** A wrapper around Dropbox Web API. * For now it has limitation in uploading files up to 150MB. - [x] **OneDriveFileProvider** A wrapper around OneDrive REST API, works with `onedrive.com` and compatible (business) servers. - * For now it has limitation in uploading files up to 100MB. - [ ] **AmazonS3FileProvider** Amazon storage backend. Used by many sites. - [ ] **GoogleFileProvider** A wrapper around Goodle Drive REST API. - [ ] **SMBFileProvider** SMB2/3 introduced in 2006, which is a file and printer sharing protocol originated from Microsoft Windows and now is replacing AFP protocol on macOS. * Data types and some basic functions are implemented but *main interface is not implemented yet!*. - * SMBv1/CIFS is insecure, deprecated and kinda tricky to be implemented due to strict memory allignment in Swift. + * For now, you can use [amosavian/AMSMB2](https://github.com/amosavian/AMSMB2) framework to connect to SMB2. ## Requirements @@ -49,7 +48,7 @@ All functions do async calls and it wont block your main thread. - iOS 8.0 , OSX 10.10 - XCode 9.0 -Legacy version is available in swift-2 branch. +Legacy version is available in swift-3 branch. ## Installation @@ -101,13 +100,19 @@ Then you can do either: * Drop `FilesProvider.xcodeproj` to you Xcode workspace and add the framework to your Embeded Binaries in target. +## Design + +To find design concepts and how to implement a custom provider, read [Concepts and Design document](Docs/DESIGN.md). + ## Usage -Each provider has a specific class which conforms to FileProvider protocol and share same syntax +Each provider has a specific class which conforms to FileProvider protocol and share same syntax. + +Find a [sample code for iOS here](Docs/Sample-iOS.swift). ### Initialization -For LocalFileProvider if you want to deal with `Documents` folder +For LocalFileProvider if you want to deal with `Documents` folder: ``` swift import FilesProvider @@ -153,11 +158,11 @@ let webdavProvider = WebDAVFileProvider(baseURL: URL(string: "http://www.example * In case you want to connect non-secure servers for WebDAV (http) or FTP in iOS 9+ / macOS 10.11+ you should disable App Transport Security (ATS) according to [this guide.](https://gist.github.com/mlynch/284699d676fe9ed0abfa) -* For Dropbox & OneDrive, user is clientID and password is Token which both must be retrieved via [OAuth2 API of Dropbox](https://www.dropbox.com/developers/reference/oauth-guide). There are libraries like [p2/OAuth2](https://github.com/p2/OAuth2) or [OAuthSwift](https://github.com/OAuthSwift/OAuthSwift) which can facilate the procedure to retrieve token. The latter is easier to use and prefered. +* For Dropbox & OneDrive, user is clientID and password is Token which both must be retrieved via OAuth2 API o. There are libraries like [p2/OAuth2](https://github.com/p2/OAuth2) or [OAuthSwift](https://github.com/OAuthSwift/OAuthSwift) which can facilate the procedure to retrieve token. The latter is easier to use and prefered. Please see [OAuth example for Dropbox and OneDrive](Docs/OAuth.md) for detailed instruction. -For interaction with UI, set delegate variable of `FileProvider` object +For interaction with UI, set delegate property of `FileProvider` object. -You can use `url(of:)` method if provider to get direct access url (local or remote files) for some file systems which allows to do so (Dropbox doesn't support and returns path simply wrapped in URL) +You can use `url(of:)` method if provider to get direct access url (local or remote files) for some file systems which allows to do so (Dropbox doesn't support and returns path simply wrapped in URL). ### Delegates @@ -466,7 +471,7 @@ We would love for you to contribute to **FileProvider**, check the `LICENSE` fil Things you may consider to help us: - [ ] Implement request/response stack for `SMBClient` -- [ ] Implement Test-case (`XCTest`) +- [x] Implement Test-case (`XCTest`) - [ ] Add Sample project for iOS - [ ] Add Sample project for macOS @@ -506,4 +511,4 @@ Distributed under the MIT license. See `LICENSE` for more information. [cocoapods-downloads]: https://img.shields.io/cocoapods/dt/FilesProvider.svg [cocoapods-apps]: https://img.shields.io/cocoapods/at/FilesProvider.svg [docs-image]: https://img.shields.io/cocoapods/metrics/doc-percent/FilesProvider.svg -[docs-url]: http://cocoadocs.org/docsets/FilesProvider/ \ No newline at end of file +[docs-url]: http://cocoadocs.org/docsets/FilesProvider/ diff --git a/Sources/AEXML/Document.swift b/Sources/AEXML/Document.swift index 480eaea..ccadb14 100644 --- a/Sources/AEXML/Document.swift +++ b/Sources/AEXML/Document.swift @@ -43,7 +43,7 @@ internal class AEXMLDocument: AEXMLElement { return rootElement } - open let options: AEXMLOptions + public let options: AEXMLOptions // MARK: - Lifecycle diff --git a/Sources/AEXML/Element.swift b/Sources/AEXML/Element.swift index f5f61a2..d3e6a86 100644 --- a/Sources/AEXML/Element.swift +++ b/Sources/AEXML/Element.swift @@ -192,7 +192,7 @@ internal class AEXMLElement { } fileprivate func removeChild(_ child: AEXMLElement) { - if let childIndex = children.index(where: { $0 === child }) { + if let childIndex = children.firstIndex(where: { $0 === child }) { children.remove(at: childIndex) } } @@ -266,7 +266,7 @@ internal class AEXMLElement { } -public extension String { +extension String { /// String representation of self with XML special characters escaped. public var xmlEscaped: String { diff --git a/Sources/CloudFileProvider.swift b/Sources/CloudFileProvider.swift index 086f19a..1c1ce80 100644 --- a/Sources/CloudFileProvider.swift +++ b/Sources/CloudFileProvider.swift @@ -37,7 +37,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { open fileprivate(set) var scope: UbiquitousScope /// Set this property to ignore initiations asserting to be on secondary thread - static open var asserting: Bool = true + static public var asserting: Bool = true /** Initializes the provider for the iCloud container associated with the specified identifier and @@ -48,7 +48,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { - Parameter containerId: The fully-qualified container identifier for an iCloud container directory. The string you specify must not contain wildcards and must be of the form `.`, where `` is your development team ID and `` is the bundle identifier of the container you want to access.\ The container identifiers for your app must be declared in the `com.apple.developer.ubiquity-container-identifiers` array of the `.entitlements` property list file in your Xcode project.\ If you specify nil for this parameter, this method uses the first container listed in the `com.apple.developer.ubiquity-container-identifiers` entitlement array. - - Parameter scope: Use `.documents` (default) to put documents that the user is allowed to access inside a Documents subdirectory. Otherwise use `.data` to store user-related data files that your app needs to share but that are not files you want the user to manipulate directly. + - Parameter scope: Use `.documents` (default) to put documents that the user is allowed to access inside a `Documents` subdirectory. Otherwise use `.data` to store user-related data files that your app needs to share but that are not files you want the user to manipulate directly. */ public convenience init? (containerId: String?, scope: UbiquitousScope = .documents) { assert(!(CloudFileProvider.asserting && Thread.isMainThread), "CloudFileProvider.init(containerId:) is not recommended to be executed on Main Thread.") @@ -82,22 +82,18 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { super.init(baseURL: baseURL) self.isCoorinating = true - #if swift(>=3.1) - let queueLabel = "FileProvider.\(Swift.type(of: self).type)" - #else - let queueLabel = "FileProvider.\(type(of: self).type)" - #endif + let queueLabel = "FileProvider.\(Swift.type(of: self).type)" dispatch_queue = DispatchQueue(label: queueLabel, attributes: .concurrent) operation_queue = OperationQueue() operation_queue.name = "\(queueLabel).Operation" } public required convenience init?(coder aDecoder: NSCoder) { - if let containerId = aDecoder.decodeObject(forKey: "containerId") as? String, - let scopeString = aDecoder.decodeObject(forKey: "scope") as? String, + if let containerId = aDecoder.decodeObject(of: NSString.self, forKey: "containerId") as String?, + let scopeString = aDecoder.decodeObject(of: NSString.self, forKey: "scope") as String?, let scope = UbiquitousScope(rawValue: scopeString) { self.init(containerId: containerId, scope: scope) - } else if let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL { + } else if let baseURL = aDecoder.decodeObject(of: NSURL.self, forKey: "baseURL") as URL? { self.init(baseURL: baseURL) } else { return nil @@ -106,6 +102,14 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { self.isCoorinating = aDecoder.decodeBool(forKey: "isCoorinating") } + deinit { + let monitors = self.monitors + self.monitors = [:] + for monitor in monitors { + self.unregisterNotifcation(path: monitor.key) + } + } + open override func encode(with aCoder: NSCoder) { super.encode(with: aCoder) aCoder.encode(self.containerId, forKey: "containerId") @@ -133,7 +137,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { open override func contentsOfDirectory(path: String, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) { // FIXME: create runloop for dispatch_queue, start query on it let query = NSPredicate(format: "TRUEPREDICATE") - _ = searchFiles(path: path, recursive: false, query: query, foundItemHandler: nil, completionHandler: completionHandler) + _ = searchFiles(path: path, recursive: false, query: query, completionHandler: completionHandler) } /// Please don't rely this function to get iCloud drive total and remaining capacity @@ -154,48 +158,10 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { - error: Error returned by system. */ open override func attributesOfItem(path: String, completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) { - dispatch_queue.async { - let pathURL = self.url(of: path) - let query = NSMetadataQuery() - query.predicate = NSPredicate(format: "%K LIKE[CD] %@", NSMetadataItemPathKey, pathURL.path) - query.valueListAttributes = [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey] - query.searchScopes = [self.scope.rawValue] - var finishObserver: NSObjectProtocol? - finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in - defer { - query.stop() - NotificationCenter.default.removeObserver(finishObserver!) - } - - query.disableUpdates() - - guard let result = (query.results as? [NSMetadataItem])?.first, let attribs = result.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else { - let error = self.cocoaError(path, code: .fileNoSuchFile) - self.dispatch_queue.async { - completionHandler(nil, error) - } - return - } - - if let file = self.mapFileObject(attributes: attribs) { - self.dispatch_queue.async { - completionHandler(file, nil) - } - } else { - let noFileError = self.cocoaError(path, code: .fileNoSuchFile) - self.dispatch_queue.async { - completionHandler(nil, noFileError) - } - } - }) - DispatchQueue.main.async { - if !query.start() { - self.dispatch_queue.async { - completionHandler(nil, self.cocoaError(path, code: .fileReadNoPermission)) - } - } - } - } + let query = NSPredicate(format: "%K LIKE[CD] %@", NSMetadataItemPathKey, path) + _ = searchFiles(path: path, recursive: false, query: query, completionHandler: { (files, error) in + completionHandler(files.first, error) + }) } /** @@ -222,6 +188,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { - error: `Error` returned by server if occured. - Returns: An `Progress` to get progress or cancel progress. Use `completedUnitCount` to iterate count of found items. */ + @discardableResult open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? { let progress = Progress(totalUnitCount: -1) @@ -304,7 +271,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { progress.setUserInfoObject(Date(), forKey: .startingTimeKey) if !mdquery.start() { self.dispatch_queue.async { - completionHandler([], self.cocoaError(path, code: .fileReadNoPermission)) + completionHandler([], CocoaError(.fileReadNoPermission, path: path)) } } } @@ -312,9 +279,9 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { return progress } - open override func isReachable(completionHandler: @escaping (Bool) -> Void) { + open override func isReachable(completionHandler: @escaping (_ success: Bool, _ error: Error?) -> Void) { dispatch_queue.async { - completionHandler(self.fileManager.ubiquityIdentityToken != nil) + completionHandler(self.fileManager.ubiquityIdentityToken != nil, nil) } } @@ -356,8 +323,8 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { progress.isCancellable = false progress.setUserInfoObject(localFile, forKey: .fileURLKey) progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) - monitorFile(path: toPath, operation: operation, progress: progress) - operation_queue.addOperation { + + let moveblock: () -> Void = { let tempFolder: URL if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) { tempFolder = FileManager.default.temporaryDirectory @@ -371,17 +338,37 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { try self.opFileManager.copyItem(at: localFile, to: tmpFile) let toUrl = self.url(of: toPath) try self.opFileManager.setUbiquitous(true, itemAt: tmpFile, destinationURL: toUrl) - self.monitorFile(path: toPath, operation: operation, progress: progress) + self.monitorTransmissionProgress(path: toPath, operation: operation, progress: progress) completionHandler?(nil) self.delegateNotify(operation) } catch { - if self.opFileManager.fileExists(atPath: tmpFile.path) { + if tmpFile.fileExists { try? self.opFileManager.removeItem(at: tmpFile) } completionHandler?(error) self.delegateNotify(operation, error: error) } } + + let dest = self.url(of: toPath) + if /* fileExists */ ((try? dest.checkResourceIsReachable()) ?? false) || + ((try? dest.checkPromisedItemIsReachable()) ?? false) { + if overwrite { + self.removeItem(path: toPath, completionHandler: { _ in + self.operation_queue.addOperation(moveblock) + }) + } else { + let e = CocoaError(.fileWriteFileExists, path: dest.path) + dispatch_queue.async { + completionHandler?(e) + } + self.delegateNotify(operation, error: e) + return nil + } + } else { + self.operation_queue.addOperation(moveblock) + } + return progress } @@ -399,7 +386,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { open override func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? { let operation = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString) let progress = super.copyItem(path: path, toLocalURL: toLocalURL, completionHandler: completionHandler) - monitorFile(path: path, operation: operation, progress: progress) + monitorTransmissionProgress(path: path, operation: operation, progress: progress) do { try self.opFileManager.startDownloadingUbiquitousItem(at: self.url(of: path)) } catch { @@ -425,7 +412,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { open override func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? { let operation = FileOperationType.fetch(path: path) let progress = super.contents(path: path, completionHandler: completionHandler) - monitorFile(path: path, operation: operation, progress: progress) + monitorTransmissionProgress(path: path, operation: operation, progress: progress) return progress } @@ -446,7 +433,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { open override func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? { let operation = FileOperationType.fetch(path: path) let progress = super.contents(path: path, offset: offset, length: length, completionHandler: completionHandler) - monitorFile(path: path, operation: operation, progress: progress) + monitorTransmissionProgress(path: path, operation: operation, progress: progress) return progress } @@ -469,7 +456,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { progress.kind = .file progress.setUserInfoObject(self.url(of: path), forKey: .fileURLKey) progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) - monitorFile(path: path, operation: operation, progress: progress) + monitorTransmissionProgress(path: path, operation: operation, progress: progress) _ = super.writeContents(path: path, contents: data, atomically: atomically, overwrite: overwrite, completionHandler: completionHandler) return progress } @@ -536,6 +523,40 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { return monitors[path] != nil } + open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void)) { + operation_queue.addOperation { + do { + var expiration: NSDate? + let url = try self.opFileManager.url(forPublishingUbiquitousItemAt: self.url(of: path), expiration: &expiration) + self.dispatch_queue.async { + completionHandler(url, nil, expiration as Date?, nil) + } + } catch { + self.dispatch_queue.async { + completionHandler(nil, nil, nil, error) + } + } + } + } + + /** + Removes local copy of file, but spares cloud copy. + - Parameter path: Path of file or directory to be removed from local. + - Parameter completionHandler: If an error parameter was provided, a presentable `Error` will be returned. + */ + open func evictItem(path: String, completionHandler: SimpleCompletionHandler) { + operation_queue.addOperation { + do { + try self.opFileManager.evictUbiquitousItem(at: self.url(of: path)) + completionHandler?(nil) + } catch { + completionHandler?(error) + } + } + } +} + +extension CloudFileProvider { fileprivate func updateQueryTypeKeys(_ queryComponent: NSPredicate) -> NSPredicate { let mapDict: [String: String] = ["url": NSMetadataItemURLKey, "name": NSMetadataItemFSNameKey, "path": NSMetadataItemPathKey, "filesize": NSMetadataItemFSSizeKey, "modifiedDate": NSMetadataItemFSContentChangeDateKey, "creationDate": NSMetadataItemFSCreationDateKey, "contentType": NSMetadataItemContentTypeKey] @@ -545,6 +566,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { case .and: return NSCompoundPredicate(andPredicateWithSubpredicates: newSub) case .not: return NSCompoundPredicate(notPredicateWithSubpredicate: newSub.first!) case .or: return NSCompoundPredicate(orPredicateWithSubpredicates: newSub) + @unknown default: fatalError() } } else if let cQuery = queryComponent as? NSComparisonPredicate { var newLeft = cQuery.leftExpression @@ -566,7 +588,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { return queryComponent } } - + fileprivate func mapFileObject(attributes attribs: [String: Any]) -> FileObject? { guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardizedFileURL, let name = attribs[NSMetadataItemFSNameKey] as? String else { @@ -574,11 +596,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { } let path = self.relativePathOf(url: url) - #if swift(>=4.0) let rpath = path.hasPrefix("/") ? String(path[path.index(after: path.startIndex)...]) : path - #else - let rpath = path.hasPrefix("/") ? path.substring(from: path.index(after: path.startIndex)) : path - #endif let relativeUrl = URL(string: rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? rpath, relativeTo: self.baseURL) let file = FileObject(url: relativeUrl ?? url, name: name, path: path) @@ -592,16 +610,84 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { return file } - lazy fileprivate var observer: KVOObserver = KVOObserver() - - fileprivate func monitorFile(path: String, operation: FileOperationType, progress: Progress?) { + fileprivate func monitorTransmissionProgress(path: String, operation: FileOperationType, progress: Progress?) { + var isDownloadingOperation: Bool + let isUploadingOperation: Bool + switch operation { + case .copy(_, destination: let dest) where dest.hasPrefix("file://"), .move(_, destination: let dest) where dest.hasPrefix("file://"): + fallthrough + case .fetch: + isDownloadingOperation = true + isUploadingOperation = false + case .copy(source: let source, _) where source.hasPrefix("file://"), .move(source: let source, _) where source.hasPrefix("file://"): + fallthrough + case .modify, .create: + isDownloadingOperation = false + isUploadingOperation = true + default: + return + } + let pathURL = self.url(of: path).standardizedFileURL let query = NSMetadataQuery() - query.predicate = NSPredicate(format: "%K LIKE[CD] %@", NSMetadataItemPathKey, pathURL.path) - query.valueListAttributes = [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataUbiquitousItemPercentDownloadedKey, NSMetadataUbiquitousItemPercentUploadedKey, NSMetadataUbiquitousItemDownloadingStatusKey, NSMetadataItemFSSizeKey] + query.predicate = NSPredicate(format: "(%K LIKE[CD] %@)", NSMetadataItemPathKey, pathURL.path) + query.valueListAttributes = [NSMetadataUbiquitousItemPercentDownloadedKey, + NSMetadataUbiquitousItemPercentUploadedKey, + NSMetadataUbiquitousItemIsUploadedKey, + NSMetadataUbiquitousItemDownloadingStatusKey, + NSMetadataItemFSSizeKey] query.searchScopes = [self.scope.rawValue] - var context = QueryProgressWrapper(provider: self, progress: progress, operation: operation) - query.addObserver(self.observer, forKeyPath: "results", options: [.initial, .new, .old], context: &context) + + var observer: NSObjectProtocol? + observer = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidUpdate, object: query, queue: .main) { [weak self] (notification) in + guard let items = notification.userInfo?[NSMetadataQueryUpdateChangedItemsKey] as? NSArray, + let item = items.firstObject as? NSMetadataItem else { + return + } + + func terminateAndRemoveObserver() { + guard observer != nil else { return } + query.stop() + observer.flatMap(NotificationCenter.default.removeObserver) + observer = nil + } + + func updateProgress(_ percent: NSNumber) { + let fraction = percent.doubleValue / 100 + self?.delegateNotify(operation, progress: fraction) + if let progress = progress { + if progress.totalUnitCount < 1, let size = item.value(forAttribute: NSMetadataItemFSSizeKey) as? NSNumber { + progress.totalUnitCount = size.int64Value + } + progress.completedUnitCount = progress.totalUnitCount > 0 ? Int64(Double(progress.totalUnitCount) * fraction) : 0 + } + if percent.doubleValue == 100.0 { + terminateAndRemoveObserver() + } + } + + for attrName in item.attributes { + switch attrName { + case NSMetadataUbiquitousItemPercentDownloadedKey: + guard isDownloadingOperation, let percent = item.value(forAttribute: attrName) as? NSNumber else { break } + updateProgress(percent) + case NSMetadataUbiquitousItemPercentUploadedKey: + guard isUploadingOperation, let percent = item.value(forAttribute: attrName) as? NSNumber else { break } + updateProgress(percent) + case NSMetadataUbiquitousItemDownloadingStatusKey: + if isDownloadingOperation, let value = item.value(forAttribute: attrName) as? String, + value == NSMetadataUbiquitousItemDownloadingStatusDownloaded { + terminateAndRemoveObserver() + } + case NSMetadataUbiquitousItemIsUploadedKey: + if isUploadingOperation, let value = item.value(forAttribute: attrName) as? NSNumber, value.boolValue { + terminateAndRemoveObserver() + } + default: + break + } + } + } DispatchQueue.main.async { query.start() @@ -609,64 +695,57 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing { } } - open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void)) { - operation_queue.addOperation { - do { - var expiration: NSDate? - let url = try self.opFileManager.url(forPublishingUbiquitousItemAt: self.url(of: path), expiration: &expiration) - self.dispatch_queue.async { - completionHandler(url, nil, expiration as Date?, nil) - } - } catch { - self.dispatch_queue.async { - completionHandler(nil, nil, nil, error) - } + fileprivate func metadataItem(for url: URL) -> NSMetadataItem? { + assert(!Thread.isMainThread, "CloudFileProvider.metadataItem(for:) is not recommended to be executed on Main Thread.") + let query = NSMetadataQuery() + query.predicate = NSPredicate(format: "(%K LIKE %@)", NSMetadataItemPathKey, url.path) + query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope, NSMetadataQueryUbiquitousDataScope] + + var item: NSMetadataItem? + + let group = DispatchGroup() + group.enter() + var finishObserver: NSObjectProtocol? + finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in + defer { + query.stop() + group.leave() + NotificationCenter.default.removeObserver(finishObserver!) } - } - } - - /// Removes local copy of file, but spares cloud copy. - /// - Parameter path: Path of file or directory to be removed from local - /// - Parameter completionHandler: If an error parameter was provided, a presentable `Error` will be returned. - open func evictItem(path: String, completionHandler: SimpleCompletionHandler) { - operation_queue.addOperation { - do { - try self.opFileManager.evictUbiquitousItem(at: self.url(of: path)) - completionHandler?(nil) - } catch { - completionHandler?(error) + + if query.resultCount > 0 { + item = query.result(at: 0) as? NSMetadataItem } + + query.disableUpdates() + + }) + + DispatchQueue.main.async { + query.start() } - } - - /// Returns current version of file on this device and all versions of files in user devices. - /// - Parameter path: Path of file or directory. - /// - Parameter completionHandler: Retrieve current version on this device and all versions available. `currentVersion` will be nil if file doesn't exist. If an error parameter was provided, a presentable `Error` will be returned. - func versionsOfItem(path: String, completionHandler: @escaping ((_ currentVersion: NSFileVersion?, _ versions: [NSFileVersion], _ error: Error?) -> Void)) { - NotImplemented() - } - - /// Resolves conflicts by selecting a version. - /// - Parameter path: Path of file or directory. - /// - Parameter version: Version than will be choose as main version. `nil` value indicates current version on this device will be selected. - /// - Parameter completionHandler: If an error parameter was provided, a presentable `Error` will be returned. - func selectVersionOfItem(path: String, version: NSFileVersion? = nil, completionHandler: SimpleCompletionHandler) { - NotImplemented() + _ = group.wait(timeout: .now() + 30) + return item } } /// Scope of iCloud, wrapper for NSMetadataQueryUbiquitous...Scope constants public enum UbiquitousScope: RawRepresentable { - /// Search all files not in the Documents directories of the app’s iCloud container directories. - /// Use this scope to store user-related data files that your app needs to share - /// but that are not files you want the user to manipulate directly. - /// - /// Raw value is equivalent to `NSMetadataQueryUbiquitousDataScope` + /** + Search all files not in the Documents directories of the app’s iCloud container directories. + Use this scope to store user-related data files that your app needs to share + but that are not files you want the user to manipulate directly. + + Raw value is equivalent to `NSMetadataQueryUbiquitousDataScope` + */ case data - /// Search all files in the Documents directories of the app’s iCloud container directories. - /// Put documents that the user is allowed to access inside a Documents subdirectory. - /// - /// Raw value is equivalent to `NSMetadataQueryUbiquitousDocumentsScope` + + /** + Search all files in the Documents directories of the app’s iCloud container directories. + Put documents that the user is allowed to access inside a Documents subdirectory. + + Raw value is equivalent to `NSMetadataQueryUbiquitousDocumentsScope` + */ case documents public typealias RawValue = String @@ -691,87 +770,3 @@ public enum UbiquitousScope: RawRepresentable { } } } - -struct QueryProgressWrapper { - weak var provider: CloudFileProvider? - weak var progress: Progress? - let operation: FileOperationType -} - -fileprivate class KVOObserver: NSObject { - override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { - guard let query = object as? NSMetadataQuery else { - return - } - guard let wrapper = context?.load(as: QueryProgressWrapper.self) else { - query.stop() - query.removeObserver(self, forKeyPath: "results") - return - } - let provider = wrapper.provider - let progress = wrapper.progress - let operation = wrapper.operation - - guard let results = change?[.newKey], let item = (results as? [NSMetadataItem])?.first else { - return - } - - query.disableUpdates() - var size = progress?.totalUnitCount ?? -1 - if size < 0, let size_d = item.value(forAttribute: NSMetadataItemFSSizeKey) as? Int64 { - size = size_d - progress?.totalUnitCount = size - } - let downloadStatus = item.value(forAttribute: NSMetadataUbiquitousItemPercentDownloadedKey) as? String ?? "" - let downloaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentDownloadedKey) as? Double ?? 0 - let uploaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentUploadedKey) as? Double ?? 0 - if (downloaded == 0 || downloaded == 100) && (uploaded > 0 && uploaded < 100) { - progress?.completedUnitCount = Int64(uploaded / 100 * Double(size)) - provider?.delegateNotify(operation, progress: uploaded / 100) - } else if (uploaded == 0 || uploaded == 100) && downloadStatus != NSMetadataUbiquitousItemDownloadingStatusCurrent { - progress?.completedUnitCount = Int64(downloaded / 100 * Double(size)) - provider?.delegateNotify(operation, progress: downloaded / 100) - } else if uploaded == 100 || downloadStatus == NSMetadataUbiquitousItemDownloadingStatusCurrent { - progress?.completedUnitCount = size - query.stop() - query.removeObserver(self, forKeyPath: "results") - provider?.delegateNotify(operation) - } - - query.enableUpdates() - } -} - -/* -func getMetadataItem(url: URL) -> NSMetadataItem? { - let query = NSMetadataQuery() - query.predicate = NSPredicate(format: "(%K LIKE %@)", NSMetadataItemPathKey, url.path) - query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope, NSMetadataQueryUbiquitousDataScope] - - var item: NSMetadataItem? - - let group = DispatchGroup() - group.enter() - var finishObserver: NSObjectProtocol? - finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in - defer { - query.stop() - group.leave() - NotificationCenter.default.removeObserver(finishObserver!) - } - - if query.resultCount > 0 { - item = query.result(at: 0) as? NSMetadataItem - } - - query.disableUpdates() - - }) - - DispatchQueue.main.async { - query.start() - } - _ = group.wait(timeout: .now() + 30) - return item -} -*/ diff --git a/Sources/DropboxFileProvider.swift b/Sources/DropboxFileProvider.swift index 33595d4..f63ffea 100644 --- a/Sources/DropboxFileProvider.swift +++ b/Sources/DropboxFileProvider.swift @@ -8,7 +8,9 @@ // import Foundation +#if os(macOS) || os(iOS) || os(tvOS) import CoreGraphics +#endif /** Allows accessing to Dropbox stored files. This provider doesn't cache or save files internally, however you can @@ -22,9 +24,9 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing { override open class var type: String { return "Dropbox" } /// Dropbox RPC API URL, which is equal with [https://api.dropboxapi.com/2/](https://api.dropboxapi.com/2/) - open let apiURL: URL + public let apiURL: URL /// Dropbox contents download/upload API URL, which is equal with [https://content.dropboxapi.com/2/](https://content.dropboxapi.com/2/) - open let contentURL: URL + public let contentURL: URL /** Initializer for Dropbox provider with given client ID and Token. @@ -42,7 +44,7 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing { } public required convenience init?(coder aDecoder: NSCoder) { - self.init(credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential) + self.init(credential: aDecoder.decodeObject(of: URLCredential.self, forKey: "credential")) self.useCache = aDecoder.decodeBool(forKey: "useCache") self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache") } @@ -87,19 +89,19 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing { let url = URL(string: "files/get_metadata", relativeTo: apiURL)! var request = URLRequest(url: url) request.httpMethod = "POST" - request.set(httpAuthentication: credential, with: .oAuth2) - request.set(httpContentType: .json) - let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString] + request.setValue(authentication: credential, with: .oAuth2) + request.setValue(contentType: .json) + let requestDictionary: [String: Any] = ["path": correctPath(path)!] request.httpBody = Data(jsonDictionary: requestDictionary) let task = session.dataTask(with: request, completionHandler: { (data, response, error) in var serverError: FileProviderHTTPError? var fileObject: DropboxFileObject? - if let response = response as? HTTPURLResponse { + if let response = response as? HTTPURLResponse, response.statusCode >= 400 { let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) } - if let json = data?.deserializeJSON(), let file = DropboxFileObject(json: json) { - fileObject = file - } + } + if let json = data?.deserializeJSON(), let file = DropboxFileObject(json: json) { + fileObject = file } completionHandler(fileObject, serverError ?? error) }) @@ -112,7 +114,7 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing { let url = URL(string: "users/get_space_usage", relativeTo: apiURL)! var request = URLRequest(url: url) request.httpMethod = "POST" - request.set(httpAuthentication: credential, with: .oAuth2) + request.setValue(authentication: credential, with: .oAuth2) let task = session.dataTask(with: request, completionHandler: { (data, response, error) in guard let json = data?.deserializeJSON() else { completionHandler(nil) @@ -132,7 +134,7 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing { Sample predicates: ``` - NSPredicate(format: "(name CONTAINS[c] 'hello') && (filesize >= 10000)") + NSPredicate(format: "(name CONTAINS[c] 'hello') && (fileSize >= 10000)") NSPredicate(format: "(modifiedDate >= %@)", Date()) NSPredicate(format: "(path BEGINSWITH %@)", "folder/child folder") ``` @@ -151,6 +153,7 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing { - error: `Error` returned by server if occured. - Returns: An `Progress` to get progress or cancel progress. Use `completedUnitCount` to iterate count of found items. */ + @discardableResult open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? { let queryStr: String? if query.predicateFormat == "TRUEPREDICATE" { @@ -162,14 +165,14 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing { let queryIsTruePredicate = query.predicateFormat == "TRUEPREDICATE" return paginated(path, requestHandler: requestHandler, pageHandler: { [weak self] (data, progress) -> (files: [FileObject], error: Error?, newToken: String?) in - guard let json = data?.deserializeJSON(), let entries = (json["entries"] ?? json["matches"]) as? [AnyObject] else { - let err = self?.urlError(path, code: .badServerResponse) + guard let json = data?.deserializeJSON(), let entries = (json["entries"] ?? json["matches"]) as? [Any] else { + let err = URLError(.badServerResponse, url: self?.url(of: path)) return ([], err, nil) } var files = [FileObject]() for entry in entries { - if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry), queryIsTruePredicate || query.evaluate(with: file.mapPredicate()) { + if let entry = entry as? [String: Any], let file = DropboxFileObject(json: entry), queryIsTruePredicate || query.evaluate(with: file.mapPredicate()) { files.append(file) progress.completedUnitCount += 1 foundItemHandler?(file) @@ -190,16 +193,16 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing { override func request(for operation: FileOperationType, overwrite: Bool = false, attributes: [URLResourceKey : Any] = [:]) -> URLRequest { func uploadRequest(to path: String) -> URLRequest { - var requestDictionary = [String: AnyObject]() + var requestDictionary = [String: Any]() let url: URL = URL(string: "files/upload", relativeTo: contentURL)! - requestDictionary["path"] = correctPath(path) as NSString? - requestDictionary["mode"] = (overwrite ? "overwrite" : "add") as NSString - requestDictionary["client_modified"] = (attributes[.contentModificationDateKey] as? Date)?.format(with: .rfc3339) as NSString? + requestDictionary["path"] = correctPath(path) + requestDictionary["mode"] = (overwrite ? "overwrite" : "add") + //requestDictionary["client_modified"] = (attributes[.contentModificationDateKey] as? Date)?.format(with: .rfc3339) var request = URLRequest(url: url) request.httpMethod = "POST" - request.set(httpAuthentication: credential, with: .oAuth2) - request.set(httpContentType: .stream) - request.set(dropboxArgKey: requestDictionary) + request.setValue(authentication: credential, with: .oAuth2) + request.setValue(contentType: .stream) + request.setValue(dropboxArgKey: requestDictionary) return request } @@ -207,8 +210,8 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing { let url = URL(string: "files/download", relativeTo: contentURL)! var request = URLRequest(url: url) request = URLRequest(url: url) - request.set(httpAuthentication: credential, with: .oAuth2) - request.set(dropboxArgKey: ["path": correctPath(path)! as NSString]) + request.setValue(authentication: credential, with: .oAuth2) + request.setValue(dropboxArgKey: ["path": correctPath(path)!]) return request } @@ -231,7 +234,7 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing { let url: String let sourcePath = operation.source let destPath = operation.destination - var requestDictionary = [String: AnyObject]() + var requestDictionary = [String: Any]() switch operation { case .create: url = "files/create_folder_v2" @@ -249,13 +252,13 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing { } var request = URLRequest(url: URL(string: url, relativeTo: apiURL)!) request.httpMethod = "POST" - request.set(httpAuthentication: credential, with: .oAuth2) - request.set(httpContentType: .json) - if let dest = correctPath(destPath) as NSString? { - requestDictionary["from_path"] = correctPath(sourcePath) as NSString? + request.setValue(authentication: credential, with: .oAuth2) + request.setValue(contentType: .json) + if let dest = correctPath(destPath) { + requestDictionary["from_path"] = correctPath(sourcePath) requestDictionary["to_path"] = dest } else { - requestDictionary["path"] = correctPath(sourcePath) as NSString? + requestDictionary["path"] = correctPath(sourcePath) } request.httpBody = Data(jsonDictionary: requestDictionary) return request @@ -264,11 +267,11 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing { override func serverError(with code: FileProviderHTTPErrorCode, path: String?, data: Data?) -> FileProviderHTTPError { let errorDesc: String? if let response = data?.deserializeJSON() { - errorDesc = (response["user_message"] as? String) ?? (response["error"]?["tag"] as? String) + errorDesc = (response["user_message"] as? String) ?? ((response["error"] as? [String: Any])?["tag"] as? String) } else { errorDesc = data.flatMap({ String(data: $0, encoding: .utf8) }) } - return FileProviderDropboxError(code: code, path: path ?? "", errorDescription: errorDesc) + return FileProviderDropboxError(code: code, path: path ?? "", serverDescription: errorDesc) } override var maxUploadSimpleSupported: Int64 { @@ -290,14 +293,14 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing { } */ // TODO: Implement /get_account & /get_current_account - + open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void)) { let url = URL(string: "files/get_temporary_link", relativeTo: apiURL)! var request = URLRequest(url: url) request.httpMethod = "POST" - request.set(httpAuthentication: credential, with: .oAuth2) - request.set(httpContentType: .json) - let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString] + request.setValue(authentication: credential, with: .oAuth2) + request.setValue(contentType: .json) + let requestDictionary: [String: Any] = ["path": correctPath(path)!] request.httpBody = Data(jsonDictionary: requestDictionary) let task = session.dataTask(with: request, completionHandler: { (data, response, error) in var serverError: FileProviderHTTPError? @@ -307,12 +310,8 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing { let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) } if let json = data?.deserializeJSON() { - if let linkStr = json["link"] as? String { - link = URL(string: linkStr) - } - if let attribDic = json["metadata"] as? [String: AnyObject] { - fileObject = DropboxFileObject(json: attribDic) - } + link = (json["link"] as? String).flatMap(URL.init(string:)) + fileObject = (json["metadata"] as? [String: Any]).flatMap(DropboxFileObject.init(json:)) } } @@ -335,15 +334,15 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing { */ open func copyItem(remoteURL: URL, to toPath: String, completionHandler: @escaping ((_ jobId: String?, _ attribute: DropboxFileObject?, _ error: Error?) -> Void)) { if remoteURL.isFileURL { - completionHandler(nil, nil, self.urlError(remoteURL.path, code: .badURL)) + completionHandler(nil, nil, URLError(.badURL, url: remoteURL)) return } let url = URL(string: "files/save_url", relativeTo: apiURL)! var request = URLRequest(url: url) request.httpMethod = "POST" - request.set(httpAuthentication: credential, with: .oAuth2) - request.set(httpContentType: .json) - let requestDictionary: [String: AnyObject] = ["path": correctPath(toPath)! as NSString, "url" : remoteURL.absoluteString as NSString] + request.setValue(authentication: credential, with: .oAuth2) + request.setValue(contentType: .json) + let requestDictionary: [String: Any] = ["path": correctPath(toPath)!, "url" : remoteURL.absoluteString] request.httpBody = Data(jsonDictionary: requestDictionary) let task = session.dataTask(with: request, completionHandler: { (data, response, error) in var serverError: FileProviderHTTPError? @@ -354,9 +353,7 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing { serverError = code.flatMap { self.serverError(with: $0, path: toPath, data: data) } if let json = data?.deserializeJSON() { jobId = json["async_job_id"] as? String - if let attribDic = json["metadata"] as? [String: AnyObject] { - fileObject = DropboxFileObject(json: attribDic) - } + fileObject = (json["metadata"] as? [String: Any]).flatMap(DropboxFileObject.init(json:)) } } completionHandler(jobId, fileObject, serverError ?? error) @@ -376,9 +373,9 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing { let url = URL(string: "files/copy_reference/save", relativeTo: apiURL)! var request = URLRequest(url: url) request.httpMethod = "POST" - request.set(httpAuthentication: credential, with: .oAuth2) - request.set(httpContentType: .json) - let requestDictionary: [String: AnyObject] = ["path": correctPath(toPath)! as NSString, "copy_reference" : reference as NSString] + request.setValue(authentication: credential, with: .oAuth2) + request.setValue(contentType: .json) + let requestDictionary: [String: Any] = ["path": correctPath(toPath)!, "copy_reference" : reference ] request.httpBody = Data(jsonDictionary: requestDictionary) let task = session.dataTask(with: request, completionHandler: { (data, response, error) in var serverError: FileProviderHTTPError? @@ -393,23 +390,8 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing { } extension DropboxFileProvider: ExtendedFileProvider { - open func thumbnailOfFileSupported(path: String) -> Bool { - switch (path as NSString).pathExtension.lowercased() { - case "jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff": - return true - case "doc", "docx", "docm", "xls", "xlsx", "xlsm": - return true - case "ppt", "pps", "ppsx", "ppsm", "pptx", "pptm": - return true - case "rtf": - return true - default: - return false - } - } - open func propertiesOfFileSupported(path: String) -> Bool { - let fileExt = (path as NSString).pathExtension.lowercased() + let fileExt = path.pathExtension.lowercased() switch fileExt { case "jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff": return true @@ -421,12 +403,55 @@ extension DropboxFileProvider: ExtendedFileProvider { return false } } - + + @discardableResult + open func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) -> Progress? { + let url = URL(string: "files/get_metadata", relativeTo: apiURL)! + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue(authentication: credential, with: .oAuth2) + request.setValue(contentType: .json) + let requestDictionary: [String: Any] = ["path": correctPath(path)!, "include_media_info": NSNumber(value: true)] + request.httpBody = Data(jsonDictionary: requestDictionary) + let task = session.dataTask(with: request, completionHandler: { (data, response, error) in + var serverError: FileProviderHTTPError? + var dic = [String: Any]() + var keys = [String]() + if let response = response as? HTTPURLResponse { + let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) + serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) } + if let json = data?.deserializeJSON(), let properties = (json["media_info"] as? [String: Any])?["metadata"] as? [String: Any] { + (dic, keys) = self.mapMediaInfo(properties) + } + } + completionHandler(dic, keys, serverError ?? error) + }) + task.resume() + return nil + } + + #if os(macOS) || os(iOS) || os(tvOS) + open func thumbnailOfFileSupported(path: String) -> Bool { + switch path.pathExtension.lowercased() { + case "jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff": + return true + case "doc", "docx", "docm", "xls", "xlsx", "xlsm": + return false + case "ppt", "pps", "ppsx", "ppsm", "pptx", "pptm": + return false + case "rtf": + return false + default: + return false + } + } + /// Default value for dimension is 64x64, according to Dropbox documentation - open func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) { + @discardableResult + open func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) -> Progress? { let url: URL let thumbAPI: Bool - switch (path as NSString).pathExtension.lowercased() { + switch path.pathExtension.lowercased() { case "jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff": url = URL(string: "files/get_thumbnail", relativeTo: contentURL)! thumbAPI = true @@ -438,13 +463,13 @@ extension DropboxFileProvider: ExtendedFileProvider { url = URL(string: "files/get_preview", relativeTo: contentURL)! thumbAPI = false default: - return + return nil } var request = URLRequest(url: url) - request.set(httpAuthentication: credential, with: .oAuth2) - var requestDictionary: [String: AnyObject] = ["path": path as NSString] + request.setValue(authentication: credential, with: .oAuth2) + var requestDictionary: [String: Any] = ["path": path] if thumbAPI { - requestDictionary["format"] = "jpeg" as NSString + requestDictionary["format"] = "jpeg" let size: String switch dimension?.height ?? 64 { case 0...32: size = "w32h32" @@ -453,55 +478,33 @@ extension DropboxFileProvider: ExtendedFileProvider { case 129...480: size = "w640h480" default: size = "w1024h768" } - requestDictionary["size"] = size as NSString + requestDictionary["size"] = size } - request.set(dropboxArgKey: requestDictionary) + request.setValue(dropboxArgKey: requestDictionary) let task = self.session.dataTask(with: request, completionHandler: { (data, response, error) in var image: ImageClass? = nil if let r = response as? HTTPURLResponse, let result = r.allHeaderFields["Dropbox-API-Result"] as? String, let jsonResult = result.deserializeJSON() { if jsonResult["error"] != nil { - completionHandler(nil, self.urlError(path, code: .cannotDecodeRawData)) + completionHandler(nil, URLError(.cannotDecodeRawData, url: self.url(of: path))) } } if let data = data { - if data.isPDF, let pageImage = DropboxFileProvider.convertToImage(pdfData: data) { + if data.isPDF, let pageImage = DropboxFileProvider.convertToImage(pdfData: data, maxSize: dimension) { image = pageImage } else if let contentType = (response as? HTTPURLResponse)?.allHeaderFields["Content-Type"] as? String, contentType.contains("text/html") { // TODO: Implement converting html returned type of get_preview to image - } else if let fetchedimage = ImageClass(data: data){ + } else { if let dimension = dimension { - image = DropboxFileProvider.scaleDown(image: fetchedimage, toSize: dimension) + image = DropboxFileProvider.scaleDown(data: data, toSize: dimension) } else { - image = fetchedimage + } } } completionHandler(image, error) }) task.resume() + return nil } - - open func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) { - let url = URL(string: "files/get_metadata", relativeTo: apiURL)! - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.set(httpAuthentication: credential, with: .oAuth2) - request.set(httpContentType: .json) - let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString, "include_media_info": NSNumber(value: true)] - request.httpBody = Data(jsonDictionary: requestDictionary) - let task = session.dataTask(with: request, completionHandler: { (data, response, error) in - var serverError: FileProviderHTTPError? - var dic = [String: Any]() - var keys = [String]() - if let response = response as? HTTPURLResponse { - let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) - serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) } - if let json = data?.deserializeJSON(), let properties = (json["media_info"] as? [String: Any])?["metadata"] as? [String: Any] { - (dic, keys) = self.mapMediaInfo(properties) - } - } - completionHandler(dic, keys, serverError ?? error) - }) - task.resume() - } + #endif } diff --git a/Sources/DropboxHelper.swift b/Sources/DropboxHelper.swift index a6caac1..dc1fc9f 100644 --- a/Sources/DropboxHelper.swift +++ b/Sources/DropboxHelper.swift @@ -12,7 +12,7 @@ import Foundation public struct FileProviderDropboxError: FileProviderHTTPError { public let code: FileProviderHTTPErrorCode public let path: String - public let errorDescription: String? + public let serverDescription: String? } /// Containts path, url and attributes of a Dropbox file or resource. @@ -22,9 +22,9 @@ public final class DropboxFileObject: FileObject { self.init(json: json) } - internal init? (json: [String: AnyObject]) { + internal init? (json: [String: Any]) { var json = json - if json["name"] == nil, let metadata = json["metadata"] as? [String: AnyObject] { + if json["name"] == nil, let metadata = json["metadata"] as? [String: Any] { json = metadata } guard let name = json["name"] as? String else { return nil } @@ -34,13 +34,13 @@ public final class DropboxFileObject: FileObject { self.serverTime = (json["server_modified"] as? String).flatMap(Date.init(rfcString:)) self.modifiedDate = (json["client_modified"] as? String).flatMap(Date.init(rfcString:)) self.type = (json[".tag"] as? String) == "folder" ? .directory : .regular - self.isReadOnly = (json["sharing_info"]?["read_only"] as? NSNumber)?.boolValue ?? false + self.isReadOnly = ((json["sharing_info"] as? [String: Any])?["read_only"] as? NSNumber)?.boolValue ?? false self.id = json["id"] as? String self.rev = json["rev"] as? String } /// The time contents of file has been modified on server, returns nil if not set - open internal(set) var serverTime: Date? { + public internal(set) var serverTime: Date? { get { return allValues[.serverDateKey] as? Date } @@ -51,7 +51,7 @@ public final class DropboxFileObject: FileObject { /// The document identifier is a value assigned by the Dropbox to a file. /// This value is used to identify the document regardless of where it is moved on a volume. - open internal(set) var id: String? { + public internal(set) var id: String? { get { return allValues[.fileResourceIdentifierKey] as? String } @@ -62,7 +62,7 @@ public final class DropboxFileObject: FileObject { /// The revision of file, which changes when a file contents are modified. /// Changes to attributes or other file metadata do not change the identifier. - open internal(set) var rev: String? { + public internal(set) var rev: String? { get { return allValues[.generationIdentifierKey] as? String } @@ -72,8 +72,8 @@ public final class DropboxFileObject: FileObject { } } -internal extension DropboxFileProvider { - internal func correctPath(_ path: String?) -> String? { +extension DropboxFileProvider { + func correctPath(_ path: String?) -> String? { guard let path = path else { return nil } if path.hasPrefix("id:") || path.hasPrefix("rev:") { return path @@ -85,17 +85,17 @@ internal extension DropboxFileProvider { return p } - internal func listRequest(path: String, queryStr: String? = nil, recursive: Bool = false) -> ((_ token: String?) -> URLRequest?) { + func listRequest(path: String, queryStr: String? = nil, recursive: Bool = false) -> ((_ token: String?) -> URLRequest?) { if let queryStr = queryStr { return { [weak self] (token) -> URLRequest? in guard let `self` = self else { return nil } let url = URL(string: "files/search", relativeTo: self.apiURL)! var request = URLRequest(url: url) request.httpMethod = "POST" - request.set(httpAuthentication: self.credential, with: .oAuth2) - request.set(httpContentType: .json) - var requestDictionary: [String: AnyObject] = ["path": self.correctPath(path) as NSString!] - requestDictionary["query"] = queryStr as NSString + request.setValue(authentication: self.credential, with: .oAuth2) + request.setValue(contentType: .json) + var requestDictionary: [String: Any] = ["path": self.correctPath(path)!] + requestDictionary["query"] = queryStr requestDictionary["start"] = NSNumber(value: (token.flatMap( { Int($0) } ) ?? 0)) request.httpBody = Data(jsonDictionary: requestDictionary) return request @@ -103,20 +103,20 @@ internal extension DropboxFileProvider { } else { return { [weak self] (token) -> URLRequest? in guard let `self` = self else { return nil } - var requestDictionary = [String: AnyObject]() + var requestDictionary = [String: Any]() let url: URL if let token = token { url = URL(string: "files/list_folder/continue", relativeTo: self.apiURL)! - requestDictionary["cursor"] = token as NSString? + requestDictionary["cursor"] = token } else { url = URL(string: "files/list_folder", relativeTo: self.apiURL)! - requestDictionary["path"] = self.correctPath(path) as NSString? + requestDictionary["path"] = self.correctPath(path) requestDictionary["recursive"] = NSNumber(value: recursive) } var request = URLRequest(url: url) request.httpMethod = "POST" - request.set(httpAuthentication: self.credential, with: .oAuth2) - request.set(httpContentType: .json) + request.setValue(authentication: self.credential, with: .oAuth2) + request.setValue(contentType: .json) request.httpBody = Data(jsonDictionary: requestDictionary) return request } diff --git a/Sources/ExtendedLocalFileProvider.swift b/Sources/ExtendedLocalFileProvider.swift index fa2ecfb..b6c7e28 100644 --- a/Sources/ExtendedLocalFileProvider.swift +++ b/Sources/ExtendedLocalFileProvider.swift @@ -6,6 +6,7 @@ // Copyright © 2017 Mousavian. Distributed under MIT license. // +#if os(macOS) || os(iOS) || os(tvOS) import Foundation import ImageIO import CoreGraphics @@ -13,7 +14,7 @@ import AVFoundation extension LocalFileProvider: ExtendedFileProvider { open func thumbnailOfFileSupported(path: String) -> Bool { - switch (path as NSString).pathExtension.lowercased() { + switch path.pathExtension.lowercased() { case LocalFileInformationGenerator.imageThumbnailExtensions.contains: return true case LocalFileInformationGenerator.audioThumbnailExtensions.contains: @@ -32,7 +33,7 @@ extension LocalFileProvider: ExtendedFileProvider { } open func propertiesOfFileSupported(path: String) -> Bool { - let fileExt = (path as NSString).pathExtension.lowercased() + let fileExt = path.pathExtension.lowercased() switch fileExt { case LocalFileInformationGenerator.imagePropertiesExtensions.contains: return LocalFileInformationGenerator.imageProperties != nil @@ -54,7 +55,8 @@ extension LocalFileProvider: ExtendedFileProvider { } } - open func thumbnailOfFile(path: String, dimension: CGSize? = nil, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) { + @discardableResult + open func thumbnailOfFile(path: String, dimension: CGSize? = nil, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) -> Progress? { let dimension = dimension ?? CGSize(width: 64, height: 64) (dispatch_queue).async { var thumbnailImage: ImageClass? = nil @@ -63,32 +65,31 @@ extension LocalFileProvider: ExtendedFileProvider { // Create Thumbnail and cache switch fileURL.pathExtension.lowercased() { case LocalFileInformationGenerator.videoThumbnailExtensions.contains: - thumbnailImage = LocalFileInformationGenerator.videoThumbnail(fileURL) + thumbnailImage = LocalFileInformationGenerator.videoThumbnail(fileURL, dimension) case LocalFileInformationGenerator.audioThumbnailExtensions.contains: - thumbnailImage = LocalFileInformationGenerator.audioThumbnail(fileURL) + thumbnailImage = LocalFileInformationGenerator.audioThumbnail(fileURL, dimension) case LocalFileInformationGenerator.imageThumbnailExtensions.contains: - thumbnailImage = LocalFileInformationGenerator.imageThumbnail(fileURL) + thumbnailImage = LocalFileInformationGenerator.imageThumbnail(fileURL, dimension) case LocalFileInformationGenerator.pdfThumbnailExtensions.contains: - thumbnailImage = LocalFileInformationGenerator.pdfThumbnail(fileURL) + thumbnailImage = LocalFileInformationGenerator.pdfThumbnail(fileURL, dimension) case LocalFileInformationGenerator.officeThumbnailExtensions.contains: - thumbnailImage = LocalFileInformationGenerator.officeThumbnail(fileURL) + thumbnailImage = LocalFileInformationGenerator.officeThumbnail(fileURL, dimension) case LocalFileInformationGenerator.customThumbnailExtensions.contains: - thumbnailImage = LocalFileInformationGenerator.customThumbnail(fileURL) + thumbnailImage = LocalFileInformationGenerator.customThumbnail(fileURL, dimension) default: completionHandler(nil, nil) return } - if let image = thumbnailImage { - let scaledImage = LocalFileProvider.scaleDown(image: image, toSize: dimension) - completionHandler(scaledImage, nil) - } + completionHandler(thumbnailImage, nil) } + return nil } - open func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String: Any], _ keys: [String], _ error: Error?) -> Void)) { + @discardableResult + open func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String: Any], _ keys: [String], _ error: Error?) -> Void)) -> Progress? { (dispatch_queue).async { - let fileExt = (path as NSString).pathExtension.lowercased() + let fileExt = path.pathExtension.lowercased() var getter: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? switch fileExt { case LocalFileInformationGenerator.imagePropertiesExtensions.contains: @@ -117,6 +118,7 @@ extension LocalFileProvider: ExtendedFileProvider { completionHandler(dic, keys, nil) } + return nil } } @@ -189,23 +191,19 @@ public struct LocalFileInformationGenerator { static public var customPropertiesExtensions: [String] = [] /// Thumbnail generator closure for image files. - static public var imageThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in - return ImageClass(contentsOfFile: fileURL.path) + static public var imageThumbnail: (_ fileURL: URL, _ dimension: CGSize?) -> ImageClass? = { fileURL, dimension in + return LocalFileProvider.scaleDown(fileURL: fileURL, toSize: dimension) } /// Thumbnail generator closure for audio and music files. - static public var audioThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in + static public var audioThumbnail: (_ fileURL: URL, _ dimension: CGSize?) -> ImageClass? = { fileURL, dimension in let playerItem = AVPlayerItem(url: fileURL) let metadataList = playerItem.asset.commonMetadata - #if swift(>=4.0) let commonKeyArtwork = AVMetadataKey.commonKeyArtwork - #else - let commonKeyArtwork = AVMetadataCommonKeyArtwork - #endif for item in metadataList { if item.commonKey == commonKeyArtwork { if let data = item.dataValue { - return ImageClass(data: data) + return LocalFileProvider.scaleDown(data: data, toSize: dimension) } } } @@ -213,11 +211,12 @@ public struct LocalFileInformationGenerator { } /// Thumbnail generator closure for video files. - static public var videoThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in + static public var videoThumbnail: (_ fileURL: URL, _ dimension: CGSize?) -> ImageClass? = { fileURL, dimension in let asset = AVAsset(url: fileURL) let assetImgGenerate = AVAssetImageGenerator(asset: asset) + assetImgGenerate.maximumSize = dimension ?? .zero assetImgGenerate.appliesPreferredTrackTransform = true - let time = CMTimeMake(asset.duration.value / 3, asset.duration.timescale) + let time = CMTime(value: asset.duration.value / 3, timescale: asset.duration.timescale) if let cgImage = try? assetImgGenerate.copyCGImage(at: time, actualTime: nil) { #if os(macOS) return ImageClass(cgImage: cgImage, size: .zero) @@ -229,19 +228,19 @@ public struct LocalFileInformationGenerator { } /// Thumbnail generator closure for portable document files files. - static public var pdfThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in - return LocalFileProvider.convertToImage(pdfURL: fileURL) + static public var pdfThumbnail: (_ fileURL: URL, _ dimension: CGSize?) -> ImageClass? = { fileURL, dimension in + return LocalFileProvider.convertToImage(pdfURL: fileURL, maxSize: dimension) } /// Thumbnail generator closure for office document files. /// - Note: No default implementation is avaiable - static public var officeThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in + static public var officeThumbnail: (_ fileURL: URL, _ dimension: CGSize?) -> ImageClass? = { fileURL, dimension in return nil } /// Thumbnail generator closure for custom type of files. /// - Note: No default implementation is avaiable - static public var customThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in + static public var customThumbnail: (_ fileURL: URL, _ dimension: CGSize?) -> ImageClass? = { fileURL, dimension in return nil } @@ -271,32 +270,36 @@ public struct LocalFileInformationGenerator { return(Int(newTopVal), Int(newBottomVal)) } - guard let cgDataRef = CGImageSourceCreateWithURL(fileURL as CFURL, nil), let cfImageDict = CGImageSourceCopyPropertiesAtIndex(cgDataRef, 0, nil) else { + guard let source = CGImageSourceCreateWithURL(fileURL as CFURL, nil), let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as NSDictionary? else { return (dic, keys) } - let imageDict = cfImageDict as NSDictionary - let tiffDict = imageDict[kCGImagePropertyTIFFDictionary as String] as? NSDictionary ?? [:] - let exifDict = imageDict[kCGImagePropertyExifDictionary as String] as? NSDictionary ?? [:] - if let pixelWidth = imageDict.object(forKey: kCGImagePropertyPixelWidth) as? NSNumber, let pixelHeight = imageDict.object(forKey: kCGImagePropertyPixelHeight) as? NSNumber { + let tiffDict = properties[kCGImagePropertyTIFFDictionary as String] as? NSDictionary ?? [:] + let exifDict = properties[kCGImagePropertyExifDictionary as String] as? NSDictionary ?? [:] + let gpsDict = properties[kCGImagePropertyGPSDictionary as String] as? NSDictionary ?? [:] + + if let pixelWidth = properties.object(forKey: kCGImagePropertyPixelWidth) as? NSNumber, let pixelHeight = properties.object(forKey: kCGImagePropertyPixelHeight) as? NSNumber { add(key: "Dimensions", value: "\(pixelWidth)x\(pixelHeight)") } - - add(key: "DPI", value: imageDict[kCGImagePropertyDPIWidth as String]) - add(key: "Device make", value: tiffDict[kCGImagePropertyTIFFMake as String]) + add(key: "DPI", value: properties[kCGImagePropertyDPIWidth as String]) + add(key: "Device maker", value: tiffDict[kCGImagePropertyTIFFMake as String]) add(key: "Device model", value: tiffDict[kCGImagePropertyTIFFModel as String]) add(key: "Lens model", value: exifDict[kCGImagePropertyExifLensModel as String]) add(key: "Artist", value: tiffDict[kCGImagePropertyTIFFArtist as String] as? String) add(key: "Copyright", value: tiffDict[kCGImagePropertyTIFFCopyright as String] as? String) add(key: "Date taken", value: tiffDict[kCGImagePropertyTIFFDateTime as String] as? String) - if let latitude = tiffDict[kCGImagePropertyGPSLatitude as String] as? NSNumber, let longitude = tiffDict[kCGImagePropertyGPSLongitude as String] as? NSNumber { - add(key: "Location", value: "\(latitude), \(longitude)") + if let latitude = gpsDict[kCGImagePropertyGPSLatitude as String] as? NSNumber, + let longitude = gpsDict[kCGImagePropertyGPSLongitude as String] as? NSNumber { + let altitudeDesc = (gpsDict[kCGImagePropertyGPSAltitude as String] as? NSNumber).map({ " at \($0.format(precision: 0))m" }) ?? "" + add(key: "Location", value: "\(latitude.format()), \(longitude.format())\(altitudeDesc)") } - add(key: "Altitude", value: tiffDict[kCGImagePropertyGPSAltitude as String] as? NSNumber) - add(key: "Area", value: tiffDict[kCGImagePropertyGPSAreaInformation as String]) + add(key: "Area", value: gpsDict[kCGImagePropertyGPSAreaInformation as String]) - add(key: "Color space", value: imageDict[kCGImagePropertyColorModel as String]) + add(key: "Color space", value: properties[kCGImagePropertyColorModel as String]) + add(key: "Color depth", value: (properties[kCGImagePropertyDepth as String] as? NSNumber).map({ "\($0) bits" })) + add(key: "Color profile", value: properties[kCGImagePropertyProfileName as String]) add(key: "Focal length", value: exifDict[kCGImagePropertyExifFocalLength as String]) + add(key: "White banance", value: exifDict[kCGImagePropertyExifWhiteBalance as String]) add(key: "F number", value: exifDict[kCGImagePropertyExifFNumber as String]) add(key: "Exposure program", value: exifDict[kCGImagePropertyExifExposureProgram as String]) @@ -320,7 +323,7 @@ public struct LocalFileInformationGenerator { } } - func makeDescription(_ key: String?) -> String? { + func makeKeyDescription(_ key: String?) -> String? { guard let key = key else { return nil } @@ -331,21 +334,39 @@ public struct LocalFileInformationGenerator { return newKey.capitalized } - guard FileManager.default.fileExists(atPath: fileURL.path) else { + func parseLocationData(_ value: String) -> (latitude: Double, longitude: Double, height: Double?)? { + let scanner = Scanner.init(string: value) + var latitude: Double = 0.0, longitude: Double = 0.0, height: Double = 0 + + if scanner.scanDouble(&latitude), scanner.scanDouble(&longitude) { + scanner.scanDouble(&height) + return (latitude, longitude, height) + } else { + return nil + } + } + + guard fileURL.fileExists else { return (dic, keys) } let playerItem = AVPlayerItem(url: fileURL) let metadataList = playerItem.asset.commonMetadata for item in metadataList { - #if swift(>=4.0) - let commonKey = item.commonKey?.rawValue - #else - let commonKey = item.commonKey - #endif - if let description = makeDescription(commonKey) { - if let value = item.stringValue { - keys.append(description) - dic[description] = value + let commonKey = item.commonKey?.rawValue + if let key = makeKeyDescription(commonKey) { + if commonKey == "location", let value = item.stringValue, let loc = parseLocationData(value) { + keys.append(key) + let heightStr: String = (loc.height as NSNumber?).map({ ", \($0.format(precision: 0))m" }) ?? "" + dic[key] = "\((loc.latitude as NSNumber).format())°, \((loc.longitude as NSNumber).format())°\(heightStr)" + } else if let value = item.dateValue { + keys.append(key) + dic[key] = value + } else if let value = item.numberValue { + keys.append(key) + dic[key] = value + } else if let value = item.stringValue { + keys.append(key) + dic[key] = value } } } @@ -372,16 +393,12 @@ public struct LocalFileInformationGenerator { dic = audioprops.prop keys = audioprops.keys dic.removeValue(forKey: "Duration") - if let index = keys.index(of: "Duration") { + if let index = keys.firstIndex(of: "Duration") { keys.remove(at: index) } } let asset = AVURLAsset(url: fileURL, options: nil) - #if swift(>=4.0) let videoTracks = asset.tracks(withMediaType: AVMediaType.video) - #else - let videoTracks = asset.tracks(withMediaType: AVMediaTypeVideo) - #endif if let videoTrack = videoTracks.first { var bitrate: Float = 0 let width = Int(videoTrack.naturalSize.width) @@ -395,11 +412,7 @@ public struct LocalFileInformationGenerator { add(key: "Duration", value: TimeInterval(duration).formatshort) add(key: "Video Bitrate", value: "\(Int(ceil(bitrate / 1000))) kbps") } - #if swift(>=4.0) let audioTracks = asset.tracks(withMediaType: AVMediaType.audio) - #else - let audioTracks = asset.tracks(withMediaType: AVMediaTypeAudio) - #endif // dic["Audio channels"] = audioTracks.count var bitrate: Float = 0 for track in audioTracks { @@ -444,21 +457,12 @@ public struct LocalFileInformationGenerator { guard let date = date else { return nil } let dateStr = date.replacingOccurrences(of: "'", with: "").replacingOccurrences(of: "D:", with: "", options: .anchored) let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyyMMddHHmmssTZ" - if let result = dateFormatter.date(from: dateStr) { - return result - } - dateFormatter.dateFormat = "yyyyMMddHHmmssZZZZZ" - if let result = dateFormatter.date(from: dateStr) { - return result - } - dateFormatter.dateFormat = "yyyyMMddHHmmssZ" - if let result = dateFormatter.date(from: dateStr) { - return result - } - dateFormatter.dateFormat = "yyyyMMddHHmmss" - if let result = dateFormatter.date(from: dateStr) { - return result + let formats: [String] = ["yyyyMMddHHmmssTZ", "yyyyMMddHHmmssZZZZZ", "yyyyMMddHHmmssZ", "yyyyMMddHHmmss"] + for format in formats { + dateFormatter.dateFormat = format + if let result = dateFormatter.date(from: dateStr) { + return result + } } return nil } @@ -486,7 +490,7 @@ public struct LocalFileInformationGenerator { add(key: "Content creator", value: getKey("Creator", from: dict)) add(key: "Creation date", value: convertDate(getKey("CreationDate", from: dict))) add(key: "Modified date", value: convertDate(getKey("ModDate", from: dict))) - add(key: "Security", value: reference.isEncrypted) + add(key: "Security", value: reference.isEncrypted ? (reference.isUnlocked ? "Present" : "Password Protected") : "None") add(key: "Allows printing", value: reference.allowsPrinting) add(key: "Allows copying", value: reference.allowsCopying) return (dic, keys) @@ -504,3 +508,4 @@ public struct LocalFileInformationGenerator { /// - Note: No default implementation is avaiable static public var customProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = nil } +#endif diff --git a/Sources/Extensions/FoundationExtensions.swift b/Sources/Extensions/FoundationExtensions.swift index dccf35e..cabe530 100755 --- a/Sources/Extensions/FoundationExtensions.swift +++ b/Sources/Extensions/FoundationExtensions.swift @@ -8,7 +8,7 @@ import Foundation -public extension Array where Element: FileObject { +extension Array where Element: FileObject { /// Returns a sorted array of `FileObject`s by criterias set in attributes. public func sort(by type: FileObjectSorting.SortType, ascending: Bool = true, isDirectoriesFirst: Bool = false) -> [Element] { let sorting = FileObjectSorting(type: type, ascending: ascending, isDirectoriesFirst: isDirectoriesFirst) @@ -22,12 +22,13 @@ public extension Array where Element: FileObject { } public extension Sequence where Iterator.Element == UInt8 { + /// Converts a byte array into hexadecimal string representation func hexString() -> String { return self.map{String(format: "%02X", $0)}.joined() } } -public extension URLFileResourceType { +extension URLFileResourceType { /// **FileProvider** returns corresponding `URLFileResourceType` of a `FileAttributeType` value public init(fileTypeValue: FileAttributeType) { switch fileTypeValue { @@ -43,7 +44,33 @@ public extension URLFileResourceType { } } -public extension URLResourceKey { +extension CocoaError { + init(_ code: CocoaError.Code, path: String?) { + if let path = path { + let userInfo: [String: Any] = [NSFilePathErrorKey: path] + self.init(code, userInfo: userInfo) + } else { + self.init(code) + } + + } +} + +extension URLError { + init(_ code: URLError.Code, url: URL?) { + if let url = url { + let userInfo: [String: Any] = [NSURLErrorKey: url, + NSURLErrorFailingURLErrorKey: url, + NSURLErrorFailingURLStringErrorKey: url.absoluteString, + ] + self.init(code, userInfo: userInfo) + } else { + self.init(code) + } + } +} + +extension URLResourceKey { /// **FileProvider** returns url of file object. public static let fileURLKey = URLResourceKey(rawValue: "NSURLFileURLKey") /// **FileProvider** returns modification date of file in server @@ -58,7 +85,7 @@ public extension URLResourceKey { public static let childrensCount = URLResourceKey(rawValue: "MFPURLChildrensCount") } -public extension ProgressUserInfoKey { +extension ProgressUserInfoKey { /// **FileProvider** returns associated `FileProviderOperationType` public static let fileProvderOperationTypeKey = ProgressUserInfoKey("MFPOperationTypeKey") /// **FileProvider** returns start date/time of operation @@ -75,15 +102,22 @@ internal extension URL { } var fileSize: Int64 { - return Int64((try? self.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? -1) + return (try? self.resourceValues(forKeys: [.fileSizeKey]))?.allValues[.fileSizeKey] as? Int64 ?? -1 } var fileExists: Bool { - return self.isFileURL && FileManager.default.fileExists(atPath: self.path) + return (try? self.checkResourceIsReachable()) ?? false } + + #if os(macOS) || os(iOS) || os(tvOS) + #else + func checkPromisedItemIsReachable() throws -> Bool { + return false + } + #endif } -public extension URLRequest { +extension URLRequest { /// Defines HTTP Authentication method required to access public enum AuthenticationType { /// Basic method for authentication @@ -97,27 +131,128 @@ public extension URLRequest { } } -struct Quality { - let value: T - let quality: Float - - var stringifed: String { - var representaion: String = String(describing: value) - let quality: Float = min(1, max(self.quality, 0)) - if let value = value as? Locale { - representaion = value.identifier.replacingOccurrences(of: "_", with: "-") - } - if let value = value as? String.Encoding { - let cfEncoding = CFStringConvertNSStringEncodingToEncoding(value.rawValue) - representaion = CFStringConvertEncodingToIANACharSetName(cfEncoding) as String? ?? "*" - } - let qualityDesc = String(format: "%.1f", quality) - return "\(representaion); q=\(qualityDesc)" - } +/// Holds file MIME, and introduces selected type MIME as constants +public struct ContentMIMEType: RawRepresentable, Hashable, Equatable { + public var rawValue: String + public typealias RawValue = String + + public init(rawValue: String) { + self.rawValue = rawValue + } + + public var hashValue: Int { return rawValue.hashValue } + public static func == (lhs: ContentMIMEType, rhs: ContentMIMEType) -> Bool { + return lhs.rawValue == rhs.rawValue + } + + /// Directory + static public let directory = ContentMIMEType(rawValue: "httpd/unix-directory") + + // Archive and Binary + + /// Binary stream and unknown types + static public let stream = ContentMIMEType(rawValue: "application/octet-stream") + /// Protable document format + static public let pdf = ContentMIMEType(rawValue: "application/pdf") + /// Zip archive + static public let zip = ContentMIMEType(rawValue: "application/zip") + /// Rar archive + static public let rarArchive = ContentMIMEType(rawValue: "application/x-rar-compressed") + /// 7-zip archive + static public let lzma = ContentMIMEType(rawValue: "application/x-7z-compressed") + /// Adobe Flash + static public let flash = ContentMIMEType(rawValue: "application/x-shockwave-flash") + /// ePub book + static public let epub = ContentMIMEType(rawValue: "application/epub+zip") + /// Java archive (jar) + static public let javaArchive = ContentMIMEType(rawValue: "application/java-archive") + + // Texts + + /// Text file + static public let plainText = ContentMIMEType(rawValue: "text/plain") + /// Coma-separated values + static public let csv = ContentMIMEType(rawValue: "text/csv") + /// Hyper-text markup language + static public let html = ContentMIMEType(rawValue: "text/html") + /// Common style sheet + static public let css = ContentMIMEType(rawValue: "text/css") + /// eXtended Markup language + static public let xml = ContentMIMEType(rawValue: "text/xml") + /// Javascript code file + static public let javascript = ContentMIMEType(rawValue: "application/javascript") + /// Javascript notation + static public let json = ContentMIMEType(rawValue: "application/json") + + // Documents + + /// Rich text file (RTF) + static public let richText = ContentMIMEType(rawValue: "application/rtf") + /// Excel 2013 (OOXML) document + static public let excel = ContentMIMEType(rawValue: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + /// Powerpoint 2013 (OOXML) document + static public let powerpoint = ContentMIMEType(rawValue: "application/vnd.openxmlformats-officedocument.presentationml.slideshow") + /// Word 2013 (OOXML) document + static public let word = ContentMIMEType(rawValue: "application/vnd.openxmlformats-officedocument.wordprocessingml.document") + + // Images + + /// Bitmap + static public let bmp = ContentMIMEType(rawValue: "image/bmp") + /// Graphics Interchange Format photo + static public let gif = ContentMIMEType(rawValue: "image/gif") + /// JPEG photo + static public let jpeg = ContentMIMEType(rawValue: "image/jpeg") + /// Portable network graphics + static public let png = ContentMIMEType(rawValue: "image/png") + + // Audio & Video + + /// MPEG Audio + static public let mpegAudio = ContentMIMEType(rawValue: "audio/mpeg") + /// MPEG Video + static public let mpeg = ContentMIMEType(rawValue: "video/mpeg") + /// MPEG4 Audio + static public let mpeg4Audio = ContentMIMEType(rawValue: "audio/mp4") + /// MPEG4 Video + static public let mpeg4 = ContentMIMEType(rawValue: "video/mp4") + /// OGG Audio + static public let ogg = ContentMIMEType(rawValue: "audio/ogg") + /// Advanced Audio Coding + static public let aac = ContentMIMEType(rawValue: "audio/x-aac") + /// Microsoft Audio Video Interleaved + static public let avi = ContentMIMEType(rawValue: "video/x-msvideo") + /// Microsoft Wave audio + static public let wav = ContentMIMEType(rawValue: "audio/x-wav") + /// Apple QuickTime format + static public let quicktime = ContentMIMEType(rawValue: "video/quicktime") + /// 3GPP + static public let threegp = ContentMIMEType(rawValue: "video/3gpp") + /// Adobe Flash video + static public let flashVideo = ContentMIMEType(rawValue: "video/x-flv") + /// Adobe Flash video + static public let flv = ContentMIMEType.flashVideo + + // Google Drive + + /// Google Drive: Folder + static public let googleFolder = ContentMIMEType(rawValue: "application/vnd.google-apps.folder") + /// Google Drive: Document (word processor) + static public let googleDocument = ContentMIMEType(rawValue: "application/vnd.google-apps.document") + /// Google Drive: Sheets (spreadsheet) + static public let googleSheets = ContentMIMEType(rawValue: "application/vnd.google-apps.spreadsheet") + /// Google Drive: Slides (presentation) + static public let googleSlides = ContentMIMEType(rawValue: "application/vnd.google-apps.presentation") + /// Google Drive: Drawing (vector draw) + static public let googleDrawing = ContentMIMEType(rawValue: "application/vnd.google-apps.drawing") + /// Google Drive: Audio + static public let googleAudio = ContentMIMEType(rawValue: "application/vnd.google-apps.audio") + /// Google Drive: Video + static public let googleVideo = ContentMIMEType(rawValue: "application/vnd.google-apps.video") } internal extension URLRequest { - mutating func set(httpAuthentication credential: URLCredential?, with type: AuthenticationType) { + mutating func setValue(authentication credential: URLCredential?, with type: AuthenticationType) { func base64(_ str: String) -> String { let plainData = str.data(using: .utf8) let base64String = plainData!.base64EncodedString(options: []) @@ -147,34 +282,27 @@ internal extension URLRequest { } } - mutating func set(httpAcceptCharset acceptCharset: String.Encoding) { + mutating func setValue(acceptCharset: String.Encoding, quality: Double? = nil) { let cfEncoding = CFStringConvertNSStringEncodingToEncoding(acceptCharset.rawValue) if let charsetString = CFStringConvertEncodingToIANACharSetName(cfEncoding) as String? { - self.addValue(charsetString, forHTTPHeaderField: "Accept-Charset") + if let qualityDesc = quality.flatMap({ String(format: "%.1f", min(0, max ($0, 1))) }) { + self.setValue("\(charsetString);q=\(qualityDesc)", forHTTPHeaderField: "Accept-Charset") + } else { + self.setValue(charsetString, forHTTPHeaderField: "Accept-Charset") + } } } - - mutating func set(httpAcceptCharset acceptCharset: Quality) { - self.addValue(acceptCharset.stringifed, forHTTPHeaderField: "Accept-Charset") - } - - mutating func set(httpAcceptCharsets acceptCharsets: [String.Encoding]) { - self.setValue(nil, forHTTPHeaderField: "Accept-Charset") - for charset in acceptCharsets { - let cfEncoding = CFStringConvertNSStringEncodingToEncoding(charset.rawValue) - if let charsetString = CFStringConvertEncodingToIANACharSetName(cfEncoding) as String? { + mutating func addValue(acceptCharset: String.Encoding, quality: Double? = nil) { + let cfEncoding = CFStringConvertNSStringEncodingToEncoding(acceptCharset.rawValue) + if let charsetString = CFStringConvertEncodingToIANACharSetName(cfEncoding) as String? { + if let qualityDesc = quality.flatMap({ String(format: "%.1f", min(0, max ($0, 1))) }) { + self.addValue("\(charsetString);q=\(qualityDesc)", forHTTPHeaderField: "Accept-Charset") + } else { self.addValue(charsetString, forHTTPHeaderField: "Accept-Charset") } } } - mutating func set(httpAcceptCharsets acceptCharsets: [Quality]) { - self.setValue(nil, forHTTPHeaderField: "Accept-Charset") - for charset in acceptCharsets.sorted(by: { $0.quality > $1.quality }) { - self.addValue(charset.stringifed, forHTTPHeaderField: "Accept-Charset") - } - } - enum Encoding: String { case all = "*" case identity @@ -182,53 +310,41 @@ internal extension URLRequest { case deflate } - mutating func set(httpAcceptEncoding acceptEncoding: Encoding) { - self.addValue(acceptEncoding.rawValue, forHTTPHeaderField: "Accept-Encoding") - } - - mutating func set(httpAcceptEncoding acceptEncoding: Quality) { - self.addValue(acceptEncoding.stringifed, forHTTPHeaderField: "Accept-Encoding") - } - - mutating func set(httpAcceptEncodings acceptEncodings: [Encoding]) { - self.setValue(nil, forHTTPHeaderField: "Accept-Encoding") - for encoding in acceptEncodings { - self.addValue(encoding.rawValue, forHTTPHeaderField: "Accept-Encoding") + mutating func setValue(acceptEncoding: Encoding, quality: Double? = nil) { + if let qualityDesc = quality.flatMap({ String(format: "%.1f", min(0, max ($0, 1))) }) { + self.setValue("\(acceptEncoding.rawValue);q=\(qualityDesc)", forHTTPHeaderField: "Accept-Encoding") + } else { + self.setValue(acceptEncoding.rawValue, forHTTPHeaderField: "Accept-Encoding") } } - mutating func set(httpAcceptEncodings acceptEncodings: [Quality]) { - self.setValue(nil, forHTTPHeaderField: "Accept-Encoding") - for encoding in acceptEncodings.sorted(by: { $0.quality > $1.quality }) { - self.addValue(encoding.stringifed, forHTTPHeaderField: "Accept-Encoding") + mutating func addValue(acceptEncoding: Encoding, quality: Double? = nil) { + if let qualityDesc = quality.flatMap({ String(format: "%.1f", min(0, max ($0, 1))) }) { + self.addValue("\(acceptEncoding.rawValue);q=\(qualityDesc)", forHTTPHeaderField: "Accept-Encoding") + } else { + self.addValue(acceptEncoding.rawValue, forHTTPHeaderField: "Accept-Encoding") } } - mutating func set(httpAcceptLanguage acceptLanguage: Locale) { + mutating func setValue(acceptLanguage: Locale, quality: Double? = nil) { let langCode = acceptLanguage.identifier.replacingOccurrences(of: "_", with: "-") - self.addValue(langCode, forHTTPHeaderField: "Accept-Language") - } - - mutating func set(httpAcceptLanguage acceptLanguage: Quality) { - self.addValue(acceptLanguage.stringifed, forHTTPHeaderField: "Accept-Language") - } - - mutating func set(httpAcceptLanguages acceptLanguages: [Locale]) { - self.setValue(nil, forHTTPHeaderField: "Accept-Language") - for lang in acceptLanguages { - let langCode = lang.identifier.replacingOccurrences(of: "_", with: "-") - self.addValue(langCode, forHTTPHeaderField: "Accept-Language") + if let qualityDesc = quality.flatMap({ String(format: "%.1f", min(0, max ($0, 1))) }) { + self.setValue("\(langCode);q=\(qualityDesc)", forHTTPHeaderField: "Accept-Language") + } else { + self.setValue(langCode, forHTTPHeaderField: "Accept-Language") } } - mutating func set(httpAcceptLanguages acceptLanguages: [Quality]) { - self.setValue(nil, forHTTPHeaderField: "Accept-Language") - for lang in acceptLanguages.sorted(by: { $0.quality > $1.quality} ) { - self.addValue(lang.stringifed, forHTTPHeaderField: "Accept-Language") + mutating func addValue(acceptLanguage: Locale, quality: Double? = nil) { + let langCode = acceptLanguage.identifier.replacingOccurrences(of: "_", with: "-") + if let qualityDesc = quality.flatMap({ String(format: "%.1f", min(0, max ($0, 1))) }) { + self.addValue("\(langCode);q=\(qualityDesc)", forHTTPHeaderField: "Accept-Language") + } else { + self.addValue(langCode, forHTTPHeaderField: "Accept-Language") } } - mutating func set(httpRangeWithOffset offset: Int64, length: Int) { + mutating func setValue(rangeWithOffset offset: Int64, length: Int) { if length > 0 { self.setValue("bytes=\(offset)-\(offset + Int64(length) - 1)", forHTTPHeaderField: "Range") } else if offset > 0 && length < 0 { @@ -236,7 +352,7 @@ internal extension URLRequest { } } - mutating func set(httpRange range: Range) { + mutating func setValue(range: Range) { let range = max(0, range.lowerBound).. 0 { self.setValue("bytes=\(range.lowerBound)-\(range.upperBound - 1)", forHTTPHeaderField: "Range") @@ -245,33 +361,18 @@ internal extension URLRequest { } } - struct ContentMIMEType: RawRepresentable { - public var rawValue: String - public typealias RawValue = String - - public init(rawValue: String) { - self.rawValue = rawValue + mutating func setValue(contentRange range: Range, totalBytes: Int64) { + let range = max(0, range.lowerBound).. 0 { + self.setValue("bytes \(range.lowerBound)-\(range.upperBound - 1)/\(totalBytes)", forHTTPHeaderField: "Content-Range") + } else if range.lowerBound > 0 { + self.setValue("bytes \(range.lowerBound)-/\(totalBytes)", forHTTPHeaderField: "Content-Range") + } else { + self.setValue("bytes 0-/\(totalBytes)", forHTTPHeaderField: "Content-Range") } - - static let javascript = ContentMIMEType(rawValue: "application/javascript") - static let json = ContentMIMEType(rawValue: "application/json") - static let pdf = ContentMIMEType(rawValue: "application/pdf") - static let stream = ContentMIMEType(rawValue: "application/octet-stream") - static let zip = ContentMIMEType(rawValue: "application/zip") - - // Texts - static let css = ContentMIMEType(rawValue: "text/css") - static let html = ContentMIMEType(rawValue: "text/html") - static let plainText = ContentMIMEType(rawValue: "text/plain") - static let xml = ContentMIMEType(rawValue: "text/xml") - - // Images - static let gif = ContentMIMEType(rawValue: "image/gif") - static let jpeg = ContentMIMEType(rawValue: "image/jpeg") - static let png = ContentMIMEType(rawValue: "image/png") } - mutating func set(httpContentType contentType: ContentMIMEType, charset: String.Encoding? = nil) { + mutating func setValue(contentType: ContentMIMEType, charset: String.Encoding? = nil) { var parameter = "" if let charset = charset { let cfEncoding = CFStringConvertNSStringEncodingToEncoding(charset.rawValue) @@ -283,7 +384,7 @@ internal extension URLRequest { self.setValue(contentType.rawValue + parameter, forHTTPHeaderField: "Content-Type") } - mutating func set(dropboxArgKey requestDictionary: [String: AnyObject]) { + mutating func setValue(dropboxArgKey requestDictionary: [String: Any]) { guard let jsonData = try? JSONSerialization.data(withJSONObject: requestDictionary, options: []) else { return } @@ -298,23 +399,21 @@ internal extension CharacterSet { static let filePathAllowed = CharacterSet.urlPathAllowed.subtracting(CharacterSet(charactersIn: ":")) } -internal extension Data { - internal var isPDF: Bool { +extension Data { + var isPDF: Bool { return self.count > 4 && self.scanString(length: 4, using: .ascii) == "%PDF" } - init? (jsonDictionary dictionary: [String: AnyObject]) { + init? (jsonDictionary dictionary: [String: Any]) { + guard JSONSerialization.isValidJSONObject(dictionary) else { return nil } guard let data = try? JSONSerialization.data(withJSONObject: dictionary, options: []) else { return nil } self = data } - func deserializeJSON() -> [String: AnyObject]? { - if let dic = try? JSONSerialization.jsonObject(with: self, options: []) as? [String: AnyObject] { - return dic - } - return nil + func deserializeJSON() -> [String: Any]? { + return (try? JSONSerialization.jsonObject(with: self, options: [])) as? [String: Any] } init(value: T) { @@ -324,36 +423,39 @@ internal extension Data { func scanValue() -> T? { guard MemoryLayout.size <= self.count else { return nil } + #if swift(>=5.0) + return self.withUnsafeBytes { $0.load(as: T.self) } + #else return self.withUnsafeBytes { $0.pointee } + #endif } func scanValue(start: Int) -> T? { let length = MemoryLayout.size guard self.count >= start + length else { return nil } - return self.subdata(in: start..=5.0) + return subdata.withUnsafeBytes { $0.load(as: T.self) } + #else + return subdata.withUnsafeBytes { $0.pointee } + #endif } func scanString(start: Int = 0, length: Int, using encoding: String.Encoding = .utf8) -> String? { guard self.count >= start + length else { return nil } return String(data: self.subdata(in: start..(from: T) -> U? { - guard MemoryLayout.size >= MemoryLayout.size else { return nil } - let data = Data(value: from) - return data.scanValue() - } } internal extension String { - init? (jsonDictionary: [String: AnyObject]) { + init? (jsonDictionary: [String: Any]) { guard let data = Data(jsonDictionary: jsonDictionary) else { return nil } self.init(data: data, encoding: .utf8) } - func deserializeJSON(using encoding: String.Encoding = .utf8) -> [String: AnyObject]? { + func deserializeJSON(using encoding: String.Encoding = .utf8) -> [String: Any]? { guard let data = self.data(using: encoding) else { return nil } @@ -374,17 +476,35 @@ internal extension String { } } -#if swift(>=4.0) -#else +extension NSNumber { + internal func format(precision: Int = 2, style: NumberFormatter.Style = .decimal) -> String { + let formatter = NumberFormatter() + formatter.maximumFractionDigits = precision + formatter.numberStyle = style + return formatter.string(from: self)! + } +} + extension String { - var count: Int { - return self.characters.count + internal var pathExtension: String { + return (self as NSString).pathExtension + } + + internal func appendingPathComponent(_ pathComponent: String) -> String { + return (self as NSString).appendingPathComponent(pathComponent) + } + + internal var lastPathComponent: String { + return (self as NSString).lastPathComponent + } + + internal var deletingLastPathComponent: String { + return (self as NSString).deletingLastPathComponent } } -#endif -internal extension TimeInterval { - internal var formatshort: String { +extension TimeInterval { + var formatshort: String { var result = "0:00" if self < TimeInterval(Int32.max) { result = "" @@ -409,35 +529,53 @@ internal extension TimeInterval { } } -public extension Date { +extension Date { /// Date formats used commonly in internet messaging defined by various RFCs. public enum RFCStandards: String { - /// Date format defined by usenet, commonly used in old implementations. + /// Obsolete (2-digit year) date format defined by RFC 822 for http. + case rfc822 = "EEE',' dd' 'MMM' 'yy HH':'mm':'ss z" + /// Obsolete (2-digit year) date format defined by RFC 850 for usenet. case rfc850 = "EEEE',' dd'-'MMM'-'yy HH':'mm':'ss z" - /// Date format defined by RFC 1132 for http. + /// Date format defined by RFC 1123 for http. case rfc1123 = "EEE',' dd' 'MMM' 'yyyy HH':'mm':'ss z" - /// Date format defined by ISO 8601, also defined in RFC 3339. Used by Dropbox. - case iso8601 = "yyyy'-'MM'-'dd'T'HH':'mm':'ssZ" + /// Date format defined by RFC 3339, as a profile of ISO 8601. + case rfc3339 = "yyyy'-'MM'-'dd'T'HH':'mm':'ssZZZZZ" + /// Date format defined RFC 3339 as rare case with milliseconds. + case rfc3339Extended = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'SSSZZZZZ" /// Date string returned by asctime() function. case asctime = "EEE MMM d HH':'mm':'ss yyyy" + // Defining a http alias allows changing default time format if a new RFC becomes standard. /// Equivalent to and defined by RFC 1123. public static let http = RFCStandards.rfc1123 - /// Equivalent to and defined by ISO 8610. - public static let rfc3339 = RFCStandards.iso8601 /// Equivalent to and defined by RFC 850. public static let usenet = RFCStandards.rfc850 - // Sorted by commonness - fileprivate static let allValues: [RFCStandards] = [.rfc1123, .rfc850, .iso8601, .asctime] - } + /* re. [RFC7231 section-7.1.1.1](https://tools.ietf.org/html/rfc7231#section-7.1.1.1) + "HTTP servers and client MUST accept all three HTTP-date formats" which are IMF-fixdate, + obsolete RFC 850 format and ANSI C's asctime() format. + + ISO 8601 format is common in JSON and XML fields, defined by RFC 3339 as a timestamp format. + Though not mandated, we check string against them to allow using Date(rfcString:) in + wider and more general sitations. + + We use RFC 822 instead of RFC 1123 to convert from string because NSDateFormatter can parse + both 2-digit and 4-digit year correctly when `dateFormat` year is 2-digit. + + These values are sorted by frequency. + */ + fileprivate static let parsingCases: [RFCStandards] = [.rfc822, .rfc850, .asctime, .rfc3339, .rfc3339Extended] + } + + private static let posixLocale = Locale(identifier: "en_US_POSIX") + private static let utcTimezone = TimeZone(identifier: "UTC") /// Checks date string against various RFC standards and returns `Date`. public init?(rfcString: String) { let dateFor: DateFormatter = DateFormatter() - dateFor.locale = Locale(identifier: "en_US") + dateFor.locale = Date.posixLocale - for standard in RFCStandards.allValues { + for standard in RFCStandards.parsingCases { dateFor.dateFormat = standard.rawValue if let date = dateFor.date(from: rfcString) { self = date @@ -449,15 +587,61 @@ public extension Date { } /// Formats date according to RFCs standard. - public func format(with standard: RFCStandards, locale: Locale? = nil, timeZone: TimeZone? = nil) -> String { + /// - Note: local and timezone paramters should be nil for `.http` standard + internal func format(with standard: RFCStandards, locale: Locale? = nil, timeZone: TimeZone? = nil) -> String { let fm = DateFormatter() fm.dateFormat = standard.rawValue - fm.timeZone = timeZone ?? TimeZone(identifier: "UTC") - fm.locale = locale ?? Locale(identifier: "en_US_POSIX") + fm.timeZone = timeZone ?? Date.utcTimezone + fm.locale = locale ?? Date.posixLocale return fm.string(from: self) } } +extension InputStream { + func readData(ofLength length: Int) throws -> Data { + var data = Data(count: length) + #if swift(>=5.0) + let result = data.withUnsafeMutableBytes { (buf) -> Int in + let p = buf.bindMemory(to: UInt8.self).baseAddress! + return self.read(p, maxLength: buf.count) + } + #else + let bufcount = data.count + let result = data.withUnsafeMutableBytes { (p) -> Int in + return self.read(p, maxLength: bufcount) + } + #endif + if result < 0 { + throw self.streamError ?? POSIXError(.EIO) + } else { + data.count = result + return data + } + } +} + +extension OutputStream { + func write(data: Data) throws -> Int { + #if swift(>=5.0) + let result = data.withUnsafeBytes { (buf) -> Int in + let p = buf.bindMemory(to: UInt8.self).baseAddress! + return self.write(p, maxLength: buf.count) + } + #else + let bufcount = data.count + let result = data.withUnsafeBytes { (p) -> Int in + return self.write(p, maxLength: bufcount) + } + #endif + if result < 0 { + throw self.streamError ?? POSIXError(.EIO) + } else { + return result + } + } +} + + internal extension NSPredicate { func findValue(forKey key: String?, operator op: NSComparisonPredicate.Operator? = nil) -> Any? { let val = findAllValues(forKey: key).lazy.filter { (op == nil || $0.operator == op!) && !$0.not } @@ -500,3 +684,31 @@ func hasSuffix(_ suffix: String) -> (_ value: String) -> Bool { value.hasSuffix(suffix) } } + +// Legacy Swift versions support + +#if swift(>=4.0) +#else +extension String { + var count: Int { + return self.characters.count + } +} +#endif + +#if swift(>=4.1) +#else +extension Array { + func compactMap(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult] { + return try self.flatMap(transform) + } +} + +extension ArraySlice { + func compactMap(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult] { + return try self.flatMap(transform) + } +} +#endif + + diff --git a/Sources/Extensions/HashMAC.swift b/Sources/Extensions/HashMAC.swift index 3ed325c..ca1d3ca 100644 --- a/Sources/Extensions/HashMAC.swift +++ b/Sources/Extensions/HashMAC.swift @@ -323,11 +323,19 @@ final class HMAC { } static func authenticate(message: Data, withKey key: Data) -> Data { + #if swift(>=5.0) + return Data(authenticate(message: Array(message), withKey: Array(key))) + #else return Data(bytes: authenticate(message: Array(message), withKey: Array(key))) + #endif } static func authenticate(message: String, withKey key: Data) -> Data { + #if swift(>=5.0) + return Data(authenticate(message: [UInt8](message.utf8), withKey: Array(key))) + #else return Data(bytes: authenticate(message: [UInt8](message.utf8), withKey: Array(key))) + #endif } } @@ -397,26 +405,18 @@ fileprivate func toUInt64Array(_ slice: ArraySlice) -> Array { return result } -fileprivate func arrayOfBytes(_ value:T, length:Int? = nil) -> [UInt8] { - let totalBytes = length ?? MemoryLayout.size - - let valuePointer = UnsafeMutablePointer.allocate(capacity: 1) - - valuePointer.pointee = value - - let bytesPointer = UnsafeMutableRawPointer(valuePointer).assumingMemoryBound(to: UInt8.self) - var bytes = [UInt8](repeating: 0, count: totalBytes) - for j in 0...size,totalBytes) { - bytes[totalBytes - 1 - j] = (bytesPointer + j).pointee +fileprivate func arrayOfBytes(_ value: T, length: Int? = nil) -> [UInt8] { + var value = value + return Swift.withUnsafeBytes(of: &value) { (buffer: UnsafeRawBufferPointer) -> [UInt8] in + if let length = length { + return Array(buffer.prefix(length)) + } else { + return Array(buffer) + } } - - valuePointer.deinitialize() - valuePointer.deallocate(capacity: 1) - - return bytes } -public extension String { +extension String { public func fp_sha256() -> [UInt8] { return SHA2.calculate([UInt8](self.utf8)) } @@ -430,7 +430,7 @@ public extension String { } } -public extension Data { +extension Data { public func fp_sha256() -> [UInt8] { return SHA2.calculate(Array(self)) } diff --git a/Sources/FPSStreamTask.swift b/Sources/FPSStreamTask.swift index d1923a0..6d04924 100644 --- a/Sources/FPSStreamTask.swift +++ b/Sources/FPSStreamTask.swift @@ -8,7 +8,24 @@ import Foundation -private var lasttaskIdAssociated = 1_000_000_000 +private var _lasttaskIdAssociated = 1_000_000_000 +let _lasttaskIdAssociated_lock: NSLock = NSLock() +var lasttaskIdAssociated: Int { + get { + _lasttaskIdAssociated_lock.lock() + defer { + _lasttaskIdAssociated_lock.unlock() + } + return _lasttaskIdAssociated + } + set { + _lasttaskIdAssociated_lock.lock() + defer { + _lasttaskIdAssociated_lock.unlock() + } + _lasttaskIdAssociated = newValue + } +} // codebeat:disable[TOTAL_LOC,TOO_MANY_IVARS] /// This class is a replica of NSURLSessionStreamTask with same api for iOS 7/8 @@ -17,14 +34,30 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { fileprivate var inputStream: InputStream? fileprivate var outputStream: OutputStream? - fileprivate var operation_queue: OperationQueue! + fileprivate let dataToBeSentLock = NSLock() + fileprivate var dataToBeSent: Data = Data() + fileprivate let dataReceivedLock = NSLock() + fileprivate var dataReceived: Data = Data() + + fileprivate var _error: Error? = nil + fileprivate var isSecure = false + public var securityLevel: StreamSocketSecurityLevel = .negotiatedSSL + + fileprivate var dispatch_queue: DispatchQueue! internal var _underlyingSession: URLSession + fileprivate var host: (hostname: String, port: Int)? + fileprivate var service: NetService? fileprivate var streamDelegate: FPSStreamDelegate? { return (_underlyingSession.delegate as? FPSStreamDelegate) } + + internal static let defaultUseURLSession = false + fileprivate var _taskIdentifier: Int fileprivate var _taskDescription: String? + var observers: [(keyPath: String, observer: NSObject, context: UnsafeMutableRawPointer?)] = [] + /// Force using `URLSessionStreamTask` for iOS 9 and later public let useURLSession: Bool @available(iOS 9.0, macOS 10.11, *) @@ -35,10 +68,21 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { return FileProviderStreamTask.streamTasks[_taskIdentifier] } + /// The end of the stream has been reached. + private var endEncountered: Bool = false + + /// Trust all certificates if `disableEvaluation`, Otherwise validate certificate chain. + public var serverTrustPolicy: ServerTrustPolicy = .performDefaultEvaluation(validateHost: true) + + /// A pointer to a buffer containing the peer ID data to set. + var peerID: UnsafeRawPointer? = nil + /// The length of the peer ID data buffer. + var peerIDLen: Int = 0 + /** * An identifier uniquely identifies the task within a given session. * - * This value is unique only within the context of a single session; + * This value is unique only within the context of a single session; * tasks in other sessions may have the same `taskIdentifier` value. */ open override var taskIdentifier: Int { @@ -80,9 +124,9 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { fileprivate var _state: URLSessionTask.State = .suspended /** - * The current state of the task—active, suspended, in the process + * The current state of the task—active, suspended, in the process * of being canceled, or completed. - */ + */ override open var state: URLSessionTask.State { if #available(iOS 9.0, macOS 10.11, *) { if self.useURLSession { @@ -97,7 +141,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { * The original request object passed when the task was created. * This value is typically the same as the currently active request (`currentRequest`) * except when the server has responded to the initial request with a redirect to a different URL. - */ + */ override open var originalRequest: URLRequest? { if #available(iOS 9.0, macOS 10.11, *) { if self.useURLSession { @@ -112,7 +156,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { * The URL request object currently being handled by the task. * This value is typically the same as the initial request (`originalRequest`) * except when the server has responded to the initial request with a redirect to a different URL. - */ + */ override open var currentRequest: URLRequest? { if #available(iOS 9.0, macOS 10.11, *) { return _underlyingTask!.currentRequest @@ -152,9 +196,9 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { * * This byte count includes only the length of the request body itself, not the request headers. * - * To be notified when this value changes, implement the + * To be notified when this value changes, implement the * `urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)` delegate method. - */ + */ override open var countOfBytesSent: Int64 { if #available(iOS 9.0, macOS 10.11, *) { if self.useURLSession { @@ -162,6 +206,10 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { } } + self.dataToBeSentLock.lock() + defer { + self.dataToBeSentLock.unlock() + } return _countOfBytesSent } @@ -170,7 +218,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { * * To be notified when this value changes, implement the `urlSession(_:dataTask:didReceive:)` delegate method (for data and upload tasks) * or the `urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)` method (for download tasks). - */ + */ override open var countOfBytesReceived: Int64 { if #available(iOS 9.0, macOS 10.11, *) { if self.useURLSession { @@ -178,6 +226,10 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { } } + dataReceivedLock.lock() + defer { + dataReceivedLock.unlock() + } return _countOfBytesRecieved } @@ -190,13 +242,19 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { * - From the `Content-Length` in the request object, if you explicitly set it. * * Otherwise, the value is `NSURLSessionTransferSizeUnknown` (`-1`) if you provided a stream or body data object, or zero (`0`) if you did not. - */ + */ override open var countOfBytesExpectedToSend: Int64 { if #available(iOS 9.0, macOS 10.11, *) { - return _underlyingTask!.countOfBytesExpectedToSend - } else { - return Int64(dataToBeSent.count) + if self.useURLSession { + return _underlyingTask!.countOfBytesExpectedToSend + } } + + self.dataToBeSentLock.lock() + defer { + self.dataToBeSentLock.unlock() + } + return Int64(dataToBeSent.count) + _countOfBytesSent } /** @@ -204,7 +262,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { * * This value is determined based on the `Content-Length` header received from the server. * If that header is absent, the value is `NSURLSessionTransferSizeUnknown`. - */ + */ override open var countOfBytesExpectedToReceive: Int64 { if #available(iOS 9.0, macOS 10.11, *) { if self.useURLSession { @@ -212,60 +270,18 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { } } - return Int64(dataReceived.count) - } - - var observers: [(keyPath: String, observer: NSObject, context: UnsafeMutableRawPointer?)] = [] - - public override func addObserver(_ observer: NSObject, forKeyPath keyPath: String, options: NSKeyValueObservingOptions = [], context: UnsafeMutableRawPointer?) { - if #available(iOS 9.0, macOS 10.11, *) { - if self.useURLSession { - self._underlyingTask?.addObserver(observer, forKeyPath: keyPath, options: options, context: context) - return - } + dataReceivedLock.lock() + defer { + dataReceivedLock.unlock() } - - switch keyPath { - case #keyPath(countOfBytesSent): - fallthrough - case #keyPath(countOfBytesReceived): - fallthrough - case #keyPath(countOfBytesExpectedToSend): - fallthrough - case #keyPath(countOfBytesExpectedToReceive): - observers.append((keyPath: keyPath, observer: observer, context: context)) - default: - break - } - super.addObserver(observer, forKeyPath: keyPath, options: options, context: context) - } - - public override func removeObserver(_ observer: NSObject, forKeyPath keyPath: String) { - var newObservers: [(keyPath: String, observer: NSObject, context: UnsafeMutableRawPointer?)] = [] - for observer in observers where observer.keyPath != keyPath { - newObservers.append(observer) - } - self.observers = newObservers - super.removeObserver(observer, forKeyPath: keyPath) - } - - public override func removeObserver(_ observer: NSObject, forKeyPath keyPath: String, context: UnsafeMutableRawPointer?) { - var newObservers: [(keyPath: String, observer: NSObject, context: UnsafeMutableRawPointer?)] = [] - for observer in observers where observer.keyPath != keyPath || observer.context != context { - newObservers.append(observer) - } - self.observers = newObservers - super.removeObserver(observer, forKeyPath: keyPath, context: context) + return Int64(dataReceived.count) } override public init() { fatalError("Use NSURLSession.fpstreamTask() method") } - fileprivate var host: (hostname: String, port: Int)? - fileprivate var service: NetService? - - internal init(session: URLSession, host: String, port: Int, useURLSession: Bool = true) { + internal init(session: URLSession, host: String, port: Int, useURLSession: Bool = defaultUseURLSession) { self._underlyingSession = session self.useURLSession = useURLSession if #available(iOS 9.0, macOS 10.11, *) { @@ -280,12 +296,11 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { lasttaskIdAssociated += 1 self._taskIdentifier = lasttaskIdAssociated self.host = (host, port) - self.operation_queue = OperationQueue() - self.operation_queue.name = "FileProviderStreamTask" - self.operation_queue.maxConcurrentOperationCount = 1 + self.dispatch_queue = DispatchQueue(label: "FileProviderStreamTask") + self.dispatch_queue.suspend() } - internal init(session: URLSession, netService: NetService, useURLSession: Bool = true) { + internal init(session: URLSession, netService: NetService, useURLSession: Bool = defaultUseURLSession) { self._underlyingSession = session self.useURLSession = useURLSession if #available(iOS 9.0, macOS 10.11, *) { @@ -300,21 +315,41 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { lasttaskIdAssociated += 1 self._taskIdentifier = lasttaskIdAssociated self.service = netService - self.operation_queue = OperationQueue() - self.operation_queue.name = "FileProviderStreamTask" - self.operation_queue.maxConcurrentOperationCount = 1 + self.dispatch_queue = DispatchQueue(label: "FileProviderStreamTask") + self.dispatch_queue.suspend() + } + + deinit { + if !self.useURLSession { + finish() + } + } + + /** + * An error object that indicates why the task failed. + * + * This value is `NULL` if the task is still active or if the transfer completed successfully. + */ + override open var error: Error? { + if #available(iOS 9.0, macOS 10.11, *) { + if useURLSession { + return _underlyingTask!.error + } + } + + return _error } /** * Cancels the task. * - * This method returns immediately, marking the task as being canceled. Once a task is marked as being canceled, + * This method returns immediately, marking the task as being canceled. Once a task is marked as being canceled, * `urlSession(_:task:didCompleteWithError:)` will be sent to the task delegate, passing an error * in the domain NSURLErrorDomain with the code `NSURLErrorCancelled`. A task may, under some circumstances, * send messages to its delegate before the cancelation is acknowledged. * * This method may be called on a task that is suspended. - */ + */ override open func cancel() { if #available(iOS 9.0, macOS 10.11, *) { if self.useURLSession { @@ -324,50 +359,23 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { } self._state = .canceling - inputStream?.setValue(kCFBooleanTrue, forKey: kCFStreamPropertyShouldCloseNativeSocket as String) - outputStream?.setValue(kCFBooleanTrue, forKey: kCFStreamPropertyShouldCloseNativeSocket as String) - - self.inputStream?.close() - self.outputStream?.close() - self.inputStream?.remove(from: RunLoop.main, forMode: .defaultRunLoopMode) - self.outputStream?.remove(from: RunLoop.main, forMode: .defaultRunLoopMode) - - self.inputStream?.delegate = nil - self.outputStream?.delegate = nil - - self.inputStream = nil - self.outputStream = nil - - self._state = .completed - self._countOfBytesSent = 0 - self._countOfBytesRecieved = 0 - } - - var _error: Error? = nil - - /** - * An error object that indicates why the task failed. - * - * This value is `NULL` if the task is still active or if the transfer completed successfully. - */ - override open var error: Error? { - if #available(iOS 9.0, macOS 10.11, *) { - if useURLSession { - return _underlyingTask!.error - } + dispatch_queue.async { + self.finish() + + self._state = .completed + self._countOfBytesSent = 0 + self._countOfBytesRecieved = 0 } - - return _error } /** * Temporarily suspends a task. * - * A task, while suspended, produces no network traffic and is not subject to timeouts. - * A download task can continue transferring data at a later time. + * A task, while suspended, produces no network traffic and is not subject to timeouts. + * A download task can continue transferring data at a later time. * All other tasks must start over when resumed. - */ + */ override open func suspend() { if #available(iOS 9.0, macOS 10.11, *) { if self.useURLSession { @@ -376,8 +384,10 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { } } - self._state = .suspended - self.operation_queue.isSuspended = true + if self._state == .running { + self._state = .suspended + self.dispatch_queue.suspend() + } } // Resumes the task, if it is suspended. @@ -389,19 +399,19 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { } } - var readStream : Unmanaged? - var writeStream : Unmanaged? - if inputStream == nil || outputStream == nil { if let host = host { - CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, host.hostname as CFString, UInt32(host.port), &readStream, &writeStream) + Stream.getStreamsToHost(withName: host.hostname, port: host.port, inputStream: &self.inputStream, outputStream: &self.outputStream) } else if let service = service { + var readStream : Unmanaged? + var writeStream : Unmanaged? let cfnetService = CFNetServiceCreate(kCFAllocatorDefault, service.domain as CFString, service.type as CFString, service.name as CFString, Int32(service.port)) CFStreamCreatePairWithSocketToNetService(kCFAllocatorDefault, cfnetService.takeRetainedValue(), &readStream, &writeStream) + inputStream = readStream?.takeRetainedValue() + outputStream = writeStream?.takeRetainedValue() } - inputStream = readStream?.takeRetainedValue() - outputStream = writeStream?.takeRetainedValue() + guard let inputStream = inputStream, let outputStream = outputStream else { return } @@ -412,24 +422,40 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { return } + if isSecure { + inputStream.setProperty(securityLevel.rawValue, forKey: .socketSecurityLevelKey) + outputStream.setProperty(securityLevel.rawValue, forKey: .socketSecurityLevelKey) + + if serverTrustPolicy.evaluate() { + // ※ Called, After setProperty securityLevel + addTrustAllCertificatesSettings() + } + + inputStream.setSSLPeerID(peerID: peerID, peerIDLen: peerIDLen) + } else { + inputStream.setProperty(StreamSocketSecurityLevel.none.rawValue, forKey: .socketSecurityLevelKey) + outputStream.setProperty(StreamSocketSecurityLevel.none.rawValue, forKey: .socketSecurityLevelKey) + } + inputStream.delegate = self outputStream.delegate = self - operation_queue.addOperation { - inputStream.schedule(in: RunLoop.main, forMode: .defaultRunLoopMode) - outputStream.schedule(in: RunLoop.main, forMode: .defaultRunLoopMode) - } + #if swift(>=4.2) + inputStream.schedule(in: RunLoop.main, forMode: .common) + #else + inputStream.schedule(in: RunLoop.main, forMode: .commonModes) + #endif + //outputStream.schedule(in: RunLoop.main, forMode: .init("kCFRunLoopDefaultMode")) inputStream.open() outputStream.open() - operation_queue.isSuspended = false - _state = .running + if self._state == .suspended { + dispatch_queue.resume() + _state = .running + } } - fileprivate var dataToBeSent: Data = Data() - fileprivate var dataReceived: Data = Data() - /** * Asynchronously reads a number of bytes from the stream, and calls a handler upon completion. * @@ -442,7 +468,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { * - Parameter data: The data read from the stream. * - Parameter atEOF: Whether or not the stream reached end-of-file (`EOF`), such that no more data can be read. * - Parameter error: An error object that indicates why the read failed, or `nil` if the read was successful. - */ + */ open func readData(ofMinLength minBytes: Int, maxLength maxBytes: Int, timeout: TimeInterval, completionHandler: @escaping (_ data: Data?, _ atEOF: Bool, _ error :Error?) -> Void) { if #available(iOS 9.0, macOS 10.11, *) { if self.useURLSession { @@ -456,25 +482,34 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { } let expireDate = Date(timeIntervalSinceNow: timeout) - operation_queue.addOperation { + dispatch_queue.async { var timedOut: Bool = false - while (self.dataReceived.count == 0 || self.dataReceived.count < minBytes) && !timedOut { + self.dataReceivedLock.lock() + while (self.dataReceived.count == 0 || self.dataReceived.count < minBytes) && !timedOut && !self.endEncountered { + self.dataReceivedLock.unlock() Thread.sleep(forTimeInterval: 0.1) + if let error = inputStream.streamError { + completionHandler(nil, inputStream.streamStatus == .atEnd, error) + return + } timedOut = expireDate < Date() + self.dataReceivedLock.lock() } + self.endEncountered = false var dR: Data? if self.dataReceived.count > maxBytes { let range: Range = 0.. 0 { dR = self.dataReceived - self.dataReceived.count = 0 + self.dataReceived.removeAll(keepingCapacity: false) } } let isEOF = inputStream.streamStatus == .atEnd && self.dataReceived.count == 0 - completionHandler(dR, isEOF, dR == nil ? inputStream.streamError : nil) + self.dataReceivedLock.unlock() + completionHandler(dR, isEOF, dR == nil ? (timedOut ? URLError(.timedOut) : inputStream.streamError) : nil) } } @@ -492,7 +527,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { * This handler is executed on the delegate queue. * This completion handler takes the following parameter: * - Parameter error: An error object that indicates why the write failed, or `nil` if the write was successful. - */ + */ open func write(_ data: Data, timeout: TimeInterval, completionHandler: @escaping (_ error: Error?) -> Void) { if #available(iOS 9.0, macOS 10.11, *) { if self.useURLSession { @@ -502,14 +537,21 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { } guard outputStream != nil else { + if self.state == .canceling || self.state == .completed { + completionHandler(URLError(.cancelled)) + } return } - operation_queue.addOperation { - self.dataToBeSent.append(data) + self.dataToBeSentLock.lock() + self.dataToBeSent.append(data) + self.dataToBeSentLock.unlock() + + dispatch_queue.async { let result = self.write(timeout: timeout, close: false) if result < 0 { - let error = self.outputStream?.streamError ?? URLError(.cannotWriteToFile) + let error = self.outputStream?.streamError ?? + URLError((self.state == .canceling || self.state == .completed) ? .cancelled : .cannotWriteToFile) completionHandler(error) } else { completionHandler(nil) @@ -518,9 +560,9 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { } /** - * Completes any already enqueued reads and writes, and then invokes the + * Completes any already enqueued reads and writes, and then invokes the * `urlSession(_:streamTask:didBecome:outputStream:)` delegate message. - */ + */ open func captureStreams() { if #available(iOS 9.0, macOS 10.11, *) { if self.useURLSession { @@ -532,9 +574,9 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { guard let outputStream = outputStream, let inputStream = inputStream else { return } - self.operation_queue.addOperation { + dispatch_queue.async { _=self.write(close: false) - while inputStream.streamStatus != .atEnd || outputStream.streamStatus == .writing { + while outputStream.streamStatus == .writing { Thread.sleep(forTimeInterval: 0.1) } self.streamDelegate?.urlSession?(self._underlyingSession, streamTask: self, didBecome: inputStream, outputStream: outputStream) @@ -545,12 +587,12 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { * Completes any enqueued reads and writes, and then closes the write side of the underlying socket. * * You may continue to read data using the `readData(ofMinLength:maxLength:timeout:completionHandler:)` - * method after calling this method. Any calls to `write(_:timeout:completionHandler:)` after calling + * method after calling this method. Any calls to `write(_:timeout:completionHandler:)` after calling * this method will result in an error. * - * Because the server may continue to write bytes to the client, it is recommended that + * Because the server may continue to write bytes to the client, it is recommended that * you continue reading until the stream reaches end-of-file (EOF). - */ + */ open func closeWrite() { if #available(iOS 9.0, macOS 10.11, *) { if self.useURLSession { @@ -559,51 +601,34 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate { } } - operation_queue.addOperation { + dispatch_queue.async { _ = self.write(close: true) } } - fileprivate func write(timeout: TimeInterval = 0, close: Bool) -> Int { - guard let outputStream = outputStream else { - return -1 - } - - var byteSent: Int = 0 - let expireDate = Date(timeIntervalSinceNow: timeout) - while self.dataToBeSent.count > 0 && (timeout == 0 || expireDate > Date()) { - let bytesWritten = self.dataToBeSent.withUnsafeBytes { - outputStream.write($0, maxLength: self.dataToBeSent.count) - } - - if bytesWritten > 0 { - let range = 0..=5.0) + let sslSettings: [NSString: Any] = [ + NSString(format: kCFStreamSSLValidatesCertificateChain): kCFBooleanFalse!, + NSString(format: kCFStreamSSLPeerName): kCFNull! + ] + #else + let sslSettings: [NSString: Any] = [ + NSString(format: kCFStreamSSLValidatesCertificateChain): kCFBooleanFalse, + NSString(format: kCFStreamSSLPeerName): kCFNull + ] + #endif + // Set custom SSL/TLS settings + inputStream?.setProperty(sslSettings, forKey: kCFStreamPropertySSLSettings as Stream.PropertyKey) + outputStream?.setProperty(sslSettings, forKey: kCFStreamPropertySSLSettings as Stream.PropertyKey) + } + + /** + * Reuse SSL/TLS session used for the SSL/TLS session cache. + * + * - Parameter task: The task authenticated connection FTP over SSL/TLS. + */ + func reuseSSLSession(task: FileProviderStreamTask) { + let sslPeerID = task.inputStream?.getSSLPeerID() + peerID = sslPeerID?.peerID + peerIDLen = sslPeerID?.peerIDLen ?? 0 + } +} + +extension Stream { + /** + * Retrieves the current peer ID data. + * + * - Returns: + * - peerID: On return, points to a buffer containing the peer ID data. + * - peerIDLen: On return, the length of the peer ID data buffer. + */ + public func getSSLPeerID() -> (peerID: UnsafeRawPointer?, peerIDLen: Int) { + var peerID: UnsafeRawPointer? = nil + var peerIDLen: Int = 0 + + if let sslContext = self.property(forKey: kCFStreamPropertySSLContext as Stream.PropertyKey) { + let _ = SSLGetPeerID(sslContext as! SSLContext, UnsafeMutablePointer(&peerID), UnsafeMutablePointer(&peerIDLen)) + } + + return (peerID, peerIDLen) + } + + /** + * Specifies data that is sufficient to uniquely identify the peer of the current session. + * This peerID is used for the TLS session cache. + * + * - Parameter peerID: A pointer to a buffer containing the peer ID data to set. + * - Parameter peerIDLen: The length of the peer ID data buffer. + */ + fileprivate func setSSLPeerID(peerID: UnsafeRawPointer?, peerIDLen: Int) { + guard peerID != nil, peerIDLen > 0 else { + return + } + + if let sslContext = self.property(forKey: kCFStreamPropertySSLContext as Stream.PropertyKey) { + let _ = SSLSetPeerID(sslContext as! SSLContext, peerID, peerIDLen) + } + } +} + +extension FileProviderStreamTask { open func stream(_ aStream: Stream, handle eventCode: Stream.Event) { if eventCode.contains(.errorOccurred) { self._error = aStream.streamError streamDelegate?.urlSession?(_underlyingSession, task: self, didCompleteWithError: error) } + if aStream == inputStream && eventCode.contains(.endEncountered) { + self.endEncountered = true + } + if aStream == inputStream && eventCode.contains(.hasBytesAvailable) { while (inputStream!.hasBytesAvailable) { var buffer = [UInt8](repeating: 0, count: 2048) let len = inputStream!.read(&buffer, maxLength: buffer.count) if len > 0 { + dataReceivedLock.lock() dataReceived.append(&buffer, count: len) + dataReceivedLock.unlock() self._countOfBytesRecieved += Int64(len) } } } } + + fileprivate func write(timeout: TimeInterval = 0, close: Bool) -> Int { + guard let outputStream = outputStream else { + return -1 + } + + var byteSent: Int = 0 + let expireDate = Date(timeIntervalSinceNow: timeout) + self.dataToBeSentLock.lock() + defer { + self.dataToBeSentLock.unlock() + } + while self.dataToBeSent.count > 0 && (timeout == 0 || expireDate > Date()) { + if let _ = outputStream.streamError { + return byteSent == 0 ? -1 : byteSent + } + + let bytesWritten: Int = (try? outputStream.write(data: self.dataToBeSent)) ?? -1 + + if bytesWritten > 0 { + self.dataToBeSent.removeFirst(bytesWritten) + byteSent += bytesWritten + self._countOfBytesSent += Int64(bytesWritten) + } else if bytesWritten < 0 { + self._error = outputStream.streamError + return bytesWritten + } + if self.dataToBeSent.count == 0 { + break + } + } + + if close { + DispatchQueue.main.sync { + outputStream.close() + shouldCloseWrite = true + } + self.streamDelegate?.urlSession?(self._underlyingSession, writeClosedFor: self) + } + return byteSent + } + + fileprivate func finish() { + peerID = nil + + inputStream?.delegate = nil + outputStream?.delegate = nil + + inputStream?.close() + #if swift(>=4.2) + inputStream?.remove(from: RunLoop.main, forMode: .common) + #else + inputStream?.remove(from: RunLoop.main, forMode: .commonModes) + #endif + inputStream = nil + + outputStream?.close() + #if swift(>=4.2) + outputStream?.remove(from: RunLoop.main, forMode: .common) + #else + outputStream?.remove(from: RunLoop.main, forMode: .commonModes) + #endif + outputStream = nil + } + +} + +extension FileProviderStreamTask { + public override func addObserver(_ observer: NSObject, forKeyPath keyPath: String, options: NSKeyValueObservingOptions = [], context: UnsafeMutableRawPointer?) { + if #available(iOS 9.0, macOS 10.11, *) { + if self.useURLSession { + self._underlyingTask?.addObserver(observer, forKeyPath: keyPath, options: options, context: context) + return + } + } + + switch keyPath { + case #keyPath(countOfBytesSent): + fallthrough + case #keyPath(countOfBytesReceived): + fallthrough + case #keyPath(countOfBytesExpectedToSend): + fallthrough + case #keyPath(countOfBytesExpectedToReceive): + observers.append((keyPath: keyPath, observer: observer, context: context)) + default: + break + } + super.addObserver(observer, forKeyPath: keyPath, options: options, context: context) + } + + public override func removeObserver(_ observer: NSObject, forKeyPath keyPath: String) { + var newObservers: [(keyPath: String, observer: NSObject, context: UnsafeMutableRawPointer?)] = [] + for observer in observers where observer.keyPath != keyPath { + newObservers.append(observer) + } + self.observers = newObservers + super.removeObserver(observer, forKeyPath: keyPath) + } + + public override func removeObserver(_ observer: NSObject, forKeyPath keyPath: String, context: UnsafeMutableRawPointer?) { + var newObservers: [(keyPath: String, observer: NSObject, context: UnsafeMutableRawPointer?)] = [] + for observer in observers where observer.keyPath != keyPath || observer.context != context { + newObservers.append(observer) + } + self.observers = newObservers + super.removeObserver(observer, forKeyPath: keyPath, context: context) + } } public extension URLSession { @@ -687,7 +939,7 @@ public extension URLSession { /** * Creates a bidirectional stream task with an NSNetService to identify the endpoint. * The NSNetService will be resolved before any IO completes. - */ + */ func fpstreamTask(withNetService service: NetService) -> FileProviderStreamTask { return FileProviderStreamTask(session: self, netService: service) } @@ -740,4 +992,4 @@ internal protocol FPSStreamDelegate : URLSessionTaskDelegate { private let ports: [String: Int] = ["http": 80, "https": 443, "smb": 445,"ftp": 21, "telnet": 23, "pop": 110, "smtp": 25, "imap": 143] private let securePorts: [String: Int] = ["ssh": 22, "https": 443, "smb": 445, "smtp": 465, - "ftps": 990,"telnet": 992, "imap": 993, "pop": 995] + "ftps": 990,"telnet": 992, "imap": 993, "pop": 995] diff --git a/Sources/FTPFileProvider.swift b/Sources/FTPFileProvider.swift index 962c532..2c5f200 100644 --- a/Sources/FTPFileProvider.swift +++ b/Sources/FTPFileProvider.swift @@ -12,12 +12,22 @@ import Foundation Allows accessing to FTP files and directories. This provider doesn't cache or save files internally. It's a complete reimplementation and doesn't use CFNetwork deprecated API. */ -open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, FileProviderReadWrite { +open class FTPFileProvider: NSObject, FileProviderBasicRemote, FileProviderOperations, FileProviderReadWrite, FileProviderReadWriteProgressive { + + /// FTP data connection mode. + public enum Mode: String { + /// Passive mode for FTP and Extended Passive mode for FTP over TLS. + case `default` + /// Data connection would establish by client to determined server host/port. + case passive + /// Data connection would establish by server to determined client's port. + case active + /// Data connection would establish by client to determined server host/port, with IPv6 support. (RFC 2428) + case extendedPassive + } + open class var type: String { return "FTP" } - open let baseURL: URL? - /// **OBSOLETED** Current active path used in `contentsOfDirectory(path:completionHandler:)` method. - @available(*, obsoleted: 0.22, message: "This property is redundant with almost no use internally.") - open var currentPath: String = "" + public let baseURL: URL? open var dispatch_queue: DispatchQueue open var operation_queue: OperationQueue { @@ -37,10 +47,7 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil public var validatingCache: Bool /// Determine either FTP session is in passive or active mode. - public let passiveMode: Bool - - /// Force to use URLSessionDownloadTask/URLSessionDataTask when possible - public var useAppleImplementation = true + public let mode: Mode fileprivate var _session: URLSession! internal var sessionDelegate: SessionDelegate? @@ -67,49 +74,86 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil } } + #if os(macOS) || os(iOS) || os(tvOS) + open var undoManager: UndoManager? = nil + #endif + /** Initializer for FTP provider with given username and password. - Note: `passive` value should be set according to server settings and firewall presence. - Parameter baseURL: a url with `ftp://hostaddress/` format. - - Parameter passive: FTP server data connection, `true` means passive connection (data connection created by client) - and `false` means active connection (data connection created by server). Default is `true` (passive mode). + - Parameter mode: FTP server data connection type. - Parameter credential: a `URLCredential` object contains user and password. - Parameter cache: A URLCache to cache downloaded files and contents. (unimplemented for FTP and should be nil) + + - Important: Extended Passive or Active modes will fallback to normal Passive or Active modes if your server + does not support extended modes. */ - public init? (baseURL: URL, passive: Bool = true, credential: URLCredential? = nil, cache: URLCache? = nil) { - guard (baseURL.scheme ?? "ftp").lowercased().hasPrefix("ftp") else { return nil } + public init? (baseURL: URL, mode: Mode = .default, credential: URLCredential? = nil, cache: URLCache? = nil) { + guard ["ftp", "ftps", "ftpes"].contains(baseURL.uw_scheme.lowercased()) else { + return nil + } guard baseURL.host != nil else { return nil } var urlComponents = URLComponents(url: baseURL, resolvingAgainstBaseURL: true)! - let defaultPort: Int = baseURL.scheme == "ftps" ? 990 : 21 + let defaultPort: Int = baseURL.scheme?.lowercased() == "ftps" ? 990 : 21 urlComponents.port = urlComponents.port ?? defaultPort urlComponents.scheme = urlComponents.scheme ?? "ftp" urlComponents.path = urlComponents.path.hasSuffix("/") ? urlComponents.path : urlComponents.path + "/" self.baseURL = urlComponents.url!.absoluteURL - self.passiveMode = passive + self.mode = mode self.useCache = false self.validatingCache = true self.cache = cache self.credential = credential + self.supportsRFC3659 = true - #if swift(>=3.1) let queueLabel = "FileProvider.\(Swift.type(of: self).type)" - #else - let queueLabel = "FileProvider.\(type(of: self).type)" - #endif dispatch_queue = DispatchQueue(label: queueLabel, attributes: .concurrent) operation_queue = OperationQueue() operation_queue.name = "\(queueLabel).Operation" + + super.init() + } + + /** + **DEPRECATED** Initializer for FTP provider with given username and password. + + - Note: `passive` value should be set according to server settings and firewall presence. + + - Parameter baseURL: a url with `ftp://hostaddress/` format. + - Parameter passive: FTP server data connection, `true` means passive connection (data connection created by client) + and `false` means active connection (data connection created by server). Default is `true` (passive mode). + - Parameter credential: a `URLCredential` object contains user and password. + - Parameter cache: A URLCache to cache downloaded files and contents. (unimplemented for FTP and should be nil) + */ + @available(*, deprecated, renamed: "init(baseURL:mode:credential:cache:)") + public convenience init? (baseURL: URL, passive: Bool, credential: URLCredential? = nil, cache: URLCache? = nil) { + self.init(baseURL: baseURL, mode: passive ? .passive : .active, credential: credential, cache: cache) } public required convenience init?(coder aDecoder: NSCoder) { - guard let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL else { return nil } - self.init(baseURL: baseURL, passive: aDecoder.decodeBool(forKey: "passiveMode"), credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential) - self.useCache = aDecoder.decodeBool(forKey: "useCache") - self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache") - self.useAppleImplementation = aDecoder.decodeBool(forKey: "useAppleImplementation") + guard let baseURL = aDecoder.decodeObject(of: NSURL.self, forKey: "baseURL") as URL? else { + if #available(macOS 10.11, iOS 9.0, tvOS 9.0, *) { + aDecoder.failWithError(CocoaError(.coderValueNotFound, + userInfo: [NSLocalizedDescriptionKey: "Base URL is not set."])) + } + return nil + } + let mode: Mode + if let modeStr = aDecoder.decodeObject(of: NSString.self, forKey: "mode") as String?, let mode_v = Mode(rawValue: modeStr) { + mode = mode_v + } else { + let passiveMode = aDecoder.decodeBool(forKey: "passiveMode") + mode = passiveMode ? .passive : .active + } + self.init(baseURL: baseURL, mode: mode, credential: aDecoder.decodeObject(of: URLCredential.self, forKey: "credential")) + self.useCache = aDecoder.decodeBool(forKey: "useCache") + self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache") + self.supportsRFC3659 = aDecoder.decodeBool(forKey: "supportsRFC3659") + self.securedDataConnection = aDecoder.decodeBool(forKey: "securedDataConnection") } public func encode(with aCoder: NSCoder) { @@ -117,8 +161,9 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil aCoder.encode(self.credential, forKey: "credential") aCoder.encode(self.useCache, forKey: "useCache") aCoder.encode(self.validatingCache, forKey: "validatingCache") - aCoder.encode(self.useAppleImplementation, forKey: "useAppleImplementation") - aCoder.encode(self.passiveMode, forKey: "passiveMode") + aCoder.encode(self.mode.rawValue, forKey: "mode") + aCoder.encode(self.supportsRFC3659, forKey: "supportsRFC3659") + aCoder.encode(self.securedDataConnection, forKey: "securedDataConnection") } public static var supportsSecureCoding: Bool { @@ -126,12 +171,13 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil } open func copy(with zone: NSZone? = nil) -> Any { - let copy = FTPFileProvider(baseURL: self.baseURL!, credential: self.credential, cache: self.cache)! + let copy = FTPFileProvider(baseURL: self.baseURL!, mode: self.mode, credential: self.credential, cache: self.cache)! copy.delegate = self.delegate copy.fileOperationDelegate = self.fileOperationDelegate copy.useCache = self.useCache copy.validatingCache = self.validatingCache - copy.useAppleImplementation = self.useAppleImplementation + copy.securedDataConnection = self.securedDataConnection + copy.supportsRFC3659 = self.supportsRFC3659 return copy } @@ -147,10 +193,33 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil } } - internal var serverSupportsRFC3659: Bool = true + internal var supportsRFC3659: Bool + + /** + Uploads files in chunk if `true`, Otherwise It will uploads entire file/data as single stream. + + - Note: Due to an internal bug in `NSURLSessionStreamTask`, it must be `true` when using Apple's stream task, + otherwise it will occasionally throw `Assertion failed: (_writeBufferAlreadyWrittenForNextWrite == 0)` + fatal error. My implementation of `FileProviderStreamTask` doesn't have this bug. + + - Note: Disabling this option will increase upload speed. + */ + public var uploadByREST: Bool = false + + /** + Determines data connection must TLS or not. `false` value indicates to use `PROT C` and + `true` value indicates to use `PROT P`. Default is `true`. + */ + public var securedDataConnection: Bool = true + + /** + Trust all certificates if `disableEvaluation`, Otherwise validate certificate chain. + Default is `performDefaultEvaluation`. + */ + public var serverTrustPolicy: ServerTrustPolicy = .performDefaultEvaluation(validateHost: true) open func contentsOfDirectory(path: String, completionHandler: @escaping ([FileObject], Error?) -> Void) { - self.contentsOfDirectory(path: path, rfc3659enabled: serverSupportsRFC3659, completionHandler: completionHandler) + self.contentsOfDirectory(path: path, rfc3659enabled: supportsRFC3659, completionHandler: completionHandler) } /** @@ -168,6 +237,8 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil let path = ftpPath(apath) let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!) + task.serverTrustPolicy = serverTrustPolicy + task.taskDescription = FileOperationType.fetch(path: path).json self.ftpLogin(task) { (error) in if let error = error { self.dispatch_queue.async { @@ -193,8 +264,8 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil } - let files: [FileObject] = contents.flatMap { - rfc3659enabled ? self.parseMLST($0, in: path) : self.parseUnixList($0, in: path) + let files: [FileObject] = contents.compactMap { + rfc3659enabled ? self.parseMLST($0, in: path) : (self.parseUnixList($0, in: path) ?? self.parseDOSList($0, in: path)) } self.dispatch_queue.async { @@ -205,7 +276,7 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil } open func attributesOfItem(path: String, completionHandler: @escaping (FileObject?, Error?) -> Void) { - self.attributesOfItem(path: path, rfc3659enabled: serverSupportsRFC3659, completionHandler: completionHandler) + self.attributesOfItem(path: path, rfc3659enabled: supportsRFC3659, completionHandler: completionHandler) } /** @@ -223,6 +294,8 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil let path = ftpPath(apath) let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!) + task.serverTrustPolicy = serverTrustPolicy + task.taskDescription = FileOperationType.fetch(path: path).json self.ftpLogin(task) { (error) in if let error = error { self.dispatch_queue.async { @@ -242,19 +315,22 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil } guard let response = response, response.hasPrefix("250") || (response.hasPrefix("50") && rfc3659enabled) else { - throw self.urlError(path, code: .badServerResponse) + throw URLError(.badServerResponse, url: self.url(of: path)) } if response.hasPrefix("500") { - self.serverSupportsRFC3659 = false + self.supportsRFC3659 = false self.attributesOfItem(path: path, rfc3659enabled: false, completionHandler: completionHandler) } - let lines = response.components(separatedBy: "\n").flatMap { $0.isEmpty ? nil : $0.trimmingCharacters(in: .whitespacesAndNewlines) } + let lines = response.components(separatedBy: "\n").compactMap { $0.isEmpty ? nil : $0.trimmingCharacters(in: .whitespacesAndNewlines) } guard lines.count > 2 else { - throw self.urlError(path, code: .badServerResponse) + throw URLError(.badServerResponse, url: self.url(of: path)) } - let file: FileObject? = rfc3659enabled ? self.parseMLST(lines[1], in: path) : self.parseUnixList(lines[1], in: path) + let dirPath = path.deletingLastPathComponent + let file: FileObject? = rfc3659enabled ? + self.parseMLST(lines[1], in: dirPath) : + (self.parseUnixList(lines[1], in: dirPath) ?? self.parseDOSList(lines[1], in: dirPath)) self.dispatch_queue.async { completionHandler(file, nil) } @@ -273,6 +349,7 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil } } + @discardableResult open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? { let progress = Progress(totalUnitCount: -1) if recursive { @@ -334,36 +411,48 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil return relativePath.replacingOccurrences(of: "/", with: "", options: .anchored) } - open func isReachable(completionHandler: @escaping (Bool) -> Void) { + open func isReachable(completionHandler: @escaping (_ success: Bool, _ error: Error?) -> Void) { self.attributesOfItem(path: "/") { (file, error) in - completionHandler(file != nil) + completionHandler(file != nil, error) } } open weak var fileOperationDelegate: FileOperationDelegate? + @discardableResult open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? { - let path = (atPath as NSString).appendingPathComponent(folderName) + "/" + let path = atPath.appendingPathComponent(folderName) + "/" return doOperation(.create(path: path), completionHandler: completionHandler) } + @discardableResult open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler) } + @discardableResult open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { return doOperation(.copy(source: path, destination: toPath), completionHandler: completionHandler) } + @discardableResult open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? { return doOperation(.remove(path: path), completionHandler: completionHandler) } + @discardableResult open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { + guard (try? localFile.checkResourceIsReachable()) ?? false else { + dispatch_queue.async { + completionHandler?(URLError(.fileDoesNotExist, url: localFile)) + } + return nil + } + // check file is not a folder guard (try? localFile.resourceValues(forKeys: [.fileResourceTypeKey]))?.fileResourceType ?? .unknown == .regular else { dispatch_queue.async { - completionHandler?(self.urlError(localFile.path, code: .fileIsDirectory)) + completionHandler?(URLError(.fileIsDirectory, url: localFile)) } return nil } @@ -373,12 +462,17 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil return nil } - let progress = Progress(totalUnitCount: 0) + let progress = Progress(totalUnitCount: -1) progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey) progress.kind = .file progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!) + task.serverTrustPolicy = serverTrustPolicy + task.taskDescription = operation.json + progress.cancellationHandler = { [weak task] in + task?.cancel() + } self.ftpLogin(task) { (error) in if let error = error { self.dispatch_queue.async { @@ -388,13 +482,18 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil return } - self.ftpStore(task, filePath: self.ftpPath(toPath), fromData: nil, fromFile: localFile, onTask: { task in + guard let stream = InputStream(url: localFile) else { + return + } + let size = localFile.fileSize + self.ftpStore(task, filePath: self.ftpPath(toPath), from: stream, size: size, onTask: { task in weak var weakTask = task progress.cancellationHandler = { weakTask?.cancel() } progress.setUserInfoObject(Date(), forKey: .startingTimeKey) }, onProgress: { bytesSent, totalSent, expectedBytes in + progress.totalUnitCount = expectedBytes progress.completedUnitCount = totalSent self.delegateNotify(operation, progress: progress.fractionCompleted) }, completionHandler: { (error) in @@ -412,145 +511,71 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil return progress } + @discardableResult open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? { let operation = FileOperationType.copy(source: path, destination: destURL.absoluteString) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else { return nil } - var progress = Progress(totalUnitCount: 0) + let progress = Progress(totalUnitCount: -1) progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey) progress.kind = .file progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) - if self.useAppleImplementation { - self.attributesOfItem(path: path, completionHandler: { (file, error) in - do { - if let error = error { - throw error - } - - if file?.isDirectory ?? false { - throw self.urlError(path, code: .fileIsDirectory) - } - } catch { - self.dispatch_queue.async { - completionHandler?(error) - self.delegateNotify(operation, error: error) - } - return - } - - progress.totalUnitCount = file?.size ?? 0 - - let task = self.session.downloadTask(with: self.url(of: path)) - completionHandlersForTasks[self.session.sessionDescription!]?[task.taskIdentifier] = completionHandler - downloadCompletionHandlersForTasks[self.session.sessionDescription!]?[task.taskIdentifier] = { tempURL in - var error: NSError? - NSFileCoordinator().coordinate(writingItemAt: tempURL, options: .forMoving, writingItemAt: destURL, options: .forReplacing, error: &error, byAccessor: { (tempURL, destURL) in - do { - try FileManager.default.moveItem(at: tempURL, to: destURL) - completionHandler?(nil) - } catch { - completionHandler?(error) - } - }) - if let error = error { - completionHandler?(error) - } + let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!) + task.serverTrustPolicy = serverTrustPolicy + task.taskDescription = operation.json + progress.cancellationHandler = { [weak task] in + task?.cancel() + } + self.ftpLogin(task) { (error) in + if let error = error { + self.dispatch_queue.async { + completionHandler?(error) } - task.taskDescription = operation.json - task.addObserver(self.sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: &progress) - task.addObserver(self.sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: &progress) - progress.cancellationHandler = { [weak task] in - task?.cancel() + return + } + + let tempURL = URL(fileURLWithPath: NSTemporaryDirectory().appendingPathComponent(UUID().uuidString)) + guard let stream = OutputStream(url: tempURL, append: false) else { + completionHandler?(CocoaError(.fileWriteUnknown, path: destURL.path)) + return + } + self.ftpDownload(task, filePath: self.ftpPath(path), to: stream, onTask: { task in + weak var weakTask = task + progress.cancellationHandler = { + weakTask?.cancel() } progress.setUserInfoObject(Date(), forKey: .startingTimeKey) - task.resume() - }) - } else { - let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!) - self.ftpLogin(task) { (error) in - if let error = error { + }, onProgress: { recevied, totalReceived, totalSize in + progress.totalUnitCount = totalSize + progress.completedUnitCount = totalReceived + self.delegateNotify(operation, progress: progress.fractionCompleted) + }) { (error) in + if error != nil { + progress.cancel() + } + do { + try FileManager.default.moveItem(at: tempURL, to: destURL) + } catch { self.dispatch_queue.async { completionHandler?(error) + self.delegateNotify(operation, error: error) } + try? FileManager.default.removeItem(at: tempURL) return } - self.ftpRetrieveFile(task, filePath: self.ftpPath(path), onTask: { task in - weak var weakTask = task - progress.cancellationHandler = { - weakTask?.cancel() - } - progress.setUserInfoObject(Date(), forKey: .startingTimeKey) - }, onProgress: { recevied, totalReceived, totalSize in - progress.totalUnitCount = totalSize - progress.completedUnitCount = totalReceived - self.delegateNotify(operation, progress: progress.fractionCompleted) - }) { (tmpurl, error) in - if let error = error { - progress.cancel() - self.dispatch_queue.async { - completionHandler?(error) - self.delegateNotify(operation, error: error) - } - return - } - - if let tmpurl = tmpurl { - try? FileManager.default.moveItem(at: tmpurl, to: destURL) - self.dispatch_queue.async { - completionHandler?(nil) - self.delegateNotify(operation) - } - } + self.dispatch_queue.async { + completionHandler?(error) + self.delegateNotify(operation, error: error) } } } return progress } - open func contents(path: String, completionHandler: @escaping ((Data?, Error?) -> Void)) -> Progress? { - let operation = FileOperationType.fetch(path: path) - guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else { - return nil - } - - if self.useAppleImplementation { - var progress = Progress(totalUnitCount: 0) - progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey) - progress.kind = .file - progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) - - let task = session.downloadTask(with: url(of: path)) - completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in - if error != nil { - progress.cancel() - } - completionHandler(nil, error) - } - downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { tempURL in - do { - let data = try Data(contentsOf: tempURL) - completionHandler(data, nil) - } catch { - completionHandler(nil, error) - } - } - task.taskDescription = operation.json - task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: &progress) - task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: &progress) - progress.cancellationHandler = { [weak task] in - task?.cancel() - } - progress.setUserInfoObject(Date(), forKey: .startingTimeKey) - task.resume() - return progress - } else { - return self.contents(path: path, offset: 0, length: -1, completionHandler: completionHandler) - } - } - + @discardableResult open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? { let operation = FileOperationType.fetch(path: path) if length == 0 || offset < 0 { @@ -560,12 +585,17 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil } return nil } - let progress = Progress(totalUnitCount: 0) + let progress = Progress(totalUnitCount: -1) progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey) progress.kind = .file progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!) + task.serverTrustPolicy = serverTrustPolicy + task.taskDescription = operation.json + progress.cancellationHandler = { [weak task] in + task?.cancel() + } self.ftpLogin(task) { (error) in if let error = error { self.dispatch_queue.async { @@ -574,7 +604,8 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil return } - self.ftpRetrieveData(task, filePath: self.ftpPath(path), from: offset, length: length, onTask: { task in + let stream = OutputStream.toMemory() + self.ftpDownload(task, filePath: self.ftpPath(path), from: offset, length: length, to: stream, onTask: { task in weak var weakTask = task progress.cancellationHandler = { weakTask?.cancel() @@ -584,7 +615,7 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil progress.totalUnitCount = totalSize progress.completedUnitCount = totalReceived self.delegateNotify(operation, progress: progress.fractionCompleted) - }) { (data, error) in + }) { (error) in if let error = error { progress.cancel() self.dispatch_queue.async { @@ -594,7 +625,7 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil return } - if let data = data { + if let data = stream.property(forKey: .dataWrittenToMemoryStreamKey) as? Data { self.dispatch_queue.async { completionHandler(data, nil) self.delegateNotify(operation) @@ -606,18 +637,24 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil return progress } + @discardableResult open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { let operation = FileOperationType.modify(path: path) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else { return nil } - let progress = Progress(totalUnitCount: Int64(data?.count ?? 0)) + let progress = Progress(totalUnitCount: Int64(data?.count ?? -1)) progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey) progress.kind = .file progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!) + task.serverTrustPolicy = serverTrustPolicy + task.taskDescription = operation.json + progress.cancellationHandler = { [weak task] in + task?.cancel() + } self.ftpLogin(task) { (error) in if let error = error { self.dispatch_queue.async { @@ -628,7 +665,9 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil } let storeHandler = { - self.ftpStore(task, filePath: self.ftpPath(path), fromData: data ?? Data(), fromFile: nil, onTask: { task in + let data = data ?? Data() + let stream = InputStream(data: data) + self.ftpStore(task, filePath: self.ftpPath(path), from: stream, size: Int64(data.count), onTask: { task in weak var weakTask = task progress.cancellationHandler = { weakTask?.cancel() @@ -663,6 +702,65 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil return progress } + public func contents(path: String, offset: Int64, length: Int, responseHandler: ((URLResponse) -> Void)?, progressHandler: @escaping (Int64, Data) -> Void, completionHandler: SimpleCompletionHandler) -> Progress? { + let operation = FileOperationType.fetch(path: path) + if length == 0 || offset < 0 { + dispatch_queue.async { + completionHandler?(nil) + self.delegateNotify(operation) + } + return nil + } + let progress = Progress(totalUnitCount: -1) + progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey) + progress.kind = .file + progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) + + let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!) + task.serverTrustPolicy = serverTrustPolicy + task.taskDescription = operation.json + progress.cancellationHandler = { [weak task] in + task?.cancel() + } + self.ftpLogin(task) { (error) in + if let error = error { + self.dispatch_queue.async { + completionHandler?(error) + } + return + } + + self.ftpDownloadData(task, filePath: self.ftpPath(path), from: offset, length: length, onTask: { task in + weak var weakTask = task + progress.cancellationHandler = { + weakTask?.cancel() + } + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) + }, onProgress: { data, recevied, totalReceived, totalSize in + progressHandler(totalReceived - recevied, data) + progress.totalUnitCount = totalSize + progress.completedUnitCount = totalReceived + self.delegateNotify(operation, progress: progress.fractionCompleted) + }) { (data, error) in + if let error = error { + progress.cancel() + self.dispatch_queue.async { + completionHandler?(error) + self.delegateNotify(operation, error: error) + } + return + } + + self.dispatch_queue.async { + completionHandler?(nil) + self.delegateNotify(operation) + } + } + } + + return progress + } + /** Creates a symbolic link at the specified path that points to an item at the given path. This method does not traverse symbolic links contained in destination path, making it possible @@ -705,6 +803,11 @@ extension FTPFileProvider { progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!) + task.serverTrustPolicy = serverTrustPolicy + task.taskDescription = operation.json + progress.cancellationHandler = { [weak task] in + task?.cancel() + } self.ftpLogin(task) { (error) in if let error = error { completionHandler?(error) @@ -721,13 +824,13 @@ extension FTPFileProvider { guard let response = response else { completionHandler?(error) - self.delegateNotify(operation, error: self.urlError(sourcePath, code: .badServerResponse)) + self.delegateNotify(operation, error: URLError(.badServerResponse, url: self.url(of: sourcePath))) return } - let codes: [Int] = response.components(separatedBy: .newlines).flatMap({ $0.isEmpty ? nil : $0}) - .flatMap { - let code = $0.components(separatedBy: .whitespaces).flatMap({ $0.isEmpty ? nil : $0}).first + let codes: [Int] = response.components(separatedBy: .newlines).compactMap({ $0.isEmpty ? nil : $0}) + .compactMap { + let code = $0.components(separatedBy: .whitespaces).compactMap({ $0.isEmpty ? nil : $0}).first return code != nil ? Int(code!) : nil } @@ -746,13 +849,16 @@ extension FTPFileProvider { case .link: errorCode = .cannotWriteToFile default: errorCode = .cannotOpenFile } - let error = self.urlError(sourcePath, code: errorCode) + let error = URLError(errorCode, url: self.url(of: sourcePath)) progress.cancel() completionHandler?(error) self.delegateNotify(operation, error: error) return } + #if os(macOS) || os(iOS) || os(tvOS) + self._registerUndo(operation) + #endif progress.completedUnitCount = progress.totalUnitCount completionHandler?(nil) self.delegateNotify(operation) @@ -803,7 +909,7 @@ extension FTPFileProvider { } guard let response = response else { - throw self.urlError(sourcePath, code: .badServerResponse) + throw URLError(.badServerResponse, url: self.url(of: sourcePath)) } if response.hasPrefix("50") { @@ -812,7 +918,7 @@ extension FTPFileProvider { } if !response.hasPrefix("2") { - throw self.urlError(sourcePath, code: .cannotRemoveFile) + throw URLError(.cannotRemoveFile, url: self.url(of: sourcePath)) } self.dispatch_queue.async { completionHandler?(nil) @@ -865,3 +971,7 @@ extension FTPFileProvider { } extension FTPFileProvider: FileProvider { } + +#if os(macOS) || os(iOS) || os(tvOS) +extension FTPFileProvider: FileProvideUndoable { } +#endif diff --git a/Sources/FTPHelper.swift b/Sources/FTPHelper.swift index e9d8b07..8c5c5ab 100644 --- a/Sources/FTPHelper.swift +++ b/Sources/FTPHelper.swift @@ -9,66 +9,110 @@ import Foundation internal extension FTPFileProvider { - func readDataUntilEOF(of task: FileProviderStreamTask, minLength: Int, receivedData: Data? = nil, timeout: TimeInterval, completionHandler: @escaping (_ data: Data?, _ errror:Error?) -> Void) { - task.readData(ofMinLength: minLength, maxLength: 65535, timeout: timeout) { (data, eof, error) in + func execute(command: String, on task: FileProviderStreamTask, minLength: Int = 4, + afterSend: ((_ error: Error?) -> Void)? = nil, + completionHandler: @escaping (_ response: String?, _ error: Error?) -> Void) { + let timeout = session.configuration.timeoutIntervalForRequest + let terminalcommand = command + "\r\n" + task.write(terminalcommand.data(using: .utf8)!, timeout: timeout) { (error) in if let error = error { completionHandler(nil, error) return } - var receivedData = receivedData - if let data = data { - if receivedData != nil { - receivedData!.append(data) - } else { - receivedData = data - } + if task.state == .suspended { + task.resume() } - if eof { - completionHandler(receivedData, nil) - } else { - self.readDataUntilEOF(of: task, minLength: 0, receivedData: receivedData, timeout: timeout, completionHandler: completionHandler) + self.readData(on: task, minLength: minLength, maxLength: 4096, timeout: timeout, afterSend: afterSend, completionHandler: completionHandler) + } + } + + func readData(on task: FileProviderStreamTask, + minLength: Int = 4, maxLength: Int = 4096, timeout: TimeInterval, + afterSend: ((_ error: Error?) -> Void)? = nil, + completionHandler: @escaping (_ response: String?, _ error: Error?) -> Void) { + task.readData(ofMinLength: minLength, maxLength: maxLength, timeout: timeout) { (data, eof, error) in + if let error = error { + completionHandler(nil, error) + return } + if let data = data, let response = String(data: data, encoding: .utf8) { + let lines = response.components(separatedBy: "\n").compactMap { $0.isEmpty ? nil : $0.trimmingCharacters(in: .whitespacesAndNewlines) } + if let last = lines.last, last.hasPrefix("1") { + // 1XX: Need to wait for some other response + let timeout = self.session.configuration.timeoutIntervalForResource + self.readData(on: task, minLength: minLength, maxLength: maxLength, timeout: timeout, afterSend: afterSend, completionHandler: completionHandler) + + // Call afterSend + afterSend?(error) + return + } + completionHandler(response.trimmingCharacters(in: .whitespacesAndNewlines), nil) + } else { + completionHandler(nil, URLError(.cannotParseResponse, url: self.url(of: ""))) + } } } - func execute(command: String, on task: FileProviderStreamTask, minLength: Int = 4, afterSend: ((_ error: Error?) -> Void)? = nil, completionHandler: @escaping (_ response: String?, _ error: Error?) -> Void) { - let timeout = session.configuration.timeoutIntervalForRequest - let terminalcommand = command + "\r\n" - task.write(terminalcommand.data(using: .utf8)!, timeout: timeout) { (error) in + func ftpUserPass(_ task: FileProviderStreamTask, completionHandler: @escaping (_ error: Error?) -> Void) { + self.execute(command: "USER \(credential?.user ?? "anonymous")", on: task) { (response, error) in if let error = error { - completionHandler(nil, error) + completionHandler(error) return } - afterSend?(error) - if task.state == .suspended { - task.resume() + guard let response = response else { + completionHandler(URLError(.badServerResponse, url: self.url(of: ""))) + return } - task.readData(ofMinLength: minLength, maxLength: 1024, timeout: timeout) { (data, eof, error) in + // successfully logged in + if response.hasPrefix("23") { + completionHandler(nil) + return + } + + // needs password + if response.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("33") { + self.execute(command: "PASS \(self.credential?.password ?? "fileprovider@")", on: task) { (response, error) in + if response?.hasPrefix("23") ?? false { + completionHandler(nil) + } else { + let error: Error = response.flatMap(FileProviderFTPError.init(message:)) ?? URLError(.userAuthenticationRequired, url: self.url(of: "")) + completionHandler(error) + } + } + return + } + + let error = FileProviderFTPError(message: response) + completionHandler(error) + } + } + + fileprivate func ftpEstablishSecureDataConnection(_ task: FileProviderStreamTask, completionHandler: @escaping (_ error: Error?) -> Void) { + self.execute(command: "PBSZ 0", on: task, completionHandler: { (response, error) in + if let error = error { + completionHandler(error) + return + } + + let prot = self.securedDataConnection ? "PROT P" : "PROT C" + self.execute(command: prot, on: task, completionHandler: { (response, error) in if let error = error { - completionHandler(nil, error) + completionHandler(error) return } - if let data = data, let response = String(data: data, encoding: .utf8) { - completionHandler(response.trimmingCharacters(in: .whitespacesAndNewlines), nil) - } else { - completionHandler(nil, self.urlError("", code: .cannotParseResponse)) - return - } - } - } + completionHandler(nil) + }) + }) } func ftpLogin(_ task: FileProviderStreamTask, completionHandler: @escaping (_ error: Error?) -> Void) { let timeout = session.configuration.timeoutIntervalForRequest - if task.state == .suspended { - task.resume() - } var isSecure = false // Implicit FTP Connection @@ -76,8 +120,9 @@ internal extension FTPFileProvider { task.startSecureConnection() isSecure = true } - - let credential = self.credential + if task.state == .suspended { + task.resume() + } task.readData(ofMinLength: 4, maxLength: 2048, timeout: timeout) { (data, eof, error) in do { @@ -86,10 +131,10 @@ internal extension FTPFileProvider { } guard let data = data, let response = String(data: data, encoding: .utf8) else { - throw self.urlError("", code: .cannotParseResponse) + throw URLError(.cannotParseResponse, url: self.url(of: "")) } - guard response.hasPrefix("22") else { + guard response.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("22") else { throw FileProviderFTPError(message: response) } } catch { @@ -97,44 +142,9 @@ internal extension FTPFileProvider { return } - let loginHandle: () -> Void = { - self.execute(command: "USER \(credential?.user ?? "anonymous")", on: task) { (response, error) in - if let error = error { - completionHandler(error) - return - } - - guard let response = response else { - completionHandler(self.urlError("", code: .badServerResponse)) - return - } - - // successfully logged in - if response.hasPrefix("23") { - completionHandler(nil) - return - } - - // needs password - if FileProviderFTPError(message: response).code == 331 { - self.execute(command: "PASS \(credential?.password ?? "fileprovider@")", on: task) { (response, error) in - if response?.hasPrefix("23") ?? false { - completionHandler(nil) - } else { - completionHandler(self.urlError("", code: .userAuthenticationRequired)) - } - } - return - } - - let error = FileProviderFTPError(message: response) - completionHandler(error) - } - } - if !isSecure && self.baseURL?.scheme == "ftpes" { // Explicit FTP Connection, by upgrading connection to FTP/SSL - self.execute(command: "AUTH TLS", on: task, minLength: 0, completionHandler: { (response, error) in + self.execute(command: "AUTH TLS", on: task, completionHandler: { (response, error) in if let error = error { completionHandler(error) return @@ -143,97 +153,120 @@ internal extension FTPFileProvider { if let response = response, response.hasPrefix("23") { task.startSecureConnection() isSecure = true - self.execute(command: "PBSZ 0\r\nPROT P", on: task, completionHandler: { (response, error) in + self.ftpEstablishSecureDataConnection(task) { error in if let error = error { completionHandler(error) return } - loginHandle() - }) + self.ftpUserPass(task, completionHandler: completionHandler) + } } }) } else if isSecure { - self.execute(command: "PBSZ 0\r\nPROT P", on: task, completionHandler: { (response, error) in + self.ftpEstablishSecureDataConnection(task) { error in if let error = error { completionHandler(error) return } - loginHandle() - }) + self.ftpUserPass(task, completionHandler: completionHandler) + } } else { - loginHandle() + self.ftpUserPass(task, completionHandler: completionHandler) } } } - func ftpCwd(_ task: FileProviderStreamTask, to path: String, completionHandler: @escaping (_ error: Error?) -> Void) { - self.execute(command: "CWD \(path)", on: task) { (response, error) in + func ftpPassive(_ task: FileProviderStreamTask, completionHandler: @escaping (_ dataTask: FileProviderStreamTask?, _ error: Error?) -> Void) { + func trimmedNumber(_ s : String) -> String { + return s.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() + } + + self.execute(command: "PASV", on: task) { (response, error) in do { if let error = error { throw error } - guard let response = response else { - throw self.urlError(path, code: .badServerResponse) + guard let response = response, let destString = response.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: " ").last else { + throw URLError(.badServerResponse, url: self.url(of: "")) } - // successfully logged in - if response.hasPrefix("25") { - completionHandler(nil) + let destArray = destString.components(separatedBy: ",").compactMap({ UInt32(trimmedNumber($0)) }) + guard destArray.count == 6 else { + throw URLError(.badServerResponse, url: self.url(of: "")) } - // not logged in - else if response.hasPrefix("55") { - throw FileProviderFTPError(message: response) + + // first 4 elements are ip, 2 next are port, as byte + var host = destArray.prefix(4).compactMap(String.init).joined(separator: ".") + let portHi = Int(destArray[4]) << 8 + let portLo = Int(destArray[5]) + let port = portHi + portLo + // IPv6 workaround + if host == "127.555.555.555" { + host = self.baseURL!.host! + } + + let passiveTask = self.session.fpstreamTask(withHostName: host, port: port) + if self.baseURL?.scheme == "ftps" || self.baseURL?.scheme == "ftpes" || self.baseURL?.port == 990 { + passiveTask.serverTrustPolicy = task.serverTrustPolicy + passiveTask.reuseSSLSession(task: task) + passiveTask.startSecureConnection() } + passiveTask.securityLevel = .tlSv1 + passiveTask.resume() + completionHandler(passiveTask, nil) } catch { - completionHandler(error) + completionHandler(nil, error) + return } + } } - func ftpPassive(_ task: FileProviderStreamTask, completionHandler: @escaping (_ dataTask: FileProviderStreamTask?, _ error: Error?) -> Void) { + func ftpExtendedPassive(_ task: FileProviderStreamTask, completionHandler: @escaping (_ dataTask: FileProviderStreamTask?, _ error: Error?) -> Void) { func trimmedNumber(_ s : String) -> String { return s.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() } - self.execute(command: "PASV", on: task) { (response, error) in + self.execute(command: "EPSV", on: task) { (response, error) in do { if let error = error { throw error } - guard let response = response, let destString = response.components(separatedBy: " ").flatMap({ $0 }).last.flatMap(String.init) else { - throw self.urlError("", code: .badServerResponse) + guard let response = response, let destString = response.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: " ").last else { + throw URLError(.badServerResponse, url: self.url(of: "")) } - let destArray = destString.components(separatedBy: ",").flatMap({ UInt32(trimmedNumber($0)) }) - guard destArray.count == 6 else { - throw self.urlError("", code: .badServerResponse) + if response.trimmingCharacters(in: .whitespaces).hasPrefix("50") { + self.ftpPassive(task, completionHandler: completionHandler) + return } - // first 4 elements are ip, 2 next are port, as byte - var host = destArray.prefix(4).flatMap(String.init).joined(separator: ".") - let portHi = Int(destArray[4]) << 8 - let portLo = Int(destArray[5]) - let port = portHi + portLo - // IPv6 workaround - if host == "127.555.555.555" { - host = self.baseURL!.host! + let destArray = destString.components(separatedBy: "|") + guard destArray.count >= 4, let port = Int(trimmedNumber(destArray[3])) else { + throw URLError(.badServerResponse, url: self.url(of: "")) + } + var host = destArray[2] + if host.isEmpty { + host = self.baseURL?.host ?? "" } let passiveTask = self.session.fpstreamTask(withHostName: host, port: port) - passiveTask.resume() if self.baseURL?.scheme == "ftps" || self.baseURL?.scheme == "ftpes" || self.baseURL?.port == 990 { + passiveTask.serverTrustPolicy = task.serverTrustPolicy + passiveTask.reuseSSLSession(task: task) passiveTask.startSecureConnection() } + passiveTask.securityLevel = .tlSv1 + passiveTask.resume() completionHandler(passiveTask, nil) } catch { completionHandler(nil, error) return } - } } @@ -245,10 +278,12 @@ internal extension FTPFileProvider { usleep(100_000) } let activeTask = self.session.fpstreamTask(withNetService: service) - activeTask.resume() if self.baseURL?.scheme == "ftps" || self.baseURL?.port == 990 { + activeTask.serverTrustPolicy = task.serverTrustPolicy + activeTask.reuseSSLSession(task: task) activeTask.startSecureConnection() } + activeTask.resume() self.execute(command: "PORT \(service.port)", on: task) { (response, error) in do { @@ -257,11 +292,11 @@ internal extension FTPFileProvider { } guard let response = response else { - throw self.urlError("", code: .badServerResponse) + throw URLError(.badServerResponse, url: self.url(of: "")) } guard !response.hasPrefix("5") else { - throw self.urlError("", code: .cannotConnectToHost) + throw URLError(.cannotConnectToHost, url: self.url(of: "")) } completionHandler(activeTask, nil) @@ -273,117 +308,110 @@ internal extension FTPFileProvider { } func ftpDataConnect(_ task: FileProviderStreamTask, completionHandler: @escaping (_ dataTask: FileProviderStreamTask?, _ error: Error?) -> Void) { - if self.passiveMode { + switch self.mode { + case .default: + if self.baseURL?.port == 990 || self.baseURL?.scheme == "ftps" || self.baseURL?.scheme == "ftpes" { + self.ftpExtendedPassive(task, completionHandler: completionHandler) + } else { + self.ftpPassive(task, completionHandler: completionHandler) + } + case .passive: self.ftpPassive(task, completionHandler: completionHandler) - } else { + case .extendedPassive: + self.ftpExtendedPassive(task, completionHandler: completionHandler) + case .active: dispatch_queue.async { self.ftpActive(task, completionHandler: completionHandler) } } } - func ftpRest(_ task: FileProviderStreamTask, startPosition: Int64, completionHandler: @escaping (_ error: Error?) -> Void) { - self.execute(command: "REST \(startPosition)", on: task) { (response, error) in - do { - if let error = error { - throw error - } - - // Successful - guard let response = response else { - throw self.urlError("", code: .badServerResponse) - } - - if response.hasPrefix("35") { - completionHandler(nil) - } else { - throw FileProviderFTPError(message: response, path: "") - } - } catch { - completionHandler(error) - } - - } - } - - func ftpList(_ task: FileProviderStreamTask, of path: String, useMLST: Bool, completionHandler: @escaping (_ contents: [String], _ error: Error?) -> Void) { + func ftpList(_ task: FileProviderStreamTask, of path: String, useMLST: Bool, + completionHandler: @escaping (_ contents: [String], _ error: Error?) -> Void) { self.ftpDataConnect(task) { (dataTask, error) in + if let error = error { completionHandler([], error) return } guard let dataTask = dataTask else { - completionHandler([], self.urlError(path, code: .badServerResponse)) + completionHandler([], URLError(.badServerResponse, url: self.url(of: path))) return } + let success_lock = NSLock() var success = false let command = useMLST ? "MLSD \(path)" : "LIST \(path)" - self.execute(command: command, on: task, minLength: 20, afterSend: { error in - // starting passive task - let timeout = self.session.configuration.timeoutIntervalForRequest - - DispatchQueue.global().async { + self.execute(command: command, on: task) { (response, error) in + do { + if let error = error { + throw error + } + + guard let response = response else { + throw URLError(.cannotParseResponse, url: self.url(of: path)) + } + + if response.hasPrefix("500") && useMLST { + dataTask.cancel() + self.supportsRFC3659 = false + throw URLError(.unsupportedURL, url: self.url(of: path)) + } + + let timeout = self.session.configuration.timeoutIntervalForRequest var finalData = Data() var eof = false + let error_lock = NSLock() var error: Error? - do { - while !eof { - let group = DispatchGroup() - group.enter() - dataTask.readData(ofMinLength: 1, maxLength: 65535, timeout: timeout, completionHandler: { (data, seof, serror) in - if let data = data { - finalData.append(data) - } - eof = seof - error = serror - group.leave() - }) - let waitResult = group.wait(timeout: .now() + timeout) - - if let error = error { - if !((error as NSError).domain == URLError.errorDomain && (error as NSError).code == URLError.cancelled.rawValue) { - throw error - } - return - } - - if waitResult == .timedOut { - throw self.urlError(path, code: .timedOut) + while !eof { + let group = DispatchGroup() + group.enter() + dataTask.readData(ofMinLength: 1, maxLength: Int.max, timeout: timeout) { (data, seof, serror) in + if let data = data { + finalData.append(data) } + eof = seof + error_lock.lock() + error = serror + error_lock.unlock() + group.leave() } + let waitResult = group.wait(timeout: .now() + timeout) - guard let response = String(data: finalData, encoding: .utf8) else { - throw self.urlError(path, code: .badServerResponse) + error_lock.lock() + if let error = error { + error_lock.unlock() + if (error as? URLError)?.code != .cancelled { + throw error + } + return } + error_lock.unlock() - let contents: [String] = response.components(separatedBy: "\n").flatMap({ $0.trimmingCharacters(in: .whitespacesAndNewlines) }) - success = true - completionHandler(contents, nil) - } catch { - completionHandler([], error) - } - } - }) { (response, error) in - do { - if let error = error { - throw error + if waitResult == .timedOut { + throw URLError(.timedOut, url: self.url(of: path)) + } } - guard let response = response else { - throw self.urlError(path, code: .cannotParseResponse) + guard let dataResponse = String(data: finalData, encoding: .utf8) else { + throw URLError(.badServerResponse, url: self.url(of: path)) } - if response.hasPrefix("500") && useMLST { - dataTask.cancel() - self.serverSupportsRFC3659 = false - throw self.urlError(path, code: .unsupportedURL) - } + let contents: [String] = dataResponse.components(separatedBy: "\n") + .compactMap({ $0.trimmingCharacters(in: .whitespacesAndNewlines) }) + success_lock.try() + success = true + success_lock.unlock() + completionHandler(contents, nil) + success_lock.try() if !success && !(response.hasPrefix("25") || response.hasPrefix("15")) { + success_lock.unlock() throw FileProviderFTPError(message: response, path: path) + } else { + success_lock.unlock() } } catch { self.dispatch_queue.async { @@ -394,18 +422,18 @@ internal extension FTPFileProvider { } } - func recursiveList(path: String, useMLST: Bool, foundItemsHandler: ((_ contents: [FileObject]) -> Void)? = nil, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) -> Progress? { + func recursiveList(path: String, useMLST: Bool, foundItemsHandler: ((_ contents: [FileObject]) -> Void)? = nil, + completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) -> Progress? { let progress = Progress(totalUnitCount: -1) let queue = DispatchQueue(label: "\(self.type).recursiveList") + let group = DispatchGroup() queue.async { - let group = DispatchGroup() var result = [FileObject]() - var success = true + var errorInfo:Error? group.enter() self.contentsOfDirectory(path: path, completionHandler: { (files, error) in - success = success && (error == nil) if let error = error { - completionHandler([], error) + errorInfo = error group.leave() return } @@ -418,10 +446,10 @@ internal extension FTPFileProvider { progress.becomeCurrent(withPendingUnitCount: Int64(directories.count)) for dir in directories { group.enter() - _=self.recursiveList(path: dir.path, useMLST: useMLST, foundItemsHandler: foundItemsHandler, completionHandler: { (contents, error) in - success = success && (error == nil) + _=self.recursiveList(path: dir.path, useMLST: useMLST, foundItemsHandler: foundItemsHandler) { + (contents, error) in if let error = error { - completionHandler([], error) + errorInfo = error group.leave() return } @@ -430,14 +458,16 @@ internal extension FTPFileProvider { result.append(contentsOf: contents) group.leave() - }) + } } progress.resignCurrent() group.leave() }) group.wait() - if success { + if let error = errorInfo { + completionHandler([], error) + } else { self.dispatch_queue.async { completionHandler(result, nil) } @@ -446,95 +476,108 @@ internal extension FTPFileProvider { return progress } - func ftpRetrieveData(_ task: FileProviderStreamTask, filePath: String, from position: Int64 = 0, length: Int = -1, onTask: ((_ task: FileProviderStreamTask) -> Void)?, onProgress: ((_ bytesReceived: Int64, _ totalReceived: Int64, _ expectedBytes: Int64) -> Void)?, completionHandler: @escaping (_ data: Data?, _ error: Error?) -> Void) { - - // Check cache - if useCache, let url = URL(string: filePath.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? filePath, relativeTo: self.baseURL!)?.absoluteURL, let cachedResponse = self.cache?.cachedResponse(for: URLRequest(url: url)), cachedResponse.data.count > 0 { - dispatch_queue.async { - completionHandler(cachedResponse.data, nil) - } - return - } + func ftpRetrieve(_ task: FileProviderStreamTask, filePath: String, from position: Int64 = 0, length: Int = -1, to stream: OutputStream, + onTask: ((_ task: FileProviderStreamTask) -> Void)?, + onProgress: @escaping (_ data: Data, _ totalReceived: Int64, _ expectedBytes: Int64) -> Void, + completionHandler: SimpleCompletionHandler) { self.attributesOfItem(path: filePath) { (file, error) in let totalSize = file?.size ?? -1 // Retreive data from server self.ftpDataConnect(task) { (dataTask, error) in if let error = error { - completionHandler(nil, error) + completionHandler?(error) return } guard let dataTask = dataTask else { - completionHandler(nil, self.urlError(filePath, code: .badServerResponse)) + completionHandler?(URLError(.badServerResponse, url: self.url(of: filePath))) return } // Send retreive command - let len = 19 /* TYPE response */ + 65 + String(position).count /* REST Response */ + 53 + filePath.count + String(totalSize).count /* RETR open response */ + 26 /* RETR Transfer complete message. */ - self.execute(command: "TYPE I" + "\r\n" + "REST \(position)" + "\r\n" + "RETR \(filePath)", on: task, minLength: len, afterSend: { error in + self.execute(command: "TYPE I" + "\r\n" + "REST \(position)" + "\r\n" + "RETR \(filePath)", on: task) { (response, error) in // starting passive task onTask?(dataTask) + defer { + dataTask.closeRead() + dataTask.closeWrite() + } + + if stream.streamStatus == .notOpen || stream.streamStatus == .closed { + stream.open() + } + let timeout = self.session.configuration.timeoutIntervalForRequest - DispatchQueue.global().async { - var finalData = Data() - var eof = false - var error: Error? - while !eof { - let group = DispatchGroup() - group.enter() - dataTask.readData(ofMinLength: 0, maxLength: 65535, timeout: timeout, completionHandler: { (data, seof, serror) in - if let data = data { - finalData.append(data) - onProgress?(Int64(data.count), Int64(finalData.count), totalSize) - } - eof = seof || (length > 0 && finalData.count >= length) - if length > 0 && finalData.count > length { - finalData.count = length - } - error = serror + var totalReceived: Int64 = 0 + var eof = false + let error_lock = NSLock() + var error: Error? + while !eof { + let group = DispatchGroup() + group.enter() + dataTask.readData(ofMinLength: 1, maxLength: Int.max, timeout: timeout) { (data, segeof, segerror) in + defer { group.leave() - }) - let waitResult = group.wait(timeout: .now() + timeout) - - if let error = error { - completionHandler(nil, error) - return } - - if waitResult == .timedOut { - completionHandler(nil, self.urlError(filePath, code: .timedOut)) + if let segerror = segerror { + error_lock.lock() + error = segerror + error_lock.unlock() return } + if let data = data { + var data = data + if length > 0, Int64(data.count) + totalReceived > Int64(length) { + data.count = Int(Int64(length) - totalReceived) + } + totalReceived += Int64(data.count) + let result = (try? stream.write(data: data)) ?? -1 + if result < 0 { + error_lock.lock() + error = stream.streamError ?? URLError(.cannotWriteToFile, url: self.url(of: filePath)) + error_lock.unlock() + eof = true + return + } + onProgress(data, totalReceived, totalSize) + } + eof = segeof || (length > 0 && totalReceived >= Int64(length)) } + let waitResult = group.wait(timeout: .now() + timeout) - if let url = URL(string: filePath.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? filePath, relativeTo: self.baseURL!)?.absoluteURL { - let urlresponse = URLResponse(url: url, mimeType: nil, expectedContentLength: finalData.count, textEncodingName: nil) - let cachedResponse = CachedURLResponse(response: urlresponse, data: finalData) - let request = URLRequest(url: url) - self.cache?.storeCachedResponse(cachedResponse, for: request) + error_lock.try() + if let error = error { + error_lock.unlock() + completionHandler?(error) + return } + error_lock.unlock() - completionHandler(finalData, nil) - return + if waitResult == .timedOut { + completionHandler?(URLError(.timedOut, url: self.url(of: filePath))) + return + } } - }) { (response, error) in + + completionHandler?(nil) + do { if let error = error { throw error } guard let response = response else { - throw self.urlError(filePath, code: .cannotParseResponse) + throw URLError(.cannotParseResponse, url: self.url(of: filePath)) } - if !(response.hasPrefix("1") || !response.hasPrefix("2")) { + if !(response.hasPrefix("1") || response.hasPrefix("2")) { throw FileProviderFTPError(message: response) } } catch { self.dispatch_queue.async { - completionHandler(nil, error) + completionHandler?(error) } } } @@ -542,97 +585,217 @@ internal extension FTPFileProvider { } } - func ftpRetrieveFile(_ task: FileProviderStreamTask, filePath: String, from position: Int64 = 0, length: Int = -1, onTask: ((_ task: FileProviderStreamTask) -> Void)?, onProgress: ((_ bytesReceived: Int64, _ totalReceived: Int64, _ expectedBytes: Int64) -> Void)?, completionHandler: @escaping (_ file: URL?, _ error: Error?) -> Void) { - let tempURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString).appendingPathExtension("tmp") + func ftpDownloadData(_ task: FileProviderStreamTask, filePath: String, from position: Int64 = 0, length: Int = -1, + onTask: ((_ task: FileProviderStreamTask) -> Void)?, + onProgress: ((_ data: Data, _ bytesReceived: Int64, _ totalReceived: Int64, _ expectedBytes: Int64) -> Void)?, + completionHandler: @escaping (_ data: Data?, _ error: Error?) -> Void) { // Check cache if useCache, let url = URL(string: filePath.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? filePath, relativeTo: self.baseURL!)?.absoluteURL, let cachedResponse = self.cache?.cachedResponse(for: URLRequest(url: url)), cachedResponse.data.count > 0 { dispatch_queue.async { - do { - try cachedResponse.data.write(to: tempURL) - completionHandler(tempURL, nil) - } catch { - completionHandler(nil, error) + completionHandler(cachedResponse.data, nil) + } + return + } + + let stream = OutputStream.toMemory() + self.ftpRetrieve(task, filePath: filePath, from: position, length: length, to: stream, onTask: onTask, onProgress: { (data, total, expected) in + onProgress?(data, Int64(data.count), total, expected) + }) { (error) in + if let error = error { + completionHandler(nil, error) + } + + guard let finalData = stream.property(forKey: .dataWrittenToMemoryStreamKey) as? Data else { + completionHandler(nil, CocoaError(.fileReadUnknown, path: filePath)) + return + } + + if let url = URL(string: filePath.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? filePath, relativeTo: self.baseURL!)?.absoluteURL { + let urlresponse = URLResponse(url: url, mimeType: nil, expectedContentLength: finalData.count, textEncodingName: nil) + let cachedResponse = CachedURLResponse(response: urlresponse, data: finalData) + let request = URLRequest(url: url) + self.cache?.storeCachedResponse(cachedResponse, for: request) + } + completionHandler(finalData, nil) + } + } + + func ftpDownload(_ task: FileProviderStreamTask, filePath: String, from position: Int64 = 0, length: Int = -1, to stream: OutputStream, + onTask: ((_ task: FileProviderStreamTask) -> Void)?, + onProgress: ((_ bytesReceived: Int64, _ totalReceived: Int64, _ expectedBytes: Int64) -> Void)?, + completionHandler: SimpleCompletionHandler) { + // Check cache + if useCache, let url = URL(string: filePath.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? filePath, relativeTo: self.baseURL!)?.absoluteURL, let cachedResponse = self.cache?.cachedResponse(for: URLRequest(url: url)), cachedResponse.data.count > 0 { + dispatch_queue.async { + let data = cachedResponse.data + stream.open() + let result = (try? stream.write(data: data)) ?? -1 + if result > 0 { + completionHandler?(nil) + } else { + completionHandler?(stream.streamError ?? URLError(.cannotWriteToFile, url: self.url(of: filePath))) } - try? FileManager.default.removeItem(at: tempURL) + stream.close() } return } - self.attributesOfItem(path: filePath) { (file, error) in - let totalSize = file?.size ?? -1 - // Retreive data from server + self.ftpRetrieve(task, filePath: filePath, from: position, length: length, to: stream, onTask: onTask, onProgress: { (data, total, expected) in + onProgress?(Int64(data.count), total, expected) + }, completionHandler: completionHandler) + } + + func ftpStore(_ task: FileProviderStreamTask, filePath: String, from stream: InputStream, size: Int64, + onTask: ((_ task: FileProviderStreamTask) -> Void)?, + onProgress: ((_ bytesSent: Int64, _ totalSent: Int64, _ expectedBytes: Int64) -> Void)?, + completionHandler: @escaping (_ error: Error?) -> Void) { + if self.uploadByREST { + ftpStoreParted(task, filePath: filePath, from: stream, size: size, onTask: onTask, onProgress: onProgress, completionHandler: completionHandler) + } else { + ftpStoreSerial(task, filePath: filePath, from: stream, size: size, onTask: onTask, onProgress: onProgress, completionHandler: completionHandler) + } + } + + func optimizedChunkSize(_ size: Int64) -> Int { + switch size { + case 0..<262_144: + return 32_768 // 0KB To 256KB, chunk size is 32KB + case 262_144..<1_048_576: + return 65_536 // 256KB To 1MB, chunk size is 64KB + case 1_048_576..<10_485_760: + return 131_072 // 1MB To 10MB, chunk size is 128KB + case 10_485_760..<33_554_432: + return 262_144 // 10MB To 32MB, chunk size is 256KB + default: + return 524_288 // Larger than 32MB, chunk size is 512KB + } + } + + func ftpStoreSerial(_ task: FileProviderStreamTask, filePath: String, from stream: InputStream, size: Int64, + onTask: ((_ task: FileProviderStreamTask) -> Void)?, + onProgress: ((_ bytesSent: Int64, _ totalSent: Int64, _ expectedBytes: Int64) -> Void)?, completionHandler: @escaping (_ error: Error?) -> Void) { + self.execute(command: "TYPE I", on: task) { (response, error) in + do { + if let error = error { + throw error + } + + guard let response = response else { + throw URLError(.cannotParseResponse, url: self.url(of: filePath)) + } + + if !response.hasPrefix("2") { + throw FileProviderFTPError(message: response, path: filePath) + } + } catch { + completionHandler(error) + return + } + self.ftpDataConnect(task) { (dataTask, error) in if let error = error { - completionHandler(nil, error) + completionHandler(error) return } guard let dataTask = dataTask else { - completionHandler(nil, self.urlError(filePath, code: .badServerResponse)) + completionHandler(URLError(.badServerResponse, url: self.url(of: filePath))) return } + let success_lock = NSLock() + var success = false - // Send retreive command - let len = 19 /* TYPE response */ + 65 + String(position).count /* REST Response */ + 53 + filePath.count + String(totalSize).count /* RETR open response */ + 26 /* RETR Transfer complete message. */ - self.execute(command: "TYPE I" + "\r\n" + "REST \(position)" + "\r\n" + "RETR \(filePath)", on: task, minLength: len, afterSend: { error in - // starting passive task + let completed_lock = NSLock() + var completed = false + func completionOnce(completion: () -> ()) { + completed_lock.lock() + guard !completed else { + completed_lock.unlock() + return + } + completion() + completed = true + completed_lock.unlock() + } + + self.execute(command: "STOR \(filePath)", on: task, afterSend: { error in onTask?(dataTask) - let timeout = self.session.configuration.timeoutIntervalForRequest - DispatchQueue.global().async { - var finalData = Data() - var eof = false - var error: Error? - while !eof { - let group = DispatchGroup() - group.enter() - dataTask.readData(ofMinLength: 0, maxLength: 65535, timeout: timeout, completionHandler: { (data, seof, serror) in - if let data = data { - finalData.append(data) - onProgress?(Int64(data.count), Int64(finalData.count), totalSize) - } - eof = seof || (length > 0 && finalData.count >= length) - if length > 0 && finalData.count > length { - finalData.count = length - } - error = serror - group.leave() - }) - let waitResult = group.wait(timeout: .now() + timeout) - - if let error = error { - completionHandler(nil, error) - return - } - - if waitResult == .timedOut { - error = self.urlError("", code: .timedOut) - completionHandler(nil, error) - return + let timeout = self.session.configuration.timeoutIntervalForResource + var error: Error? + + let chunkSize = self.optimizedChunkSize(size) + let lock = NSLock() + var sent: Int64 = 0 + + stream.open() + + repeat { + guard !completed else { + return + } + + lock.lock() + + guard var subdata = try? stream.readData(ofLength: chunkSize) else { + lock.unlock() + completionOnce { + completionHandler(stream.streamError ?? URLError(.requestBodyStreamExhausted, url: self.url(of: filePath))) } + return } + lock.unlock() + if subdata.isEmpty { break } + + let group = DispatchGroup() + group.enter() + dataTask.write(subdata, timeout: timeout, completionHandler: { (serror) in + lock.lock() + if let serror = serror { + error = serror + } else { + sent += Int64(subdata.count) + let totalsent = sent + let sentbytes = Int64(subdata.count) + onProgress?(sentbytes, totalsent, size) + print("ftp", filePath, dataTask.countOfBytesSent, dataTask.countOfBytesExpectedToSend, totalsent) + } + lock.unlock() + group.leave() + }) + let waitResult = group.wait(timeout: .now() + timeout) - if let url = URL(string: filePath.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? filePath, relativeTo: self.baseURL!)?.absoluteURL { - let urlresponse = URLResponse(url: url, mimeType: nil, expectedContentLength: finalData.count, textEncodingName: nil) - let cachedResponse = CachedURLResponse(response: urlresponse, data: finalData) - let request = URLRequest(url: url) - self.cache?.storeCachedResponse(cachedResponse, for: request) + lock.lock() + + if let error = error { + lock.unlock() + completionOnce { + completionHandler(error) + } + return } - self.dispatch_queue.async { - do { - try finalData.write(to: tempURL) - completionHandler(tempURL, nil) - // Removing temporary file after coordinating - NSFileCoordinator().coordinate(writingItemAt: tempURL, options: .forDeleting, error: nil, byAccessor: { (tempURL) in - try? FileManager.default.removeItem(at: tempURL) - }) - } catch { - completionHandler(nil, error) + if waitResult == .timedOut { + lock.unlock() + completionOnce { + completionHandler(URLError(.timedOut, url: self.url(of: filePath))) } + return } + lock.unlock() + } while stream.streamStatus != .atEnd + + success_lock.lock() + success = true + success_lock.unlock() + + if self.securedDataConnection { + dataTask.stopSecureConnection() } + // TOFIX: Close read/write stream for receive a FTP response from the server + dataTask.closeRead(immediate: true) + dataTask.closeWrite() }) { (response, error) in do { if let error = error { @@ -640,143 +803,199 @@ internal extension FTPFileProvider { } guard let response = response else { - throw self.urlError(filePath, code: .cannotParseResponse) + throw URLError(.cannotParseResponse, url: self.url(of: filePath)) } - if !(response.hasPrefix("1") || response.hasPrefix("2")) { - throw FileProviderFTPError(message: response) + let lines = response.components(separatedBy: "\n").compactMap { $0.isEmpty ? nil : $0.trimmingCharacters(in: .whitespacesAndNewlines) } + if lines.count > 0 { + for line in lines { + if !(line.hasPrefix("1") || line.hasPrefix("2")) { + // FTP Error Response + throw FileProviderFTPError(message: response, path: filePath) + } + } + } + + success_lock.lock() + if success, let last = lines.last, last.hasPrefix("2") { + success_lock.unlock() + // File successfully transferred. + completionOnce { + completionHandler(nil) + } + return + } else { + success_lock.unlock() + throw URLError(.cannotCreateFile, url: self.url(of: filePath)) } } catch { - self.dispatch_queue.async { - completionHandler(nil, error) + success_lock.lock() + if !success { + dataTask.cancel() + } + success_lock.unlock() + + completionOnce { + completionHandler(error) } } } } } + + } - func ftpStore(_ task: FileProviderStreamTask, filePath: String, fromData: Data?, fromFile: URL?, onTask: ((_ task: FileProviderStreamTask) -> Void)?, onProgress: ((_ bytesSent: Int64, _ totalSent: Int64, _ expectedBytes: Int64) -> Void)?, completionHandler: @escaping (_ error: Error?) -> Void) { - let timeout = self.session.configuration.timeoutIntervalForRequest + func ftpStoreParted(_ task: FileProviderStreamTask, filePath: String, from stream: InputStream, size: Int64, from position: Int64 = 0, + onTask: ((_ task: FileProviderStreamTask) -> Void)?, + onProgress: ((_ bytesSent: Int64, _ totalSent: Int64, _ expectedBytes: Int64) -> Void)?, + completionHandler: @escaping (_ error: Error?) -> Void) { operation_queue.addOperation { - guard let size: Int64 = (fromData != nil ? Int64(fromData!.count) : nil) ?? fromFile?.fileSize else { return } - + let timeout = self.session.configuration.timeoutIntervalForResource var error: Error? - let chunkSize: Int - switch size { - case 0..<262_144: chunkSize = 32_768 // 0KB To 256KB, chunk size is 32KB - case 262_144..<1_048_576: chunkSize = 65_536 // 256KB To 1MB, chunk size is 64KB - case 1_048_576..<10_485_760: chunkSize = 131_072 // 1MB To 10MB, chunk size is 128KB - case 10_048_576..<33_554_432: chunkSize = 262_144 // 10MB To 32MB, chunk size is 256KB - default: chunkSize = 524_288 // Larger than 32MB, chunk size is 512KB - } + let chunkSize = self.optimizedChunkSize(size) - var fileHandle: FileHandle? - if let file = fromFile { - fileHandle = FileHandle(forReadingAtPath: file.path) - } + stream.open() defer { - fileHandle?.closeFile() + stream.close() } - - var eof = false - var sent: Int64 = 0 - - while !eof { - let subdata: Data - if let data = fromData { - let endIndex = min(data.count, Int(sent) + chunkSize) - eof = endIndex == data.count - subdata = data.subdata(in: Int(sent).. Void)?, completionHandler: @escaping (_ error: Error?) -> Void) { - self.ftpDataConnect(task) { (dataTask, error) in - if let error = error { + func ftpStore(_ task: FileProviderStreamTask, data: Data, to filePath: String, from position: Int64, + onTask: ((_ task: FileProviderStreamTask) -> Void)?, + completionHandler: @escaping (_ error: Error?) -> Void) { + self.execute(command: "TYPE I", on: task) { (response, error) in + do { + if let error = error { + throw error + } + + guard let response = response else { + throw URLError(.cannotParseResponse, url: self.url(of: filePath)) + } + + if !response.hasPrefix("2") { + throw FileProviderFTPError(message: response) + } + } catch { completionHandler(error) return } - guard let dataTask = dataTask else { - completionHandler(self.urlError(filePath, code: .badServerResponse)) - return - } - - // Send retreive command - var success = false - let len = 19 /* TYPE response */ + 65 + String(position).count /* REST Response */ + 44 + filePath.count /* STOR open response */ + 10 /* RETR Transfer complete message. */ - self.execute(command: "TYPE I" + "\r\n" + "REST \(position)" + "\r\n" + "STOR \(filePath)", on: task, minLength: len, afterSend: { error in - // starting passive task - let timeout = self.session.configuration.timeoutIntervalForRequest - onTask?(dataTask) - - if data.count == 0 { return } - - dataTask.write(data, timeout: timeout, completionHandler: { (error) in - if let error = error { - completionHandler(error) - return - } - success = true - - dataTask.closeRead() - dataTask.closeWrite() - }) - }) { (response, error) in - guard success else { return } - + self.execute(command: "REST \(position)", on: task, completionHandler: { (response, error) in do { if let error = error { throw error } guard let response = response else { - throw self.urlError(filePath, code: .cannotParseResponse) + throw URLError(.cannotParseResponse, url: self.url(of: filePath)) } - if !(response.hasPrefix("1") || response.hasPrefix("2")) { + if !response.hasPrefix("35") { throw FileProviderFTPError(message: response) } - - completionHandler(nil) } catch { - self.dispatch_queue.async { + completionHandler(error) + return + } + + self.ftpDataConnect(task) { (dataTask, error) in + if let error = error { completionHandler(error) + return + } + + guard let dataTask = dataTask else { + completionHandler(URLError(.badServerResponse, url: self.url(of: filePath))) + return + } + + // Send retreive command + let success_lock = NSLock() + var success = false + self.execute(command: "STOR \(filePath)", on: task, minLength: 44 + filePath.count + 4, afterSend: { error in + // starting passive task + let timeout = self.session.configuration.timeoutIntervalForRequest + onTask?(dataTask) + + if data.count == 0 { return } + + dataTask.write(data, timeout: timeout, completionHandler: { (error) in + if let error = error { + completionHandler(error) + return + } + success_lock.lock() + success = true + success_lock.unlock() + + completionHandler(nil) + }) + }) { (response, error) in + success_lock.lock() + guard success else { + success_lock.unlock() + return + } + success_lock.unlock() + + do { + if let error = error { + throw error + } + + guard let response = response else { + throw URLError(.cannotParseResponse, url: self.url(of: filePath)) + } + + if !(response.hasPrefix("1") || response.hasPrefix("2")) { + throw FileProviderFTPError(message: response) + } + } catch { + self.dispatch_queue.async { + completionHandler(error) + } + } } } - } + }) } + + } func ftpQuit(_ task: FileProviderStreamTask) { @@ -788,6 +1007,7 @@ internal extension FTPFileProvider { func ftpPath(_ apath: String) -> String { // path of base url should be concreted into file path! And remove final slash + let apath = apath.replacingOccurrences(of: "/", with: "", options: [.anchored]) var path = baseURL!.appendingPathComponent(apath).path.replacingOccurrences(of: "/", with: "", options: [.anchored, .backwards]) // Fixing slashes @@ -803,14 +1023,14 @@ internal extension FTPFileProvider { let nearDateFormatter = DateFormatter() nearDateFormatter.calendar = gregorian nearDateFormatter.locale = Locale(identifier: "en_US_POSIX") - nearDateFormatter.dateFormat = "MMM dd hh:ss yyyy" + nearDateFormatter.dateFormat = "MMM dd hh:mm yyyy" let farDateFormatter = DateFormatter() farDateFormatter.calendar = gregorian farDateFormatter.locale = Locale(identifier: "en_US_POSIX") farDateFormatter.dateFormat = "MMM dd yyyy" let thisYear = gregorian.component(.year, from: Date()) - let components = text.components(separatedBy: " ").flatMap { $0.isEmpty ? nil : $0 } + let components = text.components(separatedBy: " ").compactMap { $0.isEmpty ? nil : $0 } guard components.count >= 9 else { return nil } let posixPermission = components[0] let linksCount = Int(components[1]) ?? 0 @@ -821,14 +1041,10 @@ internal extension FTPFileProvider { let name = components[8..=4.0) + let file = FileObject(url: url(of: path), name: name, path: "/" + path) let typeChar = posixPermission.first ?? Character(" ") - #else - let typeChar = posixPermission.characters.first ?? Character(" ") - #endif switch String(typeChar) { case "d": file.type = .directory case "l": file.type = .symbolicLink @@ -851,8 +1067,35 @@ internal extension FTPFileProvider { return file } + func parseDOSList(_ text: String, in path: String) -> FileObject? { + let gregorian = Calendar(identifier: .gregorian) + let dateFormatter = DateFormatter() + dateFormatter.calendar = gregorian + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.dateFormat = "M-d-y hh:mma" + + let components = text.components(separatedBy: " ").compactMap { $0.isEmpty ? nil : $0 } + guard components.count >= 4 else { return nil } + let size = Int64(components[2]) ?? -1 + let date = components[0..<2].joined(separator: " ") + let name = components[3.. FileObject? { - var components = text.components(separatedBy: ";").flatMap { $0.isEmpty ? nil : $0 } + var components = text.components(separatedBy: ";").compactMap { $0.isEmpty ? nil : $0 } guard components.count > 1 else { return nil } let nameOrPath = components.removeLast().trimmingCharacters(in: .whitespacesAndNewlines) @@ -860,21 +1103,21 @@ internal extension FTPFileProvider { let name: String if nameOrPath.hasPrefix("/") { correctedPath = nameOrPath.replacingOccurrences(of: baseURL!.path, with: "", options: .anchored) - name = (nameOrPath as NSString).lastPathComponent + name = nameOrPath.lastPathComponent } else { name = nameOrPath - correctedPath = (path as NSString).appendingPathComponent(nameOrPath) + correctedPath = path.appendingPathComponent(nameOrPath) } correctedPath = correctedPath.replacingOccurrences(of: "/", with: "", options: .anchored) var attributes = [String: String]() for component in components { - let keyValue = component.components(separatedBy: "=") .flatMap { $0.isEmpty ? nil : $0 } + let keyValue = component.components(separatedBy: "=").compactMap { $0.isEmpty ? nil : $0 } guard keyValue.count >= 2, !keyValue[0].isEmpty else { continue } attributes[keyValue[0].lowercased()] = keyValue.dropFirst().joined(separator: "=") } - let file = FileObject(url: url(of: correctedPath), name: name, path: correctedPath) + let file = FileObject(url: url(of: correctedPath), name: name, path: "/" + correctedPath) let dateFormatter = DateFormatter() dateFormatter.calendar = Calendar(identifier: .gregorian) dateFormatter.locale = Locale(identifier: "en_US_POSIX") @@ -920,38 +1163,37 @@ internal extension FTPFileProvider { } /// Contains error code and description returned by FTP/S provider. -public struct FileProviderFTPError: Error { +public struct FileProviderFTPError: LocalizedError { /// HTTP status code returned for error by server. public let code: Int /// Path of file/folder casued that error public let path: String /// Contents returned by server as error description - public let errorDescription: String? + public let serverDescription: String? - init(code: Int, path: String, errorDescription: String?) { + init(code: Int, path: String, serverDescription: String?) { self.code = code self.path = path - self.errorDescription = errorDescription + self.serverDescription = serverDescription + } + + init(message response: String) { + self.init(message: response, path: "") } - init(message response: String, path: String = "") { + init(message response: String, path: String) { let message = response.components(separatedBy: .newlines).last ?? "No Response" - #if swift(>=4.0) - let startIndex = (message.index(of: "-") ?? message.index(of: " ")) ?? message.startIndex + let startIndex = (message.firstIndex(of: "-") ?? message.firstIndex(of: " ")) ?? message.startIndex self.code = Int(message[.. 0 { - #if swift(>=4.0) - self.errorDescription = message[startIndex...].trimmingCharacters(in: .whitespacesAndNewlines) - #else - self.errorDescription = message.substring(from: startIndex).trimmingCharacters(in: .whitespacesAndNewlines) - #endif + self.serverDescription = message[startIndex...].trimmingCharacters(in: .whitespacesAndNewlines) } else { - self.errorDescription = message + self.serverDescription = message } } + + public var errorDescription: String? { + return serverDescription + } } diff --git a/Sources/FileObject.swift b/Sources/FileObject.swift index 56322ab..bfb959f 100644 --- a/Sources/FileObject.swift +++ b/Sources/FileObject.swift @@ -9,7 +9,7 @@ import Foundation /// Containts path, url and attributes of a file or resource. -open class FileObject: Equatable { +open class FileObject: NSObject { /// A `Dictionary` contains file information, using `URLResourceKey` keys. open internal(set) var allValues: [URLResourceKey: Any] @@ -19,6 +19,7 @@ open class FileObject: Equatable { internal init(url: URL?, name: String, path: String) { self.allValues = [URLResourceKey: Any]() + super.init() if let url = url { self.url = url } @@ -33,7 +34,10 @@ open class FileObject: Equatable { if let url = allValues[.fileURLKey] as? URL { return url } else { - let path = self.path.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? self.path + var path = self.path.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? self.path + if path.hasPrefix("/") { + path.remove(at: path.startIndex) + } return URL(string: path) ?? URL(string: "/")! } } @@ -147,32 +151,48 @@ open class FileObject: Equatable { open var isSymLink: Bool { return self.type == .symbolicLink } +} + +extension FileObject { + open override var hash: Int { + #if swift(>=4.2) + var hasher = Hasher() + hasher.combine(url) + hasher.combine(size) + hasher.combine(modifiedDate) + return hasher.finalize() + #else + let hashURL = self.url.hashValue + let hashSize = self.size.hashValue + return (hashURL << 7) &+ hashURL &+ hashSize + #endif + } - /// Check `FileObject` equality - public static func ==(lhs: FileObject, rhs: FileObject) -> Bool { - if rhs === lhs { + open override func isEqual(_ object: Any?) -> Bool { + guard let object = object as? FileObject else { return false } + if self === object { return true } - #if swift(>=3.1) - if Swift.type(of: lhs) != Swift.type(of: rhs) { + if Swift.type(of: self) != Swift.type(of: object) { return false } - #else - if type(of: lhs) != type(of: rhs) { - return false - } - #endif - if let rurl = rhs.allValues[.fileURLKey] as? URL, let lurl = lhs.allValues[.fileURLKey] as? URL { - return rurl == lurl + if let rurl = self.allValues[.fileURLKey] as? URL, let lurl = object.allValues[.fileURLKey] as? URL { + return rurl == lurl && self.size == object.size } - return rhs.path == lhs.path && rhs.size == lhs.size && rhs.modifiedDate == lhs.modifiedDate + return self.path == object.path && self.size == object.size && self.modifiedDate == object.modifiedDate } - +} + +extension FileObject { internal func mapPredicate() -> [String: Any] { - let mapDict: [URLResourceKey: String] = [.fileURLKey: "url", .nameKey: "name", .pathKey: "path", .fileSizeKey: "filesize", .creationDateKey: "creationDate", - .contentModificationDateKey: "modifiedDate", .isHiddenKey: "isHidden", .isWritableKey: "isWritable", .serverDateKey: "serverDate", .entryTagKey: "entryTag", .mimeTypeKey: "mimeType"] - let typeDict: [URLFileResourceType: String] = [.directory: "directory", .regular: "regular", .symbolicLink: "symbolicLink", .unknown: "unknown"] + let mapDict: [URLResourceKey: String] = [.fileURLKey: "url", .nameKey: "name", .pathKey: "path", + .fileSizeKey: "fileSize", .creationDateKey: "creationDate", + .contentModificationDateKey: "modifiedDate", .isHiddenKey: "isHidden", + .isWritableKey: "isWritable", .serverDateKey: "serverDate", + .entryTagKey: "entryTag", .mimeTypeKey: "mimeType"] + let typeDict: [URLFileResourceType: String] = [.directory: "directory", .regular: "regular", + .symbolicLink: "symbolicLink", .unknown: "unknown"] var result = [String: Any]() for (key, value) in allValues { if let convertkey = mapDict[key] { @@ -180,6 +200,7 @@ open class FileObject: Equatable { } } result["eTag"] = result["entryTag"] + result["filesize"] = result["fileSize"] result["isReadOnly"] = self.isReadOnly result["isDirectory"] = self.isDirectory result["isRegularFile"] = self.isRegularFile @@ -190,9 +211,11 @@ open class FileObject: Equatable { /// Converts macOS spotlight query for searching files to a query that can be used for `searchFiles()` method static public func convertPredicate(fromSpotlight query: NSPredicate) -> NSPredicate { - let mapDict: [String: URLResourceKey] = [NSMetadataItemURLKey: .fileURLKey, NSMetadataItemFSNameKey: .nameKey, NSMetadataItemPathKey: .pathKey, - NSMetadataItemFSSizeKey: .fileSizeKey, NSMetadataItemFSCreationDateKey: .creationDateKey, - NSMetadataItemFSContentChangeDateKey: .contentModificationDateKey, "kMDItemFSInvisible": .isHiddenKey, "kMDItemFSIsWriteable": .isWritableKey, "kMDItemKind": .mimeTypeKey] + let mapDict: [String: URLResourceKey] = [NSMetadataItemURLKey: .fileURLKey, NSMetadataItemFSNameKey: .nameKey, + NSMetadataItemPathKey: .pathKey, NSMetadataItemFSSizeKey: .fileSizeKey, + NSMetadataItemFSCreationDateKey: .creationDateKey, NSMetadataItemFSContentChangeDateKey: .contentModificationDateKey, + "kMDItemFSInvisible": .isHiddenKey, "kMDItemFSIsWriteable": .isWritableKey, + "kMDItemKind": .mimeTypeKey] if let cQuery = query as? NSCompoundPredicate { let newSub = cQuery.subpredicates.map { convertPredicate(fromSpotlight: $0 as! NSPredicate) } @@ -200,6 +223,7 @@ open class FileObject: Equatable { case .and: return NSCompoundPredicate(andPredicateWithSubpredicates: newSub) case .not: return NSCompoundPredicate(notPredicateWithSubpredicate: newSub[0]) case .or: return NSCompoundPredicate(orPredicateWithSubpredicates: newSub) + @unknown default: fatalError() } } else if let cQuery = query as? NSComparisonPredicate { var newLeft = cQuery.leftExpression @@ -218,7 +242,7 @@ open class FileObject: Equatable { } /// Containts attributes of a provider. -open class VolumeObject { +open class VolumeObject: NSObject { /// A `Dictionary` contains volume information, using `URLResourceKey` keys. open internal(set) var allValues: [URLResourceKey: Any] @@ -245,7 +269,16 @@ open class VolumeObject { allValues[.volumeNameKey] = newValue } } - + + /// The root directory of the resource’s volume, returned as an `URL` object. + open internal(set) var uuid: String? { + get { + return allValues[.volumeUUIDStringKey] as? String + } + set { + allValues[.volumeUUIDStringKey] = newValue + } + } /// the volume’s capacity in bytes, return -1 if is undetermined. open internal(set) var totalCapacity: Int64 { @@ -307,7 +340,6 @@ open class VolumeObject { } } - /// Sorting FileObject array by given criteria, **not thread-safe** public struct FileObjectSorting { @@ -383,8 +415,8 @@ public struct FileObjectSorting { case .nameCaseInsensitive: return ($0.name).localizedCaseInsensitiveCompare($1.name) == (ascending ? .orderedAscending : .orderedDescending) case .extension: - let kind1 = $0.isDirectory ? "folder" : ($0.path as NSString).pathExtension - let kind2 = $1.isDirectory ? "folder" : ($1.path as NSString).pathExtension + let kind1 = $0.isDirectory ? "folder" : $0.path.pathExtension + let kind2 = $1.isDirectory ? "folder" : $1.path.pathExtension return kind1.localizedCaseInsensitiveCompare(kind2) == (ascending ? .orderedAscending : .orderedDescending) case .modifiedDate: let fileMod1 = $0.modifiedDate ?? Date.distantPast diff --git a/Sources/FileProvider.swift b/Sources/FileProvider.swift index 2c1d948..3d425a9 100644 --- a/Sources/FileProvider.swift +++ b/Sources/FileProvider.swift @@ -9,9 +9,11 @@ import Foundation #if os(iOS) || os(tvOS) import UIKit +import ImageIO public typealias ImageClass = UIImage #elseif os(macOS) import Cocoa +import ImageIO public typealias ImageClass = NSImage #endif @@ -19,7 +21,7 @@ public typealias ImageClass = NSImage public typealias SimpleCompletionHandler = ((_ error: Error?) -> Void)? /// This protocol defines FileProvider neccesary functions and properties to connect and get contents list -public protocol FileProviderBasic: class, NSSecureCoding { +public protocol FileProviderBasic: class, NSSecureCoding, CustomDebugStringConvertible { /// An string to identify type of provider. static var type: String { get } @@ -109,7 +111,7 @@ public protocol FileProviderBasic: class, NSSecureCoding { Sample predicates: ``` - NSPredicate(format: "(name CONTAINS[c] 'hello') && (filesize >= 10000)") + NSPredicate(format: "(name CONTAINS[c] 'hello') && (fileSize >= 10000)") NSPredicate(format: "(modifiedDate >= %@)", Date()) NSPredicate(format: "(path BEGINSWITH %@)", "folder/child folder") ``` @@ -140,7 +142,7 @@ public protocol FileProviderBasic: class, NSSecureCoding { func url(of path: String) -> URL - /// Returns the relative path of url, wothout percent encoding. Even if url is absolute or + /// Returns the relative path of url, without percent encoding. Even if url is absolute or /// retrieved from another provider, it will try to resolve the url against `baseURL` of /// current provider. It's highly recomended to use this method for displaying purposes. /// @@ -153,7 +155,8 @@ public protocol FileProviderBasic: class, NSSecureCoding { /// - Note: To prevent race condition, use this method wisely and avoid it as far possible. /// /// - Parameter success: indicated server is reachable or not. - func isReachable(completionHandler: @escaping(_ success: Bool) -> Void) + /// - Parameter error: `Error` returned by server if occured. + func isReachable(completionHandler: @escaping(_ success: Bool, _ error: Error?) -> Void) } extension FileProviderBasic { @@ -162,6 +165,33 @@ extension FileProviderBasic { return self.searchFiles(path: path, recursive: recursive, query: predicate, foundItemHandler: foundItemHandler, completionHandler: completionHandler) } + /** + Search files inside directory using query asynchronously. + + Sample predicates: + ``` + NSPredicate(format: "(name CONTAINS[c] 'hello') && (filesize >= 10000)") + NSPredicate(format: "(modifiedDate >= %@)", Date()) + NSPredicate(format: "(path BEGINSWITH %@)", "folder/child folder") + ``` + + - Note: Don't pass Spotlight predicates to this method directly, use `FileProvider.convertSpotlightPredicateTo()` method to get usable predicate. + + - Important: A file name criteria should be provided for Dropbox. + + - Parameters: + - path: location of directory to start search + - recursive: Searching subdirectories of path + - query: An `NSPredicate` object with keys like `FileObject` members, except `size` which becomes `filesize`. + - completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured. + - files: all files meat the `query` criteria. + - error: `Error` returned by server if occured. + - Returns: An `Progress` to get progress or cancel progress. Use `completedUnitCount` to iterate count of found items. + */ + func searchFiles(path: String, recursive: Bool, query: NSPredicate, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? { + return searchFiles(path: path, recursive: recursive, query: query, foundItemHandler: nil, completionHandler: completionHandler) + } + /// The maximum number of queued operations that can execute at the same time. /// /// The default value of this property is `OperationQueue.defaultMaxConcurrentOperationCount`. @@ -174,12 +204,11 @@ extension FileProviderBasic { } } - /// Returns total and used capacity in provider container asynchronously. - @available(*, deprecated, message: "Use storageProperties which returns VolumeObject") - func storageProperties(completionHandler: @escaping (_ total: Int64, _ used: Int64) -> Void) { - self.storageProperties { (info) in - completionHandler(info?.totalCapacity ?? -1, info?.usage ?? 0) - } + public var debugDescription: String { + let typeDesc = "\(Self.type) provider" + let urlDesc = self.baseURL.map({ " - " + $0.absoluteString }) ?? "" + let credentialDesc = self.credential?.user.map({ " - " + $0.debugDescription }) ?? "" + return typeDesc + urlDesc + credentialDesc } } @@ -221,12 +250,6 @@ public protocol FileProviderBasicRemote: FileProviderBasic { var validatingCache: Bool { get set } } -internal protocol FileProviderBasicRemoteInternal: FileProviderBasic { - var completionHandlersForTasks: [Int: SimpleCompletionHandler] { get set } - var downloadCompletionHandlersForTasks: [Int: (URL) -> Void] { get set } - var dataCompletionHandlersForTasks: [Int: (Data) -> Void] { get set } -} - internal extension FileProviderBasicRemote { func returnCachedDate(with request: URLRequest, validatingCache: Bool, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Swift.Void) -> Bool { guard let cache = self.cache else { return false } @@ -409,15 +432,7 @@ public protocol FileProviderOperations: FileProviderBasic { func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? } -public extension FileProviderOperations { - /// *DEPRECATED:* Use Use FileProviderReadWrite.writeContents(path:, data:, completionHandler:) method instead. - @available(*, deprecated, message: "Use FileProviderReadWrite.writeContents(path:, data:, completionHandler:) method instead.") - @discardableResult - public func create(file: String, at: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> Progress? { - let path = (at as NSString).appendingPathComponent(file) - return (self as? FileProviderReadWrite)?.writeContents(path: path, contents: data, completionHandler: completionHandler) - } - +extension FileProviderOperations { @discardableResult public func moveItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> Progress? { return self.moveItem(path: path, to: to, overwrite: false, completionHandler: completionHandler) @@ -434,7 +449,7 @@ public extension FileProviderOperations { } } -internal extension FileProviderOperations { +extension FileProviderOperations { internal func delegateNotify(_ operation: FileOperationType, error: Error? = nil) { DispatchQueue.main.async(execute: { if let error = error { @@ -563,6 +578,72 @@ extension FileProviderReadWrite { } } +/// Defines method for fetching file contents progressivly +public protocol FileProviderReadWriteProgressive { + /** + Retreives a `Data` object with a portion contents of the file asynchronously vis contents argument of completion handler. + If path specifies a directory, or if some other error occurs, data will be nil. + + - Parameters: + - path: Path of file. + - progressHandler: a closure which will be called every time a new data received from server. + - position: Start position of returned data, indexed from zero. + - data: returned `Data` from server. + - completionHandler: a closure which will be called after receiving is completed or an error occureed. + - Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. + */ + @discardableResult + func contents(path: String, progressHandler: @escaping (_ position: Int64, _ data: Data) -> Void, completionHandler: SimpleCompletionHandler) -> Progress? + + /** + Retreives a `Data` object with a portion contents of the file asynchronously vis contents argument of completion handler. + If path specifies a directory, or if some other error occurs, data will be nil. + + - Parameters: + - path: Path of file. + - responseHandler: a closure which will be called after fetching server response. + - response: `URLResponse` returned from server. + - progressHandler: a closure which will be called every time a new data received from server. + - position: Start position of returned data, indexed from zero. + - data: returned `Data` from server. + - completionHandler: a closure which will be called after receiving is completed or an error occureed. + - Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. + */ + @discardableResult + func contents(path: String, responseHandler: ((_ response: URLResponse) -> Void)?, progressHandler: @escaping (_ position: Int64, _ data: Data) -> Void, completionHandler: SimpleCompletionHandler) -> Progress? + + /** + Retreives a `Data` object with a portion contents of the file asynchronously vis contents argument of completion handler. + If path specifies a directory, or if some other error occurs, data will be nil. + + - Parameters: + - path: Path of file. + - offset: First byte index which should be read. **Starts from 0.** + - length: Bytes count of data. Pass `-1` to read until the end of file. + - responseHandler: a closure which will be called after fetching server response. + - response: `URLResponse` returned from server. + - progressHandler: a closure which will be called every time a new data received from server. + - position: Start position of returned data, indexed from offset. + - data: returned `Data` from server. + - completionHandler: a closure which will be called after receiving is completed or an error occureed. + - Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. + */ + @discardableResult + func contents(path: String, offset: Int64, length: Int, responseHandler: ((_ response: URLResponse) -> Void)?, progressHandler: @escaping (_ position: Int64, _ data: Data) -> Void, completionHandler: SimpleCompletionHandler) -> Progress? +} + +extension FileProviderReadWriteProgressive { + @discardableResult + public func contents(path: String, progressHandler: @escaping (_ position: Int64, _ data: Data) -> Void, completionHandler: SimpleCompletionHandler) -> Progress? { + return contents(path: path, offset: 0, length: -1, responseHandler: nil, progressHandler: progressHandler, completionHandler: completionHandler) + } + + @discardableResult + public func contents(path: String, responseHandler: ((_ response: URLResponse) -> Void)?, progressHandler: @escaping (_ position: Int64, _ data: Data) -> Void, completionHandler: SimpleCompletionHandler) -> Progress? { + return contents(path: path, offset: 0, length: -1, responseHandler: responseHandler, progressHandler: progressHandler, completionHandler: completionHandler) + } +} + /// Allows a file provider to notify changes occured public protocol FileProviderMonitor: FileProviderBasic { @@ -595,6 +676,7 @@ public protocol FileProviderMonitor: FileProviderBasic { func isRegisteredForNotification(path: String) -> Bool } +#if os(macOS) || os(iOS) || os(tvOS) /// Allows undo file operations done by provider public protocol FileProvideUndoable: FileProviderOperations { /// To initialize undo manager either call `setupUndoManager()` or set it manually. @@ -611,7 +693,7 @@ public protocol FileProvideUndoable: FileProviderOperations { func canUndo(operation: FileOperationType) -> Bool } -public extension FileProvideUndoable { +extension FileProvideUndoable { public func canUndo(operation: FileOperationType) -> Bool { return undoOperation(for: operation) != nil } @@ -649,7 +731,45 @@ public extension FileProvideUndoable { self.undoManager = UndoManager() self.undoManager?.levelsOfUndo = 10 } + + public func _registerUndo(_ operation: FileOperationType) { + #if os(macOS) || os(iOS) || os(tvOS) + guard let undoManager = self.undoManager, let undoOp = self.undoOperation(for: operation) else { + return + } + + let undoBox = UndoBox(provider: self, operation: operation, undoOperation: undoOp) + undoManager.beginUndoGrouping() + undoManager.registerUndo(withTarget: undoBox, selector: #selector(UndoBox.doSimpleOperation(_:)), object: undoBox) + undoManager.setActionName(operation.actionDescription) + undoManager.endUndoGrouping() + #endif + } +} + +class UndoBox: NSObject { + weak var provider: FileProvideUndoable? + let operation: FileOperationType + let undoOperation: FileOperationType + + init(provider: FileProvideUndoable, operation: FileOperationType, undoOperation: FileOperationType) { + self.provider = provider + self.operation = operation + self.undoOperation = undoOperation + } + + @objc internal func doSimpleOperation(_ box: UndoBox) { + switch self.undoOperation { + case .move(source: let source, destination: let dest): + _=provider?.moveItem(path: source, to: dest, completionHandler: nil) + case .remove(let path): + _=provider?.removeItem(path: path, completionHandler: nil) + default: + break + } + } } +#endif /// This protocol defines method to share a public link with other users public protocol FileProviderSharing { @@ -670,6 +790,29 @@ public protocol FileProviderSharing { func publicLink(to path: String, completionHandler: @escaping (_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void) } +//efines protocol for provider allows symbolic link operations. +public protocol FileProviderSymbolicLink { + /** + Creates a symbolic link at the specified path that points to an item at the given path. + This method does not traverse symbolic links contained in destination path, making it possible + to create symbolic links to locations that do not yet exist. + Also, if the final path component is a symbolic link, that link is not followed. + + - Parameters: + - symbolicLink: The file path at which to create the new symbolic link. The last component of the path issued as the name of the link. + - withDestinationPath: The path that contains the item to be pointed to by the link. In other words, this is the destination of the link. + - completionHandler: If an error parameter was provided, a presentable `Error` will be returned. + */ + func create(symbolicLink path: String, withDestinationPath destPath: String, completionHandler: SimpleCompletionHandler) + + /// Returns the path of the item pointed to by a symbolic link. + /// + /// - Parameters: + /// - path: The path of a file or directory. + /// - completionHandler: Returns destination url of given symbolic link, or an `Error` object if it fails. + func destination(ofSymbolicLink path: String, completionHandler: @escaping (_ file: FileObject?, _ error: Error?) -> Void) +} + /// Defines protocol for provider allows all common operations. public protocol FileProvider: FileProviderOperations, FileProviderReadWrite, NSCopying { } @@ -677,17 +820,13 @@ public protocol FileProvider: FileProviderOperations, FileProviderReadWrite, NSC internal let pathTrimSet = CharacterSet(charactersIn: " /") extension FileProviderBasic { public var type: String { - #if swift(>=3.1) return Swift.type(of: self).type - #else - return type(of: self).type - #endif } public func url(of path: String) -> URL { var rpath: String = path rpath = rpath.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? rpath - if let baseURL = baseURL { + if let baseURL = baseURL?.absoluteURL { if rpath.hasPrefix("/") { rpath.remove(at: rpath.startIndex) } @@ -705,20 +844,12 @@ extension FileProviderBasic { } // resolve url string against baseurl - if baseURL?.isFileURL ?? false { - guard let baseURL = self.baseURL?.standardizedFileURL else { return url.absoluteString } - let standardPath = url.absoluteString.replacingOccurrences(of: "file:///private/var/", with: "file:///var/", options: .anchored) - let standardBase = baseURL.absoluteString.replacingOccurrences(of: "file:///private/var/", with: "file:///var/", options: .anchored) - let standardRelativePath = standardPath.replacingOccurrences(of: standardBase, with: "/").replacingOccurrences(of: "/", with: "", options: .anchored) + guard let baseURL = self.baseURL else { return url.absoluteString } + let standardRelativePath = url.absoluteString.replacingOccurrences(of: baseURL.absoluteString, with: "/").replacingOccurrences(of: "/", with: "", options: .anchored) + if URLComponents(string: standardRelativePath)?.host?.isEmpty ?? true { return standardRelativePath.removingPercentEncoding ?? standardRelativePath } else { - guard let baseURL = self.baseURL else { return url.absoluteString } - let standardRelativePath = url.absoluteString.replacingOccurrences(of: baseURL.absoluteString, with: "/").replacingOccurrences(of: "/", with: "", options: .anchored) - if URLComponents(string: standardRelativePath)?.host?.isEmpty ?? true { - return standardRelativePath.removingPercentEncoding ?? standardRelativePath - } else { - return relativePath.replacingOccurrences(of: "/", with: "", options: .anchored) - } + return relativePath.replacingOccurrences(of: "/", with: "", options: .anchored) } } @@ -759,17 +890,7 @@ extension FileProviderBasic { } _ = group.wait(timeout: .now() + 5) let finalFile = result + (!fileExt.isEmpty ? "." + fileExt : "") - return (dirPath as NSString).appendingPathComponent(finalFile) - } - - internal func urlError(_ path: String, code: URLError.Code) -> Error { - let fileURL = self.url(of: path) - return URLError(code, userInfo: [NSURLErrorKey: fileURL, NSURLErrorFailingURLErrorKey: fileURL, NSURLErrorFailingURLStringErrorKey: fileURL.absoluteString]) - } - - internal func cocoaError(_ path: String, code: CocoaError.Code) -> Error { - let fileURL = self.url(of: path) - return CocoaError(code, userInfo: [NSFilePathErrorKey: path, NSURLErrorKey: fileURL]) + return dirPath.appendingPathComponent(finalFile) } internal func NotImplemented(_ fn: String = #function, file: StaticString = #file) { @@ -779,12 +900,6 @@ extension FileProviderBasic { /// Define methods to get preview and thumbnail for files or folders public protocol ExtendedFileProvider: FileProviderBasic { - /// Returuns true if thumbnail preview is supported by provider and file type accordingly. - /// - /// - Parameter path: path of file. - /// - Returns: A `Bool` idicates path can have thumbnail. - func thumbnailOfFileSupported(path: String) -> Bool - /// Returns true if provider supports fetching properties of file like dimensions, duration, etc. /// Usually media or document files support these meta-infotmations. /// @@ -792,6 +907,30 @@ public protocol ExtendedFileProvider: FileProviderBasic { /// - Returns: A `Bool` idicates path can have properties. func propertiesOfFileSupported(path: String) -> Bool + /** + Fetching properties of file like dimensions, duration, etc. It's variant depending on file type. + Images, videos and audio files meta-information will be returned. + + - Note: `LocalFileInformationGenerator` variables can be set to change default behavior of + thumbnail and properties generator of `LocalFileProvider`. + + - Parameters: + - path: path of file. + - completionHandler: a closure with result of preview image or error. + - propertiesDictionary: A `Dictionary` of proprty keys and values. + - keys: An `Array` contains ordering of keys. + - error: Error returned by system. + */ + @discardableResult + func propertiesOfFile(path: String, completionHandler: @escaping (_ propertiesDictionary: [String: Any], _ keys: [String], _ error: Error?) -> Void) -> Progress? + + #if os(macOS) || os(iOS) || os(tvOS) + /// Returuns true if thumbnail preview is supported by provider and file type accordingly. + /// + /// - Parameter path: path of file. + /// - Returns: A `Bool` idicates path can have thumbnail. + func thumbnailOfFileSupported(path: String) -> Bool + /** Generates and returns a thumbnail preview of document asynchronously. The defualt dimension of returned image is different regarding provider type, usually 64x64 pixels. @@ -802,7 +941,8 @@ public protocol ExtendedFileProvider: FileProviderBasic { - image: `NSImage`/`UIImage` object contains preview. - error: `Error` returned by system. */ - func thumbnailOfFile(path: String, completionHandler: @escaping (_ image: ImageClass?, _ error: Error?) -> Void) + @discardableResult + func thumbnailOfFile(path: String, completionHandler: @escaping (_ image: ImageClass?, _ error: Error?) -> Void) -> Progress? /** Generates and returns a thumbnail preview of document asynchronously. The defualt dimension of returned image is different @@ -818,148 +958,150 @@ public protocol ExtendedFileProvider: FileProviderBasic { - image: `NSImage`/`UIImage` object contains preview. - error: `Error` returned by system. */ - func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping (_ image: ImageClass?, _ error: Error?) -> Void) - - /** - Fetching properties of file like dimensions, duration, etc. It's variant depending on file type. - Images, videos and audio files meta-information will be returned. - - - Note: `LocalFileInformationGenerator` variables can be set to change default behavior of - thumbnail and properties generator of `LocalFileProvider`. - - - Parameters: - - path: path of file. - - completionHandler: a closure with result of preview image or error. - - propertiesDictionary: A `Dictionary` of proprty keys and values. - - keys: An `Array` contains ordering of keys. - - error: Error returned by system. - */ - func propertiesOfFile(path: String, completionHandler: @escaping (_ propertiesDictionary: [String: Any], _ keys: [String], _ error: Error?) -> Void) + @discardableResult + func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping (_ image: ImageClass?, _ error: Error?) -> Void) -> Progress? + #endif } +#if os(macOS) || os(iOS) || os(tvOS) extension ExtendedFileProvider { - public func thumbnailOfFile(path: String, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) { - self.thumbnailOfFile(path: path, dimension: nil, completionHandler: completionHandler) + @discardableResult + public func thumbnailOfFile(path: String, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) -> Progress? { + return self.thumbnailOfFile(path: path, dimension: nil, completionHandler: completionHandler) } - internal static func convertToImage(pdfData: Data?, page: Int = 1) -> ImageClass? { + internal static func convertToImage(pdfData: Data?, page: Int = 1, maxSize: CGSize?) -> ImageClass? { guard let pdfData = pdfData else { return nil } let cfPDFData: CFData = pdfData as CFData if let provider = CGDataProvider(data: cfPDFData), let reference = CGPDFDocument(provider), let pageRef = reference.page(at: page) { - return self.convertToImage(pdfPage: pageRef) + return self.convertToImage(pdfPage: pageRef, maxSize: maxSize) } return nil } - internal static func convertToImage(pdfURL: URL, page: Int = 1) -> ImageClass? { + internal static func convertToImage(pdfURL: URL, page: Int = 1, maxSize: CGSize?) -> ImageClass? { // To accelerate, supporting only local file URL guard pdfURL.isFileURL else { return nil } if let reference = CGPDFDocument(pdfURL as CFURL), let pageRef = reference.page(at: page) { - return self.convertToImage(pdfPage: pageRef) + return self.convertToImage(pdfPage: pageRef, maxSize: maxSize) } return nil } - private static func convertToImage(pdfPage: CGPDFPage) -> ImageClass? { + private static func convertToImage(pdfPage: CGPDFPage, maxSize: CGSize?) -> ImageClass? { + let scale: CGFloat let frame = pdfPage.getBoxRect(CGPDFBox.mediaBox) - var size = frame.size - let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) - - #if os(macOS) - #if swift(>=3.2) - let ppp = Int(NSScreen.main?.backingScaleFactor ?? 1) // fetch device is retina or not - #else - let ppp = Int(NSScreen.main()?.backingScaleFactor ?? 1) // fetch device is retina or not - #endif - - size.width *= CGFloat(ppp) - size.height *= CGFloat(ppp) - - #if swift(>=3.2) - let rep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(size.width), pixelsHigh: Int(size.height), - bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: .calibratedRGB, - bytesPerRow: 0, bitsPerPixel: 0) - #else - let rep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(size.width), pixelsHigh: Int(size.height), - bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: NSCalibratedRGBColorSpace, - bytesPerRow: 0, bitsPerPixel: 0) - #endif - - guard let context = NSGraphicsContext(bitmapImageRep: rep!) else { - return nil - } - - NSGraphicsContext.saveGraphicsState() - #if swift(>=4.0) - NSGraphicsContext.current = context + if let maxSize = maxSize { + scale = min(maxSize.width / frame.width, maxSize.height / frame.height) + } else { + #if os(macOS) + scale = NSScreen.main?.backingScaleFactor ?? 1.0 // fetch device is retina or not #else - NSGraphicsContext.setCurrent(context) + scale = UIScreen.main.scale // fetch device is retina or not #endif + } + let rect = CGRect(origin: .zero, size: frame.size) + let size = CGSize(width: frame.size.width * scale, height: frame.size.height * scale) + let transform = pdfPage.getDrawingTransform(CGPDFBox.mediaBox, rect: rect, rotate: 0, preserveAspectRatio: true) + + #if os(macOS) + let rep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(size.width), pixelsHigh: Int(size.height), + bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: .calibratedRGB, + bytesPerRow: 0, bitsPerPixel: 0) + + guard let context = NSGraphicsContext(bitmapImageRep: rep!) else { + return nil + } - let transform = pdfPage.getDrawingTransform(CGPDFBox.mediaBox, rect: rect, rotate: 0, preserveAspectRatio: true) - context.cgContext.concatenate(transform) - - context.cgContext.translateBy(x: 0, y: size.height) - context.cgContext.scaleBy(x: CGFloat(ppp), y: CGFloat(-ppp)) - context.cgContext.drawPDFPage(pdfPage) - - let resultingImage = NSImage(size: size) - resultingImage.addRepresentation(rep!) - return resultingImage + NSGraphicsContext.saveGraphicsState() + NSGraphicsContext.current = context + + context.cgContext.concatenate(transform) + context.cgContext.translateBy(x: 0, y: size.height) + context.cgContext.scaleBy(x: scale, y: -scale) + context.cgContext.drawPDFPage(pdfPage) + + let resultingImage = NSImage(size: size) + resultingImage.addRepresentation(rep!) + return resultingImage #else - let ppp = Int(UIScreen.main.scale) // fetch device is retina or not - size.width *= CGFloat(ppp) - size.height *= CGFloat(ppp) - UIGraphicsBeginImageContext(size) - guard let context = UIGraphicsGetCurrentContext() else { - return nil - } - context.saveGState() - let transform = pdfPage.getDrawingTransform(CGPDFBox.mediaBox, rect: rect, rotate: 0, preserveAspectRatio: true) + + let handler : (CGContext) -> Void = { context in context.concatenate(transform) - context.translateBy(x: 0, y: size.height) - context.scaleBy(x: CGFloat(ppp), y: CGFloat(-ppp)) + context.scaleBy(x: CGFloat(scale), y: CGFloat(-scale)) context.setFillColor(UIColor.white.cgColor) context.fill(rect) context.drawPDFPage(pdfPage) - + } + + if #available(iOS 10.0, tvOS 10.0, *) { + return UIGraphicsImageRenderer(size: size).image { (rendererContext) in + handler(rendererContext.cgContext) + } + } else { + UIGraphicsBeginImageContext(size) + guard let context = UIGraphicsGetCurrentContext() else { + return nil + } + context.saveGState() + handler(context) context.restoreGState() let resultingImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return resultingImage + } #endif } - internal static func scaleDown(image: ImageClass, toSize maxSize: CGSize) -> ImageClass { - let height, width: CGFloat - if image.size.width > image.size.height { - width = maxSize.width - height = (image.size.height / image.size.width) * width + internal static func scaleDown(fileURL: URL, toSize maxSize: CGSize?) -> ImageClass? { + guard let source = CGImageSourceCreateWithURL(fileURL as CFURL, nil) else { + return nil + } + return scaleDown(source: source, toSize: maxSize) + } + + internal static func scaleDown(data: Data, toSize maxSize: CGSize?) -> ImageClass? { + guard let source = CGImageSourceCreateWithData(data as CFData, nil) else { + return nil + } + return scaleDown(source: source, toSize: maxSize) + } + + internal static func scaleDown(source: CGImageSource, toSize maxSize: CGSize?) -> ImageClass? { + let options: [NSString: Any] + if let maxSize = maxSize { + let pixelSize: CGFloat + let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) + + if let width: CGFloat = ((properties as NSDictionary?)?.object(forKey: kCGImagePropertyPixelWidth) as? CGFloat), + let height: CGFloat = ((properties as NSDictionary?)?.object(forKey: kCGImagePropertyPixelHeight) as? CGFloat) { + pixelSize = (width / maxSize.width < height / maxSize.height) ? maxSize.width : maxSize.height + } else { + pixelSize = max(maxSize.width, maxSize.height) + } + + options = [ + kCGImageSourceThumbnailMaxPixelSize: pixelSize, + kCGImageSourceCreateThumbnailFromImageAlways: true] } else { - height = maxSize.height - width = (image.size.width / image.size.height) * height + options = [ + kCGImageSourceCreateThumbnailFromImageAlways: true] } - let newSize = CGSize(width: width, height: height) - + guard let image = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else { + return nil + } #if os(macOS) - var imageRect = NSRect(origin: .zero, size: image.size) - let imageRef = image.cgImage(forProposedRect: &imageRect, context: nil, hints: nil) - - // Create NSImage from the CGImage using the new size - return NSImage(cgImage: imageRef!, size: newSize) + return ImageClass(cgImage: image, size: .zero) #else - UIGraphicsBeginImageContextWithOptions(newSize, false, 0.0) - image.draw(in: CGRect(origin: .zero, size: newSize)) - let newImage = UIGraphicsGetImageFromCurrentImageContext() ?? image - UIGraphicsEndImageContext() - return newImage + return ImageClass(cgImage: image) #endif } } +#endif /// Operation type description of file operation, included files path in associated values. public enum FileOperationType: CustomStringConvertible { @@ -1014,7 +1156,7 @@ public enum FileOperationType: CustomStringConvertible { return mirror.children.dropFirst().first?.value as? String } - init? (json: [String: AnyObject]) { + init? (json: [String: Any]) { guard let type = json["type"] as? String, let source = json["source"] as? String else { return nil } @@ -1043,17 +1185,13 @@ public enum FileOperationType: CustomStringConvertible { } internal var json: String? { - var dictionary: [String: AnyObject] = ["type": self.description as NSString] - dictionary["source"] = source as NSString? - dictionary["dest"] = destination as NSString? + var dictionary: [String: Any] = ["type": self.description] + dictionary["source"] = source + dictionary["dest"] = destination return String(jsonDictionary: dictionary) } } -/// Allows to get progress or cancel an in-progress operation, useful for remote providers -@available(*, obsoleted: 1.0, message: "Use Foudation.Progress class instead.") -public protocol OperationHandle {} - /// Delegate methods for reporting provider's operation result and progress, when it's ready to update /// user interface. /// All methods are called in main thread to avoids UI bugs. diff --git a/Sources/HTTPFileProvider.swift b/Sources/HTTPFileProvider.swift index 26f98a7..5b1f041 100644 --- a/Sources/HTTPFileProvider.swift +++ b/Sources/HTTPFileProvider.swift @@ -14,14 +14,9 @@ import Foundation No instance of this class should (and can) be created. Use derived classes instead. It leads to a crash with `fatalError()`. */ -open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, FileProviderReadWrite { +open class HTTPFileProvider: NSObject, FileProviderBasicRemote, FileProviderOperations, FileProviderReadWrite, FileProviderReadWriteProgressive { open class var type: String { fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.") } - open let baseURL: URL? - - /// **OBSOLETED** Current active path used in `contentsOfDirectory(path:completionHandler:)` method. - @available(*, obsoleted: 0.22, message: "This property is redundant with almost no use internally.") - open var currentPath: String = "" - + public let baseURL: URL? open var dispatch_queue: DispatchQueue open var operation_queue: OperationQueue { willSet { @@ -77,11 +72,15 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi return _longpollSession! } + #if os(macOS) || os(iOS) || os(tvOS) + open var undoManager: UndoManager? = nil + #endif + /** This is parent initializer for subclasses. Using this method on `HTTPFileProvider` will fail as `type` is not implemented. - Parameters: - - baseURL: Location of WebDAV server. + - baseURL: Location of server. - credential: An `URLCredential` object with `user` and `password`. - cache: A URLCache to cache downloaded files and contents. */ @@ -94,20 +93,31 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi self.cache = cache self.credential = credential - #if swift(>=3.1) - let queueLabel = "FileProvider.\(Swift.type(of: self).type)" - #else - let queueLabel = "FileProvider.\(type(of: self).type)" - #endif + let queueLabel = "FileProvider.\(Swift.type(of: self).type)" dispatch_queue = DispatchQueue(label: queueLabel, attributes: .concurrent) operation_queue = OperationQueue() operation_queue.name = "\(queueLabel).Operation" + + super.init() } public required convenience init?(coder aDecoder: NSCoder) { fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.") } + deinit { + if let sessionuuid = _session?.sessionDescription { + removeSessionHandler(for: sessionuuid) + } + + if fileProviderCancelTasksOnInvalidating { + _session?.invalidateAndCancel() + } else { + _session?.finishTasksAndInvalidate() + } + longpollSession.invalidateAndCancel() + } + public func encode(with aCoder: NSCoder) { aCoder.encode(self.baseURL, forKey: "baseURL") aCoder.encode(self.credential, forKey: "credential") @@ -123,18 +133,6 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.") } - deinit { - if let sessionuuid = _session?.sessionDescription { - removeSessionHandler(for: sessionuuid) - } - - if fileProviderCancelTasksOnInvalidating { - _session?.invalidateAndCancel() - } else { - _session?.finishTasksAndInvalidate() - } - } - open func contentsOfDirectory(path: String, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) { fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.") } @@ -147,40 +145,84 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.") } + @discardableResult open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? { fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.") } - open func isReachable(completionHandler: @escaping (Bool) -> Void) { + open func isReachable(completionHandler: @escaping (_ success: Bool, _ error: Error?) -> Void) { self.storageProperties { volume in - completionHandler(volume != nil) + if volume != nil { + completionHandler(volume != nil, nil) + return + } else { + self.contentsOfDirectory(path: "", completionHandler: { (files, error) in + completionHandler(false, error) + }) + } + } + } + + // Nothing special for these two funcs, just reimplemented to workaround a bug in swift to allow override in subclasses! + open func url(of path: String) -> URL { + var rpath: String = path + rpath = rpath.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? rpath + if let baseURL = baseURL { + if rpath.hasPrefix("/") { + rpath.remove(at: rpath.startIndex) + } + return URL(string: rpath, relativeTo: baseURL) ?? baseURL + } else { + return URL(string: rpath) ?? URL(string: "/")! + } + } + + open func relativePathOf(url: URL) -> String { + // check if url derieved from current base url + let relativePath = url.relativePath + if !relativePath.isEmpty, url.baseURL == self.baseURL { + return (relativePath.removingPercentEncoding ?? relativePath).replacingOccurrences(of: "/", with: "", options: .anchored) + } + + // resolve url string against baseurl + guard let baseURL = self.baseURL else { return url.absoluteString } + let standardRelativePath = url.absoluteString.replacingOccurrences(of: baseURL.absoluteString, with: "/").replacingOccurrences(of: "/", with: "", options: .anchored) + if URLComponents(string: standardRelativePath)?.host?.isEmpty ?? true { + return standardRelativePath.removingPercentEncoding ?? standardRelativePath + } else { + return relativePath.replacingOccurrences(of: "/", with: "", options: .anchored) } } open weak var fileOperationDelegate: FileOperationDelegate? + @discardableResult open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? { - let path = (atPath as NSString).appendingPathComponent(folderName) + "/" + let path = atPath.appendingPathComponent(folderName) + "/" return doOperation(.create(path: path), completionHandler: completionHandler) } + @discardableResult open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { return doOperation(.move(source: path, destination: toPath), overwrite: overwrite, completionHandler: completionHandler) } + @discardableResult open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { return doOperation(.copy(source: path, destination: toPath), overwrite: overwrite, completionHandler: completionHandler) } + @discardableResult open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? { return doOperation(.remove(path: path), completionHandler: completionHandler) } + @discardableResult open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { // check file is not a folder guard (try? localFile.resourceValues(forKeys: [.fileResourceTypeKey]))?.fileResourceType ?? .unknown == .regular else { dispatch_queue.async { - completionHandler?(self.urlError(localFile.path, code: .fileIsDirectory)) + completionHandler?(URLError(.fileIsDirectory, url: localFile)) } return nil } @@ -190,14 +232,15 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi return nil } let request = self.request(for: operation, overwrite: overwrite) - return upload_simple(toPath, request: request, localFile: localFile, operation: operation, completionHandler: completionHandler) + return upload_file(toPath, request: request, localFile: localFile, operation: operation, completionHandler: completionHandler) } + @discardableResult open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? { let operation = FileOperationType.copy(source: path, destination: destURL.absoluteString) let request = self.request(for: operation) - let cantLoadError = urlError(path, code: .cannotLoadFromNetwork) - return self.download_simple(path: path, request: request, operation: operation, completionHandler: { [weak self] (tempURL, error) in + let cantLoadError = URLError(.cannotLoadFromNetwork, url: self.url(of: path)) + return self.download_file(path: path, request: request, operation: operation, completionHandler: { [weak self] (tempURL, error) in do { if let error = error { throw error @@ -207,11 +250,11 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi throw cantLoadError } + #if os(macOS) || os(iOS) || os(tvOS) var coordError: NSError? NSFileCoordinator().coordinate(writingItemAt: tempURL, options: .forMoving, writingItemAt: destURL, options: .forReplacing, error: &coordError, byAccessor: { (tempURL, destURL) in do { try FileManager.default.moveItem(at: tempURL, to: destURL) - completionHandler?(nil) self?.delegateNotify(operation) } catch { @@ -223,6 +266,17 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi if let error = coordError { throw error } + #else + do { + try FileManager.default.moveItem(at: tempURL, to: destURL) + completionHandler?(nil) + self?.delegateNotify(operation) + } catch { + completionHandler?(error) + self?.delegateNotify(operation, error: error) + } + #endif + } catch { completionHandler?(error) self?.delegateNotify(operation, error: error) @@ -243,12 +297,11 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi - error: `Error` returned by system if occured. - Returns: An `Progress` to get progress or cancel progress. */ - open func contents(path: String, offset: Int64 = 0, responseHandler: ((_ response: URLResponse) -> Void)? = nil, progressHandler: @escaping (_ position: Int64, _ data: Data) -> Void, completionHandler: SimpleCompletionHandler) -> Progress? { + @discardableResult + open func contents(path: String, offset: Int64 = 0, length: Int = -1, responseHandler: ((_ response: URLResponse) -> Void)? = nil, progressHandler: @escaping (_ position: Int64, _ data: Data) -> Void, completionHandler: SimpleCompletionHandler) -> Progress? { let operation = FileOperationType.fetch(path: path) var request = self.request(for: operation) - if offset > 0 { - request.addValue("bytes \(offset)-", forHTTPHeaderField: "Range") - } + request.setValue(rangeWithOffset: offset, length: length) var position: Int64 = offset return download_progressive(path: path, request: request, operation: operation, responseHandler: responseHandler, progressHandler: { data in progressHandler(position, data) @@ -256,6 +309,7 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi }, completionHandler: (completionHandler ?? { _ in return })) } + @discardableResult open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? { if length == 0 || offset < 0 { dispatch_queue.async { @@ -266,33 +320,37 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi let operation = FileOperationType.fetch(path: path) var request = self.request(for: operation) - let cantLoadError = urlError(path, code: .cannotLoadFromNetwork) - request.set(httpRangeWithOffset: offset, length: length) - return self.download_simple(path: path, request: request, operation: operation, completionHandler: { (tempURL, error) in + let cantLoadError = URLError(.cannotLoadFromNetwork, url: self.url(of: path)) + request.setValue(rangeWithOffset: offset, length: length) + + let stream = OutputStream.toMemory() + return self.download(path: path, request: request, operation: operation, stream: stream) { (error) in do { if let error = error { throw error } - guard let tempURL = tempURL else { + guard let data = stream.property(forKey: .dataWrittenToMemoryStreamKey) as? Data else { throw cantLoadError } - - let data = try Data(contentsOf: tempURL) completionHandler(data, nil) } catch { completionHandler(nil, error) } - }) + + } } - public func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { + @discardableResult + open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { let operation = FileOperationType.modify(path: path) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else { return nil } + let data = data ?? Data() let request = self.request(for: operation, overwrite: overwrite, attributes: [.contentModificationDateKey: Date()]) - return upload_simple(path, request: request, data: data ?? Data(), operation: operation, completionHandler: completionHandler) + let stream = InputStream(data: data) + return upload(path, request: request, stream: stream, size: Int64(data.count), operation: operation, completionHandler: completionHandler) } internal func request(for operation: FileOperationType, overwrite: Bool = false, attributes: [URLResourceKey: Any] = [:]) -> URLRequest { @@ -303,16 +361,27 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.") } - internal func multiStatusHandler(source: String, data: Data, completionHandler: SimpleCompletionHandler) -> Void { + internal func multiStatusError(operation: FileOperationType, data: Data) -> FileProviderHTTPError? { // WebDAV will override this function + return nil } - fileprivate func doOperation(_ operation: FileOperationType, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? { + /** + This is the main function to init urlsession task for specified file operation. + + You won't need to override this function unless another network request must be done before intended operation, + such as retrieving file id from file path. Then you must call `super.doOperation()` + + In case you have to call super method asyncronously, create a `Progress` object and pass ot to `progress` parameter. + */ + @discardableResult + internal func doOperation(_ operation: FileOperationType, overwrite: Bool = false, progress: Progress? = nil, + completionHandler: SimpleCompletionHandler) -> Progress? { guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else { return nil } - let progress = Progress(totalUnitCount: 1) + let progress = progress ?? Progress(totalUnitCount: 1) progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey) progress.kind = .file progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) @@ -320,22 +389,32 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi let request = self.request(for: operation, overwrite: overwrite) let task = session.dataTask(with: request, completionHandler: { (data, response, error) in - var serverError: FileProviderHTTPError? - if let response = response as? HTTPURLResponse, response.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) { - serverError = self.serverError(with: code, path: operation.source, data: data) - } - - if let response = response as? HTTPURLResponse, FileProviderHTTPErrorCode(rawValue: response.statusCode) == .multiStatus, let data = data { - self.multiStatusHandler(source: operation.source, data: data, completionHandler: completionHandler) - } - - if serverError == nil && error == nil { + do { + if let error = error { + throw error + } + + if let response = response as? HTTPURLResponse { + if response.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) { + throw self.serverError(with: code, path: operation.source, data: data) + } + + if FileProviderHTTPErrorCode(rawValue: response.statusCode) == .multiStatus, let data = data, + let ms_error = self.multiStatusError(operation: operation, data: data) { + throw ms_error + } + } + + #if os(macOS) || os(iOS) || os(tvOS) + self._registerUndo(operation) + #endif progress.completedUnitCount = 1 - } else { - progress.cancel() + completionHandler?(nil) + self.delegateNotify(operation) + } catch { + completionHandler?(error) + self.delegateNotify(operation, error: error) } - completionHandler?(serverError ?? error) - self.delegateNotify(operation, error: serverError ?? error) }) task.taskDescription = operation.json progress.cancellationHandler = { [weak task] in @@ -346,62 +425,70 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi return progress } - /// This method should be used in subclasses to fetch directory content from servers which support paginated results. - /// Almost all HTTP based provider, except WebDAV, supports this method. - /// - /// - Important: Please use `[weak self]` when implementing handlers to prevent retain cycles. In these cases, - /// return `nil` as the result of handler as the operation will be aborted. - /// - /// - Parameters: - /// - path: path of directory which enqueued for listing, for informational use like errpr reporting. - /// - requestHandler: Get token of next page and returns appropriate `URLRequest` to be sent to server. - /// handler can return `nil` to cancel entire operation. - /// - token: Token of the page which `URLRequest` is needed, token will be `nil` for initial page. . - /// - pageHandler: Handler which is called after fetching results of a page to parse data. will return parse result as - /// array of `FileObject` or error if data is nil or parsing is failed. Method will not continue to next page if - /// `error` is returned, otherwise `nextToken` will be used for next page. `nil` value for `newToken` will indicate - /// last page of directory contents. - /// - data: Raw data returned from server. Handler should parse them and return files. - /// - progress: `Progress` object that `completedUnits` will be increased when a new `FileObject` is parsed in method. - /// - completionHandler: All file objects returned by `pageHandler` will be passed to this handler, or error if occured. - /// This handler will be called when `pageHandler` returns `nil for `newToken`. - /// - contents: all files parsed via `pageHandler` will be return aggregated. - /// - error: `Error` returned by server. `nil` means success. If exists, it means `contents` are incomplete. - internal func paginated(_ path: String, requestHandler: @escaping (_ token: String?) -> URLRequest?, pageHandler: @escaping (_ data: Data?, _ progress: Progress) -> (files: [FileObject], error: Error?, newToken: String?), completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) -> Progress { + // codebeat:disable[ARITY] + + /** + This method should be used in subclasses to fetch directory content from servers which support paginated results. + Almost all HTTP based provider, except WebDAV, supports this method. + + - Important: Please use `[weak self]` when implementing handlers to prevent retain cycles. In these cases, + return `nil` as the result of handler as the operation will be aborted. + + - Parameters: + - path: path of directory which enqueued for listing, for informational use like errpr reporting. + - requestHandler: Get token of next page and returns appropriate `URLRequest` to be sent to server. + handler can return `nil` to cancel entire operation. + - token: Token of the page which `URLRequest` is needed, token will be `nil` for initial page. + - pageHandler: Handler which is called after fetching results of a page to parse data. will return parse result as + array of `FileObject` or error if data is nil or parsing is failed. Method will not continue to next page if + `error` is returned, otherwise `nextToken` will be used for next page. `nil` value for `newToken` will indicate + last page of directory contents. + - data: Raw data returned from server. Handler should parse them and return files. + - progress: `Progress` object that `completedUnits` will be increased when a new `FileObject` is parsed in method. + - completionHandler: All file objects returned by `pageHandler` will be passed to this handler, or error if occured. + This handler will be called when `pageHandler` returns `nil for `newToken`. + - contents: all files parsed via `pageHandler` will be return aggregated. + - error: `Error` returned by server. `nil` means success. If exists, it means `contents` are incomplete. + */ + internal func paginated(_ path: String, requestHandler: @escaping (_ token: String?) -> URLRequest?, + pageHandler: @escaping (_ data: Data?, _ progress: Progress) -> (files: [FileObject], error: Error?, newToken: String?), + completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) -> Progress { let progress = Progress(totalUnitCount: -1) self.paginated(path, startToken: nil, currentProgress: progress, previousResult: [], requestHandler: requestHandler, pageHandler: pageHandler, completionHandler: completionHandler) return progress } - // codebeat:disable[ARITY] - private func paginated(_ path: String, startToken: String?, currentProgress progress: Progress, previousResult: [FileObject], requestHandler: @escaping (_ token: String?) -> URLRequest?, pageHandler: @escaping (_ data: Data?, _ progress: Progress) -> (files: [FileObject], error: Error?, newToken: String?), completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) { + private func paginated(_ path: String, startToken: String?, currentProgress progress: Progress, + previousResult: [FileObject], requestHandler: @escaping (_ token: String?) -> URLRequest?, + pageHandler: @escaping (_ data: Data?, _ progress: Progress) -> (files: [FileObject], error: Error?, newToken: String?), + completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) { guard !progress.isCancelled, let request = requestHandler(startToken) else { return } let task = session.dataTask(with: request, completionHandler: { (data, response, error) in - if let error = error { - completionHandler(previousResult, error) - return - } - if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { - let responseError = self.serverError(with: rCode, path: path, data: data) - completionHandler(previousResult, responseError) - return - } - - let (newFiles, err, newToken) = pageHandler(data, progress) - if let error = err { + do { + if let error = error { + throw error + } + if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { + throw self.serverError(with: rCode, path: path, data: data) + } + + let (newFiles, err, newToken) = pageHandler(data, progress) + if let error = err { + throw error + } + + let files = previousResult + newFiles + if let newToken = newToken, !progress.isCancelled { + _ = self.paginated(path, startToken: newToken, currentProgress: progress, previousResult: files, requestHandler: requestHandler, pageHandler: pageHandler, completionHandler: completionHandler) + } else { + completionHandler(files, nil) + } + } catch { completionHandler(previousResult, error) - return } - let files = previousResult + newFiles - if let newToken = newToken, !progress.isCancelled { - _ = self.paginated(path, startToken: newToken, currentProgress: progress, previousResult: files, requestHandler: requestHandler, pageHandler: pageHandler, completionHandler: completionHandler) - } else { - completionHandler(files, nil) - } - }) progress.cancellationHandler = { [weak task] in task?.cancel() @@ -413,16 +500,60 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi internal var maxUploadSimpleSupported: Int64 { return Int64.max } - internal func upload_simple(_ targetPath: String, request: URLRequest, data: Data? = nil, localFile: URL? = nil, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? { - let size: Int64 - if let data = data { - size = Int64(data.count) - } else if let localFile = localFile { - let fSize = (try? localFile.resourceValues(forKeys: [.fileSizeKey]))?.fileSize - size = Int64(fSize ?? -1) - } else { + func upload_task(_ targetPath: String, progress: Progress, task: URLSessionTask, operation: FileOperationType, + completionHandler: SimpleCompletionHandler) -> Void { + + var allData = Data() + dataCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { data in + allData.append(data) + } + + completionHandlersForTasks[self.session.sessionDescription!]?[task.taskIdentifier] = { [weak self] error in + var responseError: FileProviderHTTPError? + if let code = (task.response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { + responseError = self?.serverError(with: rCode, path: targetPath, data: allData) + } + if !(responseError == nil && error == nil) { + progress.cancel() + } + completionHandler?(responseError ?? error) + self?.delegateNotify(operation, error: responseError ?? error) + } + task.taskDescription = operation.json + sessionDelegate?.observerProgress(of: task, using: progress, kind: .upload) + progress.cancellationHandler = { [weak task] in + task?.cancel() + } + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) + task.resume() + } + + func upload(_ targetPath: String, request: URLRequest, stream: InputStream, size: Int64, operation: FileOperationType, + completionHandler: SimpleCompletionHandler) -> Progress? { + if size > maxUploadSimpleSupported { + let error = self.serverError(with: .payloadTooLarge, path: targetPath, data: nil) + completionHandler?(error) + self.delegateNotify(operation, error: error) return nil } + + let progress = Progress(totalUnitCount: size) + progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey) + progress.kind = .file + progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) + + var request = request + request.httpBodyStream = stream + let task = session.uploadTask(withStreamedRequest: request) + self.upload_task(targetPath, progress: progress, task: task, operation: operation, completionHandler: completionHandler) + + return progress + } + + func upload_file(_ targetPath: String, request: URLRequest, localFile: URL, operation: FileOperationType, + completionHandler: SimpleCompletionHandler) -> Progress? { + let fSize = (try? localFile.resourceValues(forKeys: [.fileSizeKey]))?.allValues[.fileSizeKey] as? Int64 + let size = Int64(fSize ?? -1) if size > maxUploadSimpleSupported { let error = self.serverError(with: .payloadTooLarge, path: targetPath, data: nil) completionHandler?(error) @@ -430,53 +561,80 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi return nil } - var progress = Progress(totalUnitCount: -1) + let progress = Progress(totalUnitCount: size) progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey) progress.kind = .file progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) - progress.totalUnitCount = size - let taskHandler = { (task: URLSessionTask) -> Void in - completionHandlersForTasks[self.session.sessionDescription!]?[task.taskIdentifier] = { [weak self] error in - var responseError: FileProviderHTTPError? - if let code = (task.response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { - // We can't fetch server result from delegate! - responseError = self?.serverError(with: rCode, path: targetPath, data: nil) - } - if !(responseError == nil && error == nil) { - progress.cancel() - } - completionHandler?(responseError ?? error) - self?.delegateNotify(operation, error: responseError ?? error) + #if os(macOS) || os(iOS) || os(tvOS) + var error: NSError? + NSFileCoordinator().coordinate(readingItemAt: localFile, options: .forUploading, error: &error, byAccessor: { (url) in + let task = self.session.uploadTask(with: request, fromFile: localFile) + self.upload_task(targetPath, progress: progress, task: task, operation: operation, completionHandler: completionHandler) + }) + if let error = error { + completionHandler?(error) + } + #else + self.upload_task(targetPath, progress: progress, task: task, operation: operation, completionHandler: completionHandler) + #endif + + return progress + } + + internal func download(path: String, request: URLRequest, operation: FileOperationType, + responseHandler: ((_ response: URLResponse) -> Void)? = nil, + stream: OutputStream, + completionHandler: @escaping (_ error: Error?) -> Void) -> Progress? { + let progress = Progress(totalUnitCount: -1) + progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey) + progress.kind = .file + progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) + + let task = session.dataTask(with: request) + if let responseHandler = responseHandler { + responseCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { response in + responseHandler(response) } - task.taskDescription = operation.json - task.addObserver(self.sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent), options: .new, context: &progress) - progress.cancellationHandler = { [weak task] in + } + + stream.open() + dataCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { [weak task, weak self] data in + guard !data.isEmpty else { return } + task.flatMap { self?.delegateNotify(operation, progress: Double($0.countOfBytesReceived) / Double($0.countOfBytesExpectedToReceive)) } + + let result = (try? stream.write(data: data)) ?? -1 + if result < 0 { + completionHandler(stream.streamError!) + self?.delegateNotify(operation, error: stream.streamError!) task?.cancel() } - progress.setUserInfoObject(Date(), forKey: .startingTimeKey) - task.resume() } - if let data = data { - let task = session.uploadTask(with: request, from: data) - taskHandler(task) - } else if let localFile = localFile { - var error: NSError? - NSFileCoordinator().coordinate(readingItemAt: localFile, options: .forUploading, error: &error, byAccessor: { (url) in - let task = self.session.uploadTask(with: request, fromFile: localFile) - taskHandler(task) - }) - if let error = error { - completionHandler?(error) + completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in + if error != nil { + progress.cancel() } + stream.close() + completionHandler(error) + self.delegateNotify(operation, error: error) } + task.taskDescription = operation.json + sessionDelegate?.observerProgress(of: task, using: progress, kind: .download) + progress.cancellationHandler = { [weak task] in + task?.cancel() + } + progress.setUserInfoObject(Date(), forKey: .startingTimeKey) + task.resume() return progress } - internal func download_progressive(path: String, request: URLRequest, operation: FileOperationType, responseHandler: ((_ response: URLResponse) -> Void)? = nil, progressHandler: @escaping (_ data: Data) -> Void, completionHandler: @escaping (_ error: Error?) -> Void) -> Progress? { - var progress = Progress(totalUnitCount: -1) + internal func download_progressive(path: String, request: URLRequest, operation: FileOperationType, + responseHandler: ((_ response: URLResponse) -> Void)? = nil, + progressHandler: @escaping (_ data: Data) -> Void, + completionHandler: @escaping (_ error: Error?) -> Void) -> Progress? { + let progress = Progress(totalUnitCount: -1) progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey) progress.kind = .file progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) @@ -488,7 +646,8 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi } } - dataCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { data in + dataCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { [weak task, weak self] data in + task.flatMap { self?.delegateNotify(operation, progress: Double($0.countOfBytesReceived) / Double($0.countOfBytesExpectedToReceive)) } progressHandler(data) } @@ -501,8 +660,7 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi } task.taskDescription = operation.json - task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: &progress) - task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: &progress) + sessionDelegate?.observerProgress(of: task, using: progress, kind: .download) progress.cancellationHandler = { [weak task] in task?.cancel() } @@ -511,19 +669,20 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi return progress } - internal func download_simple(path: String, request: URLRequest, operation: FileOperationType, completionHandler: @escaping ((_ tempURL: URL?, _ error: Error?) -> Void)) -> Progress? { - var progress = Progress(totalUnitCount: -1) + internal func download_file(path: String, request: URLRequest, operation: FileOperationType, + completionHandler: @escaping ((_ tempURL: URL?, _ error: Error?) -> Void)) -> Progress? { + let progress = Progress(totalUnitCount: -1) progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey) progress.kind = .file progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) let task = session.downloadTask(with: request) completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in - if error != nil { + if let error = error { progress.cancel() + completionHandler(nil, error) + self.delegateNotify(operation, error: error) } - completionHandler(nil, error) - self.delegateNotify(operation, error: error) } downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { tempURL in guard let httpResponse = task.response as? HTTPURLResponse , httpResponse.statusCode < 300 else { @@ -541,8 +700,7 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi completionHandler(tempURL, nil) } task.taskDescription = operation.json - task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: &progress) - task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: &progress) + sessionDelegate?.observerProgress(of: task, using: progress, kind: .download) progress.cancellationHandler = { [weak task] in task?.cancel() } @@ -553,3 +711,7 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi } extension HTTPFileProvider: FileProvider { } + +#if os(macOS) || os(iOS) || os(tvOS) +extension HTTPFileProvider: FileProvideUndoable { } +#endif diff --git a/Sources/LocalFileProvider.swift b/Sources/LocalFileProvider.swift index ee9cb12..1a856e4 100644 --- a/Sources/LocalFileProvider.swift +++ b/Sources/LocalFileProvider.swift @@ -14,12 +14,9 @@ import Foundation it uses `FileManager` foundation class with some additions like searching and reading a portion of file. */ -open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndoable { +open class LocalFileProvider: NSObject, FileProvider, FileProviderMonitor, FileProviderSymbolicLink { open class var type: String { return "Local" } open fileprivate(set) var baseURL: URL? - /// **OBSOLETED** Current active path used in `contentsOfDirectory(path:completionHandler:)` method. - @available(*, obsoleted: 0.21, message: "This property is redundant with almost no use internally.") - open var currentPath: String = "" open var dispatch_queue: DispatchQueue open var operation_queue: OperationQueue open weak var delegate: FileProviderDelegate? @@ -31,8 +28,9 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo open private(set) var opFileManager = FileManager() fileprivate var fileProviderManagerDelegate: LocalFileProviderManagerDelegate? = nil + #if os(macOS) || os(iOS) || os(tvOS) open var undoManager: UndoManager? = nil - + /** Forces file operations to use `NSFileCoordinating`, should be set `true` if: - Files are on ubiquity (iCloud) container. @@ -43,6 +41,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo otherwise it's `false` to accelerate operations. */ open var isCoorinating: Bool + #endif /** Initializes provider for the specified common directory in the requested domains. @@ -56,6 +55,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo self.init(baseURL: FileManager.default.urls(for: directory, in: domainMask).first!) } + #if os(macOS) || os(iOS) || os(tvOS) /** Failable initializer for the specified shared container directory, allows data and files to be shared among app and extensions regarding sandbox requirements. Container ID is same with app group specified in project `Capabilities` @@ -92,6 +92,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo try? fileManager.createDirectory(at: finalBaseURL, withIntermediateDirectories: true) } + #endif /// Initializes provider for the specified local URL. /// @@ -104,29 +105,39 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo self.credential = nil self.isCoorinating = false - #if swift(>=3.1) let queueLabel = "FileProvider.\(Swift.type(of: self).type)" - #else - let queueLabel = "FileProvider.\(type(of: self).type)" - #endif dispatch_queue = DispatchQueue(label: queueLabel, attributes: .concurrent) operation_queue = OperationQueue() operation_queue.name = "\(queueLabel).Operation" + super.init() + fileProviderManagerDelegate = LocalFileProviderManagerDelegate(provider: self) opFileManager.delegate = fileProviderManagerDelegate } public required convenience init?(coder aDecoder: NSCoder) { - guard let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL else { + guard let baseURL = aDecoder.decodeObject(of: NSURL.self, forKey: "baseURL") as URL? else { + if #available(macOS 10.11, iOS 9.0, tvOS 9.0, *) { + aDecoder.failWithError(CocoaError(.coderValueNotFound, + userInfo: [NSLocalizedDescriptionKey: "Base URL is not set."])) + } return nil } self.init(baseURL: baseURL) self.isCoorinating = aDecoder.decodeBool(forKey: "isCoorinating") } + deinit { + let monitors = self.monitors + self.monitors = [] + for monitor in monitors { + monitor.stop() + } + } + open func encode(with aCoder: NSCoder) { - aCoder.encode(self.baseURL, forKey: "currentPath") + aCoder.encode(self.baseURL, forKey: "baseURL") aCoder.encode(self.isCoorinating, forKey: "isCoorinating") } @@ -137,7 +148,9 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo public func copy(with zone: NSZone? = nil) -> Any { let copy = LocalFileProvider(baseURL: self.baseURL!) copy.undoManager = self.undoManager + #if os(macOS) || os(iOS) || os(tvOS) copy.isCoorinating = self.isCoorinating + #endif copy.delegate = self.delegate copy.fileOperationDelegate = self.fileOperationDelegate return copy @@ -147,7 +160,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo dispatch_queue.async { do { let contents = try self.fileManager.contentsOfDirectory(at: self.url(of: path), includingPropertiesForKeys: nil, options: .skipsSubdirectoryDescendants) - let filesAttributes = contents.flatMap({ (fileURL) -> LocalFileObject? in + let filesAttributes = contents.compactMap({ (fileURL) -> LocalFileObject? in let path = self.relativePathOf(url: fileURL) return LocalFileObject(fileWithPath: path, relativeTo: self.baseURL) }) @@ -175,6 +188,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo } } + @discardableResult open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? { let progress = Progress(totalUnitCount: -1) progress.setUserInfoObject(self.url(of: path), forKey: .fileURLKey) @@ -203,54 +217,49 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo return progress } - open func isReachable(completionHandler: @escaping (_ success: Bool) -> Void) { + open func isReachable(completionHandler: @escaping (_ success: Bool, _ error: Error?) -> Void) { dispatch_queue.async { - completionHandler(self.fileManager.isReadableFile(atPath: self.baseURL!.path)) + do { + let isReachable = try self.baseURL!.checkResourceIsReachable() + completionHandler(isReachable, nil) + } catch { + completionHandler(false, error) + } + } + } + + open func relativePathOf(url: URL) -> String { + // check if url derieved from current base url + let relativePath = url.relativePath + if !relativePath.isEmpty, url.baseURL == self.baseURL { + return (relativePath.removingPercentEncoding ?? relativePath).replacingOccurrences(of: "/", with: "", options: .anchored) } + + guard let baseURL = self.baseURL?.standardizedFileURL else { return url.absoluteString } + let standardPath = url.absoluteString.replacingOccurrences(of: "file:///private/var/", with: "file:///var/", options: .anchored) + let standardBase = baseURL.absoluteString.replacingOccurrences(of: "file:///private/var/", with: "file:///var/", options: .anchored) + let standardRelativePath = standardPath.replacingOccurrences(of: standardBase, with: "/").replacingOccurrences(of: "/", with: "", options: .anchored) + return standardRelativePath.removingPercentEncoding ?? standardRelativePath } open weak var fileOperationDelegate : FileOperationDelegate? @discardableResult open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? { - let operation = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(folderName) + "/") + let operation = FileOperationType.create(path: atPath.appendingPathComponent(folderName) + "/") return self.doOperation(operation, completionHandler: completionHandler) } @discardableResult open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? { let operation = FileOperationType.move(source: path, destination: toPath) - - let fileExists = ((try? self.url(of: toPath).checkResourceIsReachable()) ?? false) || - ((try? self.url(of: toPath).checkPromisedItemIsReachable()) ?? false) - if !overwrite && fileExists { - let e = self.cocoaError(toPath, code: .fileWriteFileExists) - dispatch_queue.async { - completionHandler?(e) - } - self.delegateNotify(operation, error: e) - return nil - } - - return self.doOperation(operation, completionHandler: completionHandler) + return self.doOperation(operation, overwrite: overwrite, completionHandler: completionHandler) } @discardableResult open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? { let operation = FileOperationType.copy(source: path, destination: toPath) - - let fileExists = ((try? self.url(of: toPath).checkResourceIsReachable()) ?? false) || - ((try? self.url(of: toPath).checkPromisedItemIsReachable()) ?? false) - if !overwrite && fileExists { - let e = self.cocoaError(toPath, code: .fileWriteFileExists) - dispatch_queue.async { - completionHandler?(e) - } - self.delegateNotify(operation, error: e) - return nil - } - - return self.doOperation(operation, completionHandler: completionHandler) + return self.doOperation(operation, overwrite: overwrite, completionHandler: completionHandler) } @discardableResult @@ -262,18 +271,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo @discardableResult open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { let operation = FileOperationType.copy(source: localFile.absoluteString, destination: toPath) - - let fileExists = ((try? self.url(of: toPath).checkResourceIsReachable()) ?? false) || - ((try? self.url(of: toPath).checkPromisedItemIsReachable()) ?? false) - if !overwrite && fileExists { - let e = self.cocoaError(toPath, code: .fileWriteFileExists) - dispatch_queue.async { - completionHandler?(e) - } - self.delegateNotify(operation, error: e) - return nil - } - return self.doOperation(operation, forUploading: true, completionHandler: completionHandler) + return self.doOperation(operation, overwrite: overwrite, forUploading: true, completionHandler: completionHandler) } @discardableResult @@ -282,15 +280,8 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo return self.doOperation(operation, completionHandler: completionHandler) } - @objc dynamic func doSimpleOperation(_ box: UndoBox) { - guard let _ = self.undoManager else { return } - _ = self.doOperation(box.undoOperation) { (_) in - return - } - } - @discardableResult - fileprivate func doOperation(_ operation: FileOperationType, data: Data? = nil, atomically: Bool = false, forUploading: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? { + fileprivate func doOperation(_ operation: FileOperationType, data: Data? = nil, overwrite: Bool = true, atomically: Bool = false, forUploading: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? { let progress = Progress(totalUnitCount: -1) progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey) progress.kind = .file @@ -312,22 +303,21 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo let source: URL = urlofpath(path: sourcePath) progress.setUserInfoObject(source, forKey: .fileURLKey) - let dest: URL? - if let destPath = destPath { - dest = urlofpath(path: destPath) - } else { - dest = nil - } + let dest = destPath.map(urlofpath(path:)) - if let undoManager = self.undoManager, let undoOp = self.undoOperation(for: operation) { - let undoBox = UndoBox(provider: self, operation: operation, undoOperation: undoOp) - undoManager.beginUndoGrouping() - undoManager.registerUndo(withTarget: self, selector: #selector(LocalFileProvider.doSimpleOperation(_:)), object: undoBox) - undoManager.setActionName(operation.actionDescription) - undoManager.endUndoGrouping() + if !overwrite, let dest = dest, /* fileExists */ ((try? dest.checkResourceIsReachable()) ?? false) || + ((try? dest.checkPromisedItemIsReachable()) ?? false) { + let e = CocoaError(.fileWriteFileExists, path: destPath!) + dispatch_queue.async { + completionHandler?(e) + } + self.delegateNotify(operation, error: e) + return nil } + #if os(macOS) || os(iOS) || os(tvOS) var successfulSecurityScopedResourceAccess = false + #endif let operationHandler: (URL, URL?) -> Void = { source, dest in do { @@ -360,19 +350,26 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo default: return } + #if os(macOS) || os(iOS) || os(tvOS) if successfulSecurityScopedResourceAccess { source.stopAccessingSecurityScopedResource() } - + #endif + + #if os(macOS) || os(iOS) || os(tvOS) + self._registerUndo(operation) + #endif progress.completedUnitCount = progress.totalUnitCount self.dispatch_queue.async { completionHandler?(nil) } self.delegateNotify(operation) } catch { + #if os(macOS) || os(iOS) || os(tvOS) if successfulSecurityScopedResourceAccess { source.stopAccessingSecurityScopedResource() } + #endif progress.cancel() self.dispatch_queue.async { completionHandler?(error) @@ -381,6 +378,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo } } + #if os(macOS) || os(iOS) || os(tvOS) if isCoorinating { successfulSecurityScopedResourceAccess = source.startAccessingSecurityScopedResource() var intents = [NSFileAccessIntent]() @@ -411,6 +409,11 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo operationHandler(source, dest) } } + #else + operation_queue.addOperation { + operationHandler(source, dest) + } + #endif return progress } @@ -444,6 +447,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo } } + #if os(macOS) || os(iOS) || os(tvOS) if isCoorinating { let intent = NSFileAccessIntent.readingIntent(with: url, options: .withoutChanges) coordinated(intents: [intent], operationHandler: operationHandler, errorHandler: { error in @@ -457,6 +461,11 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo operationHandler(url) } } + #else + dispatch_queue.async { + operationHandler(url) + } + #endif return progress } @@ -487,7 +496,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo let operationHandler: (URL) -> Void = { url in do { guard let handle = FileHandle(forReadingAtPath: url.path) else { - throw self.cocoaError(path, code: .fileNoSuchFile) + throw CocoaError(.fileNoSuchFile, path: path) } defer { @@ -498,13 +507,13 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo progress.totalUnitCount = size guard size > offset else { progress.cancel() - throw self.cocoaError(path, code: .fileReadTooLarge) + throw CocoaError(.fileReadTooLarge, path: path) } progress.setUserInfoObject(Date(), forKey: .startingTimeKey) handle.seek(toFileOffset: UInt64(offset)) guard Int64(handle.offsetInFile) == offset else { progress.cancel() - throw self.cocoaError(path, code: .fileReadTooLarge) + throw CocoaError(.fileReadTooLarge, path: path) } let data = handle.readData(ofLength: length) @@ -522,6 +531,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo } } + #if os(macOS) || os(iOS) || os(tvOS) if isCoorinating { let intent = NSFileAccessIntent.readingIntent(with: url, options: .withoutChanges) coordinated(intents: [intent], operationHandler: operationHandler, errorHandler: { error in @@ -533,7 +543,11 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo operationHandler(url) } } - + #else + dispatch_queue.async { + operationHandler(url) + } + #endif return progress } @@ -542,7 +556,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo let fileExists = ((try? self.url(of: path).checkResourceIsReachable()) ?? false) || ((try? self.url(of: path).checkPromisedItemIsReachable()) ?? false) if !overwrite && fileExists { - let e = self.cocoaError(path, code: .fileWriteFileExists) + let e = CocoaError(.fileWriteFileExists, path: path) dispatch_queue.async { completionHandler?(e) } @@ -554,24 +568,19 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo return self.doOperation(operation, data: data ?? Data(), atomically: atomically, completionHandler: completionHandler) } - fileprivate var monitors = [LocalFolderMonitor]() + fileprivate var monitors = [LocalFileMonitor]() open func registerNotifcation(path: String, eventHandler: @escaping (() -> Void)) { self.unregisterNotifcation(path: path) - let dirurl = self.url(of: path) - let isdir = (try? dirurl.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory ?? false - if !isdir { - return - } - let monitor = LocalFolderMonitor(url: dirurl) { - eventHandler() + let url = self.url(of: path) + if let monitor = LocalFileMonitor(url: url, handler: eventHandler) { + monitor.start() + monitors.append(monitor) } - monitor.start() - monitors.append(monitor) } open func unregisterNotifcation(path: String) { - var removedMonitor: LocalFolderMonitor? + var removedMonitor: LocalFileMonitor? for (i, monitor) in monitors.enumerated() { if self.relativePathOf(url: monitor.url) == path { removedMonitor = monitors.remove(at: i) @@ -585,22 +594,20 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo return monitors.map( { self.relativePathOf(url: $0.url) } ).contains(path.trimmingCharacters(in: CharacterSet(charactersIn: "/"))) } - /** - Creates a symbolic link at the specified path that points to an item at the given path. - This method does not traverse symbolic links contained in destination path, making it possible - to create symbolic links to locations that do not yet exist. - Also, if the final path component is a symbolic link, that link is not followed. - - - Parameters: - - symbolicLink: The file path at which to create the new symbolic link. The last component of the path issued as the name of the link. - - withDestinationPath: The path that contains the item to be pointed to by the link. In other words, this is the destination of the link. - - completionHandler: If an error parameter was provided, a presentable `Error` will be returned. - */ open func create(symbolicLink path: String, withDestinationPath destPath: String, completionHandler: SimpleCompletionHandler) { operation_queue.addOperation { let operation = FileOperationType.link(link: path, target: destPath) do { - try self.opFileManager.createSymbolicLink(at: self.url(of: path), withDestinationURL: self.url(of: destPath)) + let url = self.url(of: path) + let destURL = self.url(of: destPath) + let homePath = NSHomeDirectory() + if destURL.path.hasPrefix(homePath) { + let canonicalHomePath = "/" + homePath.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + let destRelativePath = destURL.path.replacingOccurrences(of: canonicalHomePath, with: "~", options: .anchored) + try self.opFileManager.createSymbolicLink(atPath: url.path, withDestinationPath: destRelativePath) + } else { + try self.opFileManager.createSymbolicLink(at: url, withDestinationURL: destURL) + } completionHandler?(nil) self.delegateNotify(operation) } catch { @@ -610,17 +617,13 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo } } - /// Returns the path of the item pointed to by a symbolic link. - /// - /// - Parameters: - /// - path: The path of a file or directory. - /// - completionHandler: Returns destination url of given symbolic link, or an `Error` object if it fails. - open func destination(ofSymbolicLink path: String, completionHandler: @escaping (_ url: URL?, _ error: Error?) -> Void) { + open func destination(ofSymbolicLink path: String, completionHandler: @escaping (_ file: FileObject?, _ error: Error?) -> Void) { dispatch_queue.async { do { let destPath = try self.opFileManager.destinationOfSymbolicLink(atPath: self.url(of: path).path) - let destUrl = URL(fileURLWithPath: destPath) - completionHandler(destUrl, nil) + let absoluteDestPath = (destPath as NSString).expandingTildeInPath + let file = LocalFileObject(fileWithPath: absoluteDestPath, relativeTo: self.baseURL) + completionHandler(file, nil) } catch { completionHandler(nil, error) } @@ -628,6 +631,9 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo } } +#if os(macOS) || os(iOS) || os(tvOS) +extension LocalFileProvider: FileProvideUndoable { } + internal extension LocalFileProvider { func coordinated(intents: [NSFileAccessIntent], operationHandler: @escaping (_ url: URL) -> Void, errorHandler: ((_ error: Error) -> Void)? = nil) { let coordinator = NSFileCoordinator(filePresenter: nil) @@ -659,3 +665,5 @@ internal extension LocalFileProvider { } } } +#endif + diff --git a/Sources/LocalHelper.swift b/Sources/LocalHelper.swift index c2ca7aa..5e0d0e9 100644 --- a/Sources/LocalHelper.swift +++ b/Sources/LocalHelper.swift @@ -17,12 +17,19 @@ public final class LocalFileObject: FileObject { /// Initiates a `LocalFileObject` with attributes of file in path. public convenience init? (fileWithPath path: String, relativeTo relativeURL: URL?) { var fileURL: URL? - var rpath = path.replacingOccurrences(of: relativeURL?.path ?? "", with: "", options: .anchored).replacingOccurrences(of: "/", with: "", options: .anchored) + var rpath = path.replacingOccurrences(of: "/", with: "", options: .anchored) + var resolvedRelativeURL: URL? + if let relPath = relativeURL?.path.replacingOccurrences(of: "/", with: "", options: .anchored), rpath.hasPrefix(relPath) { + rpath = rpath.replacingOccurrences(of: relPath, with: "", options: .anchored).replacingOccurrences(of: "/", with: "", options: .anchored) + resolvedRelativeURL = relativeURL + } else { + resolvedRelativeURL = relativeURL + } if #available(iOS 9.0, macOS 10.11, *) { - fileURL = URL(fileURLWithPath: rpath, relativeTo: relativeURL) + fileURL = URL(fileURLWithPath: rpath, relativeTo: resolvedRelativeURL) } else { rpath = rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? rpath - fileURL = URL(string: rpath, relativeTo: relativeURL) ?? relativeURL + fileURL = URL(string: rpath, relativeTo: resolvedRelativeURL) ?? resolvedRelativeURL } if let fileURL = fileURL { @@ -48,7 +55,7 @@ public final class LocalFileObject: FileObject { } /// The total size allocated on disk for the file - open internal(set) var allocatedSize: Int64 { + public internal(set) var allocatedSize: Int64 { get { return allValues[.fileAllocatedSizeKey] as? Int64 ?? 0 } @@ -60,7 +67,7 @@ public final class LocalFileObject: FileObject { /// The document identifier is a value assigned by the kernel/system to a file or directory. /// This value is used to identify the document regardless of where it is moved on a volume. /// The identifier persists across system restarts. - open internal(set) var id: Int? { + public internal(set) var id: Int? { get { return allValues[.documentIdentifierKey] as? Int } @@ -71,7 +78,7 @@ public final class LocalFileObject: FileObject { /// The revision of file, which changes when a file contents are modified. /// Changes to attributes or other file metadata do not change the identifier. - open var rev: String? { + public var rev: String? { get { let data = allValues[.generationIdentifierKey] as? Data return data?.map { String(format: "%02hhx", $0) }.joined() @@ -79,7 +86,7 @@ public final class LocalFileObject: FileObject { } /// Count of children items of a driectory. It costs disk access for local directories. - open public(set) override var childrensCount: Int? { + public override var childrensCount: Int? { get { return try? FileManager.default.contentsOfDirectory(atPath: self.url.path).count } @@ -89,24 +96,49 @@ public final class LocalFileObject: FileObject { } } -internal final class LocalFolderMonitor { +public final class LocalFileMonitor { fileprivate let source: DispatchSourceFileSystemObject fileprivate let descriptor: CInt fileprivate let qq: DispatchQueue = DispatchQueue.global(qos: .default) fileprivate var state: Bool = false - fileprivate var monitoredTime: TimeInterval = Date().timeIntervalSinceReferenceDate - var url: URL + + fileprivate var _monitoredTime: TimeInterval = Date().timeIntervalSinceReferenceDate + fileprivate let _monitoredTimeLock = NSLock() + fileprivate var monitoredTime: TimeInterval { + get { + _monitoredTimeLock.lock() + defer { + _monitoredTimeLock.unlock() + } + return _monitoredTime + } + set { + _monitoredTimeLock.lock() + defer { + _monitoredTimeLock.unlock() + } + _monitoredTime = newValue + } + } + + public var url: URL /// Creates a folder monitor object with monitoring enabled. - init(url: URL, handler: @escaping ()->Void) { + public init?(url: URL, handler: @escaping ()->Void) { self.url = url - descriptor = open((url as NSURL).fileSystemRepresentation, O_EVTONLY) - source = DispatchSource.makeFileSystemObjectSource(fileDescriptor: descriptor, eventMask: .write, queue: qq) + descriptor = url.absoluteURL.withUnsafeFileSystemRepresentation { rep in + guard let rep = rep else { return -1 } + return open(rep, O_EVTONLY) + } + guard descriptor >= 0 else { return nil } + let event: DispatchSource.FileSystemEvent = url.fileIsDirectory ? [.write] : .all + source = DispatchSource.makeFileSystemObjectSource(fileDescriptor: descriptor, eventMask: event, queue: qq) + // Folder monitoring is recursive and deep. Monitoring a root folder may be very costly // We have a 0.2 second delay to ensure we wont call handler 1000s times when there is // a huge file operation. This ensures app will work smoothly while this 250 milisec won't // affect user experince much - let main_handler: ()->Void = { [weak self] in + let main_handler: DispatchSourceProtocol.DispatchSourceHandler = { [weak self] in guard let `self` = self else { return } if Date().timeIntervalSinceReferenceDate < self.monitoredTime + 0.2 { return @@ -126,7 +158,7 @@ internal final class LocalFolderMonitor { } /// Starts sending notifications if currently stopped - func start() { + public func start() { if !state { state = true source.resume() @@ -134,7 +166,7 @@ internal final class LocalFolderMonitor { } /// Stops sending notifications if currently enabled - func stop() { + public func stop() { if state { state = false source.suspend() @@ -223,15 +255,3 @@ internal class LocalFileProviderManagerDelegate: NSObject, FileManagerDelegate { return delegate.fileProvider(provider, shouldProceedAfterError: error, operation: .link(link: srcPath, target: dstPath)) } } - -class UndoBox: NSObject { - weak var provider: FileProvideUndoable? - let operation: FileOperationType - let undoOperation: FileOperationType - - init(provider: FileProvideUndoable, operation: FileOperationType, undoOperation: FileOperationType) { - self.provider = provider - self.operation = operation - self.undoOperation = undoOperation - } -} diff --git a/Sources/OneDriveFileProvider.swift b/Sources/OneDriveFileProvider.swift index 53a870f..79b6e1e 100644 --- a/Sources/OneDriveFileProvider.swift +++ b/Sources/OneDriveFileProvider.swift @@ -1,4 +1,3 @@ - // // OneDriveFileProvider.swift // FileProvider @@ -8,8 +7,10 @@ // import Foundation +#if os(macOS) || os(iOS) || os(tvOS) import CoreGraphics - +#endif + /** Allows accessing to OneDrive stored files, either hosted on Microsoft servers or business coprporate one. This provider doesn't cache or save files internally, however you can set `useCache` and `cache` properties @@ -92,8 +93,15 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing { } } } + + /// Microsoft Graph URL + public static var graphURL = URL(string: "https://graph.microsoft.com/")! + + /// Microsoft Graph URL + public static var graphVersion = "v1.0" + /// Route for container, default is `.me`. - open let route: Route + public let route: Route /** Initializer for Onedrive provider with given client ID and Token. @@ -109,11 +117,9 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing { - cache: A URLCache to cache downloaded files and contents. */ @available(*, deprecated, message: "use init(credential:, serverURL:, route:, cache:) instead.") - public init(credential: URLCredential?, serverURL: URL? = nil, drive: String?, cache: URLCache? = nil) { - let baseURL = serverURL?.absoluteURL ?? URL(string: "https://api.onedrive.com/")! - let refinedBaseURL = baseURL.absoluteString.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("") - self.route = drive.flatMap({ UUID(uuidString: $0) }).flatMap({ Route.drive(uuid: $0) }) ?? .me - super.init(baseURL: refinedBaseURL, credential: credential, cache: cache) + public convenience init(credential: URLCredential?, serverURL: URL? = nil, drive: String?, cache: URLCache? = nil) { + let route: Route = drive.flatMap({ UUID(uuidString: $0) }).flatMap({ Route.drive(uuid: $0) }) ?? .me + self.init(credential: credential, serverURL: serverURL, route: route, cache: cache) } /** @@ -131,7 +137,8 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing { - cache: A URLCache to cache downloaded files and contents. */ public init(credential: URLCredential?, serverURL: URL? = nil, route: Route = .me, cache: URLCache? = nil) { - let baseURL = serverURL?.absoluteURL ?? URL(string: "https://api.onedrive.com/")! + let baseURL = (serverURL?.absoluteURL ?? OneDriveFileProvider.graphURL) + .appendingPathComponent(OneDriveFileProvider.graphVersion, isDirectory: true) let refinedBaseURL = baseURL.absoluteString.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("") self.route = route super.init(baseURL: refinedBaseURL, credential: credential, cache: cache) @@ -139,13 +146,13 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing { public required convenience init?(coder aDecoder: NSCoder) { let route: Route - if let driveId = aDecoder.decodeObject(forKey: "drive") as? String, let uuid = UUID(uuidString: driveId) { + if let driveId = aDecoder.decodeObject(of: NSString.self, forKey: "drive") as String?, let uuid = UUID(uuidString: driveId) { route = .drive(uuid: uuid) } else { - route = (aDecoder.decodeObject(forKey: "route") as? String).flatMap({ Route(rawValue: $0) }) ?? .me + route = (aDecoder.decodeObject(of: NSString.self, forKey: "route") as String?).flatMap({ Route(rawValue: $0) }) ?? .me } - self.init(credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential, - serverURL: aDecoder.decodeObject(forKey: "baseURL") as? URL, + self.init(credential: aDecoder.decodeObject(of: URLCredential.self, forKey: "credential"), + serverURL: aDecoder.decodeObject(of: NSURL.self, forKey: "baseURL") as URL?, route: route) self.useCache = aDecoder.decodeBool(forKey: "useCache") self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache") @@ -165,25 +172,36 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing { return copy } + /** + Returns an Array of `FileObject`s identifying the the directory entries via asynchronous completion handler. + + If the directory contains no entries or an error is occured, this method will return the empty array. + + - Parameters: + - path: path to target directory. If empty, root will be iterated. + - completionHandler: a closure with result of directory entries or error. + - contents: An array of `FileObject` identifying the the directory entries. + - error: Error returned by system. + */ open override func contentsOfDirectory(path: String, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) { _ = paginated(path, requestHandler: { [weak self] (token) -> URLRequest? in guard let `self` = self else { return nil } let url = token.flatMap(URL.init(string:)) ?? self.url(of: path, modifier: "children") var request = URLRequest(url: url) request.httpMethod = "GET" - request.set(httpAuthentication: self.credential, with: .oAuth2) + request.setValue(authentication: self.credential, with: .oAuth2) return request }, pageHandler: { [weak self] (data, _) -> (files: [FileObject], error: Error?, newToken: String?) in guard let `self` = self else { return ([], nil, nil) } - guard let json = data?.deserializeJSON(), let entries = json["value"] as? [AnyObject] else { - let err = self.urlError(path, code: .badServerResponse) + guard let json = data?.deserializeJSON(), let entries = json["value"] as? [Any] else { + let err = URLError(.badServerResponse, url: self.url(of: path)) return ([], err, nil) } var files = [FileObject]() for entry in entries { - if let entry = entry as? [String: AnyObject], let file = OneDriveFileObject(baseURL: self.baseURL, route: self.route, json: entry) { + if let entry = entry as? [String: Any], let file = OneDriveFileObject(baseURL: self.baseURL, route: self.route, json: entry) { files.append(file) } } @@ -191,29 +209,43 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing { }, completionHandler: completionHandler) } + /** + Returns a `FileObject` containing the attributes of the item (file, directory, symlink, etc.) at the path in question via asynchronous completion handler. + + If the directory contains no entries or an error is occured, this method will return the empty `FileObject`. + + - Parameters: + - path: path to target directory. If empty, attributes of root will be returned. + - completionHandler: a closure with result of directory entries or error. + - attributes: A `FileObject` containing the attributes of the item. + - error: Error returned by system. + */ open override func attributesOfItem(path: String, completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) { var request = URLRequest(url: url(of: path)) request.httpMethod = "GET" - request.set(httpAuthentication: credential, with: .oAuth2) + request.setValue(authentication: self.credential, with: .oAuth2) let task = session.dataTask(with: request, completionHandler: { (data, response, error) in var serverError: FileProviderHTTPError? var fileObject: OneDriveFileObject? - if let response = response as? HTTPURLResponse { + if let response = response as? HTTPURLResponse, response.statusCode >= 400 { let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) } - if let json = data?.deserializeJSON(), let file = OneDriveFileObject(baseURL: self.baseURL, route: self.route, json: json) { - fileObject = file - } + } + if let json = data?.deserializeJSON(), let file = OneDriveFileObject(baseURL: self.baseURL, route: self.route, json: json) { + fileObject = file } completionHandler(fileObject, serverError ?? error) }) task.resume() } + /// Returns volume/provider information asynchronously. + /// - Parameter volumeInfo: Information of filesystem/Provider returned by system/server. open override func storageProperties(completionHandler: @escaping (_ volumeInfo: VolumeObject?) -> Void) { - var request = URLRequest(url: url(of: "")) + let url = URL(string: route.drivePath, relativeTo: baseURL)! + var request = URLRequest(url: url) request.httpMethod = "GET" - request.set(httpAuthentication: credential, with: .oAuth2) + request.setValue(authentication: self.credential, with: .oAuth2) let task = session.dataTask(with: request, completionHandler: { (data, response, error) in guard let json = data?.deserializeJSON() else { completionHandler(nil) @@ -222,17 +254,44 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing { let volume = VolumeObject(allValues: [:]) volume.url = request.url + volume.uuid = json["id"] as? String volume.name = json["name"] as? String volume.creationDate = (json["createdDateTime"] as? String).flatMap { Date(rfcString: $0) } - volume.totalCapacity = (json["quota"]?["total"] as? NSNumber)?.int64Value ?? -1 - volume.availableCapacity = (json["quota"]?["remaining"] as? NSNumber)?.int64Value ?? 0 + let quota = json["quota"] as? [String: Any] + volume.totalCapacity = (quota?["total"] as? NSNumber)?.int64Value ?? -1 + volume.availableCapacity = (quota?["remaining"] as? NSNumber)?.int64Value ?? 0 completionHandler(volume) }) task.resume() } + /** + Search files inside directory using query asynchronously. + + Sample predicates: + ``` + NSPredicate(format: "(name CONTAINS[c] 'hello') && (fileSize >= 10000)") + NSPredicate(format: "(modifiedDate >= %@)", Date()) + NSPredicate(format: "(path BEGINSWITH %@)", "folder/child folder") + ``` + + - Note: Don't pass Spotlight predicates to this method directly, use `FileProvider.convertSpotlightPredicateTo()` method to get usable predicate. + + - Important: A file name criteria should be provided for Dropbox. + + - Parameters: + - path: location of directory to start search + - recursive: Searching subdirectories of path + - query: An `NSPredicate` object with keys like `FileObject` members, except `size` which becomes `filesize`. + - foundItemHandler: Closure which is called when a file is found + - completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured. + - files: all files meat the `query` criteria. + - error: `Error` returned by server if occured. + - Returns: An `Progress` to get progress or cancel progress. Use `completedUnitCount` to iterate count of found items. + */ + @discardableResult open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? { - let queryStr = query.findValue(forKey: "name") as? String ?? query.findAllValues(forKey: nil).flatMap { $0.value as? String }.first + let queryStr = query.findValue(forKey: "name") as? String ?? query.findAllValues(forKey: nil).compactMap { $0.value as? String }.first return paginated(path, requestHandler: { [weak self] (token) -> URLRequest? in guard let `self` = self else { return nil } @@ -256,14 +315,14 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing { return request }, pageHandler: { [weak self] (data, progress) -> (files: [FileObject], error: Error?, newToken: String?) in guard let `self` = self else { return ([], nil, nil) } - guard let json = data?.deserializeJSON(), let entries = json["value"] as? [AnyObject] else { - let err = self.urlError(path, code: .badServerResponse) + guard let json = data?.deserializeJSON(), let entries = json["value"] as? [Any] else { + let err = URLError(.badServerResponse, url: self.url(of: path)) return ([], err, nil) } var foundFiles = [FileObject]() for entry in entries { - if let entry = entry as? [String: AnyObject], let file = OneDriveFileObject(baseURL: self.baseURL, route: self.route, json: entry), query.evaluate(with: file.mapPredicate()) { + if let entry = entry as? [String: Any], let file = OneDriveFileObject(baseURL: self.baseURL, route: self.route, json: entry), query.evaluate(with: file.mapPredicate()) { foundFiles.append(file) foundItemHandler?(file) } @@ -273,50 +332,130 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing { }, completionHandler: completionHandler) } + /** + Returns an independent url to access the file. Some providers like `Dropbox` due to their nature. + don't return an absolute url to be used to access file directly. + - Parameter path: Relative path of file or directory. + - Returns: An url, can be used to access to file directly. + */ + open override func url(of path: String) -> URL { + return OneDriveFileObject.url(of: path, modifier: nil, baseURL: baseURL!, route: route) + } + + /** + Returns an independent url to access the file. Some providers like `Dropbox` due to their nature. + don't return an absolute url to be used to access file directly. + - Parameter path: Relative path of file or directory. + - Parameter modifier: Added to end of url to indicate what it can used for, e.g. `contents` to fetch data. + - Returns: An url, can be used to access to file directly. + */ open func url(of path: String, modifier: String? = nil) -> URL { - var url: URL = baseURL! - var rpath: String = path - let isId = path.hasPrefix("id:") - - url.appendPathComponent(route.drivePath) - - if isId { - url.appendPathComponent("root:") - } else { - url.appendPathComponent("items") - } - - rpath = rpath.trimmingCharacters(in: pathTrimSet) - - switch (modifier == nil, rpath.isEmpty, isId) { - case (true, false, _): - url.appendPathComponent(rpath) - case (true, true, _): - break - case (false, true, _): - url.appendPathComponent(modifier!) - case (false, false, true): - url.appendPathComponent(rpath) - url.appendPathComponent(modifier!) - case (false, false, false): - url.appendPathComponent(rpath + ":") - url.appendPathComponent(modifier!) - } - - return url + return OneDriveFileObject.url(of: path, modifier: modifier, baseURL: baseURL!, route: route) + } + + open override func relativePathOf(url: URL) -> String { + return OneDriveFileObject.relativePathOf(url: url, baseURL: baseURL, route: route) } - open override func isReachable(completionHandler: @escaping (Bool) -> Void) { + /// Checks the connection to server or permission on local + /// + /// - Note: To prevent race condition, use this method wisely and avoid it as far possible. + /// + /// - Parameter success: indicated server is reachable or not. + open override func isReachable(completionHandler: @escaping (_ success: Bool, _ error: Error?) -> Void) { var request = URLRequest(url: url(of: "")) request.httpMethod = "HEAD" - request.set(httpAuthentication: credential, with: .oAuth2) + request.setValue(authentication: credential, with: .oAuth2) let task = session.dataTask(with: request, completionHandler: { (data, response, error) in let status = (response as? HTTPURLResponse)?.statusCode ?? 400 - completionHandler(status == 200) + if status >= 400, let code = FileProviderHTTPErrorCode(rawValue: status) { + let errorDesc = data.flatMap({ String(data: $0, encoding: .utf8) }) + let error = FileProviderOneDriveError(code: code, path: "", serverDescription: errorDesc) + completionHandler(false, error) + return + } + completionHandler(status == 200, error) }) task.resume() } + /*internal var cachedDriveID: String? + + override func doOperation(_ operation: FileOperationType, overwrite: Bool, progress: Progress?, completionHandler: SimpleCompletionHandler) -> Progress? { + switch operation { + case .copy(source: let source, destination: let dest) where !source.hasPrefix("file://") && !dest.hasPrefix("file://"): + fallthrough + case .move: + if self.cachedDriveID != nil { + return super.doOperation(operation, overwrite: overwrite, progress: progress, completionHandler: completionHandler) + } else { + let progress = Progress(totalUnitCount: 1) + self.storageProperties(completionHandler: { (volume) in + if let volumeId = volume?.uuid { + self.cachedDriveID = volumeId + _ = super.doOperation(operation, overwrite: overwrite, progress: progress, completionHandler: completionHandler) + } else { + let error = self.urlError(operation.source, code: .badServerResponse) + completionHandler?(error) + } + }) + return progress + } + default: + return super.doOperation(operation, overwrite: overwrite, progress: progress, completionHandler: completionHandler) + } + }*/ + + /** + Uploads a file from local file url to designated path asynchronously. + Method will fail if source is not a local url with `file://` scheme. + + - Note: It's safe to assume that this method only works on individual files and **won't** copy folders recursively. + + - Parameters: + - localFile: a file url to file. + - to: destination path of file, including file/directory name. + - overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`. + - completionHandler: If an error parameter was provided, a presentable `Error` will be returned. + - Returns: An `Progress` to get progress or cancel progress. + */ + @discardableResult + open override func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { + // check file is not a folder + guard (try? localFile.resourceValues(forKeys: [.fileResourceTypeKey]))?.fileResourceType ?? .unknown == .regular else { + dispatch_queue.async { + completionHandler?(URLError(.fileIsDirectory, url: localFile)) + } + return nil + } + + let operation = FileOperationType.copy(source: localFile.absoluteString, destination: toPath) + guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else { + return nil + } + return self.upload_multipart_file(toPath, file: localFile, operation: operation, overwrite: overwrite, completionHandler: completionHandler) + } + + /** + Write the contents of the `Data` to a location asynchronously. + It will return error if file is already exists. + Not attomically by default, unless the provider enforces it. + + - Parameters: + - path: Path of target file. + - contents: Data to be written into file, pass nil to create empty file. + - completionHandler: If an error parameter was provided, a presentable `Error` will be returned. + - Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`. + */ + @discardableResult + open override func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { + let operation = FileOperationType.modify(path: path) + guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else { + return nil + } + return upload_multipart_data(path, data: data ?? Data(), operation: operation, overwrite: overwrite, completionHandler: completionHandler) + } + override func request(for operation: FileOperationType, overwrite: Bool = false, attributes: [URLResourceKey : Any] = [:]) -> URLRequest { func correctPath(_ path: String) -> String { @@ -336,23 +475,24 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing { case .fetch(path: let path): method = "GET" url = self.url(of: path, modifier: "content") + case .create(path: let path) where path.hasSuffix("/"): + method = "POST" + let parent = path.deletingLastPathComponent + url = self.url(of: parent, modifier: "children") case .modify(path: let path): method = "PUT" let queryStr = overwrite ? "" : "?@name.conflictBehavior=fail" - url = self.url(of: path, modifier: "content\(queryStr)") - case .create(path: let path): - method = "CREATE" - url = self.url(of: path) - case .copy(let source, let dest) where !source.hasPrefix("file://") && !dest.hasPrefix("file://"): - method = "POST" - url = self.url(of: source) - case .copy(let source, let dest) where source.hasPrefix("file://"): + url = URL(string: self.url(of: path, modifier: "content").absoluteString + queryStr)! + case .copy(source: let source, destination: let dest) where source.hasPrefix("file://"): method = "PUT" let queryStr = overwrite ? "" : "?@name.conflictBehavior=fail" - url = self.url(of: dest, modifier: "content\(queryStr)") - case .copy(let source, let dest) where dest.hasPrefix("file://"): + url = URL(string: self.url(of: dest, modifier: "content").absoluteString + queryStr)! + case .copy(source: let source, destination: let dest) where dest.hasPrefix("file://"): method = "GET" url = self.url(of: source, modifier: "content") + case .copy(source: let source, destination: _): + method = "POST" + url = self.url(of: source, modifier: "copy") case .move(source: let source, destination: _): method = "PATCH" url = self.url(of: source) @@ -364,30 +504,42 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing { } var request = URLRequest(url: url) request.httpMethod = method - request.set(httpAuthentication: credential, with: .oAuth2) - // Remove gzip to fix availability of progress per (Oleg Marchik)[https://github.com/evilutioner] PR (#61) - request.set(httpAcceptEncodings: [.deflate, .identity]) + request.setValue(authentication: self.credential, with: .oAuth2) + // Remove gzip to fix availability of progress re. (Oleg Marchik)[https://github.com/evilutioner] PR (#61) + if method == "GET" { + request.setValue(acceptEncoding: .deflate) + request.addValue(acceptEncoding: .identity) + } switch operation { + case .create(path: let path) where path.hasSuffix("/"): + request.setValue(contentType: .json) + var requestDictionary = [String: Any]() + let name = path.lastPathComponent + requestDictionary["name"] = name + requestDictionary["folder"] = [String: Any]() + requestDictionary["@microsoft.graph.conflictBehavior"] = "fail" + request.httpBody = Data(jsonDictionary: requestDictionary) case .copy(let source, let dest) where !source.hasPrefix("file://") && !dest.hasPrefix("file://"), .move(source: let source, destination: let dest): - request.set(httpContentType: .json) - let cdest = correctPath(dest) as NSString - var parentRefrence: [String: AnyObject] = [:] + request.setValue(contentType: .json, charset: .utf8) + let cdest = correctPath(dest) + var parentReference: [String: Any] = [:] if cdest.hasPrefix("id:") { - parentRefrence["id"] = cdest.components(separatedBy: "/").first as NSString? - switch self.route { - case .drive(uuid: let uuid): - parentRefrence["driveId"] = uuid.uuidString as NSString - default: - break - } + parentReference["id"] = cdest.components(separatedBy: "/").first?.replacingOccurrences(of: "id:", with: "", options: .anchored) } else { - parentRefrence["path"] = cdest.deletingLastPathComponent as NSString + parentReference["path"] = "/drive/root:".appendingPathComponent(cdest.deletingLastPathComponent) } - var requestDictionary = [String: AnyObject]() - requestDictionary["parentReference"] = parentRefrence as NSDictionary - requestDictionary["name"] = (cdest as NSString).lastPathComponent as NSString + switch self.route { + case .drive(uuid: let uuid): + parentReference["driveId"] = uuid.uuidString + default: + //parentReference["driveId"] = cachedDriveID ?? "" + break + } + var requestDictionary = [String: Any]() + requestDictionary["parentReference"] = parentReference + requestDictionary["name"] = cdest.lastPathComponent request.httpBody = Data(jsonDictionary: requestDictionary) default: break @@ -399,15 +551,15 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing { override func serverError(with code: FileProviderHTTPErrorCode, path: String?, data: Data?) -> FileProviderHTTPError { let errorDesc: String? if let response = data?.deserializeJSON() { - errorDesc = response["error"]?["message"] as? String + errorDesc = (response["error"] as? [String: Any])?["message"] as? String } else { errorDesc = data.flatMap({ String(data: $0, encoding: .utf8) }) } - return FileProviderOneDriveError(code: code, path: path ?? "", errorDescription: errorDesc) + return FileProviderOneDriveError(code: code, path: path ?? "", serverDescription: errorDesc) } override var maxUploadSimpleSupported: Int64 { - return 104_857_600 // 100MB + return 4_194_304 // 4MB! } fileprivate func registerNotifcation(path: String, eventHandler: (() -> Void)) { @@ -424,23 +576,22 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing { } open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void)) { - var request = URLRequest(url: self.url(of: path, modifier: "action.createLink")) + var request = URLRequest(url: self.url(of: path, modifier: "createLink")) request.httpMethod = "POST" - let requestDictionary: [String: AnyObject] = ["type": "view" as NSString] + let requestDictionary: [String: Any] = ["type": "view"] request.httpBody = Data(jsonDictionary: requestDictionary) let task = session.dataTask(with: request, completionHandler: { (data, response, error) in var serverError: FileProviderHTTPError? var link: URL? - if let response = response as? HTTPURLResponse { + if let response = response as? HTTPURLResponse, response.statusCode >= 400 { let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) } - if let json = data?.deserializeJSON() { - if let linkDic = json["link"] as? NSDictionary, let linkStr = linkDic["webUrl"] as? String { - link = URL(string: linkStr) - } + } + if let json = data?.deserializeJSON() { + if let linkDic = json["link"] as? [String: Any], let linkStr = linkDic["webUrl"] as? String { + link = URL(string: linkStr) } } - completionHandler(link, nil, nil, serverError ?? error) }) task.resume() @@ -449,12 +600,35 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing { extension OneDriveFileProvider: ExtendedFileProvider { - open func thumbnailOfFileSupported(path: String) -> Bool { + open func propertiesOfFileSupported(path: String) -> Bool { return true } - open func propertiesOfFileSupported(path: String) -> Bool { - let fileExt = (path as NSString).pathExtension.lowercased() + @discardableResult + open func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) -> Progress? { + var request = URLRequest(url: url(of: path)) + request.httpMethod = "GET" + request.setValue(authentication: credential, with: .oAuth2) + let task = session.dataTask(with: request, completionHandler: { (data, response, error) in + var serverError: FileProviderHTTPError? + var dic = [String: Any]() + var keys = [String]() + if let response = response as? HTTPURLResponse { + let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) + serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) } + } + if let json = data?.deserializeJSON() { + (dic, keys) = self.mapMediaInfo(json) + } + completionHandler(dic, keys, serverError ?? error) + }) + task.resume() + return nil + } + + #if os(macOS) || os(iOS) || os(tvOS) + open func thumbnailOfFileSupported(path: String) -> Bool { + let fileExt = path.pathExtension.lowercased() switch fileExt { case "jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff": return true @@ -462,53 +636,43 @@ extension OneDriveFileProvider: ExtendedFileProvider { return true case "mp4", "mpg", "3gp", "mov", "avi", "wmv": return true + case "doc", "docx", "xls", "xlsx", "ppt", "pptx", "pdf": + return true default: return false } } - open func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) { - let url: URL - if let dimension = dimension { - url = self.url(of: path, modifier: "thumbnails/0/=c\(dimension.width)x\(dimension.height)/content") - } else { - url = self.url(of: path, modifier: "thumbnails/0/small/content") + @discardableResult + open func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) -> Progress? { + let thumbQuery: String + switch dimension.map( {max($0.width, $0.height) } ) ?? 0 { + case 0...96: thumbQuery = "small" + case 97...176: thumbQuery = "medium" + default: thumbQuery = "large" } - + /*if let dimension = dimension { + thumbQuery = "c\(Int(dimension.width))x\(Int(dimension.height))" + } else { + thumbQuery = "small" + }*/ + let url = self.url(of: path, modifier: "thumbnails") + .appendingPathComponent("0").appendingPathComponent(thumbQuery) + .appendingPathComponent("content") var request = URLRequest(url: url) - request.set(httpAuthentication: credential, with: .oAuth2) + request.setValue(authentication: credential, with: .oAuth2) let task = self.session.dataTask(with: request, completionHandler: { (data, response, error) in var image: ImageClass? = nil - if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { + if let code = (response as? HTTPURLResponse)?.statusCode , code >= 400, let rCode = FileProviderHTTPErrorCode(rawValue: code) { let responseError = self.serverError(with: rCode, path: path, data: data) completionHandler(nil, responseError) return } - if let data = data { - image = ImageClass(data: data) - } + image = data.flatMap(ImageClass.init(data:)) completionHandler(image, error) }) task.resume() + return nil } - - open func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) { - var request = URLRequest(url: url(of: path)) - request.httpMethod = "GET" - request.set(httpAuthentication: credential, with: .oAuth2) - let task = session.dataTask(with: request, completionHandler: { (data, response, error) in - var serverError: FileProviderHTTPError? - var dic = [String: Any]() - var keys = [String]() - if let response = response as? HTTPURLResponse { - let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) - serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) } - if let json = data?.deserializeJSON() { - (dic, keys) = self.mapMediaInfo(json) - } - } - completionHandler(dic, keys, serverError ?? error) - }) - task.resume() - } + #endif } diff --git a/Sources/OneDriveHelper.swift b/Sources/OneDriveHelper.swift index a072e5e..4318ab4 100644 --- a/Sources/OneDriveHelper.swift +++ b/Sources/OneDriveHelper.swift @@ -12,46 +12,49 @@ import Foundation public struct FileProviderOneDriveError: FileProviderHTTPError { public let code: FileProviderHTTPErrorCode public let path: String - public let errorDescription: String? + public let serverDescription: String? } /// Containts path, url and attributes of a OneDrive file or resource. public final class OneDriveFileObject: FileObject { - internal init(baseURL: URL?, name: String, path: String) { - let rpath = (URL(string:path)?.appendingPathComponent(name).absoluteString)!.replacingOccurrences(of: "/", with: "", options: .anchored) - let url = URL(string: rpath, relativeTo: baseURL) ?? URL(string: rpath)! - - super.init(url: url, name: name, path: rpath.removingPercentEncoding ?? path) - } - internal convenience init? (baseURL: URL?, route: OneDriveFileProvider.Route, jsonStr: String) { guard let json = jsonStr.deserializeJSON() else { return nil } self.init(baseURL: baseURL, route: route, json: json) } - internal convenience init? (baseURL: URL?, route: OneDriveFileProvider.Route, json: [String: AnyObject]) { + internal init? (baseURL: URL?, route: OneDriveFileProvider.Route, json: [String: Any]) { guard let name = json["name"] as? String else { return nil } - guard let path = json["parentReference"]?["path"] as? String else { return nil } - var lPath = path.replacingOccurrences(of: route.drivePath, with: "", options: .anchored, range: nil) - lPath = lPath.replacingOccurrences(of: "/:", with: "", options: .anchored) - lPath = lPath.replacingOccurrences(of: "//", with: "", options: .anchored) - self.init(baseURL: baseURL, name: name, path: lPath) + guard let id = json["id"] as? String else { return nil } + let path: String + if let refpath = (json["parentReference"] as? [String: Any])?["path"] as? String { + let parentPath: String + if let colonIndex = refpath.firstIndex(of: ":") { + parentPath = String(refpath[refpath.index(after: colonIndex)...]) + } else { + parentPath = refpath + } + path = parentPath.appendingPathComponent(name) + } else { + path = "id:\(id)" + } + let url = baseURL.map { OneDriveFileObject.url(of: path, modifier: nil, baseURL: $0, route: route) } + super.init(url: url, name: name, path: path) + self.id = id self.size = (json["size"] as? NSNumber)?.int64Value ?? -1 - self.childrensCount = json["folder"]?["childCount"] as? Int + self.childrensCount = (json["folder"] as? [String: Any])?["childCount"] as? Int self.modifiedDate = (json["lastModifiedDateTime"] as? String).flatMap { Date(rfcString: $0) } self.creationDate = (json["createdDateTime"] as? String).flatMap { Date(rfcString: $0) } self.type = json["folder"] != nil ? .directory : .regular - self.contentType = json["file"]?["mimeType"] as? String ?? "application/octet-stream" - self.id = json["id"] as? String + self.contentType = ((json["file"] as? [String: Any])?["mimeType"] as? String).flatMap(ContentMIMEType.init(rawValue:)) ?? .stream self.entryTag = json["eTag"] as? String - let hashes = json["file"]?["hashes"] as? NSDictionary + let hashes = (json["file"] as? [String: Any])?["hashes"] as? [String: Any] // checks for both sha1 or quickXor. First is available in personal drives, second in business one. - self.hash = (hashes?["sha1Hash"] as? String) ?? (hashes?["quickXorHash"] as? String) + self.fileHash = (hashes?["sha1Hash"] as? String) ?? (hashes?["quickXorHash"] as? String) } /// The document identifier is a value assigned by the OneDrive to a file. /// This value is used to identify the document regardless of where it is moved on a volume. - open internal(set) var id: String? { + public internal(set) var id: String? { get { return allValues[.fileResourceIdentifierKey] as? String } @@ -61,17 +64,17 @@ public final class OneDriveFileObject: FileObject { } /// MIME type of file contents returned by OneDrive server. - open internal(set) var contentType: String { + public internal(set) var contentType: ContentMIMEType { get { - return allValues[.mimeTypeKey] as? String ?? "application/octet-stream" + return (allValues[.mimeTypeKey] as? String).flatMap(ContentMIMEType.init(rawValue:)) ?? .stream } set { - allValues[.mimeTypeKey] = newValue + allValues[.mimeTypeKey] = newValue.rawValue } } /// HTTP E-Tag, can be used to mark changed files. - open internal(set) var entryTag: String? { + public internal(set) var entryTag: String? { get { return allValues[.entryTagKey] as? String } @@ -81,7 +84,7 @@ public final class OneDriveFileObject: FileObject { } /// Calculated hash from OneDrive server. Hex string SHA1 in personal or Base65 string [QuickXOR](https://dev.onedrive.com/snippets/quickxorhash.htm) in business drives. - open internal(set) var hash: String? { + public internal(set) var fileHash: String? { get { return allValues[.documentIdentifierKey] as? String } @@ -89,9 +92,224 @@ public final class OneDriveFileObject: FileObject { allValues[.documentIdentifierKey] = newValue } } + + static func url(of path: String, modifier: String?, baseURL: URL, route: OneDriveFileProvider.Route) -> URL { + var url: URL = baseURL + let isId = path.hasPrefix("id:") + var rpath: String = path.replacingOccurrences(of: "id:", with: "", options: .anchored) + + //url.appendPathComponent("v1.0") + url.appendPathComponent(route.drivePath) + + if rpath.isEmpty { + url.appendPathComponent("root") + } else if isId { + url.appendPathComponent("items") + } else { + url.appendPathComponent("root:") + } + + rpath = rpath.trimmingCharacters(in: pathTrimSet) + + switch (modifier == nil, rpath.isEmpty, isId) { + case (true, false, _): + url.appendPathComponent(rpath) + case (true, true, _): + break + case (false, true, _): + url.appendPathComponent(modifier!) + case (false, false, true): + url.appendPathComponent(rpath) + url.appendPathComponent(modifier!) + case (false, false, false): + url.appendPathComponent(rpath + ":") + url.appendPathComponent(modifier!) + } + + return url + } + + static func relativePathOf(url: URL, baseURL: URL?, route: OneDriveFileProvider.Route) -> String { + let base = baseURL?.appendingPathComponent(route.drivePath).path ?? "" + + let crudePath = url.path.replacingOccurrences(of: base, with: "", options: .anchored) + .trimmingCharacters(in: CharacterSet(charactersIn: "/")) + + switch crudePath { + case hasPrefix("items/"): + let components = crudePath.components(separatedBy: "/") + return components.dropFirst().first.map { "id:\($0)" } ?? "" + case hasPrefix("root:"): + return crudePath.components(separatedBy: ":").dropFirst().first ?? "" + default: + return "" + } + } } -internal extension OneDriveFileProvider { +extension OneDriveFileProvider { + func upload_multipart_data(_ targetPath: String, data: Data, operation: FileOperationType, + overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? { + return self.upload_multipart(targetPath, operation: operation, size: Int64(data.count), overwrite: overwrite, dataProvider: { + let range = $0.clamped(to: 0.. Progress? { + // upload task can't handle uploading file + + return self.upload_multipart(targetPath, operation: operation, size: file.fileSize, overwrite: overwrite, dataProvider: { range in + guard let handle = FileHandle(forReadingAtPath: file.path) else { + throw CocoaError(.fileNoSuchFile, path: targetPath) + } + + defer { + handle.closeFile() + } + + let offset = range.lowerBound + handle.seek(toFileOffset: UInt64(offset)) + guard Int64(handle.offsetInFile) == offset else { + throw CocoaError(.fileReadTooLarge, path: targetPath) + } + + return handle.readData(ofLength: range.count) + }, completionHandler: completionHandler) + } + + private func upload_multipart(_ targetPath: String, operation: FileOperationType, size: Int64, overwrite: Bool, + dataProvider: @escaping (Range) throws -> Data, completionHandler: SimpleCompletionHandler) -> Progress? { + guard size > 0 else { return nil } + + let progress = Progress(totalUnitCount: size) + progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey) + progress.kind = .file + progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey) + + let createURL = self.url(of: targetPath, modifier: "createUploadSession") + var createRequest = URLRequest(url: createURL) + createRequest.httpMethod = "POST" + createRequest.setValue(authentication: self.credential, with: .oAuth2) + createRequest.setValue(contentType: .json) + if overwrite { + createRequest.httpBody = Data(jsonDictionary: ["item": ["@microsoft.graph.conflictBehavior": "replace"] as NSDictionary]) + } else { + createRequest.httpBody = Data(jsonDictionary: ["item": ["@microsoft.graph.conflictBehavior": "fail"] as NSDictionary]) + } + let createSessionTask = session.dataTask(with: createRequest) { (data, response, error) in + if let error = error { + completionHandler?(error) + return + } + + if let data = data, let json = data.deserializeJSON(), + let uploadURL = (json["uploadUrl"] as? String).flatMap(URL.init(string:)) { + self.upload_multipart(url: uploadURL, operation: operation, size: size, progress: progress, dataProvider: dataProvider, completionHandler: completionHandler) + } + } + createSessionTask.resume() + + return progress + } + + private func upload_multipart(url: URL, operation: FileOperationType, size: Int64, range: Range? = nil, uploadedSoFar: Int64 = 0, + progress: Progress, dataProvider: @escaping (Range) throws -> Data, completionHandler: SimpleCompletionHandler) { + guard !progress.isCancelled else { return } + + let maximumSize: Int64 = 10_485_760 // Recommended by OneDrive documentations and divides evenly by 320 KiB, max 60MiB. + var request = URLRequest(url: url) + request.httpMethod = "PUT" + request.setValue(authentication: self.credential, with: .oAuth2) + + let finalRange: Range + if let range = range { + if range.count > maximumSize { + finalRange = range.lowerBound..<(range.upperBound + maximumSize) + } else { + finalRange = range + } + } else { + finalRange = 0..(uncheckedBounds: (lower: lower, upper: upper)) + self.upload_multipart(url: url, operation: operation, size: size, range: range, uploadedSoFar: uploaded, progress: progress, + dataProvider: dataProvider, completionHandler: completionHandler) + return + } + + if let _ = json["id"] as? String { + completionHandler?(nil) + self.delegateNotify(operation) + } + } + + task.resume() + } + static let dateFormatter = DateFormatter() static let decimalFormatter = NumberFormatter() @@ -116,7 +334,7 @@ internal extension OneDriveFileProvider { var keys = [String]() func add(key: String, value: Any?) { - if let value = value { + if let value = value, !((value as? String)?.isEmpty ?? false) { keys.append(key) dic[key] = value } @@ -153,8 +371,17 @@ internal extension OneDriveFileProvider { if let audio = json["audio"] as? [String: Any] { for (key, value) in audio { + var value = value if key == "bitrate" || key == "isVariableBitrate" { continue } let casedKey = spaceCamelCase(key) + switch casedKey { + case "Duration": + value = (value as? Int64).map { (TimeInterval($0) / 1000).formatshort } as Any + case "Bitrate": + value = (value as? Int64).map { "\($0)kbps" } as Any + default: + break + } add(key: casedKey, value: value) } } diff --git a/Sources/RemoteSession.swift b/Sources/RemoteSession.swift index 3cf0a12..1f41932 100755 --- a/Sources/RemoteSession.swift +++ b/Sources/RemoteSession.swift @@ -10,7 +10,7 @@ import Foundation /// A protocol defines properties for errors returned by HTTP/S based providers. /// Including Dropbox, OneDrive and WebDAV. -public protocol FileProviderHTTPError: Error, CustomStringConvertible { +public protocol FileProviderHTTPError: LocalizedError, CustomStringConvertible { /// HTTP status codes as an enum. typealias Code = FileProviderHTTPErrorCode /// HTTP status code returned for error by server. @@ -18,12 +18,12 @@ public protocol FileProviderHTTPError: Error, CustomStringConvertible { /// Path of file/folder casued that error var path: String { get } /// Contents returned by server as error description - var errorDescription: String? { get } + var serverDescription: String? { get } } extension FileProviderHTTPError { public var description: String { - return code.description + return "Status \(code.rawValue): \(code.description)" } public var localizedDescription: String { @@ -40,12 +40,14 @@ internal func initEmptySessionHandler(_ uuid: String) { completionHandlersForTasks[uuid] = [:] downloadCompletionHandlersForTasks[uuid] = [:] dataCompletionHandlersForTasks[uuid] = [:] + responseCompletionHandlersForTasks[uuid] = [:] } internal func removeSessionHandler(for uuid: String) { _ = completionHandlersForTasks.removeValue(forKey: uuid) _ = downloadCompletionHandlersForTasks.removeValue(forKey: uuid) _ = dataCompletionHandlersForTasks.removeValue(forKey: uuid) + _ = responseCompletionHandlersForTasks.removeValue(forKey: uuid) } /// All objects set to `FileProviderRemote.session` must be an instance of this class @@ -59,50 +61,93 @@ final public class SessionDelegate: NSObject, URLSessionDataDelegate, URLSession self.credential = fileProvider.credential } - open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { - if let progress = context?.load(as: Progress.self), let newVal = change?[.newKey] as? Int64 { - switch keyPath ?? "" { - case #keyPath(URLSessionTask.countOfBytesReceived): - progress.completedUnitCount = newVal - if let startTime = progress.userInfo[ProgressUserInfoKey.startingTimeKey] as? Date, let task = object as? URLSessionTask { - let elapsed = Date().timeIntervalSince(startTime) - let throughput = Double(newVal) / elapsed - progress.setUserInfoObject(NSNumber(value: throughput), forKey: .throughputKey) - if task.countOfBytesExpectedToReceive > 0 { - let remain = task.countOfBytesExpectedToReceive - task.countOfBytesReceived - let estimatedTimeRemaining = Double(remain) / elapsed - progress.setUserInfoObject(NSNumber(value: estimatedTimeRemaining), forKey: .estimatedTimeRemainingKey) - } + public enum ObserveKind { + case upload + case download + } + + private let observeProgressesLock = NSLock() + private var observeProgresses = [(task: URLSessionTask, progress: Progress, kind: ObserveKind)]() + + public func observerProgress(of task: URLSessionTask, using: Progress, kind: ObserveKind) { + switch kind { + case .upload: + task.addObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent), options: .new, context: nil) + case .download: + task.addObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: nil) + task.addObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: nil) + } + observeProgressesLock.lock() + observeProgresses.append((task, using, kind)) + observeProgressesLock.unlock() + } + + func removeObservers(for task: URLSessionTask) { + observeProgressesLock.lock() + observeProgresses = observeProgresses.filter { (item) -> Bool in + if item.task == task { + switch item.kind { + case .upload: + task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent)) + case .download: + task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived)) + task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive)) } - case #keyPath(URLSessionTask.countOfBytesSent): - progress.completedUnitCount = newVal - if let startTime = progress.userInfo[ProgressUserInfoKey.startingTimeKey] as? Date, let task = object as? URLSessionTask { - let elapsed = Date().timeIntervalSince(startTime) - let throughput = Double(newVal) / elapsed - progress.setUserInfoObject(NSNumber(value: throughput), forKey: .throughputKey) - if task.countOfBytesExpectedToSend > 0 { - let remain = task.countOfBytesExpectedToSend - task.countOfBytesSent - let estimatedTimeRemaining = Double(remain) / elapsed - progress.setUserInfoObject(NSNumber(value: estimatedTimeRemaining), forKey: .estimatedTimeRemainingKey) - } + return false + } else { + return true + } + } + observeProgressesLock.unlock() + } + + public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + guard let context = context, let keyPath = keyPath, keyPath.contains("countOfBytes") else { return } + let progress = context.assumingMemoryBound(to: Progress.self).pointee + guard let newVal = change?[.newKey] as? Int64 else { return } + + switch keyPath { + case #keyPath(URLSessionTask.countOfBytesReceived): + progress.completedUnitCount = newVal + if let startTime = progress.userInfo[ProgressUserInfoKey.startingTimeKey] as? Date, let task = object as? URLSessionTask { + let elapsed = Date().timeIntervalSince(startTime) + let throughput = Double(newVal) / elapsed + progress.setUserInfoObject(NSNumber(value: throughput), forKey: .throughputKey) + if task.countOfBytesExpectedToReceive > 0 { + let remain = task.countOfBytesExpectedToReceive - task.countOfBytesReceived + let estimatedTimeRemaining = Double(remain) / elapsed + progress.setUserInfoObject(NSNumber(value: estimatedTimeRemaining), forKey: .estimatedTimeRemainingKey) } - case #keyPath(URLSessionTask.countOfBytesExpectedToReceive), #keyPath(URLSessionTask.countOfBytesExpectedToSend): - progress.totalUnitCount = newVal - default: - super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) } + case #keyPath(URLSessionTask.countOfBytesSent): + if let startTime = progress.userInfo[ProgressUserInfoKey.startingTimeKey] as? Date, let task = object as? URLSessionTask { + let elapsed = Date().timeIntervalSince(startTime) + let throughput = Double(newVal) / elapsed + progress.setUserInfoObject(NSNumber(value: throughput), forKey: .throughputKey) + + // wokaround for multipart uploading + let json = task.taskDescription?.deserializeJSON() + let uploadedBytes = ((json?["uploadedBytes"] as? Int64) ?? 0) + newVal + let totalBytes = (json?["totalBytes"] as? Int64) ?? task.countOfBytesExpectedToSend + progress.completedUnitCount = uploadedBytes + if totalBytes > 0 { + let remain = totalBytes - uploadedBytes + let estimatedTimeRemaining = Double(remain) / elapsed + progress.setUserInfoObject(NSNumber(value: estimatedTimeRemaining), forKey: .estimatedTimeRemainingKey) + } + } else { + progress.completedUnitCount = newVal + } + case #keyPath(URLSessionTask.countOfBytesExpectedToReceive), #keyPath(URLSessionTask.countOfBytesExpectedToSend): + progress.totalUnitCount = newVal + default: + super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) } } // codebeat:disable[ARITY] public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { - if task is URLSessionUploadTask { - task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent)) - //task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToSend)) - } else if task is URLSessionDownloadTask { - task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived)) - task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive)) - } + self.removeObservers(for: task) _ = dataCompletionHandlersForTasks[session.sessionDescription!]?.removeValue(forKey: task.taskIdentifier) if !(error == nil && task is URLSessionDownloadTask) { @@ -110,32 +155,6 @@ final public class SessionDelegate: NSObject, URLSessionDataDelegate, URLSession completionHandler?(error) _ = completionHandlersForTasks[session.sessionDescription!]?.removeValue(forKey: task.taskIdentifier) } - - guard let json = task.taskDescription?.deserializeJSON(), - let op = FileOperationType(json: json), let fileProvider = fileProvider else { - return - } - - switch op { - case .fetch: - if task is URLSessionDataTask { - task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived)) - task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive)) - } - default: - break - } - - if !(task is URLSessionDownloadTask), case FileOperationType.fetch = op { - return - } - if #available(iOS 9.0, macOS 10.11, *) { - if task is URLSessionStreamTask { - return - } - } - - fileProvider.delegateNotify(op, error: error) } public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { @@ -154,10 +173,10 @@ final public class SessionDelegate: NSObject, URLSessionDataDelegate, URLSession public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { if let completionHandler = dataCompletionHandlersForTasks[session.sessionDescription!]?[dataTask.taskIdentifier] { - if let json = dataTask.taskDescription?.deserializeJSON(), + /*if let json = dataTask.taskDescription?.deserializeJSON(), let op = FileOperationType(json: json), let fileProvider = fileProvider { fileProvider.delegateNotify(op, progress: Double(dataTask.countOfBytesReceived) / Double(dataTask.countOfBytesExpectedToReceive)) - } + }*/ completionHandler(data) } @@ -181,7 +200,11 @@ final public class SessionDelegate: NSObject, URLSessionDataDelegate, URLSession return } - fileProvider.delegateNotify(op, progress: Double(totalBytesSent) / Double(totalBytesExpectedToSend)) + // wokaround for multipart uploading + let uploadedBytes = (json["uploadedBytes"] as? Int64) ?? 0 + let totalBytes = (json["totalBytes"] as? Int64) ?? totalBytesExpectedToSend + + fileProvider.delegateNotify(op, progress: Double(uploadedBytes + totalBytesSent) / Double(totalBytes)) } public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { @@ -203,7 +226,7 @@ final public class SessionDelegate: NSObject, URLSessionDataDelegate, URLSession authenticate(didReceive: challenge, completionHandler: completionHandler) } - func authenticate(didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + private func authenticate(didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { switch (challenge.previousFailureCount, credential != nil) { case (0...1, true): completionHandler(.useCredential, credential) diff --git a/Sources/SMBClient.swift b/Sources/SMBClient.swift index c512401..b5cd4e4 100644 --- a/Sources/SMBClient.swift +++ b/Sources/SMBClient.swift @@ -11,152 +11,198 @@ import Foundation // This client implementation is for little-endian platform, namely x86, x64 & arm // For big-endian platforms like PowerPC, there must be a huge overhaul -protocol FileProviderSMBTaskDelegate: class { - func receivedResponse(client: FileProviderSMBTask, response: SMBResponse, for: SMBRequest) +enum SMBClientError: Error { + case streamNotOpened + case timedOut } -class FileProviderSMBTask: FileProviderStreamTask { - var timeout: TimeInterval = 30 +@objcMembers +class SMBClient: NSObject, StreamDelegate { + fileprivate var inputStream: InputStream? + fileprivate var outputStream: OutputStream? + fileprivate var operation_queue: OperationQueue! - private(set) var lastMessageID: UInt64 = 0 - private(set) var sessionId: UInt64 = 0 - private func messageId() -> UInt64 { + fileprivate var host: (hostname: String, port: Int)? + fileprivate var service: NetService? + + public var timeout: TimeInterval = 30 + + internal private(set) var messageId: UInt64 = 0 + fileprivate func createMessageId() -> UInt64 { defer { - lastMessageID += 1 + messageId += 1 + } + return messageId + } + + internal private(set) var credit: UInt16 = 0 + fileprivate func consumeCredit() -> UInt16 { + if credit > 0 { + credit -= 1 + return credit + } else { + return 0 } - return lastMessageID } + private(set) var sessionId: UInt64 = 0 + private(set) var establishedTrees = Array() private(set) var requestStack = [Int: SMBRequest]() private(set) var responseStack = [Int: SMBResponse]() - weak var delegate: FileProviderSMBTaskDelegate? - - func sendNegotiate(completionHandler: SimpleCompletionHandler) -> UInt64 { - let mId = messageId() - let smbHeader = SMB2.Header(command: .NEGOTIATE, creditRequestResponse: UInt16(126), messageId: mId, treeId: UInt32(0), sessionId: UInt64(0)) - let msg = SMB2.NegotiateRequest() - let data = createSMB2Message(header: smbHeader, message: msg) - self.write(data, timeout: timeout, completionHandler: { (e) in - completionHandler?(e) - }) - return mId + init(host: String, port: Int) { + self.host = (host, port) + self.operation_queue = OperationQueue() + self.operation_queue.name = "FileProviderStreamTask" + self.operation_queue.maxConcurrentOperationCount = 1 + super.init() } - func sendSessionSetup(completionHandler: SimpleCompletionHandler) -> UInt64 { - let mId = messageId() - let credit = UInt16(sessionId > 0 ? 124 : 125) - let smbHeader = SMB2.Header(command: SMB2.Command.SESSION_SETUP, creditRequestResponse: credit, messageId: mId, treeId: UInt32(0), sessionId: sessionId) - let msg = SMB2.SessionSetupRequest(singing: []) - let data = createSMB2Message(header: smbHeader, message: msg) - self.write(data, timeout: timeout, completionHandler: { (e) in - if self.sessionId == 0 { - self.readData(ofMinLength: 64, maxLength: 65536, timeout: self.timeout, completionHandler: { (data, eof, e2) in - // TODO: set session id - completionHandler?(e2 ?? e) - }) - } - }) - return mId + deinit { + close() } - func sendTreeConnect(completionHandler: SimpleCompletionHandler) -> UInt64 { - let req = self.currentRequest ?? self.originalRequest - guard let url = req?.url, let host = url.host else { - return 0 - } - let mId = messageId() - let smbHeader = SMB2.Header(command: .TREE_CONNECT, creditRequestResponse: 123, messageId: mId, treeId: 0, sessionId: sessionId) - let cmp = url.pathComponents - let share = cmp.first ?? "" - let tcHeader = SMB2.TreeConnectRequest.Header(flags: []) - let msg = SMB2.TreeConnectRequest(header: tcHeader, host: host, share: share) - let data = createSMB2Message(header: smbHeader, message: msg!) - self.write(data, timeout: timeout, completionHandler: { (e) in - completionHandler?(e) + fileprivate func open(secure: Bool = false) { + var readStream : Unmanaged? + var writeStream : Unmanaged? + + if inputStream == nil || outputStream == nil { + if let host = host { + CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, host.hostname as CFString, UInt32(host.port), &readStream, &writeStream) + } else if let service = service { + let cfnetService = CFNetServiceCreate(kCFAllocatorDefault, service.domain as CFString, service.type as CFString, service.name as CFString, Int32(service.port)) + CFStreamCreatePairWithSocketToNetService(kCFAllocatorDefault, cfnetService.takeRetainedValue(), &readStream, &writeStream) + } - }) - return mId + inputStream = readStream?.takeRetainedValue() + outputStream = writeStream?.takeRetainedValue() + } + + guard let inputStream = inputStream, let outputStream = outputStream else { + return + } + + if secure { + inputStream.setProperty(StreamSocketSecurityLevel.negotiatedSSL.rawValue, forKey: .socketSecurityLevelKey) + outputStream.setProperty(StreamSocketSecurityLevel.negotiatedSSL.rawValue, forKey: .socketSecurityLevelKey) + } + + inputStream.delegate = self + outputStream.delegate = self + inputStream.schedule(in: RunLoop.main, forMode: .init("kCFRunLoopDefaultMode")) + outputStream.schedule(in: RunLoop.main, forMode: .init("kCFRunLoopDefaultMode")) + inputStream.open() + outputStream.open() + + operation_queue.isSuspended = false } - func sendTreeDisconnect(id treeId: UInt32, completionHandler: SimpleCompletionHandler) -> UInt64 { - let mId = messageId() - let smbHeader = SMB2.Header(command: .TREE_DISCONNECT, creditRequestResponse: 111, messageId: mId, treeId: treeId, sessionId: sessionId) - let msg = SMB2.TreeDisconnect() - let data = createSMB2Message(header: smbHeader, message: msg) - self.write(data, timeout: timeout, completionHandler: { (e) in - completionHandler?(e) - }) - return mId + fileprivate func close() { + self.inputStream?.close() + self.outputStream?.close() + self.inputStream?.remove(from: RunLoop.main, forMode: .init("kCFRunLoopDefaultMode")) + self.outputStream?.remove(from: RunLoop.main, forMode: .init("kCFRunLoopDefaultMode")) + self.inputStream?.delegate = nil + self.outputStream?.delegate = nil + + self.inputStream = nil + self.outputStream = nil } - func sendLogoff(id treeId: UInt32, completionHandler: SimpleCompletionHandler) -> UInt64 { - let mId = messageId() - let smbHeader = SMB2.Header(command: .LOGOFF, creditRequestResponse: 0, messageId: mId, treeId: 0, sessionId: sessionId) - let msg = SMB2.LogOff() - let data = createSMB2Message(header: smbHeader, message: msg) - self.write(data, timeout: timeout, completionHandler: { (e) in - completionHandler?(e) - }) - return mId + @discardableResult + fileprivate func write(data: Data) throws -> Int { + guard let outputStream = self.outputStream else { + throw SMBClientError.streamNotOpened + } + let expireDate = Date(timeIntervalSinceNow: timeout) + var data = data + var byteSent: Int = 0 + while data.count > 0 { + let bytesWritten: Int = (try? outputStream.write(data: data)) ?? -1 + + if bytesWritten > 0 { + let range = 0.. 0 { + + /*dataReceived.append(&buffer, count: len) + self._countOfBytesRecieved += Int64(len)*/ + } + } + } } } // MARK: create and analyse messages -extension FileProviderSMBTask { +extension SMBClient { + internal func sendMessage(_ message: SMBRequestBody, toTree treeId: UInt32, completionHandler: SimpleCompletionHandler) -> UInt64 { + let mId = createMessageId() + let credit = consumeCredit() + let smbHeader = SMB2.Header(command: message.command, creditRequestResponse: credit, messageId: mId, treeId: treeId, sessionId: sessionId) + let data = createRequest(header: smbHeader, message: message) + operation_queue.addOperation { + do { + try self.write(data: data) + completionHandler?(nil) + } catch { + completionHandler?(error) + } + } + return mId + } +} + + +fileprivate extension SMBClient { func determineSMBVersion(_ data: Data) -> Float { let smbverChar: Int8 = Int8(bitPattern: data.first ?? 0) let version = 0 - smbverChar return Float(version) } - func digestSMBMessage(_ data: Data) throws -> (header: SMB1.Header, blocks: [(params: [UInt16], message: Data?)]) { - guard data.count > 30 else { - throw URLError(.badServerResponse) - } - var buffer = [UInt8](repeating: 0, count: data.count) - guard determineSMBVersion(data) == 1 else { - throw SMBFileProviderError.incompatibleHeader - } - let headersize = MemoryLayout.size - let header: SMB1.Header = data.scanValue()! - var blocks = [(params: [UInt16], message: Data?)]() - var offset = headersize - while offset < data.count { - let paramWords: [UInt16] - let paramWordsCount = Int(buffer[offset]) - guard data.count > (paramWordsCount * 2 + offset) else { - throw SMBFileProviderError.incorrectParamsLength - } - offset += MemoryLayout.size - var rawParamWords = [UInt8](buffer[offset..<(offset + paramWordsCount * 2)]) - let paramData = Data(bytesNoCopy: UnsafeMutablePointer(&rawParamWords), count: rawParamWords.count, deallocator: .free) - paramWords = paramData.scanValue()! - offset += paramWordsCount * 2 - let messageBytesCountHi = Int(buffer[1]) << 8 - let messageBytesCount = Int(buffer[0]) + messageBytesCountHi - offset += MemoryLayout.size - guard data.count >= (offset + messageBytesCount) else { - throw SMBFileProviderError.incorrectMessageLength - } - let rawMessage = [UInt8](buffer[offset..<(offset + messageBytesCount)]) - offset += messageBytesCount - let message = Data(bytes: rawMessage) - blocks.append((params: paramWords, message: message)) - } - return (header, blocks) + func createRequest(header: SMB2.Header, message: SMBRequestBody) -> Data { + var result = Data(value: header) + result.append(message.data()) + return result } - func digestSMB2Message(_ data: Data) throws -> SMBResponse? { + func responseOf(_ data: Data) throws -> SMBResponse? { guard data.count > 65 else { throw URLError(.badServerResponse) } - guard determineSMBVersion(data) == 2 else { + guard determineSMBVersion(data) >= 2 else { throw SMBFileProviderError.incompatibleHeader } let headersize = MemoryLayout.size @@ -203,12 +249,12 @@ extension FileProviderSMBTask { return (header, SMB2.SetInfoResponse(data: messageData)) case .OPLOCK_BREAK: return (header, nil) // FIXME: - case .INVALID: + default: throw SMBFileProviderError.invalidCommand } } - func createSMBMessage(header: SMB1.Header, blocks: [(params: Data?, message: Data?)]) -> Data { + /*func createSMBMessage(header: SMB1.Header, blocks: [(params: Data?, message: Data?)]) -> Data { var result = Data(value: header) for block in blocks { var paramWordsCount = UInt8(block.params?.count ?? 0) @@ -224,11 +270,42 @@ extension FileProviderSMBTask { } } return result - } + }*/ - func createSMB2Message(header: SMB2.Header, message: SMBRequestBody) -> Data { - var result = Data(value: header) - result.append(message.data()) - return result - } + /*func digestSMBMessage(_ data: Data) throws -> (header: SMB1.Header, blocks: [(params: [UInt16], message: Data?)]) { + guard data.count > 30 else { + throw URLError(.badServerResponse) + } + var buffer = [UInt8](repeating: 0, count: data.count) + guard determineSMBVersion(data) == 1 else { + throw SMBFileProviderError.incompatibleHeader + } + let headersize = MemoryLayout.size + let header: SMB1.Header = data.scanValue()! + var blocks = [(params: [UInt16], message: Data?)]() + var offset = headersize + while offset < data.count { + let paramWords: [UInt16] + let paramWordsCount = Int(buffer[offset]) + guard data.count > (paramWordsCount * 2 + offset) else { + throw SMBFileProviderError.incorrectParamsLength + } + offset += MemoryLayout.size + var rawParamWords = [UInt8](buffer[offset..<(offset + paramWordsCount * 2)]) + let paramData = Data(bytesNoCopy: UnsafeMutablePointer(&rawParamWords), count: rawParamWords.count, deallocator: .free) + paramWords = paramData.scanValue()! + offset += paramWordsCount * 2 + let messageBytesCountHi = Int(buffer[1]) << 8 + let messageBytesCount = Int(buffer[0]) + messageBytesCountHi + offset += MemoryLayout.size + guard data.count >= (offset + messageBytesCount) else { + throw SMBFileProviderError.incorrectMessageLength + } + let rawMessage = [UInt8](buffer[offset..<(offset + messageBytesCount)]) + offset += messageBytesCount + let message = Data(bytes: rawMessage) + blocks.append((params: paramWords, message: message)) + } + return (header, blocks) + }*/ } diff --git a/Sources/SMBFileProvider.swift b/Sources/SMBFileProvider.swift index 6f5338d..58ef68d 100644 --- a/Sources/SMBFileProvider.swift +++ b/Sources/SMBFileProvider.swift @@ -11,7 +11,6 @@ import Foundation class SMBFileProvider: FileProvider, FileProviderMonitor { open class var type: String { return "SMB" } open var baseURL: URL? - open var currentPath: String = "" open var dispatch_queue: DispatchQueue open var operation_queue: OperationQueue open weak var delegate: FileProviderDelegate? @@ -25,11 +24,7 @@ class SMBFileProvider: FileProvider, FileProviderMonitor { } self.baseURL = baseURL.appendingPathComponent("") - #if swift(>=3.1) let queueLabel = "FileProvider.\(Swift.type(of: self).type)" - #else - let queueLabel = "FileProvider.\(type(of: self).type)" - #endif dispatch_queue = DispatchQueue(label: queueLabel, attributes: .concurrent) operation_queue = OperationQueue() operation_queue.name = "\(queueLabel).Operation" @@ -38,18 +33,20 @@ class SMBFileProvider: FileProvider, FileProviderMonitor { } public required convenience init?(coder aDecoder: NSCoder) { - guard let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL else { + guard let baseURL = aDecoder.decodeObject(of: NSURL.self, forKey: "baseURL") as URL? else { + if #available(macOS 10.11, iOS 9.0, tvOS 9.0, *) { + aDecoder.failWithError(CocoaError(.coderValueNotFound, + userInfo: [NSLocalizedDescriptionKey: "Base URL is not set."])) + } return nil } self.init(baseURL: baseURL, - credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential) - self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? "" + credential: aDecoder.decodeObject(of: URLCredential.self, forKey: "credential")) } open func encode(with aCoder: NSCoder) { aCoder.encode(self.baseURL, forKey: "baseURL") aCoder.encode(self.credential, forKey: "credential") - aCoder.encode(self.currentPath, forKey: "currentPath") } public static var supportsSecureCoding: Bool { @@ -68,7 +65,7 @@ class SMBFileProvider: FileProvider, FileProviderMonitor { NotImplemented() } - func isReachable(completionHandler: @escaping (Bool) -> Void) { + func isReachable(completionHandler: @escaping (_ success: Bool, _ error: Error?) -> Void) { NotImplemented() } @@ -138,7 +135,6 @@ class SMBFileProvider: FileProvider, FileProviderMonitor { open func copy(with zone: NSZone? = nil) -> Any { let copy = SMBFileProvider(baseURL: self.baseURL!, credential: self.credential!)! - copy.currentPath = self.currentPath copy.delegate = self.delegate copy.fileOperationDelegate = self.fileOperationDelegate return copy diff --git a/Sources/SMBTypes/SMB2DataTypes.swift b/Sources/SMBTypes/SMB2DataTypes.swift index 309151e..7aacb02 100644 --- a/Sources/SMBTypes/SMB2DataTypes.swift +++ b/Sources/SMBTypes/SMB2DataTypes.swift @@ -8,11 +8,32 @@ import Foundation +protocol Option: RawRepresentable, Hashable { + +} + +extension Option where RawValue: Hashable { + var hashValue: Int { + return rawValue.hashValue + } +} + +extension Option where RawValue: Equatable { + static func ==(lhs: Self, rhs: Self) -> Bool { + return lhs.rawValue == rhs.rawValue + } +} + protocol SMBRequestBody { + static var command: SMB2.Command { get } func data() -> Data } extension SMBRequestBody { + var command: SMB2.Command { + return Swift.type(of: self).command + } + func data() -> Data { return Data(value: self) } diff --git a/Sources/SMBTypes/SMB2FileHandle.swift b/Sources/SMBTypes/SMB2FileHandle.swift index 04bf4e0..e7b1200 100644 --- a/Sources/SMBTypes/SMB2FileHandle.swift +++ b/Sources/SMBTypes/SMB2FileHandle.swift @@ -12,6 +12,8 @@ extension SMB2 { // MARK: SMB2 Create struct CreateRequest: SMBRequestBody { + static var command: SMB2.Command = .CREATE + let header: CreateRequest.Header let name: String? let contexts: [CreateContext] @@ -48,38 +50,14 @@ extension SMB2 { struct Header { let size: UInt16 fileprivate let securityFlags: UInt8 - fileprivate var _requestedOplockLevel: UInt8 - var requestedOplockLevel: OplockLevel { - get { - return OplockLevel(rawValue: _requestedOplockLevel)! - } - set { - _requestedOplockLevel = newValue.rawValue - } - } - fileprivate var _impersonationLevel: UInt32 - var impersonationLevel: ImpersonationLevel { - get { - return ImpersonationLevel(rawValue: _impersonationLevel)! - } - set { - _impersonationLevel = newValue.rawValue - } - } + var requestedOplockLevel: OplockLevel + var impersonationLevel: ImpersonationLevel fileprivate let flags: UInt64 fileprivate let reserved: UInt64 let access: FileAccessMask let fileAttributes: FileAttributes let shareAccess: ShareAccess - fileprivate var _desposition: UInt32 - var desposition: CreateDisposition { - get { - return CreateDisposition(rawValue: _desposition)! - } - set { - _desposition = newValue.rawValue - } - } + var desposition: CreateDisposition let options: CreateOptions var nameOffset: UInt16 var nameLength: UInt16 @@ -89,14 +67,14 @@ extension SMB2 { init(requestedOplockLevel: OplockLevel = .NONE, impersonationLevel: ImpersonationLevel = .anonymous, access: FileAccessMask = [.GENERIC_ALL], fileAttributes: FileAttributes = [], shareAccess: ShareAccess = [.READ], desposition: CreateDisposition = .OPEN_IF, options: CreateOptions = []) { self.size = 57 self.securityFlags = 0 - self._requestedOplockLevel = requestedOplockLevel.rawValue - self._impersonationLevel = impersonationLevel.rawValue + self.requestedOplockLevel = requestedOplockLevel + self.impersonationLevel = impersonationLevel self.flags = 0 self.reserved = 0 self.access = access self.fileAttributes = fileAttributes self.shareAccess = shareAccess - self._desposition = desposition.rawValue + self.desposition = desposition self.options = options self.nameOffset = 0 self.nameLength = 0 @@ -135,36 +113,44 @@ extension SMB2 { fileprivate static let RESERVE_OPFILTER = CreateOptions(rawValue: 0x00100000) } - enum CreateDisposition: UInt32 { + struct CreateDisposition: Option { + init(rawValue: UInt32) { + self.rawValue = rawValue + } + + var rawValue: UInt32 /// If the file already exists, supersede it. Otherwise, create the file. - case SUPERSEDE = 0x00000000 + public static let SUPERSEDE = CreateDisposition(rawValue: 0x00000000) /// If the file already exists, return success; otherwise, fail the operation. - case OPEN = 0x00000001 + public static let OPEN = CreateDisposition(rawValue: 0x00000001) /// If the file already exists, fail the operation; otherwise, create the file. - case CREATE = 0x00000002 + public static let CREATE = CreateDisposition(rawValue: 0x00000002) /// Open the file if it already exists; otherwise, create the file. - case OPEN_IF = 0x00000003 + public static let OPEN_IF = CreateDisposition(rawValue: 0x00000003) /// Overwrite the file if it already exists; otherwise, fail the operation. - case OVERWRITE = 0x00000004 + public static let OVERWRITE = CreateDisposition(rawValue: 0x00000004) /// Overwrite the file if it already exists; otherwise, create the file. - case OVERWRITE_IF = 0x00000005 + public static let OVERWRITE_IF = CreateDisposition(rawValue: 0x00000005) } - enum ImpersonationLevel: UInt32 { - case anonymous = 0x00000000 - case identification = 0x00000001 - case impersonation = 0x00000002 - case delegate = 0x00000003 + struct ImpersonationLevel { + init(rawValue: UInt32) { + self.rawValue = rawValue + } + + var rawValue: UInt32 + + public static let anonymous = ImpersonationLevel(rawValue: 0x00000000) + public static let identification = ImpersonationLevel(rawValue: 0x00000001) + public static let impersonation = ImpersonationLevel(rawValue: 0x00000002) + public static let delegate = ImpersonationLevel(rawValue: 0x00000003) } } struct CreateResponse: SMBResponseBody { struct Header { let size: UInt16 - fileprivate let _oplockLevel: UInt8 - var oplockLevel: OplockLevel { - return OplockLevel(rawValue: _oplockLevel)! - } + let oplockLevel: OplockLevel fileprivate let reserved: UInt32 let creationTime: SMBTime let lastAccessTime: SMBTime @@ -248,34 +234,47 @@ extension SMB2 { return result } - enum ContextNames: String { + struct ContextNames: Option { + init(rawValue: String) { + self.rawValue = rawValue + } + + let rawValue: String + /// Request Create Context: Extended attributes - case EA_BUFFER = "ExtA" + public static let EA_BUFFER = ContextNames(rawValue: "ExtA") /// Request Create Context: Security descriptor - case SD_BUFFER = "SecD" + public static let SD_BUFFER = ContextNames(rawValue: "SecD") /// Request & Response Create Context: Open to be durable - case DURABLE_HANDLE = "DHnQ" - case DURABLE_HANDLE_RESPONSE_V2 = "DH2Q" + public static let DURABLE_HANDLE = ContextNames(rawValue: "DHnQ") + /// Request & Response Create Context: Open to be durable + public static let DURABLE_HANDLE_RESPONSE_V2 = ContextNames(rawValue: "DH2Q") /// Request Create Context: Reconnect to a durable open after being disconnected - case DURABLE_HANDLE_RECONNECT = "DHnC" + public static let DURABLE_HANDLE_RECONNECT = ContextNames(rawValue: "DHnC") /// Request Create Context: Required allocation size of the newly created file - case ALLOCATION_SIZE = "AISi" + public static let ALLOCATION_SIZE = ContextNames(rawValue: "AISi") /// Request & Response Create Context: Maximal access information - case QUERY_MAXIMAL_ACCESS = "MxAc" - case TIMEWARP_TOKEN = "TWrp" + public static let QUERY_MAXIMAL_ACCESS = ContextNames(rawValue: "MxAc") + public static let TIMEWARP_TOKEN = ContextNames(rawValue: "TWrp") /// Response Create Context: DiskID of the open file in a volume. - case QUERY_ON_DISK_ID = "QFid" + public static let QUERY_ON_DISK_ID = ContextNames(rawValue: "QFid") /// Response Create Context: A lease. This value is only supported for the SMB 2.1 and 3.x dialect family. - case LEASE = "RqLs" + public static let LEASE = ContextNames(rawValue: "RqLs") } } - enum OplockLevel: UInt8 { - case NONE = 0x00 - case LEVEL_II = 0x01 - case EXCLUSIVE = 0x08 - case BATCH = 0x09 - case LEASE = 0xFF + struct OplockLevel { + let rawValue: UInt8 + + init(rawValue: UInt8) { + self.rawValue = rawValue + } + + public static let NONE = OplockLevel(rawValue: 0x00) + public static let LEVEL_II = OplockLevel(rawValue: 0x01) + public static let EXCLUSIVE = OplockLevel(rawValue: 0x08) + public static let BATCH = OplockLevel(rawValue: 0x09) + public static let LEASE = OplockLevel(rawValue: 0xFF) } struct ShareAccess: OptionSet { @@ -358,6 +357,8 @@ extension SMB2 { // MARK: SMB2 Close struct CloseRequest: SMBRequestBody { + static var command: SMB2.Command = .CLOSE + let size: UInt16 let flags: CloseFlags fileprivate let reserved2: UInt32 @@ -399,6 +400,8 @@ extension SMB2 { // MARK: SMB2 Flush struct FlushRequest: SMBRequestBody { + static var command: SMB2.Command = .FLUSH + let size: UInt16 fileprivate let reserved: UInt16 fileprivate let reserved2: UInt32 diff --git a/Sources/SMBTypes/SMB2FileOperation.swift b/Sources/SMBTypes/SMB2FileOperation.swift index 18f1b3b..a541cc9 100644 --- a/Sources/SMBTypes/SMB2FileOperation.swift +++ b/Sources/SMBTypes/SMB2FileOperation.swift @@ -12,6 +12,8 @@ extension SMB2 { // MARK: SMB2 Read struct ReadRequest: SMBRequestBody { + static var command: SMB2.Command = .READ + let size: UInt16 fileprivate let padding: UInt8 let flags: ReadRequest.Flags @@ -19,10 +21,7 @@ extension SMB2 { let offset: UInt64 let fileId: FileId let minimumLength: UInt32 - fileprivate let _channel: UInt32 - var channel: Channel { - return Channel(rawValue: _channel) ?? .NONE - } + let channel: Channel let remainingBytes: UInt32 fileprivate let channelInfoOffset: UInt16 fileprivate let channelInfoLength: UInt16 @@ -36,7 +35,7 @@ extension SMB2 { self.offset = offset self.fileId = fileId self.minimumLength = minimumLength - self._channel = channel.rawValue + self.channel = channel self.remainingBytes = remainingBytes self.channelInfoOffset = 0 self.channelInfoLength = 0 @@ -77,15 +76,23 @@ extension SMB2 { } } - enum Channel: UInt32 { - case NONE = 0x00000000 - case RDMA_V1 = 0x00000001 - case RDMA_V1_INVALIDATE = 0x00000002 + struct Channel: Option { + init(rawValue: UInt32) { + self.rawValue = rawValue + } + + let rawValue: UInt32 + + public static let NONE = Channel(rawValue: 0x00000000) + public static let RDMA_V1 = Channel(rawValue: 0x00000001) + public static let RDMA_V1_INVALIDATE = Channel(rawValue: 0x00000002) } // MARK: SMB2 Write struct WriteRequest: SMBRequestBody { + static var command: SMB2.Command = .WRITE + let header: WriteRequest.Header let channelInfo: ChannelInfo? let fileData: Data @@ -96,10 +103,7 @@ extension SMB2 { let length: UInt32 let offset: UInt64 let fileId: FileId - fileprivate let _channel: UInt32 - var channel: Channel { - return Channel(rawValue: _channel) ?? .NONE - } + let channel: Channel let remainingBytes: UInt32 let channelInfoOffset: UInt16 let channelInfoLength: UInt16 @@ -115,7 +119,7 @@ extension SMB2 { channelInfoLength = UInt16(MemoryLayout.size) } let dataOffset = UInt16(MemoryLayout.size + MemoryLayout.size) + channelInfoLength - self.header = WriteRequest.Header(size: UInt16(49), dataOffset: dataOffset, length: UInt32(data.count), offset: offset, fileId: fileId, _channel: channel.rawValue, remainingBytes: remainingBytes, channelInfoOffset: channelInfoOffset, channelInfoLength: channelInfoLength, flags: flags) + self.header = WriteRequest.Header(size: UInt16(49), dataOffset: dataOffset, length: UInt32(data.count), offset: offset, fileId: fileId, channel: channel, remainingBytes: remainingBytes, channelInfoOffset: channelInfoOffset, channelInfoLength: channelInfoLength, flags: flags) self.channelInfo = channelInfo self.fileData = data } @@ -152,6 +156,8 @@ extension SMB2 { } struct ChannelInfo: SMBRequestBody { + static var command: SMB2.Command = .WRITE + let offset: UInt64 let token: UInt32 let length: UInt32 @@ -160,6 +166,8 @@ extension SMB2 { // MARK: SMB2 Lock struct LockElement: SMBRequestBody { + static var command: SMB2.Command = .LOCK + let offset: UInt64 let length: UInt64 let flags: LockElement.Flags @@ -180,6 +188,8 @@ extension SMB2 { } struct LockRequest: SMBRequestBody { + static var command: SMB2.Command = .LOCK + let header: LockRequest.Header let locks: [LockElement] @@ -217,6 +227,8 @@ extension SMB2 { // MARK: SMB2 Cancel struct CancelRequest: SMBRequestBody { + static var command: SMB2.Command = .CANCEL + let size: UInt16 let reserved: UInt16 diff --git a/Sources/SMBTypes/SMB2IOCtl.swift b/Sources/SMBTypes/SMB2IOCtl.swift index bece831..0e9a5bf 100644 --- a/Sources/SMBTypes/SMB2IOCtl.swift +++ b/Sources/SMBTypes/SMB2IOCtl.swift @@ -16,12 +16,14 @@ extension SMB2 { */ struct IOCtlRequest: SMBRequestBody { + static var command: SMB2.Command = .IOCTL + let header: Header let requestData: IOCtlRequestProtocol? init(fileId: FileId ,ctlCode: IOCtlCode, requestData: IOCtlRequestProtocol?, flags: IOCtlRequest.Flags = []) { let offset = requestData != nil ? UInt32(MemoryLayout.size + MemoryLayout.size) : 0 - self.header = Header(size: 57, reserved: 0, _ctlCode: ctlCode.rawValue, fileId: fileId, inputOffset: offset, inputCount: UInt32((requestData?.data().count ?? 0)), maxInputResponse: 0, outputOffset: offset, outputCount: 0, maxOutputResponse: UInt32(Int32.max), flags: flags, reserved2: 0) + self.header = Header(size: 57, reserved: 0, ctlCode: ctlCode, fileId: fileId, inputOffset: offset, inputCount: UInt32((requestData?.data().count ?? 0)), maxInputResponse: 0, outputOffset: offset, outputCount: 0, maxOutputResponse: UInt32(Int32.max), flags: flags, reserved2: 0) self.requestData = requestData } @@ -36,10 +38,7 @@ extension SMB2 { struct Header { let size: UInt16 fileprivate let reserved: UInt16 - fileprivate let _ctlCode: UInt32 - var ctlCode: IOCtlCode { - return IOCtlCode(rawValue: _ctlCode)! - } + let ctlCode: IOCtlCode let fileId: FileId let inputOffset: UInt32 let inputCount: UInt32 @@ -92,10 +91,7 @@ extension SMB2 { struct Header { let size: UInt16 fileprivate let reserved: UInt16 - fileprivate let _ctlCode: UInt32 - var ctlCode: IOCtlCode { - return IOCtlCode(rawValue: _ctlCode)! - } + let ctlCode: IOCtlCode let fileId: FileId let inputOffset: UInt32 let inputCount: UInt32 @@ -106,35 +102,43 @@ extension SMB2 { } } - enum IOCtlCode: UInt32 { - case DFS_GET_REFERRALS = 0x00060194 - case DFS_GET_REFERRALS_EX = 0x000601B0 - case SET_REPARSE_POINT = 0x000900A4 - case FILE_LEVEL_TRIM = 0x00098208 - case PIPE_PEEK = 0x0011400C - case PIPE_WAIT = 0x00110018 + struct IOCtlCode: Option { + let rawValue: UInt32 + + init(rawValue: UInt32) { + self.rawValue = rawValue + } + + public static let DFS_GET_REFERRALS = IOCtlCode(rawValue: 0x00060194) + public static let DFS_GET_REFERRALS_EX = IOCtlCode(rawValue: 0x000601B0) + public static let SET_REPARSE_POINT = IOCtlCode(rawValue: 0x000900A4) + public static let FILE_LEVEL_TRIM = IOCtlCode(rawValue: 0x00098208) + public static let PIPE_PEEK = IOCtlCode(rawValue: 0x0011400C) + public static let PIPE_WAIT = IOCtlCode(rawValue: 0x00110018) /// PIPE_TRANSCEIVE is valid only on a named pipe with mode set to FILE_PIPE_MESSAGE_MODE. - case PIPE_TRANSCEIVE = 0x0011C017 + public static let PIPE_TRANSCEIVE = IOCtlCode(rawValue: 0x0011C017) /// Get ResumeKey used by the client to uniquely identify the source file in an FSCTL_SRV_COPYCHUNK or FSCTL_SRV_COPYCHUNK_WRITE request. - case SRV_REQUEST_RESUME_KEY = 0x00140078 + public static let SRV_REQUEST_RESUME_KEY = IOCtlCode(rawValue: 0x00140078) /// Get all the revision time-stamps that are associated with the Tree Connect share in which the open resides - case SRV_ENUMERATE_SNAPSHOTS = 0x00144064 + public static let SRV_ENUMERATE_SNAPSHOTS = IOCtlCode(rawValue: 0x00144064) /// Reads a chunk of file for performing server side copy operations. - case SRV_COPYCHUNK = 0x001440F2 + public static let SRV_COPYCHUNK = IOCtlCode(rawValue: 0x001440F2) /// Retrieve data from the Content Information File associated with a specified file, not valid for the SMB 2.0.2 dialect. - case SRV_READ_HASH = 0x001441BB + public static let SRV_READ_HASH = IOCtlCode(rawValue: 0x001441BB) /// Writes the chunk of file for performing server side copy operations. - case SRV_COPYCHUNK_WRITE = 0x001480F2 + public static let SRV_COPYCHUNK_WRITE = IOCtlCode(rawValue: 0x001480F2) /// Request resiliency for a specified open file, not valid for the SMB 2.0.2 dialect. - case LMR_REQUEST_RESILIENCY = 0x001401D4 + public static let LMR_REQUEST_RESILIENCY = IOCtlCode(rawValue: 0x001401D4) /// Get server network interface info e.g. link speed and socket address information - case QUERY_NETWORK_INTERFACE_INFO = 0x001401FC + public static let QUERY_NETWORK_INTERFACE_INFO = IOCtlCode(rawValue: 0x001401FC) /// Request validation of a previous SMB 2 NEGOTIATE, valid for SMB 3.0 and SMB 3.0.2 dialects. - case VALIDATE_NEGOTIATE_INFO = 0x00140204 + public static let VALIDATE_NEGOTIATE_INFO = IOCtlCode(rawValue: 0x00140204) } struct IOCtlRequestData { struct CopyChunk: IOCtlRequestProtocol { + static var command: SMB2.Command = .IOCTL + let sourceKey: (UInt64, UInt64, UInt64) let chunkCount: UInt32 let chunks: [Chunk] @@ -156,31 +160,26 @@ extension SMB2 { } struct ReadHash: IOCtlRequestProtocol { - let _hashType: UInt32 - var hashType: IOCtlHashType { - return IOCtlHashType(rawValue: _hashType) ?? .PEER_DIST - } - let _hashVersion: UInt32 - var hashVersion: IOCtlHashVersion { - return IOCtlHashVersion(rawValue: _hashVersion) ?? .VER_1 - } - let _hashRetrievalType: UInt32 - var hashRetrievalType: IOCtlHashRetrievalType { - return IOCtlHashRetrievalType(rawValue: _hashRetrievalType) ?? .FILE_BASED - } + static var command: SMB2.Command = .IOCTL + + let _hashType: IOCtlHashType + let _hashVersion: IOCtlHashVersion + let _hashRetrievalType: IOCtlHashRetrievalType let length: UInt32 let offset: UInt64 init(offset: UInt64, length: UInt32, hashType: IOCtlHashType = .PEER_DIST, hashVersion: IOCtlHashVersion = .VER_1, hashRetrievalType: IOCtlHashRetrievalType = .FILE_BASED) { - self._hashType = hashType.rawValue - self._hashVersion = hashVersion.rawValue - self._hashRetrievalType = hashRetrievalType.rawValue + self._hashType = hashType + self._hashVersion = hashVersion + self._hashRetrievalType = hashRetrievalType self.length = length self.offset = offset } } struct ResilencyRequest: IOCtlRequestProtocol { + static var command: SMB2.Command = .IOCTL + let timeout: UInt32 fileprivate let reserved: UInt32 @@ -192,6 +191,8 @@ extension SMB2 { } struct ValidateNegotiateInfo: IOCtlRequestProtocol { + static var command: SMB2.Command = .IOCTL + let header: ValidateNegotiateInfo.Header let dialects: [UInt16] @@ -309,11 +310,11 @@ extension SMB2 { static let ipv6: sa_family_t = 0x17 var sockaddr: sockaddr_in { - return Data.mapMemory(from: self.sockaddrStorage)! + return unsafeBitCast(self.sockaddrStorage, to: sockaddr_in.self) } var sockaddr6: sockaddr_in6 { - return Data.mapMemory(from: self.sockaddrStorage)! + return unsafeBitCast(self.sockaddrStorage, to: sockaddr_in6.self) } } } @@ -340,17 +341,35 @@ extension SMB2 { static let RDMA_CAPABLE = IOCtlCapabilities(rawValue: 0x00000002) } - enum IOCtlHashType: UInt32 { - case PEER_DIST = 0x00000001 + struct IOCtlHashType: Option { + let rawValue: UInt32 + + init(rawValue: UInt32) { + self.rawValue = rawValue + } + + public static let PEER_DIST = IOCtlHashType(rawValue: 0x00000001) } - enum IOCtlHashVersion: UInt32 { - case VER_1 = 0x00000001 - case VER_2 = 0x00000002 + struct IOCtlHashVersion: Option { + let rawValue: UInt32 + + init(rawValue: UInt32) { + self.rawValue = rawValue + } + + public static let VER_1 = IOCtlHashVersion(rawValue: 0x00000001) + public static let VER_2 = IOCtlHashVersion(rawValue: 0x00000002) } - enum IOCtlHashRetrievalType: UInt32 { - case HASH_BASED = 0x00000001 - case FILE_BASED = 0x00000002 + struct IOCtlHashRetrievalType: Option { + let rawValue: UInt32 + + init(rawValue: UInt32) { + self.rawValue = rawValue + } + + public static let HASH_BASED = IOCtlHashRetrievalType(rawValue: 0x00000001) + public static let FILE_BASED = IOCtlHashRetrievalType(rawValue: 0x00000002) } } diff --git a/Sources/SMBTypes/SMB2Notification.swift b/Sources/SMBTypes/SMB2Notification.swift index 23cdb92..8a44429 100644 --- a/Sources/SMBTypes/SMB2Notification.swift +++ b/Sources/SMBTypes/SMB2Notification.swift @@ -12,6 +12,8 @@ extension SMB2 { // MARK: SMB2 Change Notify struct ChangeNotifyRequest: SMBRequestBody { + static var command: SMB2.Command = .CHANGE_NOTIFY + let size: UInt16 let flags: ChangeNotifyRequest.Flags let outputBufferLength: UInt32 @@ -87,9 +89,7 @@ extension SMB2 { while i < maxLoop { let nextOffset: UInt32 = data.scanValue(start: offset) ?? 0 let actionValue: UInt32 = data.scanValue(start: offset + 4) ?? 0 - guard let action = FileNotifyAction(rawValue: actionValue) else { - continue - } + let action = FileNotifyAction(rawValue: actionValue) let fileNameLen = Int(data.scanValue(start: offset + 8) as UInt32? ?? 0) let fileName = data.scanString(start: offset + 12, length: fileNameLen, using: .utf16) ?? "" @@ -106,28 +106,38 @@ extension SMB2 { } } - enum FileNotifyAction: UInt32 { + struct FileNotifyAction: Option { + init(rawValue: UInt32) { + self.rawValue = rawValue + } + + init(_ rawValue: UInt32) { + self.rawValue = rawValue + } + + let rawValue: UInt32 + /// The file was added to the directory. - case ADDED = 0x00000001 + public static let ADDED = FileNotifyAction(0x00000001) /// The file was removed from the directory. - case REMOVED = 0x00000002 + public static let REMOVED = FileNotifyAction(0x00000002) /// The file was modified. This can be a change to the data or attributes of the file. - case MODIFIED = 0x00000003 + public static let MODIFIED = FileNotifyAction(0x00000003) /// The file was renamed, and this is the old name. If the new name resides outside of the directory being monitored, the client will not receive the FILE_ACTION_RENAMED_NEW_NAME bit value. - case RENAMED_OLD_NAME = 0x00000004 + public static let RENAMED_OLD_NAME = FileNotifyAction(0x00000004) /// The file was renamed, and this is the new name. If the old name resides outside of the directory being monitored, the client will not receive the FILE_ACTION_RENAME_OLD_NAME bit value. - case RENAMED_NEW_NAME = 0x00000005 + public static let RENAMED_NEW_NAME = FileNotifyAction(0x00000005) /// The file was added to a named stream. - case ADDED_STREAM = 0x00000006 + public static let ADDED_STREAM = FileNotifyAction(0x00000006) /// The file was removed from the named stream. - case REMOVED_STREAM = 0x00000007 + public static let REMOVED_STREAM = FileNotifyAction(0x00000007) /// The file was modified. This can be a change to the data or attributes of the file. - case MODIFIED_STREAM = 0x00000008 + public static let MODIFIED_STREAM = FileNotifyAction(0x00000008) /// An object ID was removed because the file the object ID referred to was deleted. This notification is only sent when the directory being monitored is the special directory "\$Extend\$ObjId:$O:$INDEX_ALLOCATION". - case REMOVED_BY_DELETE = 0x00000009 + public static let REMOVED_BY_DELETE = FileNotifyAction(0x00000009) /// An attempt to tunnel object ID information to a file being created or renamed failed because the object ID is in use by another file on the same volume. This notification is only sent when the directory being monitored is the special directory "\$Extend\$ObjId:$O:$INDEX_ALLOCATION". - case NOT_TUNNELLED = 0x0000000A + public static let NOT_TUNNELLED = FileNotifyAction(0x0000000A) /// An attempt to tunnel object ID information to a file being renamed failed because the file already has an object ID. This notification is only sent when the directory being monitored is the special directory "\$Extend\$ObjId:$O:$INDEX_ALLOCATION". - case TUNNELLED_ID_COLLISION = 0x0000000B + public static let TUNNELLED_ID_COLLISION = FileNotifyAction(0x0000000B) } } diff --git a/Sources/SMBTypes/SMB2Query.swift b/Sources/SMBTypes/SMB2Query.swift index 3a4a3b3..1ca1e20 100644 --- a/Sources/SMBTypes/SMB2Query.swift +++ b/Sources/SMBTypes/SMB2Query.swift @@ -12,6 +12,8 @@ extension SMB2 { // MARK: SMB2 Query Directory struct QueryDirectoryRequest: SMBRequestBody { + static let command: SMB2.Command = .QUERY_DIRECTORY + let header: QueryDirectoryRequest.Header let searchPattern: String? @@ -70,17 +72,23 @@ extension SMB2 { let header: SMB2FilesInformationHeader switch type { case .fileDirectoryInformation: - header = buffer.scanValue(start: offset) as FileDirectoryInformationHeader! + let tHeader: FileDirectoryInformationHeader = buffer.scanValue(start: offset)! + header = tHeader case .fileFullDirectoryInformation: - header = buffer.scanValue(start: offset) as FileFullDirectoryInformationHeader! + let tHeader: FileFullDirectoryInformationHeader = buffer.scanValue(start: offset)! + header = tHeader case .fileIdFullDirectoryInformation: - header = buffer.scanValue(start: offset) as FileIdFullDirectoryInformationHeader! + let tHeader: FileIdFullDirectoryInformationHeader = buffer.scanValue(start: offset)! + header = tHeader case .fileBothDirectoryInformation: - header = buffer.scanValue(start: offset) as FileBothDirectoryInformationHeader! + let tHeader: FileBothDirectoryInformationHeader = buffer.scanValue(start: offset)! + header = tHeader case .fileIdBothDirectoryInformation: - header = buffer.scanValue(start: offset) as FileIdBothDirectoryInformationHeader! + let tHeader: FileIdBothDirectoryInformationHeader = buffer.scanValue(start: offset)! + header = tHeader case .fileNamesInformation: - header = buffer.scanValue(start: offset) as FileNamesInformationHeader! + let tHeader: FileNamesInformationHeader = buffer.scanValue(start: offset)! + header = tHeader default: return [] } @@ -96,8 +104,10 @@ extension SMB2 { } init? (data: Data) { - let offset = Int(data.scanValue(start: 2) as UInt16!) - let length = Int(data.scanValue(start: 4) as UInt32!) + let tOffset: UInt16 = data.scanValue(start: 2)! + let offset = Int(tOffset) + let tLength: UInt32 = data.scanValue(start: 4)! + let length = Int(tLength) guard data.count > offset + length else { return nil } @@ -108,6 +118,8 @@ extension SMB2 { // MARK: SMB2 Query Info struct QueryInfoRequest: SMBRequestBody { + static var command: SMB2.Command = .QUERY_INFO + let header: Header let buffer: Data? @@ -196,7 +208,8 @@ extension SMB2 { /*let offsetData = data.subdataWithRange(NSRange(location: 2, length: 2)) let offset: UInt16 = decode(offsetData)*/ - let length = Int(data.scanValue(start: 4) as UInt32!) + let tLength: UInt32 = data.scanValue(start: 4)! + let length = Int(tLength) guard data.count >= 8 + length else { return nil diff --git a/Sources/SMBTypes/SMB2QueryTypes.swift b/Sources/SMBTypes/SMB2QueryTypes.swift index 4202a5e..a7c8391 100644 --- a/Sources/SMBTypes/SMB2QueryTypes.swift +++ b/Sources/SMBTypes/SMB2QueryTypes.swift @@ -15,88 +15,100 @@ protocol SMB2FilesInformationHeader: SMBResponseBody { } extension SMB2 { - enum FileInformationEnum: UInt8 { - case none = 0x00 - case fileDirectoryInformation = 0x01 - case fileFullDirectoryInformation = 0x02 - case fileBothDirectoryInformation = 0x03 - case fileBasicInformation = 0x04 - case fileStandardInformation = 0x05 - case fileInternalInformation = 0x06 - case fileEaInformation = 0x07 - case fileAccessInformation = 0x08 - case fileNameInformation = 0x09 - case fileRenameInformation = 0x0A - case fileLinkInformation = 0x0B - case fileNamesInformation = 0x0C - case fileDispositionInformation = 0x0D - case filePositionInformation = 0x0E - case fileFullEaInformation = 0x0F - case fileModeInformation = 0x10 - case fileAlignmentInformation = 0x11 - case fileAllInformation = 0x12 - case fileAllocationInformation = 0x13 - case fileEndOfFileInformation = 0x14 - case fileAlternateNameInformation = 0x15 - case fileStreamInformation = 0x16 - case filePipeInformation = 0x17 - case filePipeLocalInformation = 0x18 - case filePipeRemoteInformation = 0x19 - case fileMailslotQueryInformation = 0x1A - case fileMailslotSetInformation = 0x1B - case fileCompressionInformation = 0x1C - case fileObjectIdInformation = 0x1D - case fileCompletionInformation = 0x1E - case fileMoveClusterInformation = 0x1F - case fileQuotaInformation = 0x20 - case fileReparsePointInformation = 0x21 - case fileNetworkOpenInformation = 0x22 - case fileAttributeTagInformation = 0x23 - case fileTrackingInformation = 0x24 - case fileIdBothDirectoryInformation = 0x25 - case fileIdFullDirectoryInformation = 0x26 - case fileValidDataLengthInformation = 0x27 - case fileShortNameInformation = 0x28 - case fileIoCompletionNotificationInformation = 0x29 - case fileIoStatusBlockRangeInformation = 0x2A - case fileIoPriorityHintInformation = 0x2B - case fileSfioReserveInformation = 0x2C - case fileSfioVolumeInformation = 0x2D - case fileHardLinkInformation = 0x2E - case fileProcessIdsUsingFileInformation = 0x2F - case fileNormalizedNameInformation = 0x30 - case fileNetworkPhysicalNameInformation = 0x31 - case fileIdGlobalTxDirectoryInformation = 0x32 - case fileIsRemoteDeviceInformation = 0x33 - case fileUnusedInformation = 0x34 - case fileNumaNodeInformation = 0x35 - case fileStandardLinkInformation = 0x36 - case fileRemoteProtocolInformation = 0x37 - case fileRenameInformationBypassAccessCheck = 0x38 - case fileLinkInformationBypassAccessCheck = 0x39 - case fileVolumeNameInformation = 0x3A - case fileIdInformation = 0x3B - case fileIdExtdDirectoryInformation = 0x3C - case fileReplaceCompletionInformation = 0x3D - case fileHardLinkFullIdInformation = 0x3E - case fileIdExtdBothDirectoryInformation = 0x3F - case fileMaximumInformation = 0x40 + struct FileInformationEnum: Option { + let rawValue: UInt8 + + init(rawValue: UInt8) { + self.rawValue = rawValue + } + + public static let none = 0x00 + public static let fileDirectoryInformation = FileInformationEnum(rawValue: 0x01) + public static let fileFullDirectoryInformation = FileInformationEnum(rawValue: 0x02) + public static let fileBothDirectoryInformation = FileInformationEnum(rawValue: 0x03) + public static let fileBasicInformation = FileInformationEnum(rawValue: 0x04) + public static let fileStandardInformation = FileInformationEnum(rawValue: 0x05) + public static let fileInternalInformation = FileInformationEnum(rawValue: 0x06) + public static let fileEaInformation = FileInformationEnum(rawValue: 0x07) + public static let fileAccessInformation = FileInformationEnum(rawValue: 0x08) + public static let fileNameInformation = FileInformationEnum(rawValue: 0x09) + public static let fileRenameInformation = FileInformationEnum(rawValue: 0x0A) + public static let fileLinkInformation = FileInformationEnum(rawValue: 0x0B) + public static let fileNamesInformation = FileInformationEnum(rawValue: 0x0C) + public static let fileDispositionInformation = FileInformationEnum(rawValue: 0x0D) + public static let filePositionInformation = FileInformationEnum(rawValue: 0x0E) + public static let fileFullEaInformation = FileInformationEnum(rawValue: 0x0F) + public static let fileModeInformation = FileInformationEnum(rawValue: 0x10) + public static let fileAlignmentInformation = FileInformationEnum(rawValue: 0x11) + public static let fileAllInformation = FileInformationEnum(rawValue: 0x12) + public static let fileAllocationInformation = FileInformationEnum(rawValue: 0x13) + public static let fileEndOfFileInformation = FileInformationEnum(rawValue: 0x14) + public static let fileAlternateNameInformation = FileInformationEnum(rawValue: 0x15) + public static let fileStreamInformation = FileInformationEnum(rawValue: 0x16) + public static let filePipeInformation = FileInformationEnum(rawValue: 0x17) + public static let filePipeLocalInformation = FileInformationEnum(rawValue: 0x18) + public static let filePipeRemoteInformation = FileInformationEnum(rawValue: 0x19) + public static let fileMailslotQueryInformation = FileInformationEnum(rawValue: 0x1A) + public static let fileMailslotSetInformation = FileInformationEnum(rawValue: 0x1B) + public static let fileCompressionInformation = FileInformationEnum(rawValue: 0x1C) + public static let fileObjectIdInformation = FileInformationEnum(rawValue: 0x1D) + public static let fileCompletionInformation = FileInformationEnum(rawValue: 0x1E) + public static let fileMoveClusterInformation = FileInformationEnum(rawValue: 0x1F) + public static let fileQuotaInformation = FileInformationEnum(rawValue: 0x20) + public static let fileReparsePointInformation = FileInformationEnum(rawValue: 0x21) + public static let fileNetworkOpenInformation = FileInformationEnum(rawValue: 0x22) + public static let fileAttributeTagInformation = FileInformationEnum(rawValue: 0x23) + public static let fileTrackingInformation = FileInformationEnum(rawValue: 0x24) + public static let fileIdBothDirectoryInformation = FileInformationEnum(rawValue: 0x25) + public static let fileIdFullDirectoryInformation = FileInformationEnum(rawValue: 0x26) + public static let fileValidDataLengthInformation = FileInformationEnum(rawValue: 0x27) + public static let fileShortNameInformation = FileInformationEnum(rawValue: 0x28) + public static let fileIoCompletionNotificationInformation = FileInformationEnum(rawValue: 0x29) + public static let fileIoStatusBlockRangeInformation = FileInformationEnum(rawValue: 0x2A) + public static let fileIoPriorityHintInformation = FileInformationEnum(rawValue: 0x2B) + public static let fileSfioReserveInformation = FileInformationEnum(rawValue: 0x2C) + public static let fileSfioVolumeInformation = FileInformationEnum(rawValue: 0x2D) + public static let fileHardLinkInformation = FileInformationEnum(rawValue: 0x2E) + public static let fileProcessIdsUsingFileInformation = FileInformationEnum(rawValue: 0x2F) + public static let fileNormalizedNameInformation = FileInformationEnum(rawValue: 0x30) + public static let fileNetworkPhysicalNameInformation = FileInformationEnum(rawValue: 0x31) + public static let fileIdGlobalTxDirectoryInformation = FileInformationEnum(rawValue: 0x32) + public static let fileIsRemoteDeviceInformation = FileInformationEnum(rawValue: 0x33) + public static let fileUnusedInformation = FileInformationEnum(rawValue: 0x34) + public static let fileNumaNodeInformation = FileInformationEnum(rawValue: 0x35) + public static let fileStandardLinkInformation = FileInformationEnum(rawValue: 0x36) + public static let fileRemoteProtocolInformation = FileInformationEnum(rawValue: 0x37) + public static let fileRenameInformationBypassAccessCheck = FileInformationEnum(rawValue: 0x38) + public static let fileLinkInformationBypassAccessCheck = FileInformationEnum(rawValue: 0x39) + public static let fileVolumeNameInformation = FileInformationEnum(rawValue: 0x3A) + public static let fileIdInformation = FileInformationEnum(rawValue: 0x3B) + public static let fileIdExtdDirectoryInformation = FileInformationEnum(rawValue: 0x3C) + public static let fileReplaceCompletionInformation = FileInformationEnum(rawValue: 0x3D) + public static let fileHardLinkFullIdInformation = FileInformationEnum(rawValue: 0x3E) + public static let fileIdExtdBothDirectoryInformation = FileInformationEnum(rawValue: 0x3F) + public static let fileMaximumInformation = FileInformationEnum(rawValue: 0x40) static let queryDirectory: [FileInformationEnum] = [.fileDirectoryInformation, .fileFullDirectoryInformation, .fileIdFullDirectoryInformation, .fileBothDirectoryInformation, .fileIdBothDirectoryInformation, .fileNamesInformation] static let queryInfoFile: [FileInformationEnum] = [.fileAccessInformation, .fileAlignmentInformation, .fileAllInformation, .fileAlternateNameInformation, .fileAttributeTagInformation, .fileBasicInformation, .fileCompressionInformation, fileEaInformation, .fileFullEaInformation, .fileInternalInformation, .fileModeInformation, .fileNetworkOpenInformation, .filePipeInformation, .filePipeLocalInformation, .filePipeRemoteInformation, .filePositionInformation, .fileStandardInformation, .fileStreamInformation] } - enum FileSystemInformationEnum: UInt8 { - case none = 0 - case fileFsAttributeInformation - case fileFsControlInformation - case fileFsDeviceInformation - case fileFsFullSizeInformation - case fileFsObjectIdInformation - case fileFsSectorSizeInformation - case fileFsSizeInformation - case fileFsVolumeInformation + struct FileSystemInformationEnum: Option { + let rawValue: UInt8 + + init(rawValue: UInt8) { + self.rawValue = rawValue + } + + public static let none = FileSystemInformationEnum(rawValue: 0x00) + public static let fileFsAttributeInformation = FileSystemInformationEnum(rawValue: 0x01) + public static let fileFsControlInformation = FileSystemInformationEnum(rawValue: 0x02) + public static let fileFsDeviceInformation = FileSystemInformationEnum(rawValue: 0x03) + public static let fileFsFullSizeInformation = FileSystemInformationEnum(rawValue: 0x04) + public static let fileFsObjectIdInformation = FileSystemInformationEnum(rawValue: 0x05) + public static let fileFsSectorSizeInformation = FileSystemInformationEnum(rawValue: 0x06) + public static let fileFsSizeInformation = FileSystemInformationEnum(rawValue: 0x07) + public static let fileFsVolumeInformation = FileSystemInformationEnum(rawValue: 0x08) } struct FileSecurityInfo: OptionSet { @@ -303,71 +315,89 @@ extension SMB2 { } struct FilePipeInformation { - fileprivate let _readMode: UInt32 - var readMode: ReadMode { - return ReadMode(rawValue: _readMode) ?? .BYTE_STREAM_MODE - } - fileprivate let _completionMode: UInt32 - var completionMode: CompletionMode { - return CompletionMode(rawValue: _completionMode) ?? .QUEUE_OPERATION - } + let readMode: ReadMode + fileprivate let completionMode: CompletionMode - enum ReadMode: UInt32 { - case BYTE_STREAM_MODE = 0x00000000 - case MESSAGE_MODE = 0x00000001 + struct ReadMode: Option { + let rawValue: UInt32 + + init(rawValue: UInt32) { + self.rawValue = rawValue + } + + public static let BYTE_STREAM_MODE = ReadMode(rawValue: 0x00000000) + public static let MESSAGE_MODE = ReadMode(rawValue: 0x00000001) } - enum CompletionMode: UInt32 { - case QUEUE_OPERATION = 0x00000000 - case COMPLETE_OPERATION = 0x00000001 + struct CompletionMode: Option { + let rawValue: UInt32 + + init(rawValue: UInt32) { + self.rawValue = rawValue + } + + public static let QUEUE_OPERATION = CompletionMode(rawValue: 0x00000000) + public static let COMPLETE_OPERATION = CompletionMode(rawValue: 0x00000001) } } struct FilePipeLocalInformation { - fileprivate let _namedPipeType: UInt32 - var namedPipeType: Type { - return Type(rawValue: _namedPipeType) ?? .BYTE_STREAM_TYPE - } - fileprivate let _namedPipeConfiguration: UInt32 - var namedPipeConfiguration: Configuration { - return Configuration(rawValue: _namedPipeConfiguration) ?? .INBOUND - } + let namedPipeType: Type + let namedPipeConfiguration: Configuration let maximumInstances: UInt32 let currentInstances: UInt32 let inboundQuota: UInt32 let readDataAvailable: UInt32 let outboundQuota: UInt32 let writeQuotaAvailable: UInt32 - fileprivate let _namedPipeState: UInt32 - var namedPipeState: State { - return State(rawValue: _namedPipeState) ?? .DISCONNECTED_STATE - } - fileprivate let _namedPipeEnd: UInt32 - var namedPipeEnd: End { - return End(rawValue: _namedPipeEnd) ?? .CLIENT_END - } + let namedPipeState: State + let namedPipeEnd: End - enum `Type`: UInt32 { - case BYTE_STREAM_TYPE = 0x00000000 - case MESSAGE_TYPE = 0x00000001 + struct `Type`: Option { + let rawValue: UInt32 + + init(rawValue: UInt32) { + self.rawValue = rawValue + } + + public static let BYTE_STREAM_TYPE = `Type`(rawValue: 0x00000000) + public static let MESSAGE_TYPE = `Type`(rawValue: 0x00000001) } - enum Configuration: UInt32 { - case INBOUND = 0x00000000 - case OUTBOUND = 0x00000001 - case FULL_DUPLEX = 0x00000002 + struct Configuration: Option { + let rawValue: UInt32 + + init(rawValue: UInt32) { + self.rawValue = rawValue + } + + public static let INBOUND = Configuration(rawValue: 0x00000000) + public static let OUTBOUND = Configuration(rawValue: 0x00000001) + public static let FULL_DUPLEX = Configuration(rawValue: 0x00000002) } - enum State: UInt32 { - case DISCONNECTED_STATE = 0x00000001 - case LISTENING_STATE = 0x00000002 - case CONNECTED_STATE = 0x00000003 - case CLOSING_STATE = 0x00000004 + struct State: Option { + let rawValue: UInt32 + + init(rawValue: UInt32) { + self.rawValue = rawValue + } + + public static let DISCONNECTED_STATE = State(rawValue: 0x00000001) + public static let LISTENING_STATE = State(rawValue: 0x00000002) + public static let CONNECTED_STATE = State(rawValue: 0x00000003) + public static let CLOSING_STATE = State(rawValue: 0x00000004) } - enum End: UInt32 { - case CLIENT_END = 0x00000000 - case SERVER_END = 0x00000001 + struct End: Option { + let rawValue: UInt32 + + init(rawValue: UInt32) { + self.rawValue = rawValue + } + + public static let CLIENT_END = End(rawValue: 0x00000000) + public static let SERVER_END = End(rawValue: 0x00000001) } } @@ -412,15 +442,18 @@ extension SMB2 { } struct FileFsDeviceInformation { - fileprivate let _deviceType: UInt32 - var deviceType: DeviceType { - return DeviceType(rawValue: _deviceType) ?? .DISK - } + let deviceType: DeviceType let charactristics: Charactristics - enum DeviceType: UInt32 { - case CD_ROM = 0x00000002 - case DISK = 0x00000007 + struct DeviceType: Option { + let rawValue: UInt32 + + init(rawValue: UInt32) { + self.rawValue = rawValue + } + + public static let CD_ROM = DeviceType(rawValue: 0x00000002) + public static let DISK = DeviceType(rawValue: 0x00000007) } struct Charactristics: OptionSet { @@ -471,7 +504,7 @@ extension SMB2 { /// The file system supports case-sensitive file names when looking up (searching for) file names in a directory. static let CASE_SENSITIVE_SEARCH = Attributes(rawValue: 0x00000001) - /// The file system preserves the case of file names when it places a name on disk. + /// The file system preserves the public static let of file names when it places a name on disk. static let CASE_PRESERVED_NAMES = Attributes(rawValue: 0x00000002) /// The file system supports Unicode in file and directory names. This flag applies only to file and directory names; the file system neither restricts nor interprets the bytes of data within a file. static let UNICODE_ON_DISK = Attributes(rawValue: 0x00000004) diff --git a/Sources/SMBTypes/SMB2Session.swift b/Sources/SMBTypes/SMB2Session.swift index 7dded51..d1e3e45 100644 --- a/Sources/SMBTypes/SMB2Session.swift +++ b/Sources/SMBTypes/SMB2Session.swift @@ -12,6 +12,8 @@ extension SMB2 { // MARK: SMB2 Negotiating struct NegotiateRequest: SMBRequestBody { + static var command: SMB2.Command = .NEGOTIATE + let header: NegotiateRequest.Header let dialects: [UInt16] let contexts: [(type: NegotiateContextType, data: Data)] @@ -178,6 +180,8 @@ extension SMB2 { // MARK: SMB2 Session Setup struct SessionSetupRequest: SMBRequestBody { + static var command: SMB2.Command = .SESSION_SETUP + let header: SessionSetupRequest.Header let buffer: Data? @@ -291,6 +295,8 @@ extension SMB2 { // MARK: SMB2 Log off struct LogOff: SMBRequestBody, SMBResponseBody { + static var command: SMB2.Command = .LOGOFF + let size: UInt16 let reserved: UInt16 @@ -303,6 +309,8 @@ extension SMB2 { // MARK: SMB2 Echo struct Echo: SMBRequestBody, SMBResponseBody { + static var command: SMB2.Command = .ECHO + let size: UInt16 let reserved: UInt16 diff --git a/Sources/SMBTypes/SMB2SetInfo.swift b/Sources/SMBTypes/SMB2SetInfo.swift index 5f4c434..6660a79 100644 --- a/Sources/SMBTypes/SMB2SetInfo.swift +++ b/Sources/SMBTypes/SMB2SetInfo.swift @@ -11,6 +11,8 @@ import Foundation extension SMB2 { // MARK: SMB2 Set Info struct SetInfoRequest: SMBRequestBody { + static var command: SMB2.Command = .SET_INFO + let header: Header let buffer: Data? diff --git a/Sources/SMBTypes/SMB2Tree.swift b/Sources/SMBTypes/SMB2Tree.swift index 2fe0e2a..41ae879 100644 --- a/Sources/SMBTypes/SMB2Tree.swift +++ b/Sources/SMBTypes/SMB2Tree.swift @@ -12,13 +12,15 @@ extension SMB2 { // MARK: SMB2 Tree Connect struct TreeConnectRequest: SMBRequestBody { + static var command: SMB2.Command = .TREE_CONNECT + let header: TreeConnectRequest.Header let buffer: Data? var path: String { - return "" + return buffer.flatMap({ String.init(data: $0, encoding: .utf16) }) ?? "" } var share: String { - return "" + return path.split(separator: "/", omittingEmptySubsequences: true).first.map(String.init) ?? "" } init? (header: TreeConnectRequest.Header, host: String, share: String) { @@ -125,6 +127,8 @@ extension SMB2 { // MARK: SMB2 Tree Disconnect struct TreeDisconnect: SMBRequestBody, SMBResponseBody { + static var command: SMB2.Command = .TREE_DISCONNECT + let size: UInt16 let reserved: UInt16 diff --git a/Sources/SMBTypes/SMB2Types.swift b/Sources/SMBTypes/SMB2Types.swift index d6f94c7..f3425df 100644 --- a/Sources/SMBTypes/SMB2Types.swift +++ b/Sources/SMBTypes/SMB2Types.swift @@ -22,20 +22,17 @@ struct SMB2 { let size: UInt16 let creditCharge: UInt16 // error messages from the server to the client - let status: UInt32 + let status: NTStatus enum StatusSeverity: UInt8 { case success = 0, information, warning, error } var statusDetails: (severity: StatusSeverity, customer: Bool, facility: UInt16, code: UInt16) { - let severity = StatusSeverity(rawValue: UInt8(status >> 30))! - return (severity, status & 0x20000000 != 0, UInt16((status & 0x0FFF0000) >> 16), UInt16(status & 0x0000FFFF)) - } - fileprivate let _command: UInt16 - var command: Command { - get { - return Command(rawValue: _command) ?? .INVALID - } + let severity = StatusSeverity(rawValue: UInt8(status.rawValue >> 30))! + return (severity, status.rawValue & 0x20000000 != 0, + UInt16((status.rawValue & 0x0FFF0000) >> 16), + UInt16(status.rawValue & 0x0000FFFF)) } + let command: Command let creditRequestResponse: UInt16 let flags: Flags var nextCommand: UInt32 @@ -54,8 +51,8 @@ struct SMB2 { init(command: Command, status: NTStatus = .SUCCESS, creditCharge: UInt16 = 0, creditRequestResponse: UInt16, flags: Flags = [], nextCommand: UInt32 = 0, messageId: UInt64, treeId: UInt32, sessionId: UInt64, signature: (UInt64, UInt64) = (0, 0)) { self.protocolID = type(of: self).protocolConst self.size = 64 - self.status = status.rawValue - self._command = command.rawValue + self.status = status + self.command = command self.creditCharge = creditCharge self.creditRequestResponse = creditRequestResponse self.flags = flags @@ -70,8 +67,8 @@ struct SMB2 { init(asyncCommand: Command, status: NTStatus = .SUCCESS, creditCharge: UInt16 = 0, creditRequestResponse: UInt16, flags: Flags = [.ASYNC_COMMAND], nextCommand: UInt32 = 0, messageId: UInt64, asyncId: UInt64, sessionId: UInt64, signature: (UInt64, UInt64) = (0, 0)) { self.protocolID = type(of: self).protocolConst self.size = 64 - self.status = status.rawValue - self._command = asyncCommand.rawValue + self.status = status + self.command = asyncCommand self.creditCharge = creditCharge self.creditRequestResponse = creditRequestResponse self.flags = flags.union([Flags.ASYNC_COMMAND]) @@ -110,27 +107,33 @@ struct SMB2 { static let REPLAY_OPERATION = Flags(rawValue: 0x20000000) } - enum Command: UInt16 { - case NEGOTIATE = 0x0000 - case SESSION_SETUP = 0x0001 - case LOGOFF = 0x0002 - case TREE_CONNECT = 0x0003 - case TREE_DISCONNECT = 0x0004 - case CREATE = 0x0005 - case CLOSE = 0x0006 - case FLUSH = 0x0007 - case READ = 0x0008 - case WRITE = 0x0009 - case LOCK = 0x000A - case IOCTL = 0x000B - case CANCEL = 0x000C - case ECHO = 0x000D - case QUERY_DIRECTORY = 0x000E - case CHANGE_NOTIFY = 0x000F - case QUERY_INFO = 0x0010 - case SET_INFO = 0x0011 - case OPLOCK_BREAK = 0x0012 - case INVALID = 0xFFFF + struct Command: Option { + init(rawValue: UInt16) { + self.rawValue = rawValue + } + + let rawValue: UInt16 + + public static let NEGOTIATE = Command(rawValue: 0x0000) + public static let SESSION_SETUP = Command(rawValue: 0x0001) + public static let LOGOFF = Command(rawValue: 0x0002) + public static let TREE_CONNECT = Command(rawValue: 0x0003) + public static let TREE_DISCONNECT = Command(rawValue: 0x0004) + public static let CREATE = Command(rawValue: 0x0005) + public static let CLOSE = Command(rawValue: 0x0006) + public static let FLUSH = Command(rawValue: 0x0007) + public static let READ = Command(rawValue: 0x0008) + public static let WRITE = Command(rawValue: 0x0009) + public static let LOCK = Command(rawValue: 0x000A) + public static let IOCTL = Command(rawValue: 0x000B) + public static let CANCEL = Command(rawValue: 0x000C) + public static let ECHO = Command(rawValue: 0x000D) + public static let QUERY_DIRECTORY = Command(rawValue: 0x000E) + public static let CHANGE_NOTIFY = Command(rawValue: 0x000F) + public static let QUERY_INFO = Command(rawValue: 0x0010) + public static let SET_INFO = Command(rawValue: 0x0011) + public static let OPLOCK_BREAK = Command(rawValue: 0x0012) + public static let INVALID = Command(rawValue: 0xFFFF) } // MARK: SMB2 Oplock Break diff --git a/Sources/SMBTypes/SMBErrorType.swift b/Sources/SMBTypes/SMBErrorType.swift index a5fe645..1411baa 100644 --- a/Sources/SMBTypes/SMBErrorType.swift +++ b/Sources/SMBTypes/SMBErrorType.swift @@ -10,126 +10,132 @@ import Foundation /// Error Types and Description -enum NTStatus: UInt32, Error, CustomStringConvertible { - case SUCCESS = 0x00000000 - case NOT_IMPLEMENTED = 0xC0000002 - case INVALID_DEVICE_REQUEST = 0xC0000010 - case ILLEGAL_FUNCTION = 0xC00000AF - case NO_SUCH_FILE = 0xC000000F - case NO_SUCH_DEVICE = 0xC000000E - case OBJECT_NAME_NOT_FOUND = 0xC0000034 - case OBJECT_PATH_INVALID = 0xC0000039 - case OBJECT_PATH_NOT_FOUND = 0xC000003A - case OBJECT_PATH_SYNTAX_BAD = 0xC000003B - case DFS_EXIT_PATH_FOUND = 0xC000009B - case REDIRECTOR_NOT_STARTED = 0xC00000FB - case TOO_MANY_OPENED_FILES = 0xC000011F - case ACCESS_DENIED = 0xC0000022 - case INVALID_LOCK_SEQUENCE = 0xC000001E - case INVALID_VIEW_SIZE = 0xC000001F - case ALREADY_COMMITTED = 0xC0000021 - case PORT_CONNECTION_REFUSED = 0xC0000041 - case THREAD_IS_TERMINATING = 0xC000004B - case DELETE_PENDING = 0xC0000056 - case PRIVILEGE_NOT_HELD = 0xC0000061 - case LOGON_FAILURE = 0xC000006D - case FILE_IS_A_DIRECTORY = 0xC00000BA - case FILE_RENAMED = 0xC00000D5 - case PROCESS_IS_TERMINATING = 0xC000010A - case DIRECTORY_NOT_EMPTY = 0xC0000101 - case CANNOT_DELETE = 0xC0000121 - case FILE_NOT_AVAILABLE = 0xC0000467 - case FILE_DELETED = 0xC0000123 - case SMB_BAD_FID = 0x00060001 - case INVALID_HANDLE = 0xC0000008 - case OBJECT_TYPE_MISMATCH = 0xC0000024 - case PORT_DISCONNECTED = 0xC0000037 - case INVALID_PORT_HANDLE = 0xC0000042 - case FILE_CLOSED = 0xC0000128 - case HANDLE_NOT_CLOSABLE = 0xC0000235 - case SECTION_TOO_BIG = 0xC0000040 - case TOO_MANY_PAGING_FILES = 0xC0000097 - case INSUFF_SERVER_RESOURCES = 0xC0000205 - case OS2_INVALID_ACCESS = 0x000C0001 - case ACCESS_DENIED_2 = 0xC00000CA - case DATA_ERROR = 0xC000009C - case NOT_SAME_DEVICE = 0xC00000D4 - case NO_MORE_FILES = 0x80000006 - case NO_MORE_ENTRIES = 0x8000001A - case UNSUCCESSFUL = 0xC0000001 - case SHARING_VIOLATION = 0xC0000043 - case FILE_LOCK_CONFLICT = 0xC0000054 - case LOCK_NOT_GRANTED = 0xC0000055 - case END_OF_FILE = 0xC0000011 - case NOT_SUPPORTED = 0xC00000BB - case OBJECT_NAME_COLLISION = 0xC0000035 - case INVALID_PARAMETER = 0xC000000D - case OS2_INVALID_LEVEL = 0x007C0001 - case OS2_NEGATIVE_SEEK = 0x00830001 - case RANGE_NOT_LOCKED = 0xC000007E - case OS2_NO_MORE_SIDS = 0x00710001 - case OS2_CANCEL_VIOLATION = 0x00AD0001 - case OS2_ATOMIC_LOCKS_NOT_SUPPORTED = 0x00AE0001 - case INVALID_INFO_CLASS = 0xC0000003 - case INVALID_PIPE_STATE = 0xC00000AD - case INVALID_READ_MODE = 0xC00000B4 - case OS2_CANNOT_COPY = 0x010A0001 - case STOPPED_ON_SYMLINK = 0x8000002D - case INSTANCE_NOT_AVAILABLE = 0xC00000AB - case PIPE_NOT_AVAILABLE = 0xC00000AC - case PIPE_BUSY = 0xC00000AE - case PIPE_CLOSING = 0xC00000B1 - case PIPE_EMPTY = 0xC00000D9 - case PIPE_DISCONNECTED = 0xC00000B0 - case BUFFER_OVERFLOW = 0x80000005 - case MORE_PROCESSING_REQUIRED = 0xC0000016 - case EA_TOO_LARGE = 0xC0000050 - case OS2_EAS_DIDNT_FIT = 0x01130001 - case EAS_NOT_SUPPORTED = 0xC000004F - case EA_LIST_INCONSISTENT = 0x80000014 - case OS2_EA_ACCESS_DENIED = 0x03E20001 - case NOTIFY_ENUM_DIR = 0x0000010C - case INVALID_SMB = 0x00010002 - case WRONG_PASSWORD = 0xC000006A - case PATH_NOT_COVERED = 0xC0000257 - case NETWORK_NAME_DELETED = 0xC00000C9 - case SMB_BAD_TID = 0x00050002 - case BAD_NETWORK_NAME = 0xC00000CC - case BAD_DEVICE_TYPE = 0xC00000CB - case SMB_BAD_COMMAND = 0x00160002 - case PRINT_QUEUE_FULL = 0xC00000C6 - case NO_SPOOL_SPACE = 0xC00000C7 - case PRINT_CANCELLED = 0xC00000C8 - case UNEXPECTED_NETWORK_ERROR = 0xC00000C4 - case IO_TIMEOUT = 0xC00000B5 - case REQUEST_NOT_ACCEPTED = 0xC00000D0 - case TOO_MANY_SESSIONS = 0xC00000CE - case SMB_BAD_UID = 0x005B0002 - case SMB_USE_MPX = 0x00FA0002 - case SMB_USE_STANDARD = 0x00FB0002 - case SMB_CONTINUE_MPX = 0x00FC0002 - case ACCOUNT_DISABLED = 0xC0000072 - case ACCOUNT_EXPIRED = 0xC0000193 - case INVALID_WORKSTATION = 0xC0000070 - case INVALID_LOGON_HOURS = 0xC000006F - case PASSWORD_EXPIRED = 0xC0000071 - case PASSWORD_MUST_CHANGE = 0xC0000224 - case SMB_NO_SUPPORT = 0xFFFF0002 - case MEDIA_WRITE_PROTECTED = 0xC00000A2 - case NO_MEDIA_IN_DEVICE = 0xC0000013 - case INVALID_DEVICE_STATE = 0xC0000184 - case DATA_ERROR_2 = 0xC000003E - case CRC_ERROR = 0xC000003F - case DISK_CORRUPT_ERROR = 0xC0000032 - case NONEXISTENT_SECTOR = 0xC0000015 - case DEVICE_PAPER_EMPTY = 0x8000000E - case WRONG_VOLUME = 0xC0000012 - case DISK_FULL = 0xC000007F - case BUFFER_TOO_SMALL = 0xC0000023 - case BAD_IMPERSONATION_LEVEL = 0xC00000A5 - case USER_SESSION_DELETED = 0xC0000203 - case NETWORK_SESSION_EXPIRED = 0xC000035C - case SMB_TOO_MANY_UIDS = 0xC000205A +struct NTStatus: Option, Error, CustomStringConvertible { + init(rawValue: UInt32) { + self.rawValue = rawValue + } + + var rawValue: UInt32 + + public static let SUCCESS = NTStatus(rawValue: 0x00000000) + public static let NOT_IMPLEMENTED = NTStatus(rawValue: 0xC0000002) + public static let INVALID_DEVICE_REQUEST = NTStatus(rawValue: 0xC0000010) + public static let ILLEGAL_FUNCTION = NTStatus(rawValue: 0xC00000AF) + public static let NO_SUCH_FILE = NTStatus(rawValue: 0xC000000F) + public static let NO_SUCH_DEVICE = NTStatus(rawValue: 0xC000000E) + public static let OBJECT_NAME_NOT_FOUND = NTStatus(rawValue: 0xC0000034) + public static let OBJECT_PATH_INVALID = NTStatus(rawValue: 0xC0000039) + public static let OBJECT_PATH_NOT_FOUND = NTStatus(rawValue: 0xC000003A) + public static let OBJECT_PATH_SYNTAX_BAD = NTStatus(rawValue: 0xC000003B) + public static let DFS_EXIT_PATH_FOUND = NTStatus(rawValue: 0xC000009B) + public static let REDIRECTOR_NOT_STARTED = NTStatus(rawValue: 0xC00000FB) + public static let TOO_MANY_OPENED_FILES = NTStatus(rawValue: 0xC000011F) + public static let ACCESS_DENIED = NTStatus(rawValue: 0xC0000022) + public static let INVALID_LOCK_SEQUENCE = NTStatus(rawValue: 0xC000001E) + public static let INVALID_VIEW_SIZE = NTStatus(rawValue: 0xC000001F) + public static let ALREADY_COMMITTED = NTStatus(rawValue: 0xC0000021) + public static let PORT_CONNECTION_REFUSED = NTStatus(rawValue: 0xC0000041) + public static let THREAD_IS_TERMINATING = NTStatus(rawValue: 0xC000004B) + public static let DELETE_PENDING = NTStatus(rawValue: 0xC0000056) + public static let PRIVILEGE_NOT_HELD = NTStatus(rawValue: 0xC0000061) + public static let LOGON_FAILURE = NTStatus(rawValue: 0xC000006D) + public static let FILE_IS_A_DIRECTORY = NTStatus(rawValue: 0xC00000BA) + public static let FILE_RENAMED = NTStatus(rawValue: 0xC00000D5) + public static let PROCESS_IS_TERMINATING = NTStatus(rawValue: 0xC000010A) + public static let DIRECTORY_NOT_EMPTY = NTStatus(rawValue: 0xC0000101) + public static let CANNOT_DELETE = NTStatus(rawValue: 0xC0000121) + public static let FILE_NOT_AVAILABLE = NTStatus(rawValue: 0xC0000467) + public static let FILE_DELETED = NTStatus(rawValue: 0xC0000123) + public static let SMB_BAD_FID = NTStatus(rawValue: 0x00060001) + public static let INVALID_HANDLE = NTStatus(rawValue: 0xC0000008) + public static let OBJECT_TYPE_MISMATCH = NTStatus(rawValue: 0xC0000024) + public static let PORT_DISCONNECTED = NTStatus(rawValue: 0xC0000037) + public static let INVALID_PORT_HANDLE = NTStatus(rawValue: 0xC0000042) + public static let FILE_CLOSED = NTStatus(rawValue: 0xC0000128) + public static let HANDLE_NOT_CLOSABLE = NTStatus(rawValue: 0xC0000235) + public static let SECTION_TOO_BIG = NTStatus(rawValue: 0xC0000040) + public static let TOO_MANY_PAGING_FILES = NTStatus(rawValue: 0xC0000097) + public static let INSUFF_SERVER_RESOURCES = NTStatus(rawValue: 0xC0000205) + public static let OS2_INVALID_ACCESS = NTStatus(rawValue: 0x000C0001) + public static let ACCESS_DENIED_2 = NTStatus(rawValue: 0xC00000CA) + public static let DATA_ERROR = NTStatus(rawValue: 0xC000009C) + public static let NOT_SAME_DEVICE = NTStatus(rawValue: 0xC00000D4) + public static let NO_MORE_FILES = NTStatus(rawValue: 0x80000006) + public static let NO_MORE_ENTRIES = NTStatus(rawValue: 0x8000001A) + public static let UNSUCCESSFUL = NTStatus(rawValue: 0xC0000001) + public static let SHARING_VIOLATION = NTStatus(rawValue: 0xC0000043) + public static let FILE_LOCK_CONFLICT = NTStatus(rawValue: 0xC0000054) + public static let LOCK_NOT_GRANTED = NTStatus(rawValue: 0xC0000055) + public static let END_OF_FILE = NTStatus(rawValue: 0xC0000011) + public static let NOT_SUPPORTED = NTStatus(rawValue: 0xC00000BB) + public static let OBJECT_NAME_COLLISION = NTStatus(rawValue: 0xC0000035) + public static let INVALID_PARAMETER = NTStatus(rawValue: 0xC000000D) + public static let OS2_INVALID_LEVEL = NTStatus(rawValue: 0x007C0001) + public static let OS2_NEGATIVE_SEEK = NTStatus(rawValue: 0x00830001) + public static let RANGE_NOT_LOCKED = NTStatus(rawValue: 0xC000007E) + public static let OS2_NO_MORE_SIDS = NTStatus(rawValue: 0x00710001) + public static let OS2_CANCEL_VIOLATION = NTStatus(rawValue: 0x00AD0001) + public static let OS2_ATOMIC_LOCKS_NOT_SUPPORTED = NTStatus(rawValue: 0x00AE0001) + public static let INVALID_INFO_CLASS = NTStatus(rawValue: 0xC0000003) + public static let INVALID_PIPE_STATE = NTStatus(rawValue: 0xC00000AD) + public static let INVALID_READ_MODE = NTStatus(rawValue: 0xC00000B4) + public static let OS2_CANNOT_COPY = NTStatus(rawValue: 0x010A0001) + public static let STOPPED_ON_SYMLINK = NTStatus(rawValue: 0x8000002D) + public static let INSTANCE_NOT_AVAILABLE = NTStatus(rawValue: 0xC00000AB) + public static let PIPE_NOT_AVAILABLE = NTStatus(rawValue: 0xC00000AC) + public static let PIPE_BUSY = NTStatus(rawValue: 0xC00000AE) + public static let PIPE_CLOSING = NTStatus(rawValue: 0xC00000B1) + public static let PIPE_EMPTY = NTStatus(rawValue: 0xC00000D9) + public static let PIPE_DISCONNECTED = NTStatus(rawValue: 0xC00000B0) + public static let BUFFER_OVERFLOW = NTStatus(rawValue: 0x80000005) + public static let MORE_PROCESSING_REQUIRED = NTStatus(rawValue: 0xC0000016) + public static let EA_TOO_LARGE = NTStatus(rawValue: 0xC0000050) + public static let OS2_EAS_DIDNT_FIT = NTStatus(rawValue: 0x01130001) + public static let EAS_NOT_SUPPORTED = NTStatus(rawValue: 0xC000004F) + public static let EA_LIST_INCONSISTENT = NTStatus(rawValue: 0x80000014) + public static let OS2_EA_ACCESS_DENIED = NTStatus(rawValue: 0x03E20001) + public static let NOTIFY_ENUM_DIR = NTStatus(rawValue: 0x0000010C) + public static let INVALID_SMB = NTStatus(rawValue: 0x00010002) + public static let WRONG_PASSWORD = NTStatus(rawValue: 0xC000006A) + public static let PATH_NOT_COVERED = NTStatus(rawValue: 0xC0000257) + public static let NETWORK_NAME_DELETED = NTStatus(rawValue: 0xC00000C9) + public static let SMB_BAD_TID = NTStatus(rawValue: 0x00050002) + public static let BAD_NETWORK_NAME = NTStatus(rawValue: 0xC00000CC) + public static let BAD_DEVICE_TYPE = NTStatus(rawValue: 0xC00000CB) + public static let SMB_BAD_COMMAND = NTStatus(rawValue: 0x00160002) + public static let PRINT_QUEUE_FULL = NTStatus(rawValue: 0xC00000C6) + public static let NO_SPOOL_SPACE = NTStatus(rawValue: 0xC00000C7) + public static let PRINT_CANCELLED = NTStatus(rawValue: 0xC00000C8) + public static let UNEXPECTED_NETWORK_ERROR = NTStatus(rawValue: 0xC00000C4) + public static let IO_TIMEOUT = NTStatus(rawValue: 0xC00000B5) + public static let REQUEST_NOT_ACCEPTED = NTStatus(rawValue: 0xC00000D0) + public static let TOO_MANY_SESSIONS = NTStatus(rawValue: 0xC00000CE) + public static let SMB_BAD_UID = NTStatus(rawValue: 0x005B0002) + public static let SMB_USE_MPX = NTStatus(rawValue: 0x00FA0002) + public static let SMB_USE_STANDARD = NTStatus(rawValue: 0x00FB0002) + public static let SMB_CONTINUE_MPX = NTStatus(rawValue: 0x00FC0002) + public static let ACCOUNT_DISABLED = NTStatus(rawValue: 0xC0000072) + public static let ACCOUNT_EXPIRED = NTStatus(rawValue: 0xC0000193) + public static let INVALID_WORKSTATION = NTStatus(rawValue: 0xC0000070) + public static let INVALID_LOGON_HOURS = NTStatus(rawValue: 0xC000006F) + public static let PASSWORD_EXPIRED = NTStatus(rawValue: 0xC0000071) + public static let PASSWORD_MUST_CHANGE = NTStatus(rawValue: 0xC0000224) + public static let SMB_NO_SUPPORT = NTStatus(rawValue: 0xFFFF0002) + public static let MEDIA_WRITE_PROTECTED = NTStatus(rawValue: 0xC00000A2) + public static let NO_MEDIA_IN_DEVICE = NTStatus(rawValue: 0xC0000013) + public static let INVALID_DEVICE_STATE = NTStatus(rawValue: 0xC0000184) + public static let DATA_ERROR_2 = NTStatus(rawValue: 0xC000003E) + public static let CRC_ERROR = NTStatus(rawValue: 0xC000003F) + public static let DISK_CORRUPT_ERROR = NTStatus(rawValue: 0xC0000032) + public static let NONEXISTENT_SECTOR = NTStatus(rawValue: 0xC0000015) + public static let DEVICE_PAPER_EMPTY = NTStatus(rawValue: 0x8000000E) + public static let WRONG_VOLUME = NTStatus(rawValue: 0xC0000012) + public static let DISK_FULL = NTStatus(rawValue: 0xC000007F) + public static let BUFFER_TOO_SMALL = NTStatus(rawValue: 0xC0000023) + public static let BAD_IMPERSONATION_LEVEL = NTStatus(rawValue: 0xC00000A5) + public static let USER_SESSION_DELETED = NTStatus(rawValue: 0xC0000203) + public static let NETWORK_SESSION_EXPIRED = NTStatus(rawValue: 0xC000035C) + public static let SMB_TOO_MANY_UIDS = NTStatus(rawValue: 0xC000205A) public var description: String { switch self { diff --git a/Sources/ServerTrustPolicy.swift b/Sources/ServerTrustPolicy.swift new file mode 100644 index 0000000..601b6a8 --- /dev/null +++ b/Sources/ServerTrustPolicy.swift @@ -0,0 +1,83 @@ +// +// ServerTrustPolicy.swift +// +// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +// MARK: - ServerTrustPolicy +/// The `ServerTrustPolicy` evaluates the server trust generally provided by an `NSURLAuthenticationChallenge` when +/// connecting to a server over a secure HTTPS connection. The policy configuration then evaluates the server trust +/// with a given set of criteria to determine whether the server trust is valid and the connection should be made. +/// +/// Using pinned certificates or public keys for evaluation helps prevent man-in-the-middle (MITM) attacks and other +/// vulnerabilities. Applications dealing with sensitive customer data or financial information are strongly encouraged +/// to route all communication over an HTTPS connection with pinning enabled. +/// +/// - performDefaultEvaluation: Uses the default server trust evaluation while allowing you to control whether to +/// validate the host provided by the challenge. Applications are encouraged to always +/// validate the host in production environments to guarantee the validity of the server's +/// certificate chain. +/// +/// - disableEvaluation: Disables all evaluation which in turn will always consider any server trust as valid. +/// +public enum ServerTrustPolicy { + case performDefaultEvaluation(validateHost: Bool) + case disableEvaluation + + // MARK: - Evaluation + /// Evaluates whether the server trust is valid. + /// + /// - returns: Whether the server trust is valid. + public func evaluate() -> Bool { + var serverTrustIsValid = false + + switch self { + case .performDefaultEvaluation(_): + break + case .disableEvaluation: + serverTrustIsValid = true + } + + return serverTrustIsValid + } + + /// Evaluates whether the server trust is valid for the given host. + /// + /// - parameter serverTrust: The server trust to evaluate. + /// - parameter host: The host of the challenge protection space. + /// + /// - returns: Whether the server trust is valid. + public func evaluate(_ serverTrust: SecTrust, forHost host: String) -> Bool { + var serverTrustIsValid = false + + switch self { + case let .performDefaultEvaluation(validateHost): + let policy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil) + SecTrustSetPolicies(serverTrust, policy) + case .disableEvaluation: + serverTrustIsValid = true + } + + return serverTrustIsValid + } +} diff --git a/Sources/WebDAVFileProvider.swift b/Sources/WebDAVFileProvider.swift index c52e3d4..85111cb 100644 --- a/Sources/WebDAVFileProvider.swift +++ b/Sources/WebDAVFileProvider.swift @@ -7,7 +7,9 @@ // import Foundation +#if os(macOS) || os(iOS) || os(tvOS) import CoreGraphics +#endif /** Allows accessing to WebDAV server files. This provider doesn't cache or save files internally, however you can @@ -44,11 +46,15 @@ open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing { } public required convenience init?(coder aDecoder: NSCoder) { - guard let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL else { + guard let baseURL = aDecoder.decodeObject(of: NSURL.self, forKey: "baseURL") as URL? else { + if #available(macOS 10.11, iOS 9.0, tvOS 9.0, *) { + aDecoder.failWithError(CocoaError(.coderValueNotFound, + userInfo: [NSLocalizedDescriptionKey: "Base URL is not set."])) + } return nil } self.init(baseURL: baseURL, - credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential) + credential: aDecoder.decodeObject(of: URLCredential.self, forKey: "credential")) self.useCache = aDecoder.decodeBool(forKey: "useCache") self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache") } @@ -62,6 +68,17 @@ open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing { return copy } + /** + Returns an Array of `FileObject`s identifying the the directory entries via asynchronous completion handler. + + If the directory contains no entries or an error is occured, this method will return the empty array. + + - Parameters: + - path: path to target directory. If empty, root will be iterated. + - completionHandler: a closure with result of directory entries or error. + - contents: An array of `FileObject` identifying the the directory entries. + - error: Error returned by system. + */ override open func contentsOfDirectory(path: String, completionHandler: @escaping (([FileObject], Error?) -> Void)) { let query = NSPredicate(format: "TRUEPREDICATE") _ = searchFiles(path: path, recursive: false, query: query, including: [], foundItemHandler: nil, completionHandler: completionHandler) @@ -92,19 +109,19 @@ open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing { If the directory contains no entries or an error is occured, this method will return the empty `FileObject`. - - Parameter path: path to target directory. If empty, attributes of root will be returned. - - Parameter including: An array which determines which file properties should be considered to fetch. - - Parameter completionHandler: a closure with result of directory entries or error. - - Parameter attributes: A `FileObject` containing the attributes of the item. - - Parameter error: Error returned by system. + - Parameters: + - path: path to target directory. If empty, attributes of root will be returned. + - completionHandler: a closure with result of directory entries or error. + - attributes: A `FileObject` containing the attributes of the item. + - error: Error returned by system. */ open func attributesOfItem(path: String, including: [URLResourceKey], completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) { let url = self.url(of: path) var request = URLRequest(url: url) request.httpMethod = "PROPFIND" request.setValue("0", forHTTPHeaderField: "Depth") - request.set(httpAuthentication: credential, with: credentialType) - request.set(httpContentType: .xml, charset: .utf8) + request.setValue(authentication: credential, with: credentialType) + request.setValue(contentType: .xml, charset: .utf8) request.httpBody = WebDavFileObject.xmlProp(including) runDataTask(with: request, completionHandler: { (data, response, error) in var responseError: FileProviderHTTPError? @@ -122,6 +139,8 @@ open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing { }) } + /// Returns volume/provider information asynchronously. + /// - Parameter volumeInfo: Information of filesystem/Provider returned by system/server. override open func storageProperties(completionHandler: @escaping (_ volumeInfo: VolumeObject?) -> Void) { // Not all WebDAV clients implements RFC2518 which allows geting storage quota. // In this case you won't get error. totalSize is NSURLSessionTransferSizeUnknown @@ -132,8 +151,8 @@ open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing { var request = URLRequest(url: baseURL) request.httpMethod = "PROPFIND" request.setValue("0", forHTTPHeaderField: "Depth") - request.set(httpAuthentication: credential, with: credentialType) - request.set(httpContentType: .xml, charset: .utf8) + request.setValue(authentication: credential, with: credentialType) + request.setValue(contentType: .xml, charset: .utf8) request.httpBody = WebDavFileObject.xmlProp([.volumeTotalCapacityKey, .volumeAvailableCapacityKey, .creationDateKey]) runDataTask(with: request, completionHandler: { (data, response, error) in guard let data = data, let attr = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL).first else { @@ -151,6 +170,31 @@ open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing { }) } + /** + Search files inside directory using query asynchronously. + + Sample predicates: + ``` + NSPredicate(format: "(name CONTAINS[c] 'hello') && (fileSize >= 10000)") + NSPredicate(format: "(modifiedDate >= %@)", Date()) + NSPredicate(format: "(path BEGINSWITH %@)", "folder/child folder") + ``` + + - Note: Don't pass Spotlight predicates to this method directly, use `FileProvider.convertSpotlightPredicateTo()` method to get usable predicate. + + - Important: A file name criteria should be provided for Dropbox. + + - Parameters: + - path: location of directory to start search + - recursive: Searching subdirectories of path + - query: An `NSPredicate` object with keys like `FileObject` members, except `size` which becomes `filesize`. + - foundItemHandler: Closure which is called when a file is found + - completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured. + - files: all files meat the `query` criteria. + - error: `Error` returned by server if occured. + - Returns: An `Progress` to get progress or cancel progress. Use `completedUnitCount` to iterate count of found items. + */ + @discardableResult open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ([FileObject], Error?) -> Void) -> Progress? { return searchFiles(path: path, recursive: recursive, query: query, including: [], foundItemHandler: foundItemHandler, completionHandler: completionHandler) } @@ -158,27 +202,38 @@ open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing { /** Search files inside directory using query asynchronously. - - Note: Query string is limited to file name, to search based on other file attributes, use NSPredicate version. + Sample predicates: + ``` + NSPredicate(format: "(name CONTAINS[c] 'hello') && (fileSize >= 10000)") + NSPredicate(format: "(modifiedDate >= %@)", Date()) + NSPredicate(format: "(path BEGINSWITH %@)", "folder/child folder") + ``` + + - Note: Don't pass Spotlight predicates to this method directly, use `FileProvider.convertSpotlightPredicateTo()` method to get usable predicate. + + - Important: A file name criteria should be provided for Dropbox. - Parameters: - path: location of directory to start search - recursive: Searching subdirectories of path - - query: Simple string that file name begins with to be search, case-insensitive. + - query: An `NSPredicate` object with keys like `FileObject` members, except `size` which becomes `filesize`. - including: An array which determines which file properties should be considered to fetch. - foundItemHandler: Closure which is called when a file is found - completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured. - files: all files meat the `query` criteria. - error: `Error` returned by server if occured. + - Returns: An `Progress` to get progress or cancel progress. Use `completedUnitCount` to iterate count of found items. */ + @discardableResult open func searchFiles(path: String, recursive: Bool, query: NSPredicate, including: [URLResourceKey], foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? { let url = self.url(of: path) var request = URLRequest(url: url) request.httpMethod = "PROPFIND" // Depth infinity is disabled on some servers. Implement workaround?! request.setValue(recursive ? "infinity" : "1", forHTTPHeaderField: "Depth") - request.set(httpAuthentication: credential, with: credentialType) - request.set(httpContentType: .xml, charset: .utf8) - request.httpBody = WebDavFileObject.xmlProp([]) + request.setValue(authentication: credential, with: credentialType) + request.setValue(contentType: .xml, charset: .utf8) + request.httpBody = WebDavFileObject.xmlProp(including) let progress = Progress(totalUnitCount: -1) progress.setUserInfoObject(url, forKey: .fileURLKey) @@ -216,23 +271,29 @@ open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing { return progress } - override open func isReachable(completionHandler: @escaping (Bool) -> Void) { + override open func isReachable(completionHandler: @escaping (_ success: Bool, _ error: Error?) -> Void) { var request = URLRequest(url: baseURL!) request.httpMethod = "PROPFIND" request.setValue("0", forHTTPHeaderField: "Depth") - request.set(httpAuthentication: credential, with: credentialType) - request.set(httpContentType: .xml, charset: .utf8) + request.setValue(authentication: credential, with: credentialType) + request.setValue(contentType: .xml, charset: .utf8) request.httpBody = WebDavFileObject.xmlProp([.volumeTotalCapacityKey, .volumeAvailableCapacityKey]) runDataTask(with: request, completionHandler: { (data, response, error) in let status = (response as? HTTPURLResponse)?.statusCode ?? 400 - completionHandler(status < 300) + if status >= 400, let code = FileProviderHTTPErrorCode(rawValue: status) { + let errorDesc = data.flatMap({ String(data: $0, encoding: .utf8) }) + let error = FileProviderWebDavError(code: code, path: "", serverDescription: errorDesc, url: self.baseURL!) + completionHandler(false, error) + return + } + completionHandler(status < 300, error) }) } open func publicLink(to path: String, completionHandler: @escaping ((URL?, FileObject?, Date?, Error?) -> Void)) { guard self.baseURL?.host?.contains("dav.yandex.") ?? false else { dispatch_queue.async { - completionHandler(nil, nil, nil, self.urlError(path, code: .resourceUnavailable)) + completionHandler(nil, nil, nil, URLError(.resourceUnavailable, url: self.url(of: path))) } return } @@ -240,8 +301,8 @@ open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing { let url = self.url(of: path) var request = URLRequest(url: url) request.httpMethod = "PROPPATCH" - request.set(httpAuthentication: credential, with: credentialType) - request.set(httpContentType: .xml, charset: .utf8) + request.setValue(authentication: credential, with: credentialType) + request.setValue(contentType: .xml, charset: .utf8) let body = "\n\ntrue\n\n" request.httpBody = body.data(using: .utf8) runDataTask(with: request, completionHandler: { (data, response, error) in @@ -303,7 +364,7 @@ open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing { var request = URLRequest(url: url) request.httpMethod = method - request.set(httpAuthentication: credential, with: credentialType) + request.setValue(authentication: credential, with: credentialType) request.setValue(overwrite ? "T" : "F", forHTTPHeaderField: "Overwrite") if let dest = operation.destination, !dest.hasPrefix("file://") { request.setValue(self.url(of:dest).absoluteString, forHTTPHeaderField: "Destination") @@ -313,16 +374,16 @@ open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing { } override func serverError(with code: FileProviderHTTPErrorCode, path: String?, data: Data?) -> FileProviderHTTPError { - return FileProviderWebDavError(code: code, path: path ?? "", errorDescription: data.flatMap({ String(data: $0, encoding: .utf8) }), url: self.url(of: path ?? "")) + return FileProviderWebDavError(code: code, path: path ?? "", serverDescription: data.flatMap({ String(data: $0, encoding: .utf8) }), url: self.url(of: path ?? "")) } - override func multiStatusHandler(source: String, data: Data, completionHandler: SimpleCompletionHandler) { + override func multiStatusError(operation: FileOperationType, data: Data) -> FileProviderHTTPError? { let xresponses = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL) for xresponse in xresponses where (xresponse.status ?? 0) >= 300 { let code = xresponse.status.flatMap { FileProviderHTTPErrorCode(rawValue: $0) } ?? .internalServerError - let error = self.serverError(with: code, path: source, data: data) - completionHandler?(error) + return self.serverError(with: code, path: operation.source, data: data) } + return nil } /* @@ -342,27 +403,70 @@ open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing { } extension WebDAVFileProvider: ExtendedFileProvider { + #if os(macOS) || os(iOS) || os(tvOS) + private func nextcloudPreviewURL(of path: String) -> URL? { + // Check that this is a Nextcloud server + guard self.baseURL?.absoluteString.lowercased().contains("remote.php/dav/files/") ?? false else { + return nil + } + + var rpath = path.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? path + if let baseURL = baseURL, + let index = baseURL.pathComponents.firstIndex(of: "remote.php") { + if rpath.hasPrefix("/") { + rpath.remove(at: rpath.startIndex) + } + + // Remove Nextcloud files path components + var previewURL = baseURL + for _ in 0 ..< baseURL.pathComponents.count - index { + previewURL.deleteLastPathComponent() + } + + previewURL.appendPathComponent("index.php") + previewURL.appendPathComponent("core") + previewURL.appendPathComponent("preview.png") + + return URL(string: previewURL.absoluteString + "?file=\(rpath)")! + } + + return nil + } + open func thumbnailOfFileSupported(path: String) -> Bool { - guard self.baseURL?.host?.contains("dav.yandex.") ?? false else { + guard self.baseURL?.host?.contains("dav.yandex.") ?? false + || nextcloudPreviewURL(of: path) != nil else { return false } let supportedExt: [String] = ["jpg", "jpeg", "png", "gif"] - return supportedExt.contains((path as NSString).pathExtension) + return supportedExt.contains(path.pathExtension.lowercased()) } - open func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((ImageClass?, Error?) -> Void)) { - guard self.baseURL?.host?.contains("dav.yandex.") ?? false else { - dispatch_queue.async { - completionHandler(nil, self.urlError(path, code: .resourceUnavailable)) + @discardableResult + open func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((ImageClass?, Error?) -> Void)) -> Progress? { + let url: URL + + if let nextcloudURL = self.nextcloudPreviewURL(of: path) { + if let dimension = dimension { + url = URL(string: nextcloudURL.absoluteString + "&x=\(dimension.width)&y=\(dimension.height)&a=1&mode=cover")! + } else { + url = URL(string: nextcloudURL.absoluteString + "&a=1&mode=cover")! } - return + } else { + guard self.baseURL?.host?.contains("dav.yandex.") ?? false else { + dispatch_queue.async { + completionHandler(nil, URLError(.resourceUnavailable, url: self.url(of: path))) + } + return nil + } + + let dimension = dimension ?? CGSize(width: 64, height: 64) + url = URL(string: self.url(of: path).absoluteString + "?preview&size=\(dimension.width)x\(dimension.height)")! } - let dimension = dimension ?? CGSize(width: 64, height: 64) - let url = URL(string: self.url(of: path).absoluteString + "?preview&size=\(dimension.width)x\(dimension.height)")! var request = URLRequest(url: url) request.httpMethod = "GET" - request.set(httpAuthentication: credential, with: credentialType) + request.setValue(authentication: credential, with: credentialType) let task = session.dataTask(with: request, completionHandler: { (data, response, error) in var responseError: FileProviderHTTPError? if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { @@ -374,16 +478,20 @@ extension WebDAVFileProvider: ExtendedFileProvider { completionHandler(data.flatMap({ ImageClass(data: $0) }), nil) }) task.resume() + return nil } + #endif open func propertiesOfFileSupported(path: String) -> Bool { return false } - open func propertiesOfFile(path: String, completionHandler: @escaping (([String : Any], [String], Error?) -> Void)) { + @discardableResult + open func propertiesOfFile(path: String, completionHandler: @escaping (([String : Any], [String], Error?) -> Void)) -> Progress? { dispatch_queue.async { - completionHandler([:], [], self.urlError(path, code: .resourceUnavailable)) + completionHandler([:], [], URLError(.resourceUnavailable, url: self.url(of: path))) } + return nil } } @@ -395,14 +503,12 @@ struct DavResponse { let status: Int? let prop: [String: String] + static let urlAllowed = CharacterSet(charactersIn: " ").inverted + init? (_ node: AEXMLElement, baseURL: URL?) { func standardizePath(_ str: String) -> String { - #if swift(>=4.0) let trimmedStr = str.hasPrefix("/") ? String(str[str.index(after: str.startIndex)...]) : str - #else - let trimmedStr = str.hasPrefix("/") ? str.substring(from: str.index(after: str.startIndex)) : str - #endif return trimmedStr.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? str } @@ -424,8 +530,10 @@ struct DavResponse { guard let hrefString = node[hreftag].value else { return nil } + // Percent-encoding space, some servers return invalid urls which space is not encoded to %20 + let hrefStrPercented = hrefString.addingPercentEncoding(withAllowedCharacters: DavResponse.urlAllowed) ?? hrefString // trying to figure out relative path out of href - let hrefAbsolute = URL(string: hrefString, relativeTo: baseURL)?.absoluteURL + let hrefAbsolute = URL(string: hrefStrPercented, relativeTo: baseURL)?.absoluteURL let relativePath: String if hrefAbsolute?.host?.replacingOccurrences(of: "www.", with: "", options: .anchored) == baseURL?.host?.replacingOccurrences(of: "www.", with: "", options: .anchored) { relativePath = hrefAbsolute?.path.replacingOccurrences(of: baseURL?.absoluteURL.path ?? "", with: "", options: .anchored, range: nil) ?? hrefString @@ -458,9 +566,11 @@ struct DavResponse { break } for propItemNode in propStatNode[proptag].children { - propDic[propItemNode.name.components(separatedBy: ":").last!.lowercased()] = propItemNode.value - if propItemNode.name.hasSuffix("resourcetype") && propItemNode.xml.contains("collection") { - propDic["getcontenttype"] = "httpd/unix-directory" + let key = propItemNode.name.components(separatedBy: ":").last!.lowercased() + guard propDic.index(forKey: key) == nil else { continue } + propDic[key] = propItemNode.value + if key == "resourcetype" && propItemNode.xml.contains("collection") { + propDic["getcontenttype"] = ContentMIMEType.directory.rawValue } } self.href = href @@ -501,24 +611,25 @@ public final class WebDavFileObject: FileObject { self.size = Int64(davResponse.prop["getcontentlength"] ?? "-1") ?? NSURLSessionTransferSizeUnknown self.creationDate = davResponse.prop["creationdate"].flatMap { Date(rfcString: $0) } self.modifiedDate = davResponse.prop["getlastmodified"].flatMap { Date(rfcString: $0) } - self.contentType = davResponse.prop["getcontenttype"] ?? "application/octet-stream" + self.contentType = davResponse.prop["getcontenttype"].flatMap(ContentMIMEType.init(rawValue:)) ?? .stream self.isHidden = (Int(davResponse.prop["ishidden"] ?? "0") ?? 0) > 0 - self.type = self.contentType == "httpd/unix-directory" ? .directory : .regular + self.isReadOnly = (Int(davResponse.prop["isreadonly"] ?? "0") ?? 0) > 0 + self.type = (self.contentType == .directory) ? .directory : .regular self.entryTag = davResponse.prop["getetag"] } /// MIME type of the file. - open internal(set) var contentType: String { + public internal(set) var contentType: ContentMIMEType { get { - return allValues[.mimeTypeKey] as? String ?? "application/octet-stream" + return (allValues[.mimeTypeKey] as? String).flatMap(ContentMIMEType.init(rawValue:)) ?? .stream } set { - allValues[.mimeTypeKey] = newValue + allValues[.mimeTypeKey] = newValue.rawValue } } /// HTTP E-Tag, can be used to mark changed files. - open internal(set) var entryTag: String? { + public internal(set) var entryTag: String? { get { return allValues[.entryTagKey] as? String } @@ -560,6 +671,8 @@ public final class WebDavFileObject: FileObject { } if propKeys.isEmpty { propKeys = "" + } else { + propKeys += "" } return propKeys } @@ -573,7 +686,7 @@ public final class WebDavFileObject: FileObject { public struct FileProviderWebDavError: FileProviderHTTPError { public let code: FileProviderHTTPErrorCode public let path: String - public let errorDescription: String? + public let serverDescription: String? /// URL of resource caused error. public let url: URL } diff --git a/Tests/FilesProviderTests.swift b/Tests/FilesProviderTests.swift new file mode 100644 index 0000000..49ce094 --- /dev/null +++ b/Tests/FilesProviderTests.swift @@ -0,0 +1,424 @@ +// +// FilesProviderTests.swift +// FilesProviderTests +// +// Created by Amir Abbas on 8/11/1396 AP. +// + +import XCTest +import FilesProvider + +class FilesProviderTests: XCTestCase, FileProviderDelegate { + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + try? FileManager.default.removeItem(at: dummyFile()) + } + + func testLocal() { + let provider = LocalFileProvider() + addTeardownBlock { + self.testRemoveFile(provider, filePath: self.testFolderName) + } + testBasic(provider) + testArchiving(provider) + testOperations(provider) + } + + func testWebDav() { + guard let urlStr = ProcessInfo.processInfo.environment["webdav_url"] else { return } + let url = URL(string: urlStr)! + let cred: URLCredential? + if let user = ProcessInfo.processInfo.environment["webdav_user"], let pass = ProcessInfo.processInfo.environment["webdav_password"] { + cred = URLCredential(user: user, password: pass, persistence: .forSession) + } else { + cred = nil + } + let provider = WebDAVFileProvider(baseURL: url, credential: cred)! + provider.delegate = self + testArchiving(provider) + addTeardownBlock { + self.testRemoveFile(provider, filePath: self.testFolderName) + } + testOperations(provider) + } + + func testDropbox() { + guard let pass = ProcessInfo.processInfo.environment["dropbox_token"] else { + return + } + let cred = URLCredential(user: "testuser", password: pass, persistence: .forSession) + let provider = DropboxFileProvider(credential: cred) + provider.delegate = self + testArchiving(provider) + addTeardownBlock { + self.testRemoveFile(provider, filePath: self.testFolderName) + } + testBasic(provider) + testOperations(provider) + } + + func testFTPPassive() { + /* + guard let urlStr = ProcessInfo.processInfo.environment["ftp_url"] else { return } + let url = URL(string: urlStr)! + let cred: URLCredential? + if let user = ProcessInfo.processInfo.environment["ftp_user"], let pass = ProcessInfo.processInfo.environment["ftp_password"] { + cred = URLCredential(user: user, password: pass, persistence: .forSession) + } else { + cred = nil + } + */ + let url = URL(string: "ftp://ftp.edmapplication.com")! + let cred = URLCredential(user: "abbas@edmapplication.com", password: "baTsivWZ4", persistence: .forSession) + //let url = URL(string: "ftpes://ftp.adidas-group.com:21")! + //let cred = URLCredential(user: "ecomwe-reversals-full", password: "rNeUj726Xqk2k", persistence: .forSession) + let provider = FTPFileProvider(baseURL: url, mode: .extendedPassive, credential: cred)! + provider.delegate = self + testArchiving(provider) + addTeardownBlock { + self.testRemoveFile(provider, filePath: self.testFolderName) + } + testOperations(provider) + } + + func testOneDrive() { + guard let pass = ProcessInfo.processInfo.environment["onedrive_token"] else { + return + } + let cred = URLCredential(user: "testuser", password: pass, persistence: .forSession) + let provider = OneDriveFileProvider(credential: cred) + provider.delegate = self + testBasic(provider) + testArchiving(provider) + addTeardownBlock { + self.testRemoveFile(provider, filePath: self.testFolderName) + } + testOperations(provider) + } + + /* + func testPerformanceExample() { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + */ + + let timeout: Double = 60.0 + let testFolderName = "Test" + let textFilePath = "/Test/file.txt" + let renamedFilePath = "/Test/renamed.txt" + let uploadFilePath = "/Test/uploaded.dat" + let sampleText = "Hello world!" + + fileprivate func testCreateFolder(_ provider: FileProvider, folderName: String) { + let desc = "Creating folder at root in \(provider.type)" + print("Test started: \(desc).") + let expectation = XCTestExpectation(description: desc) + provider.create(folder: folderName, at: "/") { (error) in + XCTAssertNil(error, "\(desc) failed: \(error?.localizedDescription ?? "no error desc")") + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout) + print("Test fulfilled: \(desc).") + } + + fileprivate func testContentsOfDirectory(_ provider: FileProvider) { + let desc = "Enumerating files list in \(provider.type)" + print("Test started: \(desc).") + let expectation = XCTestExpectation(description: desc) + provider.contentsOfDirectory(path: "/") { (files, error) in + XCTAssertNil(error, "\(desc) failed: \(error?.localizedDescription ?? "no error desc")") + XCTAssertGreaterThan(files.count, 0, "list is empty") + let testFolder = files.filter({ $0.name == self.testFolderName }).first + XCTAssertNotNil(testFolder, "Test folder didn't listed") + guard testFolder != nil else { return } + XCTAssertTrue(testFolder!.isDirectory, "Test entry is not a folder") + XCTAssertLessThanOrEqual(testFolder!.size, 0, "folder size is not -1") + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout) + print("Test fulfilled: \(desc).") + } + + fileprivate func testAttributesOfFile(_ provider: FileProvider, filePath: String) { + let desc = "Attrubutes of file in \(provider.type)" + print("Test started: \(desc).") + let expectation = XCTestExpectation(description: desc) + provider.attributesOfItem(path: filePath) { (fileObject, error) in + XCTAssertNil(error, "\(desc) failed: \(error?.localizedDescription ?? "no error desc")") + XCTAssertNotNil(fileObject, "file '\(filePath)' didn't exist") + guard fileObject != nil else { return } + XCTAssertEqual(fileObject!.path, filePath, "file path is different from '\(filePath)'") + XCTAssertEqual(fileObject!.type, URLFileResourceType.regular, "file '\(filePath)' is not a regular file") + XCTAssertGreaterThan(fileObject!.size, 0, "file '\(filePath)' is empty") + XCTAssertNotNil(fileObject!.modifiedDate, "file '\(filePath)' has no modification date") + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout) + print("Test fulfilled: \(desc).") + } + + fileprivate func testCreateFile(_ provider: FileProvider, filePath: String) { + let desc = "Creating file in \(provider.type)" + print("Test started: \(desc).") + let expectation = XCTestExpectation(description: desc) + let data = sampleText.data(using: .ascii) + provider.writeContents(path: filePath, contents: data, overwrite: true) { (error) in + XCTAssertNil(error, "\(desc) failed: \(error?.localizedDescription ?? "no error desc")") + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout * 3) + print("Test fulfilled: \(desc).") + } + + fileprivate func testContentsFile(_ provider: FileProvider, filePath: String, hasSampleText: Bool = true) { + let desc = "Reading file in \(provider.type)" + print("Test started: \(desc).") + let expectation = XCTestExpectation(description: desc) + provider.contents(path: filePath) { (data, error) in + XCTAssertNil(error, "\(desc) failed: \(error?.localizedDescription ?? "no error desc")") + XCTAssertNotNil(data, "no data for test file") + if data != nil && hasSampleText { + let str = String(data: data!, encoding: .ascii) + XCTAssertNotNil(str, "test file data not readable") + XCTAssertEqual(str, self.sampleText, "test file data didn't matched") + } + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout * 3) + print("Test fulfilled: \(desc).") + } + + fileprivate func testRenameFile(_ provider: FileProvider, filePath: String, to toPath: String) { + let desc = "Renaming file in \(provider.type)" + print("Test started: \(desc).") + let expectation = XCTestExpectation(description: desc) + provider.moveItem(path: filePath, to: toPath, overwrite: true) { (error) in + XCTAssertNil(error, "\(desc) failed: \(error?.localizedDescription ?? "no error desc")") + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout) + print("Test fulfilled: \(desc).") + } + + fileprivate func testCopyFile(_ provider: FileProvider, filePath: String, to toPath: String) { + let desc = "Copying file in \(provider.type)" + print("Test started: \(desc).") + let expectation = XCTestExpectation(description: desc) + provider.copyItem(path: filePath, to: toPath, overwrite: true) { (error) in + XCTAssertNil(error, "\(desc) failed: \(error?.localizedDescription ?? "no error desc")") + expectation.fulfill() + } + if provider is FTPFileProvider { + // FTP will need to download and upload file again. + wait(for: [expectation], timeout: timeout * 6) + } else { + wait(for: [expectation], timeout: timeout) + } + print("Test fulfilled: \(desc).") + } + + fileprivate func testRemoveFile(_ provider: FileProvider, filePath: String) { + let desc = "Deleting file in \(provider.type)" + print("Test started: \(desc).") + let expectation = XCTestExpectation(description: desc) + provider.removeItem(path: filePath) { (error) in + XCTAssertNil(error, "\(desc) failed: \(error?.localizedDescription ?? "no error desc")") + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout) + print("Test fulfilled: \(desc).") + } + + private func randomData(size: Int = 262144) -> Data { + var keyData = Data(count: size) + let count = keyData.count + let result = keyData.withUnsafeMutableBytes { + SecRandomCopyBytes(kSecRandomDefault, count, $0) + } + if result == errSecSuccess { + return keyData + } else { + fatalError("Problem generating random bytes") + } + } + + fileprivate func dummyFile() -> URL { + let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("dummyfile.dat") + + if !FileManager.default.fileExists(atPath: url.path) { + let data = randomData() + try! data.write(to: url) + } + return url + } + + fileprivate func testUploadFile(_ provider: FileProvider, filePath: String) { + // test Upload/Download + let desc = "Uploading file in \(provider.type)" + print("Test started: \(desc).") + let expectation = XCTestExpectation(description: desc) + let dummy = dummyFile() + provider.copyItem(localFile: dummy, to: filePath) { (error) in + XCTAssertNil(error, "\(desc) failed: \(error?.localizedDescription ?? "no error desc")") + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout * 3) + print("Test fulfilled: \(desc).") + } + + fileprivate func testDownloadFile(_ provider: FileProvider, filePath: String) { + let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("downloadedfile.dat") + let desc = "Downloading file in \(provider.type)" + print("Test started: \(desc).") + let expectation = XCTestExpectation(description: desc) + provider.copyItem(path: filePath, toLocalURL: url) { (error) in + XCTAssertNil(error, "\(desc) failed: \(error?.localizedDescription ?? "no error desc")") + XCTAssertTrue(FileManager.default.fileExists(atPath: url.path), "downloaded file doesn't exist") + let size = (try? FileManager.default.attributesOfItem(atPath: url.path))?[FileAttributeKey.size] as? Int64 + XCTAssertEqual(size, 262144, "downloaded file size is unexpected") + XCTAssert(FileManager.default.contentsEqual(atPath: self.dummyFile().path, andPath: url.path), "downloaded data is corrupted") + try? FileManager.default.removeItem(at: url) + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout * 3) + print("Test fulfilled: \(desc).") + } + + fileprivate func testStorageProperties(_ provider: FileProvider, isExpected: Bool) { + let desc = "Querying volume in \(provider.type)" + print("Test started: \(desc).") + let expectation = XCTestExpectation(description: desc) + provider.storageProperties { (volume) in + if !isExpected { + XCTAssertNotNil(volume, "volume information is nil") + guard volume != nil else { return } + XCTAssertGreaterThan(volume!.totalCapacity, 0, "capacity must be greater than 0") + XCTAssertGreaterThan(volume!.availableCapacity, 0, "available capacity must be greater than 0") + XCTAssertEqual(volume!.totalCapacity, volume!.availableCapacity + volume!.usage, "total capacity is not equal to usage + available") + } else { + XCTAssertNil(volume, "volume information must be nil") + } + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout) + print("Test fulfilled: \(desc).") + } + + fileprivate func testReachability(_ provider: FileProvider) { + let desc = "Reachability of \(provider.type)" + print("Test started: \(desc).") + let expectation = XCTestExpectation(description: desc) + provider.isReachable { (status, error) in + XCTAssertTrue(status, "\(provider.type) not reachable: \(error?.localizedDescription ?? "no error desc")") + expectation.fulfill() + } + wait(for: [expectation], timeout: timeout) + print("Test fulfilled: \(desc).") + } + + fileprivate func testArchiving(_ provider: FileProvider) { + let archivedData = NSKeyedArchiver.archivedData(withRootObject: provider) + let unarchived = NSKeyedUnarchiver.unarchiveObject(with: archivedData) as? FileProvider + XCTAssertNotNil(unarchived) + XCTAssertEqual(unarchived?.baseURL, provider.baseURL, "archived provider is not same with original") + XCTAssertEqual(unarchived?.credential, provider.credential, "archived provider is not same with original") + if let provider = provider as? FileProviderBasicRemote, let unarchived_r = unarchived as? FileProviderBasicRemote { + XCTAssertEqual(unarchived_r.useCache, provider.useCache, "archived provider is not same with original") + XCTAssertEqual(unarchived_r.validatingCache, provider.validatingCache, "archived provider is not same with original") + } + if let provider = provider as? OneDriveFileProvider, let unarchived_o = unarchived as? OneDriveFileProvider { + XCTAssertEqual(unarchived_o.route.rawValue, provider.route.rawValue, "archived provider is not same with original") + } + } + + fileprivate func testSymlink(_ provider: FileProvider & FileProviderSymbolicLink, filePath: String) { + let desc = "Symlink in \(provider.type)" + print("Test started: \(desc).") + let expectation = XCTestExpectation(description: desc) + provider.create(symbolicLink: filePath + " Link", withDestinationPath: filePath) { (error) in + XCTAssertNil(error, "\(desc) failed: \(error?.localizedDescription ?? "no error desc")") + provider.destination(ofSymbolicLink: filePath + " Link", completionHandler: { (fileObject, error) in + provider.removeItem(path: filePath + " Link", completionHandler: nil) + XCTAssertNil(error, "\(desc) failed: \(error?.localizedDescription ?? "no error desc")") + XCTAssertNotNil(fileObject, "file '\(filePath)' didn't exist") + guard fileObject != nil else { return } + XCTAssertEqual(fileObject!.path, filePath, "file path is different from '\(filePath)'") + XCTAssertEqual(fileObject!.type, URLFileResourceType.regular, "file '\(filePath)' is not a regular file") + expectation.fulfill() + }) + } + wait(for: [expectation], timeout: timeout) + print("Test fulfilled: \(desc).") + } + + fileprivate func testBasic(_ provider: FileProvider) { + let filepath = "/test/file.txt" + let fileurl = provider.url(of: filepath) + let composedfilepath = provider.relativePathOf(url: fileurl) + XCTAssertEqual(composedfilepath, "test/file.txt", "file url synthesis error") + + let dirpath = "/test/" + let dirurl = provider.url(of: dirpath) + let composeddirpath = provider.relativePathOf(url: dirurl) + XCTAssertEqual(composeddirpath, "test", "directory url synthesis error") + + let rooturl1 = provider.url(of: "") + let rooturl2 = provider.url(of: "/") + XCTAssertEqual(rooturl1, rooturl2, "root url synthesis error") + } + + fileprivate func testOperations(_ provider: FileProvider) { + // Test file operations + testReachability(provider) + testCreateFolder(provider, folderName: testFolderName) + testContentsOfDirectory(provider) + testCreateFile(provider, filePath: textFilePath) + testAttributesOfFile(provider, filePath: textFilePath) + testContentsFile(provider, filePath: textFilePath) + testRenameFile(provider, filePath: textFilePath, to: renamedFilePath) + testCopyFile(provider, filePath: renamedFilePath, to: textFilePath) + if let provider = provider as? FileProvider & FileProviderSymbolicLink { + testSymlink(provider, filePath: textFilePath) + } + testRemoveFile(provider, filePath: textFilePath) + + // TODO: Test search + // TODO: Test provider delegate + + // Test upload/download + testUploadFile(provider, filePath: uploadFilePath) + testDownloadFile(provider, filePath: uploadFilePath) + } + + func fileproviderSucceed(_ fileProvider: FileProviderOperations, operation: FileOperationType) { + return + } + + func fileproviderFailed(_ fileProvider: FileProviderOperations, operation: FileOperationType, error: Error) { + return + } + + func fileproviderProgress(_ fileProvider: FileProviderOperations, operation: FileOperationType, progress: Float) { + switch operation { + case .copy(source: let source, destination: let dest) where dest.hasPrefix("file://"): + print("Downloading \(source) to \((dest as NSString).lastPathComponent): \(progress * 100) completed.") + case .copy(source: let source, destination: let dest) where source.hasPrefix("file://"): + print("Uploading \((source as NSString).lastPathComponent) to \(dest): \(progress * 100) completed.") + case .copy(source: let source, destination: let dest): + print("Copy \(source) to \(dest): \(progress * 100) completed.") + default: + break + } + return + } +} diff --git a/Tests/Info.plist b/Tests/Info.plist new file mode 100644 index 0000000..aed7ff6 --- /dev/null +++ b/Tests/Info.plist @@ -0,0 +1,27 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + +