Compare commits
330 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ccb7961171 | |||
| e4fc6b24c0 | |||
| f22af8d002 | |||
| e5e5faa4e8 | |||
| f2cd571d7a | |||
| 4202f5e1bd | |||
| 3040215ce3 | |||
| bd2f2b3954 | |||
| 9d19768e1c | |||
| bcc774d3a8 | |||
| ad9768a584 | |||
| a089cbc21c | |||
| 090baa3b61 | |||
| fc75c85b14 | |||
| 6c34a4e9a8 | |||
| 37ce9c95fc | |||
| b39c1c4e82 | |||
| 8f0cbf8513 | |||
| 2690551b7f | |||
| a6550b0ec3 | |||
| 4b0fffc691 | |||
| a3a584c7d5 | |||
| 90f846c88f | |||
| be38e2731d | |||
| f384d267cb | |||
| 8e617c552d | |||
| 173fba56d7 | |||
| 09cb34352f | |||
| fd89a04c8e | |||
| 1bf42b489d | |||
| 50bbe7f2fa | |||
| 7e8bc05cb1 | |||
| 516144ead6 | |||
| e6b8e60321 | |||
| 5868766d6f | |||
| 8cc6bf56b6 | |||
| 729d9b9896 | |||
| 634a40cbc4 | |||
| 19f9113a00 | |||
| a3931982eb | |||
| 9b8db9e160 | |||
| 611366be7a | |||
| fec0346e49 | |||
| bd77c6ecd1 | |||
| f20bcf7e51 | |||
| 3c87dfb69b | |||
| d29826c56c | |||
| 3f09c6ee2c | |||
| b2b82abd16 | |||
| 0fb3fe5466 | |||
| 26038004ac | |||
| 463663275a | |||
| 7c25bcdef7 | |||
| df781dbe10 | |||
| f99d40c7b5 | |||
| 76e0865cdc | |||
| f3d049f608 | |||
| d0b4d13c01 | |||
| 783e58cf5c | |||
| fe0ff89ec6 | |||
| bf75db8f45 | |||
| 06c24d634d | |||
| 9d45b1f9c6 | |||
| 56bc1703ba | |||
| 76ff978df4 | |||
| 435ed37d93 | |||
| 3d563c3bfa | |||
| 628ed96654 | |||
| 9e4a803345 | |||
| 0e53036855 | |||
| 87317c1d42 | |||
| f0c6cf155a | |||
| 4c35e446a7 | |||
| 47516c63ad | |||
| 6cd5b7d8c5 | |||
| 598444c90f | |||
| 370866442a | |||
| 9cc9a6a96a | |||
| 7959edf671 | |||
| 2969af28b8 | |||
| d41e86b30a | |||
| 5d4414897b | |||
| aaa1def9e7 | |||
| 1827946a20 | |||
| 024a3637b4 | |||
| 0a9cce0cf9 | |||
| b20fc3efe5 | |||
| d379c563d8 | |||
| 682dd40072 | |||
| 3f35e600cd | |||
| 434945da87 | |||
| 9cd2fa2e3a | |||
| a3cb468c29 | |||
| 7be6985454 | |||
| a58f0ecbe6 | |||
| 04ac3e22e7 | |||
| c8de7fdb69 | |||
| 223dca1d1c | |||
| 229770108e | |||
| 42b879d4e2 | |||
| 5800c9a2ec | |||
| 7c9b2bf09a | |||
| ba08cdda1d | |||
| b380685932 | |||
| 5a5beb6891 | |||
| c7201a8be7 | |||
| d2a03369fd | |||
| d08098008a | |||
| 3c5303214e | |||
| ad8f333d16 | |||
| 5e8f653c52 | |||
| e4cd7ddf22 | |||
| 1efb0e3fe5 | |||
| 1292856646 | |||
| e39f9c29ec | |||
| f43199c22a | |||
| 3d44de0b40 | |||
| 21b5214481 | |||
| f0b4925db2 | |||
| 0da957d25f | |||
| 0de558c160 | |||
| cad68da076 | |||
| 39da09edd4 | |||
| 5129aee1b5 | |||
| 478f0819b5 | |||
| b2a7800f7c | |||
| 96c102d156 | |||
| 8aedd8e72a | |||
| 5b55debe8a | |||
| 0fa062a946 | |||
| 879f86c1f9 | |||
| 80d5f02bbd | |||
| b8a7721b2f | |||
| 44b4784cd3 | |||
| 7d9e2247f2 | |||
| 0ace562442 | |||
| 63d831ef90 | |||
| d6b91348a3 | |||
| fd9d4c1ab4 | |||
| 41e266c2a9 | |||
| 6127f4a7d9 | |||
| faca943beb | |||
| 9345436fa2 | |||
| 6228d88d41 | |||
| 5b395915a9 | |||
| e4f12a502b | |||
| 8faad0ec65 | |||
| bd5569d213 | |||
| 14ed279879 | |||
| fd293f7bdb | |||
| 3d9625e243 | |||
| 0d017ebfc4 | |||
| fd36df67f3 | |||
| 5be8c4ef5e | |||
| 0374fd7688 | |||
| 5ddfa43555 | |||
| 21850bb548 | |||
| b166e111e0 | |||
| 1dd7561215 | |||
| f94719deb0 | |||
| b13df0a977 | |||
| 24af7aa4c2 | |||
| 06039ad993 | |||
| d8fec3e346 | |||
| 5c93bc8731 | |||
| 61ba245189 | |||
| 02e6cd37dd | |||
| 55608fb8d0 | |||
| 3e3582f6fa | |||
| dd7a9d20b6 | |||
| d4a9b4a34f | |||
| 34c663e62c | |||
| 1415dda987 | |||
| cd465c1288 | |||
| a605b0cd85 | |||
| bf7043de29 | |||
| ff5e13931f | |||
| 75af738d2e | |||
| f54a1253e4 | |||
| 1394a92662 | |||
| dab171c755 | |||
| ea5de2e2aa | |||
| 2253cca086 | |||
| e15a900ade | |||
| 5c2c56c44c | |||
| ff4bbdf0de | |||
| 163a218ac2 | |||
| 81401ee36f | |||
| 759ba3c7bf | |||
| f5c8f6308b | |||
| 4dbb0adb18 | |||
| bf62d585fd | |||
| f21f658874 | |||
| e6eba3d198 | |||
| 6959a14dc1 | |||
| 29a9e0fb82 | |||
| c7b4e1f124 | |||
| 99a433a0fc | |||
| 4023fbc62e | |||
| 1045901d7c | |||
| 528d5eebc3 | |||
| 079f8f4b77 | |||
| e12f386a9d | |||
| 38e217bc19 | |||
| 0b41abd4ef | |||
| aa781adeb2 | |||
| d61e51ba1c | |||
| cdff7db32e | |||
| 9533a0e3c9 | |||
| 194673b3b6 | |||
| 194c8a41aa | |||
| c290377433 | |||
| 330a22c45d | |||
| e899804e28 | |||
| 489a9a16d0 | |||
| 93359d5173 | |||
| 12ff3a410d | |||
| a7de09362b | |||
| b1fe37cf2a | |||
| bd59eacee2 | |||
| 42579be371 | |||
| edd4914ca7 | |||
| bd3e1bd74f | |||
| 371c481482 | |||
| dea1d01bf3 | |||
| ae4cd1dff3 | |||
| 72520973e9 | |||
| 63016285af | |||
| 3245c9df03 | |||
| ee1fa89747 | |||
| 4a6b25deac | |||
| 2423cbd0f6 | |||
| 480099ca8a | |||
| 755bf6bf84 | |||
| 525825ff5d | |||
| a35384dc31 | |||
| 0c558fdbb4 | |||
| e30861ad18 | |||
| 538e9bb42d | |||
| ef2d69c54c | |||
| 9f210ef356 | |||
| 5a7dfa1039 | |||
| be7a370a42 | |||
| b84e6ea7ab | |||
| 82b1d2c450 | |||
| c38ee1ccd3 | |||
| 1a44df3fd7 | |||
| eff3725680 | |||
| 9368f636d3 | |||
| 3f2fda638f | |||
| 6737f98978 | |||
| 3e55cc60f7 | |||
| d7ae709cf7 | |||
| 4f6e3c7bd5 | |||
| b121fe6182 | |||
| 29b7f720f5 | |||
| 067e14b76f | |||
| 09e39ada82 | |||
| 6889b302b7 | |||
| 96ce12f226 | |||
| 091fd14a88 | |||
| 10aac532f7 | |||
| 3aac0a88c4 | |||
| 24355a4c6c | |||
| d2cf657ab2 | |||
| f2d54c7f92 | |||
| 0e9a82a288 | |||
| ea7e089718 | |||
| 19ea14ebc1 | |||
| 47885c0a4e | |||
| ac44aa190f | |||
| 50f6a393a0 | |||
| 779b38f381 | |||
| a374841883 | |||
| 0123d8b117 | |||
| fdc4bb818d | |||
| a08a9fe7a0 | |||
| 792ac6b015 | |||
| 8bad5944bc | |||
| 6ef2ab11c4 | |||
| 7f27d46c70 | |||
| b1ec99b1b8 | |||
| c4b8065cd3 | |||
| 2d8454c711 | |||
| 3b35c066de | |||
| 7d8de9cefe | |||
| e84fef20ea | |||
| 4a9a3196a2 | |||
| f4e6d277ae | |||
| 1328a8e9e2 | |||
| 5bf620916f | |||
| 4f56e20441 | |||
| 9dda618b73 | |||
| da60c05188 | |||
| 4366855d54 | |||
| fe05fd83fe | |||
| 826d207e6b | |||
| 66fc1e1284 | |||
| a1d489f5a5 | |||
| ad2699cd19 | |||
| 97ae86cedb | |||
| 71b07cac1b | |||
| a15f8f3809 | |||
| dc1270d8d1 | |||
| 68b1e23be3 | |||
| 057c9fd940 | |||
| 6d63322779 | |||
| fc6b46d17a | |||
| bb9c08e309 | |||
| 315ead606e | |||
| d63e9c7f04 | |||
| a5fe28c18d | |||
| 542c18bab6 | |||
| dca5dddbfd | |||
| 6764c23f1b | |||
| 955a86372b | |||
| 32a20fbc49 | |||
| 921ba19afa | |||
| 8625b0464c | |||
| 1bf9d1eadd | |||
| cbf774ba9e | |||
| 5e2f911cda | |||
| f423834021 | |||
| 0772c87122 | |||
| 011c535760 | |||
| 468a2bc9e9 | |||
| 33be492499 | |||
| c4049b961f | |||
| 940c7c1028 | |||
| b4ace7e680 |
@@ -0,0 +1 @@
|
||||
4.0
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
language: objective-c
|
||||
osx_image: xcode9
|
||||
xcode_project: $PROJECTNAME.xcodeproj
|
||||
env:
|
||||
global:
|
||||
- PROJECT="$PROJECTNAME.xcodeproj"
|
||||
- FRAMEWORK_NAME="$PROJECTNAME.framework"
|
||||
- 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" CARTHAGEDEPLOY="YES"
|
||||
- DESTINATION="OS=11.0,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=11.0,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-travis-formatter
|
||||
|
||||
script:
|
||||
- set -o pipefail
|
||||
- xcodebuild -version
|
||||
|
||||
# Build Example in Debug if specified
|
||||
- if [ $BUILD_EXAMPLE == "YES" ]; then
|
||||
xcodebuild -project "$WORKSPACE" -scheme "$EXAMPLE_SCHEME" -sdk "$SDK" -destination "$DESTINATION" -configuration Debug ONLY_ACTIVE_ARCH=NO build | xcpretty;
|
||||
fi
|
||||
|
||||
# Build Framework in Debug and Run Tests if specified
|
||||
- if [ $RUN_TESTS == "YES" ]; then
|
||||
xcodebuild -project "$PROJECT" -scheme "$SCHEME" -sdk "$SDK" -destination "$DESTINATION" -configuration Debug ONLY_ACTIVE_ARCH=NO ENABLE_TESTABILITY=YES test | xcpretty;
|
||||
else
|
||||
xcodebuild -project "$PROJECT" -scheme "$SCHEME" -sdk "$SDK" -destination "$DESTINATION" -configuration Debug ONLY_ACTIVE_ARCH=NO build | xcpretty;
|
||||
fi
|
||||
|
||||
# Build Framework in Release and Run Tests if specified
|
||||
- if [ $RUN_TESTS == "YES" ]; then
|
||||
xcodebuild -project "$PROJECT" -scheme "$SCHEME" -sdk "$SDK" -destination "$DESTINATION" -configuration Release ONLY_ACTIVE_ARCH=NO ENABLE_TESTABILITY=YES test | xcpretty;
|
||||
else
|
||||
xcodebuild -project "$PROJECT" -scheme "$SCHEME" -sdk "$SDK" -destination "$DESTINATION" -configuration Release ONLY_ACTIVE_ARCH=NO build | xcpretty;
|
||||
fi
|
||||
|
||||
# Run `pod lib lint` if specified
|
||||
- if [ $POD == "YES" ]; then
|
||||
pod lib lint --quick;
|
||||
fi
|
||||
|
||||
after_success:
|
||||
# Run `pod trunk push` if specified
|
||||
- if [ $POD == "YES" ] && [ -n "$TRAVIS_TAG" ]; then
|
||||
pod trunk push --allow-warnings;
|
||||
fi
|
||||
|
||||
# - bash <(curl -s https://codecov.io/bash)
|
||||
before_deploy:
|
||||
- if [ $CARTHAGEDEPLOY == "YES" ] && [ -n "$TRAVIS_TAG" ]; then
|
||||
brew update;
|
||||
brew outdated carthage || brew upgrade carthage;
|
||||
carthage version;
|
||||
carthage build --no-skip-current --verbose;
|
||||
carthage archive $PROJECTNAME;
|
||||
fi
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key: "$GITHUBTOKEN"
|
||||
file: $FRAMEWORK_NAME.zip
|
||||
skip_cleanup: true
|
||||
on:
|
||||
# repo: amosavian/$PROJECTNAME
|
||||
repo: amosavian/FileProvider
|
||||
tags: true
|
||||
condition: "$CARTHAGEDEPLOY = YES"
|
||||
+273
@@ -0,0 +1,273 @@
|
||||

|
||||
|
||||
# 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:)`.
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
```
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@@ -1,804 +0,0 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 46;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
799396A71D48C02300086753 /* AEXML.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396921D48C02300086753 /* AEXML.swift */; };
|
||||
799396A81D48C02300086753 /* AEXML.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396921D48C02300086753 /* AEXML.swift */; };
|
||||
799396A91D48C02300086753 /* AEXML.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396921D48C02300086753 /* AEXML.swift */; };
|
||||
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 */; };
|
||||
799396B01D48C02300086753 /* FileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396951D48C02300086753 /* FileProvider.swift */; };
|
||||
799396B11D48C02300086753 /* FileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396951D48C02300086753 /* FileProvider.swift */; };
|
||||
799396B21D48C02300086753 /* FileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396951D48C02300086753 /* FileProvider.swift */; };
|
||||
799396B31D48C02300086753 /* LocalFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396961D48C02300086753 /* LocalFileProvider.swift */; };
|
||||
799396B41D48C02300086753 /* LocalFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396961D48C02300086753 /* LocalFileProvider.swift */; };
|
||||
799396B51D48C02300086753 /* LocalFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396961D48C02300086753 /* LocalFileProvider.swift */; };
|
||||
799396B61D48C02300086753 /* SMBClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396971D48C02300086753 /* SMBClient.swift */; };
|
||||
799396B71D48C02300086753 /* SMBClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396971D48C02300086753 /* SMBClient.swift */; };
|
||||
799396B81D48C02300086753 /* SMBClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396971D48C02300086753 /* SMBClient.swift */; };
|
||||
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 */; };
|
||||
799396C21D48C02300086753 /* SMB2FileHandle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7993969C1D48C02300086753 /* SMB2FileHandle.swift */; };
|
||||
799396C31D48C02300086753 /* SMB2FileHandle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7993969C1D48C02300086753 /* SMB2FileHandle.swift */; };
|
||||
799396C41D48C02300086753 /* SMB2FileHandle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7993969C1D48C02300086753 /* SMB2FileHandle.swift */; };
|
||||
799396C51D48C02300086753 /* SMB2FileOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7993969D1D48C02300086753 /* SMB2FileOperation.swift */; };
|
||||
799396C61D48C02300086753 /* SMB2FileOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7993969D1D48C02300086753 /* SMB2FileOperation.swift */; };
|
||||
799396C71D48C02300086753 /* SMB2FileOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7993969D1D48C02300086753 /* SMB2FileOperation.swift */; };
|
||||
799396C81D48C02300086753 /* SMB2IOCtl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7993969E1D48C02300086753 /* SMB2IOCtl.swift */; };
|
||||
799396C91D48C02300086753 /* SMB2IOCtl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7993969E1D48C02300086753 /* SMB2IOCtl.swift */; };
|
||||
799396CA1D48C02300086753 /* SMB2IOCtl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7993969E1D48C02300086753 /* SMB2IOCtl.swift */; };
|
||||
799396CB1D48C02300086753 /* SMB2Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7993969F1D48C02300086753 /* SMB2Query.swift */; };
|
||||
799396CC1D48C02300086753 /* SMB2Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7993969F1D48C02300086753 /* SMB2Query.swift */; };
|
||||
799396CD1D48C02300086753 /* SMB2Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7993969F1D48C02300086753 /* SMB2Query.swift */; };
|
||||
799396CE1D48C02300086753 /* SMB2Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396A01D48C02300086753 /* SMB2Session.swift */; };
|
||||
799396CF1D48C02300086753 /* SMB2Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396A01D48C02300086753 /* SMB2Session.swift */; };
|
||||
799396D01D48C02300086753 /* SMB2Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396A01D48C02300086753 /* SMB2Session.swift */; };
|
||||
799396D11D48C02300086753 /* SMB2SetInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396A11D48C02300086753 /* SMB2SetInfo.swift */; };
|
||||
799396D21D48C02300086753 /* SMB2SetInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396A11D48C02300086753 /* SMB2SetInfo.swift */; };
|
||||
799396D31D48C02300086753 /* SMB2SetInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396A11D48C02300086753 /* SMB2SetInfo.swift */; };
|
||||
799396D41D48C02300086753 /* SMB2Tree.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396A21D48C02300086753 /* SMB2Tree.swift */; };
|
||||
799396D51D48C02300086753 /* SMB2Tree.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396A21D48C02300086753 /* SMB2Tree.swift */; };
|
||||
799396D61D48C02300086753 /* SMB2Tree.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396A21D48C02300086753 /* SMB2Tree.swift */; };
|
||||
799396D71D48C02300086753 /* SMB2Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396A31D48C02300086753 /* SMB2Types.swift */; };
|
||||
799396D81D48C02300086753 /* SMB2Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396A31D48C02300086753 /* SMB2Types.swift */; };
|
||||
799396D91D48C02300086753 /* SMB2Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396A31D48C02300086753 /* SMB2Types.swift */; };
|
||||
799396DA1D48C02300086753 /* SMBErrorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396A41D48C02300086753 /* SMBErrorType.swift */; };
|
||||
799396DB1D48C02300086753 /* SMBErrorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396A41D48C02300086753 /* SMBErrorType.swift */; };
|
||||
799396DC1D48C02300086753 /* SMBErrorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396A41D48C02300086753 /* SMBErrorType.swift */; };
|
||||
799396DD1D48C02300086753 /* TCPSocketClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396A51D48C02300086753 /* TCPSocketClient.swift */; };
|
||||
799396DE1D48C02300086753 /* TCPSocketClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396A51D48C02300086753 /* TCPSocketClient.swift */; };
|
||||
799396DF1D48C02300086753 /* TCPSocketClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396A51D48C02300086753 /* TCPSocketClient.swift */; };
|
||||
799396E01D48C02300086753 /* WebDAVFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396A61D48C02300086753 /* WebDAVFileProvider.swift */; };
|
||||
799396E11D48C02300086753 /* WebDAVFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396A61D48C02300086753 /* WebDAVFileProvider.swift */; };
|
||||
799396E21D48C02300086753 /* WebDAVFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396A61D48C02300086753 /* WebDAVFileProvider.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
799396671D48B7F600086753 /* FileProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FileProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
799396751D48B80D00086753 /* FileProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FileProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
799396821D48B82700086753 /* FileProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FileProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
7993968B1D48B8C700086753 /* Info-iOS.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Info-iOS.plist"; sourceTree = "<group>"; };
|
||||
7993968C1D48B8C700086753 /* Info-MacOS.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Info-MacOS.plist"; sourceTree = "<group>"; };
|
||||
7993968D1D48B8C700086753 /* Info-tvOS.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Info-tvOS.plist"; sourceTree = "<group>"; };
|
||||
799396921D48C02300086753 /* AEXML.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AEXML.swift; sourceTree = "<group>"; };
|
||||
799396931D48C02300086753 /* DropboxFileProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DropboxFileProvider.swift; sourceTree = "<group>"; };
|
||||
799396941D48C02300086753 /* FileProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FileProvider.h; sourceTree = "<group>"; };
|
||||
799396951D48C02300086753 /* FileProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileProvider.swift; sourceTree = "<group>"; };
|
||||
799396961D48C02300086753 /* LocalFileProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalFileProvider.swift; sourceTree = "<group>"; };
|
||||
799396971D48C02300086753 /* SMBClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMBClient.swift; sourceTree = "<group>"; };
|
||||
799396981D48C02300086753 /* SMBFileProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMBFileProvider.swift; sourceTree = "<group>"; };
|
||||
7993969A1D48C02300086753 /* CIFSTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CIFSTypes.swift; sourceTree = "<group>"; };
|
||||
7993969B1D48C02300086753 /* SMB2DataTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMB2DataTypes.swift; sourceTree = "<group>"; };
|
||||
7993969C1D48C02300086753 /* SMB2FileHandle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMB2FileHandle.swift; sourceTree = "<group>"; };
|
||||
7993969D1D48C02300086753 /* SMB2FileOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMB2FileOperation.swift; sourceTree = "<group>"; };
|
||||
7993969E1D48C02300086753 /* SMB2IOCtl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMB2IOCtl.swift; sourceTree = "<group>"; };
|
||||
7993969F1D48C02300086753 /* SMB2Query.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMB2Query.swift; sourceTree = "<group>"; };
|
||||
799396A01D48C02300086753 /* SMB2Session.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMB2Session.swift; sourceTree = "<group>"; };
|
||||
799396A11D48C02300086753 /* SMB2SetInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMB2SetInfo.swift; sourceTree = "<group>"; };
|
||||
799396A21D48C02300086753 /* SMB2Tree.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMB2Tree.swift; sourceTree = "<group>"; };
|
||||
799396A31D48C02300086753 /* SMB2Types.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMB2Types.swift; sourceTree = "<group>"; };
|
||||
799396A41D48C02300086753 /* SMBErrorType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMBErrorType.swift; sourceTree = "<group>"; };
|
||||
799396A51D48C02300086753 /* TCPSocketClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TCPSocketClient.swift; sourceTree = "<group>"; };
|
||||
799396A61D48C02300086753 /* WebDAVFileProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebDAVFileProvider.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
799396631D48B7F600086753 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
799396711D48B80D00086753 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
7993967E1D48B82700086753 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
7993965B1D48B7BF00086753 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
799396911D48C02300086753 /* Sources */,
|
||||
7993968A1D48B8C700086753 /* Pod */,
|
||||
799396681D48B7F600086753 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
799396681D48B7F600086753 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
799396671D48B7F600086753 /* FileProvider.framework */,
|
||||
799396751D48B80D00086753 /* FileProvider.framework */,
|
||||
799396821D48B82700086753 /* FileProvider.framework */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7993968A1D48B8C700086753 /* Pod */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7993968B1D48B8C700086753 /* Info-iOS.plist */,
|
||||
7993968C1D48B8C700086753 /* Info-MacOS.plist */,
|
||||
7993968D1D48B8C700086753 /* Info-tvOS.plist */,
|
||||
);
|
||||
path = Pod;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
799396911D48C02300086753 /* Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
799396991D48C02300086753 /* SMBTypes */,
|
||||
799396921D48C02300086753 /* AEXML.swift */,
|
||||
799396931D48C02300086753 /* DropboxFileProvider.swift */,
|
||||
799396941D48C02300086753 /* FileProvider.h */,
|
||||
799396951D48C02300086753 /* FileProvider.swift */,
|
||||
799396961D48C02300086753 /* LocalFileProvider.swift */,
|
||||
799396971D48C02300086753 /* SMBClient.swift */,
|
||||
799396981D48C02300086753 /* SMBFileProvider.swift */,
|
||||
799396A51D48C02300086753 /* TCPSocketClient.swift */,
|
||||
799396A61D48C02300086753 /* WebDAVFileProvider.swift */,
|
||||
);
|
||||
path = Sources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
799396991D48C02300086753 /* SMBTypes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7993969A1D48C02300086753 /* CIFSTypes.swift */,
|
||||
799396A41D48C02300086753 /* SMBErrorType.swift */,
|
||||
7993969B1D48C02300086753 /* SMB2DataTypes.swift */,
|
||||
799396A31D48C02300086753 /* SMB2Types.swift */,
|
||||
799396A21D48C02300086753 /* SMB2Tree.swift */,
|
||||
799396A01D48C02300086753 /* SMB2Session.swift */,
|
||||
7993969D1D48C02300086753 /* SMB2FileOperation.swift */,
|
||||
7993969C1D48C02300086753 /* SMB2FileHandle.swift */,
|
||||
7993969E1D48C02300086753 /* SMB2IOCtl.swift */,
|
||||
7993969F1D48C02300086753 /* SMB2Query.swift */,
|
||||
799396A11D48C02300086753 /* SMB2SetInfo.swift */,
|
||||
);
|
||||
path = SMBTypes;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXHeadersBuildPhase section */
|
||||
799396641D48B7F600086753 /* Headers */ = {
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
799396721D48B80D00086753 /* Headers */ = {
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
7993967F1D48B82700086753 /* Headers */ = {
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXHeadersBuildPhase section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
799396661D48B7F600086753 /* FileProvider iOS */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 7993966F1D48B7F600086753 /* Build configuration list for PBXNativeTarget "FileProvider iOS" */;
|
||||
buildPhases = (
|
||||
799396621D48B7F600086753 /* Sources */,
|
||||
799396631D48B7F600086753 /* Frameworks */,
|
||||
799396641D48B7F600086753 /* Headers */,
|
||||
799396651D48B7F600086753 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = "FileProvider iOS";
|
||||
productName = "FileProvider iOS";
|
||||
productReference = 799396671D48B7F600086753 /* FileProvider.framework */;
|
||||
productType = "com.apple.product-type.framework";
|
||||
};
|
||||
799396741D48B80D00086753 /* FileProvider OSX */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 7993967A1D48B80D00086753 /* Build configuration list for PBXNativeTarget "FileProvider OSX" */;
|
||||
buildPhases = (
|
||||
799396701D48B80D00086753 /* Sources */,
|
||||
799396711D48B80D00086753 /* Frameworks */,
|
||||
799396721D48B80D00086753 /* Headers */,
|
||||
799396731D48B80D00086753 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = "FileProvider OSX";
|
||||
productName = "FileProvider OSX";
|
||||
productReference = 799396751D48B80D00086753 /* FileProvider.framework */;
|
||||
productType = "com.apple.product-type.framework";
|
||||
};
|
||||
799396811D48B82700086753 /* FileProvider tvOS */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 799396871D48B82700086753 /* Build configuration list for PBXNativeTarget "FileProvider tvOS" */;
|
||||
buildPhases = (
|
||||
7993967D1D48B82700086753 /* Sources */,
|
||||
7993967E1D48B82700086753 /* Frameworks */,
|
||||
7993967F1D48B82700086753 /* Headers */,
|
||||
799396801D48B82700086753 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = "FileProvider tvOS";
|
||||
productName = "FileProvider tvOS";
|
||||
productReference = 799396821D48B82700086753 /* FileProvider.framework */;
|
||||
productType = "com.apple.product-type.framework";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
7993965C1D48B7BF00086753 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 0730;
|
||||
TargetAttributes = {
|
||||
799396661D48B7F600086753 = {
|
||||
CreatedOnToolsVersion = 7.3.1;
|
||||
};
|
||||
799396741D48B80D00086753 = {
|
||||
CreatedOnToolsVersion = 7.3.1;
|
||||
};
|
||||
799396811D48B82700086753 = {
|
||||
CreatedOnToolsVersion = 7.3.1;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 7993965F1D48B7BF00086753 /* Build configuration list for PBXProject "FileProvider" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = English;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
);
|
||||
mainGroup = 7993965B1D48B7BF00086753;
|
||||
productRefGroup = 799396681D48B7F600086753 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
799396661D48B7F600086753 /* FileProvider iOS */,
|
||||
799396741D48B80D00086753 /* FileProvider OSX */,
|
||||
799396811D48B82700086753 /* FileProvider tvOS */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
799396651D48B7F600086753 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
799396731D48B80D00086753 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
799396801D48B82700086753 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
799396621D48B7F600086753 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
799396B31D48C02300086753 /* LocalFileProvider.swift in Sources */,
|
||||
799396D41D48C02300086753 /* SMB2Tree.swift in Sources */,
|
||||
799396C81D48C02300086753 /* SMB2IOCtl.swift in Sources */,
|
||||
799396DD1D48C02300086753 /* TCPSocketClient.swift in Sources */,
|
||||
799396D71D48C02300086753 /* SMB2Types.swift in Sources */,
|
||||
799396C51D48C02300086753 /* SMB2FileOperation.swift in Sources */,
|
||||
799396BF1D48C02300086753 /* SMB2DataTypes.swift in Sources */,
|
||||
799396B91D48C02300086753 /* SMBFileProvider.swift in Sources */,
|
||||
799396BC1D48C02300086753 /* CIFSTypes.swift in Sources */,
|
||||
799396D11D48C02300086753 /* SMB2SetInfo.swift in Sources */,
|
||||
799396A71D48C02300086753 /* AEXML.swift in Sources */,
|
||||
799396CE1D48C02300086753 /* SMB2Session.swift in Sources */,
|
||||
799396E01D48C02300086753 /* WebDAVFileProvider.swift in Sources */,
|
||||
799396DA1D48C02300086753 /* SMBErrorType.swift in Sources */,
|
||||
799396C21D48C02300086753 /* SMB2FileHandle.swift in Sources */,
|
||||
799396CB1D48C02300086753 /* SMB2Query.swift in Sources */,
|
||||
799396AA1D48C02300086753 /* DropboxFileProvider.swift in Sources */,
|
||||
799396B01D48C02300086753 /* FileProvider.swift in Sources */,
|
||||
799396B61D48C02300086753 /* SMBClient.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
799396701D48B80D00086753 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
799396B41D48C02300086753 /* LocalFileProvider.swift in Sources */,
|
||||
799396D51D48C02300086753 /* SMB2Tree.swift in Sources */,
|
||||
799396C91D48C02300086753 /* SMB2IOCtl.swift in Sources */,
|
||||
799396DE1D48C02300086753 /* TCPSocketClient.swift in Sources */,
|
||||
799396D81D48C02300086753 /* SMB2Types.swift in Sources */,
|
||||
799396C61D48C02300086753 /* SMB2FileOperation.swift in Sources */,
|
||||
799396C01D48C02300086753 /* SMB2DataTypes.swift in Sources */,
|
||||
799396BA1D48C02300086753 /* SMBFileProvider.swift in Sources */,
|
||||
799396BD1D48C02300086753 /* CIFSTypes.swift in Sources */,
|
||||
799396D21D48C02300086753 /* SMB2SetInfo.swift in Sources */,
|
||||
799396A81D48C02300086753 /* AEXML.swift in Sources */,
|
||||
799396CF1D48C02300086753 /* SMB2Session.swift in Sources */,
|
||||
799396E11D48C02300086753 /* WebDAVFileProvider.swift in Sources */,
|
||||
799396DB1D48C02300086753 /* SMBErrorType.swift in Sources */,
|
||||
799396C31D48C02300086753 /* SMB2FileHandle.swift in Sources */,
|
||||
799396CC1D48C02300086753 /* SMB2Query.swift in Sources */,
|
||||
799396AB1D48C02300086753 /* DropboxFileProvider.swift in Sources */,
|
||||
799396B11D48C02300086753 /* FileProvider.swift in Sources */,
|
||||
799396B71D48C02300086753 /* SMBClient.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
7993967D1D48B82700086753 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
799396B51D48C02300086753 /* LocalFileProvider.swift in Sources */,
|
||||
799396D61D48C02300086753 /* SMB2Tree.swift in Sources */,
|
||||
799396CA1D48C02300086753 /* SMB2IOCtl.swift in Sources */,
|
||||
799396DF1D48C02300086753 /* TCPSocketClient.swift in Sources */,
|
||||
799396D91D48C02300086753 /* SMB2Types.swift in Sources */,
|
||||
799396C71D48C02300086753 /* SMB2FileOperation.swift in Sources */,
|
||||
799396C11D48C02300086753 /* SMB2DataTypes.swift in Sources */,
|
||||
799396BB1D48C02300086753 /* SMBFileProvider.swift in Sources */,
|
||||
799396BE1D48C02300086753 /* CIFSTypes.swift in Sources */,
|
||||
799396D31D48C02300086753 /* SMB2SetInfo.swift in Sources */,
|
||||
799396A91D48C02300086753 /* AEXML.swift in Sources */,
|
||||
799396D01D48C02300086753 /* SMB2Session.swift in Sources */,
|
||||
799396E21D48C02300086753 /* WebDAVFileProvider.swift in Sources */,
|
||||
799396DC1D48C02300086753 /* SMBErrorType.swift in Sources */,
|
||||
799396C41D48C02300086753 /* SMB2FileHandle.swift in Sources */,
|
||||
799396CD1D48C02300086753 /* SMB2Query.swift in Sources */,
|
||||
799396AC1D48C02300086753 /* DropboxFileProvider.swift in Sources */,
|
||||
799396B21D48C02300086753 /* FileProvider.swift in Sources */,
|
||||
799396B81D48C02300086753 /* SMBClient.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
799396601D48B7BF00086753 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
799396611D48B7BF00086753 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
7993966D1D48B7F600086753 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
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=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEFINES_MODULE = YES;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"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;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FileProvider-iOS";
|
||||
PRODUCT_NAME = FileProvider;
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
7993966E1D48B7F600086753 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
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=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEFINES_MODULE = YES;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
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;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FileProvider-iOS";
|
||||
PRODUCT_NAME = FileProvider;
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
7993967B1D48B80D00086753 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
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 = "-";
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEFINES_MODULE = YES;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
FRAMEWORK_VERSION = A;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"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";
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.10;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FileProvider-OSX";
|
||||
PRODUCT_NAME = FileProvider;
|
||||
SDKROOT = macosx;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
7993967C1D48B80D00086753 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
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 = "-";
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEFINES_MODULE = YES;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
FRAMEWORK_VERSION = A;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
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";
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.10;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FileProvider-OSX";
|
||||
PRODUCT_NAME = FileProvider;
|
||||
SDKROOT = macosx;
|
||||
SKIP_INSTALL = YES;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
799396881D48B82700086753 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
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;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEFINES_MODULE = YES;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"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";
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FileProvider-tvOS";
|
||||
PRODUCT_NAME = FileProvider;
|
||||
SDKROOT = appletvos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
TARGETED_DEVICE_FAMILY = 3;
|
||||
TVOS_DEPLOYMENT_TARGET = 9.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
799396891D48B82700086753 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
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;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEFINES_MODULE = YES;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
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";
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FileProvider-tvOS";
|
||||
PRODUCT_NAME = FileProvider;
|
||||
SDKROOT = appletvos;
|
||||
SKIP_INSTALL = YES;
|
||||
TARGETED_DEVICE_FAMILY = 3;
|
||||
TVOS_DEPLOYMENT_TARGET = 9.0;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
7993965F1D48B7BF00086753 /* Build configuration list for PBXProject "FileProvider" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
799396601D48B7BF00086753 /* Debug */,
|
||||
799396611D48B7BF00086753 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
7993966F1D48B7F600086753 /* Build configuration list for PBXNativeTarget "FileProvider iOS" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
7993966D1D48B7F600086753 /* Debug */,
|
||||
7993966E1D48B7F600086753 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
7993967A1D48B80D00086753 /* Build configuration list for PBXNativeTarget "FileProvider OSX" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
7993967B1D48B80D00086753 /* Debug */,
|
||||
7993967C1D48B80D00086753 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
799396871D48B82700086753 /* Build configuration list for PBXNativeTarget "FileProvider tvOS" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
799396881D48B82700086753 /* Debug */,
|
||||
799396891D48B82700086753 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 7993965C1D48B7BF00086753 /* Project object */;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
#
|
||||
# Be sure to run `pod spec lint FileProvider.podspec' to ensure this is a
|
||||
# Be sure to run `pod spec lint FilesProvider.podspec' to ensure this is a
|
||||
# valid spec and to remove all comments including this before submitting the spec.
|
||||
#
|
||||
# To learn more about Podspec attributes see http://docs.cocoapods.org/specification.html
|
||||
@@ -15,9 +15,9 @@ Pod::Spec.new do |s|
|
||||
# summary should be tweet-length, and the description more in depth.
|
||||
#
|
||||
|
||||
s.name = "FileProvider"
|
||||
s.version = "0.3.3"
|
||||
s.summary = "NSFileManager replacement for Local and Remote (WebDAV/Dropbox/SMB2) files on iOS and MacOS."
|
||||
s.name = "FilesProvider"
|
||||
s.version = "0.25.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.
|
||||
# * Think: What does it do? Why did you write it? What is the focus?
|
||||
@@ -26,8 +26,8 @@ Pod::Spec.new do |s|
|
||||
# * Finally, don't worry about the indent, CocoaPods strips it!
|
||||
s.description = <<-DESC
|
||||
This Swift library provide a swifty way to deal with local and remote files
|
||||
and directories in same way. For now Local and WebDAV providers are ready to use
|
||||
and SMB2, Dropbox, FTP and AmazonS3 is planned for future.
|
||||
and directories in same way. For now Local, WebDAV and Dropbox providers are ready to use.
|
||||
SMB2, FTP and AmazonS3 is planned for future.
|
||||
DESC
|
||||
|
||||
s.homepage = "https://github.com/amosavian/FileProvider"
|
||||
@@ -55,10 +55,9 @@ 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.social_media_url = "https://twitter.com/amosavian"
|
||||
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 = "4.1"
|
||||
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"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
+10
-10
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0730"
|
||||
LastUpgradeVersion = "0930"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -15,9 +15,9 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "799396741D48B80D00086753"
|
||||
BuildableName = "FileProvider.framework"
|
||||
BlueprintName = "FileProvider OSX"
|
||||
ReferencedContainer = "container:FileProvider.xcodeproj">
|
||||
BuildableName = "FilesProvider.framework"
|
||||
BlueprintName = "FilesProvider OSX"
|
||||
ReferencedContainer = "container:FilesProvider.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
@@ -46,9 +46,9 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "799396741D48B80D00086753"
|
||||
BuildableName = "FileProvider.framework"
|
||||
BlueprintName = "FileProvider OSX"
|
||||
ReferencedContainer = "container:FileProvider.xcodeproj">
|
||||
BuildableName = "FilesProvider.framework"
|
||||
BlueprintName = "FilesProvider OSX"
|
||||
ReferencedContainer = "container:FilesProvider.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
@@ -64,9 +64,9 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "799396741D48B80D00086753"
|
||||
BuildableName = "FileProvider.framework"
|
||||
BlueprintName = "FileProvider OSX"
|
||||
ReferencedContainer = "container:FileProvider.xcodeproj">
|
||||
BuildableName = "FilesProvider.framework"
|
||||
BlueprintName = "FilesProvider OSX"
|
||||
ReferencedContainer = "container:FilesProvider.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
+10
-10
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0730"
|
||||
LastUpgradeVersion = "0930"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -15,9 +15,9 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "799396661D48B7F600086753"
|
||||
BuildableName = "FileProvider.framework"
|
||||
BlueprintName = "FileProvider iOS"
|
||||
ReferencedContainer = "container:FileProvider.xcodeproj">
|
||||
BuildableName = "FilesProvider.framework"
|
||||
BlueprintName = "FilesProvider iOS"
|
||||
ReferencedContainer = "container:FilesProvider.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
@@ -46,9 +46,9 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "799396661D48B7F600086753"
|
||||
BuildableName = "FileProvider.framework"
|
||||
BlueprintName = "FileProvider iOS"
|
||||
ReferencedContainer = "container:FileProvider.xcodeproj">
|
||||
BuildableName = "FilesProvider.framework"
|
||||
BlueprintName = "FilesProvider iOS"
|
||||
ReferencedContainer = "container:FilesProvider.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
@@ -64,9 +64,9 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "799396661D48B7F600086753"
|
||||
BuildableName = "FileProvider.framework"
|
||||
BlueprintName = "FileProvider iOS"
|
||||
ReferencedContainer = "container:FileProvider.xcodeproj">
|
||||
BuildableName = "FilesProvider.framework"
|
||||
BlueprintName = "FilesProvider iOS"
|
||||
ReferencedContainer = "container:FilesProvider.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
+10
-10
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0730"
|
||||
LastUpgradeVersion = "0930"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -15,9 +15,9 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "799396811D48B82700086753"
|
||||
BuildableName = "FileProvider.framework"
|
||||
BlueprintName = "FileProvider tvOS"
|
||||
ReferencedContainer = "container:FileProvider.xcodeproj">
|
||||
BuildableName = "FilesProvider.framework"
|
||||
BlueprintName = "FilesProvider tvOS"
|
||||
ReferencedContainer = "container:FilesProvider.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
@@ -46,9 +46,9 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "799396811D48B82700086753"
|
||||
BuildableName = "FileProvider.framework"
|
||||
BlueprintName = "FileProvider tvOS"
|
||||
ReferencedContainer = "container:FileProvider.xcodeproj">
|
||||
BuildableName = "FilesProvider.framework"
|
||||
BlueprintName = "FilesProvider tvOS"
|
||||
ReferencedContainer = "container:FilesProvider.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
@@ -64,9 +64,9 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "799396811D48B82700086753"
|
||||
BuildableName = "FileProvider.framework"
|
||||
BlueprintName = "FileProvider tvOS"
|
||||
ReferencedContainer = "container:FileProvider.xcodeproj">
|
||||
BuildableName = "FilesProvider.framework"
|
||||
BlueprintName = "FilesProvider tvOS"
|
||||
ReferencedContainer = "container:FilesProvider.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Amir Abbas Mousavian
|
||||
Copyright (c) 2016-17 Amir Abbas Mousavian
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// swift-tools-version:4.0
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "FilesProvider",
|
||||
products: [
|
||||
// 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",
|
||||
dependencies: [],
|
||||
path: "Sources"
|
||||
),
|
||||
.testTarget(name: "FilesProviderTests",
|
||||
dependencies: ["FilesProvider"],
|
||||
path: "Tests"
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.3.0</string>
|
||||
<string>${BUNDLE_VERSION_STRING}</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.3.0</string>
|
||||
<string>${BUNDLE_VERSION_STRING}</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.3.0</string>
|
||||
<string>${BUNDLE_VERSION_STRING}</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
|
||||
@@ -1,140 +1,215 @@
|
||||
# FileProvider (experimental)
|
||||

|
||||
|
||||
>This Swift library provide a swifty way to deal with local and remote files and directories in a unified way.
|
||||
> This Swift library provide a swifty way to deal with local and remote files and directories in a unified way.
|
||||
|
||||
<center>
|
||||
|
||||
[![Swift Version][swift-image]][swift-url]
|
||||
[![Platform][platform-image]](#)
|
||||
[![License][license-image]][license-url]
|
||||
[]()
|
||||
[](https://img.shields.io/cocoapods/v/FileProvider.svg)
|
||||
[![codebeat badge][codebeat-image]][codebeat-url]
|
||||
[![Build Status][travis-image]][travis-url]
|
||||
[![Codebeat Badge][codebeat-image]][codebeat-url]
|
||||
|
||||
[![Release version][release-image]][release-url]
|
||||
[][cocoapods]
|
||||
[![Carthage compatible][carthage-image]](https://github.com/Carthage/Carthage)
|
||||
[![Cocoapods Downloads][cocoapods-downloads]][cocoapods]
|
||||
[![Cocoapods Apps][cocoapods-apps]][cocoapods]
|
||||
|
||||
</center>
|
||||
|
||||
<!---
|
||||
[![Build Status][travis-image]][travis-url]
|
||||
[](https://github.com/Carthage/Carthage)
|
||||
[![Cocoapods Doc][docs-image]][docs-url]
|
||||
[](https://codecov.io/gh/amosavian/FileProvider)
|
||||
--->
|
||||
|
||||
This library provides implementaion of WebDav and SMB/CIFS (incomplete) and local files.
|
||||
This library provides implementaion of WebDav, FTP, Dropbox, OneDrive and SMB2 (incomplete) and local files.
|
||||
|
||||
All functions are async calls and it wont block your main thread.
|
||||
|
||||
Local and WebDAV providers are fully tested and can be used in production environment.
|
||||
All functions do async calls and it wont block your main thread.
|
||||
|
||||
## Features
|
||||
|
||||
- [x] **LocalFileProvider** a wrapper around `NSFileManager` with some additions like searching and reading a portion of file.
|
||||
- [x] **WebDAVFileProvider** WebDAV protocol is usual file transmission system on Macs.
|
||||
- [ ] **SMBFileProvider** SMB/CIFS and SMB2/3 are file and printer sharing protocol which is originated from IBM & Microsoft and SMB2/3 is now replacing AFP protocol on MacOS. I implemented data types and some basic functions but *main interface is not implemented yet!*
|
||||
- [ ] **DropboxFileProvider** *partially implemented*
|
||||
- [ ] **FTPFileProvider**
|
||||
- [ ] **AmazonS3FileProvider**
|
||||
- [x] **LocalFileProvider** a wrapper around `FileManager` with some additions like builtin coordinating, searching and reading a portion of file.
|
||||
- [x] **CloudFileProvider** A wrapper around app's ubiquitous container API of iCloud Drive.
|
||||
- [x] **WebDAVFileProvider** WebDAV protocol is defacto file transmission standard, supported by some cloud services like `ownCloud`, `Box.com` and `Yandex.disk`.
|
||||
- [x] **FTPFileProvider** While deprecated in 1990s due to serious security concerns, it's still in use on some Web hosts.
|
||||
- [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.
|
||||
- [ ] **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.
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Swift 2.2 or 2.3**
|
||||
- **Swift 4.0 or higher**
|
||||
- iOS 8.0 , OSX 10.10
|
||||
- XCode 7.3
|
||||
- XCode 9.0
|
||||
|
||||
Legacy version is available in swift-3 branch.
|
||||
|
||||
## Installation
|
||||
|
||||
### Cocoapods / Carthage / Swift Package Manager
|
||||
### Important: this library has been renamed to avoid conflict in iOS 11, macOS 10.13 and Xcode 9.0. Please read issue [#53](https://github.com/amosavian/FileProvider/issues/53) to find more.
|
||||
|
||||
FileProvider supports both CocoaPods.
|
||||
|
||||
### Cocoapods / Carthage / Swift Package Manager
|
||||
|
||||
Add this line to your pods file:
|
||||
|
||||
pod "FileProvider"
|
||||
```ruby
|
||||
pod "FilesProvider"
|
||||
```
|
||||
|
||||
Or add this to Cartfile:
|
||||
|
||||
```
|
||||
github "amosavian/FileProvider"
|
||||
```
|
||||
|
||||
Or to use in Swift Package Manager add this line in `Dependencies`:
|
||||
|
||||
```swift
|
||||
.Package(url: "https://github.com/amosavian/FileProvider.git", majorVersion: 0)
|
||||
```
|
||||
|
||||
### Manually
|
||||
|
||||
### Git
|
||||
To have latest updates with ease, use this command on terminal to get a clone:
|
||||
|
||||
git clone https://github.com/amosavian/FileProvider FileProvider
|
||||
|
||||
```bash
|
||||
git clone https://github.com/amosavian/FileProvider
|
||||
```
|
||||
|
||||
You can update your library using this command in FileProvider folder:
|
||||
|
||||
git pull
|
||||
```bash
|
||||
git pull
|
||||
```
|
||||
|
||||
if you have a git based project, use this command in your projects directory to add this project as a submodule to your project:
|
||||
|
||||
git submodule add https://github.com/amosavian/FileProvider FileProvider
|
||||
```bash
|
||||
git submodule add https://github.com/amosavian/FileProvider
|
||||
```
|
||||
Then you can do either:
|
||||
|
||||
### Manually
|
||||
Copy Source folder to your project and Voila!
|
||||
* Copy Source folder to your project and Voila!
|
||||
|
||||
* 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:
|
||||
|
||||
let documentsProvider = LocalFileProvider()
|
||||
``` swift
|
||||
import FilesProvider
|
||||
|
||||
is equal to:
|
||||
|
||||
let documentPath = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true);
|
||||
let documentsURL = NSURL(fileURLWithPath: documentPath);
|
||||
let documentsProvider = LocalFileProvider(baseURL: documentsURL)
|
||||
let documentsProvider = LocalFileProvider()
|
||||
|
||||
// Equals with:
|
||||
let documentsProvider = LocalFileProvider(for: .documentDirectory, in: .userDomainMask)
|
||||
|
||||
// Equals with:
|
||||
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
let documentsProvider = LocalFileProvider(baseURL: documentsURL)
|
||||
```
|
||||
|
||||
Also for using group shared container:
|
||||
|
||||
```swift
|
||||
import FilesProvider
|
||||
|
||||
let documentsProvider = LocalFileProvider(sharedContainerId: "group.yourcompany.appContainer")
|
||||
// Replace your group identifier
|
||||
```
|
||||
|
||||
You can't change the base url later. and all paths are related to this base url by default.
|
||||
|
||||
To initialize an iCloud Container provider look at [here](https://medium.com/ios-os-x-development/icloud-drive-documents-1a46b5706fe1) to see how to update project settings then use below code, This will automatically manager creating Documents folder in container:
|
||||
|
||||
```swift
|
||||
import FilesProvider
|
||||
|
||||
let documentsProvider = CloudFileProvider(containerId: nil)
|
||||
```
|
||||
|
||||
|
||||
For remote file providers authentication may be necessary:
|
||||
|
||||
let credential = NSURLCredential(user: "user", password: "pass", persistence: NSURLCredentialPersistence.Permanent)
|
||||
let webdavProvider = WebDAVFileProvider(baseURL: NSURL(string: "http://www.example.com/dav")!, credential: credential)
|
||||
``` swift
|
||||
import FilesProvider
|
||||
|
||||
* In case you want to connect non-secure servers for WebDAV (http) in iOS 9+ / macOS 10.11+ you should disable App Transport Security (ATS) according to [this guide.](https://gist.github.com/mlynch/284699d676fe9ed0abfa)
|
||||
let credential = URLCredential(user: "user", password: "pass", persistence: .permanent)
|
||||
let webdavProvider = WebDAVFileProvider(baseURL: URL(string: "http://www.example.com/dav")!, credential: credential)
|
||||
```
|
||||
|
||||
* For Dropbox, 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.
|
||||
* 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 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 `absoluteURL()` 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)
|
||||
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
|
||||
|
||||
For updating User interface please consider using delegate method instead of completion handlers. Delegate methods are guaranteed to run in main thread to avoid bugs.
|
||||
|
||||
It's simply tree method which indicated whether the operation failed, succeed and how much of operation has been done (suitable for uploading and downloading operations).
|
||||
There's simply three method which indicated whether the operation failed, succeed and how much of operation has been done (suitable for uploading and downloading operations).
|
||||
|
||||
Your class should conforms `FileProviderDelegate` class:
|
||||
|
||||
override func viewDidLoad() {
|
||||
documentsProvider.delegate = self
|
||||
}
|
||||
```swift
|
||||
override func viewDidLoad() {
|
||||
documentsProvider.delegate = self as FileProviderDelegate
|
||||
}
|
||||
|
||||
func fileproviderSucceed(fileProvider: FileProviderOperations, operation: FileOperation) {
|
||||
switch operation {
|
||||
case .Copy(source: let source, destination: let dest):
|
||||
NSLog("\(source) copied to \(dest).")
|
||||
case .Remove(path: let path):
|
||||
NSLog("\(path) has been deleted.")
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func fileproviderFailed(fileProvider: FileProviderOperations, operation: FileOperation) {
|
||||
switch operation {
|
||||
case .Copy(source: let source, destination: let dest):
|
||||
NSLog("copy of \(source) failed.")
|
||||
case .Remove(path: let path):
|
||||
NSLog("\(path) can't be deleted.")
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func fileproviderProgress(fileProvider: FileProviderOperations, operation: FileOperation, progress: Float) {
|
||||
switch operation {
|
||||
case .Copy(source: let source, destination: let dest):
|
||||
NSLog("Copy\(source) to \(dest): \(progress * 100) completed.")
|
||||
default:
|
||||
break
|
||||
}
|
||||
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:
|
||||
print("\(operation.actionDescription) from \(operation.source!) to \(operation.destination) succeed")
|
||||
}
|
||||
}
|
||||
|
||||
**Note:** `fileproviderProgress()` delegate method is not called by `LocalFileProvider`.
|
||||
func fileproviderFailed(_ fileProvider: FileProviderOperations, operation: FileOperationType) {
|
||||
switch operation {
|
||||
case .copy(source: let source, destination: let dest):
|
||||
print("copy of \(source) failed.")
|
||||
case .remove:
|
||||
print("file can't be deleted.")
|
||||
default:
|
||||
print("\(operation.actionDescription) from \(operation.source!) to \(operation.destination) failed")
|
||||
}
|
||||
}
|
||||
|
||||
func fileproviderProgress(_ fileProvider: FileProviderOperations, operation: FileOperationType, progress: Float) {
|
||||
switch operation {
|
||||
case .copy(source: let source, destination: let dest):
|
||||
print("Copy\(source) to \(dest): \(progress * 100) completed.")
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** `fileproviderProgress()` delegate method is not called by `LocalFileProvider` currently.
|
||||
|
||||
It's recommended to use completion handlers for error handling or result processing.
|
||||
|
||||
@@ -142,11 +217,11 @@ It's recommended to use completion handlers for error handling or result process
|
||||
|
||||
You can also implement `FileOperationDelegate` protocol to control behaviour of file operation (copy, move/rename, remove and linking), and decide which files should be removed for example and which won't.
|
||||
|
||||
`fileProvider:shouldDoOperation:` method is called before doing a operation. You sould return `true` if you want to do operation or `false` if you want to stop that operation.
|
||||
`fileProvider(shouldDoOperation:)` method is called before doing a operation. You sould return `true` if you want to do operation or `false` if you want to stop that operation.
|
||||
|
||||
`fileProvider:shouldProceedAfterError:operation:` will be called if an error occured during file operations. Return `true` if you want to continue operation on next files or `false` if you want stop operation further. Default value is false if you don't implement delegate.
|
||||
`fileProvider(shouldProceedAfterError:, operation:)` will be called if an error occured during file operations. Return `true` if you want to continue operation on next files or `false` if you want stop operation further. Default value is false if you don't implement delegate.
|
||||
|
||||
**Note: these methods will be called for files in a directory and its subfolders recursively.**
|
||||
**Note: In `LocalFileProvider`, these methods will be called for files in a directory and its subfolders recursively.**
|
||||
|
||||
### Directory contents and file attributes
|
||||
|
||||
@@ -154,106 +229,252 @@ There is a `FileObject` class which holds file attributes like size and creation
|
||||
|
||||
For a single file:
|
||||
|
||||
documentsProvider.attributesOfItemAtPath(path: "/file.txt", completionHandler: {
|
||||
(attributes: LocalFileObject?, error: ErrorType?) -> Void} in
|
||||
if let attributes = attributes {
|
||||
print("File Size: \(attributes.size)")
|
||||
print("Creation Date: \(attributes.createdDate)")
|
||||
print("Modification Date: \(modifiedDate)")
|
||||
print("Is Read Only: \(isReadOnly)")
|
||||
}
|
||||
)
|
||||
```swift
|
||||
documentsProvider.attributesOfItem(path: "/file.txt", completionHandler: {
|
||||
attributes, error in
|
||||
if let attributes = attributes {
|
||||
print("File Size: \(attributes.size)")
|
||||
print("Creation Date: \(attributes.creationDate)")
|
||||
print("Modification Date: \(attributes.modifiedDate)")
|
||||
print("Is Read Only: \(attributes.isReadOnly)")
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
To get list of files in a directory:
|
||||
|
||||
documentsProvider.contentsOfDirectoryAtPath(path: "/", completionHandler: {
|
||||
(contents: [LocalFileObject], error: ErrorType?) -> Void} in
|
||||
for file in contents {
|
||||
print("Name: \(attributes.name)")
|
||||
print("Size: \(attributes.size)")
|
||||
print("Creation Date: \(attributes.createdDate)")
|
||||
print("Modification Date: \(modifiedDate)")
|
||||
}
|
||||
)
|
||||
```swift
|
||||
documentsProvider.contentsOfDirectory(path: "/", completionHandler: {
|
||||
contents, error in
|
||||
for file in contents {
|
||||
print("Name: \(file.name)")
|
||||
print("Size: \(file.size)")
|
||||
print("Creation Date: \(file.creationDate)")
|
||||
print("Modification Date: \(file.modifiedDate)")
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Change current directory
|
||||
To get size of strage and used/free space:
|
||||
|
||||
documentsProvider.currentPath = "/New Folder"
|
||||
// now path is ~/Documents/New Folder
|
||||
```swift
|
||||
func storageProperties(completionHandler: { total, used in
|
||||
print("Total Storage Space: \(total)")
|
||||
print("Used Space: \(used)")
|
||||
print("Free Space: \(total - used)")
|
||||
})
|
||||
```
|
||||
|
||||
You can then pass "" (empty string) to contentsOfDirectoryAtPath method to list files in current directory.
|
||||
* if this function is unavailable on provider or an error has been occurred, total space will be reported `-1` and used space `0`
|
||||
|
||||
### Creating File and Folders
|
||||
|
||||
Creating new directory:
|
||||
|
||||
documentsProvider.createFolder(folderName: "new folder", atPath: "/", completionHandler: nil)
|
||||
```swift
|
||||
documentsProvider.create(folder: "new folder", at: "/", completionHandler: { error in
|
||||
if let error = error {
|
||||
// Error handling here
|
||||
} else {
|
||||
// The operation succeed
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Creating new file from data stream:
|
||||
|
||||
let data = "hello world!".dataUsingEncoding(NSUTF8StringEncoding)
|
||||
let file = FileObject(name: "old.txt", createdDate: NSDate(), modifiedDate: NSDate(), isHidden: false, isReadOnly: true)
|
||||
documentsProvider.createFile(fileAttribs: file, atPath: "/", contents: data, completionHandler: nil)
|
||||
To create a file, use `writeContents(path:, content:, atomically:, completionHandler:)` method.
|
||||
|
||||
### Copy and Move/Rename Files
|
||||
|
||||
Copy file old.txt to new.txt in current path:
|
||||
|
||||
documentsProvider.copyItemAtPath(path: "new folder/old.txt", toPath: "new.txt", overwrite: false, completionHandler: nil)
|
||||
```swift
|
||||
documentsProvider.copyItem(path: "new folder/old.txt", to: "new.txt", overwrite: false, completionHandler: nil)
|
||||
```
|
||||
|
||||
Move file old.txt to new.txt in current path:
|
||||
|
||||
documentsProvider.moveItemAtPath(path: "new folder/old.txt", toPath: "new.txt", overwrite: false, completionHandler: nil)
|
||||
```swift
|
||||
documentsProvider.moveItem(path: "new folder/old.txt", to: "new.txt", overwrite: false, completionHandler: nil)
|
||||
```
|
||||
|
||||
**Note:** To have a consistent behavior, create intermediate directories first if necessary.
|
||||
|
||||
### Delete Files
|
||||
|
||||
documentsProvider.removeItemAtPath(path: "new.txt", completionHandler: nil)
|
||||
```swift
|
||||
documentsProvider.removeItem(path: "new.txt", completionHandler: nil)
|
||||
```
|
||||
|
||||
***Caution:*** This method will delete directories with all it's content recursively.
|
||||
### Fetching Contents of File
|
||||
|
||||
### Retrieve Content of File
|
||||
There is two method for this purpose, one of them loads entire file into `Data` and another can load a portion of file.
|
||||
|
||||
There is two method for this purpose, one of them loads entire file into NSData and another can load a portion of file.
|
||||
|
||||
documentsProvider.contentsAtPath(path: "old.txt", completionHandler: {
|
||||
(contents: NSData?, error: ErrorType?) -> Void
|
||||
if let contents = contents {
|
||||
print(String(data: contents, encoding: NSUTF8StringEncoding)) // "hello world!"
|
||||
}
|
||||
})
|
||||
```swift
|
||||
documentsProvider.contents(path: "old.txt", completionHandler: {
|
||||
contents, error in
|
||||
if let contents = contents {
|
||||
print(String(data: contents, encoding: .utf8)) // "hello world!"
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
If you want to retrieve a portion of file you should can `contentsAtPath` method with offset and length arguments. Please note first byte of file has offset: 0.
|
||||
If you want to retrieve a portion of file you can use `contents` method with offset and length arguments. Please note first byte of file has offset: 0.
|
||||
|
||||
documentsProvider.contentsAtPath(path: "old.txt", offset: 2, length: 5, completionHandler: {
|
||||
(contents: NSData?, error: ErrorType?) -> Void
|
||||
if let contents = contents {
|
||||
print(String(data: contents, encoding: NSUTF8StringEncoding)) // "llo w"
|
||||
}
|
||||
})
|
||||
```swift
|
||||
documentsProvider.contents(path: "old.txt", offset: 2, length: 5, completionHandler: {
|
||||
contents, error in
|
||||
if let contents = contents {
|
||||
print(String(data: contents, encoding: .utf8)) // "llo w"
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Write Data To Files
|
||||
|
||||
let data = "What's up Newyork!".dataUsingEncoding(NSUTF8StringEncoding)
|
||||
documentsProvider.writeContentsAtPath(path: "old.txt", contents data: data, atomically: true, completionHandler: nil)
|
||||
```swift
|
||||
let data = "What's up Newyork!".data(encoding: .utf8)
|
||||
documentsProvider.writeContents(path: "old.txt", content: data, atomically: true, completionHandler: nil)
|
||||
```
|
||||
|
||||
### Monitoring FIle Changes
|
||||
### Copying Files to and From Local Storage
|
||||
|
||||
There are two methods to download and upload files between provider's and local storage. These methods use `URLSessionDownloadTask` and `URLSessionUploadTask` classes and allows to use background session and provide progress via delegate.
|
||||
|
||||
To upload a file:
|
||||
|
||||
```swift
|
||||
let fileURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("image.jpg")
|
||||
documentsProvider.copyItem(localFile: fileURL, to: "/upload/image.jpg", overwrite: true, completionHandler: nil)
|
||||
```
|
||||
|
||||
To download a file:
|
||||
|
||||
```swift
|
||||
let fileURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("image.jpg")
|
||||
documentsProvider.copyItem(path: "/download/image.jpg", toLocalURL: fileURL, overwrite: true, completionHandler: nil)
|
||||
```
|
||||
|
||||
* It's safe only to assume these methods **won't** handle directories to upload/download recursively. If you need, you can list directories, create directories on target and copy files using these methods.
|
||||
* FTP provider allows developer to either use apple implemented `URLSessionDownloadTask` or custom implemented method based on stream task via `useAppleImplementation` property. FTP protocol is not supported by background session.
|
||||
|
||||
### Operation Progress
|
||||
|
||||
Creating/Copying/Deleting/Searching functions return a `(NS)Progress`. It provides operation type, progress and a `.cancel()` method which allows you to cancel operation in midst. You can check `cancellable` property to check either you can cancel operation via this object or not.
|
||||
|
||||
- **Note:** Progress reporting is not supported by native `(NS)FileManager` so `LocalFileProvider`.
|
||||
|
||||
### Undo Operations
|
||||
|
||||
Providers conform to `FileProviderUndoable` can perform undo for **some** operations like moving/renaming, copying and creating (file or folder). **Now, only `LocalFileProvider` supports this feature.** To implement:
|
||||
|
||||
```swift
|
||||
// To setup a new UndoManager:
|
||||
documentsProvider.setupUndoManager()
|
||||
// or if you have an UndoManager object already:
|
||||
documentsProvider.undoManager = self.undoManager
|
||||
|
||||
// e.g.: To undo last operation manually:
|
||||
documentsProvider.undoManager?.undo()
|
||||
```
|
||||
|
||||
You can also bind `UndoManager` object with view controller to use shake gesture and builtin undo support in iOS/macOS, add these code to your ViewController class like this sample code:
|
||||
|
||||
```swift
|
||||
class ViewController: UIViewController
|
||||
override var canBecomeFirstResponder: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override var undoManager: UndoManager? {
|
||||
return (provider as? FileProvideUndoable)?.undoManager
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
// Your code here
|
||||
UIApplication.shared.applicationSupportsShakeToEdit = true
|
||||
self.becomeFirstResponder()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
// Your code here
|
||||
UIApplication.shared.applicationSupportsShakeToEdit = false
|
||||
self.resignFirstResponder()
|
||||
}
|
||||
// The rest of your implementation
|
||||
}
|
||||
```
|
||||
|
||||
### File Coordination
|
||||
|
||||
`LocalFileProvider` and its descendents has a `isCoordinating` property. By setting this, provider will use `NSFileCoordinating` class to do all file operations. It's mandatory for iCloud, while recommended when using shared container or anywhere that simultaneous operations on a file/folder is common.
|
||||
|
||||
### Monitoring File Changes
|
||||
|
||||
You can monitor updates in some file system (Local and SMB2), there is three methods in supporting provider you can use to register a handler, to unregister and to check whether it's being monitored or not. It's useful to find out when new files added or removed from directory and update user interface. The handler will be dispatched to main threads to avoid UI bugs with a 0.25 sec delay.
|
||||
|
||||
documentsProvider.registerNotifcation(provider.currentPath)
|
||||
{
|
||||
// calling functions to update UI
|
||||
}
|
||||
```swift
|
||||
// to register a new notification handler
|
||||
documentsProvider.registerNotifcation(path: "/") {
|
||||
// calling functions to update UI
|
||||
}
|
||||
|
||||
// To discontinue monitoring folders:
|
||||
documentsProvider.unregisterNotifcation(provider.currentPath)
|
||||
// To discontinue monitoring folders:
|
||||
documentsProvider.unregisterNotifcation(path: "/")
|
||||
```
|
||||
|
||||
* **Please note** in LocalFileProvider it will also monitor changes in subfolders. This behaviour can varies according to file system specification.
|
||||
|
||||
### Thumbnail and meta-information
|
||||
|
||||
Providers which conform `ExtendedFileProvider` are able to generate thumbnail or provide file meta-information for images, media and pdf files.
|
||||
|
||||
Local, OneDrive and Dropbox providers support this functionality.
|
||||
|
||||
##### Thumbnails
|
||||
To check either file thumbnail is supported or not and fetch thumbnail, use (and modify) these example code:
|
||||
|
||||
```swift
|
||||
let path = "/newImage.jpg"
|
||||
let thumbSize = CGSize(width: 64, height: 64) // or nil which renders to default dimension of provider
|
||||
if documentsProvider.thumbnailOfFileSupported(path: path {
|
||||
documentsProvider.thumbnailOfFile(path: file.path, dimension: thumbSize, completionHandler: { (image, error) in
|
||||
// Interacting with UI must be placed in main thread
|
||||
DispatchQueue.main.async {
|
||||
self.previewImage.image = image
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* Please note it won't cache generated images. if you don't do it yourself, it may hit you app's performance.
|
||||
|
||||
##### Meta-informations
|
||||
|
||||
To get meta-information like image/video taken date, location, dimension, etc., use (and modify) these example code:
|
||||
|
||||
```swift
|
||||
if documentsProvider..propertiesOfFile(path: file.path, completionHandler: { (propertiesDictionary, keys, error) in
|
||||
for key in keys {
|
||||
print("\(key): \(propertiesDictionary[key])")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* **Bonus:** You can modify/extend Local provider generator by setting `LocalFileInformationGenerator` static variables and methods
|
||||
|
||||
## Contribute
|
||||
|
||||
We would love for you to contribute to **FileProvider**, check the `LICENSE` file for more info.
|
||||
|
||||
Things you may consider to help us:
|
||||
|
||||
- [ ] Implement request/response stack for `SMBClient`
|
||||
- [x] Implement Test-case (`XCTest`)
|
||||
- [ ] Add Sample project for iOS
|
||||
- [ ] Add Sample project for macOS
|
||||
|
||||
## Projects in use
|
||||
|
||||
* [EDM - Browse and Receive Files](https://itunes.apple.com/us/app/edm-browse-and-receive-files/id948397575?ls=1&mt=8)
|
||||
@@ -263,21 +484,35 @@ If you used this library in your project, you can open an issue to inform us.
|
||||
|
||||
## Meta
|
||||
|
||||
Amir-Abbas Mousavian – [@amosavian](https://twitter.com/amosavian)
|
||||
Amir-Abbas Mousavian – [@amosavian](https://twitter.com/amosavian)
|
||||
|
||||
Thanks to [Hootan Moradi](https://github.com/hoootan) for designing logo.
|
||||
|
||||
Distributed under the MIT license. See `LICENSE` for more information.
|
||||
|
||||
[https://github.com/yourname/github-link](https://github.com/dbader/)
|
||||
[https://github.com/amosavian/](https://github.com/amosavian/)
|
||||
|
||||
[swift-image]:https://img.shields.io/badge/swift-2.2%2C%202.3-green.svg
|
||||
[cocoapods]: https://cocoapods.org/pods/FilesProvider
|
||||
[cocoapods-old]: https://cocoapods.org/pods/FileProvider
|
||||
[swift-image]: https://img.shields.io/badge/swift-4.0-orange.svg
|
||||
[swift-url]: https://swift.org/
|
||||
[license-image]: https://img.shields.io/badge/License-MIT-blue.svg
|
||||
[platform-image]: https://img.shields.io/cocoapods/p/FilesProvider.svg
|
||||
[license-image]: https://img.shields.io/github/license/amosavian/FileProvider.svg
|
||||
[license-url]: LICENSE
|
||||
[codebeat-image]: https://codebeat.co/badges/7b359f48-78eb-4647-ab22-56262a827517
|
||||
[codebeat-url]: https://codebeat.co/projects/github-com-amosavian-fileprovider
|
||||
[travis-image]: https://travis-ci.org/amosavian/FileProvider.svg
|
||||
[travis-url]: https://travis-ci.org/amosavian/FileProvider
|
||||
[release-url]: https://github.com/amosavian/FileProvider/releases
|
||||
[release-image]: https://img.shields.io/github/release/amosavian/FileProvider.svg
|
||||
[carthage-image]: https://img.shields.io/badge/Carthage-compatible-4BC51D.svg
|
||||
[cocoapods-downloads-old]: https://img.shields.io/cocoapods/dt/FileProvider.svg
|
||||
[cocoapods-apps-old]: https://img.shields.io/cocoapods/at/FileProvider.svg
|
||||
[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/
|
||||
## Support on Beerpay
|
||||
Hey dude! Help me out for a couple of :beers:!
|
||||
|
||||
|
||||
<!---
|
||||
[travis-image]: https://img.shields.io/travis/dbader/node-datadog-metrics/master.svg?style=flat-square
|
||||
[travis-url]: https://travis-ci.org/dbader/node-datadog-metrics
|
||||
--->
|
||||
[](https://beerpay.io/amosavian/FileProvider) [](https://beerpay.io/amosavian/FileProvider?focus=wish)
|
||||
@@ -1,492 +0,0 @@
|
||||
//
|
||||
// AEXML.swift
|
||||
//
|
||||
// Copyright (c) 2014 Marko Tadić <tadija@me.com> http://tadija.net
|
||||
//
|
||||
// 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: - AEXMLElement
|
||||
|
||||
/**
|
||||
This is base class for holding XML structure.
|
||||
|
||||
You can access its structure by using subscript like this: `element["foo"]["bar"]` which would
|
||||
return `<bar></bar>` element from `<element><foo><bar></bar></foo></element>` XML as an `AEXMLElement` object.
|
||||
*/
|
||||
public class AEXMLElement {
|
||||
|
||||
/// A type representing an error value that can be inside `error` property.
|
||||
public enum Error: ErrorType {
|
||||
case ElementNotFound
|
||||
case RootElementMissing
|
||||
}
|
||||
|
||||
private struct Defaults {
|
||||
static let name = String()
|
||||
static let attributes = [String : String]()
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// Every `AEXMLElement` should have its parent element instead of `AEXMLDocument` which parent is `nil`.
|
||||
public private(set) weak var parent: AEXMLElement?
|
||||
|
||||
/// Child XML elements.
|
||||
public private(set) var children: [AEXMLElement] = [AEXMLElement]()
|
||||
|
||||
/// XML Element name (defaults to empty string).
|
||||
public var name: String
|
||||
|
||||
/// XML Element value.
|
||||
public var value: String?
|
||||
|
||||
/// XML Element attributes (defaults to empty dictionary).
|
||||
public var attributes: [String : String]
|
||||
|
||||
/// Error value (`nil` if there is no error).
|
||||
public var error: Error?
|
||||
|
||||
/// String representation of `value` property (if `value` is `nil` this is empty String).
|
||||
public var stringValue: String { return value ?? String() }
|
||||
|
||||
/// Boolean representation of `value` property (if `value` is "true" or 1 this is `True`, otherwise `False`).
|
||||
public var boolValue: Bool { return stringValue.lowercaseString == "true" || Int(stringValue) == 1 ? true : false }
|
||||
|
||||
/// Integer representation of `value` property (this is **0** if `value` can't be represented as Integer).
|
||||
public var intValue: Int { return Int(stringValue) ?? 0 }
|
||||
|
||||
/// Double representation of `value` property (this is **0.00** if `value` can't be represented as Double).
|
||||
public var doubleValue: Double { return (stringValue as NSString).doubleValue }
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
/**
|
||||
Designated initializer - all parameters are optional.
|
||||
|
||||
- parameter name: XML element name.
|
||||
- parameter value: XML element value
|
||||
- parameter attributes: XML element attributes
|
||||
|
||||
- returns: An initialized `AEXMLElement` object.
|
||||
*/
|
||||
public init(_ name: String? = nil, value: String? = nil, attributes: [String : String]? = nil) {
|
||||
self.name = name ?? Defaults.name
|
||||
self.value = value
|
||||
self.attributes = attributes ?? Defaults.attributes
|
||||
}
|
||||
|
||||
// MARK: XML Read
|
||||
|
||||
/// The first element with given name **(Empty element with error if not exists)**.
|
||||
public subscript(key: String) -> AEXMLElement {
|
||||
guard let
|
||||
first = children.filter({ $0.name == key }).first
|
||||
else {
|
||||
let errorElement = AEXMLElement(key)
|
||||
errorElement.error = Error.ElementNotFound
|
||||
return errorElement
|
||||
}
|
||||
return first
|
||||
}
|
||||
|
||||
/// Returns all of the elements with equal name as `self` **(nil if not exists)**.
|
||||
public var all: [AEXMLElement]? { return parent?.children.filter { $0.name == self.name } }
|
||||
|
||||
/// Returns the first element with equal name as `self` **(nil if not exists)**.
|
||||
public var first: AEXMLElement? { return all?.first }
|
||||
|
||||
/// Returns the last element with equal name as `self` **(nil if not exists)**.
|
||||
public var last: AEXMLElement? { return all?.last }
|
||||
|
||||
/// Returns number of all elements with equal name as `self`.
|
||||
public var count: Int { return all?.count ?? 0 }
|
||||
|
||||
private func allWithCondition(fulfillCondition: (element: AEXMLElement) -> Bool) -> [AEXMLElement]? {
|
||||
var found = [AEXMLElement]()
|
||||
if let elements = all {
|
||||
for element in elements {
|
||||
if fulfillCondition(element: element) {
|
||||
found.append(element)
|
||||
}
|
||||
}
|
||||
return found.count > 0 ? found : nil
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Returns all elements with given value.
|
||||
|
||||
- parameter value: XML element value.
|
||||
|
||||
- returns: Optional Array of found XML elements.
|
||||
*/
|
||||
public func allWithValue(value: String) -> [AEXMLElement]? {
|
||||
let found = allWithCondition { (element) -> Bool in
|
||||
return element.value == value
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
/**
|
||||
Returns all elements with given attributes.
|
||||
|
||||
- parameter attributes: Dictionary of Keys and Values of attributes.
|
||||
|
||||
- returns: Optional Array of found XML elements.
|
||||
*/
|
||||
public func allWithAttributes(attributes: [String : String]) -> [AEXMLElement]? {
|
||||
let found = allWithCondition { (element) -> Bool in
|
||||
var countAttributes = 0
|
||||
for (key, value) in attributes {
|
||||
if element.attributes[key] == value {
|
||||
countAttributes += 1
|
||||
}
|
||||
}
|
||||
return countAttributes == attributes.count
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
// MARK: XML Write
|
||||
|
||||
/**
|
||||
Adds child XML element to `self`.
|
||||
|
||||
- parameter child: Child XML element to add.
|
||||
|
||||
- returns: Child XML element with `self` as `parent`.
|
||||
*/
|
||||
public func addChild(child: AEXMLElement) -> AEXMLElement {
|
||||
child.parent = self
|
||||
children.append(child)
|
||||
return child
|
||||
}
|
||||
|
||||
/**
|
||||
Adds child XML element to `self`.
|
||||
|
||||
- parameter name: Child XML element name.
|
||||
- parameter value: Child XML element value.
|
||||
- parameter attributes: Child XML element attributes.
|
||||
|
||||
- returns: Child XML element with `self` as `parent`.
|
||||
*/
|
||||
public func addChild(name name: String, value: String? = nil, attributes: [String : String]? = nil) -> AEXMLElement {
|
||||
let child = AEXMLElement(name, value: value, attributes: attributes)
|
||||
return addChild(child)
|
||||
}
|
||||
|
||||
/// Removes `self` from `parent` XML element.
|
||||
public func removeFromParent() {
|
||||
parent?.removeChild(self)
|
||||
}
|
||||
|
||||
private func removeChild(child: AEXMLElement) {
|
||||
if let childIndex = children.indexOf({ $0 === child }) {
|
||||
children.removeAtIndex(childIndex)
|
||||
}
|
||||
}
|
||||
|
||||
private var parentsCount: Int {
|
||||
var count = 0
|
||||
var element = self
|
||||
while let parent = element.parent {
|
||||
count += 1
|
||||
element = parent
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
private func indentation(depth: Int) -> String {
|
||||
var count = depth
|
||||
var indent = String()
|
||||
|
||||
while count > 0 {
|
||||
indent += "\t"
|
||||
count -= 1
|
||||
}
|
||||
|
||||
return indent
|
||||
}
|
||||
|
||||
/// Complete hierarchy of `self` and `children` in **XML** escaped and formatted String
|
||||
public var xmlString: String {
|
||||
var xml = String()
|
||||
|
||||
// open element
|
||||
xml += indentation(parentsCount - 1)
|
||||
xml += "<\(name)"
|
||||
|
||||
if attributes.count > 0 {
|
||||
// insert attributes
|
||||
for (key, value) in attributes {
|
||||
xml += " \(key)=\"\(value.xmlEscaped)\""
|
||||
}
|
||||
}
|
||||
|
||||
if value == nil && children.count == 0 {
|
||||
// close element
|
||||
xml += " />"
|
||||
} else {
|
||||
if children.count > 0 {
|
||||
// add children
|
||||
xml += ">\n"
|
||||
for child in children {
|
||||
xml += "\(child.xmlString)\n"
|
||||
}
|
||||
// add indentation
|
||||
xml += indentation(parentsCount - 1)
|
||||
xml += "</\(name)>"
|
||||
} else {
|
||||
// insert string value and close element
|
||||
xml += ">\(stringValue.xmlEscaped)</\(name)>"
|
||||
}
|
||||
}
|
||||
|
||||
return xml
|
||||
}
|
||||
|
||||
/// Same as `xmlString` but without `\n` and `\t` characters
|
||||
public var xmlStringCompact: String {
|
||||
let chars = NSCharacterSet(charactersInString: "\n\t")
|
||||
return xmlString.componentsSeparatedByCharactersInSet(chars).joinWithSeparator("")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public extension String {
|
||||
|
||||
/// String representation of self with XML special characters escaped.
|
||||
public var xmlEscaped: String {
|
||||
// we need to make sure "&" is escaped first. Not doing this may break escaping the other characters
|
||||
var escaped = stringByReplacingOccurrencesOfString("&", withString: "&", options: .LiteralSearch)
|
||||
|
||||
// replace the other four special characters
|
||||
let escapeChars = ["<" : "<", ">" : ">", "'" : "'", "\"" : """]
|
||||
for (char, echar) in escapeChars {
|
||||
escaped = escaped.stringByReplacingOccurrencesOfString(char, withString: echar, options: .LiteralSearch)
|
||||
}
|
||||
|
||||
return escaped
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - AEXMLDocument
|
||||
|
||||
/**
|
||||
This class is inherited from `AEXMLElement` and has a few addons to represent **XML Document**.
|
||||
|
||||
XML Parsing is also done with this object.
|
||||
*/
|
||||
public class AEXMLDocument: AEXMLElement {
|
||||
|
||||
private struct Defaults {
|
||||
static let version = 1.0
|
||||
static let encoding = "utf-8"
|
||||
static let standalone = "no"
|
||||
static let documentName = "AEXMLDocument"
|
||||
}
|
||||
|
||||
/// Default options used by NSXMLParser
|
||||
public struct NSXMLParserOptions {
|
||||
public var shouldProcessNamespaces = false
|
||||
public var shouldReportNamespacePrefixes = false
|
||||
public var shouldResolveExternalEntities = false
|
||||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// This is only used for XML Document header (default value is 1.0).
|
||||
public let version: Double
|
||||
|
||||
/// This is only used for XML Document header (default value is "utf-8").
|
||||
public let encoding: String
|
||||
|
||||
/// This is only used for XML Document header (default value is "no").
|
||||
public let standalone: String
|
||||
|
||||
/// Options for NSXMLParser (default values are `false`)
|
||||
public let xmlParserOptions: NSXMLParserOptions
|
||||
|
||||
/// Root (the first child element) element of XML Document **(Empty element with error if not exists)**.
|
||||
public var root: AEXMLElement {
|
||||
guard let rootElement = children.first else {
|
||||
let errorElement = AEXMLElement()
|
||||
errorElement.error = Error.RootElementMissing
|
||||
return errorElement
|
||||
}
|
||||
return rootElement
|
||||
}
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
/**
|
||||
Designated initializer - Creates and returns XML Document object.
|
||||
|
||||
- parameter version: Version value for XML Document header (defaults to 1.0).
|
||||
- parameter encoding: Encoding value for XML Document header (defaults to "utf-8").
|
||||
- parameter standalone: Standalone value for XML Document header (defaults to "no").
|
||||
- parameter root: Root XML element for XML Document (defaults to `nil`).
|
||||
- parameter xmlParserOptions: Options for NSXMLParser (defaults to `false` for all).
|
||||
|
||||
- returns: An initialized XML Document object.
|
||||
*/
|
||||
public init(version: Double = Defaults.version,
|
||||
encoding: String = Defaults.encoding,
|
||||
standalone: String = Defaults.standalone,
|
||||
root: AEXMLElement? = nil,
|
||||
xmlParserOptions: NSXMLParserOptions = NSXMLParserOptions())
|
||||
{
|
||||
// set document properties
|
||||
self.version = version
|
||||
self.encoding = encoding
|
||||
self.standalone = standalone
|
||||
self.xmlParserOptions = xmlParserOptions
|
||||
|
||||
// init super with default name
|
||||
super.init(Defaults.documentName)
|
||||
|
||||
// document has no parent element
|
||||
parent = nil
|
||||
|
||||
// add root element to document (if any)
|
||||
if let rootElement = root {
|
||||
addChild(rootElement)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Convenience initializer - used for parsing XML data (by calling `loadXMLData:` internally).
|
||||
|
||||
- parameter version: Version value for XML Document header (defaults to 1.0).
|
||||
- parameter encoding: Encoding value for XML Document header (defaults to "utf-8").
|
||||
- parameter standalone: Standalone value for XML Document header (defaults to "no").
|
||||
- parameter xmlData: XML data to parse.
|
||||
- parameter xmlParserOptions: Options for NSXMLParser (defaults to `false` for all).
|
||||
|
||||
- returns: An initialized XML Document object containing parsed data. Throws error if data could not be parsed.
|
||||
*/
|
||||
public convenience init(version: Double = Defaults.version,
|
||||
encoding: String = Defaults.encoding,
|
||||
standalone: String = Defaults.standalone,
|
||||
xmlData: NSData,
|
||||
xmlParserOptions: NSXMLParserOptions = NSXMLParserOptions()) throws
|
||||
{
|
||||
self.init(version: version, encoding: encoding, standalone: standalone, xmlParserOptions: xmlParserOptions)
|
||||
try loadXMLData(xmlData)
|
||||
}
|
||||
|
||||
// MARK: Read XML
|
||||
|
||||
/**
|
||||
Creates instance of `AEXMLParser` (private class which is simple wrapper around `NSXMLParser`)
|
||||
and starts parsing the given XML data. Throws error if data could not be parsed.
|
||||
|
||||
- parameter data: XML which should be parsed.
|
||||
*/
|
||||
public func loadXMLData(data: NSData) throws {
|
||||
children.removeAll(keepCapacity: false)
|
||||
let xmlParser = AEXMLParser(xmlDocument: self, xmlData: data)
|
||||
try xmlParser.parse()
|
||||
}
|
||||
|
||||
// MARK: Override
|
||||
|
||||
/// Override of `xmlString` property of `AEXMLElement` - it just inserts XML Document header at the beginning.
|
||||
public override var xmlString: String {
|
||||
var xml = "<?xml version=\"\(version)\" encoding=\"\(encoding)\" standalone=\"\(standalone)\"?>\n"
|
||||
for child in children {
|
||||
xml += child.xmlString
|
||||
}
|
||||
return xml
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - AEXMLParser
|
||||
|
||||
private class AEXMLParser: NSObject, NSXMLParserDelegate {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
let xmlDocument: AEXMLDocument
|
||||
let xmlData: NSData
|
||||
|
||||
var currentParent: AEXMLElement?
|
||||
var currentElement: AEXMLElement?
|
||||
var currentValue = String()
|
||||
var parseError: NSError?
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
||||
init(xmlDocument: AEXMLDocument, xmlData: NSData) {
|
||||
self.xmlDocument = xmlDocument
|
||||
self.xmlData = xmlData
|
||||
currentParent = xmlDocument
|
||||
super.init()
|
||||
}
|
||||
|
||||
// MARK: XML Parse
|
||||
|
||||
func parse() throws {
|
||||
let parser = NSXMLParser(data: xmlData)
|
||||
parser.delegate = self
|
||||
|
||||
parser.shouldProcessNamespaces = xmlDocument.xmlParserOptions.shouldProcessNamespaces
|
||||
parser.shouldReportNamespacePrefixes = xmlDocument.xmlParserOptions.shouldReportNamespacePrefixes
|
||||
parser.shouldResolveExternalEntities = xmlDocument.xmlParserOptions.shouldResolveExternalEntities
|
||||
|
||||
let success = parser.parse()
|
||||
if !success {
|
||||
throw parseError ?? NSError(domain: "net.tadija.AEXML", code: 1, userInfo: nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: NSXMLParserDelegate
|
||||
|
||||
@objc func parser(parser: NSXMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) {
|
||||
currentValue = String()
|
||||
currentElement = currentParent?.addChild(name: elementName, attributes: attributeDict)
|
||||
currentParent = currentElement
|
||||
}
|
||||
|
||||
@objc func parser(parser: NSXMLParser, foundCharacters string: String) {
|
||||
currentValue += string
|
||||
let newValue = currentValue.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
|
||||
currentElement?.value = newValue == String() ? nil : newValue
|
||||
}
|
||||
|
||||
@objc func parser(parser: NSXMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
|
||||
currentParent = currentParent?.parent
|
||||
currentElement = nil
|
||||
}
|
||||
|
||||
@objc func parser(parser: NSXMLParser, parseErrorOccurred parseError: NSError) {
|
||||
self.parseError = parseError
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
//
|
||||
// Document.swift
|
||||
//
|
||||
// Copyright (c) 2014-2016 Marko Tadić <tadija@me.com> http://tadija.net
|
||||
//
|
||||
// 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
|
||||
|
||||
/**
|
||||
This class is inherited from `AEXMLElement` and has a few addons to represent **XML Document**.
|
||||
|
||||
XML Parsing is also done with this object.
|
||||
*/
|
||||
internal class AEXMLDocument: AEXMLElement {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
/// Root (the first child element) element of XML Document **(Empty element with error if not exists)**.
|
||||
open var root: AEXMLElement {
|
||||
guard let rootElement = children.first else {
|
||||
let errorElement = AEXMLElement(name: "Error")
|
||||
errorElement.error = AEXMLError.rootElementMissing
|
||||
return errorElement
|
||||
}
|
||||
return rootElement
|
||||
}
|
||||
|
||||
open let options: AEXMLOptions
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
/**
|
||||
Designated initializer - Creates and returns new XML Document object.
|
||||
|
||||
- parameter root: Root XML element for XML Document (defaults to `nil`).
|
||||
- parameter options: Options for XML Document header and parser settings (defaults to `AEXMLOptions()`).
|
||||
|
||||
- returns: Initialized XML Document object.
|
||||
*/
|
||||
public init(root: AEXMLElement? = nil, options: AEXMLOptions = AEXMLOptions()) {
|
||||
self.options = options
|
||||
|
||||
let documentName = String(describing: AEXMLDocument.self)
|
||||
super.init(name: documentName)
|
||||
|
||||
// document has no parent element
|
||||
parent = nil
|
||||
|
||||
// add root element to document (if any)
|
||||
if let rootElement = root {
|
||||
_ = addChild(rootElement)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Convenience initializer - used for parsing XML data (by calling `loadXMLData:` internally).
|
||||
|
||||
- parameter xmlData: XML data to parse.
|
||||
- parameter options: Options for XML Document header and parser settings (defaults to `AEXMLOptions()`).
|
||||
|
||||
- returns: Initialized XML Document object containing parsed data. Throws error if data could not be parsed.
|
||||
*/
|
||||
public convenience init(xml: Data, options: AEXMLOptions = AEXMLOptions()) throws {
|
||||
self.init(options: options)
|
||||
try loadXML(xml)
|
||||
}
|
||||
|
||||
/**
|
||||
Convenience initializer - used for parsing XML string (by calling `init(xmlData:options:)` internally).
|
||||
|
||||
- parameter xmlString: XML string to parse.
|
||||
- parameter encoding: String encoding for creating `Data` from `xmlString` (defaults to `String.Encoding.utf8`)
|
||||
- parameter options: Options for XML Document header and parser settings (defaults to `AEXMLOptions()`).
|
||||
|
||||
- returns: Initialized XML Document object containing parsed data. Throws error if data could not be parsed.
|
||||
*/
|
||||
public convenience init(xml: String,
|
||||
encoding: String.Encoding = String.Encoding.utf8,
|
||||
options: AEXMLOptions = AEXMLOptions()) throws
|
||||
{
|
||||
guard let data = xml.data(using: encoding) else { throw AEXMLError.parsingFailed }
|
||||
try self.init(xml: data, options: options)
|
||||
}
|
||||
|
||||
// MARK: - Parse XML
|
||||
|
||||
/**
|
||||
Creates instance of `AEXMLParser` (private class which is simple wrapper around `XMLParser`)
|
||||
and starts parsing the given XML data. Throws error if data could not be parsed.
|
||||
|
||||
- parameter data: XML which should be parsed.
|
||||
*/
|
||||
open func loadXML(_ data: Data) throws {
|
||||
children.removeAll(keepingCapacity: false)
|
||||
let xmlParser = AEXMLParser(document: self, data: data)
|
||||
try xmlParser.parse()
|
||||
}
|
||||
|
||||
// MARK: - Override
|
||||
|
||||
/// Override of `xml` property of `AEXMLElement` - it just inserts XML Document header at the beginning.
|
||||
open override var xml: String {
|
||||
var xml = "\(options.documentHeader.xmlString)\n"
|
||||
for child in children {
|
||||
xml += child.xml
|
||||
}
|
||||
return xml
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
//
|
||||
// Element.swift
|
||||
//
|
||||
// Copyright (c) 2014-2016 Marko Tadić <tadija@me.com> http://tadija.net
|
||||
//
|
||||
// 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
|
||||
|
||||
/**
|
||||
This is base class for holding XML structure.
|
||||
|
||||
You can access its structure by using subscript like this: `element["foo"]["bar"]` which would
|
||||
return `<bar></bar>` element from `<element><foo><bar></bar></foo></element>` XML as an `AEXMLElement` object.
|
||||
*/
|
||||
internal class AEXMLElement {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
/// Every `AEXMLElement` should have its parent element instead of `AEXMLDocument` which parent is `nil`.
|
||||
open internal(set) weak var parent: AEXMLElement?
|
||||
|
||||
/// Child XML elements.
|
||||
open internal(set) var children = [AEXMLElement]()
|
||||
|
||||
/// XML Element name.
|
||||
open var name: String
|
||||
|
||||
/// XML Element value.
|
||||
open var value: String?
|
||||
|
||||
/// XML Element attributes.
|
||||
open var attributes: [String : String]
|
||||
|
||||
/// Error value (`nil` if there is no error).
|
||||
open var error: AEXMLError?
|
||||
|
||||
/// String representation of `value` property (if `value` is `nil` this is empty String).
|
||||
open var string: String { return value ?? String() }
|
||||
|
||||
/// Boolean representation of `value` property (if `value` is "true" or 1 this is `True`, otherwise `False`).
|
||||
open var bool: Bool { return string.lowercased() == "true" || Int(string) == 1 ? true : false }
|
||||
|
||||
/// Integer representation of `value` property (this is **0** if `value` can't be represented as Integer).
|
||||
open var int: Int { return Int(string) ?? 0 }
|
||||
|
||||
/// Double representation of `value` property (this is **0.00** if `value` can't be represented as Double).
|
||||
open var double: Double { return Double(string) ?? 0.00 }
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
/**
|
||||
Designated initializer - all parameters are optional.
|
||||
|
||||
- parameter name: XML element name.
|
||||
- parameter value: XML element value (defaults to `nil`).
|
||||
- parameter attributes: XML element attributes (defaults to empty dictionary).
|
||||
|
||||
- returns: An initialized `AEXMLElement` object.
|
||||
*/
|
||||
public init(name: String, value: String? = nil, attributes: [String : String] = [String : String]()) {
|
||||
self.name = name
|
||||
self.value = value
|
||||
self.attributes = attributes
|
||||
}
|
||||
|
||||
// MARK: - XML Read
|
||||
|
||||
/// The first element with given name **(Empty element with error if not exists)**.
|
||||
open subscript(key: String) -> AEXMLElement {
|
||||
guard let
|
||||
first = children.first(where: { $0.name == key })
|
||||
else {
|
||||
let errorElement = AEXMLElement(name: key)
|
||||
errorElement.error = AEXMLError.elementNotFound
|
||||
return errorElement
|
||||
}
|
||||
return first
|
||||
}
|
||||
|
||||
/// Returns all of the elements with equal name as `self` **(nil if not exists)**.
|
||||
open var all: [AEXMLElement]? { return parent?.children.filter { $0.name == self.name } }
|
||||
|
||||
/// Returns the first element with equal name as `self` **(nil if not exists)**.
|
||||
open var first: AEXMLElement? { return all?.first }
|
||||
|
||||
/// Returns the last element with equal name as `self` **(nil if not exists)**.
|
||||
open var last: AEXMLElement? { return all?.last }
|
||||
|
||||
/// Returns number of all elements with equal name as `self`.
|
||||
open var count: Int { return all?.count ?? 0 }
|
||||
|
||||
fileprivate func filter(withCondition condition: (AEXMLElement) -> Bool) -> [AEXMLElement]? {
|
||||
guard let elements = all else { return nil }
|
||||
|
||||
var found = [AEXMLElement]()
|
||||
for element in elements {
|
||||
if condition(element) {
|
||||
found.append(element)
|
||||
}
|
||||
}
|
||||
|
||||
return found.count > 0 ? found : nil
|
||||
}
|
||||
|
||||
/**
|
||||
Returns all elements with given value.
|
||||
|
||||
- parameter value: XML element value.
|
||||
|
||||
- returns: Optional Array of found XML elements.
|
||||
*/
|
||||
open func all(withValue value: String) -> [AEXMLElement]? {
|
||||
let found = filter { (element) -> Bool in
|
||||
return element.value == value
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
/**
|
||||
Returns all elements with given attributes.
|
||||
|
||||
- parameter attributes: Dictionary of Keys and Values of attributes.
|
||||
|
||||
- returns: Optional Array of found XML elements.
|
||||
*/
|
||||
open func all(withAttributes attributes: [String : String]) -> [AEXMLElement]? {
|
||||
let found = filter { (element) -> Bool in
|
||||
var countAttributes = 0
|
||||
for (key, value) in attributes {
|
||||
if element.attributes[key] == value {
|
||||
countAttributes += 1
|
||||
}
|
||||
}
|
||||
return countAttributes == attributes.count
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
// MARK: - XML Write
|
||||
|
||||
/**
|
||||
Adds child XML element to `self`.
|
||||
|
||||
- parameter child: Child XML element to add.
|
||||
|
||||
- returns: Child XML element with `self` as `parent`.
|
||||
*/
|
||||
@discardableResult open func addChild(_ child: AEXMLElement) -> AEXMLElement {
|
||||
child.parent = self
|
||||
children.append(child)
|
||||
return child
|
||||
}
|
||||
|
||||
/**
|
||||
Adds child XML element to `self`.
|
||||
|
||||
- parameter name: Child XML element name.
|
||||
- parameter value: Child XML element value (defaults to `nil`).
|
||||
- parameter attributes: Child XML element attributes (defaults to empty dictionary).
|
||||
|
||||
- returns: Child XML element with `self` as `parent`.
|
||||
*/
|
||||
@discardableResult open func addChild(name: String,
|
||||
value: String? = nil,
|
||||
attributes: [String : String] = [String : String]()) -> AEXMLElement
|
||||
{
|
||||
let child = AEXMLElement(name: name, value: value, attributes: attributes)
|
||||
return addChild(child)
|
||||
}
|
||||
|
||||
/// Removes `self` from `parent` XML element.
|
||||
open func removeFromParent() {
|
||||
parent?.removeChild(self)
|
||||
}
|
||||
|
||||
fileprivate func removeChild(_ child: AEXMLElement) {
|
||||
if let childIndex = children.index(where: { $0 === child }) {
|
||||
children.remove(at: childIndex)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var parentsCount: Int {
|
||||
var count = 0
|
||||
var element = self
|
||||
|
||||
while let parent = element.parent {
|
||||
count += 1
|
||||
element = parent
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
fileprivate func indent(withDepth depth: Int) -> String {
|
||||
var count = depth
|
||||
var indent = String()
|
||||
|
||||
while count > 0 {
|
||||
indent += "\t"
|
||||
count -= 1
|
||||
}
|
||||
|
||||
return indent
|
||||
}
|
||||
|
||||
/// Complete hierarchy of `self` and `children` in **XML** escaped and formatted String
|
||||
open var xml: String {
|
||||
var xml = String()
|
||||
|
||||
// open element
|
||||
xml += indent(withDepth: parentsCount - 1)
|
||||
xml += "<\(name)"
|
||||
|
||||
if attributes.count > 0 {
|
||||
// insert attributes
|
||||
for (key, value) in attributes {
|
||||
xml += " \(key)=\"\(value.xmlEscaped)\""
|
||||
}
|
||||
}
|
||||
|
||||
if value == nil && children.count == 0 {
|
||||
// close element
|
||||
xml += " />"
|
||||
} else {
|
||||
if children.count > 0 {
|
||||
// add children
|
||||
xml += ">\n"
|
||||
for child in children {
|
||||
xml += "\(child.xml)\n"
|
||||
}
|
||||
// add indentation
|
||||
xml += indent(withDepth: parentsCount - 1)
|
||||
xml += "</\(name)>"
|
||||
} else {
|
||||
// insert string value and close element
|
||||
xml += ">\(string.xmlEscaped)</\(name)>"
|
||||
}
|
||||
}
|
||||
|
||||
return xml
|
||||
}
|
||||
|
||||
/// Same as `xmlString` but without `\n` and `\t` characters
|
||||
open var xmlCompact: String {
|
||||
let chars = CharacterSet(charactersIn: "\n\t")
|
||||
return xml.components(separatedBy: chars).joined(separator: "")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public extension String {
|
||||
|
||||
/// String representation of self with XML special characters escaped.
|
||||
public var xmlEscaped: String {
|
||||
// we need to make sure "&" is escaped first. Not doing this may break escaping the other characters
|
||||
var escaped = replacingOccurrences(of: "&", with: "&", options: .literal)
|
||||
|
||||
// replace the other four special characters
|
||||
let escapeChars = ["<" : "<", ">" : ">", "'" : "'", "\"" : """]
|
||||
for (char, echar) in escapeChars {
|
||||
escaped = escaped.replacingOccurrences(of: char, with: echar, options: .literal)
|
||||
}
|
||||
|
||||
return escaped
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// Error.swift
|
||||
//
|
||||
// Copyright (c) 2014-2016 Marko Tadić <tadija@me.com> http://tadija.net
|
||||
//
|
||||
// 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
|
||||
|
||||
/// A type representing error value that can be thrown or inside `error` property of `AEXMLElement`.
|
||||
internal enum AEXMLError: Error {
|
||||
/// This will be inside `error` property of `AEXMLElement` when subscript is used for not-existing element.
|
||||
case elementNotFound
|
||||
|
||||
/// This will be inside `error` property of `AEXMLDocument` when there is no root element.
|
||||
case rootElementMissing
|
||||
|
||||
/// `AEXMLDocument` can throw this error on `init` or `loadXMLData` if parsing with `XMLParser` was not successful.
|
||||
case parsingFailed
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2014-2017 Marko Tadić <tadija@me.com> http://tadija.net
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// Options.swift
|
||||
// AEXML
|
||||
//
|
||||
// Created by Marko Tadic on 9/10/16.
|
||||
// Copyright © 2016 AE. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Options used in `AEXMLDocument`
|
||||
internal struct AEXMLOptions {
|
||||
|
||||
/// Values used in XML Document header
|
||||
public struct DocumentHeader {
|
||||
/// Version value for XML Document header (defaults to 1.0).
|
||||
public var version = 1.0
|
||||
|
||||
/// Encoding value for XML Document header (defaults to "utf-8").
|
||||
public var encoding = "utf-8"
|
||||
|
||||
/// Standalone value for XML Document header (defaults to "no").
|
||||
public var standalone = "no"
|
||||
|
||||
/// XML Document header
|
||||
public var xmlString: String {
|
||||
return "<?xml version=\"\(version)\" encoding=\"\(encoding)\" standalone=\"\(standalone)\"?>"
|
||||
}
|
||||
}
|
||||
|
||||
/// Settings used by `Foundation.XMLParser`
|
||||
public struct ParserSettings {
|
||||
/// Parser reports the namespaces and qualified names of elements. (defaults to `false`)
|
||||
public var shouldProcessNamespaces = false
|
||||
|
||||
/// Parser reports the prefixes indicating the scope of namespace declarations. (defaults to `false`)
|
||||
public var shouldReportNamespacePrefixes = false
|
||||
|
||||
/// Parser reports declarations of external entities. (defaults to `false`)
|
||||
public var shouldResolveExternalEntities = false
|
||||
}
|
||||
|
||||
/// Values used in XML Document header (defaults to `DocumentHeader()`)
|
||||
public var documentHeader = DocumentHeader()
|
||||
|
||||
/// Settings used by `Foundation.XMLParser` (defaults to `ParserSettings()`)
|
||||
public var parserSettings = ParserSettings()
|
||||
|
||||
/// Designated initializer - Creates and returns default `AEXMLOptions`.
|
||||
public init() {}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
//
|
||||
// Parser.swift
|
||||
//
|
||||
// Copyright (c) 2014-2016 Marko Tadić <tadija@me.com> http://tadija.net
|
||||
//
|
||||
// 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
|
||||
|
||||
/// Simple wrapper around `Foundation.XMLParser`.
|
||||
internal class AEXMLParser: NSObject, XMLParserDelegate {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
let document: AEXMLDocument
|
||||
let data: Data
|
||||
|
||||
var currentParent: AEXMLElement?
|
||||
var currentElement: AEXMLElement?
|
||||
var currentValue = String()
|
||||
|
||||
var parseError: Error?
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
init(document: AEXMLDocument, data: Data) {
|
||||
self.document = document
|
||||
self.data = data
|
||||
currentParent = document
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
// MARK: - API
|
||||
|
||||
func parse() throws {
|
||||
let parser = XMLParser(data: data)
|
||||
parser.delegate = self
|
||||
|
||||
parser.shouldProcessNamespaces = document.options.parserSettings.shouldProcessNamespaces
|
||||
parser.shouldReportNamespacePrefixes = document.options.parserSettings.shouldReportNamespacePrefixes
|
||||
parser.shouldResolveExternalEntities = document.options.parserSettings.shouldResolveExternalEntities
|
||||
|
||||
let success = parser.parse()
|
||||
|
||||
if !success {
|
||||
guard let error = parseError else { throw AEXMLError.parsingFailed }
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - XMLParserDelegate
|
||||
|
||||
@objc func parser(_ parser: XMLParser,
|
||||
didStartElement elementName: String,
|
||||
namespaceURI: String?,
|
||||
qualifiedName qName: String?,
|
||||
attributes attributeDict: [String : String])
|
||||
{
|
||||
currentValue = String()
|
||||
currentElement = currentParent?.addChild(name: elementName, attributes: attributeDict)
|
||||
currentParent = currentElement
|
||||
}
|
||||
|
||||
@objc func parser(_ parser: XMLParser, foundCharacters string: String) {
|
||||
currentValue += string
|
||||
let newValue = currentValue.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
||||
currentElement?.value = newValue == String() ? nil : newValue
|
||||
}
|
||||
|
||||
@objc func parser(_ parser: XMLParser,
|
||||
didEndElement elementName: String,
|
||||
namespaceURI: String?,
|
||||
qualifiedName qName: String?)
|
||||
{
|
||||
currentParent = currentParent?.parent
|
||||
currentElement = nil
|
||||
}
|
||||
|
||||
@objc func parser(_ parser: XMLParser, parseErrorOccurred parseError: Error) {
|
||||
self.parseError = parseError
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,794 @@
|
||||
//
|
||||
// CloudFileProvider.swift
|
||||
// FileProvider
|
||||
//
|
||||
// Created by Amir Abbas Mousavian.
|
||||
// Copyright © 2017 Mousavian. Distributed under MIT license.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
Allows accessing to iCloud Drive stored files. Determine scope when initializing, to either access
|
||||
to public documents folder or files stored as data.
|
||||
|
||||
To setup a functional iCloud container, please
|
||||
[read this page](https://medium.com/ios-os-x-development/icloud-drive-documents-1a46b5706fe1).
|
||||
*/
|
||||
open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
|
||||
/// An string to identify type of provider.
|
||||
open override class var type: String { return "iCloudDrive" }
|
||||
|
||||
/// Forces file operations to use `NSFileCoordinating`,
|
||||
/// Actually this is readonly, and value is always true.
|
||||
override open var isCoorinating: Bool {
|
||||
get {
|
||||
return true
|
||||
}
|
||||
set {
|
||||
assert(newValue, "CloudFileProvider.isCoorinating can't be set to false")
|
||||
}
|
||||
}
|
||||
|
||||
/// The fully-qualified container identifier for an iCloud container directory.
|
||||
open fileprivate(set) var containerId: String?
|
||||
|
||||
/// Scope of container, indicates user can manipulate data/files or not.
|
||||
open fileprivate(set) var scope: UbiquitousScope
|
||||
|
||||
/// Set this property to ignore initiations asserting to be on secondary thread
|
||||
static open var asserting: Bool = true
|
||||
|
||||
/**
|
||||
Initializes the provider for the iCloud container associated with the specified identifier and
|
||||
establishes access to that container.
|
||||
|
||||
- Important: Do not call this method from your app’s main thread. Because this method might take a nontrivial amount of time to set up iCloud and return the requested URL, you should always call it from a secondary thread.
|
||||
|
||||
- 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 `<TEAMID>.<CONTAINER>`, where `<TEAMID>` is your development team ID and `<CONTAINER>` 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.
|
||||
*/
|
||||
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.")
|
||||
guard FileManager.default.ubiquityIdentityToken != nil else {
|
||||
return nil
|
||||
}
|
||||
guard let ubiquityURL = FileManager.default.url(forUbiquityContainerIdentifier: containerId) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let baseURL: URL
|
||||
if scope == .documents {
|
||||
baseURL = ubiquityURL.appendingPathComponent("Documents/")
|
||||
} else {
|
||||
baseURL = ubiquityURL
|
||||
}
|
||||
|
||||
self.init(baseURL: baseURL)
|
||||
self.containerId = containerId
|
||||
self.scope = scope
|
||||
|
||||
// To prepare FileManager objects?!
|
||||
fileManager.url(forUbiquityContainerIdentifier: containerId)
|
||||
opFileManager.url(forUbiquityContainerIdentifier: containerId)
|
||||
|
||||
try? fileManager.createDirectory(at: baseURL, withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
public override init(baseURL: URL) {
|
||||
self.scope = .data
|
||||
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
|
||||
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(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(of: NSURL.self, forKey: "baseURL") as URL? {
|
||||
self.init(baseURL: baseURL)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
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")
|
||||
aCoder.encode(self.scope.rawValue, forKey: "scope")
|
||||
}
|
||||
|
||||
open override func copy(with zone: NSZone? = nil) -> Any {
|
||||
let copy = CloudFileProvider(containerId: self.containerId, scope: self.scope)
|
||||
copy?.delegate = self.delegate
|
||||
copy?.fileOperationDelegate = self.fileOperationDelegate
|
||||
return copy as Any
|
||||
}
|
||||
|
||||
/**
|
||||
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) {
|
||||
// FIXME: create runloop for dispatch_queue, start query on it
|
||||
let query = NSPredicate(format: "TRUEPREDICATE")
|
||||
_ = searchFiles(path: path, recursive: false, query: query, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
/// Please don't rely this function to get iCloud drive total and remaining capacity
|
||||
/// - Important: iCloud Storage size and free space is unavailable, it returns local space
|
||||
open override func storageProperties(completionHandler: @escaping (VolumeObject?) -> Void) {
|
||||
super.storageProperties(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) {
|
||||
let query = NSPredicate(format: "%K LIKE[CD] %@", NSMetadataItemPathKey, path)
|
||||
_ = searchFiles(path: path, recursive: false, query: query, completionHandler: { (files, error) in
|
||||
completionHandler(files.first, error)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
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 progress = Progress(totalUnitCount: -1)
|
||||
|
||||
let pathURL = self.url(of: path)
|
||||
progress.setUserInfoObject(pathURL, forKey: .fileURLKey)
|
||||
let mdquery = NSMetadataQuery()
|
||||
mdquery.predicate = NSPredicate(format: "(%K BEGINSWITH[CD] %@) && (\(updateQueryTypeKeys(query).predicateFormat))", NSMetadataItemPathKey, pathURL.path)
|
||||
mdquery.valueListAttributes = [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]
|
||||
mdquery.searchScopes = [self.scope.rawValue]
|
||||
|
||||
var lastReportedCount = 0
|
||||
|
||||
progress.cancellationHandler = { [weak mdquery] in
|
||||
mdquery?.stop()
|
||||
}
|
||||
|
||||
var updateObserver: NSObjectProtocol?
|
||||
if let foundItemHandler = foundItemHandler {
|
||||
// FIXME: Remove this section as it won't work as expected on iCloud
|
||||
updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryGatheringProgress, object: mdquery, queue: nil, using: { (notification) in
|
||||
mdquery.disableUpdates()
|
||||
|
||||
for index in lastReportedCount..<mdquery.resultCount {
|
||||
guard let attribs = (mdquery.result(at: index) as? NSMetadataItem)?.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
|
||||
continue
|
||||
}
|
||||
|
||||
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardized, recursive || url.deletingLastPathComponent().path.trimmingCharacters(in: pathTrimSet) == pathURL.path.trimmingCharacters(in: pathTrimSet) else {
|
||||
continue
|
||||
}
|
||||
|
||||
if let file = self.mapFileObject(attributes: attribs), query.evaluate(with: file.mapPredicate()) {
|
||||
foundItemHandler(file)
|
||||
}
|
||||
}
|
||||
lastReportedCount = mdquery.resultCount
|
||||
progress.totalUnitCount = Int64(lastReportedCount)
|
||||
|
||||
mdquery.enableUpdates()
|
||||
})
|
||||
}
|
||||
|
||||
var finishObserver: NSObjectProtocol?
|
||||
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: mdquery, queue: nil, using: { (notification) in
|
||||
defer {
|
||||
mdquery.stop()
|
||||
finishObserver.flatMap(NotificationCenter.default.removeObserver)
|
||||
finishObserver = nil
|
||||
updateObserver.flatMap(NotificationCenter.default.removeObserver)
|
||||
updateObserver = nil
|
||||
}
|
||||
|
||||
guard let results = mdquery.results as? [NSMetadataItem] else {
|
||||
return
|
||||
}
|
||||
|
||||
mdquery.disableUpdates()
|
||||
|
||||
var contents = [FileObject]()
|
||||
for result in results {
|
||||
guard let attribs = result.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
|
||||
continue
|
||||
}
|
||||
|
||||
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardized, recursive || url.deletingLastPathComponent().path.trimmingCharacters(in: pathTrimSet) == pathURL.path.trimmingCharacters(in: pathTrimSet) else {
|
||||
continue
|
||||
}
|
||||
|
||||
if let file = self.mapFileObject(attributes: attribs), query.evaluate(with: file.mapPredicate()) {
|
||||
contents.append(file)
|
||||
}
|
||||
}
|
||||
progress.completedUnitCount = Int64(contents.count)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(contents, nil)
|
||||
}
|
||||
})
|
||||
|
||||
DispatchQueue.main.async {
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
if !mdquery.start() {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler([], self.cocoaError(path, code: .fileReadNoPermission))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return progress
|
||||
}
|
||||
|
||||
open override func isReachable(completionHandler: @escaping (_ success: Bool, _ error: Error?) -> Void) {
|
||||
dispatch_queue.async {
|
||||
completionHandler(self.fileManager.ubiquityIdentityToken != nil, nil)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Removes the file or directory at the specified path.
|
||||
|
||||
- Important: Due to a bug (race condition?) in Apple API, it takes about 3-5 seconds to update containing folder
|
||||
list and triggering notification registered for directory while completion handler will run almost immediately.
|
||||
It's your responsibility to workaourd this bug/feature and mark file as deleted in your software.
|
||||
|
||||
- Parameters:
|
||||
- path: file or directory path.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: A `Progress` object to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
open override func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
return super.removeItem(path: path, 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.
|
||||
|
||||
- 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: A `Progress` object to get progress or cancel progress.
|
||||
*/
|
||||
@discardableResult
|
||||
open override func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
// TODO: Make use of overwrite parameter
|
||||
let operation = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
|
||||
let progress = Progress(totalUnitCount: -1)
|
||||
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
||||
progress.kind = .file
|
||||
progress.isCancellable = false
|
||||
progress.setUserInfoObject(localFile, forKey: .fileURLKey)
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
||||
|
||||
let moveblock: () -> Void = {
|
||||
let tempFolder: URL
|
||||
if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) {
|
||||
tempFolder = FileManager.default.temporaryDirectory
|
||||
} else {
|
||||
tempFolder = URL(fileURLWithPath: NSTemporaryDirectory())
|
||||
}
|
||||
let tmpFile = tempFolder.appendingPathComponent(UUID().uuidString)
|
||||
|
||||
do {
|
||||
progress.totalUnitCount = localFile.fileSize
|
||||
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)
|
||||
completionHandler?(nil)
|
||||
self.delegateNotify(operation)
|
||||
} catch {
|
||||
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 = self.cocoaError(dest.path, code: .fileWriteFileExists)
|
||||
dispatch_queue.async {
|
||||
completionHandler?(e)
|
||||
}
|
||||
self.delegateNotify(operation, error: e)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
self.operation_queue.addOperation(moveblock)
|
||||
}
|
||||
|
||||
return progress
|
||||
}
|
||||
|
||||
/**
|
||||
Download a file from `path` to designated local file url asynchronously.
|
||||
Method will fail if destination is not a local url with `file://` scheme.
|
||||
|
||||
- Parameters:
|
||||
- path: original file or directory path.
|
||||
- toLocalURL: destination local url of file, including file/directory name.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: A `Progress` object to get progress or cancel progress.
|
||||
*/
|
||||
@discardableResult
|
||||
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)
|
||||
do {
|
||||
try self.opFileManager.startDownloadingUbiquitousItem(at: self.url(of: path))
|
||||
} catch {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
return nil
|
||||
}
|
||||
return progress
|
||||
}
|
||||
|
||||
/**
|
||||
Retreives a `Data` object with the 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.
|
||||
- completionHandler: a closure with result of file contents or error.
|
||||
`contents`: contents of file in a `Data` object.
|
||||
`error`: Error returned by system.
|
||||
- Returns: A `Progress` object to get progress or cancel progress.
|
||||
*/
|
||||
@discardableResult
|
||||
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)
|
||||
return 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.
|
||||
- completionHandler: a closure with result of file contents or error.
|
||||
`contents`: contents of file in a `Data` object.
|
||||
`error`: Error returned by system.
|
||||
- Returns: A `Progress` object to get progress or cancel progress.
|
||||
*/
|
||||
@discardableResult
|
||||
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)
|
||||
return progress
|
||||
}
|
||||
|
||||
/**
|
||||
Write the contents of the `Data` to a location asynchronously.
|
||||
|
||||
- Parameters:
|
||||
- path: Path of target file.
|
||||
- contents: Data to be written into file.
|
||||
- overwrite: Destination file should be overwritten if file is already exists. Default is `false`.
|
||||
- atomically: data will be written to a temporary file before writing to final location. Default is `false`.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: A `Progress` object 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.fetch(path: path)
|
||||
let progress = Progress(totalUnitCount: -1)
|
||||
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
||||
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)
|
||||
_ = super.writeContents(path: path, contents: data, atomically: atomically, overwrite: overwrite, completionHandler: completionHandler)
|
||||
return progress
|
||||
}
|
||||
|
||||
fileprivate var monitors = [String: (NSMetadataQuery, NSObjectProtocol)]()
|
||||
|
||||
/**
|
||||
Starts monitoring a path and its subpaths, including files and folders, for any change,
|
||||
including copy, move/rename, content changes, etc.
|
||||
To avoid thread congestion, `evetHandler` will be triggered with 0.2 seconds interval,
|
||||
and has a 0.25 second delay, to ensure it's called after updates.
|
||||
|
||||
- Note: this functionality is available only in `LocalFileProvider` and `CloudFileProvider`.
|
||||
- Note: `eventHandler` is not called on main thread, for updating UI. dispatch routine to main thread.
|
||||
- Important: `eventHandler` may be called if file is changed in recursive subpaths of registered path.
|
||||
This may cause negative impact on performance if a root path is being monitored.
|
||||
|
||||
- Parameters:
|
||||
- path: path of directory.
|
||||
- eventHandler: Closure executed after change, on a secondary thread.
|
||||
*/
|
||||
open override func registerNotifcation(path: String, eventHandler: @escaping (() -> Void)) {
|
||||
self.unregisterNotifcation(path: path)
|
||||
let pathURL = self.url(of: path)
|
||||
let query = NSMetadataQuery()
|
||||
query.predicate = NSPredicate(format: "(%K BEGINSWITH %@)", NSMetadataItemPathKey, pathURL.path)
|
||||
query.valueListAttributes = []
|
||||
query.searchScopes = [self.scope.rawValue]
|
||||
|
||||
let updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidUpdate, object: query, queue: nil, using: { (notification) in
|
||||
|
||||
query.disableUpdates()
|
||||
|
||||
eventHandler()
|
||||
|
||||
query.enableUpdates()
|
||||
})
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if query.start() {
|
||||
self.monitors[path] = (query, updateObserver)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stops monitoring the path.
|
||||
///
|
||||
/// - Parameter path: path of directory.
|
||||
open override func unregisterNotifcation(path: String) {
|
||||
guard let (query, observer) = monitors[path] else {
|
||||
return
|
||||
}
|
||||
query.disableUpdates()
|
||||
query.stop()
|
||||
NotificationCenter.default.removeObserver(observer)
|
||||
monitors.removeValue(forKey: path)
|
||||
}
|
||||
|
||||
/// Investigate either the path is registered for change notification or not.
|
||||
///
|
||||
/// - Parameter path: path of directory.
|
||||
/// - Returns: Directory is being monitored or not.
|
||||
open override func isRegisteredForNotification(path: String) -> Bool {
|
||||
return monitors[path] != nil
|
||||
}
|
||||
|
||||
fileprivate func updateQueryTypeKeys(_ queryComponent: NSPredicate) -> NSPredicate {
|
||||
let mapDict: [String: String] = ["url": NSMetadataItemURLKey, "name": NSMetadataItemFSNameKey, "path": NSMetadataItemPathKey, "filesize": NSMetadataItemFSSizeKey, "modifiedDate": NSMetadataItemFSContentChangeDateKey, "creationDate": NSMetadataItemFSCreationDateKey, "contentType": NSMetadataItemContentTypeKey]
|
||||
|
||||
if let cQuery = queryComponent as? NSCompoundPredicate {
|
||||
let newSub = cQuery.subpredicates.map { updateQueryTypeKeys($0 as! NSPredicate) }
|
||||
switch cQuery.compoundPredicateType {
|
||||
case .and: return NSCompoundPredicate(andPredicateWithSubpredicates: newSub)
|
||||
case .not: return NSCompoundPredicate(notPredicateWithSubpredicate: newSub.first!)
|
||||
case .or: return NSCompoundPredicate(orPredicateWithSubpredicates: newSub)
|
||||
}
|
||||
} else if let cQuery = queryComponent as? NSComparisonPredicate {
|
||||
var newLeft = cQuery.leftExpression
|
||||
var newRight = cQuery.rightExpression
|
||||
if newLeft.expressionType == .keyPath, let newKey = mapDict[newLeft.keyPath] {
|
||||
newLeft = NSExpression(forKeyPath: newKey)
|
||||
}
|
||||
if newRight.expressionType == .keyPath, let newKey = mapDict[newRight.keyPath] {
|
||||
newRight = NSExpression(forKeyPath: newKey)
|
||||
}
|
||||
if newLeft.expressionType == .keyPath, newLeft.keyPath == "type" {
|
||||
newRight = NSExpression(forConstantValue: newRight.constantValue as? String == "directory" ? "public.directory": "public.data")
|
||||
}
|
||||
if newRight.expressionType == .keyPath, newRight.keyPath == "type" {
|
||||
newLeft = NSExpression(forConstantValue: newLeft.constantValue as? String == "directory" ? "public.directory": "public.data")
|
||||
}
|
||||
return NSComparisonPredicate(leftExpression: newLeft, rightExpression: newRight, modifier: cQuery.comparisonPredicateModifier, type: cQuery.predicateOperatorType, options: cQuery.options)
|
||||
} else {
|
||||
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 {
|
||||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
file.size = (attribs[NSMetadataItemFSSizeKey] as? NSNumber)?.int64Value ?? -1
|
||||
file.creationDate = attribs[NSMetadataItemFSCreationDateKey] as? Date
|
||||
file.modifiedDate = attribs[NSMetadataItemFSContentChangeDateKey] as? Date
|
||||
let isFolder = (attribs[NSMetadataItemContentTypeTreeKey] as? [String])?.contains("public.folder") ?? false
|
||||
let isSymbolic = (attribs[NSMetadataItemContentTypeTreeKey] as? [String])?.contains("public.symlink") ?? false
|
||||
file.type = isFolder ? .directory : (isSymbolic ? .symbolicLink : .regular)
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
fileprivate func monitorFile(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 = [NSMetadataUbiquitousItemPercentDownloadedKey,
|
||||
NSMetadataUbiquitousItemPercentUploadedKey,
|
||||
NSMetadataUbiquitousItemIsUploadedKey,
|
||||
NSMetadataUbiquitousItemDownloadingStatusKey,
|
||||
NSMetadataItemFSSizeKey]
|
||||
query.searchScopes = [self.scope.rawValue]
|
||||
|
||||
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()
|
||||
progress?.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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`
|
||||
*/
|
||||
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`
|
||||
*/
|
||||
case documents
|
||||
|
||||
public typealias RawValue = String
|
||||
|
||||
public init? (rawValue: String) {
|
||||
switch rawValue {
|
||||
case NSMetadataQueryUbiquitousDataScope:
|
||||
self = .data
|
||||
case NSMetadataQueryUbiquitousDocumentsScope:
|
||||
self = .documents
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public var rawValue: String {
|
||||
switch self {
|
||||
case .data:
|
||||
return NSMetadataQueryUbiquitousDataScope
|
||||
case .documents:
|
||||
return NSMetadataQueryUbiquitousDocumentsScope
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
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
|
||||
}
|
||||
*/
|
||||
+446
-333
@@ -1,3 +1,4 @@
|
||||
|
||||
//
|
||||
// DropboxFileProvider.swift
|
||||
// FileProvider
|
||||
@@ -7,297 +8,279 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if os(macOS) || os(iOS) || os(tvOS)
|
||||
import CoreGraphics
|
||||
#endif
|
||||
|
||||
public enum FileProviderDropboxErrorCode: Int {
|
||||
case BadInputParameter = 400
|
||||
case ExpiredToken = 401
|
||||
case Forbidden = 403
|
||||
case Endpoint = 409
|
||||
case TooManyRequests = 429
|
||||
case InternalServer = 500
|
||||
case BadGateway = 502
|
||||
}
|
||||
|
||||
public struct FileProviderDropboxError: ErrorType, CustomStringConvertible {
|
||||
public let code: FileProviderDropboxErrorCode
|
||||
public let path: String
|
||||
/**
|
||||
Allows accessing to Dropbox stored files. This provider doesn't cache or save files internally, however you can
|
||||
set `useCache` and `cache` properties to use Foundation `NSURLCache` system.
|
||||
|
||||
- Note: You can pass file id or rev instead of file path, e.g `"id:1234abcd"` or `"rev:1234abcd"`, to point to a file or folder by ID.
|
||||
|
||||
- Note: Uploading files and data are limited to 150MB, for now.
|
||||
*/
|
||||
open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
override open class var type: String { return "Dropbox" }
|
||||
|
||||
public var description: String {
|
||||
switch code {
|
||||
case .BadInputParameter: return "Bad input parameter."
|
||||
case .ExpiredToken: return "Bad or expired token. To fix this, you should re-authenticate the user."
|
||||
case .Forbidden: return "Forbidden."
|
||||
case .Endpoint: return "Endpoint-specific error."
|
||||
case .TooManyRequests: return "Your app is making too many requests"
|
||||
case .InternalServer: return "An error occurred on the Dropbox servers."
|
||||
case .BadGateway: return "An error occurred on the Dropbox servers."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class DropboxFileObject: FileObject {
|
||||
public let serverTime: NSDate?
|
||||
public let id: String?
|
||||
public let rev: String?
|
||||
/// Dropbox RPC API URL, which is equal with [https://api.dropboxapi.com/2/](https://api.dropboxapi.com/2/)
|
||||
open 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 init(absoluteURL: NSURL, name: String, path: String, size: Int64, serverTime: NSDate?, createdDate: NSDate?, modifiedDate: NSDate?, fileType: FileType, isHidden: Bool, isReadOnly: Bool, id: String?, rev: String?) {
|
||||
self.serverTime = serverTime
|
||||
self.id = id
|
||||
self.rev = rev
|
||||
super.init(absoluteURL: absoluteURL, name: name, path: path, size: size, createdDate: createdDate, modifiedDate: modifiedDate, fileType: fileType, isHidden: isHidden, isReadOnly: isReadOnly)
|
||||
}
|
||||
}
|
||||
|
||||
// Because this class uses NSURLSession, it's necessary to disable App Transport Security
|
||||
// in case of using this class with unencrypted HTTP connection.
|
||||
|
||||
public class DropboxFileProvider: NSObject, FileProviderBasic {
|
||||
public let type: String = "WebDAV"
|
||||
public let isPathRelative: Bool = true
|
||||
public let baseURL: NSURL?
|
||||
public var currentPath: String = ""
|
||||
public var dispatch_queue: dispatch_queue_t {
|
||||
willSet {
|
||||
assert(_session == nil, "It's not effective to change dispatch_queue property after session is initialized.")
|
||||
}
|
||||
}
|
||||
public weak var delegate: FileProviderDelegate?
|
||||
public let credential: NSURLCredential?
|
||||
|
||||
private var _session: NSURLSession?
|
||||
private var session: NSURLSession {
|
||||
if _session == nil {
|
||||
let queue = NSOperationQueue()
|
||||
//queue.underlyingQueue = dispatch_queue
|
||||
_session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration(), delegate: self, delegateQueue: queue)
|
||||
}
|
||||
return _session!
|
||||
/**
|
||||
Initializer for Dropbox provider with given client ID and Token.
|
||||
These parameters 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.
|
||||
|
||||
- Parameter credential: a `URLCredential` object with Client ID set as `user` and Token set as `password`.
|
||||
- Parameter cache: A URLCache to cache downloaded files and contents.
|
||||
*/
|
||||
public init(credential: URLCredential?, cache: URLCache? = nil) {
|
||||
self.apiURL = URL(string: "https://api.dropboxapi.com/2/")!
|
||||
self.contentURL = URL(string: "https://content.dropboxapi.com/2/")!
|
||||
super.init(baseURL: nil, credential: credential, cache: cache)
|
||||
}
|
||||
|
||||
public init? (baseURL: NSURL, credential: NSURLCredential?) {
|
||||
if !["http", "https"].contains(baseURL.uw_scheme.lowercaseString) {
|
||||
return nil
|
||||
}
|
||||
self.baseURL = baseURL
|
||||
dispatch_queue = dispatch_queue_create("FileProvider.\(type)", DISPATCH_QUEUE_CONCURRENT)
|
||||
//let url = baseURL.uw_absoluteString
|
||||
self.credential = credential
|
||||
public required convenience init?(coder aDecoder: NSCoder) {
|
||||
self.init(credential: aDecoder.decodeObject(of: URLCredential.self, forKey: "credential"))
|
||||
self.useCache = aDecoder.decodeBool(forKey: "useCache")
|
||||
self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache")
|
||||
}
|
||||
|
||||
deinit {
|
||||
_session?.invalidateAndCancel()
|
||||
override open func copy(with zone: NSZone? = nil) -> Any {
|
||||
let copy = DropboxFileProvider(credential: self.credential, cache: self.cache)
|
||||
copy.delegate = self.delegate
|
||||
copy.fileOperationDelegate = self.fileOperationDelegate
|
||||
copy.useCache = self.useCache
|
||||
copy.validatingCache = self.validatingCache
|
||||
return copy
|
||||
}
|
||||
|
||||
public func contentsOfDirectoryAtPath(path: String, completionHandler: ((contents: [FileObject], error: ErrorType?) -> Void)) {
|
||||
NotImplemented()
|
||||
/**
|
||||
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) {
|
||||
let query = NSPredicate(format: "TRUEPREDICATE")
|
||||
_ = searchFiles(path: path, recursive: false, query: query, foundItemHandler: nil, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func attributesOfItemAtPath(path: String, completionHandler: ((attributes: FileObject?, error: ErrorType?) -> Void)) {
|
||||
let url = NSURL(string: "https://api.dropboxapi.com/2/files/list_revisions")!
|
||||
let request = NSMutableURLRequest(URL: url)
|
||||
request.HTTPMethod = "POST"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
let requestDictionary = ["path": correctPath(path)!]
|
||||
request.HTTPBody = dictionaryToJSON(requestDictionary)?.dataUsingEncoding(NSUTF8StringEncoding)
|
||||
let task = session.dataTaskWithRequest(request) { (data, response, error) in
|
||||
if let response = response as? NSHTTPURLResponse {
|
||||
defer {
|
||||
self.delegateNotify(FileOperation.Create(path: path), error: error)
|
||||
}
|
||||
let code = FileProviderDropboxErrorCode(rawValue: response.statusCode)
|
||||
let dbError: FileProviderDropboxError? = code != nil ? FileProviderDropboxError(code: code!, path: path) : nil
|
||||
if let data = data, let jsonStr = String(data: data, encoding: NSUTF8StringEncoding) {
|
||||
let json = self.jsonToDictionary(jsonStr)
|
||||
if (json?["is_deleted"] as? NSNumber)?.boolValue ?? false, let entries = json?["entries"] as? [AnyObject] where entries.count > 0 , let entry = entries[0] as? [String: AnyObject], let file = self.mapToFileObject(entry) {
|
||||
completionHandler(attributes: file, error: dbError)
|
||||
return
|
||||
}
|
||||
}
|
||||
completionHandler(attributes: nil, error: dbError)
|
||||
return
|
||||
/**
|
||||
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) {
|
||||
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: AnyObject] = ["path": correctPath(path)! as NSString]
|
||||
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, response.statusCode >= 400 {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
|
||||
serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) }
|
||||
}
|
||||
completionHandler(attributes: nil, error: error)
|
||||
}
|
||||
if let json = data?.deserializeJSON(), let file = DropboxFileObject(json: json) {
|
||||
fileObject = file
|
||||
}
|
||||
completionHandler(fileObject, serverError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
|
||||
public weak var fileOperationDelegate: FileOperationDelegate?
|
||||
}
|
||||
|
||||
extension DropboxFileProvider: FileProviderOperations {
|
||||
public func createFolder(folderName: String, atPath: String, completionHandler: SimpleCompletionHandler) {
|
||||
let path = (atPath as NSString).stringByAppendingPathComponent(folderName) + "/"
|
||||
doOperation(.Create(path: path), completionHandler: completionHandler)
|
||||
/// Returns volume/provider information asynchronously.
|
||||
/// - Parameter volumeInfo: Information of filesystem/Provider returned by system/server.
|
||||
open override func storageProperties(completionHandler: @escaping (_ volumeInfo: VolumeObject?) -> Void) {
|
||||
let url = URL(string: "users/get_space_usage", relativeTo: apiURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
let volume = VolumeObject(allValues: [:])
|
||||
volume.totalCapacity = ((json["allocation"] as? NSDictionary)?["allocated"] as? NSNumber)?.int64Value ?? -1
|
||||
volume.usage = (json["used"] as? NSNumber)?.int64Value ?? 0
|
||||
completionHandler(volume)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
|
||||
public func createFile(fileAttribs: FileObject, atPath path: String, contents data: NSData?, completionHandler: SimpleCompletionHandler) {
|
||||
self.writeContentsAtPath(path, contents: data ?? NSData(), 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`.
|
||||
- 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: String?
|
||||
if query.predicateFormat == "TRUEPREDICATE" {
|
||||
queryStr = nil
|
||||
} else {
|
||||
queryStr = query.findValue(forKey: "name", operator: .beginsWith) as? String
|
||||
}
|
||||
let requestHandler = self.listRequest(path: path, queryStr: queryStr, recursive: recursive)
|
||||
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)
|
||||
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()) {
|
||||
files.append(file)
|
||||
progress.completedUnitCount += 1
|
||||
foundItemHandler?(file)
|
||||
}
|
||||
}
|
||||
let ncursor: String?
|
||||
if let hasmore = (json["has_more"] as? NSNumber)?.boolValue, hasmore {
|
||||
ncursor = json["cursor"] as? String
|
||||
} else if let hasmore = (json["more"] as? NSNumber)?.boolValue, hasmore {
|
||||
ncursor = (json["start"] as? Int).flatMap(String.init)
|
||||
} else {
|
||||
ncursor = nil
|
||||
}
|
||||
return (files, nil, ncursor)
|
||||
}, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func moveItemAtPath(path: String, toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) {
|
||||
doOperation(.Move(source: path, destination: toPath), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func copyItemAtPath(path: String, toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) {
|
||||
doOperation(.Copy(source: path, destination: toPath), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func removeItemAtPath(path: String, completionHandler: SimpleCompletionHandler) {
|
||||
doOperation(.Remove(path: path), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
private func doOperation(operation: FileOperation, completionHandler: SimpleCompletionHandler) {
|
||||
let url: String
|
||||
var path: String?, fromPath: String?, toPath: String?
|
||||
override func request(for operation: FileOperationType, overwrite: Bool = false, attributes: [URLResourceKey : Any] = [:]) -> URLRequest {
|
||||
|
||||
func uploadRequest(to path: String) -> URLRequest {
|
||||
var requestDictionary = [String: AnyObject]()
|
||||
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?
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue(authentication: credential, with: .oAuth2)
|
||||
request.setValue(contentType: .stream)
|
||||
request.setValue(dropboxArgKey: requestDictionary)
|
||||
return request
|
||||
}
|
||||
|
||||
func downloadRequest(from path: String) -> URLRequest {
|
||||
let url = URL(string: "files/download", relativeTo: contentURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request = URLRequest(url: url)
|
||||
request.setValue(authentication: credential, with: .oAuth2)
|
||||
request.setValue(dropboxArgKey: ["path": correctPath(path)! as NSString])
|
||||
return request
|
||||
}
|
||||
|
||||
// content operations
|
||||
switch operation {
|
||||
case .Create(path: let p):
|
||||
url = "https://api.dropboxapi.com/2/files/create_folder"
|
||||
path = p
|
||||
case .Copy(source: let fp, destination: let tp):
|
||||
url = "https://api.dropboxapi.com/2/files/copy"
|
||||
fromPath = fp
|
||||
toPath = tp
|
||||
case .Move(source: let fp, destination: let tp):
|
||||
url = "https://api.dropboxapi.com/2/files/move"
|
||||
fromPath = fp
|
||||
toPath = tp
|
||||
case .Modify(path: let p):
|
||||
return
|
||||
case .Remove(path: let p):
|
||||
url = "https://api.dropboxapi.com/2/files/delete"
|
||||
path = p
|
||||
case .Link(link: _, target: _):
|
||||
return
|
||||
case .copy(source: let source, destination: let dest) where dest.lowercased().hasPrefix("file://"):
|
||||
return downloadRequest(from: source)
|
||||
case .fetch(let path):
|
||||
return downloadRequest(from: path)
|
||||
case .copy(source: let source, destination: let dest) where source.lowercased().hasPrefix("file://"):
|
||||
return uploadRequest(to: dest)
|
||||
case .modify(let path):
|
||||
return uploadRequest(to: path)
|
||||
default:
|
||||
return self.apiRequest(for: operation, overwrite: overwrite)
|
||||
}
|
||||
let request = NSMutableURLRequest(URL: NSURL(string: url)!)
|
||||
request.HTTPMethod = "POST"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
}
|
||||
|
||||
func apiRequest(for operation: FileOperationType, overwrite: Bool = false) -> URLRequest {
|
||||
let url: String
|
||||
let sourcePath = operation.source
|
||||
let destPath = operation.destination
|
||||
var requestDictionary = [String: AnyObject]()
|
||||
requestDictionary["path"] = correctPath(path)
|
||||
requestDictionary["from_path"] = correctPath(fromPath)
|
||||
requestDictionary["to_path"] = correctPath(toPath)
|
||||
request.HTTPBody = dictionaryToJSON(requestDictionary)?.dataUsingEncoding(NSUTF8StringEncoding)
|
||||
let task = session.dataTaskWithRequest(request) { (data, response, error) in
|
||||
if let response = response as? NSHTTPURLResponse {
|
||||
let code = FileProviderDropboxErrorCode(rawValue: response.statusCode)
|
||||
let dbError: FileProviderDropboxError? = code != nil ? FileProviderDropboxError(code: code!, path: path ?? fromPath ?? "") : nil
|
||||
defer {
|
||||
self.delegateNotify(operation, error: error ?? dbError)
|
||||
}
|
||||
/*if let data = data, let jsonStr = String(data: data, encoding: NSUTF8StringEncoding) {
|
||||
let json = self.jsonToDictionary(jsonStr)
|
||||
}*/
|
||||
completionHandler?(error: dbError)
|
||||
return
|
||||
}
|
||||
completionHandler?(error: error)
|
||||
switch operation {
|
||||
case .create:
|
||||
url = "files/create_folder_v2"
|
||||
|
||||
case .copy:
|
||||
url = "files/copy_v2"
|
||||
requestDictionary["allow_shared_folder"] = NSNumber(value: true)
|
||||
case .move:
|
||||
url = "files/move_v2"
|
||||
requestDictionary["allow_shared_folder"] = NSNumber(value: true)
|
||||
case .remove:
|
||||
url = "files/delete_v2"
|
||||
default: // modify, link, fetch
|
||||
fatalError("Unimplemented operation \(operation.description) in \(#file)")
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
|
||||
public func copyLocalFileToPath(localFile: NSURL, toPath: String, completionHandler: SimpleCompletionHandler) {
|
||||
NotImplemented()
|
||||
let request = NSMutableURLRequest(URL: absoluteURL(toPath))
|
||||
request.HTTPMethod = "PUT"
|
||||
let task = session.uploadTaskWithRequest(request, fromFile: localFile) { (data, response, error) in
|
||||
completionHandler?(error: error)
|
||||
self.delegateNotify(.Move(source: localFile.uw_absoluteString, destination: toPath), error: error)
|
||||
var request = URLRequest(url: URL(string: url, relativeTo: apiURL)!)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue(authentication: credential, with: .oAuth2)
|
||||
request.setValue(contentType: .json)
|
||||
if let dest = correctPath(destPath) as NSString? {
|
||||
requestDictionary["from_path"] = correctPath(sourcePath) as NSString?
|
||||
requestDictionary["to_path"] = dest
|
||||
} else {
|
||||
requestDictionary["path"] = correctPath(sourcePath) as NSString?
|
||||
}
|
||||
task.taskDescription = self.dictionaryToJSON(["type": "Copy", "source": localFile.uw_absoluteString, "dest": toPath])
|
||||
task.resume()
|
||||
request.httpBody = Data(jsonDictionary: requestDictionary)
|
||||
return request
|
||||
}
|
||||
|
||||
public func copyPathToLocalFile(path: String, toLocalURL destURL: NSURL, completionHandler: SimpleCompletionHandler) {
|
||||
let url = NSURL(string: "https://api.dropboxapi.com/2/files/download")!
|
||||
let request = NSMutableURLRequest(URL: url)
|
||||
request.HTTPMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
let requestDictionary = ["path": path]
|
||||
request.setValue(dictionaryToJSON(requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
|
||||
let task = session.downloadTaskWithRequest(request, completionHandler: { (cacheURL, response, error) in
|
||||
guard let cacheURL = cacheURL, let httpResponse = response as? NSHTTPURLResponse where httpResponse.statusCode < 300 else {
|
||||
let code = FileProviderDropboxErrorCode(rawValue: (response as? NSHTTPURLResponse)?.statusCode ?? -1)
|
||||
let dbError: FileProviderDropboxError? = code != nil ? FileProviderDropboxError(code: code!, path: path) : nil
|
||||
completionHandler?(error: dbError ?? error)
|
||||
return
|
||||
}
|
||||
do {
|
||||
try NSFileManager.defaultManager().moveItemAtURL(cacheURL, toURL: destURL)
|
||||
completionHandler?(error: nil)
|
||||
} catch let e {
|
||||
completionHandler?(error: e)
|
||||
}
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
||||
extension DropboxFileProvider: FileProviderReadWrite {
|
||||
public func contentsAtPath(path: String, completionHandler: ((contents: NSData?, error: ErrorType?) -> Void)) {
|
||||
self.contentsAtPath(path, offset: 0, length: -1, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func contentsAtPath(path: String, offset: Int64, length: Int, completionHandler: ((contents: NSData?, error: ErrorType?) -> Void)) {
|
||||
let url = NSURL(string: "https://api.dropboxapi.com/2/files/download")!
|
||||
let request = NSMutableURLRequest(URL: url)
|
||||
request.HTTPMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
if length > 0 {
|
||||
request.setValue("bytes=\(offset)-\(offset + length)", forHTTPHeaderField: "Range")
|
||||
} else if offset > 0 && length < 0 {
|
||||
request.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
|
||||
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)
|
||||
} else {
|
||||
errorDesc = data.flatMap({ String(data: $0, encoding: .utf8) })
|
||||
}
|
||||
let requestDictionary = ["path": path]
|
||||
request.setValue(dictionaryToJSON(requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
|
||||
let task = session.downloadTaskWithRequest(request, completionHandler: { (cacheURL, response, error) in
|
||||
guard let cacheURL = cacheURL, let httpResponse = response as? NSHTTPURLResponse where httpResponse.statusCode < 300 else {
|
||||
let code = FileProviderDropboxErrorCode(rawValue: (response as? NSHTTPURLResponse)?.statusCode ?? -1)
|
||||
let dbError: FileProviderDropboxError? = code != nil ? FileProviderDropboxError(code: code!, path: path) : nil
|
||||
completionHandler(contents: nil, error: dbError ?? error)
|
||||
return
|
||||
}
|
||||
let destURL = NSURL(fileURLWithPath: NSTemporaryDirectory()).uw_URLByAppendingPathComponent(cacheURL.lastPathComponent ?? "tmpfile")
|
||||
do {
|
||||
try NSFileManager.defaultManager().moveItemAtURL(cacheURL, toURL: destURL)
|
||||
completionHandler(contents: NSData(contentsOfURL: destURL), error: error)
|
||||
} catch let e {
|
||||
completionHandler(contents: nil, error: e)
|
||||
}
|
||||
})
|
||||
task.resume()
|
||||
return FileProviderDropboxError(code: code, path: path ?? "", serverDescription: errorDesc)
|
||||
}
|
||||
|
||||
public func writeContentsAtPath(path: String, contents data: NSData, atomically: Bool = false, completionHandler: SimpleCompletionHandler) {
|
||||
NotImplemented()
|
||||
let url = atomically ? absoluteURL(path).uw_URLByAppendingPathExtension("tmp") : absoluteURL(path)
|
||||
let request = NSMutableURLRequest(URL: url)
|
||||
request.HTTPMethod = "PUT"
|
||||
let task = session.uploadTaskWithRequest(request, fromData: data) { (data, response, error) in
|
||||
defer {
|
||||
self.delegateNotify(.Modify(path: path), error: error)
|
||||
}
|
||||
if atomically {
|
||||
self.moveItemAtPath((path as NSString).stringByAppendingPathExtension("tmp")!, toPath: path, completionHandler: completionHandler)
|
||||
}
|
||||
if let error = error {
|
||||
// If there is no error, completionHandler has been executed by move command
|
||||
completionHandler?(error: error)
|
||||
}
|
||||
}
|
||||
task.taskDescription = self.dictionaryToJSON(["type": "Modify", "source": path])
|
||||
task.resume()
|
||||
override var maxUploadSimpleSupported: Int64 {
|
||||
return 157_286_400 // 150MB
|
||||
}
|
||||
|
||||
public func searchFilesAtPath(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: ((files: [FileObject], error: ErrorType?) -> Void)) {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
private func registerNotifcation(path: String, eventHandler: (() -> Void)) {
|
||||
/* There is two ways to monitor folders chaging in Dropbox. Either using webooks
|
||||
/*
|
||||
fileprivate func registerNotifcation(path: String, eventHandler: (() -> Void)) {
|
||||
/* There is two ways to monitor folders changing in Dropbox. Either using webooks
|
||||
* which means you have to implement a server to translate it to push notifications
|
||||
* or using apiv2 list_folder/longpoll method. The second one is implemeted here.
|
||||
* Tough webhooks are much more efficient, longpoll is much simpler to implement!
|
||||
@@ -305,89 +288,219 @@ extension DropboxFileProvider: FileProviderReadWrite {
|
||||
*/
|
||||
NotImplemented()
|
||||
}
|
||||
private func unregisterNotifcation(path: String) {
|
||||
fileprivate func unregisterNotifcation(path: String) {
|
||||
NotImplemented()
|
||||
}
|
||||
}
|
||||
*/
|
||||
// TODO: Implement /get_account & /get_current_account
|
||||
|
||||
internal extension DropboxFileProvider {
|
||||
private func mapToFileObject(jsonStr: String) -> DropboxFileObject? {
|
||||
guard let json = self.jsonToDictionary(jsonStr) else { return nil }
|
||||
return self.mapToFileObject(json)
|
||||
}
|
||||
|
||||
private func mapToFileObject(json: [String: AnyObject]) -> DropboxFileObject? {
|
||||
guard let name = json["name"] as? String else { return nil }
|
||||
guard let path = json["path_display"] as? String else { return nil }
|
||||
let href = NSURL(string: path)!
|
||||
let size = (json["size"] as? NSNumber)?.longLongValue ?? -1
|
||||
let serverTime = resolveDate(json["server_modified"] as? String ?? "")
|
||||
let modifiedDate = resolveDate(json["client_modified"] as? String ?? "")
|
||||
let isDirectory = (json[".tag"] as? String) == "folder"
|
||||
let isReadonly = (json["sharing_info"]?["read_only"] as? NSNumber)?.boolValue ?? false
|
||||
let id = json["id"] as? String
|
||||
let rev = json["id"] as? String
|
||||
return DropboxFileObject(absoluteURL: href, name: name, path: path, size: size, serverTime: serverTime, createdDate: nil, modifiedDate: modifiedDate, fileType: isDirectory ? .Directory : .Regular, isHidden: false, isReadOnly: isReadonly, id: id, rev: rev)
|
||||
}
|
||||
|
||||
private func delegateNotify(operation: FileOperation, error: ErrorType?) {
|
||||
dispatch_async(dispatch_get_main_queue(), {
|
||||
if error == nil {
|
||||
self.delegate?.fileproviderSucceed(self, operation: operation)
|
||||
} else {
|
||||
self.delegate?.fileproviderFailed(self, operation: operation)
|
||||
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.setValue(authentication: credential, with: .oAuth2)
|
||||
request.setValue(contentType: .json)
|
||||
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString]
|
||||
request.httpBody = Data(jsonDictionary: requestDictionary)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderHTTPError?
|
||||
var link: URL?
|
||||
var fileObject: DropboxFileObject?
|
||||
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() {
|
||||
link = (json["link"] as? String).flatMap(URL.init(string:))
|
||||
fileObject = (json["metadata"] as? [String: AnyObject]).flatMap(DropboxFileObject.init(json:))
|
||||
}
|
||||
}
|
||||
|
||||
let expiration: Date? = link != nil ? Date(timeIntervalSinceNow: 4 * 60 * 60) : nil
|
||||
completionHandler(link, fileObject, expiration, serverError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
|
||||
/**
|
||||
Downloads a file from remote url to designated path asynchronously.
|
||||
|
||||
- Parameters:
|
||||
- remoteURL: a valid remote url to file.
|
||||
- to: Destination path of file, including file/directory name.
|
||||
- completionHandler: a closure with result of directory entries or error.
|
||||
- `jobId`: Job ID returned by Dropbox to monitor the copy/download progress.
|
||||
- `attribute`: A `FileObject` containing the attributes of the item.
|
||||
- `error`: Error returned by Dropbox.
|
||||
*/
|
||||
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))
|
||||
return
|
||||
}
|
||||
let url = URL(string: "files/save_url", relativeTo: apiURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue(authentication: credential, with: .oAuth2)
|
||||
request.setValue(contentType: .json)
|
||||
let requestDictionary: [String: AnyObject] = ["path": correctPath(toPath)! as NSString, "url" : remoteURL.absoluteString as NSString]
|
||||
request.httpBody = Data(jsonDictionary: requestDictionary)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderHTTPError?
|
||||
var jobId: String?
|
||||
var fileObject: DropboxFileObject?
|
||||
if let response = response as? HTTPURLResponse {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
|
||||
serverError = code.flatMap { self.serverError(with: $0, path: toPath, data: data) }
|
||||
if let json = data?.deserializeJSON() {
|
||||
jobId = json["async_job_id"] as? String
|
||||
fileObject = (json["metadata"] as? [String: AnyObject]).flatMap(DropboxFileObject.init(json:))
|
||||
}
|
||||
}
|
||||
completionHandler(jobId, fileObject, serverError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
|
||||
/**
|
||||
Copys a file from another user Dropbox storage to designated path asynchronously.
|
||||
|
||||
- Parameters:
|
||||
- reference: a valid reference string from another user via `copy_reference/get` REST method.
|
||||
- to: Destination path of file, including file/directory name.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
*/
|
||||
open func copyItem(reference: String, to toPath: String, completionHandler: SimpleCompletionHandler) {
|
||||
let url = URL(string: "files/copy_reference/save", relativeTo: apiURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue(authentication: credential, with: .oAuth2)
|
||||
request.setValue(contentType: .json)
|
||||
let requestDictionary: [String: AnyObject] = ["path": correctPath(toPath)! as NSString, "copy_reference" : reference as NSString]
|
||||
request.httpBody = Data(jsonDictionary: requestDictionary)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderHTTPError?
|
||||
if let response = response as? HTTPURLResponse {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
|
||||
serverError = code.flatMap { self.serverError(with: $0, path: toPath, data: data) }
|
||||
}
|
||||
completionHandler?(serverError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
||||
extension DropboxFileProvider: FileProvider {}
|
||||
|
||||
// MARK: URLSession delegate
|
||||
extension DropboxFileProvider: NSURLSessionDataDelegate, NSURLSessionDownloadDelegate {
|
||||
public func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
|
||||
return
|
||||
}
|
||||
|
||||
public func URLSession(session: NSURLSession, task: NSURLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
|
||||
guard let desc = task.taskDescription, let json = jsonToDictionary(desc) else {
|
||||
return
|
||||
}
|
||||
guard let type = json["type"] as? String, let source = json["source"] as? String else {
|
||||
return
|
||||
}
|
||||
let dest = json["dest"] as? String
|
||||
let op : FileOperation
|
||||
switch type {
|
||||
case "Create":
|
||||
op = .Create(path: source)
|
||||
case "Copy":
|
||||
guard let dest = dest else { return }
|
||||
op = .Copy(source: source, destination: dest)
|
||||
case "Move":
|
||||
guard let dest = dest else { return }
|
||||
op = .Move(source: source, destination: dest)
|
||||
case "Modify":
|
||||
op = .Modify(path: source)
|
||||
case "Remove":
|
||||
op = .Remove(path: source)
|
||||
case "Link":
|
||||
guard let dest = dest else { return }
|
||||
op = .Link(link: source, target: dest)
|
||||
extension DropboxFileProvider: ExtendedFileProvider {
|
||||
open func propertiesOfFileSupported(path: String) -> Bool {
|
||||
let fileExt = (path as NSString).pathExtension.lowercased()
|
||||
switch fileExt {
|
||||
case "jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff":
|
||||
return true
|
||||
/*case "mp3", "aac", "m4a":
|
||||
return true*/
|
||||
case "mp4", "mpg", "3gp", "mov", "avi":
|
||||
return true
|
||||
default:
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
let progress = Float(totalBytesSent) / Float(totalBytesExpectedToSend)
|
||||
|
||||
self.delegate?.fileproviderProgress(self, operation: op, progress: progress)
|
||||
}
|
||||
|
||||
public func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
|
||||
guard let desc = downloadTask.taskDescription, let json = jsonToDictionary(desc), let source = json["source"] as? String, dest = json["dest"] as? String else {
|
||||
return
|
||||
}
|
||||
|
||||
self.delegate?.fileproviderProgress(self, operation: .Copy(source: source, destination: dest), progress: Float(totalBytesWritten) / Float(totalBytesExpectedToWrite))
|
||||
@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: 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()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// Default value for dimension is 64x64, according to Dropbox documentation
|
||||
@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() {
|
||||
case "jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff":
|
||||
url = URL(string: "files/get_thumbnail", relativeTo: contentURL)!
|
||||
thumbAPI = true
|
||||
case "doc", "docx", "docm", "xls", "xlsx", "xlsm":
|
||||
fallthrough
|
||||
case "ppt", "pps", "ppsx", "ppsm", "pptx", "pptm":
|
||||
fallthrough
|
||||
case "rtf":
|
||||
url = URL(string: "files/get_preview", relativeTo: contentURL)!
|
||||
thumbAPI = false
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
var request = URLRequest(url: url)
|
||||
request.setValue(authentication: credential, with: .oAuth2)
|
||||
var requestDictionary: [String: AnyObject] = ["path": path as NSString]
|
||||
if thumbAPI {
|
||||
requestDictionary["format"] = "jpeg" as NSString
|
||||
let size: String
|
||||
switch dimension?.height ?? 64 {
|
||||
case 0...32: size = "w32h32"
|
||||
case 33...64: size = "w64h64"
|
||||
case 65...128: size = "w128h128"
|
||||
case 129...480: size = "w640h480"
|
||||
default: size = "w1024h768"
|
||||
}
|
||||
requestDictionary["size"] = size as NSString
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
if let data = data {
|
||||
if data.isPDF, let pageImage = DropboxFileProvider.convertToImage(pdfData: data) {
|
||||
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) {
|
||||
image = dimension.map({ DropboxFileProvider.scaleDown(image: fetchedimage, toSize: $0) }) ?? fetchedimage
|
||||
}
|
||||
}
|
||||
completionHandler(image, error)
|
||||
})
|
||||
task.resume()
|
||||
return nil
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
//
|
||||
// DropboxHelper.swift
|
||||
// FileProvider
|
||||
//
|
||||
// Created by Amir Abbas Mousavian.
|
||||
// Copyright © 2016 Mousavian. Distributed under MIT license.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Error returned by Dropbox server when trying to access or do operations on a file or folder.
|
||||
public struct FileProviderDropboxError: FileProviderHTTPError {
|
||||
public let code: FileProviderHTTPErrorCode
|
||||
public let path: String
|
||||
public let serverDescription: String?
|
||||
}
|
||||
|
||||
/// Containts path, url and attributes of a Dropbox file or resource.
|
||||
public final class DropboxFileObject: FileObject {
|
||||
internal convenience init? (jsonStr: String) {
|
||||
guard let json = jsonStr.deserializeJSON() else { return nil }
|
||||
self.init(json: json)
|
||||
}
|
||||
|
||||
internal init? (json: [String: AnyObject]) {
|
||||
var json = json
|
||||
if json["name"] == nil, let metadata = json["metadata"] as? [String: AnyObject] {
|
||||
json = metadata
|
||||
}
|
||||
guard let name = json["name"] as? String else { return nil }
|
||||
guard let path = json["path_display"] as? String else { return nil }
|
||||
super.init(url: nil, name: name, path: path)
|
||||
self.size = (json["size"] as? NSNumber)?.int64Value ?? -1
|
||||
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.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? {
|
||||
get {
|
||||
return allValues[.serverDateKey] as? Date
|
||||
}
|
||||
set {
|
||||
allValues[.serverDateKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// 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? {
|
||||
get {
|
||||
return allValues[.fileResourceIdentifierKey] as? String
|
||||
}
|
||||
set {
|
||||
allValues[.fileResourceIdentifierKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// 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? {
|
||||
get {
|
||||
return allValues[.generationIdentifierKey] as? String
|
||||
}
|
||||
set {
|
||||
allValues[.generationIdentifierKey] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal extension DropboxFileProvider {
|
||||
internal func correctPath(_ path: String?) -> String? {
|
||||
guard let path = path else { return nil }
|
||||
if path.hasPrefix("id:") || path.hasPrefix("rev:") {
|
||||
return path
|
||||
}
|
||||
var p = path.hasPrefix("/") ? path : "/" + path
|
||||
if p.hasSuffix("/") {
|
||||
p.remove(at: p.index(before:p.endIndex))
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
internal 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.setValue(authentication: self.credential, with: .oAuth2)
|
||||
request.setValue(contentType: .json)
|
||||
var requestDictionary: [String: AnyObject] = ["path": self.correctPath(path)! as NSString]
|
||||
requestDictionary["query"] = queryStr as NSString
|
||||
requestDictionary["start"] = NSNumber(value: (token.flatMap( { Int($0) } ) ?? 0))
|
||||
request.httpBody = Data(jsonDictionary: requestDictionary)
|
||||
return request
|
||||
}
|
||||
} else {
|
||||
return { [weak self] (token) -> URLRequest? in
|
||||
guard let `self` = self else { return nil }
|
||||
var requestDictionary = [String: AnyObject]()
|
||||
let url: URL
|
||||
if let token = token {
|
||||
url = URL(string: "files/list_folder/continue", relativeTo: self.apiURL)!
|
||||
requestDictionary["cursor"] = token as NSString?
|
||||
} else {
|
||||
url = URL(string: "files/list_folder", relativeTo: self.apiURL)!
|
||||
requestDictionary["path"] = self.correctPath(path) as NSString?
|
||||
requestDictionary["recursive"] = NSNumber(value: recursive)
|
||||
}
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue(authentication: self.credential, with: .oAuth2)
|
||||
request.setValue(contentType: .json)
|
||||
request.httpBody = Data(jsonDictionary: requestDictionary)
|
||||
return request
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal extension DropboxFileProvider {
|
||||
static let dateFormatter = DateFormatter()
|
||||
static let decimalFormatter = NumberFormatter()
|
||||
|
||||
func mapMediaInfo(_ json: [String: Any]) -> (dictionary: [String: Any], keys: [String]) {
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
if let dimensions = json["dimensions"] as? [String: Any], let height = dimensions["height"] as? UInt64, let width = dimensions["width"] as? UInt64 {
|
||||
keys.append("Dimensions")
|
||||
dic["Dimensions"] = "\(width)x\(height)"
|
||||
}
|
||||
if let location = json["location"] as? [String: Any], let latitude = location["latitude"] as? Double, let longitude = location["longitude"] as? Double {
|
||||
|
||||
DropboxFileProvider.decimalFormatter.numberStyle = .decimal
|
||||
DropboxFileProvider.decimalFormatter.maximumFractionDigits = 5
|
||||
keys.append("Location")
|
||||
let latStr = DropboxFileProvider.decimalFormatter.string(from: NSNumber(value: latitude))!
|
||||
let longStr = DropboxFileProvider.decimalFormatter.string(from: NSNumber(value: longitude))!
|
||||
dic["Location"] = "\(latStr), \(longStr)"
|
||||
}
|
||||
if let timeTakenStr = json["time_taken"] as? String, let timeTaken = Date(rfcString: timeTakenStr) {
|
||||
keys.append("Date taken")
|
||||
DropboxFileProvider.dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
dic["Date taken"] = DropboxFileProvider.dateFormatter.string(from: timeTaken)
|
||||
}
|
||||
if let duration = json["duration"] as? UInt64 {
|
||||
keys.append("Duration")
|
||||
dic["Duration"] = TimeInterval(duration).formatshort
|
||||
}
|
||||
return (dic, keys)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,530 @@
|
||||
//
|
||||
// ExtendedLocalFileProvider.swift
|
||||
// FileProvider
|
||||
//
|
||||
// Created by Amir Abbas Mousavian.
|
||||
// Copyright © 2017 Mousavian. Distributed under MIT license.
|
||||
//
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS)
|
||||
import Foundation
|
||||
import ImageIO
|
||||
import CoreGraphics
|
||||
import AVFoundation
|
||||
|
||||
extension LocalFileProvider: ExtendedFileProvider {
|
||||
open func thumbnailOfFileSupported(path: String) -> Bool {
|
||||
switch (path as NSString).pathExtension.lowercased() {
|
||||
case LocalFileInformationGenerator.imageThumbnailExtensions.contains:
|
||||
return true
|
||||
case LocalFileInformationGenerator.audioThumbnailExtensions.contains:
|
||||
return true
|
||||
case LocalFileInformationGenerator.videoThumbnailExtensions.contains:
|
||||
return true
|
||||
case LocalFileInformationGenerator.pdfThumbnailExtensions.contains:
|
||||
return true
|
||||
case LocalFileInformationGenerator.officeThumbnailExtensions.contains:
|
||||
return true
|
||||
case LocalFileInformationGenerator.customThumbnailExtensions.contains:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
open func propertiesOfFileSupported(path: String) -> Bool {
|
||||
let fileExt = (path as NSString).pathExtension.lowercased()
|
||||
switch fileExt {
|
||||
case LocalFileInformationGenerator.imagePropertiesExtensions.contains:
|
||||
return LocalFileInformationGenerator.imageProperties != nil
|
||||
case LocalFileInformationGenerator.audioPropertiesExtensions.contains:
|
||||
return LocalFileInformationGenerator.audioProperties != nil
|
||||
case LocalFileInformationGenerator.videoPropertiesExtensions.contains:
|
||||
return LocalFileInformationGenerator.videoProperties != nil
|
||||
case LocalFileInformationGenerator.pdfPropertiesExtensions.contains:
|
||||
return LocalFileInformationGenerator.pdfProperties != nil
|
||||
case LocalFileInformationGenerator.archivePropertiesExtensions.contains:
|
||||
return LocalFileInformationGenerator.archiveProperties != nil
|
||||
case LocalFileInformationGenerator.officePropertiesExtensions.contains:
|
||||
return LocalFileInformationGenerator.officeProperties != nil
|
||||
case LocalFileInformationGenerator.customPropertiesExtensions.contains:
|
||||
return LocalFileInformationGenerator.customProperties != nil
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
// Check cache
|
||||
let fileURL = self.url(of: path)
|
||||
// Create Thumbnail and cache
|
||||
switch fileURL.pathExtension.lowercased() {
|
||||
case LocalFileInformationGenerator.videoThumbnailExtensions.contains:
|
||||
thumbnailImage = LocalFileInformationGenerator.videoThumbnail(fileURL)
|
||||
case LocalFileInformationGenerator.audioThumbnailExtensions.contains:
|
||||
thumbnailImage = LocalFileInformationGenerator.audioThumbnail(fileURL)
|
||||
case LocalFileInformationGenerator.imageThumbnailExtensions.contains:
|
||||
thumbnailImage = LocalFileInformationGenerator.imageThumbnail(fileURL)
|
||||
case LocalFileInformationGenerator.pdfThumbnailExtensions.contains:
|
||||
thumbnailImage = LocalFileInformationGenerator.pdfThumbnail(fileURL)
|
||||
case LocalFileInformationGenerator.officeThumbnailExtensions.contains:
|
||||
thumbnailImage = LocalFileInformationGenerator.officeThumbnail(fileURL)
|
||||
case LocalFileInformationGenerator.customThumbnailExtensions.contains:
|
||||
thumbnailImage = LocalFileInformationGenerator.customThumbnail(fileURL)
|
||||
default:
|
||||
completionHandler(nil, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if let image = thumbnailImage {
|
||||
let scaledImage = LocalFileProvider.scaleDown(image: image, toSize: dimension)
|
||||
completionHandler(scaledImage, nil)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@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()
|
||||
var getter: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))?
|
||||
switch fileExt {
|
||||
case LocalFileInformationGenerator.imagePropertiesExtensions.contains:
|
||||
getter = LocalFileInformationGenerator.imageProperties
|
||||
case LocalFileInformationGenerator.audioPropertiesExtensions.contains:
|
||||
getter = LocalFileInformationGenerator.audioProperties
|
||||
case LocalFileInformationGenerator.videoPropertiesExtensions.contains:
|
||||
getter = LocalFileInformationGenerator.videoProperties
|
||||
case LocalFileInformationGenerator.pdfPropertiesExtensions.contains:
|
||||
getter = LocalFileInformationGenerator.pdfProperties
|
||||
case LocalFileInformationGenerator.archivePropertiesExtensions.contains:
|
||||
getter = LocalFileInformationGenerator.archiveProperties
|
||||
case LocalFileInformationGenerator.officePropertiesExtensions.contains:
|
||||
getter = LocalFileInformationGenerator.officeProperties
|
||||
case LocalFileInformationGenerator.customPropertiesExtensions.contains:
|
||||
getter = LocalFileInformationGenerator.customProperties
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
if let getterMethod = getter {
|
||||
(dic, keys) = getterMethod(self.url(of: path))
|
||||
}
|
||||
|
||||
completionHandler(dic, keys, nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds supported file types and thumbnail/properties generator for specefied type of file
|
||||
public struct LocalFileInformationGenerator {
|
||||
/// Image extensions supportes for thumbnail.
|
||||
///
|
||||
/// Default: `["jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff", "ico"]`
|
||||
static public var imageThumbnailExtensions: [String] = ["heic", "jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff", "ico"]
|
||||
|
||||
/// Audio and music extensions supportes for thumbnail.
|
||||
///
|
||||
/// Default: `["mp1", "mp2", "mp3", "mpa", "mpga", "m1a", "m2a", "m4a", "m4b", "m4p", "m4r", "aac", "snd", "caf", "aa", "aax", "adts", "aif", "aifc", "aiff", "au", "flac", "amr", "wav", "wave", "bwf", "ac3", "eac3", "ec3", "cdda"]`
|
||||
static public var audioThumbnailExtensions: [String] = ["mp1", "mp2", "mp3", "mpa", "mpga", "m1a", "m2a", "m4a", "m4b", "m4p", "m4r", "aac", "snd", "caf", "aa", "aax", "adts", "aif", "aifc", "aiff", "au", "flac", "amr", "wav", "wave", "bwf", "ac3", "eac3", "ec3", "cdda"]
|
||||
|
||||
/// Video extensions supportes for thumbnail.
|
||||
///
|
||||
/// Default: `["mov", "mp4", "mpg4", "m4v", "mqv", "mpg", "mpeg", "avi", "vfw", "3g2", "3gp", "3gp2", "3gpp", "qt"]`
|
||||
static public var videoThumbnailExtensions: [String] = ["mov", "mp4", "mpg4", "m4v", "mqv", "mpg", "mpeg", "avi", "vfw", "3g2", "3gp", "3gp2", "3gpp", "qt"]
|
||||
|
||||
/// Portable document file extensions supportes for thumbnail.
|
||||
///
|
||||
/// Default: `["pdf"]`
|
||||
static public var pdfThumbnailExtensions: [String] = ["pdf"]
|
||||
|
||||
/// Office document extensions supportes for thumbnail.
|
||||
///
|
||||
/// Default: `empty`
|
||||
static public var officeThumbnailExtensions: [String] = []
|
||||
|
||||
/// Custom document extensions supportes for thumbnail.
|
||||
///
|
||||
/// Default: `empty`
|
||||
static public var customThumbnailExtensions: [String] = []
|
||||
|
||||
|
||||
/// Image extensions supportes for properties.
|
||||
///
|
||||
/// Default: `["jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff"]`
|
||||
static public var imagePropertiesExtensions: [String] = ["heic", "jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff"]
|
||||
|
||||
/// Audio and music extensions supportes for properties.
|
||||
///
|
||||
/// Default: `["mp1", "mp2", "mp3", "mpa", "mpga", "m1a", "m2a", "m4a", "m4b", "m4p", "m4r", "aac", "snd", "caf", "aa", "aax", "adts", "aif", "aifc", "aiff", "au", "flac", "amr", "wav", "wave", "bwf", "ac3", "eac3", "ec3", "cdda"]`
|
||||
static public var audioPropertiesExtensions: [String] = ["mp1", "mp2", "mp3", "mpa", "mpga", "m1a", "m2a", "m4a", "m4b", "m4p", "m4r", "aac", "snd", "caf", "aa", "aax", "adts", "aif", "aifc", "aiff", "au", "flac", "amr", "wav", "wave", "bwf", "ac3", "eac3", "ec3", "cdda"]
|
||||
|
||||
/// Video extensions supportes for properties.
|
||||
///
|
||||
/// Default: `["mov", "mp4", "mpg4", "m4v", "mqv", "mpg", "mpeg", "avi", "vfw", "3g2", "3gp", "3gp2", "3gpp", "qt"]`
|
||||
static public var videoPropertiesExtensions: [String] = ["mov", "mp4", "mpg4", "m4v", "mqv", "mpg", "mpeg", "avi", "vfw", "3g2", "3gp", "3gp2", "3gpp", "qt"]
|
||||
|
||||
/// Portable document file extensions supportes for properties.
|
||||
///
|
||||
/// Default: `["pdf"]`
|
||||
static public var pdfPropertiesExtensions: [String] = ["pdf"]
|
||||
|
||||
/// Archive extensions (like zip) supportes for properties.
|
||||
///
|
||||
/// Default: `empty`
|
||||
static public var archivePropertiesExtensions: [String] = []
|
||||
|
||||
/// Office document extensions supportes for properties.
|
||||
///
|
||||
/// Default: `empty`
|
||||
static public var officePropertiesExtensions: [String] = []
|
||||
|
||||
/// Custom document extensions supportes for properties.
|
||||
///
|
||||
/// Default: `empty`
|
||||
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)
|
||||
}
|
||||
|
||||
/// Thumbnail generator closure for audio and music files.
|
||||
static public var audioThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL 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 nil
|
||||
}
|
||||
|
||||
/// Thumbnail generator closure for video files.
|
||||
static public var videoThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in
|
||||
let asset = AVAsset(url: fileURL)
|
||||
let assetImgGenerate = AVAssetImageGenerator(asset: asset)
|
||||
assetImgGenerate.appliesPreferredTrackTransform = true
|
||||
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)
|
||||
#else
|
||||
return ImageClass(cgImage: cgImage)
|
||||
#endif
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Thumbnail generator closure for portable document files files.
|
||||
static public var pdfThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in
|
||||
return LocalFileProvider.convertToImage(pdfURL: fileURL)
|
||||
}
|
||||
|
||||
/// Thumbnail generator closure for office document files.
|
||||
/// - Note: No default implementation is avaiable
|
||||
static public var officeThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL 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
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Properties generator closure for image files.
|
||||
static public var imageProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
|
||||
func add(key: String, value: Any?) {
|
||||
if let value = value, !((value as? String)?.isEmpty ?? false) {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
func simplify(_ top:Int64, _ bottom:Int64) -> (newTop:Int, newBottom:Int) {
|
||||
var x = top
|
||||
var y = bottom
|
||||
while (y != 0) {
|
||||
let buffer = y
|
||||
y = x % y
|
||||
x = buffer
|
||||
}
|
||||
let hcfVal = x
|
||||
let newTopVal = top/hcfVal
|
||||
let newBottomVal = bottom/hcfVal
|
||||
return(Int(newTopVal), Int(newBottomVal))
|
||||
}
|
||||
|
||||
guard let cgDataRef = CGImageSourceCreateWithURL(fileURL as CFURL, nil), let cfImageDict = CGImageSourceCopyPropertiesAtIndex(cgDataRef, 0, nil) 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 ?? [:]
|
||||
let gpsDict = imageDict[kCGImagePropertyGPSDictionary as String] as? NSDictionary ?? [:]
|
||||
|
||||
if let pixelWidth = imageDict.object(forKey: kCGImagePropertyPixelWidth) as? NSNumber, let pixelHeight = imageDict.object(forKey: kCGImagePropertyPixelHeight) as? NSNumber {
|
||||
add(key: "Dimensions", value: "\(pixelWidth)x\(pixelHeight)")
|
||||
}
|
||||
add(key: "DPI", value: imageDict[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 = 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: "Area", value: gpsDict[kCGImagePropertyGPSAreaInformation as String])
|
||||
|
||||
add(key: "Color space", value: imageDict[kCGImagePropertyColorModel as String])
|
||||
add(key: "Color depth", value: (imageDict[kCGImagePropertyDepth as String] as? NSNumber).map({ "\($0) bits" }))
|
||||
add(key: "Color profile", value: imageDict[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])
|
||||
|
||||
if let exp = exifDict[kCGImagePropertyExifExposureTime as String] as? NSNumber {
|
||||
let expfrac = simplify(Int64(exp.doubleValue * 1_163_962_800_000), 1_163_962_800_000)
|
||||
add(key: "Exposure time", value: "\(expfrac.newTop)/\(expfrac.newBottom)")
|
||||
}
|
||||
add(key: "ISO speed", value: (exifDict[kCGImagePropertyExifISOSpeedRatings as String] as? [NSNumber])?.first)
|
||||
return (dic, keys)
|
||||
}
|
||||
|
||||
/// Properties generator closure for audio and music files.
|
||||
static var audioProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
|
||||
func add(key: String, value: Any?) {
|
||||
if let value = value {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
func makeKeyDescription(_ key: String?) -> String? {
|
||||
guard let key = key else {
|
||||
return nil
|
||||
}
|
||||
guard let regex = try? NSRegularExpression(pattern: "([a-z])([A-Z])" , options: []) else {
|
||||
return nil
|
||||
}
|
||||
let newKey = regex.stringByReplacingMatches(in: key, options: [], range: NSRange(location: 0, length: (key as NSString).length) , withTemplate: "$1 $2")
|
||||
return newKey.capitalized
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
}
|
||||
}
|
||||
if let ap = try? AVAudioPlayer(contentsOf: fileURL) {
|
||||
add(key: "Duration", value: ap.duration.formatshort)
|
||||
add(key: "Bitrate", value: ap.settings[AVSampleRateKey] as? Int)
|
||||
}
|
||||
return (dic, keys)
|
||||
}
|
||||
|
||||
/// Properties generator closure for video files.
|
||||
static public var videoProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
|
||||
func add(key: String, value: Any?) {
|
||||
if let value = value {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if let audioprops = LocalFileInformationGenerator.audioProperties?(fileURL) {
|
||||
dic = audioprops.prop
|
||||
keys = audioprops.keys
|
||||
dic.removeValue(forKey: "Duration")
|
||||
if let index = keys.index(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)
|
||||
let height = Int(videoTrack.naturalSize.height)
|
||||
add(key: "Dimensions", value: "\(width)x\(height)")
|
||||
var duration: Int64 = 0
|
||||
for track in videoTracks {
|
||||
duration += track.timeRange.duration.timescale > 0 ? track.timeRange.duration.value / Int64(track.timeRange.duration.timescale) : 0
|
||||
bitrate += track.estimatedDataRate
|
||||
}
|
||||
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 {
|
||||
bitrate += track.estimatedDataRate
|
||||
}
|
||||
add(key: "Audio Bitrate", value: "\(Int(ceil(bitrate / 1000))) kbps")
|
||||
return (dic, keys)
|
||||
}
|
||||
|
||||
/// Properties generator closure for protable documents files.
|
||||
static public var pdfProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
|
||||
func add(key: String, value: Any?) {
|
||||
if let value = value, !((value as? String)?.isEmpty ?? false) {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
func getKey(_ key: String, from dict: CGPDFDictionaryRef) -> String? {
|
||||
var cfStrValue: CGPDFStringRef?
|
||||
if (CGPDFDictionaryGetString(dict, key, &cfStrValue)), let value = cfStrValue.flatMap({ CGPDFStringCopyTextString($0) }) {
|
||||
return value as String
|
||||
}
|
||||
var cfArrayValue: CGPDFArrayRef?
|
||||
if (CGPDFDictionaryGetArray(dict, key, &cfArrayValue)), let cfArray = cfArrayValue {
|
||||
var array = [String]()
|
||||
for i in 0..<CGPDFArrayGetCount(cfArray) {
|
||||
var cfItemValue: CGPDFStringRef?
|
||||
if CGPDFArrayGetString(cfArray, i, &cfItemValue), let item = cfItemValue.flatMap({ CGPDFStringCopyTextString($0) }) {
|
||||
array.append(item as String)
|
||||
}
|
||||
}
|
||||
return array.joined(separator: ", ")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertDate(_ date: String?) -> Date? {
|
||||
guard let date = date else { return nil }
|
||||
let dateStr = date.replacingOccurrences(of: "'", with: "").replacingOccurrences(of: "D:", with: "", options: .anchored)
|
||||
let dateFormatter = DateFormatter()
|
||||
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
|
||||
}
|
||||
|
||||
guard let provider = CGDataProvider(url: fileURL as CFURL), let reference = CGPDFDocument(provider), let dict = reference.info else {
|
||||
return (dic, keys)
|
||||
}
|
||||
add(key: "Title", value: getKey("Title", from: dict))
|
||||
add(key: "Author", value: getKey("Author", from: dict))
|
||||
add(key: "Subject", value: getKey("Subject", from: dict))
|
||||
add(key: "Producer", value: getKey("Producer", from: dict))
|
||||
add(key: "Keywords", value: getKey("Keywords", from: dict))
|
||||
var majorVersion: Int32 = 0
|
||||
var minorVersion: Int32 = 0
|
||||
reference.getVersion(majorVersion: &majorVersion, minorVersion: &minorVersion)
|
||||
if majorVersion > 0 {
|
||||
add(key: "Version", value: String(majorVersion) + "." + String(minorVersion))
|
||||
}
|
||||
add(key: "Pages", value: reference.numberOfPages)
|
||||
|
||||
if reference.numberOfPages > 0, let pageRef = reference.page(at: 1) {
|
||||
let size = pageRef.getBoxRect(CGPDFBox.mediaBox).size
|
||||
add(key: "Resolution", value: "\(Int(size.width))x\(Int(size.height))")
|
||||
}
|
||||
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: "Allows printing", value: reference.allowsPrinting)
|
||||
add(key: "Allows copying", value: reference.allowsCopying)
|
||||
return (dic, keys)
|
||||
}
|
||||
|
||||
/// Properties generator closure for video files.
|
||||
/// - Note: No default implementation is avaiable
|
||||
static public var archiveProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = nil
|
||||
|
||||
/// Properties generator closure for office doument files.
|
||||
/// - Note: No default implementation is avaiable
|
||||
static public var officeProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = nil
|
||||
|
||||
/// Properties generator closure for custom type of files.
|
||||
/// - Note: No default implementation is avaiable
|
||||
static public var customProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = nil
|
||||
}
|
||||
#endif
|
||||
+616
@@ -0,0 +1,616 @@
|
||||
//
|
||||
// FileProviderExtensions.swift
|
||||
// FileProvider
|
||||
//
|
||||
// Created by Amir Abbas on 12/27/1395 AP.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public 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)
|
||||
return sorting.sort(self) as! [Element]
|
||||
}
|
||||
|
||||
/// Sorts array of `FileObject`s by criterias set in attributes.
|
||||
public mutating func sorted(by type: FileObjectSorting.SortType, ascending: Bool = true, isDirectoriesFirst: Bool = false) {
|
||||
self = self.sort(by: type, ascending: ascending, isDirectoriesFirst: isDirectoriesFirst)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
/// **FileProvider** returns corresponding `URLFileResourceType` of a `FileAttributeType` value
|
||||
public init(fileTypeValue: FileAttributeType) {
|
||||
switch fileTypeValue {
|
||||
case FileAttributeType.typeCharacterSpecial: self = .characterSpecial
|
||||
case FileAttributeType.typeDirectory: self = .directory
|
||||
case FileAttributeType.typeBlockSpecial: self = .blockSpecial
|
||||
case FileAttributeType.typeRegular: self = .regular
|
||||
case FileAttributeType.typeSymbolicLink: self = .symbolicLink
|
||||
case FileAttributeType.typeSocket: self = .socket
|
||||
case FileAttributeType.typeUnknown: self = .unknown
|
||||
default: self = .unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension URLResourceKey {
|
||||
/// **FileProvider** returns url of file object.
|
||||
public static let fileURLKey = URLResourceKey(rawValue: "NSURLFileURLKey")
|
||||
/// **FileProvider** returns modification date of file in server
|
||||
public static let serverDateKey = URLResourceKey(rawValue: "NSURLServerDateKey")
|
||||
/// **FileProvider** returns HTTP ETag string of remote resource
|
||||
public static let entryTagKey = URLResourceKey(rawValue: "NSURLEntryTagKey")
|
||||
/// **FileProvider** returns MIME type of file, if returned by server
|
||||
public static let mimeTypeKey = URLResourceKey(rawValue: "NSURLMIMETypeIdentifierKey")
|
||||
/// **FileProvider** returns either file is encrypted or not
|
||||
public static let isEncryptedKey = URLResourceKey(rawValue: "NSURLIsEncryptedKey")
|
||||
/// **FileProvider** count of items in directory
|
||||
public static let childrensCount = URLResourceKey(rawValue: "MFPURLChildrensCount")
|
||||
}
|
||||
|
||||
public extension ProgressUserInfoKey {
|
||||
/// **FileProvider** returns associated `FileProviderOperationType`
|
||||
public static let fileProvderOperationTypeKey = ProgressUserInfoKey("MFPOperationTypeKey")
|
||||
/// **FileProvider** returns start date/time of operation
|
||||
public static let startingTimeKey = ProgressUserInfoKey("NSProgressStartingTimeKey")
|
||||
}
|
||||
|
||||
internal extension URL {
|
||||
var uw_scheme: String {
|
||||
return self.scheme ?? ""
|
||||
}
|
||||
|
||||
var fileIsDirectory: Bool {
|
||||
return (try? self.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory ?? false
|
||||
}
|
||||
|
||||
var fileSize: Int64 {
|
||||
return Int64((try? self.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? -1)
|
||||
}
|
||||
|
||||
var fileExists: Bool {
|
||||
return (try? self.checkResourceIsReachable()) ?? false
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS)
|
||||
#else
|
||||
func checkPromisedItemIsReachable() throws -> Bool {
|
||||
return false
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public extension URLRequest {
|
||||
/// Defines HTTP Authentication method required to access
|
||||
public enum AuthenticationType {
|
||||
/// Basic method for authentication
|
||||
case basic
|
||||
/// Digest method for authentication
|
||||
case digest
|
||||
/// OAuth 1.0 method for authentication (OAuth)
|
||||
case oAuth1
|
||||
/// OAuth 2.0 method for authentication (Bearer)
|
||||
case oAuth2
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 setValue(authentication credential: URLCredential?, with type: AuthenticationType) {
|
||||
func base64(_ str: String) -> String {
|
||||
let plainData = str.data(using: .utf8)
|
||||
let base64String = plainData!.base64EncodedString(options: [])
|
||||
return base64String
|
||||
}
|
||||
|
||||
guard let credential = credential else { return }
|
||||
switch type {
|
||||
case .basic:
|
||||
let user = credential.user?.replacingOccurrences(of: ":", with: "") ?? ""
|
||||
let pass = credential.password ?? ""
|
||||
let authStr = "\(user):\(pass)"
|
||||
if let base64Auth = authStr.data(using: .utf8)?.base64EncodedString() {
|
||||
self.setValue("Basic \(base64Auth)", forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
case .digest:
|
||||
// handled by RemoteSessionDelegate
|
||||
break
|
||||
case .oAuth1:
|
||||
if let oauth = credential.password {
|
||||
self.setValue("OAuth \(oauth)", forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
case .oAuth2:
|
||||
if let bearer = credential.password {
|
||||
self.setValue("Bearer \(bearer)", forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutating func setValue(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.setValue("\(charsetString);q=\(qualityDesc)", forHTTPHeaderField: "Accept-Charset")
|
||||
} else {
|
||||
self.setValue(charsetString, forHTTPHeaderField: "Accept-Charset")
|
||||
}
|
||||
}
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Encoding: String {
|
||||
case all = "*"
|
||||
case identity
|
||||
case gzip
|
||||
case deflate
|
||||
}
|
||||
|
||||
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 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 setValue(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.setValue("\(langCode);q=\(qualityDesc)", forHTTPHeaderField: "Accept-Language")
|
||||
} else {
|
||||
self.setValue(langCode, 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 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 {
|
||||
self.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
|
||||
}
|
||||
}
|
||||
|
||||
mutating func setValue(range: Range<Int>) {
|
||||
let range = max(0, range.lowerBound)..<range.upperBound
|
||||
if range.upperBound < Int.max && range.count > 0 {
|
||||
self.setValue("bytes=\(range.lowerBound)-\(range.upperBound - 1)", forHTTPHeaderField: "Range")
|
||||
} else if range.lowerBound > 0 {
|
||||
self.setValue("bytes=\(range.lowerBound)-", forHTTPHeaderField: "Range")
|
||||
}
|
||||
}
|
||||
|
||||
mutating func setValue(contentRange range: Range<Int64>, totalBytes: Int64) {
|
||||
let range = max(0, range.lowerBound)..<range.upperBound
|
||||
if range.upperBound < Int.max && range.count > 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")
|
||||
}
|
||||
}
|
||||
|
||||
mutating func setValue(contentType: ContentMIMEType, charset: String.Encoding? = nil) {
|
||||
var parameter = ""
|
||||
if let charset = charset {
|
||||
let cfEncoding = CFStringConvertNSStringEncodingToEncoding(charset.rawValue)
|
||||
if let charsetString = CFStringConvertEncodingToIANACharSetName(cfEncoding) as String? {
|
||||
parameter = ";charset=" + charsetString
|
||||
}
|
||||
}
|
||||
|
||||
self.setValue(contentType.rawValue + parameter, forHTTPHeaderField: "Content-Type")
|
||||
}
|
||||
|
||||
mutating func setValue(dropboxArgKey requestDictionary: [String: AnyObject]) {
|
||||
guard let jsonData = try? JSONSerialization.data(withJSONObject: requestDictionary, options: []) else {
|
||||
return
|
||||
}
|
||||
guard var jsonString = String(data: jsonData, encoding: .utf8) else { return }
|
||||
jsonString = jsonString.asciiEscaped().replacingOccurrences(of: "\\/", with: "/")
|
||||
|
||||
self.setValue(jsonString, forHTTPHeaderField: "Dropbox-API-Arg")
|
||||
}
|
||||
}
|
||||
|
||||
internal extension CharacterSet {
|
||||
static let filePathAllowed = CharacterSet.urlPathAllowed.subtracting(CharacterSet(charactersIn: ":"))
|
||||
}
|
||||
|
||||
internal extension Data {
|
||||
internal var isPDF: Bool {
|
||||
return self.count > 4 && self.scanString(length: 4, using: .ascii) == "%PDF"
|
||||
}
|
||||
|
||||
init? (jsonDictionary dictionary: [String: AnyObject]) {
|
||||
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]? {
|
||||
return (try? JSONSerialization.jsonObject(with: self, options: [])) as? [String: AnyObject]
|
||||
}
|
||||
|
||||
init<T>(value: T) {
|
||||
var value = value
|
||||
self = Data(buffer: UnsafeBufferPointer(start: &value, count: 1))
|
||||
}
|
||||
|
||||
func scanValue<T>() -> T? {
|
||||
guard MemoryLayout<T>.size <= self.count else { return nil }
|
||||
return self.withUnsafeBytes { $0.pointee }
|
||||
}
|
||||
|
||||
func scanValue<T>(start: Int) -> T? {
|
||||
let length = MemoryLayout<T>.size
|
||||
guard self.count >= start + length else { return nil }
|
||||
return self.subdata(in: start..<start+length).withUnsafeBytes { $0.pointee }
|
||||
}
|
||||
|
||||
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..<start+length), encoding: encoding)
|
||||
}
|
||||
}
|
||||
|
||||
internal extension String {
|
||||
init? (jsonDictionary: [String: AnyObject]) {
|
||||
guard let data = Data(jsonDictionary: jsonDictionary) else {
|
||||
return nil
|
||||
}
|
||||
self.init(data: data, encoding: .utf8)
|
||||
}
|
||||
|
||||
func deserializeJSON(using encoding: String.Encoding = .utf8) -> [String: AnyObject]? {
|
||||
guard let data = self.data(using: encoding) else {
|
||||
return nil
|
||||
}
|
||||
return data.deserializeJSON()
|
||||
}
|
||||
|
||||
func asciiEscaped() -> String {
|
||||
var res = ""
|
||||
for char in self.unicodeScalars {
|
||||
let substring = String(char)
|
||||
if substring.canBeConverted(to: .ascii) {
|
||||
res.append(substring)
|
||||
} else {
|
||||
res = res.appendingFormat("\\u%04x", char.value)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
internal 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)!
|
||||
}
|
||||
}
|
||||
|
||||
internal extension TimeInterval {
|
||||
internal var formatshort: String {
|
||||
var result = "0:00"
|
||||
if self < TimeInterval(Int32.max) {
|
||||
result = ""
|
||||
var time = DateComponents()
|
||||
time.hour = Int(self / 3600)
|
||||
time.minute = Int((self.truncatingRemainder(dividingBy: 3600)) / 60)
|
||||
time.second = Int(self.truncatingRemainder(dividingBy: 60))
|
||||
let formatter = NumberFormatter()
|
||||
formatter.paddingCharacter = "0"
|
||||
formatter.minimumIntegerDigits = 2
|
||||
formatter.maximumFractionDigits = 0
|
||||
let formatterFirst = NumberFormatter()
|
||||
formatterFirst.maximumFractionDigits = 0
|
||||
if time.hour! > 0 {
|
||||
result = "\(formatterFirst.string(from: NSNumber(value: time.hour!))!):\(formatter.string(from: NSNumber(value: time.minute!))!):\(formatter.string(from: NSNumber(value: time.second!))!)"
|
||||
} else {
|
||||
result = "\(formatterFirst.string(from: NSNumber(value: time.minute!))!):\(formatter.string(from: NSNumber(value: time.second!))!)"
|
||||
}
|
||||
}
|
||||
result = result.trimmingCharacters(in: CharacterSet(charactersIn: ": "))
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
public extension Date {
|
||||
/// Date formats used commonly in internet messaging defined by various RFCs.
|
||||
public enum RFCStandards: String {
|
||||
/// 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 1123 for http.
|
||||
case rfc1123 = "EEE',' dd' 'MMM' 'yyyy HH':'mm':'ss z"
|
||||
/// 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 RFC 850.
|
||||
public static let usenet = RFCStandards.rfc850
|
||||
|
||||
/* 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 = Date.posixLocale
|
||||
|
||||
for standard in RFCStandards.parsingCases {
|
||||
dateFor.dateFormat = standard.rawValue
|
||||
if let date = dateFor.date(from: rfcString) {
|
||||
self = date
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Formats date according to RFCs standard.
|
||||
/// - 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 ?? Date.utcTimezone
|
||||
fm.locale = locale ?? Date.posixLocale
|
||||
return fm.string(from: self)
|
||||
}
|
||||
}
|
||||
|
||||
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 }
|
||||
return val.first?.value
|
||||
}
|
||||
|
||||
func findAllValues(forKey key: String?) -> [(value: Any, operator: NSComparisonPredicate.Operator, not: Bool)] {
|
||||
if let cQuery = self as? NSCompoundPredicate {
|
||||
let find = cQuery.subpredicates.flatMap { ($0 as! NSPredicate).findAllValues(forKey: key) }
|
||||
if cQuery.compoundPredicateType == .not {
|
||||
return find.map { return ($0.value, $0.operator, !$0.not) }
|
||||
}
|
||||
return find
|
||||
} else if let cQuery = self as? NSComparisonPredicate {
|
||||
if cQuery.leftExpression.expressionType == .keyPath, key == nil || cQuery.leftExpression.keyPath == key!, let const = cQuery.rightExpression.constantValue {
|
||||
return [(value: const, operator: cQuery.predicateOperatorType, false)]
|
||||
}
|
||||
if cQuery.rightExpression.expressionType == .keyPath, key == nil || cQuery.rightExpression.keyPath == key!, let const = cQuery.leftExpression.constantValue {
|
||||
return [(value: const, operator: cQuery.predicateOperatorType, false)]
|
||||
}
|
||||
return []
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ~=<T>(pattern: (T) -> Bool, value: T) -> Bool {
|
||||
return pattern(value)
|
||||
}
|
||||
|
||||
func hasPrefix(_ prefix: String) -> (_ value: String) -> Bool {
|
||||
return { (value: String) -> Bool in
|
||||
value.hasPrefix(prefix)
|
||||
}
|
||||
}
|
||||
|
||||
func hasSuffix(_ suffix: String) -> (_ value: String) -> Bool {
|
||||
return { (value: String) -> Bool in
|
||||
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<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult] {
|
||||
return try self.flatMap(transform)
|
||||
}
|
||||
}
|
||||
|
||||
extension ArraySlice {
|
||||
func compactMap<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult] {
|
||||
return try self.flatMap(transform)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -0,0 +1,448 @@
|
||||
// Originally based on CryptoSwift by Marcin Krzyżanowski <marcin.krzyzanowski@gmail.com>
|
||||
// Copyright (C) 2014 Marcin Krzyżanowski <marcin.krzyzanowski@gmail.com>
|
||||
// This software is provided 'as-is', without any express or implied warranty.
|
||||
//
|
||||
// In no event will the authors be held liable for any damages arising from the use of this software.
|
||||
//
|
||||
// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
|
||||
//
|
||||
// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required.
|
||||
// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
|
||||
// - This notice may not be removed or altered from any source or binary distribution.
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol SHA2Variant {
|
||||
static var size: Int { get }
|
||||
static var h: [UInt64] { get }
|
||||
static var k: [UInt64] { get }
|
||||
|
||||
static func resultingArray<T>(_ hh:[T]) -> ArraySlice<T>
|
||||
static func calculate(_ message: [UInt8]) -> [UInt8]
|
||||
}
|
||||
|
||||
protocol SHA2Variant32: SHA2Variant { }
|
||||
protocol SHA2Variant64: SHA2Variant { }
|
||||
|
||||
extension SHA2Variant32 {
|
||||
static var size: Int {
|
||||
return 64
|
||||
}
|
||||
|
||||
static var k: [UInt64] {
|
||||
return [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2]
|
||||
}
|
||||
|
||||
// codebeat:disable[ABC]
|
||||
static func calculate(_ message: [UInt8]) -> [UInt8] {
|
||||
var tmpMessage = message
|
||||
|
||||
let len = Self.size
|
||||
|
||||
// Step 1. Append Padding Bits
|
||||
tmpMessage.append(0x80) // append one bit (UInt8 with one bit) to message
|
||||
|
||||
// append "0" bit until message length in bits ≡ 448 (mod 512)
|
||||
var msgLength = tmpMessage.count
|
||||
var counter = 0
|
||||
|
||||
while msgLength % len != (len - 8) {
|
||||
counter += 1
|
||||
msgLength += 1
|
||||
}
|
||||
|
||||
tmpMessage.append(contentsOf: [UInt8](repeating: 0, count: counter))
|
||||
|
||||
// hash values
|
||||
var hh: [UInt32] = Self.h.map { UInt32($0) }
|
||||
let k = Self.k
|
||||
|
||||
// append message length, in a 64-bit big-endian integer. So now the message length is a multiple of 512 bits.
|
||||
tmpMessage.append(contentsOf: arrayOfBytes(message.count * 8, length: 64 / 8))
|
||||
|
||||
// Process the message in successive 512-bit chunks:
|
||||
let chunkSizeBytes = 512 / 8 // 64
|
||||
for chunk in BytesSequence(chunkSize: chunkSizeBytes, data: tmpMessage) {
|
||||
// break chunk into sixteen 32-bit words M[j], 0 ≤ j ≤ 15, big-endian
|
||||
// Extend the sixteen 32-bit words into sixty-four 32-bit words:
|
||||
var M:[UInt32] = [UInt32](repeating: 0, count: k.count)
|
||||
for x in 0..<M.count {
|
||||
switch (x) {
|
||||
case 0...15:
|
||||
let start: Int = chunk.startIndex + (x * MemoryLayout.size(ofValue: M[x]))
|
||||
let end: Int = start + MemoryLayout.size(ofValue: M[x])
|
||||
let le = toUInt32Array(chunk[start..<end])[0]
|
||||
M[x] = le.bigEndian
|
||||
break
|
||||
default:
|
||||
let s0 = rotateRight(M[x-15], n: 7) ^ rotateRight(M[x-15], n: 18) ^ (M[x-15] >> 3) //FIXME: n
|
||||
let s1 = rotateRight(M[x-2], n: 17) ^ rotateRight(M[x-2], n: 19) ^ (M[x-2] >> 10)
|
||||
let s2 = M[x-16]
|
||||
let s3 = M[x-7]
|
||||
M[x] = s2 &+ s0 &+ s3 &+ s1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var A = hh[0], B = hh[1], C = hh[2], D = hh[3], E = hh[4], F = hh[5], G = hh[6], H = hh[7]
|
||||
|
||||
// Main loop
|
||||
for j in 0..<k.count {
|
||||
let s0 = rotateRight(A,n: 2) ^ rotateRight(A,n: 13) ^ rotateRight(A,n: 22)
|
||||
let maj = (A & B) ^ (A & C) ^ (B & C)
|
||||
let t2 = s0 &+ maj
|
||||
let s1 = rotateRight(E,n: 6) ^ rotateRight(E,n: 11) ^ rotateRight(E,n: 25)
|
||||
let ch = (E & F) ^ ((~E) & G)
|
||||
let t1 = H &+ s1 &+ ch &+ UInt32(k[j]) &+ M[j]
|
||||
|
||||
H = G; G = F; F = E; E = D &+ t1
|
||||
D = C; C = B; B = A; A = t1 &+ t2
|
||||
}
|
||||
|
||||
hh[0] = (hh[0] &+ A)
|
||||
hh[1] = (hh[1] &+ B)
|
||||
hh[2] = (hh[2] &+ C)
|
||||
hh[3] = (hh[3] &+ D)
|
||||
hh[4] = (hh[4] &+ E)
|
||||
hh[5] = (hh[5] &+ F)
|
||||
hh[6] = (hh[6] &+ G)
|
||||
hh[7] = (hh[7] &+ H)
|
||||
}
|
||||
|
||||
// Produce the final hash value (big-endian) as a 160 bit number:
|
||||
var result = [UInt8]()
|
||||
result.reserveCapacity(hh.count / 4)
|
||||
Self.resultingArray(hh).forEach {
|
||||
let item = $0.bigEndian
|
||||
#if swift(>=4.0)
|
||||
result.append(UInt8(truncatingIfNeeded: item))
|
||||
result.append(UInt8(truncatingIfNeeded: item >> 8))
|
||||
result.append(UInt8(truncatingIfNeeded: item >> 16))
|
||||
result.append(UInt8(truncatingIfNeeded: item >> 24))
|
||||
#else
|
||||
result.append(UInt8(item))
|
||||
result.append(UInt8((item >> 8) & 0xFF))
|
||||
result.append(UInt8((item >> 16) & 0xFF))
|
||||
result.append(UInt8((item >> 24) & 0xFF))
|
||||
#endif
|
||||
|
||||
}
|
||||
return result
|
||||
}
|
||||
// codebeat:enable[ABC]
|
||||
}
|
||||
|
||||
extension SHA2Variant64 {
|
||||
static var size: Int {
|
||||
return 128
|
||||
}
|
||||
|
||||
static var k: [UInt64] {
|
||||
return [0x428a2f98d728ae22, 0x7137449123ef65cd, 0xb5c0fbcfec4d3b2f, 0xe9b5dba58189dbbc, 0x3956c25bf348b538,
|
||||
0x59f111f1b605d019, 0x923f82a4af194f9b, 0xab1c5ed5da6d8118, 0xd807aa98a3030242, 0x12835b0145706fbe,
|
||||
0x243185be4ee4b28c, 0x550c7dc3d5ffb4e2, 0x72be5d74f27b896f, 0x80deb1fe3b1696b1, 0x9bdc06a725c71235,
|
||||
0xc19bf174cf692694, 0xe49b69c19ef14ad2, 0xefbe4786384f25e3, 0x0fc19dc68b8cd5b5, 0x240ca1cc77ac9c65,
|
||||
0x2de92c6f592b0275, 0x4a7484aa6ea6e483, 0x5cb0a9dcbd41fbd4, 0x76f988da831153b5, 0x983e5152ee66dfab,
|
||||
0xa831c66d2db43210, 0xb00327c898fb213f, 0xbf597fc7beef0ee4, 0xc6e00bf33da88fc2, 0xd5a79147930aa725,
|
||||
0x06ca6351e003826f, 0x142929670a0e6e70, 0x27b70a8546d22ffc, 0x2e1b21385c26c926, 0x4d2c6dfc5ac42aed,
|
||||
0x53380d139d95b3df, 0x650a73548baf63de, 0x766a0abb3c77b2a8, 0x81c2c92e47edaee6, 0x92722c851482353b,
|
||||
0xa2bfe8a14cf10364, 0xa81a664bbc423001, 0xc24b8b70d0f89791, 0xc76c51a30654be30, 0xd192e819d6ef5218,
|
||||
0xd69906245565a910, 0xf40e35855771202a, 0x106aa07032bbd1b8, 0x19a4c116b8d2d0c8, 0x1e376c085141ab53,
|
||||
0x2748774cdf8eeb99, 0x34b0bcb5e19b48a8, 0x391c0cb3c5c95a63, 0x4ed8aa4ae3418acb, 0x5b9cca4f7763e373,
|
||||
0x682e6ff3d6b2b8a3, 0x748f82ee5defb2fc, 0x78a5636f43172f60, 0x84c87814a1f0ab72, 0x8cc702081a6439ec,
|
||||
0x90befffa23631e28, 0xa4506cebde82bde9, 0xbef9a3f7b2c67915, 0xc67178f2e372532b, 0xca273eceea26619c,
|
||||
0xd186b8c721c0c207, 0xeada7dd6cde0eb1e, 0xf57d4f7fee6ed178, 0x06f067aa72176fba, 0x0a637dc5a2c898a6,
|
||||
0x113f9804bef90dae, 0x1b710b35131c471b, 0x28db77f523047d84, 0x32caab7b40c72493, 0x3c9ebe0a15c9bebc,
|
||||
0x431d67c49c100d4c, 0x4cc5d4becb3e42b6, 0x597f299cfc657e2a, 0x5fcb6fab3ad6faec, 0x6c44198c4a475817]
|
||||
}
|
||||
|
||||
// codebeat:disable[ABC]
|
||||
static func calculate(_ message: [UInt8]) -> [UInt8] {
|
||||
var tmpMessage = message
|
||||
|
||||
let len = Self.size
|
||||
|
||||
// Step 1. Append Padding Bits
|
||||
tmpMessage.append(0x80) // append one bit (UInt8 with one bit) to message
|
||||
|
||||
// append "0" bit until message length in bits ≡ 448 (mod 512)
|
||||
var msgLength = tmpMessage.count
|
||||
var counter = 0
|
||||
|
||||
while msgLength % len != (len - 8) {
|
||||
counter += 1
|
||||
msgLength += 1
|
||||
}
|
||||
|
||||
tmpMessage += [UInt8](repeating: 0, count: counter)
|
||||
|
||||
// hash values
|
||||
var hh: [UInt64] = Self.h
|
||||
let k = Self.k
|
||||
|
||||
// append message length, in a 64-bit big-endian integer. So now the message length is a multiple of 512 bits.
|
||||
tmpMessage += arrayOfBytes(message.count * 8, length: 64 / 8)
|
||||
|
||||
// Process the message in successive 1024-bit chunks:
|
||||
let chunkSizeBytes = 1024 / 8 // 128
|
||||
for chunk in BytesSequence(chunkSize: chunkSizeBytes, data: tmpMessage) {
|
||||
// break chunk into sixteen 64-bit words M[j], 0 ≤ j ≤ 15, big-endian
|
||||
// Extend the sixteen 64-bit words into eighty 64-bit words:
|
||||
var M = [UInt64](repeating: 0, count: k.count)
|
||||
for x in 0..<M.count {
|
||||
switch (x) {
|
||||
case 0...15:
|
||||
let start = chunk.startIndex + (x * MemoryLayout.size(ofValue: M[x]))
|
||||
let end = start + MemoryLayout.size(ofValue: M[x])
|
||||
let le = toUInt64Array(chunk[start..<end])[0]
|
||||
M[x] = le.bigEndian
|
||||
break
|
||||
default:
|
||||
let s0 = rotateRight(M[x-15], n: 1) ^ rotateRight(M[x-15], n: 8) ^ (M[x-15] >> 7)
|
||||
let s1 = rotateRight(M[x-2], n: 19) ^ rotateRight(M[x-2], n: 61) ^ (M[x-2] >> 6)
|
||||
let s2 = M[x-16]
|
||||
let s3 = M[x-7]
|
||||
M[x] = s2 &+ s0 &+ s3 &+ s1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var A = hh[0], B = hh[1], C = hh[2], D = hh[3], E = hh[4], F = hh[5], G = hh[6], H = hh[7]
|
||||
|
||||
// Main loop
|
||||
for j in 0..<k.count {
|
||||
let s0 = rotateRight(A,n: 28) ^ rotateRight(A,n: 34) ^ rotateRight(A,n: 39) //FIXME: n:
|
||||
let maj = (A & B) ^ (A & C) ^ (B & C)
|
||||
let t2 = s0 &+ maj
|
||||
let s1 = rotateRight(E,n: 14) ^ rotateRight(E,n: 18) ^ rotateRight(E,n: 41)
|
||||
let ch = (E & F) ^ ((~E) & G)
|
||||
let t1 = H &+ s1 &+ ch &+ k[j] &+ UInt64(M[j])
|
||||
|
||||
H = G; G = F; F = E; E = D &+ t1
|
||||
D = C; C = B; B = A; A = t1 &+ t2
|
||||
}
|
||||
|
||||
hh[0] = (hh[0] &+ A)
|
||||
hh[1] = (hh[1] &+ B)
|
||||
hh[2] = (hh[2] &+ C)
|
||||
hh[3] = (hh[3] &+ D)
|
||||
hh[4] = (hh[4] &+ E)
|
||||
hh[5] = (hh[5] &+ F)
|
||||
hh[6] = (hh[6] &+ G)
|
||||
hh[7] = (hh[7] &+ H)
|
||||
}
|
||||
|
||||
// Produce the final hash value (big-endian)
|
||||
var result = [UInt8]()
|
||||
result.reserveCapacity(hh.count / 4)
|
||||
Self.resultingArray(hh).forEach {
|
||||
let item = $0.bigEndian
|
||||
var partialResult = [UInt8]()
|
||||
partialResult.reserveCapacity(8)
|
||||
for i in 0..<8 {
|
||||
let shift = UInt64(8 * i)
|
||||
partialResult.append(UInt8((item >> shift) & 0xff))
|
||||
}
|
||||
result += partialResult
|
||||
}
|
||||
return result
|
||||
}
|
||||
// codebeat:enable[ABC]
|
||||
}
|
||||
|
||||
final class SHA256 : SHA2Variant32 {
|
||||
static let h: [UInt64] = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]
|
||||
|
||||
static func resultingArray<T>(_ hh: [T]) -> ArraySlice<T> {
|
||||
return ArraySlice(hh)
|
||||
}
|
||||
}
|
||||
|
||||
final class SHA384 : SHA2Variant64 {
|
||||
static let h: [UInt64] = [0xcbbb9d5dc1059ed8, 0x629a292a367cd507, 0x9159015a3070dd17, 0x152fecd8f70e5939, 0x67332667ffc00b31, 0x8eb44a8768581511, 0xdb0c2e0d64f98fa7, 0x47b5481dbefa4fa4]
|
||||
|
||||
public static func resultingArray<T>(_ hh: [T]) -> ArraySlice<T> {
|
||||
return hh[0..<6]
|
||||
}
|
||||
}
|
||||
|
||||
final class SHA512 : SHA2Variant64 {
|
||||
static let h: [UInt64] = [0x6a09e667f3bcc908, 0xbb67ae8584caa73b, 0x3c6ef372fe94f82b, 0xa54ff53a5f1d36f1, 0x510e527fade682d1, 0x9b05688c2b3e6c1f, 0x1f83d9abfb41bd6b, 0x5be0cd19137e2179]
|
||||
|
||||
static func resultingArray<T>(_ hh: [T]) -> ArraySlice<T> {
|
||||
return ArraySlice(hh)
|
||||
}
|
||||
}
|
||||
|
||||
final class SHA2<Variant: SHA2Variant> {
|
||||
static var size: Int {
|
||||
return Variant.size
|
||||
}
|
||||
|
||||
static func calculate(_ message: [UInt8]) -> [UInt8] {
|
||||
return Variant.calculate(message)
|
||||
}
|
||||
}
|
||||
|
||||
final class HMAC<Variant: SHA2Variant> {
|
||||
public static func authenticate(message:[UInt8], withKey key: [UInt8]) -> [UInt8] {
|
||||
var key = key
|
||||
|
||||
if (key.count > Variant.size) {
|
||||
key = Variant.calculate(key)
|
||||
}
|
||||
|
||||
if (key.count < Variant.size) { // keys shorter than blocksize are zero-padded
|
||||
key = key + [UInt8](repeating: 0, count: Variant.size - key.count)
|
||||
}
|
||||
|
||||
var opad = [UInt8](repeating: 0x5c, count: Variant.size)
|
||||
for (idx, _) in key.enumerated() {
|
||||
opad[idx] = key[idx] ^ opad[idx]
|
||||
}
|
||||
var ipad = [UInt8](repeating: 0x36, count: Variant.size)
|
||||
for (idx, _) in key.enumerated() {
|
||||
ipad[idx] = key[idx] ^ ipad[idx]
|
||||
}
|
||||
|
||||
let ipadAndMessageHash = Variant.calculate(ipad + message)
|
||||
let finalHash = Variant.calculate(opad + ipadAndMessageHash);
|
||||
|
||||
return finalHash
|
||||
}
|
||||
|
||||
static func authenticate(message: String, withKey key: [UInt8]) -> [UInt8] {
|
||||
return authenticate(message: [UInt8](message.utf8), withKey: key)
|
||||
}
|
||||
|
||||
static func authenticate(message: Data, withKey key: Data) -> Data {
|
||||
return Data(bytes: authenticate(message: Array(message), withKey: Array(key)))
|
||||
}
|
||||
|
||||
static func authenticate(message: String, withKey key: Data) -> Data {
|
||||
return Data(bytes: authenticate(message: [UInt8](message.utf8), withKey: Array(key)))
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct BytesSequence: Sequence {
|
||||
let chunkSize: Int
|
||||
let data: [UInt8]
|
||||
|
||||
init(chunkSize: Int, data: [UInt8]) {
|
||||
self.chunkSize = chunkSize
|
||||
self.data = data
|
||||
}
|
||||
|
||||
func makeIterator() -> AnyIterator<ArraySlice<UInt8>> {
|
||||
var offset:Int = 0
|
||||
|
||||
return AnyIterator {
|
||||
let end = Swift.min(self.chunkSize, self.data.count - offset)
|
||||
let result = self.data[offset..<offset + end]
|
||||
offset += result.count
|
||||
return result.count > 0 ? result : nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func rotateRight(_ x:UInt16, n:UInt16) -> UInt16 {
|
||||
return (x >> n) | (x << (16 - n))
|
||||
}
|
||||
|
||||
fileprivate func rotateRight(_ x:UInt32, n:UInt32) -> UInt32 {
|
||||
return (x >> n) | (x << (32 - n))
|
||||
}
|
||||
|
||||
fileprivate func rotateRight(_ x:UInt64, n:UInt64) -> UInt64 {
|
||||
return ((x >> n) | (x << (64 - n)))
|
||||
}
|
||||
|
||||
fileprivate func toUInt32Array(_ slice: ArraySlice<UInt8>) -> Array<UInt32> {
|
||||
var result = Array<UInt32>()
|
||||
result.reserveCapacity(16)
|
||||
|
||||
for idx in stride(from: slice.startIndex, to: slice.endIndex, by: MemoryLayout<UInt32>.size) {
|
||||
let val1:UInt32 = (UInt32(slice[idx.advanced(by: 3)]) << 24)
|
||||
let val2:UInt32 = (UInt32(slice[idx.advanced(by: 2)]) << 16)
|
||||
let val3:UInt32 = (UInt32(slice[idx.advanced(by: 1)]) << 8)
|
||||
let val4:UInt32 = UInt32(slice[idx])
|
||||
let val:UInt32 = val1 | val2 | val3 | val4
|
||||
result.append(val)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fileprivate func toUInt64Array(_ slice: ArraySlice<UInt8>) -> Array<UInt64> {
|
||||
var result = Array<UInt64>()
|
||||
result.reserveCapacity(32)
|
||||
for idx in stride(from: slice.startIndex, to: slice.endIndex, by: MemoryLayout<UInt64>.size) {
|
||||
var val:UInt64 = 0
|
||||
val |= UInt64(slice[idx.advanced(by: 7)]) << 56
|
||||
val |= UInt64(slice[idx.advanced(by: 6)]) << 48
|
||||
val |= UInt64(slice[idx.advanced(by: 5)]) << 40
|
||||
val |= UInt64(slice[idx.advanced(by: 4)]) << 32
|
||||
val |= UInt64(slice[idx.advanced(by: 3)]) << 24
|
||||
val |= UInt64(slice[idx.advanced(by: 2)]) << 16
|
||||
val |= UInt64(slice[idx.advanced(by: 1)]) << 8
|
||||
val |= UInt64(slice[idx.advanced(by: 0)]) << 0
|
||||
result.append(val)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fileprivate func arrayOfBytes<T>(_ value:T, length:Int? = nil) -> [UInt8] {
|
||||
let totalBytes = length ?? MemoryLayout<T>.size
|
||||
|
||||
let valuePointer = UnsafeMutablePointer<T>.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..<min(MemoryLayout<T>.size,totalBytes) {
|
||||
bytes[totalBytes - 1 - j] = (bytesPointer + j).pointee
|
||||
}
|
||||
|
||||
valuePointer.deinitialize(count: 1)
|
||||
#if swift(>=4.1)
|
||||
valuePointer.deallocate()
|
||||
#else
|
||||
valuePointer.deallocate(capacity: 1)
|
||||
#endif
|
||||
return bytes
|
||||
}
|
||||
|
||||
public extension String {
|
||||
public func fp_sha256() -> [UInt8] {
|
||||
return SHA2<SHA256>.calculate([UInt8](self.utf8))
|
||||
}
|
||||
|
||||
public func fp_sha384() -> [UInt8] {
|
||||
return SHA2<SHA384>.calculate([UInt8](self.utf8))
|
||||
}
|
||||
|
||||
public func fp_sha512() -> [UInt8] {
|
||||
return SHA2<SHA512>.calculate([UInt8](self.utf8))
|
||||
}
|
||||
}
|
||||
|
||||
public extension Data {
|
||||
public func fp_sha256() -> [UInt8] {
|
||||
return SHA2<SHA256>.calculate(Array(self))
|
||||
}
|
||||
|
||||
public func fp_sha384() -> [UInt8] {
|
||||
return SHA2<SHA384>.calculate(Array(self))
|
||||
}
|
||||
|
||||
public func fp_sha512() -> [UInt8] {
|
||||
return SHA2<SHA512>.calculate(Array(self))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,891 @@
|
||||
//
|
||||
// FPSStreamTask.swift
|
||||
// FileProvider
|
||||
//
|
||||
// Created by Amir Abbas Mousavian.
|
||||
// Copyright © 2016 Mousavian. Distributed under MIT license.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
private var _lasttaskIdAssociated = 1_000_000_000
|
||||
let _lasttaskIdAssociated_lock: NSLock = NSLock()
|
||||
var lasttaskIdAssociated: Int {
|
||||
get {
|
||||
_lasttaskIdAssociated_lock.try()
|
||||
defer {
|
||||
_lasttaskIdAssociated_lock.unlock()
|
||||
}
|
||||
return _lasttaskIdAssociated
|
||||
}
|
||||
set {
|
||||
_lasttaskIdAssociated_lock.try()
|
||||
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
|
||||
/// while it can actually fallback to NSURLSessionStreamTask in iOS 9.
|
||||
public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
|
||||
fileprivate var _inputStream: InputStream?
|
||||
fileprivate var _outputStream: OutputStream?
|
||||
|
||||
let _inputStream_lock: NSLock = NSLock()
|
||||
var inputStream: InputStream? {
|
||||
get {
|
||||
_inputStream_lock.try()
|
||||
defer {
|
||||
_inputStream_lock.unlock()
|
||||
}
|
||||
return _inputStream
|
||||
}
|
||||
set {
|
||||
_inputStream_lock.try()
|
||||
defer {
|
||||
_inputStream_lock.unlock()
|
||||
}
|
||||
_inputStream = newValue
|
||||
}
|
||||
}
|
||||
|
||||
let _outputStream_lock: NSLock = NSLock()
|
||||
var outputStream: OutputStream? {
|
||||
get {
|
||||
_outputStream_lock.try()
|
||||
defer {
|
||||
_outputStream_lock.unlock()
|
||||
}
|
||||
return _outputStream
|
||||
}
|
||||
set {
|
||||
_outputStream_lock.try()
|
||||
defer {
|
||||
_outputStream_lock.unlock()
|
||||
}
|
||||
_outputStream = newValue
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var operation_queue: OperationQueue!
|
||||
internal var _underlyingSession: URLSession
|
||||
fileprivate var streamDelegate: FPSStreamDelegate? {
|
||||
return (_underlyingSession.delegate as? FPSStreamDelegate)
|
||||
}
|
||||
fileprivate var _taskIdentifier: Int
|
||||
fileprivate var _taskDescription: String?
|
||||
|
||||
/// Force using `URLSessionStreamTask` for iOS 9 and later
|
||||
public let useURLSession: Bool
|
||||
@available(iOS 9.0, macOS 10.11, *)
|
||||
fileprivate static var streamTasks = [Int: URLSessionStreamTask]()
|
||||
|
||||
@available(iOS 9.0, macOS 10.11, *)
|
||||
internal var _underlyingTask: URLSessionStreamTask? {
|
||||
return FileProviderStreamTask.streamTasks[_taskIdentifier]
|
||||
}
|
||||
|
||||
/**
|
||||
* An identifier uniquely identifies the task within a given 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 {
|
||||
if #available(iOS 9.0, macOS 10.11, *) {
|
||||
if self.useURLSession {
|
||||
return _underlyingTask!.taskIdentifier
|
||||
}
|
||||
}
|
||||
|
||||
return _taskIdentifier
|
||||
}
|
||||
|
||||
/// An app-provided description of the current task.
|
||||
///
|
||||
/// This value may be nil. It is intended to contain human-readable strings that you can
|
||||
/// then display to the user as part of your app’s user interface.
|
||||
open override var taskDescription: String? {
|
||||
get {
|
||||
if #available(iOS 9.0, macOS 10.11, *) {
|
||||
if self.useURLSession {
|
||||
return _underlyingTask!.taskDescription
|
||||
}
|
||||
}
|
||||
|
||||
return _taskDescription
|
||||
}
|
||||
@objc(setTaskDescription:)
|
||||
set {
|
||||
if #available(iOS 9.0, macOS 10.11, *) {
|
||||
if self.useURLSession {
|
||||
_underlyingTask!.taskDescription = newValue
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_taskDescription = newValue
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var _state: URLSessionTask.State = .suspended
|
||||
/**
|
||||
* 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 {
|
||||
return _underlyingTask!.state
|
||||
}
|
||||
}
|
||||
|
||||
return _state
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
return _underlyingTask!.originalRequest
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var _countOfBytesSent: Int64 = 0 {
|
||||
willSet {
|
||||
for observer in observers where observer.keyPath == "countOfBytesSent" {
|
||||
observer.observer.observeValue(forKeyPath: observer.keyPath, of: self, change: [.oldKey: _countOfBytesSent, .oldKey: newValue], context: observer.context)
|
||||
}
|
||||
}
|
||||
didSet {
|
||||
for observer in observers where observer.keyPath == "countOfBytesSent" {
|
||||
observer.observer.observeValue(forKeyPath: observer.keyPath, of: self, change: [.oldKey: oldValue, .oldKey: _countOfBytesSent], context: observer.context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var _countOfBytesRecieved: Int64 = 0 {
|
||||
willSet {
|
||||
for observer in observers where observer.keyPath == "countOfBytesRecieved" {
|
||||
observer.observer.observeValue(forKeyPath: observer.keyPath, of: self, change: [.oldKey: _countOfBytesRecieved, .oldKey: newValue], context: observer.context)
|
||||
}
|
||||
}
|
||||
didSet {
|
||||
for observer in observers where observer.keyPath == "countOfBytesRecieved" {
|
||||
observer.observer.observeValue(forKeyPath: observer.keyPath, of: self, change: [.oldKey: oldValue, .oldKey: _countOfBytesRecieved], context: observer.context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of bytes that the task has sent to the server in the request body.
|
||||
*
|
||||
* 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
|
||||
* `urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)` delegate method.
|
||||
*/
|
||||
override open var countOfBytesSent: Int64 {
|
||||
if #available(iOS 9.0, macOS 10.11, *) {
|
||||
if self.useURLSession {
|
||||
return _underlyingTask!.countOfBytesSent
|
||||
}
|
||||
}
|
||||
|
||||
return _countOfBytesSent
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of bytes that the task has received from the server in the response body.
|
||||
*
|
||||
* 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 {
|
||||
return _underlyingTask!.countOfBytesReceived
|
||||
}
|
||||
}
|
||||
|
||||
return _countOfBytesRecieved
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of bytes that the task expects to send in the request body.
|
||||
*
|
||||
* The `URL` loading system can determine the length of the upload data in three ways:
|
||||
* - From the length of the `NSData` object provided as the upload body.
|
||||
* - From the length of the file on disk provided as the upload body of an upload task (not a download task).
|
||||
* - 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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of bytes that the task expects to receive in the response body.
|
||||
*
|
||||
* 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 {
|
||||
return _underlyingTask!.countOfBytesExpectedToReceive
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
override public init() {
|
||||
fatalError("Use NSURLSession.fpstreamTask() method")
|
||||
}
|
||||
|
||||
fileprivate var host: (hostname: String, port: Int)?
|
||||
fileprivate var service: NetService?
|
||||
|
||||
internal static let defaultUseURLSession = false
|
||||
|
||||
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, *) {
|
||||
if useURLSession {
|
||||
let task = session.streamTask(withHostName: host, port: port)
|
||||
self._taskIdentifier = task.taskIdentifier
|
||||
FileProviderStreamTask.streamTasks[_taskIdentifier] = task
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
lasttaskIdAssociated += 1
|
||||
self._taskIdentifier = lasttaskIdAssociated
|
||||
self.host = (host, port)
|
||||
self.operation_queue = OperationQueue()
|
||||
self.operation_queue.name = "FileProviderStreamTask"
|
||||
self.operation_queue.maxConcurrentOperationCount = 1
|
||||
}
|
||||
|
||||
internal init(session: URLSession, netService: NetService, useURLSession: Bool = defaultUseURLSession) {
|
||||
self._underlyingSession = session
|
||||
self.useURLSession = useURLSession
|
||||
if #available(iOS 9.0, macOS 10.11, *) {
|
||||
if useURLSession {
|
||||
let task = session.streamTask(with: netService)
|
||||
self._taskIdentifier = task.taskIdentifier
|
||||
FileProviderStreamTask.streamTasks[_taskIdentifier] = task
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
lasttaskIdAssociated += 1
|
||||
self._taskIdentifier = lasttaskIdAssociated
|
||||
self.service = netService
|
||||
self.operation_queue = OperationQueue()
|
||||
self.operation_queue.name = "FileProviderStreamTask"
|
||||
self.operation_queue.maxConcurrentOperationCount = 1
|
||||
}
|
||||
|
||||
deinit {
|
||||
if !self.useURLSession {
|
||||
self.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the task.
|
||||
*
|
||||
* 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 {
|
||||
_underlyingTask!.cancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
self.operation_queue.isSuspended = true
|
||||
self._state = .canceling
|
||||
|
||||
self.inputStream?.delegate = nil
|
||||
self.outputStream?.delegate = nil
|
||||
|
||||
self.inputStream?.close()
|
||||
self.outputStream?.close()
|
||||
|
||||
self.inputStream?.remove(from: RunLoop.main, forMode: .defaultRunLoopMode)
|
||||
self.outputStream?.remove(from: RunLoop.main, forMode: .defaultRunLoopMode)
|
||||
|
||||
self.inputStream = nil
|
||||
self.outputStream = nil
|
||||
|
||||
self._state = .completed
|
||||
self._countOfBytesSent = 0
|
||||
self._countOfBytesRecieved = 0
|
||||
}
|
||||
|
||||
var __error: Error? = nil
|
||||
|
||||
let _error_lock: NSLock = NSLock()
|
||||
var _error: Error? {
|
||||
get {
|
||||
_error_lock.try()
|
||||
defer {
|
||||
_error_lock.unlock()
|
||||
}
|
||||
return __error
|
||||
}
|
||||
set {
|
||||
_error_lock.try()
|
||||
defer {
|
||||
_error_lock.unlock()
|
||||
}
|
||||
__error = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* All other tasks must start over when resumed.
|
||||
*/
|
||||
override open func suspend() {
|
||||
if #available(iOS 9.0, macOS 10.11, *) {
|
||||
if self.useURLSession {
|
||||
_underlyingTask!.suspend()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
self._state = .suspended
|
||||
self.operation_queue.isSuspended = true
|
||||
}
|
||||
|
||||
// Resumes the task, if it is suspended.
|
||||
override open func resume() {
|
||||
if #available(iOS 9.0, macOS 10.11, *) {
|
||||
if self.useURLSession {
|
||||
_underlyingTask!.resume()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var readStream : Unmanaged<CFReadStream>?
|
||||
var writeStream : Unmanaged<CFWriteStream>?
|
||||
|
||||
if self.inputStream == nil || self.outputStream == nil {
|
||||
if let host = self.host {
|
||||
CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, host.hostname as CFString, UInt32(host.port), &readStream, &writeStream)
|
||||
} else if let service = self.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)
|
||||
}
|
||||
|
||||
self.inputStream = readStream?.takeRetainedValue()
|
||||
self.outputStream = writeStream?.takeRetainedValue()
|
||||
guard let inputStream = self.inputStream, let outputStream = self.outputStream else {
|
||||
return
|
||||
}
|
||||
self.streamDelegate?.urlSession?(self._underlyingSession, streamTask: self, didBecome: inputStream, outputStream: outputStream)
|
||||
}
|
||||
|
||||
guard let inputStream = self.inputStream, let outputStream = self.outputStream else {
|
||||
return
|
||||
}
|
||||
|
||||
if self.isSecure {
|
||||
inputStream.setProperty(self.securityLevel.rawValue, forKey: .socketSecurityLevelKey)
|
||||
outputStream.setProperty(self.securityLevel.rawValue, forKey: .socketSecurityLevelKey)
|
||||
} else {
|
||||
inputStream.setProperty(StreamSocketSecurityLevel.none.rawValue, forKey: .socketSecurityLevelKey)
|
||||
outputStream.setProperty(StreamSocketSecurityLevel.none.rawValue, forKey: .socketSecurityLevelKey)
|
||||
}
|
||||
|
||||
inputStream.delegate = self
|
||||
outputStream.delegate = self
|
||||
|
||||
inputStream.schedule(in: RunLoop.main, forMode: .defaultRunLoopMode)
|
||||
outputStream.schedule(in: RunLoop.main, forMode: .defaultRunLoopMode)
|
||||
|
||||
inputStream.open()
|
||||
outputStream.open()
|
||||
|
||||
self.operation_queue.isSuspended = false
|
||||
self._state = .running
|
||||
}
|
||||
|
||||
fileprivate var dataToBeSent: Data = Data()
|
||||
fileprivate var _dataReceived: Data = Data()
|
||||
|
||||
// We are updating `dataReceived` from main thread while reading it from operation_queue.
|
||||
let _dataReceived_lock: NSLock = NSLock()
|
||||
|
||||
var dataReceived: Data {
|
||||
get {
|
||||
if !_dataReceived_lock.try() {
|
||||
print("not locked")
|
||||
}
|
||||
|
||||
defer {
|
||||
_dataReceived_lock.unlock()
|
||||
}
|
||||
return _dataReceived
|
||||
}
|
||||
set {
|
||||
if !_dataReceived_lock.try() {
|
||||
print("not locked")
|
||||
}
|
||||
defer {
|
||||
_dataReceived_lock.unlock()
|
||||
}
|
||||
_dataReceived = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously reads a number of bytes from the stream, and calls a handler upon completion.
|
||||
*
|
||||
* - Parameter minBytes: The minimum number of bytes to read.
|
||||
* - ParametermaxBytes: The maximum number of bytes to read.
|
||||
* - Parameter timeout: A timeout for reading bytes. If the read is not completed within the specified interval,
|
||||
* the read is canceled and the completionHandler is called with an error. Pass `0` to prevent a read from timing out.
|
||||
* - Parameter completionHandler: The completion handler to call when all bytes are read, or an error occurs.
|
||||
* This handler is executed on the delegate queue. This completion handler takes the following parameters:
|
||||
* - 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 {
|
||||
_underlyingTask!.readData(ofMinLength: minBytes, maxLength: maxBytes, timeout: timeout, completionHandler: completionHandler)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
guard let inputStream = inputStream else {
|
||||
return
|
||||
}
|
||||
|
||||
let expireDate = Date(timeIntervalSinceNow: timeout)
|
||||
operation_queue.addOperation {
|
||||
var timedOut: Bool = false
|
||||
while (self.dataReceived.count == 0 || self.dataReceived.count < minBytes) && !timedOut {
|
||||
if let error = inputStream.streamError {
|
||||
completionHandler(nil, inputStream.streamStatus == .atEnd, error)
|
||||
return
|
||||
}
|
||||
|
||||
Thread.sleep(forTimeInterval: 0.1)
|
||||
timedOut = expireDate < Date()
|
||||
}
|
||||
var dR: Data?
|
||||
if self.dataReceived.count > maxBytes {
|
||||
let range: Range = 0..<maxBytes
|
||||
dR = self.dataReceived.subdata(in: range)
|
||||
self.dataReceived.removeFirst(maxBytes)
|
||||
} else {
|
||||
if self.dataReceived.count > 0 {
|
||||
dR = self.dataReceived
|
||||
self.dataReceived.removeAll(keepingCapacity: false)
|
||||
}
|
||||
}
|
||||
let isEOF = inputStream.streamStatus == .atEnd && self.dataReceived.count == 0
|
||||
completionHandler(dR, isEOF, dR == nil ? inputStream.streamError : nil)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously writes the specified data to the stream, and calls a handler upon completion.
|
||||
*
|
||||
* There is no guarantee that the remote side of the stream has received all of the written bytes
|
||||
* at the time that `completionHandler` is called, only that all of the data has been written to the kernel.
|
||||
*
|
||||
* - Parameter data: The data to be written.
|
||||
* - Parameter timeout: A timeout for writing bytes. If the write is not completed within the specified interval,
|
||||
* the write is canceled and the `completionHandler` is called with an error.
|
||||
* Pass `0` to prevent a write from timing out.
|
||||
* - Parameter completionHandler: The completion handler to call when all bytes are written, or an error occurs.
|
||||
* 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 {
|
||||
_underlyingTask!.write(data, timeout: timeout, completionHandler: completionHandler)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
guard outputStream != nil else {
|
||||
return
|
||||
}
|
||||
|
||||
operation_queue.addOperation {
|
||||
self.dataToBeSent.append(data)
|
||||
let result = self.write(timeout: timeout, close: false)
|
||||
if result < 0 {
|
||||
let error = self.outputStream?.streamError ?? URLError(.cannotWriteToFile)
|
||||
completionHandler(error)
|
||||
} else {
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
_underlyingTask!.captureStreams()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
guard let outputStream = outputStream, let inputStream = inputStream else {
|
||||
return
|
||||
}
|
||||
self.operation_queue.addOperation {
|
||||
_=self.write(close: false)
|
||||
while inputStream.streamStatus != .atEnd || outputStream.streamStatus == .writing {
|
||||
Thread.sleep(forTimeInterval: 0.1)
|
||||
}
|
||||
self.streamDelegate?.urlSession?(self._underlyingSession, streamTask: self, didBecome: inputStream, outputStream: outputStream)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* this method will result in an error.
|
||||
*
|
||||
* 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 {
|
||||
_underlyingTask!.closeWrite()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
operation_queue.addOperation {
|
||||
_ = 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()) {
|
||||
if let _ = outputStream.streamError {
|
||||
return byteSent == 0 ? -1 : byteSent
|
||||
}
|
||||
|
||||
let bytesWritten = self.dataToBeSent.withUnsafeBytes {
|
||||
outputStream.write($0, maxLength: self.dataToBeSent.count)
|
||||
}
|
||||
|
||||
if bytesWritten > 0 {
|
||||
let range = 0..<bytesWritten
|
||||
self.dataToBeSent.replaceSubrange(range, with: Data())
|
||||
byteSent += bytesWritten
|
||||
} else if bytesWritten < 0 {
|
||||
self._error = outputStream.streamError
|
||||
return bytesWritten
|
||||
}
|
||||
if self.dataToBeSent.count == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
self._countOfBytesSent += Int64(byteSent)
|
||||
if close {
|
||||
outputStream.close()
|
||||
self.streamDelegate?.urlSession?(self._underlyingSession, writeClosedFor: self)
|
||||
}
|
||||
return byteSent
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes any enqueued reads and writes, and then closes the read side of the underlying socket.
|
||||
*
|
||||
* You may continue to write data using the `write(_:timeout:completionHandler:)` method after
|
||||
* calling this method. Any calls to `readData(ofMinLength:maxLength:timeout:completionHandler:)`
|
||||
* after calling this method will result in an error.
|
||||
*/
|
||||
open func closeRead() {
|
||||
if #available(iOS 9.0, macOS 10.11, *) {
|
||||
if self.useURLSession {
|
||||
_underlyingTask!.closeRead()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
guard let inputStream = inputStream else {
|
||||
return
|
||||
}
|
||||
operation_queue.addOperation {
|
||||
while inputStream.streamStatus != .atEnd {
|
||||
Thread.sleep(forTimeInterval: 0.1)
|
||||
}
|
||||
inputStream.close()
|
||||
self.streamDelegate?.urlSession?(self._underlyingSession, readClosedFor: self)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var isSecure = false
|
||||
|
||||
public var securityLevel: StreamSocketSecurityLevel = .negotiatedSSL
|
||||
/**
|
||||
* Completes any enqueued reads and writes, and establishes a secure connection.
|
||||
*
|
||||
* Authentication callbacks are sent to the session's delegate using the
|
||||
* `urlSession(_:task:didReceive:completionHandler:)` method.
|
||||
*/
|
||||
open func startSecureConnection() {
|
||||
if #available(iOS 9.0, macOS 10.11, *) {
|
||||
if self.useURLSession {
|
||||
_underlyingTask!.startSecureConnection()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
isSecure = true
|
||||
operation_queue.addOperation {
|
||||
if let inputStream = self.inputStream, let outputStream = self.outputStream,
|
||||
inputStream.property(forKey: .socketSecurityLevelKey) as? String == StreamSocketSecurityLevel.none.rawValue {
|
||||
inputStream.setProperty(self.securityLevel.rawValue, forKey: .socketSecurityLevelKey)
|
||||
outputStream.setProperty(self.securityLevel.rawValue, forKey: .socketSecurityLevelKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes any enqueued reads and writes, and closes the secure connection.
|
||||
*/
|
||||
open func stopSecureConnection() {
|
||||
if #available(iOS 9.0, macOS 10.11, *) {
|
||||
if self.useURLSession {
|
||||
_underlyingTask!.stopSecureConnection()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
isSecure = false
|
||||
operation_queue.addOperation {
|
||||
if let inputStream = self.inputStream, let outputStream = self.outputStream,
|
||||
inputStream.property(forKey: .socketSecurityLevelKey) as? String != StreamSocketSecurityLevel.none.rawValue {
|
||||
|
||||
inputStream.setProperty(StreamSocketSecurityLevel.none.rawValue, forKey: .socketSecurityLevelKey)
|
||||
outputStream.setProperty(StreamSocketSecurityLevel.none.rawValue, forKey: .socketSecurityLevelKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var _retriesForInputStream: Int = 0
|
||||
private var _retriesForOutputStream: Int = 0
|
||||
open func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
|
||||
if eventCode.contains(.errorOccurred) {
|
||||
let retries: Int
|
||||
if aStream == self.inputStream {
|
||||
retries = _retriesForInputStream
|
||||
_retriesForInputStream += 1
|
||||
} else if aStream == self.outputStream {
|
||||
retries = _retriesForOutputStream
|
||||
_retriesForOutputStream += 1
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
if retries < 3 {
|
||||
aStream.open()
|
||||
} else {
|
||||
self._error = aStream.streamError
|
||||
streamDelegate?.urlSession?(_underlyingSession, task: self, didCompleteWithError: error)
|
||||
}
|
||||
}
|
||||
|
||||
if aStream == self.inputStream && eventCode.contains(.hasBytesAvailable) {
|
||||
while (self.inputStream!.hasBytesAvailable) {
|
||||
var buffer = [UInt8](repeating: 0, count: 2048)
|
||||
let len = self.inputStream!.read(&buffer, maxLength: buffer.count)
|
||||
if len > 0 {
|
||||
self.dataReceived.append(&buffer, count: len)
|
||||
self._countOfBytesRecieved += Int64(len)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension URLSession {
|
||||
/// Creates a bidirectional stream task to a given host and port.
|
||||
func fpstreamTask(withHostName hostname: String, port: Int) -> FileProviderStreamTask {
|
||||
return FileProviderStreamTask(session: self, host: hostname, port: port)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
internal protocol FPSStreamDelegate : URLSessionTaskDelegate {
|
||||
|
||||
|
||||
/**
|
||||
* Indiciates that the read side of a connection has been closed. Any
|
||||
* outstanding reads complete, but future reads will immediately fail.
|
||||
* This may be sent even when no reads are in progress. However, when
|
||||
* this delegate message is received, there may still be bytes
|
||||
* available. You only know that no more bytes are available when you
|
||||
* are able to read until EOF. */
|
||||
@objc optional func urlSession(_ session: URLSession, readClosedFor streamTask: FileProviderStreamTask)
|
||||
|
||||
|
||||
/**
|
||||
* Indiciates that the write side of a connection has been closed.
|
||||
* Any outstanding writes complete, but future writes will immediately
|
||||
* fail.
|
||||
*/
|
||||
@objc optional func urlSession(_ session: URLSession, writeClosedFor streamTask: FileProviderStreamTask)
|
||||
|
||||
|
||||
/**
|
||||
* A notification that the system has determined that a better route
|
||||
* to the host has been detected (eg, a wi-fi interface becoming
|
||||
* available.) This is a hint to the delegate that it may be
|
||||
* desirable to create a new task for subsequent work. Note that
|
||||
* there is no guarantee that the future task will be able to connect
|
||||
* to the host, so callers should should be prepared for failure of
|
||||
* reads and writes over any new interface. */
|
||||
@objc optional func urlSession(_ session: URLSession, betterRouteDiscoveredFor streamTask: FileProviderStreamTask)
|
||||
|
||||
|
||||
/**
|
||||
* The given task has been completed, and unopened NSInputStream and
|
||||
* NSOutputStream objects are created from the underlying network
|
||||
* connection. This will only be invoked after all enqueued IO has
|
||||
* completed (including any necessary handshakes.) The streamTask
|
||||
* will not receive any further delegate messages.
|
||||
*/
|
||||
@objc optional func urlSession(_ session: URLSession, streamTask: FileProviderStreamTask, didBecome inputStream: InputStream, outputStream: OutputStream)
|
||||
}
|
||||
// codebeat:enable[TOTAL_LOC,TOO_MANY_IVARS]
|
||||
|
||||
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]
|
||||
@@ -0,0 +1,903 @@
|
||||
//
|
||||
// FTPFileProvider.swift
|
||||
// FileProvider
|
||||
//
|
||||
// Created by Amir Abbas Mousavian.
|
||||
// Copyright © 2017 Mousavian. Distributed under MIT license.
|
||||
//
|
||||
|
||||
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: 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?
|
||||
|
||||
open var dispatch_queue: DispatchQueue
|
||||
open var operation_queue: OperationQueue {
|
||||
willSet {
|
||||
assert(_session == nil, "It's not effective to change dispatch_queue property after session is initialized.")
|
||||
}
|
||||
}
|
||||
|
||||
open weak var delegate: FileProviderDelegate?
|
||||
open var credential: URLCredential? {
|
||||
didSet {
|
||||
sessionDelegate?.credential = self.credential
|
||||
}
|
||||
}
|
||||
open private(set) var cache: URLCache?
|
||||
public var useCache: Bool
|
||||
public var validatingCache: Bool
|
||||
|
||||
/// Determine either FTP session is in passive or active mode.
|
||||
public let mode: Mode
|
||||
|
||||
fileprivate var _session: URLSession!
|
||||
internal var sessionDelegate: SessionDelegate?
|
||||
public var session: URLSession {
|
||||
get {
|
||||
if _session == nil {
|
||||
self.sessionDelegate = SessionDelegate(fileProvider: self)
|
||||
let config = URLSessionConfiguration.default
|
||||
_session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDelegate?, delegateQueue: self.operation_queue)
|
||||
_session.sessionDescription = UUID().uuidString
|
||||
initEmptySessionHandler(_session.sessionDescription!)
|
||||
}
|
||||
return _session
|
||||
}
|
||||
|
||||
set {
|
||||
assert(newValue.delegate is SessionDelegate, "session instances should have a SessionDelegate instance as delegate.")
|
||||
_session = newValue
|
||||
if _session.sessionDescription?.isEmpty ?? true {
|
||||
_session.sessionDescription = UUID().uuidString
|
||||
}
|
||||
self.sessionDelegate = newValue.delegate as? SessionDelegate
|
||||
initEmptySessionHandler(_session.sessionDescription!)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
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 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, 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?.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.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 }
|
||||
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) {
|
||||
aCoder.encode(self.baseURL, forKey: "baseURL")
|
||||
aCoder.encode(self.credential, forKey: "credential")
|
||||
aCoder.encode(self.useCache, forKey: "useCache")
|
||||
aCoder.encode(self.validatingCache, forKey: "validatingCache")
|
||||
aCoder.encode(self.mode.rawValue, forKey: "mode")
|
||||
aCoder.encode(self.supportsRFC3659, forKey: "supportsRFC3659")
|
||||
aCoder.encode(self.securedDataConnection, forKey: "securedDataConnection")
|
||||
}
|
||||
|
||||
public static var supportsSecureCoding: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
open func copy(with zone: NSZone? = nil) -> Any {
|
||||
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.securedDataConnection = self.securedDataConnection
|
||||
copy.supportsRFC3659 = self.supportsRFC3659
|
||||
return copy
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let sessionuuid = _session?.sessionDescription {
|
||||
removeSessionHandler(for: sessionuuid)
|
||||
}
|
||||
|
||||
if fileProviderCancelTasksOnInvalidating {
|
||||
_session?.invalidateAndCancel()
|
||||
} else {
|
||||
_session?.finishTasksAndInvalidate()
|
||||
}
|
||||
}
|
||||
|
||||
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 = FileProviderStreamTask.defaultUseURLSession
|
||||
|
||||
/**
|
||||
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
|
||||
|
||||
open func contentsOfDirectory(path: String, completionHandler: @escaping ([FileObject], Error?) -> Void) {
|
||||
self.contentsOfDirectory(path: path, rfc3659enabled: supportsRFC3659, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
/**
|
||||
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.
|
||||
|
||||
- Parameter path: path to target directory. If empty, root will be iterated.
|
||||
- Parameter rfc3659enabled: uses MLST command instead of old LIST to get files attributes, default is `true`.
|
||||
- Parameter completionHandler: a closure with result of directory entries or error.
|
||||
- Parameter contents: An array of `FileObject` identifying the the directory entries.
|
||||
- Parameter error: Error returned by system.
|
||||
*/
|
||||
open func contentsOfDirectory(path apath: String, rfc3659enabled: Bool , completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) {
|
||||
let path = ftpPath(apath)
|
||||
|
||||
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
|
||||
self.ftpLogin(task) { (error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler([], error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
self.ftpList(task, of: self.ftpPath(path), useMLST: rfc3659enabled, completionHandler: { (contents, error) in
|
||||
defer {
|
||||
self.ftpQuit(task)
|
||||
}
|
||||
if let error = error {
|
||||
if let uerror = error as? URLError, uerror.code == .unsupportedURL {
|
||||
self.contentsOfDirectory(path: path, rfc3659enabled: false, completionHandler: completionHandler)
|
||||
return
|
||||
}
|
||||
|
||||
self.dispatch_queue.async {
|
||||
completionHandler([], error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
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 {
|
||||
completionHandler(files, nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
open func attributesOfItem(path: String, completionHandler: @escaping (FileObject?, Error?) -> Void) {
|
||||
self.attributesOfItem(path: path, rfc3659enabled: supportsRFC3659, 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`.
|
||||
|
||||
- Parameter path: path to target directory. If empty, attributes of root will be returned.
|
||||
- Parameter rfc3659enabled: uses MLST command instead of old LIST to get files attributes, default is true.
|
||||
- Parameter 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 apath: String, rfc3659enabled: Bool, completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) {
|
||||
let path = ftpPath(apath)
|
||||
|
||||
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
|
||||
self.ftpLogin(task) { (error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let command = rfc3659enabled ? "MLST \(path)" : "LIST \(path)"
|
||||
self.execute(command: command, on: task, completionHandler: { (response, error) in
|
||||
defer {
|
||||
self.ftpQuit(task)
|
||||
}
|
||||
do {
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
guard let response = response, response.hasPrefix("250") || (response.hasPrefix("50") && rfc3659enabled) else {
|
||||
throw self.urlError(path, code: .badServerResponse)
|
||||
}
|
||||
|
||||
if response.hasPrefix("500") {
|
||||
self.supportsRFC3659 = false
|
||||
self.attributesOfItem(path: path, rfc3659enabled: false, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
let dirPath = (path as NSString).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)
|
||||
}
|
||||
} catch {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
open func storageProperties(completionHandler: @escaping (_ volume: VolumeObject?) -> Void) {
|
||||
dispatch_queue.async {
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
|
||||
@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 {
|
||||
return self.recursiveList(path: path, useMLST: true, foundItemsHandler: { items in
|
||||
if let foundItemHandler = foundItemHandler {
|
||||
for item in items where query.evaluate(with: item.mapPredicate()) {
|
||||
foundItemHandler(item)
|
||||
}
|
||||
progress.totalUnitCount = Int64(items.count)
|
||||
}
|
||||
}, completionHandler: {files, error in
|
||||
if let error = error {
|
||||
completionHandler([], error)
|
||||
return
|
||||
}
|
||||
|
||||
let foundFiles = files.filter { query.evaluate(with: $0.mapPredicate()) }
|
||||
completionHandler(foundFiles, nil)
|
||||
})
|
||||
} else {
|
||||
self.contentsOfDirectory(path: path, completionHandler: { (items, error) in
|
||||
if let error = error {
|
||||
completionHandler([], error)
|
||||
return
|
||||
}
|
||||
|
||||
var result = [FileObject]()
|
||||
for item in items where query.evaluate(with: item.mapPredicate()) {
|
||||
foundItemHandler?(item)
|
||||
result.append(item)
|
||||
}
|
||||
completionHandler(result, nil)
|
||||
})
|
||||
}
|
||||
|
||||
return progress
|
||||
}
|
||||
|
||||
open func url(of path: String?) -> URL {
|
||||
let path = path?.trimmingCharacters(in: CharacterSet(charactersIn: "/ ")).addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? (path ?? "")
|
||||
|
||||
var baseUrlComponent = URLComponents(url: self.baseURL!, resolvingAgainstBaseURL: true)
|
||||
baseUrlComponent?.user = credential?.user
|
||||
baseUrlComponent?.password = credential?.password
|
||||
return URL(string: path, relativeTo: baseUrlComponent?.url ?? baseURL) ?? baseUrlComponent?.url ?? baseURL!
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if !relativePath.isEmpty, self.baseURL == self.url(of: "/") {
|
||||
return (relativePath.removingPercentEncoding ?? relativePath).replacingOccurrences(of: "/", with: "", options: .anchored)
|
||||
}
|
||||
|
||||
return relativePath.replacingOccurrences(of: "/", with: "", options: .anchored)
|
||||
}
|
||||
|
||||
open func isReachable(completionHandler: @escaping (_ success: Bool, _ error: Error?) -> Void) {
|
||||
self.attributesOfItem(path: "/") { (file, error) in
|
||||
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) + "/"
|
||||
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? {
|
||||
// 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))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
let operation = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let progress = Progress(totalUnitCount: 0)
|
||||
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!)
|
||||
self.ftpLogin(task) { (error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
self.ftpStore(task, filePath: self.ftpPath(toPath), fromData: nil, fromFile: localFile, 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
|
||||
if error != nil {
|
||||
progress.cancel()
|
||||
}
|
||||
self.ftpQuit(task)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
let progress = Progress(totalUnitCount: 0)
|
||||
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!)
|
||||
self.ftpLogin(task) { (error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
self.ftpDownload(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return progress
|
||||
}
|
||||
|
||||
@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 {
|
||||
dispatch_queue.async {
|
||||
completionHandler(Data(), nil)
|
||||
self.delegateNotify(operation)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
let progress = Progress(totalUnitCount: 0)
|
||||
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!)
|
||||
self.ftpLogin(task) { (error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
self.ftpFileData(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: { _, recevied, totalReceived, totalSize in
|
||||
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(nil, error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let data = data {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(data, nil)
|
||||
self.delegateNotify(operation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
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!)
|
||||
self.ftpLogin(task) { (error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let storeHandler = {
|
||||
self.ftpStore(task, filePath: self.ftpPath(path), fromData: data ?? Data(), fromFile: nil, onTask: { task in
|
||||
weak var weakTask = task
|
||||
progress.cancellationHandler = {
|
||||
weakTask?.cancel()
|
||||
}
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
}, onProgress: { bytesSent, totalSent, expectedBytes in
|
||||
progress.completedUnitCount = totalSent
|
||||
self.delegateNotify(operation, progress: progress.fractionCompleted)
|
||||
}, completionHandler: { (error) in
|
||||
if error != nil {
|
||||
progress.cancel()
|
||||
}
|
||||
self.ftpQuit(task)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if overwrite {
|
||||
storeHandler()
|
||||
} else {
|
||||
self.attributesOfItem(path: path, completionHandler: { (file, erroe) in
|
||||
if file == nil {
|
||||
storeHandler()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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: 0)
|
||||
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!)
|
||||
self.ftpLogin(task) { (error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
self.ftpFileData(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
|
||||
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.
|
||||
|
||||
- Note: Many servers does't support this functionality.
|
||||
|
||||
- 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) {
|
||||
let operation = FileOperationType.link(link: path, target: destPath)
|
||||
_=self.doOperation(operation, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
|
||||
extension FTPFileProvider {
|
||||
fileprivate func doOperation(_ operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
let sourcePath = operation.source
|
||||
let destPath = operation.destination
|
||||
|
||||
let command: String
|
||||
switch operation {
|
||||
case .create: command = "MKD \(ftpPath(sourcePath))"
|
||||
case .copy: command = "SITE CPFR \(ftpPath(sourcePath))\r\nSITE CPTO \(ftpPath(destPath!))"
|
||||
case .move: command = "RNFR \(ftpPath(sourcePath))\r\nRNTO \(ftpPath(destPath!))"
|
||||
case .remove: command = "DELE \(ftpPath(sourcePath))"
|
||||
case .link: command = "SITE SYMLINK \(ftpPath(sourcePath)) \(ftpPath(destPath!))"
|
||||
default: return nil // modify, fetch
|
||||
}
|
||||
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!)
|
||||
self.ftpLogin(task) { (error) in
|
||||
if let error = error {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
return
|
||||
}
|
||||
|
||||
self.execute(command: command, on: task, completionHandler: { (response, error) in
|
||||
if let error = error {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let response = response else {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: self.urlError(sourcePath, code: .badServerResponse))
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if codes.filter({ (450..<560).contains($0) }).count > 0 {
|
||||
let errorCode: URLError.Code
|
||||
switch operation {
|
||||
case .create: errorCode = .cannotCreateFile
|
||||
case .modify: errorCode = .cannotWriteToFile
|
||||
case .copy:
|
||||
self.fallbackCopy(operation, progress: progress, completionHandler: completionHandler)
|
||||
return
|
||||
case .move: errorCode = .cannotMoveFile
|
||||
case .remove:
|
||||
self.fallbackRemove(operation, progress: progress, on: task, completionHandler: completionHandler)
|
||||
return
|
||||
case .link: errorCode = .cannotWriteToFile
|
||||
default: errorCode = .cannotOpenFile
|
||||
}
|
||||
let error = self.urlError(sourcePath, code: errorCode)
|
||||
progress.cancel()
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
return
|
||||
}
|
||||
|
||||
progress.completedUnitCount = progress.totalUnitCount
|
||||
completionHandler?(nil)
|
||||
self.delegateNotify(operation)
|
||||
})
|
||||
}
|
||||
|
||||
progress.cancellationHandler = { [weak task] in
|
||||
task?.cancel()
|
||||
}
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
return progress
|
||||
}
|
||||
|
||||
private func fallbackCopy(_ operation: FileOperationType, progress: Progress, completionHandler: SimpleCompletionHandler) {
|
||||
let sourcePath = operation.source
|
||||
guard let destPath = operation.destination else { return }
|
||||
|
||||
let localURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString).appendingPathExtension("tmp")
|
||||
|
||||
progress.becomeCurrent(withPendingUnitCount: 1)
|
||||
_ = self.copyItem(path: sourcePath, toLocalURL: localURL) { (error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
progress.becomeCurrent(withPendingUnitCount: 1)
|
||||
_ = self.copyItem(localFile: localURL, to: destPath) { error in
|
||||
completionHandler?(nil)
|
||||
self.delegateNotify(operation)
|
||||
}
|
||||
progress.resignCurrent()
|
||||
}
|
||||
progress.resignCurrent()
|
||||
return
|
||||
}
|
||||
|
||||
private func fallbackRemove(_ operation: FileOperationType, progress: Progress, on task: FileProviderStreamTask, completionHandler: SimpleCompletionHandler) {
|
||||
let sourcePath = operation.source
|
||||
|
||||
self.execute(command: "SITE RMDIR \(ftpPath(sourcePath))", on: task) { (response, error) in
|
||||
do {
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
guard let response = response else {
|
||||
throw self.urlError(sourcePath, code: .badServerResponse)
|
||||
}
|
||||
|
||||
if response.hasPrefix("50") {
|
||||
self.fallbackRecursiveRemove(operation, progress: progress, on: task, completionHandler: completionHandler)
|
||||
return
|
||||
}
|
||||
|
||||
if !response.hasPrefix("2") {
|
||||
throw self.urlError(sourcePath, code: .cannotRemoveFile)
|
||||
}
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(nil)
|
||||
}
|
||||
self.delegateNotify(operation)
|
||||
} catch {
|
||||
progress.cancel()
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
}
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func fallbackRecursiveRemove(_ operation: FileOperationType, progress: Progress, on task: FileProviderStreamTask, completionHandler: SimpleCompletionHandler) {
|
||||
let sourcePath = operation.source
|
||||
|
||||
_ = self.recursiveList(path: sourcePath, useMLST: true, completionHandler: { (contents, error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
progress.becomeCurrent(withPendingUnitCount: 1)
|
||||
let recursiveProgress = Progress(totalUnitCount: Int64(contents.count))
|
||||
let sortedContents = contents.sorted(by: {
|
||||
$0.path.localizedStandardCompare($1.path) == .orderedDescending
|
||||
})
|
||||
progress.resignCurrent()
|
||||
var command = ""
|
||||
for file in sortedContents {
|
||||
command += (file.isDirectory ? "RMD \(self.ftpPath(file.path))" : "DELE \(self.ftpPath(file.path))") + "\r\n"
|
||||
}
|
||||
command += "RMD \(self.ftpPath(sourcePath))"
|
||||
|
||||
self.execute(command: command, on: task, completionHandler: { (response, error) in
|
||||
recursiveProgress.completedUnitCount += 1
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
// TODO: Digest response
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
extension FTPFileProvider: FileProvider { }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,438 @@
|
||||
//
|
||||
// FileProvider.swift
|
||||
// FileProvider
|
||||
//
|
||||
// Created by Amir Abbas Mousavian.
|
||||
// Copyright © 2016 Mousavian. Distributed under MIT license.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Containts path, url and attributes of a file or resource.
|
||||
open class FileObject: NSObject {
|
||||
/// A `Dictionary` contains file information, using `URLResourceKey` keys.
|
||||
open internal(set) var allValues: [URLResourceKey: Any]
|
||||
|
||||
public init(allValues: [URLResourceKey: Any]) {
|
||||
self.allValues = allValues
|
||||
}
|
||||
|
||||
internal init(url: URL?, name: String, path: String) {
|
||||
self.allValues = [URLResourceKey: Any]()
|
||||
super.init()
|
||||
if let url = url {
|
||||
self.url = url
|
||||
}
|
||||
self.name = name
|
||||
self.path = path
|
||||
}
|
||||
|
||||
/// URL to access the resource, can be a relative URL against base URL.
|
||||
/// not supported by Dropbox provider.
|
||||
open internal(set) var url: URL {
|
||||
get {
|
||||
if let url = allValues[.fileURLKey] as? URL {
|
||||
return url
|
||||
} else {
|
||||
let path = self.path.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? self.path
|
||||
return URL(string: path) ?? URL(string: "/")!
|
||||
}
|
||||
}
|
||||
set {
|
||||
allValues[.fileURLKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Name of the file, usually equals with the last path component
|
||||
open internal(set) var name: String {
|
||||
get {
|
||||
return allValues[.nameKey] as! String
|
||||
}
|
||||
set {
|
||||
allValues[.nameKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Relative path of file object
|
||||
open internal(set) var path: String {
|
||||
get {
|
||||
return allValues[.pathKey] as! String
|
||||
}
|
||||
set {
|
||||
allValues[.pathKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Size of file on disk, return -1 for directories.
|
||||
open internal(set) var size: Int64 {
|
||||
get {
|
||||
return allValues[.fileSizeKey] as? Int64 ?? -1
|
||||
}
|
||||
set {
|
||||
allValues[.fileSizeKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Count of children items of a driectory.
|
||||
open internal(set) var childrensCount: Int? {
|
||||
get {
|
||||
return allValues[.childrensCount] as? Int
|
||||
}
|
||||
set {
|
||||
allValues[.childrensCount] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// The time contents of file has been created, returns nil if not set
|
||||
open internal(set) var creationDate: Date? {
|
||||
get {
|
||||
return allValues[.creationDateKey] as? Date
|
||||
}
|
||||
set {
|
||||
allValues[.creationDateKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// The time contents of file has been modified, returns nil if not set
|
||||
open internal(set) var modifiedDate: Date? {
|
||||
get {
|
||||
return allValues[.contentModificationDateKey] as? Date
|
||||
}
|
||||
set {
|
||||
allValues[.contentModificationDateKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// return resource type of file, usually directory, regular or symLink
|
||||
open internal(set) var type: URLFileResourceType {
|
||||
get {
|
||||
return allValues[.fileResourceTypeKey] as? URLFileResourceType ?? .unknown
|
||||
}
|
||||
set {
|
||||
allValues[.fileResourceTypeKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// File is hidden either because begining with dot or filesystem flags
|
||||
/// Setting this value on a file begining with dot has no effect
|
||||
open internal(set) var isHidden: Bool {
|
||||
get {
|
||||
return allValues[.isHiddenKey] as? Bool ?? false
|
||||
}
|
||||
set {
|
||||
allValues[.isHiddenKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// File can not be written
|
||||
open internal(set) var isReadOnly: Bool {
|
||||
get {
|
||||
return !(allValues[.isWritableKey] as? Bool ?? true)
|
||||
}
|
||||
set {
|
||||
allValues[.isWritableKey] = !newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// File is a Directory
|
||||
open var isDirectory: Bool {
|
||||
return self.type == .directory
|
||||
}
|
||||
|
||||
/// File is a normal file
|
||||
open var isRegularFile: Bool {
|
||||
return self.type == .regular
|
||||
}
|
||||
|
||||
/// File is a Symbolic link
|
||||
open var isSymLink: Bool {
|
||||
return self.type == .symbolicLink
|
||||
}
|
||||
}
|
||||
|
||||
extension FileObject {
|
||||
open override var hashValue: Int {
|
||||
let hashURL = self.url.hashValue
|
||||
let hashSize = self.size.hashValue
|
||||
return (hashURL << 7) &+ hashURL &+ hashSize
|
||||
}
|
||||
|
||||
open override var hash: Int {
|
||||
return self.hashValue
|
||||
}
|
||||
|
||||
/// Check `FileObject` equality
|
||||
public static func ==(lhs: FileObject, rhs: FileObject) -> Bool {
|
||||
if rhs === lhs {
|
||||
return true
|
||||
}
|
||||
#if swift(>=3.1)
|
||||
if Swift.type(of: lhs) != Swift.type(of: rhs) {
|
||||
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 && rhs.size == lhs.size
|
||||
}
|
||||
return rhs.path == lhs.path && rhs.size == lhs.size && rhs.modifiedDate == lhs.modifiedDate
|
||||
}
|
||||
|
||||
open override func isEqual(_ object: Any?) -> Bool {
|
||||
guard let object = object as? FileObject else { return false }
|
||||
return self == object
|
||||
}
|
||||
}
|
||||
|
||||
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"]
|
||||
var result = [String: Any]()
|
||||
for (key, value) in allValues {
|
||||
if let convertkey = mapDict[key] {
|
||||
result[convertkey] = value
|
||||
}
|
||||
}
|
||||
result["eTag"] = result["entryTag"]
|
||||
result["filesize"] = result["fileSize"]
|
||||
result["isReadOnly"] = self.isReadOnly
|
||||
result["isDirectory"] = self.isDirectory
|
||||
result["isRegularFile"] = self.isRegularFile
|
||||
result["isSymLink"] = self.isSymLink
|
||||
result["type"] = typeDict[self.type] ?? "unknown"
|
||||
return result
|
||||
}
|
||||
|
||||
/// 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]
|
||||
|
||||
if let cQuery = query as? NSCompoundPredicate {
|
||||
let newSub = cQuery.subpredicates.map { convertPredicate(fromSpotlight: $0 as! NSPredicate) }
|
||||
switch cQuery.compoundPredicateType {
|
||||
case .and: return NSCompoundPredicate(andPredicateWithSubpredicates: newSub)
|
||||
case .not: return NSCompoundPredicate(notPredicateWithSubpredicate: newSub[0])
|
||||
case .or: return NSCompoundPredicate(orPredicateWithSubpredicates: newSub)
|
||||
}
|
||||
} else if let cQuery = query as? NSComparisonPredicate {
|
||||
var newLeft = cQuery.leftExpression
|
||||
var newRight = cQuery.rightExpression
|
||||
if newLeft.expressionType == .keyPath, let newKey = mapDict[newLeft.keyPath] {
|
||||
newLeft = NSExpression(forKeyPath: newKey.rawValue)
|
||||
}
|
||||
if newRight.expressionType == .keyPath, let newKey = mapDict[newRight.keyPath] {
|
||||
newRight = NSExpression(forKeyPath: newKey.rawValue)
|
||||
}
|
||||
return NSComparisonPredicate(leftExpression: newLeft, rightExpression: newRight, modifier: cQuery.comparisonPredicateModifier, type: cQuery.predicateOperatorType, options: cQuery.options)
|
||||
} else {
|
||||
return query
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Containts attributes of a provider.
|
||||
open class VolumeObject: NSObject {
|
||||
/// A `Dictionary` contains volume information, using `URLResourceKey` keys.
|
||||
open internal(set) var allValues: [URLResourceKey: Any]
|
||||
|
||||
public init(allValues: [URLResourceKey: Any]) {
|
||||
self.allValues = allValues
|
||||
}
|
||||
|
||||
/// The root directory of the resource’s volume, returned as an `URL` object.
|
||||
open internal(set) var url: URL? {
|
||||
get {
|
||||
return allValues[.volumeURLKey] as? URL
|
||||
}
|
||||
set {
|
||||
allValues[.volumeURLKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// The name of the volume.
|
||||
open internal(set) var name: String? {
|
||||
get {
|
||||
return allValues[.volumeNameKey] as? String
|
||||
}
|
||||
set {
|
||||
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 {
|
||||
get {
|
||||
return allValues[.volumeTotalCapacityKey] as? Int64 ?? -1
|
||||
}
|
||||
set {
|
||||
allValues[.volumeTotalCapacityKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// The volume’s available capacity in bytes.
|
||||
open internal(set) var availableCapacity: Int64 {
|
||||
get {
|
||||
return allValues[.volumeAvailableCapacityKey] as? Int64 ?? 0
|
||||
}
|
||||
set {
|
||||
allValues[.volumeAvailableCapacityKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
open internal(set) var usage: Int64 {
|
||||
get {
|
||||
return totalCapacity >= 0 ? totalCapacity - availableCapacity : -availableCapacity
|
||||
}
|
||||
set {
|
||||
availableCapacity = totalCapacity >= 0 ? totalCapacity - newValue : -newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// the volume’s creation date, returned as an `Date` object, or NULL if it cannot be determined
|
||||
open internal(set) var creationDate: Date? {
|
||||
get {
|
||||
return allValues[.volumeCreationDateKey] as? Date
|
||||
}
|
||||
set {
|
||||
allValues[.volumeCreationDateKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Determining whether the volume is read-only
|
||||
open internal(set) var isReadOnly: Bool {
|
||||
get {
|
||||
return allValues[.volumeIsReadOnlyKey] as? Bool ?? false
|
||||
}
|
||||
set {
|
||||
allValues[.volumeIsReadOnlyKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 10.0, macOS 10.12, tvOS 10.0, *)
|
||||
open internal(set) var isEncrypted: Bool {
|
||||
get {
|
||||
return allValues[.volumeIsEncryptedKey] as? Bool ?? false
|
||||
}
|
||||
set {
|
||||
allValues[.volumeIsEncryptedKey] = !newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Sorting FileObject array by given criteria, **not thread-safe**
|
||||
public struct FileObjectSorting {
|
||||
|
||||
/// Determines sort kind by which item of File object
|
||||
public enum SortType {
|
||||
/// Sorting by default Finder (case-insensitive) behavior
|
||||
case name
|
||||
/// Sorting by case-sensitive form of file name
|
||||
case nameCaseSensitive
|
||||
/// Sorting by case-in sensitive form of file name
|
||||
case nameCaseInsensitive
|
||||
/// Sorting by file type
|
||||
case `extension`
|
||||
/// Sorting by file modified date
|
||||
case modifiedDate
|
||||
/// Sorting by file creation date
|
||||
case creationDate
|
||||
/// Sorting by file modified date
|
||||
case size
|
||||
|
||||
/// all sort types
|
||||
static var allItems: [SortType] {
|
||||
return [.name, .nameCaseSensitive, .nameCaseInsensitive, .extension,
|
||||
.modifiedDate,.creationDate, .size]
|
||||
}
|
||||
}
|
||||
|
||||
public let sortType: SortType
|
||||
/// puts A before Z, default is true
|
||||
public let ascending: Bool
|
||||
/// puts directories on top, regardless of other attributes, default is false
|
||||
public let isDirectoriesFirst: Bool
|
||||
|
||||
public static let nameAscending = FileObjectSorting(type: .name, ascending: true)
|
||||
public static let nameDesceding = FileObjectSorting(type: .name, ascending: false)
|
||||
public static let sizeAscending = FileObjectSorting(type: .size, ascending: true)
|
||||
public static let sizeDesceding = FileObjectSorting(type: .size, ascending: false)
|
||||
public static let extensionAscending = FileObjectSorting(type: .extension, ascending: true)
|
||||
public static let extensionDesceding = FileObjectSorting(type: .extension, ascending: false)
|
||||
public static let modifiedAscending = FileObjectSorting(type: .modifiedDate, ascending: true)
|
||||
public static let modifiedDesceding = FileObjectSorting(type: .modifiedDate, ascending: false)
|
||||
public static let createdAscending = FileObjectSorting(type: .creationDate, ascending: true)
|
||||
public static let createdDesceding = FileObjectSorting(type: .creationDate, ascending: false)
|
||||
|
||||
/// Initializes a `FileObjectSorting` allows to sort an `Array` of `FileObject`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - type: Determines to sort based on which file property.
|
||||
/// - ascending: `true` of resulting `Array` is ascending
|
||||
/// - isDirectoriesFirst: Puts directoris on the top of resulting `Array`.
|
||||
public init (type: SortType, ascending: Bool = true, isDirectoriesFirst: Bool = false) {
|
||||
self.sortType = type
|
||||
self.ascending = ascending
|
||||
self.isDirectoriesFirst = isDirectoriesFirst
|
||||
}
|
||||
|
||||
/// Sorts array of `FileObject`s by criterias set in attributes.
|
||||
public func sort(_ files: [FileObject]) -> [FileObject] {
|
||||
return files.sorted {
|
||||
if isDirectoriesFirst {
|
||||
if ($0.isDirectory) && !($1.isDirectory) {
|
||||
return true
|
||||
}
|
||||
if !($0.isDirectory) && ($1.isDirectory) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
switch sortType {
|
||||
case .name:
|
||||
return ($0.name).localizedStandardCompare($1.name) == (ascending ? .orderedAscending : .orderedDescending)
|
||||
case .nameCaseSensitive:
|
||||
return ($0.name).localizedCompare($1.name) == (ascending ? .orderedAscending : .orderedDescending)
|
||||
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
|
||||
return kind1.localizedCaseInsensitiveCompare(kind2) == (ascending ? .orderedAscending : .orderedDescending)
|
||||
case .modifiedDate:
|
||||
let fileMod1 = $0.modifiedDate ?? Date.distantPast
|
||||
let fileMod2 = $1.modifiedDate ?? Date.distantPast
|
||||
return ascending ? fileMod1 < fileMod2 : fileMod1 > fileMod2
|
||||
case .creationDate:
|
||||
let fileCreation1 = $0.creationDate ?? Date.distantPast
|
||||
let fileCreation2 = $1.creationDate ?? Date.distantPast
|
||||
return ascending ? fileCreation1 < fileCreation2 : fileCreation1 > fileCreation2
|
||||
case .size:
|
||||
return ascending ? $0.size < $1.size : $0.size > $1.size
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,9 @@
|
||||
// Created by Amir Abbas Mousavian on 5/6/95.
|
||||
//
|
||||
//
|
||||
|
||||
#import <TargetConditionals.h>
|
||||
|
||||
#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR
|
||||
#import <UIKit/UIKit.h>
|
||||
//! Project version number for FileProvider iOS.
|
||||
|
||||
+1084
-291
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,641 @@
|
||||
//
|
||||
// HTTPFileProvider.swift
|
||||
// FilesProvider
|
||||
//
|
||||
// Created by Amir Abbas Mousavian.
|
||||
// Copyright © 2017 Mousavian. Distributed under MIT license.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
The abstract base class for all REST/Web based providers such as WebDAV, Dropbox, OneDrive, Google Drive, etc. and encapsulates basic
|
||||
functionalitis such as downloading/uploading.
|
||||
|
||||
No instance of this class should (and can) be created. Use derived classes instead. It leads to a crash with `fatalError()`.
|
||||
*/
|
||||
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?
|
||||
open var dispatch_queue: DispatchQueue
|
||||
open var operation_queue: OperationQueue {
|
||||
willSet {
|
||||
assert(_session == nil, "It's not effective to change dispatch_queue property after session is initialized.")
|
||||
}
|
||||
}
|
||||
|
||||
open weak var delegate: FileProviderDelegate?
|
||||
open var credential: URLCredential? {
|
||||
didSet {
|
||||
sessionDelegate?.credential = self.credential
|
||||
}
|
||||
}
|
||||
open private(set) var cache: URLCache?
|
||||
public var useCache: Bool
|
||||
public var validatingCache: Bool
|
||||
|
||||
fileprivate var _session: URLSession!
|
||||
internal fileprivate(set) var sessionDelegate: SessionDelegate?
|
||||
public var session: URLSession {
|
||||
get {
|
||||
if _session == nil {
|
||||
self.sessionDelegate = SessionDelegate(fileProvider: self)
|
||||
let config = URLSessionConfiguration.default
|
||||
config.urlCache = cache
|
||||
config.requestCachePolicy = .returnCacheDataElseLoad
|
||||
_session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDelegate?, delegateQueue: self.operation_queue)
|
||||
_session.sessionDescription = UUID().uuidString
|
||||
initEmptySessionHandler(_session.sessionDescription!)
|
||||
}
|
||||
return _session
|
||||
}
|
||||
|
||||
set {
|
||||
assert(newValue.delegate is SessionDelegate, "session instances should have a SessionDelegate instance as delegate.")
|
||||
_session = newValue
|
||||
if _session.sessionDescription?.isEmpty ?? true {
|
||||
_session.sessionDescription = UUID().uuidString
|
||||
}
|
||||
self.sessionDelegate = newValue.delegate as? SessionDelegate
|
||||
initEmptySessionHandler(_session.sessionDescription!)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var _longpollSession: URLSession?
|
||||
/// This session has extended timeout up to 10 minutes, suitable for monitoring.
|
||||
internal var longpollSession: URLSession {
|
||||
if _longpollSession == nil {
|
||||
let config = URLSessionConfiguration.default
|
||||
config.timeoutIntervalForRequest = 600
|
||||
_longpollSession = URLSession(configuration: config, delegate: nil, delegateQueue: nil)
|
||||
}
|
||||
return _longpollSession!
|
||||
}
|
||||
|
||||
/**
|
||||
This is parent initializer for subclasses. Using this method on `HTTPFileProvider` will fail as `type` is not implemented.
|
||||
|
||||
- Parameters:
|
||||
- baseURL: Location of server.
|
||||
- credential: An `URLCredential` object with `user` and `password`.
|
||||
- cache: A URLCache to cache downloaded files and contents.
|
||||
*/
|
||||
public init(baseURL: URL?, credential: URLCredential?, cache: URLCache?) {
|
||||
// Make base url absolute and path as directory
|
||||
let urlStr = baseURL?.absoluteString
|
||||
self.baseURL = urlStr.flatMap { $0.hasSuffix("/") ? URL(string: $0) : URL(string: $0 + "/") }
|
||||
self.useCache = false
|
||||
self.validatingCache = true
|
||||
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
|
||||
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")
|
||||
aCoder.encode(self.useCache, forKey: "useCache")
|
||||
aCoder.encode(self.validatingCache, forKey: "validatingCache")
|
||||
}
|
||||
|
||||
public static var supportsSecureCoding: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
open func copy(with zone: NSZone? = nil) -> Any {
|
||||
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
|
||||
}
|
||||
|
||||
open func contentsOfDirectory(path: String, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) {
|
||||
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
|
||||
}
|
||||
|
||||
open func attributesOfItem(path: String, completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) {
|
||||
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
|
||||
}
|
||||
|
||||
open func storageProperties(completionHandler: @escaping (_ volumeInfo: VolumeObject?) -> Void) {
|
||||
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 (_ success: Bool, _ error: Error?) -> Void) {
|
||||
self.storageProperties { volume in
|
||||
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) + "/"
|
||||
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))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
let operation = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
let request = self.request(for: operation, overwrite: overwrite)
|
||||
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
|
||||
do {
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
guard let tempURL = tempURL else {
|
||||
throw cantLoadError
|
||||
}
|
||||
|
||||
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 {
|
||||
completionHandler?(error)
|
||||
self?.delegateNotify(operation, error: error)
|
||||
}
|
||||
})
|
||||
|
||||
if let error = coordError {
|
||||
throw error
|
||||
}
|
||||
} catch {
|
||||
completionHandler?(error)
|
||||
self?.delegateNotify(operation, error: error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
Progressively fetch data of file and returns fetched data in `progressHandler`.
|
||||
If path specifies a directory, or if some other error occurs, data will be nil.
|
||||
|
||||
- Parameters:
|
||||
- path: Path of file.
|
||||
- progressHandler: a closure called every time a new `Data` is available.
|
||||
- position: start position of data fetched.
|
||||
- data: a portion of contents of file in a `Data` object.
|
||||
- completionHandler: a closure with result of file contents or error.
|
||||
- error: `Error` returned by system if occured.
|
||||
- Returns: An `Progress` to get progress or cancel 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)
|
||||
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)
|
||||
position += Int64(data.count)
|
||||
}, 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 {
|
||||
completionHandler(Data(), nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
let operation = FileOperationType.fetch(path: path)
|
||||
var request = self.request(for: operation)
|
||||
let cantLoadError = urlError(path, code: .cannotLoadFromNetwork)
|
||||
request.setValue(rangeWithOffset: offset, length: length)
|
||||
return self.download_simple(path: path, request: request, operation: operation, completionHandler: { (tempURL, error) in
|
||||
do {
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
guard let tempURL = tempURL else {
|
||||
throw cantLoadError
|
||||
}
|
||||
|
||||
let data = try Data(contentsOf: tempURL)
|
||||
completionHandler(data, nil)
|
||||
} catch {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@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 request = self.request(for: operation, overwrite: overwrite, attributes: [.contentModificationDateKey: Date()])
|
||||
return upload_data(path, request: request, data: data ?? Data(), operation: operation, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
internal func request(for operation: FileOperationType, overwrite: Bool = false, attributes: [URLResourceKey: Any] = [:]) -> URLRequest {
|
||||
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
|
||||
}
|
||||
|
||||
internal func serverError(with code: FileProviderHTTPErrorCode, path: String?, data: Data?) -> FileProviderHTTPError {
|
||||
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
|
||||
}
|
||||
|
||||
internal func multiStatusHandler(source: String, data: Data, completionHandler: SimpleCompletionHandler) -> Void {
|
||||
// WebDAV will override this function
|
||||
}
|
||||
|
||||
/**
|
||||
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 ?? Progress(totalUnitCount: 1)
|
||||
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
||||
progress.kind = .file
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
||||
|
||||
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 {
|
||||
if response.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) {
|
||||
serverError = self.serverError(with: code, path: operation.source, data: data)
|
||||
}
|
||||
|
||||
if FileProviderHTTPErrorCode(rawValue: response.statusCode) == .multiStatus, let data = data {
|
||||
self.multiStatusHandler(source: operation.source, data: data, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
|
||||
if serverError == nil && error == nil {
|
||||
progress.completedUnitCount = 1
|
||||
} else {
|
||||
progress.cancel()
|
||||
}
|
||||
|
||||
completionHandler?(serverError ?? error)
|
||||
self.delegateNotify(operation, error: serverError ?? error)
|
||||
})
|
||||
task.taskDescription = operation.json
|
||||
progress.cancellationHandler = { [weak task] in
|
||||
task?.cancel()
|
||||
}
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
task.resume()
|
||||
return 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
|
||||
}
|
||||
|
||||
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 {
|
||||
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()
|
||||
}
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
task.resume()
|
||||
}
|
||||
// codebeat:enable[ARITY]
|
||||
|
||||
internal var maxUploadSimpleSupported: Int64 { return Int64.max }
|
||||
|
||||
func upload_task(_ targetPath: String, progress: Progress, task: URLSessionTask, operation: FileOperationType,
|
||||
completionHandler: SimpleCompletionHandler) -> Void {
|
||||
var progress = progress
|
||||
|
||||
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
|
||||
task.addObserver(self.sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent), options: .new, context: &progress)
|
||||
progress.cancellationHandler = { [weak task] in
|
||||
task?.cancel()
|
||||
}
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
task.resume()
|
||||
}
|
||||
|
||||
func upload_data(_ targetPath: String, request: URLRequest, data: Data, operation: FileOperationType,
|
||||
completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let size: Int64 = Int64(data.count)
|
||||
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)
|
||||
|
||||
let task = session.uploadTask(with: request, from: data)
|
||||
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]))?.fileSize
|
||||
let size = Int64(fSize ?? -1)
|
||||
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 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)
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
|
||||
if error != nil {
|
||||
progress.cancel()
|
||||
}
|
||||
completionHandler(error)
|
||||
self.delegateNotify(operation, error: 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
|
||||
}
|
||||
|
||||
internal func download_simple(path: String, request: URLRequest, operation: FileOperationType,
|
||||
completionHandler: @escaping ((_ tempURL: URL?, _ error: Error?) -> Void)) -> Progress? {
|
||||
var 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 {
|
||||
progress.cancel()
|
||||
}
|
||||
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 {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: (task.response as? HTTPURLResponse)?.statusCode ?? -1)
|
||||
let errorData : Data? = try? Data(contentsOf: tempURL)
|
||||
let serverError = code.flatMap { self.serverError(with: $0, path: path, data: errorData) }
|
||||
if serverError != nil {
|
||||
progress.cancel()
|
||||
}
|
||||
completionHandler(nil, serverError)
|
||||
self.delegateNotify(operation)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
progress.cancellationHandler = { [weak task] in
|
||||
task?.cancel()
|
||||
}
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
task.resume()
|
||||
return progress
|
||||
}
|
||||
}
|
||||
|
||||
extension HTTPFileProvider: FileProvider { }
|
||||
+614
-416
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,241 @@
|
||||
//
|
||||
// LocalFileProvider.swift
|
||||
// FileProvider
|
||||
//
|
||||
// Created by Amir Abbas Mousavian.
|
||||
// Copyright © 2016 Mousavian. Distributed under MIT license.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Containts path, url and attributes of a local file or resource.
|
||||
public final class LocalFileObject: FileObject {
|
||||
internal override init(url: URL?, name: String, path: String) {
|
||||
super.init(url: url, name: name, path: path)
|
||||
}
|
||||
|
||||
/// 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)
|
||||
if #available(iOS 9.0, macOS 10.11, *) {
|
||||
fileURL = URL(fileURLWithPath: rpath, relativeTo: relativeURL)
|
||||
} else {
|
||||
rpath = rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? rpath
|
||||
fileURL = URL(string: rpath, relativeTo: relativeURL) ?? relativeURL
|
||||
}
|
||||
|
||||
if let fileURL = fileURL {
|
||||
self.init(fileWithURL: fileURL)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Initiates a `LocalFileObject` with attributes of file in url.
|
||||
public convenience init?(fileWithURL fileURL: URL) {
|
||||
do {
|
||||
let values = try fileURL.resourceValues(forKeys: [.nameKey, .fileSizeKey, .totalFileSizeKey, .fileAllocatedSizeKey, .totalFileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .fileResourceTypeKey, .isHiddenKey, .isWritableKey, .typeIdentifierKey, .generationIdentifierKey, .documentIdentifierKey])
|
||||
let path = fileURL.relativePath.hasPrefix("/") ? fileURL.relativePath : "/" + fileURL.relativePath
|
||||
|
||||
self.init(url: fileURL, name: values.name ?? fileURL.lastPathComponent, path: path)
|
||||
for (key, value) in values.allValues {
|
||||
self.allValues[key] = value
|
||||
}
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// The total size allocated on disk for the file
|
||||
open internal(set) var allocatedSize: Int64 {
|
||||
get {
|
||||
return allValues[.fileAllocatedSizeKey] as? Int64 ?? 0
|
||||
}
|
||||
set {
|
||||
allValues[.fileAllocatedSizeKey] = Int(exactly: newValue) ?? Int.max
|
||||
}
|
||||
}
|
||||
|
||||
/// 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? {
|
||||
get {
|
||||
return allValues[.documentIdentifierKey] as? Int
|
||||
}
|
||||
set {
|
||||
allValues[.documentIdentifierKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// 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? {
|
||||
get {
|
||||
let data = allValues[.generationIdentifierKey] as? Data
|
||||
return data?.map { String(format: "%02hhx", $0) }.joined()
|
||||
}
|
||||
}
|
||||
|
||||
/// Count of children items of a driectory. It costs disk access for local directories.
|
||||
open public(set) override var childrensCount: Int? {
|
||||
get {
|
||||
return try? FileManager.default.contentsOfDirectory(atPath: self.url.path).count
|
||||
}
|
||||
set {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
public var url: URL
|
||||
|
||||
/// Creates a folder monitor object with monitoring enabled.
|
||||
public init(url: URL, handler: @escaping ()->Void) {
|
||||
self.url = url
|
||||
descriptor = open((url.absoluteURL as NSURL).fileSystemRepresentation, O_EVTONLY)
|
||||
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
|
||||
guard let `self` = self else { return }
|
||||
if Date().timeIntervalSinceReferenceDate < self.monitoredTime + 0.2 {
|
||||
return
|
||||
}
|
||||
self.monitoredTime = Date().timeIntervalSinceReferenceDate
|
||||
self.source.suspend()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: {
|
||||
handler()
|
||||
self.source.resume()
|
||||
})
|
||||
}
|
||||
source.setEventHandler(handler: main_handler)
|
||||
source.setCancelHandler {
|
||||
close(self.descriptor)
|
||||
}
|
||||
start()
|
||||
}
|
||||
|
||||
/// Starts sending notifications if currently stopped
|
||||
public func start() {
|
||||
if !state {
|
||||
state = true
|
||||
source.resume()
|
||||
}
|
||||
}
|
||||
|
||||
/// Stops sending notifications if currently enabled
|
||||
public func stop() {
|
||||
if state {
|
||||
state = false
|
||||
source.suspend()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
source.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
internal class LocalFileProviderManagerDelegate: NSObject, FileManagerDelegate {
|
||||
weak var provider: LocalFileProvider?
|
||||
|
||||
init(provider: LocalFileProvider) {
|
||||
self.provider = provider
|
||||
}
|
||||
|
||||
func fileManager(_ fileManager: FileManager, shouldCopyItemAt srcURL: URL, to dstURL: URL) -> Bool {
|
||||
guard let provider = self.provider, let delegate = provider.fileOperationDelegate else {
|
||||
return true
|
||||
}
|
||||
let srcPath = provider.relativePathOf(url: srcURL)
|
||||
let dstPath = provider.relativePathOf(url: dstURL)
|
||||
return delegate.fileProvider(provider, shouldDoOperation: .copy(source: srcPath, destination: dstPath))
|
||||
}
|
||||
|
||||
func fileManager(_ fileManager: FileManager, shouldMoveItemAt srcURL: URL, to dstURL: URL) -> Bool {
|
||||
guard let provider = self.provider, let delegate = provider.fileOperationDelegate else {
|
||||
return true
|
||||
}
|
||||
let srcPath = provider.relativePathOf(url: srcURL)
|
||||
let dstPath = provider.relativePathOf(url: dstURL)
|
||||
return delegate.fileProvider(provider, shouldDoOperation: .move(source: srcPath, destination: dstPath))
|
||||
}
|
||||
|
||||
func fileManager(_ fileManager: FileManager, shouldRemoveItemAt URL: URL) -> Bool {
|
||||
guard let provider = self.provider, let delegate = provider.fileOperationDelegate else {
|
||||
return true
|
||||
}
|
||||
let path = provider.relativePathOf(url: URL)
|
||||
return delegate.fileProvider(provider, shouldDoOperation: .remove(path: path))
|
||||
}
|
||||
|
||||
func fileManager(_ fileManager: FileManager, shouldLinkItemAt srcURL: URL, to dstURL: URL) -> Bool {
|
||||
guard let provider = self.provider, let delegate = provider.fileOperationDelegate else {
|
||||
return true
|
||||
}
|
||||
let srcPath = provider.relativePathOf(url: srcURL)
|
||||
let dstPath = provider.relativePathOf(url: dstURL)
|
||||
return delegate.fileProvider(provider, shouldDoOperation: .link(link: srcPath, target: dstPath))
|
||||
}
|
||||
|
||||
func fileManager(_ fileManager: FileManager, shouldProceedAfterError error: Error, copyingItemAt srcURL: URL, to dstURL: URL) -> Bool {
|
||||
guard let provider = self.provider, let delegate = provider.fileOperationDelegate else {
|
||||
return false
|
||||
}
|
||||
let srcPath = provider.relativePathOf(url: srcURL)
|
||||
let dstPath = provider.relativePathOf(url: dstURL)
|
||||
return delegate.fileProvider(provider, shouldProceedAfterError: error, operation: .copy(source: srcPath, destination: dstPath))
|
||||
}
|
||||
|
||||
func fileManager(_ fileManager: FileManager, shouldProceedAfterError error: Error, movingItemAt srcURL: URL, to dstURL: URL) -> Bool {
|
||||
guard let provider = self.provider, let delegate = provider.fileOperationDelegate else {
|
||||
return false
|
||||
}
|
||||
let srcPath = provider.relativePathOf(url: srcURL)
|
||||
let dstPath = provider.relativePathOf(url: dstURL)
|
||||
return delegate.fileProvider(provider, shouldProceedAfterError: error, operation: .move(source: srcPath, destination: dstPath))
|
||||
}
|
||||
|
||||
func fileManager(_ fileManager: FileManager, shouldProceedAfterError error: Error, removingItemAt URL: URL) -> Bool {
|
||||
guard let provider = self.provider, let delegate = provider.fileOperationDelegate else {
|
||||
return false
|
||||
}
|
||||
let path = provider.relativePathOf(url: URL)
|
||||
return delegate.fileProvider(provider, shouldProceedAfterError: error, operation: .remove(path: path))
|
||||
}
|
||||
|
||||
func fileManager(_ fileManager: FileManager, shouldProceedAfterError error: Error, linkingItemAt srcURL: URL, to dstURL: URL) -> Bool {
|
||||
guard let provider = self.provider, let delegate = provider.fileOperationDelegate else {
|
||||
return false
|
||||
}
|
||||
let srcPath = provider.relativePathOf(url: srcURL)
|
||||
let dstPath = provider.relativePathOf(url: dstURL)
|
||||
return delegate.fileProvider(provider, shouldProceedAfterError: error, operation: .link(link: srcPath, target: dstPath))
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS)
|
||||
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
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,645 @@
|
||||
//
|
||||
// OneDriveFileProvider.swift
|
||||
// FileProvider
|
||||
//
|
||||
// Created by Amir Abbas Mousavian.
|
||||
// Copyright © 2017 Mousavian. Distributed under MIT license.
|
||||
//
|
||||
|
||||
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
|
||||
to use Foundation `NSURLCache` system.
|
||||
|
||||
- Note: You can pass file id instead of file path, e.g `"id:1234abcd"`, to point to a file or folder by ID.
|
||||
|
||||
- Note: Uploading files and data are limited to 100MB, for now.
|
||||
*/
|
||||
open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
override open class var type: String { return "OneDrive" }
|
||||
|
||||
/// Route to access file container on OneDrive. For default logined user use `.me` otherwise you can acesss
|
||||
/// container based on drive id, group id, site id or user id for another user's default container
|
||||
public enum Route: RawRepresentable {
|
||||
/// Access to default container for current user
|
||||
case me
|
||||
/// Access to a specific drive by id
|
||||
case drive(uuid: UUID)
|
||||
/// Access to a default drive of a group by their id
|
||||
case group(uuid: UUID)
|
||||
/// Access to a default drive of a site by their id
|
||||
case site(uuid: UUID)
|
||||
/// Access to a default drive of a user by their id
|
||||
case user(uuid: UUID)
|
||||
|
||||
public init?(rawValue: String) {
|
||||
let components = rawValue.components(separatedBy: ";")
|
||||
guard let type = components.first else {
|
||||
return nil
|
||||
}
|
||||
if type == "me" {
|
||||
self = .me
|
||||
}
|
||||
guard let uuid = components.last.flatMap({ UUID(uuidString: $0) }) else {
|
||||
return nil
|
||||
}
|
||||
switch type {
|
||||
case "drive":
|
||||
self = .drive(uuid: uuid)
|
||||
case "group":
|
||||
self = .group(uuid: uuid)
|
||||
case "site":
|
||||
self = .site(uuid: uuid)
|
||||
case "user":
|
||||
self = .user(uuid: uuid)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public var rawValue: String {
|
||||
switch self {
|
||||
case .me:
|
||||
return "me;"
|
||||
case .drive(uuid: let uuid):
|
||||
return "drive;" + uuid.uuidString
|
||||
case .group(uuid: let uuid):
|
||||
return "group;" + uuid.uuidString
|
||||
case .site(uuid: let uuid):
|
||||
return "site;" + uuid.uuidString
|
||||
case .user(uuid: let uuid):
|
||||
return "user;" + uuid.uuidString
|
||||
}
|
||||
}
|
||||
|
||||
/// Return path component in URL for selected drive
|
||||
var drivePath: String {
|
||||
switch self {
|
||||
case .me:
|
||||
return "me/drive"
|
||||
case .drive(uuid: let uuid):
|
||||
return "drives/" + uuid.uuidString
|
||||
case .group(uuid: let uuid):
|
||||
return "groups/" + uuid.uuidString + "/drive"
|
||||
case .site(uuid: let uuid):
|
||||
return "sites/" + uuid.uuidString + "/drive"
|
||||
case .user(uuid: let uuid):
|
||||
return "users/" + uuid.uuidString + "/drive"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
/**
|
||||
Initializer for Onedrive provider with given client ID and Token.
|
||||
These parameters must be retrieved via [Authentication for the OneDrive API](https://dev.onedrive.com/auth/readme.htm).
|
||||
|
||||
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.
|
||||
|
||||
- Parameters:
|
||||
- credential: a `URLCredential` object with Client ID set as `user` and Token set as `password`.
|
||||
- serverURL: server url, Set it if you are trying to connect OneDrive Business server, otherwise leave it
|
||||
`nil` to connect to OneDrive Personal user.
|
||||
- drive: drive name for user on server, default value is `root`.
|
||||
- cache: A URLCache to cache downloaded files and contents.
|
||||
*/
|
||||
@available(*, deprecated, message: "use init(credential:, serverURL:, route:, cache:) instead.")
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
Initializer for Onedrive provider with given client ID and Token.
|
||||
These parameters must be retrieved via [Authentication for the OneDrive API](https://dev.onedrive.com/auth/readme.htm).
|
||||
|
||||
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. Also you can use [auth0/Lock](https://github.com/auth0/Lock.iOS-OSX) which provides graphical user interface.
|
||||
|
||||
- Parameters:
|
||||
- credential: a `URLCredential` object with Client ID set as `user` and Token set as `password`.
|
||||
- serverURL: server url, Set it if you are trying to connect OneDrive Business server, otherwise leave it
|
||||
`nil` to connect to OneDrive Personal uses.
|
||||
- route: drive name for user on server, default value is `.me`.
|
||||
- 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 ?? 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)
|
||||
}
|
||||
|
||||
public required convenience init?(coder aDecoder: NSCoder) {
|
||||
let route: Route
|
||||
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
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
open override func encode(with aCoder: NSCoder) {
|
||||
super.encode(with: aCoder)
|
||||
aCoder.encode(self.route.rawValue, forKey: "route")
|
||||
}
|
||||
|
||||
open override func copy(with zone: NSZone? = nil) -> Any {
|
||||
let copy = OneDriveFileProvider(credential: self.credential, serverURL: self.baseURL, route: self.route, cache: self.cache)
|
||||
copy.delegate = self.delegate
|
||||
copy.fileOperationDelegate = self.fileOperationDelegate
|
||||
copy.useCache = self.useCache
|
||||
copy.validatingCache = self.validatingCache
|
||||
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.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)
|
||||
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) {
|
||||
files.append(file)
|
||||
}
|
||||
}
|
||||
return (files, nil, json["@odata.nextLink"] as? String)
|
||||
}, 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.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, 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
|
||||
}
|
||||
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) {
|
||||
let url = URL(string: route.drivePath, relativeTo: baseURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
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).compactMap { $0.value as? String }.first
|
||||
|
||||
return paginated(path, requestHandler: { [weak self] (token) -> URLRequest? in
|
||||
guard let `self` = self else { return nil }
|
||||
|
||||
let url: URL
|
||||
if let next = token.flatMap(URL.init(string:)) {
|
||||
url = next
|
||||
} else {
|
||||
let bURL = self.baseURL!.appendingPathComponent(self.route.drivePath).appendingPathComponent("root/search")
|
||||
var components = URLComponents(url: bURL, resolvingAgainstBaseURL: false)!
|
||||
let qItem = URLQueryItem(name: "q", value: (queryStr ?? "*"))
|
||||
components.queryItems = [qItem]
|
||||
if recursive {
|
||||
components.queryItems?.append(URLQueryItem(name: "expand", value: "children"))
|
||||
}
|
||||
url = components.url!
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
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)
|
||||
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()) {
|
||||
foundFiles.append(file)
|
||||
foundItemHandler?(file)
|
||||
}
|
||||
}
|
||||
|
||||
return (foundFiles, nil, json["@odata.nextLink"] as? String)
|
||||
}, 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 {
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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.setValue(authentication: credential, with: .oAuth2)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
let status = (response as? HTTPURLResponse)?.statusCode ?? 400
|
||||
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()
|
||||
}
|
||||
|
||||
/**
|
||||
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?(self.urlError(localFile.path, code: .fileIsDirectory))
|
||||
}
|
||||
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 {
|
||||
if path.hasPrefix("id:") {
|
||||
return path
|
||||
}
|
||||
var p = path.hasPrefix("/") ? path : "/" + path
|
||||
if p.hasSuffix("/") {
|
||||
p.remove(at: p.index(before:p.endIndex))
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
let method: String
|
||||
let url: URL
|
||||
switch operation {
|
||||
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 as NSString).deletingLastPathComponent
|
||||
url = self.url(of: parent, modifier: "children")
|
||||
case .modify(path: let path):
|
||||
method = "PUT"
|
||||
let queryStr = overwrite ? "" : "?@name.conflictBehavior=fail"
|
||||
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 = 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)
|
||||
case .remove(path: let path):
|
||||
method = "DELETE"
|
||||
url = self.url(of: path)
|
||||
default: // link
|
||||
fatalError("Unimplemented operation \(operation.description) in \(#file)")
|
||||
}
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = method
|
||||
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: AnyObject]()
|
||||
let name = (path as NSString).lastPathComponent
|
||||
requestDictionary["name"] = name as NSString
|
||||
requestDictionary["folder"] = NSDictionary()
|
||||
requestDictionary["@microsoft.graph.conflictBehavior"] = "fail" as NSString
|
||||
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.setValue(contentType: .json, charset: .utf8)
|
||||
let cdest = correctPath(dest) as NSString
|
||||
var parentReference: [String: AnyObject] = [:]
|
||||
if cdest.hasPrefix("id:") {
|
||||
parentReference["id"] = cdest.components(separatedBy: "/").first?.replacingOccurrences(of: "id:", with: "", options: .anchored) as NSString?
|
||||
} else {
|
||||
parentReference["path"] = ("/drive/root:" as NSString).appendingPathComponent(cdest.deletingLastPathComponent) as NSString
|
||||
}
|
||||
switch self.route {
|
||||
case .drive(uuid: let uuid):
|
||||
parentReference["driveId"] = uuid.uuidString as NSString
|
||||
default:
|
||||
//parentReference["driveId"] = cachedDriveID as NSString? ?? ""
|
||||
break
|
||||
}
|
||||
var requestDictionary = [String: AnyObject]()
|
||||
requestDictionary["parentReference"] = parentReference as NSDictionary
|
||||
requestDictionary["name"] = (cdest as NSString).lastPathComponent as NSString
|
||||
request.httpBody = Data(jsonDictionary: requestDictionary)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
return request
|
||||
}
|
||||
|
||||
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
|
||||
} else {
|
||||
errorDesc = data.flatMap({ String(data: $0, encoding: .utf8) })
|
||||
}
|
||||
return FileProviderOneDriveError(code: code, path: path ?? "", serverDescription: errorDesc)
|
||||
}
|
||||
|
||||
override var maxUploadSimpleSupported: Int64 {
|
||||
return 4_194_304 // 4MB!
|
||||
}
|
||||
|
||||
fileprivate func registerNotifcation(path: String, eventHandler: (() -> Void)) {
|
||||
/* There is two ways to monitor folders changing in OneDrive. Either using webooks
|
||||
* which means you have to implement a server to translate it to push notifications
|
||||
* or using apiv2 list_folder/longpoll method. The second one is implemeted here.
|
||||
* Tough webhooks are much more efficient, longpoll is much simpler to implement!
|
||||
* You can implemnt your own webhook service and replace this method accordingly.
|
||||
*/
|
||||
NotImplemented()
|
||||
}
|
||||
fileprivate func unregisterNotifcation(path: String) {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
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: "createLink"))
|
||||
request.httpMethod = "POST"
|
||||
let requestDictionary: [String: AnyObject] = ["type": "view" as NSString]
|
||||
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, 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)
|
||||
}
|
||||
}
|
||||
completionHandler(link, nil, nil, serverError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension OneDriveFileProvider: ExtendedFileProvider {
|
||||
open func propertiesOfFileSupported(path: String) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
@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 as NSString).pathExtension.lowercased()
|
||||
switch fileExt {
|
||||
case "jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff":
|
||||
return true
|
||||
case "mp3", "aac", "m4a", "wma":
|
||||
return true
|
||||
case "mp4", "mpg", "3gp", "mov", "avi", "wmv":
|
||||
return true
|
||||
case "doc", "docx", "xls", "xlsx", "ppt", "pptx", "pdf":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@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"
|
||||
}
|
||||
let url = self.url(of: path, modifier: "thumbnails")
|
||||
.appendingPathComponent("0").appendingPathComponent(thumbQuery)
|
||||
.appendingPathComponent("content")
|
||||
var request = URLRequest(url: url)
|
||||
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 >= 400, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
let responseError = self.serverError(with: rCode, path: path, data: data)
|
||||
completionHandler(nil, responseError)
|
||||
return
|
||||
}
|
||||
image = data.flatMap(ImageClass.init(data:))
|
||||
completionHandler(image, error)
|
||||
})
|
||||
task.resume()
|
||||
return nil
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
//
|
||||
// OneDriveHelper.swift
|
||||
// FileProvider
|
||||
//
|
||||
// Created by Amir Abbas Mousavian.
|
||||
// Copyright © 2017 Mousavian. Distributed under MIT license.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Error returned by OneDrive server when trying to access or do operations on a file or folder.
|
||||
public struct FileProviderOneDriveError: FileProviderHTTPError {
|
||||
public let code: FileProviderHTTPErrorCode
|
||||
public let path: String
|
||||
public let serverDescription: String?
|
||||
}
|
||||
|
||||
/// Containts path, url and attributes of a OneDrive file or resource.
|
||||
public final class OneDriveFileObject: FileObject {
|
||||
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 init? (baseURL: URL?, route: OneDriveFileProvider.Route, json: [String: AnyObject]) {
|
||||
guard let name = json["name"] as? String else { return nil }
|
||||
guard let id = json["id"] as? String else { return nil }
|
||||
let path: String
|
||||
if let refpath = json["parentReference"]?["path"] as? String {
|
||||
let parentPath: String
|
||||
if let colonIndex = refpath.index(of: ":") {
|
||||
#if swift(>=4.0)
|
||||
parentPath = String(refpath[refpath.index(after: colonIndex)...])
|
||||
#else
|
||||
parentPath = refpath.substring(from: refpath.index(after: colonIndex))
|
||||
#endif
|
||||
} else {
|
||||
parentPath = refpath
|
||||
}
|
||||
path = (parentPath as NSString).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.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).flatMap(ContentMIMEType.init(rawValue:)) ?? .stream
|
||||
self.entryTag = json["eTag"] as? String
|
||||
let hashes = json["file"]?["hashes"] as? NSDictionary
|
||||
// checks for both sha1 or quickXor. First is available in personal drives, second in business one.
|
||||
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? {
|
||||
get {
|
||||
return allValues[.fileResourceIdentifierKey] as? String
|
||||
}
|
||||
set {
|
||||
allValues[.fileResourceIdentifierKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// MIME type of file contents returned by OneDrive server.
|
||||
open internal(set) var contentType: ContentMIMEType {
|
||||
get {
|
||||
return (allValues[.mimeTypeKey] as? String).flatMap(ContentMIMEType.init(rawValue:)) ?? .stream
|
||||
}
|
||||
set {
|
||||
allValues[.mimeTypeKey] = newValue.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP E-Tag, can be used to mark changed files.
|
||||
open internal(set) var entryTag: String? {
|
||||
get {
|
||||
return allValues[.entryTagKey] as? String
|
||||
}
|
||||
set {
|
||||
allValues[.entryTagKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 fileHash: String? {
|
||||
get {
|
||||
return allValues[.documentIdentifierKey] as? String
|
||||
}
|
||||
set {
|
||||
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 as NSString).pathComponents
|
||||
return components.dropFirst().first.map { "id:\($0)" } ?? ""
|
||||
case hasPrefix("root:"):
|
||||
return crudePath.components(separatedBy: ":").dropFirst().first ?? ""
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal extension OneDriveFileProvider {
|
||||
internal 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..<Int64(data.count))
|
||||
return data[range]
|
||||
}, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
internal func upload_multipart_file(_ targetPath: String, file: URL, operation: FileOperationType,
|
||||
overwrite: Bool, completionHandler: SimpleCompletionHandler) -> 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 self.cocoaError(targetPath, code: .fileNoSuchFile)
|
||||
}
|
||||
|
||||
defer {
|
||||
handle.closeFile()
|
||||
}
|
||||
|
||||
let offset = range.lowerBound
|
||||
handle.seek(toFileOffset: UInt64(offset))
|
||||
guard Int64(handle.offsetInFile) == offset else {
|
||||
throw self.cocoaError(targetPath, code: .fileReadTooLarge)
|
||||
}
|
||||
|
||||
return handle.readData(ofLength: range.count)
|
||||
}, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
private func upload_multipart(_ targetPath: String, operation: FileOperationType, size: Int64, overwrite: Bool,
|
||||
dataProvider: @escaping (Range<Int64>) 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<Int64>? = nil, uploadedSoFar: Int64 = 0,
|
||||
progress: Progress, dataProvider: @escaping (Range<Int64>) throws -> Data, completionHandler: SimpleCompletionHandler) {
|
||||
guard !progress.isCancelled else { return }
|
||||
var progress = progress
|
||||
|
||||
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<Int64>
|
||||
if let range = range {
|
||||
if range.count > maximumSize {
|
||||
finalRange = range.lowerBound..<(range.upperBound + maximumSize)
|
||||
} else {
|
||||
finalRange = range
|
||||
}
|
||||
} else {
|
||||
finalRange = 0..<min(maximumSize, size)
|
||||
}
|
||||
request.setValue(contentRange: finalRange, totalBytes: size)
|
||||
|
||||
let data: Data
|
||||
do {
|
||||
data = try dataProvider(finalRange)
|
||||
} catch {
|
||||
dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
}
|
||||
self.delegateNotify(operation, error: error)
|
||||
return
|
||||
}
|
||||
let task = session.uploadTask(with: request, from: data)
|
||||
|
||||
var dictionary: [String: AnyObject] = ["type": operation.description as NSString]
|
||||
dictionary["source"] = operation.source as NSString?
|
||||
dictionary["dest"] = operation.destination as NSString?
|
||||
dictionary["uploadedBytes"] = uploadedSoFar as NSNumber
|
||||
dictionary["totalBytes"] = data.count as NSNumber
|
||||
task.taskDescription = String(jsonDictionary: dictionary)
|
||||
task.addObserver(self.sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent), options: .new, context: &progress)
|
||||
progress.cancellationHandler = { [weak task, weak self] in
|
||||
task?.cancel()
|
||||
var deleteRequest = URLRequest(url: url)
|
||||
deleteRequest.httpMethod = "DELETE"
|
||||
self?.session.dataTask(with: deleteRequest).resume()
|
||||
}
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
|
||||
var allData = Data()
|
||||
dataCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { data in
|
||||
allData.append(data)
|
||||
}
|
||||
// We retain self here intentionally to allow resuming upload, This behavior may change anytime!
|
||||
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { [weak task] error in
|
||||
if let error = error {
|
||||
progress.cancel()
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let json = allData.deserializeJSON() else {
|
||||
let error = URLError(.badServerResponse, userInfo: [NSURLErrorKey: url, NSURLErrorFailingURLErrorKey: url, NSURLErrorFailingURLStringErrorKey: url.absoluteString])
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
return
|
||||
}
|
||||
|
||||
if let _ = json["error"] {
|
||||
let code = ((task?.response as? HTTPURLResponse)?.statusCode).flatMap(FileProviderHTTPErrorCode.init(rawValue:)) ?? .badRequest
|
||||
let error = self.serverError(with: code, path: self.relativePathOf(url: url), data: allData)
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
return
|
||||
}
|
||||
|
||||
if let ranges = json["nextExpectedRanges"] as? [String], let firstRange = ranges.first {
|
||||
let uploaded = uploadedSoFar + Int64(finalRange.count)
|
||||
let comp = firstRange.components(separatedBy: "-")
|
||||
let lower = comp.first.flatMap(Int64.init) ?? uploaded
|
||||
let upper = comp.dropFirst().first.flatMap(Int64.init) ?? Int64.max
|
||||
let range = Range<Int64>(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()
|
||||
|
||||
func mapMediaInfo(_ json: [String: Any]) -> (dictionary: [String: Any], keys: [String]) {
|
||||
|
||||
func spaceCamelCase(_ text: String) -> String {
|
||||
var newString: String = ""
|
||||
|
||||
let upperCase = CharacterSet.uppercaseLetters
|
||||
for scalar in text.unicodeScalars {
|
||||
if upperCase.contains(scalar) {
|
||||
newString.append(" ")
|
||||
}
|
||||
let character = Character(scalar)
|
||||
newString.append(character)
|
||||
}
|
||||
|
||||
return newString.capitalized
|
||||
}
|
||||
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
|
||||
func add(key: String, value: Any?) {
|
||||
if let value = value {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if let parent = json["image"] as? [String: Any] ?? json["video"] as? [String: Any], let height = parent["height"] as? UInt64, let width = parent["width"] as? UInt64 {
|
||||
add(key: "Dimensions", value: "\(width)x\(height)")
|
||||
}
|
||||
if let location = json["location"] as? [String: Any], let latitude = location["latitude"] as? Double, let longitude = location["longitude"] as? Double {
|
||||
OneDriveFileProvider.decimalFormatter.numberStyle = .decimal
|
||||
OneDriveFileProvider.decimalFormatter.maximumFractionDigits = 5
|
||||
let latStr = OneDriveFileProvider.decimalFormatter.string(from: NSNumber(value: latitude))!
|
||||
let longStr = OneDriveFileProvider.decimalFormatter.string(from: NSNumber(value: longitude))!
|
||||
add(key: "Location", value: "\(latStr), \(longStr)")
|
||||
}
|
||||
if let parent = json["image"] as? [String: Any] ?? json["video"] as? [String: Any], let duration = parent["duration"] as? UInt64 {
|
||||
add(key: "Duration", value: (TimeInterval(duration) / 1000).formatshort)
|
||||
}
|
||||
if let timeTakenStr = json["takenDateTime"] as? String, let timeTaken = Date(rfcString: timeTakenStr) {
|
||||
OneDriveFileProvider.dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
add(key: "Date taken", value: OneDriveFileProvider.dateFormatter.string(from: timeTaken))
|
||||
}
|
||||
|
||||
if let photo = json["photo"] as? [String: Any] {
|
||||
add(key: "Device make", value: photo["cameraMake"] as? String)
|
||||
add(key: "Device model", value: photo["cameraModel"] as? String)
|
||||
add(key: "focalLength", value: photo["focalLength"] as? Double)
|
||||
add(key: "fNumber", value: photo["fNumber"] as? Double)
|
||||
if let expNom = photo["exposureNumerator"] as? Double, let expDen = photo["exposureDenominator"] as? Double {
|
||||
add(key: "Exposure time", value: "\(Int(expNom))/\(Int(expDen))")
|
||||
}
|
||||
add(key: "ISO speed", value: photo["iso"] as? Int64)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
add(key: "Bitrate", value: (json["video"] as? NSDictionary)?["bitrate"] as? Int)
|
||||
|
||||
return (dic, keys)
|
||||
}
|
||||
}
|
||||
Executable
+389
@@ -0,0 +1,389 @@
|
||||
//
|
||||
// SessionDelegate.swift
|
||||
// FileProvider
|
||||
//
|
||||
// Created by Amir Abbas Mousavian.
|
||||
// Copyright © 2016 Mousavian. Distributed under MIT license.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// A protocol defines properties for errors returned by HTTP/S based providers.
|
||||
/// Including Dropbox, OneDrive and WebDAV.
|
||||
public protocol FileProviderHTTPError: LocalizedError, CustomStringConvertible {
|
||||
/// HTTP status codes as an enum.
|
||||
typealias Code = FileProviderHTTPErrorCode
|
||||
/// HTTP status code returned for error by server.
|
||||
var code: FileProviderHTTPErrorCode { get }
|
||||
/// Path of file/folder casued that error
|
||||
var path: String { get }
|
||||
/// Contents returned by server as error description
|
||||
var serverDescription: String? { get }
|
||||
}
|
||||
|
||||
extension FileProviderHTTPError {
|
||||
public var description: String {
|
||||
return "Status \(code.rawValue): \(code.description)"
|
||||
}
|
||||
|
||||
public var errorDescription: String? {
|
||||
return "Status \(code.rawValue): \(code.description)"
|
||||
}
|
||||
}
|
||||
|
||||
internal var completionHandlersForTasks = [String: [Int: SimpleCompletionHandler]]()
|
||||
internal var downloadCompletionHandlersForTasks = [String: [Int: (URL) -> Void]]()
|
||||
internal var dataCompletionHandlersForTasks = [String: [Int: (Data) -> Void]]()
|
||||
internal var responseCompletionHandlersForTasks = [String: [Int: (URLResponse) -> Void]]()
|
||||
|
||||
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
|
||||
final public class SessionDelegate: NSObject, URLSessionDataDelegate, URLSessionDownloadDelegate, URLSessionStreamDelegate {
|
||||
|
||||
weak var fileProvider: (FileProviderBasicRemote & FileProviderOperations)?
|
||||
var credential: URLCredential?
|
||||
|
||||
public init(fileProvider: FileProviderBasicRemote & FileProviderOperations) {
|
||||
self.fileProvider = fileProvider
|
||||
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)
|
||||
}
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
||||
_ = dataCompletionHandlersForTasks[session.sessionDescription!]?.removeValue(forKey: task.taskIdentifier)
|
||||
if !(error == nil && task is URLSessionDownloadTask) {
|
||||
let completionHandler = completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] ?? nil
|
||||
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) {
|
||||
let dcompletionHandler = downloadCompletionHandlersForTasks[session.sessionDescription!]?[downloadTask.taskIdentifier]
|
||||
dcompletionHandler?(location)
|
||||
_ = downloadCompletionHandlersForTasks[session.sessionDescription!]?.removeValue(forKey: downloadTask.taskIdentifier)
|
||||
_ = completionHandlersForTasks[session.sessionDescription!]?.removeValue(forKey: downloadTask.taskIdentifier)
|
||||
}
|
||||
|
||||
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
|
||||
let handler = responseCompletionHandlersForTasks[session.sessionDescription!]?[dataTask.taskIdentifier] ?? nil
|
||||
handler?(response)
|
||||
completionHandler(.allow)
|
||||
_ = responseCompletionHandlersForTasks[session.sessionDescription!]?.removeValue(forKey: dataTask.taskIdentifier)
|
||||
}
|
||||
|
||||
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
|
||||
if let completionHandler = dataCompletionHandlersForTasks[session.sessionDescription!]?[dataTask.taskIdentifier] {
|
||||
/*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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
|
||||
guard let json = task.taskDescription?.deserializeJSON(),
|
||||
let op = FileOperationType(json: json), let fileProvider = fileProvider else {
|
||||
return
|
||||
}
|
||||
|
||||
switch op {
|
||||
case .create(path: let path):
|
||||
if path.hasSuffix("/") { return }
|
||||
break
|
||||
case .modify:
|
||||
break
|
||||
case .copy(source: let source, destination: _) where source.hasPrefix("file://"):
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if totalBytesExpectedToWrite == NSURLSessionTransferSizeUnknown { return }
|
||||
|
||||
guard let json = downloadTask.taskDescription?.deserializeJSON(),
|
||||
let op = FileOperationType(json: json), let fileProvider = fileProvider else {
|
||||
return
|
||||
}
|
||||
|
||||
fileProvider.delegateNotify(op, progress: Double(totalBytesWritten) / Double(totalBytesExpectedToWrite))
|
||||
}
|
||||
|
||||
public func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
||||
authenticate(didReceive: challenge, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
||||
authenticate(didReceive: challenge, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
func authenticate(didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
||||
switch (challenge.previousFailureCount, credential != nil) {
|
||||
case (0...1, true):
|
||||
completionHandler(.useCredential, credential)
|
||||
case (0, false):
|
||||
completionHandler(.useCredential, challenge.proposedCredential)
|
||||
default:
|
||||
completionHandler(.performDefaultHandling, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP status codes as an enum.
|
||||
public enum FileProviderHTTPErrorCode: Int, CustomStringConvertible {
|
||||
/// `Continue` informational status with HTTP code 100
|
||||
case `continue` = 100
|
||||
/// `Switching Protocols` informational status with HTTP code 101
|
||||
case switchingProtocols = 101
|
||||
/// `Processing` informational status with HTTP code 102
|
||||
case processing = 102
|
||||
/// `OK` success status with HTTP code 200
|
||||
case ok = 200
|
||||
/// `Created` success status with HTTP code 201
|
||||
case created = 201
|
||||
/// `Accepted` success status with HTTP code 202
|
||||
case accepted = 202
|
||||
/// `Non Authoritative Information` success status with HTTP code 203
|
||||
case nonAuthoritativeInformation = 203
|
||||
/// `No Content` success status with HTTP code 204
|
||||
case noContent = 204
|
||||
/// `ResetcContent` success status with HTTP code 205
|
||||
case resetContent = 205
|
||||
/// `Partial Content` success status with HTTP code 206
|
||||
case partialContent = 206
|
||||
/// `Multi Status` success status with HTTP code 207
|
||||
case multiStatus = 207
|
||||
/// `Already Reported` success status with HTTP code 208
|
||||
case alreadyReported = 208
|
||||
/// `IM Used` success status with HTTP code 226
|
||||
case imUsed = 226
|
||||
/// `Multiple Choices` redirection status with HTTP code 300
|
||||
case multipleChoices = 300
|
||||
/// `Moved Permanently` redirection status with HTTP code 301
|
||||
case movedPermanently = 301
|
||||
/// `Found` redirection status with HTTP code 302
|
||||
case found = 302
|
||||
/// `See Other` redirection status with HTTP code 303
|
||||
case seeOther = 303
|
||||
/// `Not Modified` redirection status with HTTP code 304
|
||||
case notModified = 304
|
||||
/// `Use Proxy` redirection status with HTTP code 305
|
||||
case useProxy = 305
|
||||
/// `Switch Proxy` redirection status with HTTP code 306
|
||||
case switchProxy = 306
|
||||
/// `Temporary Redirect` redirection status with HTTP code 307
|
||||
case temporaryRedirect = 307
|
||||
/// `Permanent Redirect` redirection status with HTTP code 308
|
||||
case permanentRedirect = 308
|
||||
/// `Bad Request` client error status with HTTP code 400
|
||||
case badRequest = 400
|
||||
/// `Unauthorized` client error status with HTTP code 401
|
||||
case unauthorized = 401
|
||||
/// `Payment Required` client error status with HTTP code 402
|
||||
case paymentRequired = 402
|
||||
/// `Forbidden` client error status with HTTP code 403
|
||||
case forbidden = 403
|
||||
/// `Not Found` client error status with HTTP code 404
|
||||
case notFound = 404
|
||||
/// `Method Not Allowed` client error status with HTTP code 405
|
||||
case methodNotAllowed = 405
|
||||
/// `Not Acceptable` client error status with HTTP code 406
|
||||
case notAcceptable = 406
|
||||
/// `Proxy Authentication Required` client error status with HTTP code 407
|
||||
case proxyAuthenticationRequired = 407
|
||||
/// `Request Timeout` client error status with HTTP code 408
|
||||
case requestTimeout = 408
|
||||
/// `Conflict` client error status with HTTP code 409
|
||||
case conflict = 409
|
||||
/// `Gone` client error status with HTTP code 410
|
||||
case gone = 410
|
||||
/// `Length Required` client error status with HTTP code 411
|
||||
case lengthRequired = 411
|
||||
/// `Precondition Failed` client error status with HTTP code 412
|
||||
case preconditionFailed = 412
|
||||
/// `Payload Too Large` client error status with HTTP code 413
|
||||
case payloadTooLarge = 413
|
||||
/// `URI Too Long` client error status with HTTP code 414
|
||||
case uriTooLong = 414
|
||||
/// `Unsupported Media Type` status with HTTP code 415
|
||||
case unsupportedMediaType = 415
|
||||
/// `Range Not Satisfiable` client error status with HTTP code 416
|
||||
case rangeNotSatisfiable = 416
|
||||
/// `Expectation Failed` client error status with HTTP code 417
|
||||
case expectationFailed = 417
|
||||
/// `Misdirected Request` client error status with HTTP code 421
|
||||
case misdirectedRequest = 421
|
||||
/// `Unprocessable Entity` client error status with HTTP code 422
|
||||
case unprocessableEntity = 422
|
||||
/// `Locked` client error status with HTTP code 423
|
||||
case locked = 423
|
||||
/// `Failed Dependency` client error status with HTTP code 424
|
||||
case failedDependency = 424
|
||||
/// `Unordered Collection` client error status with HTTP code 425
|
||||
case unorderedCollection = 425
|
||||
/// `Upgrade Required` client error status with HTTP code 426
|
||||
case upgradeRequired = 426
|
||||
/// `Precondition Required` client error status with HTTP code 428
|
||||
case preconditionRequired = 428
|
||||
/// `Too Many Requests` client error status with HTTP code 429
|
||||
case tooManyRequests = 429
|
||||
/// `Request Header Fields Too Large` client error status with HTTP code 431
|
||||
case requestHeaderFieldsTooLarge = 431
|
||||
/// `Unavailable For Legal Reasons` client error status with HTTP code 451
|
||||
case unavailableForLegalReasons = 451
|
||||
/// `Internal Server Error` server error status with HTTP code 500
|
||||
case internalServerError = 500
|
||||
/// `Bad Gateway` server error status with HTTP code 502
|
||||
case badGateway = 502
|
||||
/// `Service Unavailable` server error status with HTTP code 503
|
||||
case serviceUnavailable = 503
|
||||
/// `Gateway Timeout` server error status with HTTP code 504
|
||||
case gatewayTimeout = 504
|
||||
/// `HTTP Version Not Supported` server error status with HTTP code 505
|
||||
case httpVersionNotSupported = 505
|
||||
/// `Variant Also Negotiates` server error status with HTTP code 506
|
||||
case variantAlsoNegotiates = 506
|
||||
/// `Insufficient Storage` server error status with HTTP code 507
|
||||
case insufficientStorage = 507
|
||||
/// `Loop Detected` server error status with HTTP code 508
|
||||
case loopDetected = 508
|
||||
/// `Bandwidth Limit Exceeded` server error status with HTTP code 509
|
||||
case bandwidthLimitExceeded = 509
|
||||
/// `Not Extended` server error status with HTTP code 510
|
||||
case notExtended = 510
|
||||
/// `Network Authentication Required` server error status with HTTP code 511
|
||||
case networkAuthenticationRequired = 511
|
||||
|
||||
fileprivate static let status1xx: [Int: String] = [100: "Continue", 101: "Switching Protocols", 102: "Processing"]
|
||||
fileprivate static let status2xx: [Int: String] = [200: "OK", 201: "Created", 202: "Accepted", 203: "Non-Authoritative Information", 204: "No Content", 205: "Reset Content", 206: "Partial Content", 207: "Multi-Status", 208: "Already Reported", 226: "IM Used"]
|
||||
fileprivate static let status3xx: [Int: String] = [300: "Multiple Choices", 301: "Moved Permanently", 302: "Found", 303: "See Other", 304: "Not Modified", 305: "Use Proxy", 306: "Switch Proxy", 307: "Temporary Redirect", 308: "Permanent Redirect"]
|
||||
fileprivate static let status4xx: [Int: String] = [400: "Bad Request", 401: "Unauthorized/Expired Session", 402: "Payment Required", 403: "Forbidden", 404: "Not Found", 405: "Method Not Allowed", 406: "Not Acceptable", 407: "Proxy Authentication Required", 408: "Request Timeout", 409: "Conflict", 410: "Gone", 411: "Length Required", 412: "Precondition Failed", 413: "Payload Too Large", 414: "URI Too Long", 415: "Unsupported Media Type", 416: "Range Not Satisfiable", 417: "Expectation Failed", 421: "Misdirected Request", 422: "Unprocessable Entity", 423: "Locked", 424: "Failed Dependency", 425: "Unordered Collection", 426: "Upgrade Required", 428: "Precondition Required", 429: "Too Many Requests", 431: "Request Header Fields Too Large", 451: "Unavailable For Legal Reasons"]
|
||||
fileprivate static let status5xx: [Int: String] = [500: "Internal Server Error", 501: "Not Implemented", 502: "Bad Gateway", 503: "Service Unavailable", 504: "Gateway Timeout", 505: "HTTP Version Not Supported", 506: "Variant Also Negotiates", 507: "Insufficient Storage", 508: "Loop Detected", 509: "Bandwidth Limit Exceeded", 510: "Not Extended", 511: "Network Authentication Required"]
|
||||
|
||||
public var description: String {
|
||||
switch self.rawValue {
|
||||
case 100...102: return FileProviderHTTPErrorCode.status1xx[self.rawValue]!
|
||||
case 200...208, 226: return FileProviderHTTPErrorCode.status2xx[self.rawValue]!
|
||||
case 300...308: return FileProviderHTTPErrorCode.status3xx[self.rawValue]!
|
||||
case 400...417, 421...426: fallthrough
|
||||
case 428, 429, 431, 451: return FileProviderHTTPErrorCode.status4xx[self.rawValue]!
|
||||
case 500...511: return FileProviderHTTPErrorCode.status5xx[self.rawValue]!
|
||||
default: return typeDescription
|
||||
}
|
||||
}
|
||||
|
||||
public var localizedDescription: String {
|
||||
return HTTPURLResponse.localizedString(forStatusCode: self.rawValue)
|
||||
}
|
||||
|
||||
/// Description of status based on first digit which indicated fail or success.
|
||||
public var typeDescription: String {
|
||||
switch self.rawValue {
|
||||
case 100...199: return "Informational"
|
||||
case 200...299: return "Success"
|
||||
case 300...399: return "Redirection"
|
||||
case 400...499: return "Client Error"
|
||||
case 500...599: return "Server Error"
|
||||
default: return "Unknown Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
+241
-122
@@ -8,116 +8,210 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
internal func encode<T>(inout value: T) -> NSData {
|
||||
return withUnsafePointer(&value) { p in
|
||||
NSData(bytes: p, length: sizeofValue(value))
|
||||
}
|
||||
}
|
||||
|
||||
internal func encode<T>(value: T) -> NSData {
|
||||
var value = value
|
||||
return withUnsafePointer(&value) { p in
|
||||
NSData(bytes: p, length: sizeofValue(value))
|
||||
}
|
||||
}
|
||||
|
||||
internal func decode<T>(data: NSData) -> T {
|
||||
let pointer = UnsafeMutablePointer<T>.alloc(sizeof(T.Type))
|
||||
data.getBytes(pointer, length: sizeof(T.Type))
|
||||
|
||||
return pointer.move()
|
||||
}
|
||||
|
||||
// This client implementation is for little-endian platform, namely x86, x64 & arm
|
||||
// For big-endian platforms like PowerPC, there must be a huge overhaul
|
||||
|
||||
class SMBProtocolClient: TCPSocketClient {
|
||||
var currentMessageID: UInt64 = 0
|
||||
enum SMBClientError: Error {
|
||||
case streamNotOpened
|
||||
case timedOut
|
||||
}
|
||||
|
||||
@objcMembers
|
||||
class SMBClient: NSObject, StreamDelegate {
|
||||
fileprivate var inputStream: InputStream?
|
||||
fileprivate var outputStream: OutputStream?
|
||||
fileprivate var operation_queue: OperationQueue!
|
||||
|
||||
func negotiateToSMB2() -> SMB2.NegotiateResponse? {
|
||||
let smbHeader = SMB2.Header(command: .NEGOTIATE, creditRequestResponse: 126, messageId: messageId(), treeId: 0, sessionId: 0)
|
||||
currentMessageID += 1
|
||||
let negMessage = SMB2.NegotiateRequest(request: SMB2.NegotiateRequest.Header(capabilities: []))
|
||||
SMBProtocolClient.createSMB2Message(smbHeader, message: negMessage)
|
||||
do {
|
||||
try self.send(data: nil)
|
||||
} catch _ {
|
||||
return nil
|
||||
}
|
||||
self.waitUntilResponse()
|
||||
let response = try? SMBProtocolClient.digestSMB2Message(dataReceived)
|
||||
return response??.message as? SMB2.NegotiateResponse
|
||||
}
|
||||
fileprivate var host: (hostname: String, port: Int)?
|
||||
fileprivate var service: NetService?
|
||||
|
||||
func sessionSetupForSMB2() -> SMB2.SessionSetupResponse? {
|
||||
return nil
|
||||
}
|
||||
public var timeout: TimeInterval = 30
|
||||
|
||||
func messageId() -> UInt64 {
|
||||
internal private(set) var messageId: UInt64 = 0
|
||||
fileprivate func createMessageId() -> UInt64 {
|
||||
defer {
|
||||
currentMessageID += 1
|
||||
messageId += 1
|
||||
}
|
||||
return currentMessageID
|
||||
return messageId
|
||||
}
|
||||
|
||||
// MARK: create and analyse messages
|
||||
internal private(set) var credit: UInt16 = 0
|
||||
fileprivate func consumeCredit() -> UInt16 {
|
||||
if credit > 0 {
|
||||
credit -= 1
|
||||
return credit
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
class func determineSMBVersion(data: NSData) -> Float {
|
||||
var smbverChar: Int8 = 0
|
||||
data.getBytes(&smbverChar, length: 1)
|
||||
private(set) var sessionId: UInt64 = 0
|
||||
|
||||
private(set) var establishedTrees = Array<SMB2.TreeConnectResponse>()
|
||||
private(set) var requestStack = [Int: SMBRequest]()
|
||||
private(set) var responseStack = [Int: SMBResponse]()
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
deinit {
|
||||
close()
|
||||
}
|
||||
|
||||
fileprivate func open(secure: Bool = false) {
|
||||
var readStream : Unmanaged<CFReadStream>?
|
||||
var writeStream : Unmanaged<CFWriteStream>?
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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: .defaultRunLoopMode)
|
||||
outputStream.schedule(in: RunLoop.main, forMode: .defaultRunLoopMode)
|
||||
inputStream.open()
|
||||
outputStream.open()
|
||||
|
||||
operation_queue.isSuspended = false
|
||||
}
|
||||
|
||||
fileprivate func close() {
|
||||
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
|
||||
}
|
||||
|
||||
@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 = data.withUnsafeBytes {
|
||||
outputStream.write($0, maxLength: data.count)
|
||||
}
|
||||
|
||||
if bytesWritten > 0 {
|
||||
let range = 0..<bytesWritten
|
||||
data.replaceSubrange(range, with: Data())
|
||||
byteSent += bytesWritten
|
||||
} else if bytesWritten < 0 {
|
||||
if let error = outputStream.streamError {
|
||||
throw error
|
||||
}
|
||||
return bytesWritten
|
||||
}
|
||||
if data.count == 0 {
|
||||
break
|
||||
}
|
||||
if expireDate < Date() {
|
||||
throw SMBClientError.timedOut
|
||||
}
|
||||
|
||||
}
|
||||
return byteSent
|
||||
}
|
||||
|
||||
var currentHandlingData: Data = Data()
|
||||
var expectedBytes = 0
|
||||
|
||||
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(.hasBytesAvailable) {
|
||||
while (inputStream!.hasBytesAvailable) {
|
||||
var buffer = [UInt8](repeating: 0, count: 65536)
|
||||
let len = inputStream!.read(&buffer, maxLength: buffer.count)
|
||||
if len > 0 {
|
||||
|
||||
/*dataReceived.append(&buffer, count: len)
|
||||
self._countOfBytesRecieved += Int64(len)*/
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: create and analyse messages
|
||||
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)
|
||||
}
|
||||
|
||||
class func digestSMBMessage(data: NSData) throws -> (header: SMB1.Header, blocks: [(params: [UInt16], message: NSData?)]) {
|
||||
guard data.length > 30 else {
|
||||
throw NSURLError.BadServerResponse
|
||||
}
|
||||
var buffer = [UInt8](count: data.length, repeatedValue: 0)
|
||||
guard determineSMBVersion(data) == 1 else {
|
||||
throw SMBFileProviderError.IncompatibleHeader
|
||||
}
|
||||
let headersize = sizeof(SMB1.Header.self)
|
||||
let header: SMB1.Header = decode(data)
|
||||
var blocks = [(params: [UInt16], message: NSData?)]()
|
||||
var offset = headersize
|
||||
while offset < data.length {
|
||||
let paramWords: [UInt16]
|
||||
let paramWordsCount = Int(buffer[offset])
|
||||
guard data.length > (paramWordsCount * 2 + offset) else {
|
||||
throw SMBFileProviderError.IncorrectParamsLength
|
||||
}
|
||||
offset += sizeof(UInt8)
|
||||
var rawParamWords = [UInt8](buffer[offset..<(offset + paramWordsCount * 2)])
|
||||
let paramData = NSData(bytesNoCopy: &rawParamWords, length: rawParamWords.count)
|
||||
paramWords = decode(paramData)
|
||||
offset += paramWordsCount * 2
|
||||
let messageBytesCountLittleEndian = [UInt8](buffer[offset...(offset + 1)])
|
||||
let messageBytesCount = Int(UnsafePointer<UInt16>(messageBytesCountLittleEndian).memory)
|
||||
offset += sizeof(UInt16)
|
||||
guard data.length >= (offset + messageBytesCount) else {
|
||||
throw SMBFileProviderError.IncorrectMessageLength
|
||||
}
|
||||
var rawMessage = [UInt8](buffer[offset..<(offset + messageBytesCount)])
|
||||
offset += messageBytesCount
|
||||
let message = NSData(bytes: &rawMessage, length: rawMessage.count)
|
||||
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
|
||||
}
|
||||
|
||||
class func digestSMB2Message(data: NSData) throws -> (header: SMB2.Header, message: SMBResponse?)? {
|
||||
guard data.length > 65 else {
|
||||
throw NSURLError.BadServerResponse
|
||||
func responseOf(_ data: Data) throws -> SMBResponse? {
|
||||
guard data.count > 65 else {
|
||||
throw URLError(.badServerResponse)
|
||||
}
|
||||
guard determineSMBVersion(data) == 2 else {
|
||||
throw SMBFileProviderError.IncompatibleHeader
|
||||
guard determineSMBVersion(data) >= 2 else {
|
||||
throw SMBFileProviderError.incompatibleHeader
|
||||
}
|
||||
let headersize = sizeof(SMB2.Header.self)
|
||||
let headerData = data.subdataWithRange(NSRange(location: 0, length: headersize))
|
||||
let messageSize = data.length - headersize
|
||||
let messageData = data.subdataWithRange(NSRange(location: headersize, length: messageSize))
|
||||
let header: SMB2.Header = decode(headerData)
|
||||
let headersize = MemoryLayout<SMB2.Header>.size
|
||||
let headerData = data.subdata(in: 0..<headersize)
|
||||
let messageSize = data.count - headersize
|
||||
let messageData = data.subdata(in: headersize..<(headersize + messageSize))
|
||||
let header: SMB2.Header = headerData.scanValue()!
|
||||
switch header.command {
|
||||
case .NEGOTIATE:
|
||||
return (header, SMB2.NegotiateResponse(data: messageData))
|
||||
@@ -136,59 +230,84 @@ class SMBProtocolClient: TCPSocketClient {
|
||||
case .FLUSH:
|
||||
return (header, SMB2.FlushResponse(data: messageData))
|
||||
case .READ:
|
||||
return (header, nil) // FIXME:
|
||||
return (header, SMB2.ReadRespone(data: messageData))
|
||||
case .WRITE:
|
||||
return (header, nil) // FIXME:
|
||||
return (header, SMB2.WriteResponse(data: messageData))
|
||||
case .LOCK:
|
||||
return (header, nil) // FIXME:
|
||||
return (header, SMB2.LockResponse(data: messageData))
|
||||
case .IOCTL:
|
||||
return (header, nil)
|
||||
return (header, SMB2.IOCtlResponse(data: messageData))
|
||||
case .CANCEL:
|
||||
return (header, nil)
|
||||
case .ECHO:
|
||||
return (header, SMB2.Echo(data: messageData))
|
||||
case .QUERY_DIRECTORY:
|
||||
return (header, nil) // FIXME:
|
||||
return (header, SMB2.QueryDirectoryResponse(data: messageData))
|
||||
case .CHANGE_NOTIFY:
|
||||
return (header, nil) // FIXME:
|
||||
return (header, SMB2.ChangeNotifyResponse(data: messageData))
|
||||
case .QUERY_INFO:
|
||||
return (header, nil) // FIXME:
|
||||
return (header, SMB2.QueryInfoResponse(data: messageData))
|
||||
case .SET_INFO:
|
||||
return (header, nil) // FIXME:
|
||||
return (header, SMB2.SetInfoResponse(data: messageData))
|
||||
case .OPLOCK_BREAK:
|
||||
return (header, nil) // FIXME:
|
||||
case .INVALID:
|
||||
throw SMBFileProviderError.InvalidCommand
|
||||
default:
|
||||
throw SMBFileProviderError.invalidCommand
|
||||
}
|
||||
}
|
||||
|
||||
class func createSMBMessage(header: SMB1.Header, blocks: [(params: NSData?, message: NSData?)]) -> NSData {
|
||||
var headerv = header
|
||||
let result = NSMutableData(data: encode(&headerv))
|
||||
/*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?.length ?? 0)
|
||||
result.appendBytes(¶mWordsCount, length: sizeofValue(paramWordsCount))
|
||||
var paramWordsCount = UInt8(block.params?.count ?? 0)
|
||||
result.append(¶mWordsCount, count: MemoryLayout.size(ofValue: paramWordsCount))
|
||||
if let params = block.params {
|
||||
result.appendData(params)
|
||||
result.append(params)
|
||||
}
|
||||
var messageLen = UInt16(block.message?.length ?? 0)
|
||||
result.appendBytes(&messageLen, length: sizeofValue(messageLen))
|
||||
var messageLen = UInt16(block.message?.count ?? 0)
|
||||
let b = UnsafeBufferPointer(start: &messageLen, count: MemoryLayout.size(ofValue: messageLen))
|
||||
result.append(b)
|
||||
if let message = block.message {
|
||||
result.appendData(message)
|
||||
result.append(message)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}*/
|
||||
|
||||
class func createSMB2Message(header: SMB2.Header, message: SMBRequest) -> NSData {
|
||||
var headerv = header
|
||||
let result = NSMutableData(data: encode(&headerv))
|
||||
result.appendData(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<SMB1.Header>.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<UInt8>.size
|
||||
var rawParamWords = [UInt8](buffer[offset..<(offset + paramWordsCount * 2)])
|
||||
let paramData = Data(bytesNoCopy: UnsafeMutablePointer<UInt8>(&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<UInt16>.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)
|
||||
}*/
|
||||
}
|
||||
|
||||
protocol FileProviderSMBHeader {
|
||||
var protocolID: UInt32 { get }
|
||||
static var protocolConst: UInt32 { get }
|
||||
}
|
||||
+113
-67
@@ -8,104 +8,150 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public class SMBFileProvider: FileProvider, FileProviderMonitor {
|
||||
public var type: String = "Samba"
|
||||
public var isPathRelative: Bool = true
|
||||
public var baseURL: NSURL?
|
||||
public var currentPath: String = ""
|
||||
public var dispatch_queue: dispatch_queue_t
|
||||
public weak var delegate: FileProviderDelegate?
|
||||
public let credential: NSURLCredential?
|
||||
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?
|
||||
open var credential: URLCredential?
|
||||
|
||||
public typealias FileObjectClass = FileObject
|
||||
|
||||
public init? (baseURL: NSURL, credential: NSURLCredential, afterInitialized: SimpleCompletionHandler) {
|
||||
guard baseURL.uw_scheme.lowercaseString == "smb" else {
|
||||
public init? (baseURL: URL, credential: URLCredential?) {
|
||||
guard baseURL.uw_scheme.lowercased() == "smb" else {
|
||||
return nil
|
||||
}
|
||||
self.baseURL = baseURL
|
||||
dispatch_queue = dispatch_queue_create("FileProvider.\(type)", DISPATCH_QUEUE_CONCURRENT)
|
||||
//let url = baseURL.uw_absoluteString
|
||||
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"
|
||||
|
||||
self.credential = credential
|
||||
}
|
||||
|
||||
public func contentsOfDirectoryAtPath(path: String, completionHandler: ((contents: [FileObjectClass], error: ErrorType?) -> Void)) {
|
||||
NotImplemented()
|
||||
dispatch_async(dispatch_queue) {
|
||||
|
||||
|
||||
public required convenience init?(coder aDecoder: NSCoder) {
|
||||
guard let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL else {
|
||||
return nil
|
||||
}
|
||||
self.init(baseURL: baseURL,
|
||||
credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential)
|
||||
self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? ""
|
||||
}
|
||||
|
||||
public func attributesOfItemAtPath(path: String, completionHandler: ((attributes: FileObjectClass?, error: ErrorType?) -> Void)) {
|
||||
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 {
|
||||
return true
|
||||
}
|
||||
|
||||
open func contentsOfDirectory(path: String, completionHandler: @escaping (_ contents: [FileObjectClass], _ error: Error?) -> Void) {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
public weak var fileOperationDelegate: FileOperationDelegate?
|
||||
|
||||
public func createFolder(folderName: String, atPath: String, completionHandler: SimpleCompletionHandler) {
|
||||
open func attributesOfItem(path: String, completionHandler: @escaping (_ attributes: FileObjectClass?, _ error: Error?) -> Void) {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
public func createFile(fileAttribs: FileObject, atPath: String, contents data: NSData?, completionHandler: SimpleCompletionHandler) {
|
||||
open func storageProperties(completionHandler: @escaping (_ volume: VolumeObject?) -> Void) {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
public func moveItemAtPath(path: String, toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) {
|
||||
func isReachable(completionHandler: @escaping (_ success: Bool, _ error: Error?) -> Void) {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
public func copyItemAtPath(path: String, toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) {
|
||||
open weak var fileOperationDelegate: FileOperationDelegate?
|
||||
|
||||
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
NotImplemented()
|
||||
return nil
|
||||
}
|
||||
|
||||
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
NotImplemented()
|
||||
return nil
|
||||
}
|
||||
|
||||
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
NotImplemented()
|
||||
return nil
|
||||
}
|
||||
|
||||
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
NotImplemented()
|
||||
return nil
|
||||
}
|
||||
|
||||
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
NotImplemented()
|
||||
return nil
|
||||
}
|
||||
|
||||
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
NotImplemented()
|
||||
return nil
|
||||
}
|
||||
|
||||
open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
|
||||
NotImplemented()
|
||||
return nil
|
||||
}
|
||||
|
||||
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
|
||||
NotImplemented()
|
||||
return nil
|
||||
}
|
||||
|
||||
open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
NotImplemented()
|
||||
return nil
|
||||
}
|
||||
|
||||
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler:((FileObjectClass) -> Void)?, completionHandler: @escaping ((_ files: [FileObjectClass], _ error: Error?) -> Void)) -> Progress? {
|
||||
NotImplemented()
|
||||
return nil
|
||||
}
|
||||
|
||||
open func registerNotifcation(path: String, eventHandler: @escaping (() -> Void)) {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
public func removeItemAtPath(path: String, completionHandler: SimpleCompletionHandler) {
|
||||
open func unregisterNotifcation(path: String) {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
public func copyLocalFileToPath(localFile: NSURL, toPath: String, completionHandler: SimpleCompletionHandler) {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
public func copyPathToLocalFile(path: String, toLocalURL: NSURL, completionHandler: SimpleCompletionHandler) {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
public func contentsAtPath(path: String, completionHandler: ((contents: NSData?, error: ErrorType?) -> Void)) {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
public func contentsAtPath(path: String, offset: Int64, length: Int, completionHandler: ((contents: NSData?, error: ErrorType?) -> Void)) {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
public func writeContentsAtPath(path: String, contents data: NSData, atomically: Bool, completionHandler: SimpleCompletionHandler) {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
public func searchFilesAtPath(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObjectClass) -> Void)?, completionHandler: ((files: [FileObjectClass], error: ErrorType?) -> Void)) {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
public func registerNotifcation(path: String, eventHandler: (() -> Void)) {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
public func unregisterNotifcation(path: String) {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
public func isRegisteredForNotification(path: String) -> Bool {
|
||||
open func isRegisteredForNotification(path: String) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: basic CIFS interactivity
|
||||
public enum SMBFileProviderError: Int, ErrorType, CustomStringConvertible {
|
||||
case BadHeader
|
||||
case IncompatibleHeader
|
||||
case IncorrectParamsLength
|
||||
case IncorrectMessageLength
|
||||
case InvalidCommand
|
||||
enum SMBFileProviderError: Int, Error, CustomStringConvertible {
|
||||
case badHeader
|
||||
case incompatibleHeader
|
||||
case incorrectParamsLength
|
||||
case incorrectMessageLength
|
||||
case invalidCommand
|
||||
|
||||
public var description: String {
|
||||
return "SMB message structure is invalid"
|
||||
@@ -113,8 +159,8 @@ public enum SMBFileProviderError: Int, ErrorType, CustomStringConvertible {
|
||||
}
|
||||
|
||||
private extension SMBFileProvider {
|
||||
private func getPID() -> UInt32 {
|
||||
return UInt32(NSProcessInfo.processInfo().processIdentifier)
|
||||
func getPID() -> UInt32 {
|
||||
return UInt32(ProcessInfo.processInfo.processIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,13 +8,14 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
// codebeat:disable[TOO_MANY_IVARS]
|
||||
// SMB/CIFS Types
|
||||
struct SMB1 {
|
||||
struct Header { // 32 bytes
|
||||
// header is always \u{ff}SMB
|
||||
let protocolID: UInt32
|
||||
static let protocolConst: UInt32 = 0x424d53ff
|
||||
private var _command: UInt8
|
||||
fileprivate var _command: UInt8
|
||||
var command: Command {
|
||||
get {
|
||||
return Command(rawValue: _command) ?? .INVALID
|
||||
@@ -24,7 +25,7 @@ struct SMB1 {
|
||||
}
|
||||
}
|
||||
// error messages from the server to the client
|
||||
private var _status: (UInt8, UInt8, UInt8, UInt8)
|
||||
fileprivate var _status: (UInt8, UInt8, UInt8, UInt8)
|
||||
var error: (Class: UInt8, code: UInt16) {
|
||||
get {
|
||||
return (_status.0, UInt16(_status.2) + (UInt16(_status.3) << 8))
|
||||
@@ -47,7 +48,7 @@ struct SMB1 {
|
||||
var flags2: Flags2
|
||||
var pidHigh: UInt16
|
||||
// encryption key used for validating messages over connectionless transports
|
||||
private var _securityKey: (UInt16, UInt16)
|
||||
fileprivate var _securityKey: (UInt16, UInt16)
|
||||
var securityKey: UInt32 {
|
||||
get {
|
||||
return UInt32(_securityKey.1) << 16 + UInt32(_securityKey.0)
|
||||
@@ -60,7 +61,7 @@ struct SMB1 {
|
||||
var securityCID: UInt16
|
||||
/// Identifier of the sequence of a message over connectionless transports
|
||||
var securitySequenceNumber: UInt16
|
||||
private var ununsed: UInt16
|
||||
fileprivate var ununsed: UInt16
|
||||
var treeId: UInt16
|
||||
var pidLow: UInt16
|
||||
var userId: UInt16
|
||||
@@ -75,7 +76,8 @@ struct SMB1 {
|
||||
}
|
||||
}
|
||||
|
||||
init(command: Command, ntStatus: UInt32 = 0, flags: Flags, flags2: Flags2 = [.LONG_NAMES, .ERR_STATUS, .UNICODE], securityKey: UInt32 = 0, securityCID: UInt16 = 0, securitySequenceNumber: UInt16 = 0, treeId: UInt16, pid: UInt32, userId: UInt16, multiplexId: UInt16) {
|
||||
// codebeat:disable[ARITY]
|
||||
init(command: Command, treeId: UInt16, pid: UInt32, userId: UInt16, multiplexId: UInt16, flags: Flags, flags2: Flags2 = [.LONG_NAMES, .ERR_STATUS, .UNICODE], ntStatus: UInt32 = 0, securityKey: UInt32 = 0, securityCID: UInt16 = 0, securitySequenceNumber: UInt16 = 0) {
|
||||
self.protocolID = Header.protocolConst
|
||||
self._command = command.rawValue
|
||||
_status = (UInt8(ntStatus & 0xff), UInt8(ntStatus >> 8 & 0xff), UInt8(ntStatus >> 16 & 0xff), UInt8(ntStatus >> 24 & 0xff))
|
||||
@@ -91,9 +93,10 @@ struct SMB1 {
|
||||
self.userId = userId
|
||||
self.multiplexId = multiplexId
|
||||
}
|
||||
// codebeat:enable[ARITY]
|
||||
}
|
||||
|
||||
struct Flags: OptionSetType {
|
||||
struct Flags: OptionSet {
|
||||
let rawValue: UInt8
|
||||
|
||||
init(rawValue: UInt8) {
|
||||
@@ -111,7 +114,7 @@ struct SMB1 {
|
||||
static let REPLY = Flags(rawValue: 0x80)
|
||||
}
|
||||
|
||||
struct Flags2: OptionSetType {
|
||||
struct Flags2: OptionSet {
|
||||
let rawValue: UInt16
|
||||
|
||||
init(rawValue: UInt16) {
|
||||
@@ -214,4 +217,5 @@ struct SMB1 {
|
||||
case WRITE_BULK_DATA = 0xDA
|
||||
case INVALID = 0xFE
|
||||
}
|
||||
}
|
||||
}
|
||||
// codebeat:enable[TOO_MANY_IVARS]
|
||||
|
||||
@@ -8,34 +8,78 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol SMBRequest {
|
||||
func data() -> NSData
|
||||
protocol Option: RawRepresentable, Hashable {
|
||||
|
||||
}
|
||||
|
||||
protocol SMBResponse {
|
||||
init? (data: NSData)
|
||||
extension Option where RawValue: Hashable {
|
||||
var hashValue: Int {
|
||||
return rawValue.hashValue
|
||||
}
|
||||
}
|
||||
|
||||
protocol IOCtlRequestProtocol: SMBRequest {}
|
||||
protocol IOCtlResponseProtocol: SMBResponse {}
|
||||
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 {
|
||||
#if swift(>=3.1)
|
||||
return Swift.type(of: self).command
|
||||
#else
|
||||
return type(of: self).command
|
||||
#endif
|
||||
}
|
||||
|
||||
func data() -> Data {
|
||||
return Data(value: self)
|
||||
}
|
||||
}
|
||||
|
||||
protocol SMBResponseBody {
|
||||
init? (data: Data)
|
||||
}
|
||||
|
||||
extension SMBResponseBody {
|
||||
init? (data: Data) {
|
||||
if let v: Self = data.scanValue() {
|
||||
self = v
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typealias SMBRequest = (header: SMB2.Header, body: SMBRequestBody?)
|
||||
typealias SMBResponse = (header: SMB2.Header, body: SMBResponseBody?)
|
||||
|
||||
protocol IOCtlRequestProtocol: SMBRequestBody {}
|
||||
protocol IOCtlResponseProtocol: SMBResponseBody {}
|
||||
|
||||
|
||||
struct SMBTime {
|
||||
var time: UInt64
|
||||
var time: Int64
|
||||
|
||||
init(time: UInt64) {
|
||||
init(time: Int64) {
|
||||
self.time = time
|
||||
}
|
||||
|
||||
init(unixTime: UInt) {
|
||||
self.time = (UInt64(unixTime) + 11644473600) * 10000000
|
||||
self.time = (Int64(unixTime) + 11644473600) * 10000000
|
||||
}
|
||||
|
||||
init(timeIntervalSince1970: NSTimeInterval) {
|
||||
self.time = UInt64((timeIntervalSince1970 + 11644473600) * 10000000)
|
||||
init(timeIntervalSince1970: TimeInterval) {
|
||||
self.time = Int64((timeIntervalSince1970 + 11644473600) * 10000000)
|
||||
}
|
||||
|
||||
init(date: NSDate) {
|
||||
init(date: Date) {
|
||||
self.init(timeIntervalSince1970: date.timeIntervalSince1970)
|
||||
}
|
||||
|
||||
@@ -43,7 +87,7 @@ struct SMBTime {
|
||||
return UInt(self.time / 10000000 - 11644473600)
|
||||
}
|
||||
|
||||
var date: NSDate {
|
||||
return NSDate(timeIntervalSince1970: Double(self.time) / 10000000 - 11644473600)
|
||||
var date: Date {
|
||||
return Date(timeIntervalSince1970: Double(self.time) / 10000000 - 11644473600)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ import Foundation
|
||||
extension SMB2 {
|
||||
// MARK: SMB2 Create
|
||||
|
||||
struct CreateRequest: SMBRequest {
|
||||
struct CreateRequest: SMBRequestBody {
|
||||
static var command: SMB2.Command = .CREATE
|
||||
|
||||
let header: CreateRequest.Header
|
||||
let name: String?
|
||||
let contexts: [CreateContext]
|
||||
@@ -22,15 +24,15 @@ extension SMB2 {
|
||||
self.contexts = contexts
|
||||
}
|
||||
|
||||
func data() -> NSData {
|
||||
func data() -> Data {
|
||||
var header = self.header
|
||||
var offset = 0x78 //UInt16(sizeof(SMB2.Header.self) + sizeof(CreateContext.Header.self) - 1)
|
||||
let body = NSMutableData()
|
||||
if let name = self.name, let nameData = name.dataUsingEncoding(NSUTF8StringEncoding) {
|
||||
var body = Data()
|
||||
if let name = self.name, let nameData = name.data(using: .utf16) {
|
||||
header.nameOffset = UInt16(offset)
|
||||
header.nameLength = UInt16(nameData.length)
|
||||
offset += nameData.length
|
||||
body.appendData(nameData)
|
||||
header.nameLength = UInt16(nameData.count)
|
||||
offset += nameData.count
|
||||
body.append(nameData)
|
||||
}
|
||||
if contexts.count > 0 {
|
||||
// TODO: Context CreateRequest implementation, 8 bit allign offset
|
||||
@@ -40,63 +42,39 @@ extension SMB2 {
|
||||
header.contextLength = 0
|
||||
//result.appendData(nameData)
|
||||
}
|
||||
let result = NSMutableData(data: encode(&header))
|
||||
result.appendData(body)
|
||||
var result = Data(value: header)
|
||||
result.append(body)
|
||||
return result
|
||||
}
|
||||
|
||||
struct Header {
|
||||
let size: UInt16
|
||||
private let securityFlags: UInt8
|
||||
private var _requestedOplockLevel: UInt8
|
||||
var requestedOplockLevel: OplockLevel {
|
||||
get {
|
||||
return OplockLevel(rawValue: _requestedOplockLevel)!
|
||||
}
|
||||
set {
|
||||
_requestedOplockLevel = newValue.rawValue
|
||||
}
|
||||
}
|
||||
private var _impersonationLevel: UInt32
|
||||
var impersonationLevel: ImpersonationLevel {
|
||||
get {
|
||||
return ImpersonationLevel(rawValue: _impersonationLevel)!
|
||||
}
|
||||
set {
|
||||
_impersonationLevel = newValue.rawValue
|
||||
}
|
||||
}
|
||||
private let flags: UInt64
|
||||
private let reserved: UInt64
|
||||
fileprivate let securityFlags: UInt8
|
||||
var requestedOplockLevel: OplockLevel
|
||||
var impersonationLevel: ImpersonationLevel
|
||||
fileprivate let flags: UInt64
|
||||
fileprivate let reserved: UInt64
|
||||
let access: FileAccessMask
|
||||
let fileAttributes: FileAttributes
|
||||
let shareAccess: ShareAccess
|
||||
private 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
|
||||
var contextOffset: UInt32
|
||||
var contextLength: UInt32
|
||||
|
||||
init(requestedOplockLevel: OplockLevel = .NONE, impersonationLevel: ImpersonationLevel = .Anonymous, access: FileAccessMask = [.GENERIC_ALL], fileAttributes: FileAttributes = [], shareAccess: ShareAccess = [.READ], desposition: CreateDisposition = .OPEN_IF, options: CreateOptions = []) {
|
||||
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
|
||||
@@ -105,7 +83,7 @@ extension SMB2 {
|
||||
}
|
||||
}
|
||||
|
||||
struct CreateOptions: OptionSetType {
|
||||
struct CreateOptions: OptionSet {
|
||||
let rawValue: UInt32
|
||||
|
||||
init(rawValue: UInt32) {
|
||||
@@ -125,47 +103,55 @@ extension SMB2 {
|
||||
static let NO_COMPRESSION = CreateOptions(rawValue: 0x00008000)
|
||||
static let OPEN_REPARSE_POINT = CreateOptions(rawValue: 0x00200000)
|
||||
static let OPEN_NO_RECALL = CreateOptions(rawValue: 0x00400000)
|
||||
private static let SYNCHRONOUS_IO_ALERT = CreateOptions(rawValue: 0x00000010)
|
||||
private static let SYNCHRONOUS_IO_NONALERT = CreateOptions(rawValue: 0x00000020)
|
||||
private static let COMPLETE_IF_OPLOCKED = CreateOptions(rawValue: 0x00000100)
|
||||
private static let REMOTE_INSTANCE = CreateOptions(rawValue: 0x00000400)
|
||||
private static let OPEN_FOR_FREE_SPACE_QUERY = CreateOptions(rawValue: 0x00800000)
|
||||
private static let OPEN_REQUIRING_OPLOCK = CreateOptions(rawValue: 0x00010000)
|
||||
private static let DISALLOW_EXCLUSIVE = CreateOptions(rawValue: 0x00020000)
|
||||
private static let RESERVE_OPFILTER = CreateOptions(rawValue: 0x00100000)
|
||||
fileprivate static let SYNCHRONOUS_IO_ALERT = CreateOptions(rawValue: 0x00000010)
|
||||
fileprivate static let SYNCHRONOUS_IO_NONALERT = CreateOptions(rawValue: 0x00000020)
|
||||
fileprivate static let COMPLETE_IF_OPLOCKED = CreateOptions(rawValue: 0x00000100)
|
||||
fileprivate static let REMOTE_INSTANCE = CreateOptions(rawValue: 0x00000400)
|
||||
fileprivate static let OPEN_FOR_FREE_SPACE_QUERY = CreateOptions(rawValue: 0x00800000)
|
||||
fileprivate static let OPEN_REQUIRING_OPLOCK = CreateOptions(rawValue: 0x00010000)
|
||||
fileprivate static let DISALLOW_EXCLUSIVE = CreateOptions(rawValue: 0x00020000)
|
||||
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: SMBResponse {
|
||||
struct CreateResponse: SMBResponseBody {
|
||||
struct Header {
|
||||
let size: UInt16
|
||||
private let _oplockLevel: UInt8
|
||||
var oplockLevel: OplockLevel {
|
||||
return OplockLevel(rawValue: _oplockLevel)!
|
||||
}
|
||||
private let reserved: UInt32
|
||||
let oplockLevel: OplockLevel
|
||||
fileprivate let reserved: UInt32
|
||||
let creationTime: SMBTime
|
||||
let lastAccessTime: SMBTime
|
||||
let lastWriteTime: SMBTime
|
||||
@@ -173,7 +159,7 @@ extension SMB2 {
|
||||
let allocationSize: UInt64
|
||||
let endOfFile: UInt64
|
||||
let fileAttributes: FileAttributes
|
||||
private let reserved2: UInt32
|
||||
fileprivate let reserved2: UInt32
|
||||
let fileId: FileId
|
||||
let contextsOffset: UInt32
|
||||
let ContextsLength: UInt32
|
||||
@@ -182,27 +168,22 @@ extension SMB2 {
|
||||
let header: CreateResponse.Header
|
||||
let contexts: [CreateContext]
|
||||
|
||||
init? (data: NSData) {
|
||||
guard data.length >= sizeof(CreateResponse.Header.self) else {
|
||||
init? (data: Data) {
|
||||
guard data.count >= MemoryLayout<CreateResponse.Header>.size else {
|
||||
return nil
|
||||
}
|
||||
self.header = decode(data)
|
||||
self.header = data.scanValue()!
|
||||
if self.header.contextsOffset > 0 {
|
||||
var contexts = [CreateContext]()
|
||||
var contextOffset = Int(self.header.contextsOffset) - sizeof(SMB2.Header.self)
|
||||
var contextOffset = Int(self.header.contextsOffset) - MemoryLayout<SMB2.Header>.size
|
||||
while contextOffset > 0 {
|
||||
guard contextOffset < data.length else {
|
||||
guard contextOffset < data.count else {
|
||||
self.contexts = contexts
|
||||
return
|
||||
}
|
||||
let contextDataHeader = data.subdataWithRange(NSRange(location: contextOffset, length: sizeof(CreateContext.Header.self)))
|
||||
if let lastContextHeader = CreateContext(data: contextDataHeader) {
|
||||
let lastContextLen = Int(lastContextHeader.header.dataOffset) + Int(lastContextHeader.header.dataLength) - contextOffset
|
||||
let lastContextData = data.subdataWithRange(NSRange(location: contextOffset, length: lastContextLen))
|
||||
if let newContext = CreateContext(data: lastContextData) {
|
||||
contexts.append(newContext)
|
||||
}
|
||||
contextOffset = Int(lastContextHeader.header.next) - sizeof(SMB2.Header.self)
|
||||
while contextOffset > 0, let context: CreateContext = data.scanValue(start: contextOffset) {
|
||||
contexts.append(context)
|
||||
contextOffset = Int(context.header.next) - MemoryLayout<SMB2.Header>.size
|
||||
}
|
||||
}
|
||||
self.contexts = contexts
|
||||
@@ -217,74 +198,86 @@ extension SMB2 {
|
||||
var next: UInt32
|
||||
let nameOffset: UInt16
|
||||
let nameLength: UInt16
|
||||
private let reserved: UInt16
|
||||
fileprivate let reserved: UInt16
|
||||
let dataOffset: UInt16
|
||||
let dataLength: UInt32
|
||||
}
|
||||
|
||||
var header: CreateContext.Header
|
||||
let buffer: NSData
|
||||
let buffer: Data
|
||||
|
||||
init(name: ContextNames, data: NSData) {
|
||||
let nameData = NSMutableData(data: (name.rawValue).dataUsingEncoding(NSUTF8StringEncoding)!)
|
||||
self.header = CreateContext.Header(next: 0, nameOffset: 32, nameLength: UInt16(nameData.length), reserved: 0, dataOffset: UInt16(nameData.length), dataLength: UInt32(data.length))
|
||||
init(name: ContextNames, data: Data) {
|
||||
let nameData = (name.rawValue).data(using: .utf16) ?? Data()
|
||||
self.header = CreateContext.Header(next: 0, nameOffset: 32, nameLength: UInt16(nameData.count), reserved: 0, dataOffset: UInt16(nameData.count), dataLength: UInt32(data.count))
|
||||
self.buffer = data
|
||||
}
|
||||
|
||||
init(name: NSUUID, data: NSData) {
|
||||
var uuid = uuid_t(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)
|
||||
name.getUUIDBytes(&uuid.0)
|
||||
let nameData = NSMutableData(bytes: &uuid, length: 16)
|
||||
self.header = CreateContext.Header(next: 0, nameOffset: 32, nameLength: UInt16(nameData.length), reserved: 0, dataOffset: UInt16(nameData.length), dataLength: UInt32(data.length))
|
||||
init(name: UUID, data: Data) {
|
||||
let uuid = name.uuid
|
||||
var nameData = Data(value: uuid)
|
||||
self.header = CreateContext.Header(next: 0, nameOffset: 32, nameLength: UInt16(nameData.count), reserved: 0, dataOffset: UInt16(nameData.count), dataLength: UInt32(data.count))
|
||||
self.buffer = data
|
||||
}
|
||||
|
||||
init? (data: NSData) {
|
||||
let headersize = sizeof(Header)
|
||||
guard data.length > headersize else {
|
||||
init? (data: Data) {
|
||||
let headersize = MemoryLayout<Header>.size
|
||||
guard data.count > headersize else {
|
||||
return nil
|
||||
}
|
||||
self.header = decode(data)
|
||||
self.buffer = data.subdataWithRange(NSRange(location: headersize, length: data.length - headersize))
|
||||
self.header = data.scanValue()!
|
||||
self.buffer = data.subdata(in: headersize..<data.count)
|
||||
}
|
||||
|
||||
func data() -> NSData {
|
||||
let result = NSMutableData(data: encode(header))
|
||||
result.appendData(buffer)
|
||||
func data() -> Data {
|
||||
var result = Data(value: header)
|
||||
result.append(buffer)
|
||||
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: OptionSetType {
|
||||
struct ShareAccess: OptionSet {
|
||||
let rawValue: UInt32
|
||||
|
||||
init(rawValue: UInt32) {
|
||||
@@ -296,7 +289,7 @@ extension SMB2 {
|
||||
static let DELETE = ShareAccess(rawValue: 0x00000004)
|
||||
}
|
||||
|
||||
struct FileAccessMask: OptionSetType {
|
||||
struct FileAccessMask: OptionSet {
|
||||
let rawValue: UInt32
|
||||
|
||||
init(rawValue: UInt32) {
|
||||
@@ -332,7 +325,7 @@ extension SMB2 {
|
||||
static let GENERIC_READ = FileAccessMask(rawValue: 0x80000000)
|
||||
}
|
||||
|
||||
struct FileAttributes: OptionSetType {
|
||||
struct FileAttributes: OptionSet {
|
||||
let rawValue: UInt32
|
||||
|
||||
init(rawValue: UInt32) {
|
||||
@@ -363,10 +356,12 @@ extension SMB2 {
|
||||
|
||||
// MARK: SMB2 Close
|
||||
|
||||
struct CloseRequest: SMBRequest {
|
||||
struct CloseRequest: SMBRequestBody {
|
||||
static var command: SMB2.Command = .CLOSE
|
||||
|
||||
let size: UInt16
|
||||
let flags: CloseFlags
|
||||
private let reserved2: UInt32
|
||||
fileprivate let reserved2: UInt32
|
||||
let filePersistantId: UInt64
|
||||
let fileVolatileId: UInt64
|
||||
|
||||
@@ -377,16 +372,12 @@ extension SMB2 {
|
||||
self.flags = []
|
||||
self.reserved2 = 0
|
||||
}
|
||||
|
||||
func data() -> NSData {
|
||||
return encode(self)
|
||||
}
|
||||
}
|
||||
|
||||
struct CloseResponse: SMBResponse {
|
||||
struct CloseResponse: SMBResponseBody {
|
||||
let size: UInt16
|
||||
let flags: CloseFlags
|
||||
private let reserved: UInt32
|
||||
fileprivate let reserved: UInt32
|
||||
let creationTime: SMBTime
|
||||
let lastAccessTime: SMBTime
|
||||
let lastWriteTime: SMBTime
|
||||
@@ -394,13 +385,9 @@ extension SMB2 {
|
||||
let allocationSize: UInt64
|
||||
let endOfFile: UInt64
|
||||
let fileAttributes: FileAttributes
|
||||
|
||||
init? (data: NSData) {
|
||||
self = decode(data)
|
||||
}
|
||||
}
|
||||
|
||||
struct CloseFlags: OptionSetType {
|
||||
struct CloseFlags: OptionSet {
|
||||
let rawValue: UInt16
|
||||
|
||||
init(rawValue: UInt16) {
|
||||
@@ -412,10 +399,12 @@ extension SMB2 {
|
||||
|
||||
// MARK: SMB2 Flush
|
||||
|
||||
struct FlushRequest: SMBRequest {
|
||||
struct FlushRequest: SMBRequestBody {
|
||||
static var command: SMB2.Command = .FLUSH
|
||||
|
||||
let size: UInt16
|
||||
private let reserved: UInt16
|
||||
private let reserved2: UInt32
|
||||
fileprivate let reserved: UInt16
|
||||
fileprivate let reserved2: UInt32
|
||||
let filePersistantId: UInt64
|
||||
let fileVolatileId: UInt64
|
||||
|
||||
@@ -426,13 +415,9 @@ extension SMB2 {
|
||||
self.reserved = 0
|
||||
self.reserved2 = 0
|
||||
}
|
||||
|
||||
func data() -> NSData {
|
||||
return encode(self)
|
||||
}
|
||||
}
|
||||
|
||||
struct FlushResponse: SMBResponse {
|
||||
struct FlushResponse: SMBResponseBody {
|
||||
let size: UInt16
|
||||
let reserved: UInt16
|
||||
|
||||
@@ -440,9 +425,5 @@ extension SMB2 {
|
||||
self.size = 4
|
||||
self.reserved = 0
|
||||
}
|
||||
|
||||
init? (data: NSData) {
|
||||
self = decode(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,22 +11,21 @@ import Foundation
|
||||
extension SMB2 {
|
||||
// MARK: SMB2 Read
|
||||
|
||||
struct ReadRequest: SMBRequest {
|
||||
struct ReadRequest: SMBRequestBody {
|
||||
static var command: SMB2.Command = .READ
|
||||
|
||||
let size: UInt16
|
||||
private let padding: UInt8
|
||||
fileprivate let padding: UInt8
|
||||
let flags: ReadRequest.Flags
|
||||
let length: UInt32
|
||||
let offset: UInt64
|
||||
let fileId: FileId
|
||||
let minimumLength: UInt32
|
||||
private let _channel: UInt32
|
||||
var channel: Channel {
|
||||
return Channel(rawValue: _channel) ?? .NONE
|
||||
}
|
||||
let channel: Channel
|
||||
let remainingBytes: UInt32
|
||||
private let channelInfoOffset: UInt16
|
||||
private let channelInfoLength: UInt16
|
||||
private let channelBuffer: UInt8
|
||||
fileprivate let channelInfoOffset: UInt16
|
||||
fileprivate let channelInfoLength: UInt16
|
||||
fileprivate let channelBuffer: UInt8
|
||||
|
||||
init (fileId: FileId, offset: UInt64, length: UInt32, flags: ReadRequest.Flags = [], minimumLength: UInt32 = 0, remainingBytes: UInt32 = 0, channel: Channel = .NONE) {
|
||||
self.size = 49
|
||||
@@ -36,18 +35,14 @@ 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
|
||||
self.channelBuffer = 0
|
||||
}
|
||||
|
||||
func data() -> NSData {
|
||||
return encode(read)
|
||||
}
|
||||
|
||||
struct Flags: OptionSetType {
|
||||
struct Flags: OptionSet {
|
||||
let rawValue: UInt8
|
||||
|
||||
init(rawValue: UInt8) {
|
||||
@@ -58,41 +53,49 @@ extension SMB2 {
|
||||
}
|
||||
}
|
||||
|
||||
struct ReadRespone: SMBResponse {
|
||||
struct ReadRespone: SMBResponseBody {
|
||||
struct Header {
|
||||
let size: UInt16
|
||||
let offset: UInt8
|
||||
private let reserved: UInt8
|
||||
fileprivate let reserved: UInt8
|
||||
let length: UInt32
|
||||
let remaining: UInt32
|
||||
private let reserved2: UInt32
|
||||
fileprivate let reserved2: UInt32
|
||||
|
||||
}
|
||||
let header: ReadRespone.Header
|
||||
let buffer: NSData
|
||||
let buffer: Data
|
||||
|
||||
init?(data: NSData) {
|
||||
guard data.length > 16 else {
|
||||
init?(data: Data) {
|
||||
guard data.count > 16 else {
|
||||
return nil
|
||||
}
|
||||
self.header = decode(data)
|
||||
let headersize = sizeof(Header)
|
||||
self.buffer = data.subdataWithRange(NSRange(location: headersize, length: data.length - headersize))
|
||||
self.header = data.scanValue()!
|
||||
let headersize = MemoryLayout<Header>.size
|
||||
self.buffer = data.subdata(in: headersize..<data.count)
|
||||
}
|
||||
}
|
||||
|
||||
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: SMBRequest {
|
||||
struct WriteRequest: SMBRequestBody {
|
||||
static var command: SMB2.Command = .WRITE
|
||||
|
||||
let header: WriteRequest.Header
|
||||
let channelInfo: ChannelInfo?
|
||||
let fileData: NSData
|
||||
let fileData: Data
|
||||
|
||||
struct Header {
|
||||
let size: UInt16
|
||||
@@ -100,39 +103,38 @@ extension SMB2 {
|
||||
let length: UInt32
|
||||
let offset: UInt64
|
||||
let fileId: FileId
|
||||
private let _channel: UInt32
|
||||
var channel: Channel {
|
||||
return Channel(rawValue: _channel) ?? .NONE
|
||||
}
|
||||
let channel: Channel
|
||||
let remainingBytes: UInt32
|
||||
let channelInfoOffset: UInt16
|
||||
let channelInfoLength: UInt16
|
||||
let flags: WriteRequest.Flags
|
||||
}
|
||||
|
||||
init(fileId: FileId, offset: UInt64, remainingBytes: UInt32 = 0, data: NSData, channel: Channel = .NONE, channelInfo: ChannelInfo? = nil, flags: WriteRequest.Flags = []) {
|
||||
// codebeat:disable[ARITY]
|
||||
init(fileId: FileId, offset: UInt64, remainingBytes: UInt32 = 0, data: Data, channel: Channel = .NONE, channelInfo: ChannelInfo? = nil, flags: WriteRequest.Flags = []) {
|
||||
var channelInfoOffset: UInt16 = 0
|
||||
var channelInfoLength: UInt16 = 0
|
||||
if channel != .NONE, let channelInfo = channelInfo {
|
||||
channelInfoOffset = UInt16(sizeof(SMB2.Header.self) + sizeof(WriteRequest.Header.self))
|
||||
channelInfoLength = UInt16(sizeof(channelInfo.dynamicType))
|
||||
if channel != .NONE, let _ = channelInfo {
|
||||
channelInfoOffset = UInt16(MemoryLayout<SMB2.Header>.size + MemoryLayout<WriteRequest.Header>.size)
|
||||
channelInfoLength = UInt16(MemoryLayout<SMB2.ChannelInfo>.size)
|
||||
}
|
||||
let dataOffset = UInt16(sizeof(SMB2.Header.self) + sizeof(WriteRequest.Header.self)) + channelInfoLength
|
||||
self.header = WriteRequest.Header(size: UInt16(49), dataOffset: dataOffset, length: UInt32(data.length), offset: offset, fileId: fileId, _channel: channel.rawValue, remainingBytes: remainingBytes, channelInfoOffset: channelInfoOffset, channelInfoLength: channelInfoLength, flags: flags)
|
||||
let dataOffset = UInt16(MemoryLayout<SMB2.Header>.size + MemoryLayout<WriteRequest.Header>.size) + channelInfoLength
|
||||
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
|
||||
}
|
||||
// codebeat:enable[ARITY]
|
||||
|
||||
func data() -> NSData {
|
||||
let result = NSMutableData(data: encode(self.header))
|
||||
func data() -> Data {
|
||||
var result = Data(value: self.header)
|
||||
if let channelInfo = channelInfo {
|
||||
result.appendData(channelInfo.data())
|
||||
result.append(channelInfo.data())
|
||||
}
|
||||
result.appendData(fileData)
|
||||
result.append(fileData)
|
||||
return result
|
||||
}
|
||||
|
||||
struct Flags: OptionSetType {
|
||||
struct Flags: OptionSet {
|
||||
let rawValue: UInt32
|
||||
|
||||
init(rawValue: UInt32) {
|
||||
@@ -144,42 +146,34 @@ extension SMB2 {
|
||||
}
|
||||
}
|
||||
|
||||
struct WriteResponse: SMBResponse {
|
||||
struct WriteResponse: SMBResponseBody {
|
||||
let size: UInt16
|
||||
private let reserved: UInt16
|
||||
fileprivate let reserved: UInt16
|
||||
let writtenBytes: UInt32
|
||||
private let remaining: UInt32
|
||||
private let channelInfoOffset: UInt16
|
||||
private let channelInfoLength: UInt16
|
||||
|
||||
init?(data: NSData) {
|
||||
self = decode(data)
|
||||
}
|
||||
fileprivate let remaining: UInt32
|
||||
fileprivate let channelInfoOffset: UInt16
|
||||
fileprivate let channelInfoLength: UInt16
|
||||
}
|
||||
|
||||
struct ChannelInfo: SMBRequest {
|
||||
struct ChannelInfo: SMBRequestBody {
|
||||
static var command: SMB2.Command = .WRITE
|
||||
|
||||
let offset: UInt64
|
||||
let token: UInt32
|
||||
let length: UInt32
|
||||
|
||||
func data() -> NSData {
|
||||
return encode(data)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: SMB2 Lock
|
||||
|
||||
struct LockElement: SMBRequest {
|
||||
struct LockElement: SMBRequestBody {
|
||||
static var command: SMB2.Command = .LOCK
|
||||
|
||||
let offset: UInt64
|
||||
let length: UInt64
|
||||
let flags: LockElement.Flags
|
||||
private let reserved: UInt32
|
||||
fileprivate let reserved: UInt32
|
||||
|
||||
func data() -> NSData {
|
||||
return encode(self)
|
||||
}
|
||||
|
||||
struct Flags: OptionSetType {
|
||||
struct Flags: OptionSet {
|
||||
let rawValue: UInt32
|
||||
|
||||
init(rawValue: UInt32) {
|
||||
@@ -193,7 +187,9 @@ extension SMB2 {
|
||||
}
|
||||
}
|
||||
|
||||
struct LockRequest: SMBRequest {
|
||||
struct LockRequest: SMBRequestBody {
|
||||
static var command: SMB2.Command = .LOCK
|
||||
|
||||
let header: LockRequest.Header
|
||||
let locks: [LockElement]
|
||||
|
||||
@@ -202,23 +198,23 @@ extension SMB2 {
|
||||
self.locks = locks
|
||||
}
|
||||
|
||||
func data() -> NSData {
|
||||
let result = NSMutableData(data: encode(header))
|
||||
func data() -> Data {
|
||||
var result = Data(value: header)
|
||||
for lock in locks {
|
||||
result.appendData(encode(lock))
|
||||
result.append(Data(value: lock))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
struct Header {
|
||||
let size: UInt16
|
||||
private let lockCount: UInt16
|
||||
fileprivate let lockCount: UInt16
|
||||
let lockSequence: UInt32
|
||||
let fileId : FileId
|
||||
}
|
||||
}
|
||||
|
||||
struct LockResponse: SMBResponse {
|
||||
struct LockResponse: SMBResponseBody {
|
||||
let size: UInt16
|
||||
let reserved: UInt16
|
||||
|
||||
@@ -226,15 +222,13 @@ extension SMB2 {
|
||||
self.size = 4
|
||||
self.reserved = 0
|
||||
}
|
||||
|
||||
init? (data: NSData) {
|
||||
self = decode(data)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: SMB2 Cancel
|
||||
|
||||
struct CancelRequest: SMBRequest {
|
||||
struct CancelRequest: SMBRequestBody {
|
||||
static var command: SMB2.Command = .CANCEL
|
||||
|
||||
let size: UInt16
|
||||
let reserved: UInt16
|
||||
|
||||
@@ -242,10 +236,5 @@ extension SMB2 {
|
||||
self.size = 4
|
||||
self.reserved = 0
|
||||
}
|
||||
|
||||
func data() -> NSData {
|
||||
return encode(self)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+117
-131
@@ -15,31 +15,30 @@ extension SMB2 {
|
||||
* IOCtl usage is usually limited in SMB to pipe requests and duplicating file inside server
|
||||
*/
|
||||
|
||||
struct IOCtlRequest: SMBRequest {
|
||||
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(sizeof(SMB2.Header.self) + sizeof(IOCtlRequest.Header.self)) : 0
|
||||
self.header = Header(size: 57, reserved: 0, _ctlCode: ctlCode.rawValue, fileId: fileId, inputOffset: offset, inputCount: UInt32((requestData?.data().length ?? 0)), maxInputResponse: 0, outputOffset: offset, outputCount: 0, maxOutputResponse: UInt32(Int32.max), flags: flags, reserved2: 0)
|
||||
let offset = requestData != nil ? UInt32(MemoryLayout<SMB2.Header>.size + MemoryLayout<IOCtlRequest.Header>.size) : 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
|
||||
}
|
||||
|
||||
func data() -> NSData {
|
||||
let result = NSMutableData(data: encode(self.header))
|
||||
func data() -> Data {
|
||||
var result = Data(value: self.header)
|
||||
if let reqData = requestData?.data() {
|
||||
result.appendData(reqData)
|
||||
result.append(reqData)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
struct Header {
|
||||
let size: UInt16
|
||||
private let reserved: UInt16
|
||||
private let _ctlCode: UInt32
|
||||
var ctlCode: IOCtlCode {
|
||||
return IOCtlCode(rawValue: _ctlCode)!
|
||||
}
|
||||
fileprivate let reserved: UInt16
|
||||
let ctlCode: IOCtlCode
|
||||
let fileId: FileId
|
||||
let inputOffset: UInt32
|
||||
let inputCount: UInt32
|
||||
@@ -48,10 +47,10 @@ extension SMB2 {
|
||||
let outputCount: UInt32
|
||||
let maxOutputResponse: UInt32
|
||||
let flags: IOCtlRequest.Flags
|
||||
private let reserved2: UInt32
|
||||
fileprivate let reserved2: UInt32
|
||||
}
|
||||
|
||||
struct Flags: OptionSetType {
|
||||
struct Flags: OptionSet {
|
||||
let rawValue: UInt32
|
||||
|
||||
init(rawValue: UInt32) {
|
||||
@@ -63,14 +62,14 @@ extension SMB2 {
|
||||
}
|
||||
}
|
||||
|
||||
struct IOCtlResponse: SMBResponse {
|
||||
struct IOCtlResponse: SMBResponseBody {
|
||||
let header: Header
|
||||
let responseData: IOCtlResponseProtocol?
|
||||
|
||||
init?(data: NSData) {
|
||||
self.header = decode(data)
|
||||
let responseRange = NSRange(location: Int(self.header.outputOffset - 64), length: Int(self.header.outputCount))
|
||||
let response = data.subdataWithRange(responseRange)
|
||||
init?(data: Data) {
|
||||
self.header = data.scanValue()!
|
||||
let endRange = Int(self.header.outputOffset - 64) + Int(self.header.outputCount)
|
||||
let response = data.subdata(in: Int(self.header.outputOffset - 64)..<endRange)
|
||||
switch self.header.ctlCode {
|
||||
case .SRV_COPYCHUNK, .SRV_COPYCHUNK_WRITE:
|
||||
self.responseData = IOCtlResponseData.SrvCopyChunk(data: response)
|
||||
@@ -91,119 +90,109 @@ extension SMB2 {
|
||||
|
||||
struct Header {
|
||||
let size: UInt16
|
||||
private let reserved: UInt16
|
||||
private let _ctlCode: UInt32
|
||||
var ctlCode: IOCtlCode {
|
||||
return IOCtlCode(rawValue: _ctlCode)!
|
||||
}
|
||||
fileprivate let reserved: UInt16
|
||||
let ctlCode: IOCtlCode
|
||||
let fileId: FileId
|
||||
let inputOffset: UInt32
|
||||
let inputCount: UInt32
|
||||
let outputOffset: UInt32
|
||||
let outputCount: UInt32
|
||||
private let flags: UInt32
|
||||
private let reserved2: UInt32
|
||||
fileprivate let flags: UInt32
|
||||
fileprivate let reserved2: UInt32
|
||||
}
|
||||
}
|
||||
|
||||
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]
|
||||
|
||||
func data() -> NSData {
|
||||
let result = NSMutableData(data: encode(sourceKey))
|
||||
result.appendData(encode(chunkCount))
|
||||
var reserved: UInt32 = 0
|
||||
result.appendData(encode(&reserved))
|
||||
return NSData()
|
||||
func data() -> Data {
|
||||
var result = Data(value: sourceKey)
|
||||
result.append(Data(value: chunkCount))
|
||||
let reserved: UInt32 = 0
|
||||
result.append(Data(value: reserved))
|
||||
return Data()
|
||||
}
|
||||
|
||||
struct Chunk {
|
||||
let sourceOffset: UInt64
|
||||
let targetOffset: UInt64
|
||||
let length: UInt32
|
||||
private let reserved: UInt32
|
||||
|
||||
func data() -> NSData {
|
||||
return encode(self)
|
||||
}
|
||||
fileprivate let reserved: UInt32
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func data() -> NSData {
|
||||
return encode(self)
|
||||
}
|
||||
}
|
||||
|
||||
struct ResilencyRequest: IOCtlRequestProtocol {
|
||||
static var command: SMB2.Command = .IOCTL
|
||||
|
||||
let timeout: UInt32
|
||||
private let reserved: UInt32
|
||||
fileprivate let reserved: UInt32
|
||||
|
||||
/// The requested time the server holds the file open after a disconnect before releasing it. This time is in milliseconds.
|
||||
init(timeout: UInt32) {
|
||||
self.timeout = timeout
|
||||
self.reserved = 0
|
||||
}
|
||||
|
||||
func data() -> NSData {
|
||||
return encode(self)
|
||||
}
|
||||
}
|
||||
|
||||
struct ValidateNegotiateInfo: IOCtlRequestProtocol {
|
||||
static var command: SMB2.Command = .IOCTL
|
||||
|
||||
let header: ValidateNegotiateInfo.Header
|
||||
let dialects: [UInt16]
|
||||
|
||||
@@ -212,9 +201,9 @@ extension SMB2 {
|
||||
self.dialects = dialects
|
||||
}
|
||||
|
||||
func data() -> NSData {
|
||||
let result = NSMutableData(data: encode(self.header))
|
||||
dialects.forEach { result.appendData(encode($0)) }
|
||||
func data() -> Data {
|
||||
var result = Data(value: self.header)
|
||||
dialects.forEach { result.append(Data(value: $0)) }
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -234,10 +223,6 @@ extension SMB2 {
|
||||
let chunksCount: UInt32
|
||||
let chunksBytesWritten: UInt32
|
||||
let totalBytesWriiten: UInt32
|
||||
|
||||
init?(data: NSData) {
|
||||
self = decode(data)
|
||||
}
|
||||
}
|
||||
|
||||
// SRV_ENUMERATE_SNAPSHOTS
|
||||
@@ -246,20 +231,21 @@ extension SMB2 {
|
||||
let returnedCount: UInt32
|
||||
let snapshots: [SMBTime]
|
||||
|
||||
init?(data: NSData) {
|
||||
self.count = decode(data)
|
||||
self.returnedCount = decode(data.subdataWithRange(NSRange(location: 4, length: 4)))
|
||||
init?(data: Data) {
|
||||
guard data.count > 8 else { return nil }
|
||||
self.count = data.scanValue()!
|
||||
self.returnedCount = data.scanValue(start: 4)!
|
||||
//let size: UInt32 = decode(data.subdataWithRange(NSRange(location: 8, length: 4)))
|
||||
var snapshots = [SMBTime]()
|
||||
let dateFormatter = NSDateFormatter()
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "'@GMT-'yyyy'.'MM'.'dd'-'HH'.'mm'.'ss"
|
||||
for i in 0..<Int(returnedCount) {
|
||||
let offset = 24 + i * 48
|
||||
if data.length < offset + 48 {
|
||||
if data.count < offset + 48 {
|
||||
return nil
|
||||
}
|
||||
let datestring = String(data: data.subdataWithRange(NSRange(location: offset, length: 48)), encoding: NSUTF16StringEncoding)
|
||||
if let datestring = datestring, let date = dateFormatter.dateFromString(datestring) {
|
||||
let datestring = data.scanString(start: offset, length: 48, using: .utf16)
|
||||
if let datestring = datestring, let date = dateFormatter.date(from: datestring) {
|
||||
snapshots.append(SMBTime(date: date))
|
||||
}
|
||||
}
|
||||
@@ -269,34 +255,23 @@ extension SMB2 {
|
||||
|
||||
struct ResumeKey: IOCtlResponseProtocol {
|
||||
let key: (UInt64, UInt64, UInt64)
|
||||
private let contextLength: UInt32
|
||||
private let context: UInt32
|
||||
|
||||
init?(data: NSData) {
|
||||
self = decode(data)
|
||||
}
|
||||
fileprivate let contextLength: UInt32
|
||||
fileprivate let context: UInt32
|
||||
}
|
||||
|
||||
struct ReadHash: IOCtlResponseProtocol {
|
||||
// TODO: Implement IOCTL READ_HASH
|
||||
|
||||
init?(data: NSData) {
|
||||
self = decode(data)
|
||||
}
|
||||
}
|
||||
|
||||
struct NetworkInterfaceInfo: IOCtlResponseProtocol {
|
||||
let items: [NetworkInterfaceInfo.Item]
|
||||
|
||||
init?(data: NSData) {
|
||||
let count = data.length / sizeof(Item)
|
||||
guard count > 0 else {
|
||||
return nil
|
||||
}
|
||||
init?(data: Data) {
|
||||
var items = [Item]()
|
||||
for i in 0..<count {
|
||||
let itemdata = data.subdataWithRange(NSRange(location: i * sizeof(Item), length: sizeof(Item)))
|
||||
items.append(decode(itemdata))
|
||||
var offset = 0
|
||||
while let item: Item = data.scanValue(start: offset) {
|
||||
items.append(item)
|
||||
offset += MemoryLayout<Item>.size
|
||||
}
|
||||
self.items = items
|
||||
}
|
||||
@@ -307,10 +282,11 @@ extension SMB2 {
|
||||
/// specifies the network interface index.
|
||||
let ifIndex: UInt32
|
||||
let capability: IOCtlCapabilities
|
||||
private let reserved: UInt32
|
||||
fileprivate let reserved: UInt32
|
||||
/// Speed of the network interface in bits per second
|
||||
let linkSpeed: UInt64
|
||||
private let sockaddrStorage: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
fileprivate let sockaddrStorage:
|
||||
(UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
@@ -334,15 +310,11 @@ extension SMB2 {
|
||||
static let ipv6: sa_family_t = 0x17
|
||||
|
||||
var sockaddr: sockaddr_in {
|
||||
var sockaddrStorage = self.sockaddrStorage
|
||||
let data = NSData(bytes: &sockaddrStorage, length: 16)
|
||||
return decode(data)
|
||||
return unsafeBitCast(self.sockaddrStorage, to: sockaddr_in.self)
|
||||
}
|
||||
|
||||
var sockaddr6: sockaddr_in6 {
|
||||
var sockaddrStorage = self.sockaddrStorage
|
||||
let data = NSData(bytes: &sockaddrStorage, length: 28)
|
||||
return decode(data)
|
||||
return unsafeBitCast(self.sockaddrStorage, to: sockaddr_in6.self)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -351,18 +323,14 @@ extension SMB2 {
|
||||
let capabilities: IOCtlCapabilities
|
||||
let guid: uuid_t
|
||||
let securityMode: UInt16
|
||||
private let _dialect: UInt16
|
||||
fileprivate let _dialect: UInt16
|
||||
var dialect: (major: Int, minor: Int) {
|
||||
return (major: Int(_dialect & 0xFF), minor: Int(_dialect >> 8))
|
||||
}
|
||||
|
||||
init?(data: NSData) {
|
||||
self = decode(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct IOCtlCapabilities: OptionSetType {
|
||||
struct IOCtlCapabilities: OptionSet {
|
||||
let rawValue: UInt32
|
||||
|
||||
init(rawValue: UInt32) {
|
||||
@@ -373,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
//
|
||||
// SMB2Notification.swift
|
||||
// FileProvider
|
||||
//
|
||||
// Created by Amir Abbas Mousavian on 5/18/95.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
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
|
||||
let fileId: FileId
|
||||
let completionFilters: CompletionFilter
|
||||
fileprivate let reserved: UInt32
|
||||
|
||||
init(fileId: FileId, completionFilters: CompletionFilter, flags: ChangeNotifyRequest.Flags = [], outputBufferLength: UInt32 = 65535) {
|
||||
self.size = 32
|
||||
self.flags = flags
|
||||
self.outputBufferLength = outputBufferLength
|
||||
self.fileId = fileId
|
||||
self.completionFilters = completionFilters
|
||||
self.reserved = 0
|
||||
}
|
||||
|
||||
struct Flags: OptionSet {
|
||||
let rawValue: UInt16
|
||||
|
||||
init(rawValue: UInt16) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
static let WATCH_TREE = Flags(rawValue: 0x0001)
|
||||
}
|
||||
|
||||
struct CompletionFilter: OptionSet {
|
||||
let rawValue: UInt32
|
||||
|
||||
init(rawValue: UInt32) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
/// The client is notified if a file-name changes.
|
||||
static let FILE_NAME = CompletionFilter(rawValue: 0x00000001)
|
||||
/// The client is notified if a directory name changes.
|
||||
static let DIR_NAME = CompletionFilter(rawValue: 0x00000002)
|
||||
/// The client is notified if a file's attributes change.
|
||||
static let ATTRIBUTES = CompletionFilter(rawValue: 0x00000004)
|
||||
/// The client is notified if a file's size changes.
|
||||
static let SIZE = CompletionFilter(rawValue: 0x00000008)
|
||||
/// The client is notified if the last write time of a file changes.
|
||||
static let LAST_WRITE = CompletionFilter(rawValue: 0x00000010)
|
||||
/// The client is notified if the last access time of a file changes.
|
||||
static let LAST_ACCESS = CompletionFilter(rawValue: 0x00000020)
|
||||
/// The client is notified if the creation time of a file changes.
|
||||
static let CREATION = CompletionFilter(rawValue: 0x00000040)
|
||||
/// The client is notified if a file's extended attributes (EAs) change.
|
||||
static let EA = CompletionFilter(rawValue: 0x00000080)
|
||||
/// The client is notified of a file's access control list (ACL) settings change.
|
||||
static let SECURITY = CompletionFilter(rawValue: 0x00000100)
|
||||
/// The client is notified if a named stream is added to a file.
|
||||
static let STREAM_NAME = CompletionFilter(rawValue: 0x00000200)
|
||||
/// The client is notified if the size of a named stream is changed.
|
||||
static let STREAM_SIZE = CompletionFilter(rawValue: 0x00000400)
|
||||
/// The client is notified if a named stream is modified.
|
||||
static let STREAM_WRITE = Flags(rawValue: 0x00000800)
|
||||
|
||||
static let all = CompletionFilter(rawValue: 0x00000FFF)
|
||||
static let list: CompletionFilter = [.FILE_NAME, .DIR_NAME]
|
||||
}
|
||||
}
|
||||
|
||||
struct ChangeNotifyResponse: SMBResponseBody {
|
||||
let notifications: [(action: FileNotifyAction, fileName: String)]
|
||||
|
||||
init?(data: Data) {
|
||||
let maxLoop = 1000
|
||||
var i = 0
|
||||
var result = [(action: FileNotifyAction, fileName: String)]()
|
||||
|
||||
var offset = 0
|
||||
while i < maxLoop {
|
||||
let nextOffset: UInt32 = data.scanValue(start: offset) ?? 0
|
||||
let actionValue: UInt32 = data.scanValue(start: offset + 4) ?? 0
|
||||
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) ?? ""
|
||||
result.append((action: action, fileName: fileName))
|
||||
|
||||
offset += Int(nextOffset)
|
||||
if nextOffset == 0 {
|
||||
break
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
|
||||
self.notifications = result
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
public static let ADDED = FileNotifyAction(0x00000001)
|
||||
/// The file was removed from the directory.
|
||||
public static let REMOVED = FileNotifyAction(0x00000002)
|
||||
/// The file was modified. This can be a change to the data or attributes of the file.
|
||||
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.
|
||||
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.
|
||||
public static let RENAMED_NEW_NAME = FileNotifyAction(0x00000005)
|
||||
/// The file was added to a named stream.
|
||||
public static let ADDED_STREAM = FileNotifyAction(0x00000006)
|
||||
/// The file was removed from the named stream.
|
||||
public static let REMOVED_STREAM = FileNotifyAction(0x00000007)
|
||||
/// The file was modified. This can be a change to the data or attributes of the file.
|
||||
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".
|
||||
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".
|
||||
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".
|
||||
public static let TUNNELLED_ID_COLLISION = FileNotifyAction(0x0000000B)
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,328 @@ import Foundation
|
||||
extension SMB2 {
|
||||
// MARK: SMB2 Query Directory
|
||||
|
||||
struct QueryDirectoryRequest: SMBRequestBody {
|
||||
static let command: SMB2.Command = .QUERY_DIRECTORY
|
||||
|
||||
let header: QueryDirectoryRequest.Header
|
||||
let searchPattern: String?
|
||||
|
||||
/// - **bufferLength:** maximum number of bytes the server is allowed to return which is the same as maxTransactSize returned by negotiation.
|
||||
/// - **searchPattern:** can hold wildcards or be nil if all entries should be returned.
|
||||
/// - **fileIndex:** The byte offset within the directory, indicating the position at which to resume the enumeration.
|
||||
init(fileId: FileId, infoClass: FileInformationEnum, flags: Flags, bufferLength: UInt32 = 65535, searchPattern: String? = nil, fileIndex: UInt32 = 0) {
|
||||
assert(FileInformationEnum.queryDirectory.contains(infoClass), "Invalid FileInformationClass used for QueryDirectoryRequest")
|
||||
let searchPatternOffset = searchPattern != nil ? MemoryLayout<SMB2.Header>.size + MemoryLayout<QueryDirectoryRequest.Header>.size : 0
|
||||
let nflags = flags.intersection(fileIndex > 0 ? [.INDEX_SPECIFIED] : [])
|
||||
let searchPatternLength = searchPattern?.data(using: .utf16)?.count ?? 0
|
||||
self.header = Header(size: 53, infoClass: infoClass, flags: nflags, fileIndex: fileIndex, fileId: fileId, searchPatternOffset: UInt8(searchPatternOffset), searchPatternLength: UInt8(searchPatternLength), bufferLength: bufferLength)
|
||||
self.searchPattern = searchPattern
|
||||
}
|
||||
|
||||
func data() -> Data {
|
||||
var result = Data(value: header)
|
||||
if let patternData = searchPattern?.data(using: .utf16) {
|
||||
result.append(patternData)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
struct Header {
|
||||
let size: UInt8
|
||||
let infoClass: FileInformationEnum
|
||||
let flags: QueryDirectoryRequest.Flags
|
||||
let fileIndex: UInt32
|
||||
let fileId: FileId
|
||||
let searchPatternOffset: UInt8
|
||||
let searchPatternLength: UInt8
|
||||
let bufferLength: UInt32
|
||||
}
|
||||
|
||||
struct Flags: OptionSet {
|
||||
let rawValue: UInt8
|
||||
|
||||
init(rawValue: UInt8) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
static let RESTART_SCANS = Flags(rawValue: 0x01)
|
||||
static let RETURN_SINGLE_ENTRY = Flags(rawValue: 0x02)
|
||||
static let INDEX_SPECIFIED = Flags(rawValue: 0x04)
|
||||
static let REOPEN = Flags(rawValue: 0x10)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: SMB2 Change Notify
|
||||
struct QueryDirectoryResponse: SMBResponseBody {
|
||||
let buffer: Data
|
||||
|
||||
func parseAs(type: FileInformationEnum) -> [(header: SMB2FilesInformationHeader, fileName: String)] {
|
||||
var offset = 0
|
||||
var result = [(header: SMB2FilesInformationHeader, fileName: String)]()
|
||||
while true {
|
||||
let header: SMB2FilesInformationHeader
|
||||
switch type {
|
||||
case .fileDirectoryInformation:
|
||||
let tHeader: FileDirectoryInformationHeader = buffer.scanValue(start: offset)!
|
||||
header = tHeader
|
||||
case .fileFullDirectoryInformation:
|
||||
let tHeader: FileFullDirectoryInformationHeader = buffer.scanValue(start: offset)!
|
||||
header = tHeader
|
||||
case .fileIdFullDirectoryInformation:
|
||||
let tHeader: FileIdFullDirectoryInformationHeader = buffer.scanValue(start: offset)!
|
||||
header = tHeader
|
||||
case .fileBothDirectoryInformation:
|
||||
let tHeader: FileBothDirectoryInformationHeader = buffer.scanValue(start: offset)!
|
||||
header = tHeader
|
||||
case .fileIdBothDirectoryInformation:
|
||||
let tHeader: FileIdBothDirectoryInformationHeader = buffer.scanValue(start: offset)!
|
||||
header = tHeader
|
||||
case .fileNamesInformation:
|
||||
let tHeader: FileNamesInformationHeader = buffer.scanValue(start: offset)!
|
||||
header = tHeader
|
||||
default:
|
||||
return []
|
||||
}
|
||||
let headersize = MemoryLayout.size(ofValue: header)
|
||||
let fileName = buffer.scanString(start: headersize, length: Int(header.fileNameLength), using: .utf16) ?? ""
|
||||
result.append((header: header, fileName: fileName))
|
||||
if header.nextEntryOffset == 0 {
|
||||
break
|
||||
}
|
||||
offset += Int(header.nextEntryOffset)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
init? (data: Data) {
|
||||
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
|
||||
}
|
||||
self.buffer = data.subdata(in: offset..<(offset + length))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: SMB2 Query Info
|
||||
|
||||
}
|
||||
struct QueryInfoRequest: SMBRequestBody {
|
||||
static var command: SMB2.Command = .QUERY_INFO
|
||||
|
||||
let header: Header
|
||||
let buffer: Data?
|
||||
|
||||
init(fileId: FileId, infoClass: FileInformationEnum, outputBufferLength: UInt32 = 65535) {
|
||||
self.header = Header(size: 41, infoType: 1, infoClass: infoClass.rawValue, outputBufferLength: outputBufferLength, inputBufferOffset: 0, reserved: 0, inputBufferLength: 0, additionalInformation: [], flags: [], fileId: fileId)
|
||||
self.buffer = nil
|
||||
}
|
||||
|
||||
init(fileId: FileId, extendedAttributes: [String], flags: Flags = [], outputBufferLength: UInt32 = 65535) {
|
||||
var buffer = Data()
|
||||
for ea in extendedAttributes {
|
||||
guard let strData = ea.data(using: .ascii) else {
|
||||
continue
|
||||
}
|
||||
let strLength = UInt8(strData.count)
|
||||
let nextOffset = UInt32(4 + 1 + strData.count)
|
||||
var data = Data(value: nextOffset)
|
||||
data.append(Data(value: strLength))
|
||||
data.append(strData)
|
||||
data.count += 1
|
||||
let padSize = (data.count) % 4
|
||||
data.count += padSize
|
||||
buffer.append(data as Data)
|
||||
}
|
||||
|
||||
let bufferOffset = UInt16(MemoryLayout<SMB2.Header>.size + MemoryLayout<QueryInfoRequest.Header>.size)
|
||||
self.header = Header(size: 41, infoType: 1, infoClass: FileInformationEnum.fileFullEaInformation.rawValue, outputBufferLength: outputBufferLength, inputBufferOffset: bufferOffset, reserved: 0, inputBufferLength: UInt32(buffer.count), additionalInformation: [], flags: flags, fileId: fileId)
|
||||
self.buffer = buffer as Data
|
||||
}
|
||||
|
||||
init(fileId: FileId, infoClass: FileSystemInformationEnum, outputBufferLength: UInt32 = 65535) {
|
||||
self.header = Header(size: 41, infoType: 2, infoClass: infoClass.rawValue, outputBufferLength: outputBufferLength, inputBufferOffset: 0, reserved: 0, inputBufferLength: 0, additionalInformation: [], flags: [], fileId: fileId)
|
||||
self.buffer = nil
|
||||
}
|
||||
|
||||
init(fileId: FileId, securityInfo: FileSecurityInfo, outputBufferLength: UInt32 = 65535) {
|
||||
self.header = Header(size: 41, infoType: 3, infoClass: 0, outputBufferLength: outputBufferLength, inputBufferOffset: 0, reserved: 0, inputBufferLength: 0, additionalInformation: securityInfo, flags: [], fileId: fileId)
|
||||
self.buffer = nil
|
||||
}
|
||||
|
||||
// TODO: Implement QUOTA_INFO init
|
||||
|
||||
func data() -> Data {
|
||||
var result = Data(value: header)
|
||||
if let buffer = buffer {
|
||||
result.append(buffer)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
struct Header {
|
||||
let size: UInt16
|
||||
let infoType: UInt8
|
||||
let infoClass: UInt8
|
||||
let outputBufferLength: UInt32
|
||||
let inputBufferOffset: UInt16
|
||||
fileprivate let reserved: UInt16
|
||||
let inputBufferLength: UInt32
|
||||
let additionalInformation: FileSecurityInfo
|
||||
let flags: QueryInfoRequest.Flags
|
||||
let fileId: FileId
|
||||
}
|
||||
|
||||
struct Flags: OptionSet {
|
||||
let rawValue: UInt32
|
||||
|
||||
init(rawValue: UInt32) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
static let RESTART_SCAN = Flags(rawValue: 0x00000001)
|
||||
static let RETURN_SINGLE_ENTRY = Flags(rawValue: 0x00000002)
|
||||
static let INDEX_SPECIFIED = Flags(rawValue: 0x00000004)
|
||||
}
|
||||
}
|
||||
|
||||
struct QueryInfoResponse: SMBResponseBody {
|
||||
let buffer: Data
|
||||
|
||||
init?(data: Data) {
|
||||
let structSize: UInt16 = data.scanValue()!
|
||||
guard structSize == 9 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
/*let offsetData = data.subdataWithRange(NSRange(location: 2, length: 2))
|
||||
let offset: UInt16 = decode(offsetData)*/
|
||||
|
||||
let tLength: UInt32 = data.scanValue(start: 4)!
|
||||
let length = Int(tLength)
|
||||
|
||||
guard data.count >= 8 + length else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.buffer = data.subdata(in: 8..<(8 + length))
|
||||
}
|
||||
|
||||
var asAccessInformation: FileAccessInformation {
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asAlignmentInformation: FileAlignmentInformation {
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asAllInformation: (header: FileAllInformationHeader, name: String) {
|
||||
let header: FileAllInformationHeader = buffer.scanValue()!
|
||||
let headersize = MemoryLayout<FileAllInformationHeader>.size
|
||||
let name = buffer.scanString(start: headersize, length: Int(header.nameLength), using: .utf16) ?? ""
|
||||
return (header, name)
|
||||
}
|
||||
|
||||
var asAlternateNameInformation: String {
|
||||
return buffer.scanString(start: 0, length: buffer.count, using: .utf16) ?? ""
|
||||
}
|
||||
|
||||
var asAttributeTagInformation: FileAttributeTagInformation {
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asBasicInformation: FileBasicInformation {
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asCompressionInformation: FileCompressionInformation {
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asEaInformation: FileEaInformation {
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asFullEaInformation: FileFullEaInformation {
|
||||
// TODO:
|
||||
return FileFullEaInformation()
|
||||
}
|
||||
|
||||
var asInternalInformation: FileInternalInformation {
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asModeInformation: FileModeInformation {
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asNetworkOpenInformation: FileNetworkOpenInformation {
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asPipeInformation: FilePipeInformation {
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asPipeLocalInformation: FilePipeLocalInformation {
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asPipeRemoteInformation: FilePipeRemoteInformation {
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asPositionInformation: FilePositionInformation {
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asStandardInformation: FileStandardInformation {
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asStreamInformation: (header: FileStreamInformationHeader, name: String) {
|
||||
let header: FileStreamInformationHeader = buffer.scanValue()!
|
||||
let headersize = MemoryLayout<FileStreamInformationHeader>.size
|
||||
let name = buffer.scanString(start: headersize, length: Int(header.streamNameLength), using: .utf16) ?? ""
|
||||
return (header, name)
|
||||
}
|
||||
|
||||
var asFsVolumeInformation: (header: FileFsVolumeInformationHeader, name: String) {
|
||||
let header: FileFsVolumeInformationHeader = buffer.scanValue()!
|
||||
let headersize = MemoryLayout<FileFsVolumeInformationHeader>.size
|
||||
let name = buffer.scanString(start: headersize, length: Int(header.labelLength), using: .utf16) ?? ""
|
||||
return (header, name)
|
||||
}
|
||||
|
||||
var asFsSizeInformation: FileFsSizeInformation {
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asFsDeviceInformation: FileFsDeviceInformation {
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asFsAttributeInformation: (header: FileFsAttributeInformationHeader, name: String) {
|
||||
let header: FileFsAttributeInformationHeader = buffer.scanValue()!
|
||||
let headersize = MemoryLayout<FileFsAttributeInformationHeader>.size
|
||||
let name = buffer.scanString(start: headersize, length: Int(header.nameLength), using: .utf16) ?? ""
|
||||
return (header, name)
|
||||
}
|
||||
|
||||
var asFsControlInformation: FileFsControlInformation {
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asFsFullSizeInformation: FileFsFullSizeInformation {
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asFsObjectIdInformation: FileFsObjectIdInformation {
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asFsSectorSizeInformation: FileFsSectorSizeInformation {
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,630 @@
|
||||
//
|
||||
// SMB2QueryTypes.swift
|
||||
// FileProvider
|
||||
//
|
||||
// Created by Amir Abbas Mousavian on 5/19/95.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol SMB2FilesInformationHeader: SMBResponseBody {
|
||||
var nextEntryOffset: UInt32 { get }
|
||||
var fileIndex: UInt32 { get }
|
||||
var fileNameLength : UInt32 { get }
|
||||
}
|
||||
|
||||
extension SMB2 {
|
||||
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]
|
||||
}
|
||||
|
||||
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 {
|
||||
let rawValue: UInt32
|
||||
|
||||
init(rawValue: UInt32) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
static let OWNER = FileSecurityInfo(rawValue: 0x00000001)
|
||||
static let GROUP = FileSecurityInfo(rawValue: 0x00000002)
|
||||
static let DACL = FileSecurityInfo(rawValue: 0x00000004)
|
||||
static let SACL = FileSecurityInfo(rawValue: 0x00000008)
|
||||
static let LABEL = FileSecurityInfo(rawValue: 0x00000010)
|
||||
static let ATTRIBUTE = FileSecurityInfo(rawValue: 0x00000020)
|
||||
static let SCOPE = FileSecurityInfo(rawValue: 0x00000040)
|
||||
static let BACKUP = FileSecurityInfo(rawValue: 0x00010000)
|
||||
}
|
||||
|
||||
struct FileDirectoryInformationHeader: SMB2FilesInformationHeader {
|
||||
let nextEntryOffset: UInt32
|
||||
let fileIndex: UInt32
|
||||
let creationTime: SMBTime
|
||||
let lastAccesTime: SMBTime
|
||||
let lastWriteTime: SMBTime
|
||||
let changeTime: SMBTime
|
||||
let fileSize: UInt64
|
||||
let allocationSize: UInt64
|
||||
let fileAttributes: FileAttributes
|
||||
let fileNameLength : UInt32
|
||||
}
|
||||
|
||||
struct FileFullDirectoryInformationHeader: SMB2FilesInformationHeader {
|
||||
let nextEntryOffset: UInt32
|
||||
let fileIndex: UInt32
|
||||
let creationTime: SMBTime
|
||||
let lastAccesTime: SMBTime
|
||||
let lastWriteTime: SMBTime
|
||||
let changeTime: SMBTime
|
||||
let fileSize: UInt64
|
||||
let allocationSize: UInt64
|
||||
let fileAttributes: FileAttributes
|
||||
let fileNameLength : UInt32
|
||||
let extendedAttributesSize: UInt32
|
||||
}
|
||||
|
||||
struct FileIdFullDirectoryInformationHeader: SMB2FilesInformationHeader {
|
||||
let nextEntryOffset: UInt32
|
||||
let fileIndex: UInt32
|
||||
let creationTime: SMBTime
|
||||
let lastAccesTime: SMBTime
|
||||
let lastWriteTime: SMBTime
|
||||
let changeTime: SMBTime
|
||||
let fileSize: UInt64
|
||||
let allocationSize: UInt64
|
||||
let fileAttributes: FileAttributes
|
||||
let fileNameLength : UInt32
|
||||
let extendedAttributesSize: UInt32
|
||||
fileprivate let reserved: UInt32
|
||||
let fileId: FileId
|
||||
}
|
||||
|
||||
struct FileBothDirectoryInformationHeader: SMB2FilesInformationHeader {
|
||||
let nextEntryOffset: UInt32
|
||||
let fileIndex: UInt32
|
||||
let creationTime: SMBTime
|
||||
let lastAccesTime: SMBTime
|
||||
let lastWriteTime: SMBTime
|
||||
let changeTime: SMBTime
|
||||
let fileSize: UInt64
|
||||
let allocationSize: UInt64
|
||||
let fileAttributes: FileAttributes
|
||||
let fileNameLength : UInt32
|
||||
let extendedAttributesSize: UInt32
|
||||
fileprivate let shortNameLen: UInt8
|
||||
fileprivate let reserved: UInt8
|
||||
fileprivate let _shortName: FileShortNameType
|
||||
var shortName: String? {
|
||||
var data = Data(value: _shortName)
|
||||
data.count = Int(shortNameLen)
|
||||
return String(data: data, encoding: .utf16)
|
||||
}
|
||||
}
|
||||
|
||||
struct FileIdBothDirectoryInformationHeader: SMB2FilesInformationHeader {
|
||||
let nextEntryOffset: UInt32
|
||||
let fileIndex: UInt32
|
||||
let creationTime: SMBTime
|
||||
let lastAccesTime: SMBTime
|
||||
let lastWriteTime: SMBTime
|
||||
let changeTime: SMBTime
|
||||
let fileSize: Int64
|
||||
let allocationSize: Int64
|
||||
let fileAttributes: FileAttributes
|
||||
let fileNameLength : UInt32
|
||||
let extendedAttributesSize: UInt32
|
||||
fileprivate let shortNameLen: UInt8
|
||||
fileprivate let reserved: UInt8
|
||||
fileprivate let _shortName: FileShortNameType
|
||||
var shortName: String? {
|
||||
var data = Data(value: _shortName)
|
||||
data.count = Int(shortNameLen)
|
||||
return String(data: data, encoding: .utf16)
|
||||
}
|
||||
fileprivate let reserved2: UInt16
|
||||
let fileId : FileId
|
||||
}
|
||||
|
||||
struct FileNamesInformationHeader: SMB2FilesInformationHeader {
|
||||
let nextEntryOffset: UInt32
|
||||
let fileIndex: UInt32
|
||||
let fileNameLength : UInt32
|
||||
}
|
||||
|
||||
typealias FileShortNameType = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)
|
||||
|
||||
struct FileAccessInformation {
|
||||
let accessMask: FileAccessMask
|
||||
}
|
||||
|
||||
struct FileAlignmentInformation {
|
||||
fileprivate let _alignment: UInt32
|
||||
var alignmentLength: UInt32 {
|
||||
return _alignment + 1
|
||||
}
|
||||
}
|
||||
|
||||
struct FileAllInformationHeader {
|
||||
let basic: FileBasicInformation
|
||||
let standard: FileStandardInformation
|
||||
let `internal`: FileInternalInformation
|
||||
let ea: FileEaInformation
|
||||
let access: FileAccessInformation
|
||||
let position: FilePositionInformation
|
||||
let mode: FileModeInformation
|
||||
let alignment: FileAlignmentInformation
|
||||
let nameLength: UInt32
|
||||
}
|
||||
|
||||
struct FileAttributeTagInformation {
|
||||
let fileAttributes: FileAttributes
|
||||
let reparseTag: UInt32
|
||||
}
|
||||
|
||||
struct FileBasicInformation {
|
||||
let creationTime: SMBTime
|
||||
let lastAccesTime: SMBTime
|
||||
let lastWriteTime: SMBTime
|
||||
let changeTime: SMBTime
|
||||
let fileAttributes: FileAttributes
|
||||
fileprivate let reserved: UInt32 = 0
|
||||
}
|
||||
|
||||
struct FileCompressionInformation {
|
||||
let compressedFileSize: Int64
|
||||
let compressionFormat: UInt16
|
||||
static let COMPRESSION_FORMAT_LZNT1 = 0x0002
|
||||
let compressionUnitShift: UInt8
|
||||
let chunkShift: UInt8
|
||||
let clusterShift: UInt8
|
||||
fileprivate let reserved: (UInt8, UInt16)
|
||||
}
|
||||
|
||||
struct FileEaInformation {
|
||||
let eaSize: UInt32
|
||||
}
|
||||
|
||||
struct FileFullEaInformation {
|
||||
// TODO
|
||||
}
|
||||
|
||||
struct FileInternalInformation {
|
||||
let indexNumber: UInt64
|
||||
}
|
||||
|
||||
struct FileModeInformation {
|
||||
let mode: Mode
|
||||
|
||||
struct Mode: OptionSet {
|
||||
let rawValue: UInt32
|
||||
|
||||
init(rawValue: UInt32) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
static let FILE_WRITE_THROUGH = Mode(rawValue: 0x00000002)
|
||||
static let FILE_SEQUENTIAL_ONLY = Mode(rawValue: 0x00000004)
|
||||
static let FILE_NO_INTERMEDIATE_BUFFERING = Mode(rawValue: 0x00000008)
|
||||
static let FILE_SYNCHRONOUS_IO_ALERT = Mode(rawValue: 0x00000010)
|
||||
static let FILE_SYNCHRONOUS_IO_NONALERT = Mode(rawValue: 0x00000020)
|
||||
static let FILE_DELETE_ON_CLOSE = Mode(rawValue: 0x00001000)
|
||||
}
|
||||
}
|
||||
|
||||
struct FileNetworkOpenInformation {
|
||||
let creationTime: SMBTime
|
||||
let lastAccesTime: SMBTime
|
||||
let lastWriteTime: SMBTime
|
||||
let changeTime: SMBTime
|
||||
let fileAttributes: FileAttributes
|
||||
fileprivate let reserved: UInt32
|
||||
}
|
||||
|
||||
struct FilePipeInformation {
|
||||
let readMode: ReadMode
|
||||
fileprivate let completionMode: CompletionMode
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
let namedPipeType: Type
|
||||
let namedPipeConfiguration: Configuration
|
||||
let maximumInstances: UInt32
|
||||
let currentInstances: UInt32
|
||||
let inboundQuota: UInt32
|
||||
let readDataAvailable: UInt32
|
||||
let outboundQuota: UInt32
|
||||
let writeQuotaAvailable: UInt32
|
||||
let namedPipeState: State
|
||||
let namedPipeEnd: End
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
struct FilePipeRemoteInformation {
|
||||
let collectDataTime: SMBTime
|
||||
let maximumCollectionCount: UInt32
|
||||
}
|
||||
|
||||
struct FilePositionInformation {
|
||||
let currentByteOffset: Int64
|
||||
}
|
||||
|
||||
struct FileStandardInformation {
|
||||
let allocationSize: Int64
|
||||
let fileSize: Int64
|
||||
let numberOfLinks: UInt32
|
||||
let deletePending: Bool
|
||||
let directory: Bool
|
||||
fileprivate let reserved: UInt16
|
||||
}
|
||||
|
||||
struct FileStreamInformationHeader {
|
||||
let nextEntryOffset: UInt32
|
||||
let streamNameLength: UInt32
|
||||
let streamSize: Int64
|
||||
let streamAllocationSize: Int64
|
||||
}
|
||||
|
||||
struct FileFsVolumeInformationHeader {
|
||||
let creationTime: SMBTime
|
||||
let serial: UInt32
|
||||
let labelLength: UInt32
|
||||
let supportObjects: Bool
|
||||
let reserved: UInt8
|
||||
}
|
||||
|
||||
struct FileFsSizeInformation {
|
||||
let totalAllocationUnits: Int64
|
||||
let availableAllocationUnits: Int64
|
||||
let sectorsPerAllocationUnit: UInt32
|
||||
let bytesPerSector: UInt32
|
||||
}
|
||||
|
||||
struct FileFsDeviceInformation {
|
||||
let deviceType: DeviceType
|
||||
let charactristics: Charactristics
|
||||
|
||||
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 {
|
||||
let rawValue: UInt32
|
||||
|
||||
init(rawValue: UInt32) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
/// Storage device supports removable media. For example, drivers for JAZ drive devices specify this characteristic, but drivers for PCMCIA flash disks do not.
|
||||
static let REMOVABLE_MEDIA = Charactristics(rawValue: 0x00000001)
|
||||
/// Indicates that the device cannot be written to.
|
||||
static let READ_ONLY_DEVICE = Charactristics(rawValue: 0x00000002)
|
||||
/// Indicates that the device is a floppy disk device.
|
||||
static let FLOPPY_DISKETTE = Charactristics(rawValue: 0x00000004)
|
||||
/// Indicates that the device supports write-once media.
|
||||
static let WRITE_ONCE_MEDIA = Charactristics(rawValue: 0x00000008)
|
||||
/// ndicates that the volume is for a remote file system like SMB or CIFS.
|
||||
static let REMOTE_DEVICE = Charactristics(rawValue: 0x00000010)
|
||||
/// Indicates that a file system is mounted on the device.
|
||||
static let DEVICE_IS_MOUNTED = Charactristics(rawValue: 0x00000020)
|
||||
/// Indicates that the volume does not directly reside on storage media, but resides on some other type of media (memory for example).
|
||||
static let VIRTUAL_VOLUME = Charactristics(rawValue: 0x00000040)
|
||||
/// By default, volumes do not check the ACL associated with the volume, but instead use the ACLs associated with individual files on the volume. When this flag is set the volume ACL is also checked.
|
||||
static let DEVICE_SECURE_OPEN = Charactristics(rawValue: 0x00000100)
|
||||
/// Indicates that the device object is part of a Terminal Services device stack.
|
||||
static let TS_DEVICE = Charactristics(rawValue: 0x00001000)
|
||||
/// ndicates that a web-based Distributed Authoring and Versioning (WebDAV) file system is mounted on the device.
|
||||
static let WEBDAV_DEVICE = Charactristics(rawValue: 0x00002000)
|
||||
/// The IO Manager normally performs a full security check for traverse access on every file open when the client is an appcontainer. Setting of this flag bypasses this enforced traverse access check if the client token already has traverse privileges.
|
||||
static let PORTABLE_DEVICE = Charactristics(rawValue: 0x0004000)
|
||||
/// Indicates that the given device resides on a portable bus like USB or Firewire and that the entire device (not just the media) can be removed from the system.
|
||||
static let DEVICE_ALLOW_APPCONTAINER_TRAVERSAL = Charactristics(rawValue: 0x00020000)
|
||||
}
|
||||
}
|
||||
|
||||
struct FileFsAttributeInformationHeader {
|
||||
let attributes: Attributes
|
||||
let maximumFileNameLength: Int32
|
||||
let nameLength: UInt32
|
||||
|
||||
struct Attributes: OptionSet {
|
||||
let rawValue: UInt32
|
||||
|
||||
init(rawValue: UInt32) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
/// 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 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)
|
||||
/// The file system preserves and enforces access control lists (ACLs).
|
||||
static let PERSISTENT_ACLS = Attributes(rawValue: 0x00000008)
|
||||
/// The file volume supports file-based compression. This flag is incompatible with the FILE_VOLUME_IS_COMPRESSED flag.
|
||||
static let FILE_COMPRESSION = Attributes(rawValue: 0x00000010)
|
||||
/// The file system supports per-user quotas.
|
||||
static let VOLUME_QUOTAS = Attributes(rawValue: 0x00000020)
|
||||
/// The file system supports sparse files.
|
||||
static let SUPPORTS_SPARSE_FILES = Attributes(rawValue: 0x00000040)
|
||||
/// The file system supports reparse points.
|
||||
static let SUPPORTS_REPARSE_POINTS = Attributes(rawValue: 0x00000080)
|
||||
/// The file system supports remote storage.
|
||||
static let REMOTE_STORAGE = Attributes(rawValue: 0x00000100)
|
||||
/// The specified volume is a compressed volume. This flag is incompatible with the FILE_FILE_COMPRESSION flag.
|
||||
static let IS_COMPRESSED = Attributes(rawValue: 0x00008000)
|
||||
/// The file system supports object identifiers.
|
||||
static let OBJECT_IDS = Attributes(rawValue: 0x00010000)
|
||||
/// The file system supports the Encrypted File System (EFS).
|
||||
static let ENCRYPTION = Attributes(rawValue: 0x00020000)
|
||||
/// The file system supports named streams. (aka. Resource Fork on MacOS)
|
||||
static let NAMED_STREAMS = Attributes(rawValue: 0x00040000)
|
||||
/// If set, the volume has been mounted in read-only mode.
|
||||
static let READ_ONLY_VOLUME = Attributes(rawValue: 0x00080000)
|
||||
/// The underlying volume is write once. (aka tapes)
|
||||
static let SEQUENTIAL_WRITE_ONCE = Attributes(rawValue: 0x00100000)
|
||||
/// The volume supports transactions.
|
||||
static let SUPPORTS_TRANSACTIONS = Attributes(rawValue: 0x00200000)
|
||||
/// The file system supports hard linking files.
|
||||
static let SUPPORTS_HARD_LINKS = Attributes(rawValue: 0x00400000)
|
||||
/// The file system persistently stores Extended Attribute information per file.
|
||||
static let SUPPORTS_EXTENDED_ATTRIBUTES = Attributes(rawValue: 0x00800000)
|
||||
/// The file system supports opening a file by FileID or ObjectID.
|
||||
static let SUPPORTS_OPEN_BY_FILE_ID = Attributes(rawValue: 0x01000000)
|
||||
/// The file system implements a USN change journal.
|
||||
static let USN_JOURNAL = Attributes(rawValue: 0x02000000)
|
||||
/// The file system supports integrity streams.
|
||||
static let SUPPORT_INTEGRITY_STREAMS = Attributes(rawValue: 0x04000000)
|
||||
/// The file system supports sharing logical clusters between files on the same volume. The file system reallocates on writes to shared clusters. Indicates that FSCTL_DUPLICATE_EXTENTS_TO_FILE is a supported operation.
|
||||
static let SUPPORTS_BLOCK_REFCOUNTING = Attributes(rawValue: 0x08000000)
|
||||
/// The file system tracks whether each cluster of a file contains valid data (either from explicit file writes or automatic zeros) or invalid data (has not yet been written to or zeroed).
|
||||
static let SUPPORTS_SPARSE_VDL = Attributes(rawValue: 0x10000000)
|
||||
}
|
||||
}
|
||||
|
||||
struct FileFsControlInformation {
|
||||
let freeSpaceStartFiltering: Int64
|
||||
let freeSpaceThreshold: Int64
|
||||
let freeSpaceStopFiltering: Int64
|
||||
let defaultQuotaThreshold: UInt64
|
||||
let defaultQuotaLimit: UInt64
|
||||
let flags: Flags
|
||||
fileprivate let padding: UInt32 = 0
|
||||
|
||||
struct Flags: OptionSet {
|
||||
let rawValue: UInt32
|
||||
|
||||
init(rawValue: UInt32) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
/// Quotas are tracked on the volume, but they are not enforced. Tracked quotas enable reporting on the file system space used by system users. If both this flag and FILE_VC_QUOTA_ENFORCE are specified, FILE_VC_QUOTA_ENFORCE is ignored.
|
||||
static let QUOTA_TRACK = Flags(rawValue: 0x00000001)
|
||||
/// Quotas are tracked and enforced on the volume.
|
||||
static let QUOTA_ENFORCE = Flags(rawValue: 0x00000002)
|
||||
/// Content indexing is disabled.
|
||||
static let CONTENT_INDEX_DISABLED = Flags(rawValue: 0x00000008)
|
||||
/// An event log entry will be created when the user exceeds his or her assigned quota warning threshold.
|
||||
static let LOG_QUOTA_THRESHOLD = Flags(rawValue: 0x00000010)
|
||||
/// An event log entry will be created when the user exceeds the assigned disk quota limit.
|
||||
static let LOG_QUOTA_LIMIT = Flags(rawValue: 0x00000020)
|
||||
/// An event log entry will be created when the volume's free space threshold is exceeded.
|
||||
static let LOG_VOLUME_THRESHOLD = Flags(rawValue: 0x00000040)
|
||||
/// An event log entry will be created when the volume's free space limit is exceeded.
|
||||
static let LOG_VOLUME_LIMIT = Flags(rawValue: 0x00000080)
|
||||
/// The quota information for the volume is incomplete because it is corrupt, or the system is in the process of rebuilding the quota information.
|
||||
static let QUOTAS_INCOMPLETE = Flags(rawValue: 0x00000100)
|
||||
/// The file system is rebuilding the quota information for the volume.
|
||||
static let QUOTAS_REBUILDING = Flags(rawValue: 0x00000200)
|
||||
}
|
||||
}
|
||||
|
||||
struct FileFsFullSizeInformation {
|
||||
let totalAllocationUnits: Int64
|
||||
let callerAvailableAllocationUnits: Int64
|
||||
let actualAvailableAllocationUnits: Int64
|
||||
let sectorsPerAllocationUnit: UInt32
|
||||
let bytesPerSector: UInt32
|
||||
}
|
||||
|
||||
struct FileFsObjectIdInformation {
|
||||
let objectId: uuid_t
|
||||
let extendedInfo: (UInt64, UInt64, UInt64, UInt64, UInt64, UInt64)
|
||||
}
|
||||
|
||||
struct FileFsSectorSizeInformation {
|
||||
let logicalBytesPerSector: UInt32
|
||||
let physicalBytesPerSectorForAtomicity: UInt32
|
||||
let physicalBytesPerSectorForPerformance: UInt32
|
||||
let effectivePhysicalBytesPerSectorForAtomicity: UInt32
|
||||
let flags: Flags
|
||||
let byteOffsetForSectorAlignment: UInt32
|
||||
let byteOffsetForPartitionAlignment: UInt32
|
||||
|
||||
struct Flags: OptionSet {
|
||||
let rawValue: UInt32
|
||||
|
||||
init(rawValue: UInt32) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
/// When set, this flag indicates that the first physical sector of the device is aligned with the first logical sector. When not set, the first physical sector of the device is misaligned with the first logical sector.
|
||||
static let ALIGNED_DEVICE = Flags(rawValue: 0x00000001)
|
||||
/// When set, this flag indicates that the partition is aligned to physical sector boundaries on the storage device.
|
||||
static let PARTITION_ALIGNED_ON_DEVICE = Flags(rawValue: 0x00000002)
|
||||
/// When set, the device reports that it does not incur a seek penalty (this typically indicates that the device does not have rotating media, such as flash-based disks).
|
||||
static let NO_SEEK_PENALTY = Flags(rawValue: 0x00000008)
|
||||
/// When set, the device supports TRIM operations, either T13 (ATA) TRIM or T10 (SCSI/SAS) UNMAP.
|
||||
static let TRIM_ENABLED = Flags(rawValue: 0x00000010)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,66 +11,75 @@ import Foundation
|
||||
extension SMB2 {
|
||||
// MARK: SMB2 Negotiating
|
||||
|
||||
struct NegotiateRequest: SMBRequest {
|
||||
let request: NegotiateRequest.Header
|
||||
let dialects: [UInt16]
|
||||
let contexts: [(type: NegotiateContextType, data: NSData)]
|
||||
struct NegotiateRequest: SMBRequestBody {
|
||||
static var command: SMB2.Command = .NEGOTIATE
|
||||
|
||||
init(request: NegotiateRequest.Header, dialects: [UInt16] = [0x0202], contexts: [(type: NegotiateContextType, data: NSData)] = []) {
|
||||
self.request = request
|
||||
let header: NegotiateRequest.Header
|
||||
let dialects: [UInt16]
|
||||
let contexts: [(type: NegotiateContextType, data: Data)]
|
||||
|
||||
init(header: NegotiateRequest.Header, dialects: [UInt16] = [0x0202], contexts: [(type: NegotiateContextType, data: Data)] = []) {
|
||||
self.header = header
|
||||
self.dialects = dialects
|
||||
self.contexts = contexts
|
||||
}
|
||||
|
||||
func data() -> NSData {
|
||||
var request = self.request
|
||||
request.dialectCount = UInt16(dialects.count)
|
||||
let dialectData = NSMutableData()
|
||||
init(dialects: [UInt16] = [0x0202], contexts: [(type: NegotiateContextType, data: Data)] = [],capabilities: GlobalCapabilities = [], clientStartTime: SMBTime? = nil, guid: uuid_t? = nil, signing: NegotiateSinging = [.ENABLED]) {
|
||||
self.header = Header(capabilities: capabilities, clientStartTime: clientStartTime, guid: guid, signing: signing)
|
||||
self.dialects = dialects
|
||||
self.contexts = contexts
|
||||
}
|
||||
|
||||
func data() -> Data {
|
||||
var header = self.header
|
||||
header.dialectCount = UInt16(dialects.count)
|
||||
var dialectData = Data()
|
||||
for dialect in dialects {
|
||||
var dialect = dialect
|
||||
dialectData.appendBytes(&dialect, length: 2)
|
||||
dialectData.append(UnsafeBufferPointer(start: &dialect, count: 2))
|
||||
}
|
||||
let pad = ((1024 - dialectData.length) % 8)
|
||||
dialectData.increaseLengthBy(pad)
|
||||
request.contextOffset = UInt32(sizeof(request.dynamicType.self)) + UInt32(dialectData.length)
|
||||
request.contextCount = UInt16(contexts.count)
|
||||
let pad = ((1024 - dialectData.count) % 8)
|
||||
dialectData.count += pad
|
||||
header.contextOffset = UInt32(MemoryLayout<NegotiateRequest.Header>.size) + UInt32(dialectData.count)
|
||||
header.contextCount = UInt16(contexts.count)
|
||||
|
||||
let contextData = NSMutableData()
|
||||
var contextData = Data()
|
||||
for context in contexts {
|
||||
var contextType = context.type.rawValue
|
||||
contextData.appendBytes(&contextType, length: 2)
|
||||
var dataLen = UInt16(context.data.length)
|
||||
contextData.increaseLengthBy(4)
|
||||
contextData.appendBytes(&dataLen, length: 2)
|
||||
contextData.append(Data(value: context.type.rawValue))
|
||||
contextData.count += 4
|
||||
contextData.append(Data(value: UInt16(context.data.count)))
|
||||
}
|
||||
let result = NSMutableData(data: encode(&request))
|
||||
result.appendData(dialectData)
|
||||
result.appendData(contextData)
|
||||
var result = Data(value: header)
|
||||
result.append(dialectData as Data)
|
||||
result.append(contextData as Data)
|
||||
return result
|
||||
}
|
||||
|
||||
struct Header {
|
||||
var size: UInt16
|
||||
var dialectCount: UInt16
|
||||
let singing: NegotiateSinging
|
||||
private let reserved: UInt16
|
||||
let signing: NegotiateSinging
|
||||
fileprivate let reserved: UInt16
|
||||
let capabilities: GlobalCapabilities
|
||||
let guid: uuid_t
|
||||
var contextOffset: UInt32
|
||||
var contextCount: UInt16
|
||||
private let reserved2: UInt16
|
||||
fileprivate let reserved2: UInt16
|
||||
var clientStartTime: SMBTime {
|
||||
let time = UInt64(contextOffset) + (UInt64(contextCount) << 32) + (UInt64(contextCount) << 48)
|
||||
let lo = Int64(contextOffset)
|
||||
let hi1 = Int64(contextCount) << 32
|
||||
let hi2 = Int64(contextCount) << 48
|
||||
let time: Int64 = lo + hi1 + hi2
|
||||
return SMBTime(time: time)
|
||||
}
|
||||
|
||||
init(singing: NegotiateSinging = [.ENABLED], capabilities: GlobalCapabilities, guid: uuid_t? = nil, clientStartTime: SMBTime? = nil) {
|
||||
init(capabilities: GlobalCapabilities, clientStartTime: SMBTime? = nil, guid: uuid_t? = nil, signing: NegotiateSinging = [.ENABLED]) {
|
||||
self.size = 36
|
||||
self.dialectCount = 0
|
||||
self.singing = singing
|
||||
self.signing = signing
|
||||
self.reserved = 0
|
||||
self.capabilities = capabilities
|
||||
self.guid = guid ?? (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
|
||||
self.guid = guid ?? (0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)
|
||||
if let clientStartTime = clientStartTime {
|
||||
let time = clientStartTime.time
|
||||
self.contextOffset = UInt32(time & 0xffffffff)
|
||||
@@ -85,28 +94,28 @@ extension SMB2 {
|
||||
}
|
||||
}
|
||||
|
||||
struct NegotiateResponse: SMBResponse {
|
||||
struct NegotiateResponse: SMBResponseBody {
|
||||
let header: NegotiateResponse.Header
|
||||
let buffer: NSData?
|
||||
let contexts: [(type: NegotiateContextType, data: NSData)]
|
||||
let buffer: Data?
|
||||
let contexts: [(type: NegotiateContextType, data: Data)]
|
||||
|
||||
init? (data: NSData) {
|
||||
if data.length < 64 {
|
||||
init? (data: Data) {
|
||||
guard data.count >= 64 else {
|
||||
return nil
|
||||
}
|
||||
self.header = decode(data)
|
||||
self.header = data.scanValue()!
|
||||
if Int(header.size) != 65 {
|
||||
return nil
|
||||
}
|
||||
let bufOffset = Int(self.header.bufferOffset) - sizeof(SMB2.Header.self)
|
||||
let bufOffset = Int(self.header.bufferOffset) - MemoryLayout<SMB2.Header>.size
|
||||
let bufLen = Int(self.header.bufferLength)
|
||||
if bufOffset > 0 && bufLen > 0 && data.length >= bufOffset + bufLen {
|
||||
self.buffer = data.subdataWithRange(NSRange(location: bufOffset, length: bufLen))
|
||||
if bufOffset > 0 && bufLen > 0 && data.count >= bufOffset + bufLen {
|
||||
self.buffer = data.subdata(in: bufOffset..<(bufOffset + bufLen))
|
||||
} else {
|
||||
self.buffer = nil
|
||||
}
|
||||
let contextCount = Int(self.header.contextCount)
|
||||
let contextOffset = Int(self.header.contextOffset) - sizeof(SMB2.Header.self)
|
||||
let contextOffset = Int(self.header.contextOffset) - MemoryLayout<SMB2.Header>.size
|
||||
if contextCount > 0 && contextOffset > 0 {
|
||||
// TODO: NegotiateResponse context support for SMB3
|
||||
self.contexts = []
|
||||
@@ -133,7 +142,7 @@ extension SMB2 {
|
||||
}
|
||||
}
|
||||
|
||||
struct NegotiateSinging: OptionSetType {
|
||||
struct NegotiateSinging: OptionSet {
|
||||
let rawValue: UInt16
|
||||
|
||||
init(rawValue: UInt16) {
|
||||
@@ -143,7 +152,7 @@ extension SMB2 {
|
||||
static let REQUIRED = NegotiateSinging(rawValue: 0x0002)
|
||||
}
|
||||
|
||||
struct NegotiateContextType: OptionSetType {
|
||||
struct NegotiateContextType: OptionSet {
|
||||
let rawValue: UInt16
|
||||
|
||||
init(rawValue: UInt16) {
|
||||
@@ -153,7 +162,7 @@ extension SMB2 {
|
||||
static let ENCRYPTION_CAPABILITIES = NegotiateContextType(rawValue: 0x0002)
|
||||
}
|
||||
|
||||
struct GlobalCapabilities: OptionSetType {
|
||||
struct GlobalCapabilities: OptionSet {
|
||||
let rawValue: UInt32
|
||||
|
||||
init(rawValue: UInt32) {
|
||||
@@ -170,22 +179,29 @@ extension SMB2 {
|
||||
|
||||
// MARK: SMB2 Session Setup
|
||||
|
||||
struct SessionSetupRequest: SMBRequest {
|
||||
let header: SessionSetupRequest.Header
|
||||
let buffer: NSData?
|
||||
struct SessionSetupRequest: SMBRequestBody {
|
||||
static var command: SMB2.Command = .SESSION_SETUP
|
||||
|
||||
init(header: SessionSetupRequest.Header, buffer: NSData) {
|
||||
let header: SessionSetupRequest.Header
|
||||
let buffer: Data?
|
||||
|
||||
init(header: SessionSetupRequest.Header, buffer: Data) {
|
||||
self.header = header
|
||||
self.buffer = buffer
|
||||
}
|
||||
|
||||
func data() -> NSData {
|
||||
init(sessionId: UInt64 = 0, flags: SessionSetupRequest.Flags = [], singing: SessionSetupSinging = [.ENABLED], capabilities: GlobalCapabilities = [], securityData: Data? = nil) {
|
||||
self.header = Header(sessionId: sessionId, flags: flags, singing: singing, capabilities: capabilities)
|
||||
self.buffer = securityData
|
||||
}
|
||||
|
||||
func data() -> Data {
|
||||
var header = self.header
|
||||
header.bufferOffset = UInt16(sizeof(SMB2.Header.self) + sizeof(SessionSetupRequest.Header.self))
|
||||
header.bufferLength = UInt16(buffer?.length ?? 0)
|
||||
let result = NSMutableData(data: encode(&header))
|
||||
header.bufferOffset = UInt16(MemoryLayout<SMB2.Header>.size + MemoryLayout<SessionSetupRequest.Header>.size)
|
||||
header.bufferLength = UInt16(buffer?.count ?? 0)
|
||||
var result = Data(value: header)
|
||||
if let buffer = self.buffer {
|
||||
result.appendData(buffer)
|
||||
result.append(buffer)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -195,7 +211,7 @@ extension SMB2 {
|
||||
let flags: SessionSetupRequest.Flags
|
||||
let signing: SessionSetupSinging
|
||||
let capabilities: GlobalCapabilities
|
||||
private let channel: UInt32
|
||||
fileprivate let channel: UInt32
|
||||
var bufferOffset: UInt16
|
||||
var bufferLength: UInt16
|
||||
let sessionId: UInt64
|
||||
@@ -213,7 +229,7 @@ extension SMB2 {
|
||||
}
|
||||
|
||||
/// Works the client implements the SMB 3.x dialect family
|
||||
struct Flags: OptionSetType {
|
||||
struct Flags: OptionSet {
|
||||
let rawValue: UInt8
|
||||
|
||||
init(rawValue: UInt8) {
|
||||
@@ -224,22 +240,22 @@ extension SMB2 {
|
||||
}
|
||||
}
|
||||
|
||||
struct SessionSetupResponse: SMBResponse {
|
||||
struct SessionSetupResponse: SMBResponseBody {
|
||||
let header: SessionSetupResponse.Header
|
||||
let buffer: NSData?
|
||||
let buffer: Data?
|
||||
|
||||
init? (data: NSData) {
|
||||
if data.length < 64 {
|
||||
init? (data: Data) {
|
||||
guard data.count >= 64 else {
|
||||
return nil
|
||||
}
|
||||
self.header = decode(data)
|
||||
self.header = data.scanValue()!
|
||||
if Int(header.size) != 9 {
|
||||
return nil
|
||||
}
|
||||
let bufOffset = Int(self.header.bufferOffset) - sizeof(SMB2.Header.self)
|
||||
let bufOffset = Int(self.header.bufferOffset) - MemoryLayout<SMB2.Header>.size
|
||||
let bufLen = Int(self.header.bufferLength)
|
||||
if bufOffset > 0 && bufLen > 0 && data.length >= bufOffset + bufLen {
|
||||
self.buffer = data.subdataWithRange(NSRange(location: bufOffset, length: bufLen))
|
||||
if bufOffset > 0 && bufLen > 0 && data.count >= bufOffset + bufLen {
|
||||
self.buffer = data.subdata(in: bufOffset..<(bufOffset + bufLen))
|
||||
} else {
|
||||
self.buffer = nil
|
||||
}
|
||||
@@ -252,7 +268,7 @@ extension SMB2 {
|
||||
let bufferLength: UInt16
|
||||
}
|
||||
|
||||
struct Flags: OptionSetType {
|
||||
struct Flags: OptionSet {
|
||||
let rawValue: UInt16
|
||||
|
||||
init(rawValue: UInt16) {
|
||||
@@ -265,20 +281,22 @@ extension SMB2 {
|
||||
}
|
||||
}
|
||||
|
||||
struct SessionSetupSinging: OptionSetType {
|
||||
struct SessionSetupSinging: OptionSet {
|
||||
let rawValue: UInt8
|
||||
|
||||
init(rawValue: UInt8) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
static let ENABLED = NegotiateSinging(rawValue: 0x01)
|
||||
static let REQUIRED = NegotiateSinging(rawValue: 0x02)
|
||||
static let ENABLED = SessionSetupSinging(rawValue: 0x01)
|
||||
static let REQUIRED = SessionSetupSinging(rawValue: 0x02)
|
||||
}
|
||||
|
||||
// MARK: SMB2 Log off
|
||||
|
||||
struct LogOff: SMBRequest, SMBResponse {
|
||||
struct LogOff: SMBRequestBody, SMBResponseBody {
|
||||
static var command: SMB2.Command = .LOGOFF
|
||||
|
||||
let size: UInt16
|
||||
let reserved: UInt16
|
||||
|
||||
@@ -286,19 +304,13 @@ extension SMB2 {
|
||||
self.size = 4
|
||||
self.reserved = 0
|
||||
}
|
||||
|
||||
init? (data: NSData) {
|
||||
self = decode(data)
|
||||
}
|
||||
|
||||
func data() -> NSData {
|
||||
return encode(self)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: SMB2 Echo
|
||||
|
||||
struct Echo: SMBRequest, SMBResponse {
|
||||
struct Echo: SMBRequestBody, SMBResponseBody {
|
||||
static var command: SMB2.Command = .ECHO
|
||||
|
||||
let size: UInt16
|
||||
let reserved: UInt16
|
||||
|
||||
@@ -306,13 +318,5 @@ extension SMB2 {
|
||||
self.size = 4
|
||||
self.reserved = 0
|
||||
}
|
||||
|
||||
init? (data: NSData) {
|
||||
self = decode(data)
|
||||
}
|
||||
|
||||
func data() -> NSData {
|
||||
return encode(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,5 +10,35 @@ import Foundation
|
||||
|
||||
extension SMB2 {
|
||||
// MARK: SMB2 Set Info
|
||||
struct SetInfoRequest: SMBRequestBody {
|
||||
static var command: SMB2.Command = .SET_INFO
|
||||
|
||||
let header: Header
|
||||
let buffer: Data?
|
||||
|
||||
func data() -> Data {
|
||||
var result = Data(value: header)
|
||||
result.append(buffer ?? Data())
|
||||
return result
|
||||
}
|
||||
|
||||
struct Header {
|
||||
let size: UInt16 = 33
|
||||
let infoType: UInt8
|
||||
fileprivate let infoClass: UInt8
|
||||
let bufferLength: UInt32
|
||||
let bufferOffset: UInt16
|
||||
fileprivate let reserved: UInt16
|
||||
let securityInfo: FileSecurityInfo
|
||||
let fileId: FileId
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
struct SetInfoResponse: SMBResponseBody {
|
||||
let size: UInt16
|
||||
|
||||
init() {
|
||||
self.size = 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,32 +11,34 @@ import Foundation
|
||||
extension SMB2 {
|
||||
// MARK: SMB2 Tree Connect
|
||||
|
||||
struct TreeConnectRequest: SMBRequest {
|
||||
struct TreeConnectRequest: SMBRequestBody {
|
||||
static var command: SMB2.Command = .TREE_CONNECT
|
||||
|
||||
let header: TreeConnectRequest.Header
|
||||
let buffer: NSData?
|
||||
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) {
|
||||
guard !host.containsString("/") && !host.containsString("/") && !share.containsString("/") && !share.containsString("/") else {
|
||||
guard !host.contains("/") && !share.contains("/") else {
|
||||
return nil
|
||||
}
|
||||
self.header = header
|
||||
let path = "\\\\\(host)\\\(share)"
|
||||
self.buffer = path.dataUsingEncoding(NSUTF8StringEncoding)
|
||||
let path = "\\\\" + host + "\\" + share
|
||||
self.buffer = path.data(using: .utf16)
|
||||
}
|
||||
|
||||
func data() -> NSData {
|
||||
func data() -> Data {
|
||||
var header = self.header
|
||||
header.pathOffset = UInt16(sizeof(SMB2.Header.self) + sizeof(TreeConnectRequest.Header.self))
|
||||
header.pathLength = UInt16(buffer?.length ?? 0)
|
||||
let result = NSMutableData(data: encode(&header))
|
||||
header.pathOffset = UInt16(MemoryLayout<SMB2.Header>.size + MemoryLayout<TreeConnectRequest.Header>.size)
|
||||
header.pathLength = UInt16(buffer?.count ?? 0)
|
||||
var result = Data(value: header)
|
||||
if let buffer = self.buffer {
|
||||
result.appendData(buffer)
|
||||
result.append(buffer)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -55,7 +57,7 @@ extension SMB2 {
|
||||
}
|
||||
}
|
||||
|
||||
struct Flags: OptionSetType {
|
||||
struct Flags: OptionSet {
|
||||
let rawValue: UInt16
|
||||
|
||||
init(rawValue: UInt16) {
|
||||
@@ -66,24 +68,17 @@ extension SMB2 {
|
||||
}
|
||||
}
|
||||
|
||||
struct TreeConnectResponse: SMBResponse {
|
||||
struct TreeConnectResponse: SMBResponseBody {
|
||||
let size: UInt16 // = 16
|
||||
private let _type: UInt8
|
||||
fileprivate let _type: UInt8
|
||||
var type: ShareType {
|
||||
return ShareType(rawValue: _type) ?? .UNKNOWN
|
||||
}
|
||||
private let reserved: UInt8
|
||||
fileprivate let reserved: UInt8
|
||||
let flags: TreeConnectResponse.ShareFlags
|
||||
let capabilities: TreeConnectResponse.Capabilities
|
||||
let maximalAccess: FileAccessMask
|
||||
|
||||
init? (data: NSData) {
|
||||
if data.length != 16 {
|
||||
return nil
|
||||
}
|
||||
self = decode(data)
|
||||
}
|
||||
|
||||
enum ShareType: UInt8 {
|
||||
case UNKNOWN = 0x00
|
||||
case DISK = 0x01
|
||||
@@ -91,7 +86,7 @@ extension SMB2 {
|
||||
case PRINT = 0x03
|
||||
}
|
||||
|
||||
struct ShareFlags: OptionSetType {
|
||||
struct ShareFlags: OptionSet {
|
||||
let rawValue: UInt32
|
||||
|
||||
init(rawValue: UInt32) {
|
||||
@@ -114,7 +109,7 @@ extension SMB2 {
|
||||
static let ENCRYPT_DATA = ShareFlags(rawValue: 0x00008000)
|
||||
}
|
||||
|
||||
struct Capabilities: OptionSetType {
|
||||
struct Capabilities: OptionSet {
|
||||
let rawValue: UInt32
|
||||
|
||||
init(rawValue: UInt32) {
|
||||
@@ -131,7 +126,9 @@ extension SMB2 {
|
||||
|
||||
// MARK: SMB2 Tree Disconnect
|
||||
|
||||
struct TreeDisconnect: SMBRequest, SMBResponse {
|
||||
struct TreeDisconnect: SMBRequestBody, SMBResponseBody {
|
||||
static var command: SMB2.Command = .TREE_DISCONNECT
|
||||
|
||||
let size: UInt16
|
||||
let reserved: UInt16
|
||||
|
||||
@@ -139,13 +136,5 @@ extension SMB2 {
|
||||
self.size = 4
|
||||
self.reserved = 0
|
||||
}
|
||||
|
||||
init? (data: NSData) {
|
||||
self = decode(data)
|
||||
}
|
||||
|
||||
func data() -> NSData {
|
||||
return encode(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol FileProviderSMBHeader {
|
||||
var protocolID: UInt32 { get }
|
||||
static var protocolConst: UInt32 { get }
|
||||
}
|
||||
|
||||
// SMB2 Types
|
||||
struct SMB2 {
|
||||
struct Header: FileProviderSMBHeader { // 64 bytes
|
||||
@@ -17,25 +22,22 @@ 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
|
||||
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))
|
||||
}
|
||||
private 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
|
||||
let messageId: UInt64
|
||||
private let reserved: UInt32
|
||||
fileprivate let reserved: UInt32
|
||||
let treeId: UInt32
|
||||
var asyncId: UInt64 {
|
||||
get {
|
||||
@@ -45,11 +47,12 @@ struct SMB2 {
|
||||
let sessionId: UInt64
|
||||
let signature: (UInt64, UInt64)
|
||||
|
||||
// codebeat:disable[ARITY]
|
||||
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 = self.dynamicType.protocolConst
|
||||
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
|
||||
@@ -62,10 +65,10 @@ 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 = self.dynamicType.protocolConst
|
||||
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])
|
||||
@@ -76,9 +79,10 @@ struct SMB2 {
|
||||
self.sessionId = sessionId
|
||||
self.signature = signature
|
||||
}
|
||||
// codebeat:enable[ARITY]
|
||||
}
|
||||
|
||||
struct Flags: OptionSetType {
|
||||
struct Flags: OptionSet {
|
||||
var rawValue: UInt32
|
||||
|
||||
init(rawValue: UInt32) {
|
||||
@@ -98,34 +102,41 @@ struct SMB2 {
|
||||
static let ASYNC_COMMAND = Flags(rawValue: 0x00000002)
|
||||
static let RELATED_OPERATIONS = Flags(rawValue: 0x00000004)
|
||||
static let SIGNED = Flags(rawValue: 0x00000008)
|
||||
private static let PRIORITY_MASK = Flags(rawValue: 0x00000070)
|
||||
fileprivate static let PRIORITY_MASK = Flags(rawValue: 0x00000070)
|
||||
static let DFS_OPERATIONS = Flags(rawValue: 0x10000000)
|
||||
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
|
||||
|
||||
|
||||
}
|
||||
|
||||
+161
-155
@@ -10,199 +10,205 @@ import Foundation
|
||||
|
||||
/// Error Types and Description
|
||||
|
||||
public enum NTStatus: UInt32, ErrorType, 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 {
|
||||
case NOT_IMPLEMENTED, INVALID_DEVICE_REQUEST, ILLEGAL_FUNCTION:
|
||||
case .NOT_IMPLEMENTED, .INVALID_DEVICE_REQUEST, .ILLEGAL_FUNCTION:
|
||||
return "Invalid Function."
|
||||
case NO_SUCH_FILE, NO_SUCH_DEVICE, OBJECT_NAME_NOT_FOUND:
|
||||
case .NO_SUCH_FILE, .NO_SUCH_DEVICE, .OBJECT_NAME_NOT_FOUND:
|
||||
return "File not found."
|
||||
case OBJECT_PATH_INVALID, OBJECT_PATH_NOT_FOUND, OBJECT_PATH_SYNTAX_BAD, DFS_EXIT_PATH_FOUND, REDIRECTOR_NOT_STARTED:
|
||||
case .OBJECT_PATH_INVALID, .OBJECT_PATH_NOT_FOUND, .OBJECT_PATH_SYNTAX_BAD, .DFS_EXIT_PATH_FOUND, .REDIRECTOR_NOT_STARTED:
|
||||
return "A component in the path prefix is not a directory."
|
||||
case TOO_MANY_OPENED_FILES:
|
||||
case .TOO_MANY_OPENED_FILES:
|
||||
return "Too many open files. No FIDs are available."
|
||||
case ACCESS_DENIED, INVALID_LOCK_SEQUENCE, INVALID_VIEW_SIZE, ALREADY_COMMITTED, PORT_CONNECTION_REFUSED, THREAD_IS_TERMINATING, DELETE_PENDING, PRIVILEGE_NOT_HELD, LOGON_FAILURE, FILE_IS_A_DIRECTORY, FILE_RENAMED, PROCESS_IS_TERMINATING, CANNOT_DELETE, FILE_DELETED:
|
||||
case .ACCESS_DENIED, .INVALID_LOCK_SEQUENCE, .INVALID_VIEW_SIZE, .ALREADY_COMMITTED, .PORT_CONNECTION_REFUSED, .THREAD_IS_TERMINATING, .DELETE_PENDING, .PRIVILEGE_NOT_HELD, .LOGON_FAILURE, .FILE_IS_A_DIRECTORY, .FILE_RENAMED, .PROCESS_IS_TERMINATING, .CANNOT_DELETE, .FILE_DELETED:
|
||||
return "Access denied."
|
||||
case SMB_BAD_FID, INVALID_HANDLE, OBJECT_TYPE_MISMATCH, PORT_DISCONNECTED, INVALID_PORT_HANDLE, FILE_CLOSED, HANDLE_NOT_CLOSABLE:
|
||||
case .SMB_BAD_FID, .INVALID_HANDLE, .OBJECT_TYPE_MISMATCH, .PORT_DISCONNECTED, .INVALID_PORT_HANDLE, .FILE_CLOSED, .HANDLE_NOT_CLOSABLE:
|
||||
return "Invalid FID."
|
||||
case SECTION_TOO_BIG, TOO_MANY_PAGING_FILES, INSUFF_SERVER_RESOURCES:
|
||||
case .SECTION_TOO_BIG, .TOO_MANY_PAGING_FILES, .INSUFF_SERVER_RESOURCES:
|
||||
return "Insufficient server memory to perform the requested operation."
|
||||
case OS2_INVALID_ACCESS:
|
||||
case .OS2_INVALID_ACCESS:
|
||||
return "Invalid open mode."
|
||||
case DATA_ERROR:
|
||||
case .DATA_ERROR:
|
||||
return "Bad data. (May be generated by IOCTL calls on the server.)"
|
||||
case DIRECTORY_NOT_EMPTY:
|
||||
case .DIRECTORY_NOT_EMPTY:
|
||||
return "Remove of directory failed because it was not empty."
|
||||
case NOT_SAME_DEVICE:
|
||||
case .NOT_SAME_DEVICE:
|
||||
return "A file system operation (such as a rename) across two devices was attempted."
|
||||
case NO_MORE_FILES:
|
||||
case .NO_MORE_FILES:
|
||||
return "No (more) files found following a file search command."
|
||||
case UNSUCCESSFUL:
|
||||
case .UNSUCCESSFUL:
|
||||
return "General error."
|
||||
case SHARING_VIOLATION:
|
||||
case .SHARING_VIOLATION:
|
||||
return "Sharing violation. A requested open mode conflicts with the sharing mode of an existing file handle."
|
||||
case FILE_LOCK_CONFLICT, LOCK_NOT_GRANTED:
|
||||
case .FILE_LOCK_CONFLICT, .LOCK_NOT_GRANTED:
|
||||
return "A lock request specified an invalid locking mode, or conflicted with an existing file lock."
|
||||
case END_OF_FILE:
|
||||
case .END_OF_FILE:
|
||||
return "Attempted to read beyond the end of the file."
|
||||
case NOT_SUPPORTED:
|
||||
case .NOT_SUPPORTED:
|
||||
return "This command is not supported by the server."
|
||||
case OBJECT_NAME_COLLISION:
|
||||
case .OBJECT_NAME_COLLISION:
|
||||
return "An attempt to create a file or directory failed because an object with the same pathname already exists."
|
||||
case INVALID_PARAMETER:
|
||||
case .INVALID_PARAMETER:
|
||||
return "A parameter supplied with the message is invalid."
|
||||
case OS2_INVALID_LEVEL:
|
||||
case .OS2_INVALID_LEVEL:
|
||||
return "Invalid information level."
|
||||
case OS2_NEGATIVE_SEEK:
|
||||
case .OS2_NEGATIVE_SEEK:
|
||||
return "An attempt was made to seek to a negative absolute offset within a file."
|
||||
case RANGE_NOT_LOCKED:
|
||||
case .RANGE_NOT_LOCKED:
|
||||
return "The byte range specified in an unlock request was not locked."
|
||||
case OS2_NO_MORE_SIDS:
|
||||
case .OS2_NO_MORE_SIDS:
|
||||
return "Maximum number of searches has been exhausted."
|
||||
case OS2_CANCEL_VIOLATION:
|
||||
case .OS2_CANCEL_VIOLATION:
|
||||
return "No lock request was outstanding for the supplied cancel region."
|
||||
case OS2_ATOMIC_LOCKS_NOT_SUPPORTED:
|
||||
case .OS2_ATOMIC_LOCKS_NOT_SUPPORTED:
|
||||
return "The file system does not support atomic changes to the lock type."
|
||||
case INVALID_INFO_CLASS, INVALID_PIPE_STATE, INVALID_READ_MODE:
|
||||
case .INVALID_INFO_CLASS, .INVALID_PIPE_STATE, .INVALID_READ_MODE:
|
||||
return "Invalid named pipe."
|
||||
case OS2_CANNOT_COPY:
|
||||
case .OS2_CANNOT_COPY:
|
||||
return "The copy functions cannot be used."
|
||||
case INSTANCE_NOT_AVAILABLE, PIPE_NOT_AVAILABLE, PIPE_BUSY:
|
||||
case .INSTANCE_NOT_AVAILABLE, .PIPE_NOT_AVAILABLE, .PIPE_BUSY:
|
||||
return "All instances of the designated named pipe are busy."
|
||||
case PIPE_CLOSING, PIPE_EMPTY:
|
||||
case .PIPE_CLOSING, .PIPE_EMPTY:
|
||||
return "The designated named pipe is in the process of being closed."
|
||||
case PIPE_DISCONNECTED:
|
||||
case .PIPE_DISCONNECTED:
|
||||
return "The designated named pipe exists, but there is no server process listening on the server side."
|
||||
case BUFFER_OVERFLOW, MORE_PROCESSING_REQUIRED:
|
||||
case .BUFFER_OVERFLOW, .MORE_PROCESSING_REQUIRED:
|
||||
return "There is more data available to read on the designated named pipe."
|
||||
case EA_TOO_LARGE, OS2_EAS_DIDNT_FIT:
|
||||
case .EA_TOO_LARGE, .OS2_EAS_DIDNT_FIT:
|
||||
return "Either there are no extended attributes, or the available extended attributes did not fit into the response."
|
||||
case EAS_NOT_SUPPORTED:
|
||||
case .EAS_NOT_SUPPORTED:
|
||||
return "The server file system does not support Extended Attributes."
|
||||
case OS2_EA_ACCESS_DENIED:
|
||||
case .OS2_EA_ACCESS_DENIED:
|
||||
return "Access to the extended attribute was denied."
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,249 +0,0 @@
|
||||
//
|
||||
// SocketTransmitter.swift
|
||||
// FileProvider
|
||||
//
|
||||
// Created by Amir Abbas Mousavian.
|
||||
// Copyright © 2016 Mousavian. Distributed under MIT license.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public class TCPSocketClient: NSObject, NSStreamDelegate {
|
||||
public static let ports = ["http": 80,
|
||||
"https": 443,
|
||||
"smb": 445,
|
||||
"ftp": 21,
|
||||
"sftp": 22,
|
||||
"sftp": 2121,
|
||||
"telnet": 23,
|
||||
"pop": 110,
|
||||
"smtp": 25,
|
||||
"imap": 143]
|
||||
public static let securePorts = ["https": 443,
|
||||
"smb": 445,
|
||||
"sftp": 22,
|
||||
"sftp": 2121,
|
||||
"telnet": 992,
|
||||
"pop": 995,
|
||||
"smtp": 465,
|
||||
"imap": 993]
|
||||
|
||||
private var inputStream: NSInputStream?
|
||||
private var outputStream: NSOutputStream?
|
||||
private var dataToBeSent: NSMutableData = NSMutableData()
|
||||
/// holds data received from server
|
||||
public let dataReceived: NSMutableData = NSMutableData()
|
||||
/// a url with valid scheme, dns or ip host and ports path and query sections will be neglected
|
||||
public let baseURL: NSURL
|
||||
/// a url with valid scheme, dns or ip host and ports path and query sections will be neglected
|
||||
public let secureConnection: Bool
|
||||
/// server's ports which is value between 1 to 65535
|
||||
private let port: UInt32
|
||||
private var open = false
|
||||
|
||||
/**
|
||||
* - parameter baseURL: a url with valid scheme, dns or ip host and ports
|
||||
* path and query sections will be neglected
|
||||
*
|
||||
* **Note** Call `connect()` to establish connection
|
||||
* - parameter secure: establishing connection using an SSL/TLS connection
|
||||
*/
|
||||
|
||||
public init?(baseURL: NSURL, secure: Bool = false) {
|
||||
self.baseURL = baseURL
|
||||
self.secureConnection = secure
|
||||
let scheme = baseURL.uw_scheme.lowercaseString
|
||||
let defaultPort = secure ? UInt32(TCPSocketClient.securePorts[scheme] ?? 0) : UInt32(TCPSocketClient.ports[scheme] ?? 0)
|
||||
self.port = baseURL.port?.unsignedIntValue ?? defaultPort
|
||||
if self.port == 0 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
disconnect()
|
||||
}
|
||||
|
||||
/**
|
||||
* Establshes a connection to desired server
|
||||
* - returns: A bool value which indicated there where no system error during
|
||||
* creating connection
|
||||
*/
|
||||
|
||||
public func connect() -> Bool {
|
||||
guard let hostStr = baseURL.host else {
|
||||
return false
|
||||
}
|
||||
var readStream : Unmanaged<CFReadStream>?
|
||||
var writeStream : Unmanaged<CFWriteStream>?
|
||||
let host : CFString = NSString(string: hostStr)
|
||||
|
||||
CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, host, self.port, &readStream, &writeStream)
|
||||
inputStream = readStream?.takeRetainedValue()
|
||||
outputStream = writeStream?.takeRetainedValue()
|
||||
|
||||
guard let inputStream = inputStream, outputStream = outputStream else {
|
||||
return false
|
||||
}
|
||||
|
||||
inputStream.delegate = self
|
||||
outputStream.delegate = self
|
||||
|
||||
inputStream.scheduleInRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode)
|
||||
outputStream.scheduleInRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode)
|
||||
|
||||
if secureConnection {
|
||||
inputStream.setProperty(NSStreamSocketSecurityLevelNegotiatedSSL, forKey: NSStreamSocketSecurityLevelKey)
|
||||
outputStream.setProperty(NSStreamSocketSecurityLevelNegotiatedSSL, forKey: NSStreamSocketSecurityLevelKey)
|
||||
}
|
||||
|
||||
inputStream.open()
|
||||
outputStream.open()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminates connection to the server
|
||||
*/
|
||||
|
||||
public func disconnect() {
|
||||
inputStream?.setValue(kCFBooleanTrue, forKey: kCFStreamPropertyShouldCloseNativeSocket as String)
|
||||
outputStream?.setValue(kCFBooleanTrue, forKey: kCFStreamPropertyShouldCloseNativeSocket as String)
|
||||
|
||||
self.inputStream?.close()
|
||||
self.outputStream?.close()
|
||||
|
||||
self.inputStream?.removeFromRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode)
|
||||
self.outputStream?.removeFromRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode)
|
||||
|
||||
self.inputStream?.delegate = nil
|
||||
self.outputStream?.delegate = nil
|
||||
|
||||
self.inputStream = nil
|
||||
self.outputStream = nil
|
||||
}
|
||||
|
||||
public func stream(aStream: NSStream, handleEvent eventCode: NSStreamEvent) {
|
||||
switch (eventCode) {
|
||||
case NSStreamEvent.ErrorOccurred:
|
||||
open = false
|
||||
case NSStreamEvent.EndEncountered:
|
||||
break
|
||||
case NSStreamEvent.None:
|
||||
break
|
||||
case NSStreamEvent.OpenCompleted:
|
||||
let activeStatus: [NSStreamStatus] = [.Open, .Reading, .Writing, .AtEnd]
|
||||
open = activeStatus.contains(inputStream?.streamStatus ?? .NotOpen) && activeStatus.contains(outputStream?.streamStatus ?? .NotOpen)
|
||||
case NSStreamEvent.HasBytesAvailable:
|
||||
var buffer = [UInt8](count: 2048, repeatedValue: 0)
|
||||
if ( aStream == inputStream) {
|
||||
while (inputStream!.hasBytesAvailable ?? false) {
|
||||
let len = inputStream!.read(&buffer, maxLength: buffer.count)
|
||||
if len > 0 {
|
||||
dataReceived.appendBytes(&buffer, length: len)
|
||||
}
|
||||
}
|
||||
}
|
||||
case NSStreamEvent.HasSpaceAvailable:
|
||||
if aStream == outputStream {
|
||||
do {
|
||||
try send(data: nil)
|
||||
} catch _ {
|
||||
NSLog("Sending error")
|
||||
}
|
||||
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends data to server
|
||||
* - parameter data: data which is intended to be sent to server
|
||||
* - throws: NSURLError.NetworkConnectionLost in case of server disconnects disgracefully
|
||||
*/
|
||||
|
||||
public func send(data data: NSData?) throws {
|
||||
guard let outputStream = outputStream else {
|
||||
return
|
||||
}
|
||||
if outputStream.hasSpaceAvailable ?? false {
|
||||
if let data = data {
|
||||
dataToBeSent.appendData(data)
|
||||
}
|
||||
|
||||
if dataToBeSent.length > 0 {
|
||||
let bytesWritten = outputStream.write(UnsafePointer(dataToBeSent.bytes), maxLength: dataToBeSent.length) ?? -1
|
||||
if bytesWritten > 0 {
|
||||
let range = NSRange(location: 0, length: bytesWritten)
|
||||
dataToBeSent.replaceBytesInRange(range, withBytes: nil, length: 0)
|
||||
} else {
|
||||
throw NSError(domain: NSURLErrorDomain, code: NSURLError.NetworkConnectionLost.rawValue, userInfo: nil)
|
||||
}
|
||||
}
|
||||
//println("Sent the following")
|
||||
} else { //steam busy
|
||||
if let data = data {
|
||||
dataToBeSent.appendData(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears entire send and receive buffer
|
||||
*/
|
||||
|
||||
public func flush() {
|
||||
dataToBeSent.length = 0
|
||||
dataReceived.length = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Put's thread in sleep until all data is sent
|
||||
* **Note:** Don't call this method from main thread
|
||||
*/
|
||||
|
||||
internal func waitUntillDataSent() {
|
||||
if NSThread.isMainThread() {
|
||||
assert(false, "waitUntillDataSent() method can't be called from main thread")
|
||||
}
|
||||
while true {
|
||||
if dataToBeSent.length == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
NSRunLoop.currentRunLoop().runUntilDate(NSDate(timeIntervalSinceNow: 0.1));
|
||||
NSThread.sleepForTimeInterval(0.1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Put's thread in sleep until all response from server is loaded into tcp stack
|
||||
* server response can be retrieved by `dataReceived` property
|
||||
* **Note:** Don't call this method from main thread
|
||||
* - returns: A Bool value indicates all response loaded from server successfullt
|
||||
*/
|
||||
|
||||
internal func waitUntilResponse() -> Bool {
|
||||
if NSThread.isMainThread() {
|
||||
assert(false, "waitUntilResponse() method can't be called from main thread")
|
||||
}
|
||||
var finished = false
|
||||
while !finished {
|
||||
switch inputStream?.streamStatus ?? .Error {
|
||||
case .AtEnd:
|
||||
finished = true
|
||||
return true
|
||||
case .Closed, .Error:
|
||||
return false
|
||||
default:
|
||||
finished = false
|
||||
}
|
||||
|
||||
NSRunLoop.currentRunLoop().runUntilDate(NSDate(timeIntervalSinceNow: 0.1));
|
||||
NSThread.sleepForTimeInterval(0.1)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
+581
-447
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,394 @@
|
||||
//
|
||||
// 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 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 result = keyData.withUnsafeMutableBytes {
|
||||
SecRandomCopyBytes(kSecRandomDefault, keyData.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 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)
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
Reference in New Issue
Block a user