Compare commits

..

272 Commits

Author SHA1 Message Date
Amir Abbas 435ed37d93 Updated pod version to 0.21 2017-10-27 09:21:17 +03:30
Amir Abbas 9e4a803345 Removed unnecessary properties from SessionDelegate 2017-10-26 14:07:57 +03:30
Amir Abbas 0e53036855 Added progressive downloading for HTTP-based providers
- Added HEIC/HEIF image thumbnail and properties
- Fixed bug: data task recieve data body called only once
2017-10-26 13:56:41 +03:30
Amir Abbas 87317c1d42 Fixed #61, OneDrive progress availibility 2017-10-13 08:55:55 +03:30
Amir Abbas 4c35e446a7 Fix provider base url (as directory), caused Cloud provider misbehaves 2017-10-13 02:31:31 +03:30
Amir Abbas 47516c63ad Refactored contentsOfDirectory to searchFiles where applicable 2017-10-12 18:30:28 +03:30
Amir Abbas 6cd5b7d8c5 Fixed error description in Dropbox (except uploads)
- Refactors in Dropbox request maker method
2017-10-11 01:14:02 +03:30
Amir Abbas 598444c90f Codebeat GPA improvements 2017-10-10 16:48:55 +03:30
Amir Abbas 370866442a Fixed Compilation error for upload size type 2017-10-10 11:40:32 +03:30
Amir Abbas 9cc9a6a96a Improving documentation
- Renaming OneDriveProvider.SubAddress to Route
- Refactors to improve codebeat score
2017-10-10 11:07:28 +03:30
Amir Abbas 2969af28b8 Small refactors 2017-10-09 13:32:46 +03:30
Amir Abbas d41e86b30a Fixed Swift 3 flatMap error 2017-10-09 11:31:02 +03:30
Amir Abbas aaa1def9e7 Refactored paginated listing in Dropbox and OneDrive
- Removed currentPath
2017-10-09 11:16:17 +03:30
Amir Abbas 024a3637b4 Fix compile time error 2017-10-04 16:32:54 +03:30
Amir Abbas b20fc3efe5 Fix compilation error on swift 3 2017-10-04 15:11:26 +03:30
Amir Abbas 682dd40072 File coordination enabled by default for copyItem(locaURL:) methods 2017-10-04 15:06:27 +03:30
Amir Abbas 3f35e600cd Removing deprecated String.characters usage
- Documentation for using file id in Dropbox & OneDrive
2017-10-04 15:04:23 +03:30
Amir Abbas 9cd2fa2e3a Compilation optimizations, refactoring 2017-10-01 02:04:29 +03:30
Amir Abbas 7be6985454 Updated OneDrive API to new version 2017-09-29 21:39:28 +03:30
Amir Abbas 04ac3e22e7 Refactored correctPath to specified providers 2017-09-29 20:51:09 +03:30
Amir Abbas c8de7fdb69 Fixed NSProgress parenting, iCloud KVO, crash in url(of) 2017-09-29 18:11:26 +03:30
Amir Abbas 42b879d4e2 Fix Swift 3 bug 2017-09-24 15:37:00 +03:30
Amir Abbas 5800c9a2ec Improved compile speed, Fixed Swift 4 warnings 2017-09-24 13:12:31 +03:30
Amir Abbas Mousavian 7c9b2bf09a Merge pull request #65 from yuyan7/fix_webdav
Fix HTTP providers not be overwritten
2017-09-16 20:31:59 +04:30
yuya-nakamura ba08cdda1d Fixed WebDAV not be overwritten 2017-09-16 23:01:57 +09:00
Amir Abbas b380685932 Added VolumeObject for storageProperties method
- Refined error handling in HTTP provider
- Added contentType and hash to OneDriveFileObject
2017-09-05 02:26:09 +04:30
Amir Abbas 5a5beb6891 Refined error handling 2017-09-04 01:22:44 +04:30
Amir Abbas c7201a8be7 Fixed currentPath docs, delegateNotify method default error arg 2017-08-29 01:40:15 +04:30
Amir Abbas d2a03369fd Fixed FTP crash on rucursive remove 2017-08-28 00:06:02 +04:30
Amir Abbas d08098008a Removed FoundationErrorEnum, Refactoed delegate progress routine 2017-08-27 13:48:29 +04:30
Amir Abbas 3c5303214e Fixed Array extension error in HashMAC 2017-08-24 15:08:22 +04:30
Amir Abbas ad8f333d16 Fixed swift 3 compile error on Array extenson 2017-08-24 08:36:11 +04:30
Amir Abbas 5e8f653c52 Fixed compile bug in Swift 3.0 for Array.Iterator.Element 2017-08-23 23:21:17 +04:30
Amir Abbas e4cd7ddf22 Fixed Quality<T> percision 2017-08-23 22:18:18 +04:30
Amir Abbas 1efb0e3fe5 Fixed Quality<T> 2017-08-23 22:15:45 +04:30
Amir Abbas 1292856646 Added convenience methods for URLRequest headers 2017-08-23 22:04:47 +04:30
Amir Abbas e39f9c29ec Merge commit '3d44de0b407c3cd604189cc161dc1fc039409763' 2017-08-23 10:54:59 +04:30
Amir Abbas f43199c22a fixed colon in url string problem across provider 2017-08-23 10:52:45 +04:30
Amir Abbas Mousavian 3d44de0b40 Added missed filePathAllowed const to CharacterSet 2017-08-23 01:22:19 +04:30
Amir Abbas 21b5214481 Deprecate currentPath property, returning error in provider delegate
- Using checkResourceIsReachable to check file exists
- fixed bug: downloading/uploading files with colon in name
2017-08-23 01:19:56 +04:30
Amir Abbas f0b4925db2 Consistancy to delegate call in all contents(of:) methods
- renamed opType instances to operation
- moved to v2 in Dropbox
- removed redundant CloudFileProvider methods
2017-08-20 22:41:53 +04:30
Amir Abbas 0da957d25f Making protocol method implementations overridable 2017-08-19 19:47:54 +04:30
Amir Abbas 0de558c160 Recursive search fixed in FTP, OneDrive, WebDAV 2017-08-19 06:49:15 +04:30
Amir Abbas cad68da076 Refactored Dropbox, WebDAV & OneDrive to HTTPFileProvider
- FileOperationType.source is not optional anymore
- critical bugfix: KVO exception crash on task completion
2017-08-19 05:51:25 +04:30
Amir Abbas Mousavian 39da09edd4 Merge pull request #60 from evilutioner/master
Fixed file downloading progress
2017-08-15 18:53:21 +04:30
Amir Abbas 5129aee1b5 Added fileURL to Progress 2017-08-15 18:41:48 +04:30
Oleg Marchik 478f0819b5 Fixed file downloading progress 2017-08-15 16:57:50 +03:00
Amir Abbas b2a7800f7c Fixed typo 2017-08-15 15:08:06 +04:30
Amir Abbas 96c102d156 Updated readme and podspec to ver 0.19.0 2017-08-15 14:57:36 +04:30
Amir Abbas 8aedd8e72a Replaced OperationHandle with (NS)Progress 2017-08-15 13:42:41 +04:30
Amir Abbas 5b55debe8a Fix warning in OneDrive provider 2017-08-12 17:50:00 +04:30
Amir Abbas Mousavian 0fa062a946 Merge pull request #59 from evilutioner/master
OneDrive: list folder issue fixed, removed escape symbols in path
2017-08-11 20:43:28 +04:30
Oleg Marchik 879f86c1f9 Merge branch 'master' into master 2017-08-11 18:07:25 +03:00
Oleg Marchik 80d5f02bbd OneDrive: list folder issue fixed, removed escape symbols in path 2017-08-11 18:04:18 +03:00
Amir Abbas Mousavian b8a7721b2f Merge pull request #58 from evilutioner/master
Dropbox bug fixing: added unicode symbols support in http header path
2017-08-10 22:29:08 +04:30
Oleg Marchik 44b4784cd3 Important dropbox bug fixing: added unicode symbols support in path 2017-08-10 11:36:15 +03:00
Amir Abbas 7d9e2247f2 Fixed Compile error, removed redundant comments 2017-08-04 00:30:46 +04:30
Amir Abbas Mousavian 0ace562442 Merge pull request #57 from evilutioner/master
Fixed outdated OneDrive API
2017-08-04 00:27:36 +04:30
evilutioner 63d831ef90 Merge branch 'master' into master 2017-08-01 16:08:51 +03:00
Oleg Marchik d6b91348a3 Fixed outdated OneDrive API 2017-08-01 12:55:39 +03:00
Amir Abbas fd9d4c1ab4 Probable fix for # 55 (OneDrive url issue) 2017-07-31 20:10:40 +04:30
Amir Abbas 41e266c2a9 Fix #54(FTP login), Refactored ExtendedLocalFileProvider 2017-07-17 05:36:30 +04:30
Amir Abbas 6127f4a7d9 Fixed Readme errors 2017-07-14 19:28:41 +04:30
Amir Abbas faca943beb Updated Readme fot FTP and SMB description 2017-07-14 16:49:55 +04:30
Amir Abbas 9345436fa2 Enhanced PDF Meta-info, Better FTPS handling 2017-07-14 16:49:24 +04:30
Amir Abbas 6228d88d41 Adjusting readme badges 2017-07-05 21:35:15 +04:30
Amir Abbas 5b395915a9 Updated Readme for Pods and SPM file 2017-07-02 18:26:34 +04:30
Amir Abbas e4f12a502b Fixed repo name and Readme 2017-07-01 16:57:23 +04:30
Amir Abbas 8faad0ec65 Fixed project name in schemes 2017-07-01 14:14:45 +04:30
Amir Abbas bd5569d213 Updated scheme names 2017-07-01 14:09:39 +04:30
Amir Abbas 14ed279879 Renamed framework for compatibility with iOS 11 2017-07-01 13:50:22 +04:30
Amir Abbas fd293f7bdb [Amend] Fixed swift 3.0 compile error for type(of:) 2017-07-01 11:08:49 +04:30
Amir Abbas 3d9625e243 Fixed swift 3.0 compile error for type(of:) 2017-07-01 11:03:22 +04:30
Amir Abbas 0d017ebfc4 Fix #52 (Swift 4.0 source compatibility) 2017-07-01 10:47:48 +04:30
Amir Abbas fd36df67f3 FTP passive property encoding
- removed redundant protocol conforming
2017-07-01 09:43:45 +04:30
Amir Abbas 5be8c4ef5e podspec and version update 2017-06-27 14:02:00 +04:30
Amir Abbas 0374fd7688 Making ftp active data task creation async 2017-06-24 12:09:16 +04:30
Amir Abbas 5ddfa43555 Fix #49 (Workaround colon in url path bug in NSURL relative url) 2017-06-24 12:04:50 +04:30
Amir Abbas 21850bb548 Probable fix for #51 2017-06-24 11:44:12 +04:30
Amir Abbas b166e111e0 FTP Active mode, FTPS bugfix, possible fix for #47 2017-05-25 17:33:56 +04:30
Amir Abbas 1dd7561215 Caching ftp server support for RFC3659, Possible fix for #47 2017-05-23 19:10:04 +04:30
Amir Abbas f94719deb0 Fixes #39 (FTP listing), Error domain determination 2017-05-05 13:02:07 +04:30
Amir Abbas b13df0a977 Fixes #38 (Creating folder on WebDAV) 2017-05-05 10:06:17 +04:30
Amir Abbas 24af7aa4c2 Fixed LocalFileProvider init baseURL issue 2017-04-20 01:27:59 +04:30
Amir Abbas 06039ad993 CloudFileProvider operation handle improvements 2017-04-20 01:06:36 +04:30
Amir Abbas Mousavian d8fec3e346 Merge pull request #37 from hansvdam/master
Making `inProgress` of LocalFileProvider working
2017-04-18 14:22:08 +04:30
Hans van Dam 5c93bc8731 making 'inProgress' of LocalFileProvider work more consistently 2017-04-18 11:03:35 +02:00
Hans van Dam 61ba245189 making 'inProgress' of LocalFileProvider work 2017-04-17 18:16:04 +02:00
Amir Abbas 02e6cd37dd WebDAV OAuth 1,2 support 2017-04-16 19:19:17 +04:30
Amir Abbas 55608fb8d0 New FileProviderSharing protocol for publicLinks
- fixed DropboxFileProvider.propertiesOfFile() bug
- minor URLRequest refactors
- Added documentation and scope declaration
2017-04-16 19:07:45 +04:30
Amir Abbas 3e3582f6fa Changed DropboxFileProvider.type constant to “Dropbox” 2017-04-15 21:31:47 +04:30
Amir Abbas dd7a9d20b6 Fixed macOS build error 2017-04-14 23:26:40 +04:30
Amir Abbas d4a9b4a34f Fixed Dropbox thumbnail issue 2017-04-14 22:07:35 +04:30
Amir Abbas 34c663e62c FileObject.url is unwraped. fixed url initializing from path 2017-04-14 18:57:50 +04:30
Amir Abbas 1415dda987 Fixed #36 (FTP Uploading bug) 2017-04-14 18:55:07 +04:30
Amir Abbas cd465c1288 Fixes #35 (Dropbox handlers), Fixed FTP response reading length 2017-04-14 11:15:09 +04:30
Amir Abbas a605b0cd85 Added FTP Recursive listing, Implemented ftp search
- Implemented FTP full directory remove
- Fixed FTP STOR bug
- Implemented relativePath(of:) for FTP
- Initial implementation of FTP active mode
2017-04-11 22:21:13 +04:30
Amir Abbas bf7043de29 Fixed relativePath(of:) bug, made it overridable
- Fixed StreamTask.taskDescription bug
2017-04-11 19:43:34 +04:30
Amir Abbas ff5e13931f Fixed WebDAVProvider.contents bug, refactored FTP Error 2017-04-09 14:09:03 +04:30
Amir Abbas 75af738d2e Made SessionDelegate init public, fixed pod issue 2017-04-05 02:05:28 +04:30
Amir Abbas f54a1253e4 Throwing error when trying to upload a directory 2017-04-05 00:24:24 +04:30
Amir Abbas 1394a92662 Add Documentation 2017-04-03 21:03:48 +04:30
Amir Abbas dab171c755 Setting sessionDelgate credential to updated one. 2017-04-03 18:59:50 +04:30
Amir Abbas ea5de2e2aa Added progress for content(path:) method
- Fixed issue with colliding handlers between sessions.
- Sessions can be set.
- SessionDelegate class is now public.
2017-04-03 18:50:13 +04:30
Amir Abbas 2253cca086 Fixed deprecated URLResourceKey items 2017-04-03 12:52:27 +04:30
Amir Abbas e15a900ade Renamed URLResourceKey additions to have Key prefix 2017-04-03 12:49:35 +04:30
Amir Abbas 5c2c56c44c Fixed: Calling completion handler for upload task
- Added including (file object properties) argument to WebDAV provider (resolves #31)
2017-04-03 12:41:11 +04:30
Amir Abbas Mousavian ff4bbdf0de Updated readme and podspec for FTP, minor fixes. 2017-04-01 14:56:20 +04:30
Amir Abbas Mousavian 163a218ac2 Fixed operation progress (in delegate) for all remote providers
- Now also compatible with background session
- added delegate notify (success, progress, failure) to FTP
- added `FTPFileProvider.useAppleImplementation`, allows developer choose to use apple download task instead of custom implementation
- enabling to download non-text files in FTP
- implementation of `url(of:) in  FTP
- various fixes in error reporting
- disabled closing streams in ftpQuit() due to crash
2017-04-01 01:33:56 +04:30
Amir Abbas Mousavian 81401ee36f Added Documentation, refactors of related Date methods 2017-03-31 10:15:39 +04:30
Amir Abbas Mousavian 759ba3c7bf Added AEXML license file 2017-03-31 00:49:08 +04:30
Amir Abbas Mousavian f5c8f6308b FileProviderStreamTask (URLSessionStreakTask replica) is now public
- closing streams after ftpQuit() executed.
- added ability to add FTP Active Mode.
2017-03-30 23:49:28 +04:30
Amir Abbas Mousavian 4dbb0adb18 FTP better error handling 2017-03-30 13:16:03 +04:30
Amir Abbas Mousavian bf62d585fd implemented FTP copy fallback, FTPS manual auth 2017-03-30 10:41:14 +04:30
Amir Abbas Mousavian f21f658874 Deprecated create(file:) method, replaced by writeContents()
- RemoteOperationHandle now retains task
- FTP provider returns correct operation handle task
2017-03-29 23:03:04 +04:30
Amir Abbas e6eba3d198 Fixed macOS and tvOS build 2017-03-29 10:13:47 +04:30
Amir Abbas 6959a14dc1 Fixed compiler error, closing streams in FTP provider 2017-03-29 09:29:22 +04:30
Amir Abbas 29a9e0fb82 Initial implementation of FTP
- ftp copy and search is not implemented
2017-03-29 04:17:03 +04:30
Amir Abbas Mousavian c7b4e1f124 Ensure baseURL is absolute, fixed warnings for Swift 3.1 2017-03-28 19:25:41 +04:30
Amir Abbas Mousavian 99a433a0fc Credential is open to set anytime 2017-03-26 15:54:55 +04:30
Amir Abbas Mousavian 4023fbc62e Fixed: WebDAV file listing omit files contains space in name 2017-03-26 02:03:43 +04:30
Amir Abbas Mousavian 1045901d7c Added NSCoding support
- Better relative path handling in WebDAV
- obsolete deprecated methods
2017-03-25 19:19:49 +04:30
Amir Abbas 528d5eebc3 Fixed relativePath(of:) crash 2017-03-18 15:58:44 +03:30
Amir Abbas 079f8f4b77 Refactored methods to extensions 2017-03-17 15:52:58 +03:30
Amir Abbas e12f386a9d Refactored DispatchTime, better ExposureTime calculation 2017-03-11 03:02:02 +03:30
Amir Abbas 38e217bc19 Better LocalFileObject initialization with empty path 2017-03-09 17:34:00 +03:30
Amir Abbas 0b41abd4ef Optimized PDF thumbnail/meta handling
- Fixed ISO speed and GPS Area image meta
- Fixed Dropbox `name ! BEGINSWiTH %` search query
2017-03-01 13:28:44 +03:30
Amir Abbas aa781adeb2 Fixed searchFiles() from string, fixing “BEGINSWITH” typo 2017-02-24 16:53:59 +03:30
Amir Abbas d61e51ba1c Fixes #29 (WebDAV authentication), minor lints/optimiziations 2017-02-24 16:24:34 +03:30
Amir Abbas cdff7db32e Fixed OneDriveProvider bugs
- fixed and enhanced searching files in Dropbox
2017-02-21 00:48:59 +03:30
Amir Abbas 9533a0e3c9 Removed redundant isPathRelative property. Now is always true.
- Note: Check documentation to workaround
- Improvement: Disabling `LocalFileProviderMonitor` while handler is running
2017-02-20 00:14:55 +03:30
Amir Abbas 194673b3b6 Added NSPredicate to searchFiles method
- public functions became open, now is overridable
- fixed urlCache documentation
2017-02-19 13:34:55 +03:30
Amir Abbas 194c8a41aa Fixed readme 2017-02-18 09:08:55 +03:30
Amir Abbas Mousavian c290377433 Updated Readme, travis.yml 2017-02-16 20:56:24 +03:30
Amir Abbas Mousavian 330a22c45d Completed Documentation, fixed a small bug. 2017-02-16 13:00:50 +03:30
Amir Abbas e899804e28 Added isReachable method to test connectivity
- Fixed Cloud provider bug when moving/deleting evicted file
2017-02-14 22:58:07 +03:30
Amir Abbas 489a9a16d0 Updated documentation, fixed Cloud provider paths bugs 2017-02-14 12:24:06 +03:30
Amir Abbas 93359d5173 Made FileProviders equatable 2017-02-13 16:45:34 +03:30
Amir Abbas 12ff3a410d Fixed critical bug in LocalFileObject when decoding path 2017-02-13 16:31:29 +03:30
Amir Abbas a7de09362b Equatable FileObject, async completion handler in cloud provider 2017-02-13 12:06:09 +03:30
Amir Abbas b1fe37cf2a Updated Readme, fixed several bugs
- non-locking snapshot of file while uploading to iCloud
- fixed: `CloudFIleProvider.doOperation()` couldn’t handle local urls
- fixed: FileProvider.type didn’t work correctly
2017-02-12 22:45:30 +03:30
Amir Abbas bd59eacee2 Fixed bugs in providers and LocalFileObject initializing 2017-02-12 03:07:40 +03:30
Amir Abbas 42579be371 Added FileProviderHTTPError protocol
- All errors from local provider is now from Cocoa error domain. No URL domain anymore.
- All errors from WebDAV/OneDrive/Dropbox conform to FileProviderHTTPError
2017-02-11 18:05:21 +03:30
Amir Abbas edd4914ca7 Linting, fixed travis wrong device 2017-02-11 06:08:17 +03:30
Amir Abbas bd3e1bd74f Fixed typos in travis.yml 2017-02-10 02:57:15 +03:30
Amir Abbas 371c481482 Fixed project name typo in travis.yml 2017-02-09 15:02:39 +03:30
Amir Abbas dea1d01bf3 Made travis.yml more generic 2017-02-09 14:46:29 +03:30
Amir Abbas ae4cd1dff3 Fixed undo manager bugs
- Fixed contents of file with offset method bugs
- Improved `LocalFileProvider.copy()` implementation
2017-02-09 03:02:21 +03:30
Amir Abbas 72520973e9 Changed FileObject.allValues key type to URLResourceKey
- LocalFileProvider.storageProperties method now uses url resource values
2017-02-08 07:30:41 +03:30
Amir Abbas 63016285af Added UndoManager support to Local provider
- Refactored Local provider operations into one function
2017-02-08 04:54:18 +03:30
Amir Abbas 3245c9df03 Updated travis.yml 2017-02-07 21:59:17 +03:30
Amir Abbas ee1fa89747 Updated travis.yml 2017-02-07 12:07:59 +03:30
Amir Abbas 4a6b25deac Updated travis.yml 2017-02-07 09:48:12 +03:30
Amir Abbas 2423cbd0f6 Updated travis.yml 2017-02-07 02:55:22 +03:30
Amir Abbas 480099ca8a Updated travis.yml 2017-02-06 19:41:57 +03:30
Amir Abbas 755bf6bf84 Updated travis.yaml 2017-02-06 19:38:14 +03:30
Amir Abbas 525825ff5d Fixed Cloud provider listing (RunLoop) bug
- Added ubiquity scope to Cloud provider
- Renamed `temporaryLink` to `publicLink`
- Added deprecated renaming guide
2017-02-06 18:03:58 +03:30
Amir Abbas Mousavian a35384dc31 Refactoring: reducing complexity of properties methods 2017-02-03 23:15:23 +03:30
Amir Abbas Mousavian 0c558fdbb4 Updated version 2017-02-03 18:33:25 +03:30
Amir Abbas Mousavian e30861ad18 Added publicLink method to OneDrive
- Added more Documentation, specially initializer
- Added option to conserve sessions after provider deinit to finish tasks
- Dropbox and OneDrive init are not bailable anymore
- Updated AEXML
2017-02-03 18:29:34 +03:30
Amir Abbas Mousavian 538e9bb42d Updated provider descriptions 2017-02-02 19:32:28 +03:30
Amir Abbas Mousavian ef2d69c54c Updated supported providers order 2017-02-02 19:26:26 +03:30
Amir Abbas Mousavian 9f210ef356 Updated readme link to cocoa pods docs 2017-02-02 13:32:58 +03:30
Amir Abbas Mousavian 5a7dfa1039 Merge commit 'b84e6ea7abbec6f9a83144d2dcd6744b568d682f'
* commit 'b84e6ea7abbec6f9a83144d2dcd6744b568d682f':
  Updated Date
2017-02-02 13:28:08 +03:30
Amir Abbas Mousavian be7a370a42 Added Documentation, get contents handling in Local provider optimized
- Changed default thumbnail dimension to 64x64 in `LocalFileProvider`
2017-02-02 13:25:40 +03:30
Amir Abbas Mousavian b84e6ea7ab Updated Date 2017-02-02 01:03:48 +03:30
Amir Abbas Mousavian 82b1d2c450 Added new badges 2017-02-01 22:18:20 +03:30
Amir Abbas Mousavian c38ee1ccd3 standardized FileObject.path property
- FileObject.path now has heading slash in all scenarios
- fixes #27, WebDAV fileObject.path was not relative
2017-02-01 21:23:11 +03:30
Amir Abbas Mousavian 1a44df3fd7 Refactoring, better webdav response handling
- Added Dropbox copy from reference method
- refactored `mapToFileObject` methods into `FileObject` initializers
- fixed `requestDictionary` type to `[String: AnyObject]`
2017-02-01 14:08:28 +03:30
Amir Abbas Mousavian eff3725680 Added release version badge 2017-02-01 00:21:48 +03:30
Amir Abbas Mousavian 9368f636d3 Added carthage badge 2017-01-31 23:53:06 +03:30
Amir Abbas Mousavian 3f2fda638f bugfix: path in LocalFileObject is not relative 2017-01-31 23:12:29 +03:30
Amir Abbas Mousavian 6737f98978 Updated travis.yml 2017-01-31 19:07:11 +03:30
Amir Abbas Mousavian 3e55cc60f7 Replaced absoluteURL with relative url (resolved #27), improving performance.
- renamed `DropboxFileProvider.copyItem(path:toRemoteURL:) ` to `DropboxFileProvider.copyItem(remoteURL:toPath:)` due to logic
2017-01-31 17:19:45 +03:30
Alek Slater d7ae709cf7 contentURL was missing the trailing slash causing it to fail for all contentURL calls with the dropbox api (#26) 2017-01-31 13:05:20 +03:30
Amir Abbas Mousavian 4f6e3c7bd5 Uncommented CloudFileProvider decription 2017-01-31 02:11:28 +03:30
Amir Abbas Mousavian b121fe6182 rectified macOS and tvOS version check 2017-01-31 01:42:03 +03:30
Amir Abbas Mousavian 29b7f720f5 Added iCloud provider change monitor and CloudOperationHandle
- fixed copyItem functions for local interoperability
2017-01-31 01:35:38 +03:30
Amir Abbas Mousavian 067e14b76f Added CloudFileProvider and coordination to LocalFileProvider
- changed `operation_queue` type to `OperationQueue`
- added `LocalFileProvider.init(sharedContainerId:)`
- added `destination(ofSymbolicLink:)` to LocalFileProvider
- added expirationDate to `temporaryLink(to:)` methods
- known issue: `CloudFileProvider` monitoring is not implemented
- known issue: `CloudFileProvider` copy from local and to local works imperfect
2017-01-30 18:51:06 +03:30
Amir Abbas Mousavian 09e39ada82 Resolved issue #24, off-by-one bug in Range header 2017-01-30 18:37:25 +03:30
Amir Abbas 6889b302b7 Removed CloudProvider due to incompleteness 2017-01-27 02:47:11 +03:30
Amir Abbas 96ce12f226 Added iCloud Drive provider CloudFileProvider
- Refactored initializers
- Replaced `NSSearchPathForDirectoriesInDomains` with `FileManager.urls(for:In:)`
2017-01-27 02:13:51 +03:30
Amir Abbas 091fd14a88 OneDrive drive name bug fix. Added sorting method to [FileObject] array.
- now return `Error` for uploading a file with size higher than limit in OneDrive / Dropbox instead of asserting
2017-01-25 16:04:16 +03:30
Amir Abbas 10aac532f7 Fixed crashing bug 2017-01-18 10:20:10 +03:30
Amir Abbas 3aac0a88c4 Fixed path in url percent encoding 2017-01-16 23:28:32 +03:30
Amir Abbas 24355a4c6c Added OneDrive support
- removed unnecessary MediaPlayer.framework dependency
- fixed multiple Dropbox provider bugs
2017-01-16 21:55:58 +03:30
Amir Abbas d2cf657ab2 Added Thumbnail and Meta-information generator to Local and Dropbox 2017-01-16 15:20:44 +03:30
Alek Slater f2d54c7f92 incorrect usage of string.remove in correctPath method
- string remove needs to be given the index before the endIndex or it will crash (#22) needs to be given the index before the endIndex or it will crash
2016-12-19 18:10:14 +03:30
Amir Abbas 0e9a82a288 Resolves #20, Dropbox/WebDav file size 2016-12-15 14:42:00 +03:30
Amir Abbas ea7e089718 Updated for Xcode 8.2 2016-12-13 19:12:08 +03:30
Amir Abbas 19ea14ebc1 Added FileObjectSorting to sort FileObjects conveniently 2016-12-12 01:57:34 +03:30
Amir Abbas 47885c0a4e Deprecated FileObject.fileType with .type 2016-12-09 21:06:11 +03:30
Alek Slater ac44aa190f Overwrite param added to copyItem + minor bugfix and improvements (#18)
- Added overwrite param to copyItem from local to remote
- fixed an issue with file upload on dropbox that wouldnt work for people in non UTC timezones.
- made filebased upload for dropbox not read file data into memory let URLSession stream from the disk instead
2016-12-09 20:58:50 +03:30
Amir Abbas 50f6a393a0 Compiler Optimizations, Dropbox readme 2016-12-09 19:07:46 +03:30
Amir Abbas 779b38f381 Default values implemented in protocol level 2016-12-09 10:04:41 +03:30
Amir Abbas a374841883 Refined SPM/manual installation guide in Readme 2016-12-08 20:41:52 +03:30
Amir Abbas 0123d8b117 Overwrite option to writeContents 2016-12-07 21:24:19 +03:30
Amir Abbas Mousavian fdc4bb818d Merge pull request #17 from skela/master
Added overwrite option to DropboxProvider.writeContents()
2016-12-07 20:13:17 +03:30
Amir Abbas Mousavian a08a9fe7a0 Updated semantics
and changed default overwrite value to false
2016-12-07 20:12:15 +03:30
Alek Slater 792ac6b015 Update DropboxFileProvider.swift
Exposes the overwrite parameter for writeContents in DropboxFileProvider
2016-12-07 11:27:51 +08:00
Amir Abbas 8bad5944bc Updated pod spec version to 0.8.1 2016-12-07 02:36:49 +03:30
Amir Abbas 6ef2ab11c4 Fixed bug in Data(value:) initializer. Switch uuid init via this 2016-12-07 02:34:34 +03:30
Amir Abbas Mousavian 7f27d46c70 Merge pull request #16 from RndmTsk/master
Fixes #15, FileObject.fileType always returns nil.
2016-12-07 02:29:28 +03:30
Terry Latanville b1ec99b1b8 Fixes #15, FileObject.fileType always returns nil. 2016-12-06 17:18:41 -05:00
Amir Abbas Mousavian c4b8065cd3 Merge pull request #14 from skela/master
Update DropboxFileProvider.swift
2016-12-06 10:51:58 +03:30
Alek Slater 2d8454c711 Update DropboxFileProvider.swift
Using the same request dict creation method that can be found in the contents method, to avoid error when trying to use copyItem to copy a remote file to a local file destination.
2016-12-06 14:51:48 +08:00
Amir Abbas Mousavian 3b35c066de fixed get_temporary_link url 2016-12-03 20:40:04 +03:30
Amir Abbas Mousavian 7d8de9cefe FileObject init accessors fixed, refactoring 2016-12-03 20:26:27 +03:30
Amir Abbas Mousavian e84fef20ea [gardening] Refactoring WebDAV copy, move and remove operations 2016-12-03 15:32:54 +03:30
Amir Abbas Mousavian 4a9a3196a2 Dropbox get_temporary_link and save_url implementations 2016-12-03 15:12:11 +03:30
Amir Abbas Mousavian f4e6d277ae Updated source highlighting 2016-12-03 13:34:32 +03:30
Amir Abbas Mousavian 1328a8e9e2 New FIleObject implementation, highlighted Readme 2016-12-03 13:21:13 +03:30
Amir Abbas Mousavian 5bf620916f Fixes #12, createFile method path in WebDAV is not correct.
- removed NSURL cast for resource values, now using swift 3 methods
2016-12-02 01:12:08 +03:30
Amir Abbas Mousavian 4f56e20441 createFile definition improved to resolve #10 2016-12-01 12:12:53 +03:30
Amir Abbas Mousavian 9dda618b73 Revert "createFile definition improved"
This reverts commit da60c05188.
2016-12-01 12:10:14 +03:30
Amir Abbas Mousavian da60c05188 createFile definition improved 2016-12-01 11:54:03 +03:30
Amir Abbas Mousavian 4366855d54 Added NSCopying conformance, SMB headers gardening 2016-11-28 19:40:09 +03:30
Amir Abbas Mousavian fe05fd83fe Replaced encode/decode methods with Data extension 2016-11-26 00:11:46 +03:30
Amir Abbas Mousavian 826d207e6b OperationHandle optimizations
- code refactoring for RemoteOperationHandle usage and description
- bug fix: move operation in Dropbox provider did copy
- bug fix: dynamic inProgress result for RemoteOperationHandle
2016-11-24 22:54:00 +03:30
Amir Abbas Mousavian 66fc1e1284 Fixed unexpected behaviors in Remote providers:
- Calling delegate and completion methods
- refactoring codes
2016-11-23 23:42:36 +03:30
Amir Abbas Mousavian a1d489f5a5 Optimization in project settings for dynamic linking 2016-11-23 19:43:28 +03:30
Amir Abbas Mousavian ad2699cd19 Update Readme for OperationHandle 2016-11-11 16:37:32 +03:30
Amir Abbas Mousavian 97ae86cedb - fixed a bug in fileByUniqueName() function
- more neat code
2016-11-11 03:45:37 +03:30
Amir Abbas Mousavian 71b07cac1b fixed NotImplement() issue on build 2016-10-30 01:55:19 +03:30
Amir Abbas Mousavian a15f8f3809 Added OperationHandle, to cancel remote operations 2016-10-29 23:11:48 +03:30
Amir Abbas Mousavian dc1270d8d1 Fixing wrong spellings in readme 2016-10-26 01:04:33 +03:30
Amir Abbas Mousavian 68b1e23be3 Using Data instead of NSData in many places 2016-10-24 20:31:19 +03:30
Amir Abbas Mousavian 057c9fd940 Update podspec to 0.5.2 2016-09-30 14:08:25 +03:30
Amir Abbas Mousavian 6d63322779 FileProviderOperationDelegate methods will call for WebDav and Dropbox providers
- Added Carthage to Travis
2016-09-30 14:07:02 +03:30
Amir Abbas Mousavian fc6b46d17a Updated swift 3 semantics 2016-09-28 19:15:05 +03:30
Amir Abbas Mousavian bb9c08e309 Add .swift-version to pass cocoapods's validation, added static type var 2016-09-27 16:59:54 +03:30
Amir Abbas Mousavian 315ead606e AnyObject casts changed to proper one according to data type 2016-09-17 12:12:52 +04:30
Amir Abbas Mousavian d63e9c7f04 Swift 3 travis & podspec update 2016-09-16 05:20:34 +04:30
Amir Abbas Mousavian a5fe28c18d Swift 3 readme update 2016-09-15 07:58:36 +04:30
Amir Abbas Mousavian 542c18bab6 Convert to Swift 2 2016-09-15 02:48:04 +04:30
Amir Abbas Mousavian dca5dddbfd Code beat exceptions added 2016-08-20 19:19:05 +04:30
Amir Abbas Mousavian 6764c23f1b Update README.md and some refactors 2016-08-15 13:50:03 +04:30
Amir Abbas Mousavian 955a86372b Refactored NSURLSession delegate in Dropbox and WebDAV 2016-08-15 13:30:37 +04:30
Amir Abbas Mousavian 32a20fbc49 CodeCov added 2016-08-11 14:26:24 +04:30
Amir Abbas Mousavian 921ba19afa travis yml update 2016-08-11 14:16:04 +04:30
Amir Abbas Mousavian 8625b0464c Travis CI integration 2016-08-11 14:04:37 +04:30
Amir Abbas Mousavian 1bf9d1eadd Swift Package Manager Added 2016-08-11 13:49:28 +04:30
Amir Abbas Mousavian cbf774ba9e update README.md 2016-08-11 12:52:04 +04:30
Amir Abbas Mousavian 5e2f911cda Dropbox provider bug fixes 2016-08-11 01:06:53 +04:30
Amir Abbas Mousavian f423834021 HTTP error codes enum optimization 2016-08-10 12:35:11 +04:30
Amir Abbas Mousavian 0772c87122 FPSStreamTass bugs fixed and now working like a charm! 2016-08-09 23:37:28 +04:30
Amir Abbas Mousavian 011c535760 FPSStreamTask (iOS7,8) on a par with NSURLSessionStreamTask in iOS9 2016-08-09 13:45:35 +04:30
Amir Abbas Mousavian 468a2bc9e9 SMB2.QueryInfo implementation 2016-08-09 02:04:42 +04:30
Amir Abbas Mousavian 33be492499 Implemented SMB.ChangeNotify command, syntax fixed 2016-08-08 15:57:08 +04:30
Amir Abbas Mousavian c4049b961f Dropbox updates, SMB2 QueryDirectory implementation
- ExtendedFileProvider protocol is implemented for Dropbox
-
2016-08-08 12:47:13 +04:30
Amir Abbas Mousavian 940c7c1028 Dropbox implementation completed and getting storage size, used 2016-08-04 23:06:24 +04:30
Amir Abbas Mousavian b4ace7e680 Unified HTTP based services (WebDAV/Dropbox) Error Handling
- DropboxFileProvider.contentsOfDirectory() method implemented
2016-08-03 13:40:12 +04:30
Amir Abbas Mousavian eccbeb7174 Fixed WebDAV connection error for Apache servers 2016-08-02 14:59:32 +04:30
Amir Abbas Mousavian a077d000bc Updated Readme 2016-07-31 01:27:13 +04:30
Amir Abbas Mousavian 6a3ea633bf WebDAV file protocol conformance to FileProvider 2016-07-29 10:57:18 +04:30
Amir Abbas Mousavian 3ca26ad3df pod 0.3.2 2016-07-28 15:10:17 +04:30
Amir Abbas Mousavian 364b93c6fd Modifier correction 2016-07-28 14:31:48 +04:30
Amir Abbas Mousavian 506952be13 Corrected modifiers and updated pod to 0.3.1 2016-07-28 14:26:37 +04:30
Amir Abbas Mousavian 69ca35f0ae Source compatibility with Swift 2.3 2016-07-27 16:12:11 +04:30
Amir Abbas Mousavian dcbe3c1c3e Updated sourcefiles tag 2016-07-27 15:19:41 +04:30
Amir Abbas Mousavian e6a450e3e2 tvOS support 2016-07-27 15:02:14 +04:30
Amir Abbas Mousavian 0fd6da3658 Cocoapod project added 2016-07-27 14:56:33 +04:30
Amir Abbas Mousavian 67b6cf64e2 insert slash in the beginning of path 2016-07-26 10:34:34 +04:30
Amir Abbas Mousavian 3ab252e598 WebDav refactors and Dropbox partial implementation 2016-07-25 19:18:13 +04:30
Amir Abbas Mousavian 09f9909363 spliced SMB2 Types into several files 2016-07-21 00:40:21 +04:30
Amir Abbas Mousavian c6d069cb46 Added Write and IOCtl commands' types 2016-07-20 17:59:25 +04:30
Amir Abbas Mousavian 0004bd4c90 update podspec and silent compiler hints 2016-07-15 16:09:46 +04:30
Amir Abbas Mousavian bf4337bcb3 Updated Readme 2016-07-15 16:00:14 +04:30
Amir Abbas Mousavian 3301d2c004 Bug fixes for path handling and LocalFolderMonitor
- Bugs in fileByUniqueName(), relativePath() and absoluteURL fixed
- Introduced delay in LocalFolderMonitor to prevent excess handler calling
2016-07-13 13:30:32 +04:30
Amir Abbas Mousavian f0cd7846d8 FileOpreationDelegate, delegate strong reference bug, fileByUniqueName method 2016-07-11 13:31:22 +04:30
Amir Abbas Mousavian 8fd7da669d Readme.md error 2016-07-06 21:34:54 +04:30
Amir Abbas Mousavian 13a68c84f8 Readme.md update for cocoapods 2016-07-06 21:32:14 +04:30
63 changed files with 16166 additions and 3944 deletions
+1
View File
@@ -0,0 +1 @@
3.0
+81
View File
@@ -0,0 +1,81 @@
language: objective-c
osx_image: xcode8.2
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"
- IOS_SDK=iphonesimulator
- MACOS_SDK=macosx
- TVOS_SDK=appletvsimulator
matrix:
- DESTINATION="OS=10.2,name=iPad Air 2" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="NO" CARTHAGEDEPLOY="YES"
- DESTINATION="OS=9.0,name=iPhone 6" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="YES" CARTHAGEDEPLOY="NO"
- DESTINATION="OS=8.1,name=iPhone 4S" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="NO" CARTHAGEDEPLOY="NO"
- DESTINATION="OS=10.1,name=Apple TV 1080p" SCHEME="$TVOS_FRAMEWORK_SCHEME" SDK="$TVOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="NO" CARTHAGEDEPLOY="NO"
# - DESTINATION="OS=10.0,name=Apple TV 1080p" SCHEME="$TVOS_FRAMEWORK_SCHEME" SDK="$TVOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="NO" CARTHAGEDEPLOY="NO"
- DESTINATION="arch=x86_64" SCHEME="$MACOS_FRAMEWORK_SCHEME" SDK="$MACOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="NO" CARTHAGEDEPLOY="NO"
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"
+11 -11
View File
@@ -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.1.0"
s.summary = "Extended Local/WebDAV/SMB/CIFS/etc. File Manager for Swift on iOS and MacOS."
s.name = "FilesProvider"
s.version = "0.21.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"
@@ -58,7 +58,7 @@ Pod::Spec.new do |s|
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.social_media_url = "https://twitter.com/amosavian"
# ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
#
@@ -67,13 +67,13 @@ Pod::Spec.new do |s|
#
# s.platform = :ios
# s.platform = :ios, "7.0"
# s.platform = :ios, "8.0"
# When using multiple platforms
s.ios.deployment_target = "8.0"
s.osx.deployment_target = "10.10"
# s.watchos.deployment_target = "2.0"
# s.tvos.deployment_target = "9.0"
s.tvos.deployment_target = "9.0"
# ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
@@ -93,8 +93,8 @@ Pod::Spec.new do |s|
# Not including the public_header_files will make all headers public.
#
s.source_files = "Source/*.swift"
s.exclude_files = "Source/Exclude"
s.source_files = "Sources/**/*.swift"
s.exclude_files = "Sources/Exclude"
# s.public_header_files = "Classes/**/*.h"
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:FileProvider.xcodeproj">
</FileRef>
</Workspace>
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0820"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "799396741D48B80D00086753"
BuildableName = "FilesProvider.framework"
BlueprintName = "FilesProvider OSX"
ReferencedContainer = "container:FilesProvider.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "799396741D48B80D00086753"
BuildableName = "FilesProvider.framework"
BlueprintName = "FilesProvider OSX"
ReferencedContainer = "container:FilesProvider.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "799396741D48B80D00086753"
BuildableName = "FilesProvider.framework"
BlueprintName = "FilesProvider OSX"
ReferencedContainer = "container:FilesProvider.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0820"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "799396661D48B7F600086753"
BuildableName = "FilesProvider.framework"
BlueprintName = "FilesProvider iOS"
ReferencedContainer = "container:FilesProvider.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "799396661D48B7F600086753"
BuildableName = "FilesProvider.framework"
BlueprintName = "FilesProvider iOS"
ReferencedContainer = "container:FilesProvider.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "799396661D48B7F600086753"
BuildableName = "FilesProvider.framework"
BlueprintName = "FilesProvider iOS"
ReferencedContainer = "container:FilesProvider.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0810"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "799396811D48B82700086753"
BuildableName = "FilesProvider.framework"
BlueprintName = "FilesProvider tvOS"
ReferencedContainer = "container:FilesProvider.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "799396811D48B82700086753"
BuildableName = "FilesProvider.framework"
BlueprintName = "FilesProvider tvOS"
ReferencedContainer = "container:FilesProvider.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "799396811D48B82700086753"
BuildableName = "FilesProvider.framework"
BlueprintName = "FilesProvider tvOS"
ReferencedContainer = "container:FilesProvider.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+1 -1
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
import PackageDescription
let package = Package(
name: "FilesProvider"
)
+26
View File
@@ -0,0 +1,26 @@
<?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>en</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>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>${BUNDLE_VERSION_STRING}</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>
+26
View File
@@ -0,0 +1,26 @@
<?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>en</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>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>${BUNDLE_VERSION_STRING}</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>
+26
View File
@@ -0,0 +1,26 @@
<?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>en</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>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>${BUNDLE_VERSION_STRING}</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>
+416 -140
View File
@@ -1,61 +1,109 @@
# FileProvider (experimental)
![File Provider](fileprovider.png)
>This Swift library provide a swifty way to deal with local and remote files and directories in same 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]
[![Platform](https://img.shields.io/badge/Platform-iOS%2C%20OSX-lightgray.svg)]()
[![codebeat badge](https://codebeat.co/badges/7b359f48-78eb-4647-ab22-56262a827517)](https://codebeat.co/projects/github-com-amosavian-fileprovider)
[![Build Status][travis-image]][travis-url]
[![Codebeat Badge][codebeat-image]][codebeat-url]
[![Release version][release-image]][release-url]
[![CocoaPods version](https://img.shields.io/cocoapods/v/FilesProvider.svg)][cocoapods]
[![Carthage compatible][carthage-image]](https://github.com/Carthage/Carthage)
[![Cocoapods Downloads][cocoapods-downloads]][cocoapods]
[![Cocoapods Apps][cocoapods-apps]][cocoapods]
Old Cocoapods repo stats:
[![Cocoapods Downloads][cocoapods-downloads-old]][cocoapods-old]
[![Cocoapods Apps][cocoapods-apps-old]][cocoapods-old]
</center>
<!---
[![Build Status][travis-image]][travis-url]
[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
[![CocoaPods Compatible](https://img.shields.io/cocoapods/v/EZSwiftExtensions.svg)](https://img.shields.io/cocoapods/v/LFAlertController.svg)
[![Cocoapods Doc][docs-image]][docs-url]
[![codecov](https://codecov.io/gh/amosavian/FileProvider/branch/master/graph/badge.svg)](https://codecov.io/gh/amosavian/FileProvider)
--->
This library provides implementaion of WebDav, FTP, Dropbox, OneDrive and SMB2 (incomplete) and local files.
This library provides implementaion of WebDav and SMB/CIFS (incomplete) and local files.
All functions are async calls and it wont block your main thread.
All functions do async calls and it wont block your main thread.
## Features
- [x] **LocalFileProvider** a wrapper for `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**
- [ ] **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.
* For now it has limitation in uploading files up to 100MB.
- [ ] **AmazonS3FileProvider** Amazon storage backend. Used by many sites.
- [ ] **GoogleFileProvider** A wrapper around Goodle Drive REST API.
- [ ] **SMBFileProvider** SMB2/3 introduced in 2006, which is a file and printer sharing protocol originated from Microsoft Windows and now is replacing AFP protocol on macOS.
* Data types and some basic functions are implemented but *main interface is not implemented yet!*.
* SMBv1/CIFS is insecure, deprecated and kinda tricky to be implemented due to strict memory allignment in Swift.
## Requirements
- **Swift 2.2**
- iOS 7.0 , OSX 10.9
- XCode 7.3
- **Swift 3.0 or higher**
- iOS 8.0 , OSX 10.10
- XCode 8.0
Legacy version is available in swift-2 branch.
## Installation
### 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.
### Cocoapods / Carthage / Swift Package Manager
I will add when project is completed is ready to use in production envioronment
Add this line to your pods file:
### Git clone
To have latest updates with ease, use this command on terminal to get a clone:
```ruby
pod "FilesProvider"
```
git clone https://github.com/amosavian/FileProvider FileProvider
You can update your library using this command in FileProvider folder:
Or add this to Cartfile:
git pull
```
github "amosavian/FileProvider"
```
### Submodule into your project
if you have a git based project, use this command in your projects directory:
Or to use in Swift Package Manager add this line in `Dependencies`:
git submodule add https://github.com/amosavian/FileProvider FileProvider
```swift
.Package(url: "https://github.com/amosavian/FileProvider.git", majorVersion: 0)
```
### Manually
Copy Source folder to your project and voila!
To have latest updates with ease, use this command on terminal to get a clone:
```bash
git clone https://github.com/amosavian/FileProvider
```
You can update your library using this command in FileProvider folder:
```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:
```bash
git submodule add https://github.com/amosavian/FileProvider
```
Then you can do either:
* 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.
## Usage
@@ -65,173 +113,401 @@ Each provider has a specific class which conforms to FileProvider protocol and s
For LocalFileProvider if you want to deal with `Documents` folder
let documentsFileProvider = LocalFileProvider()
``` swift
import FilesProvider
is equal to:
let documentPath = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true);
let documentsURL = NSURL(fileURLWithPath: documentPath);
let documentsFileProvider = 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: "http://www.example.com/dav", credential: credential)
``` swift
import FilesProvider
let credential = URLCredential(user: "user", password: "pass", persistence: .permanent)
let webdavProvider = WebDAVFileProvider(baseURL: URL(string: "http://www.example.com/dav")!, credential: credential)
```
* In case you want to connect non-secure servers for WebDAV (http) or FTP in iOS 9+ / macOS 10.11+ you should disable App Transport Security (ATS) according to [this guide.](https://gist.github.com/mlynch/284699d676fe9ed0abfa)
* For Dropbox & OneDrive, user is clientID and password is Token which both must be retrieved via [OAuth2 API of Dropbox](https://www.dropbox.com/developers/reference/oauth-guide). There are libraries like [p2/OAuth2](https://github.com/p2/OAuth2) or [OAuthSwift](https://github.com/OAuthSwift/OAuthSwift) which can facilate the procedure to retrieve token. The latter is easier to use and prefered.
For interaction with UI, set delegate variable of `FileProvider` object
### Delegate
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() {
documentsFileProvider.delegate = self
}
```swift
override func viewDidLoad() {
documentsProvider.delegate = self as FileProviderDelegate
}
func fileproviderSucceed(fileProvider: FileProvider, 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: FileProvider, 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: FileProvider, 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")
}
}
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
}
}
```
Use completion handlers for error handling or result processing as far as possible.
**Note:** `fileproviderProgress()` delegate method is not called by `LocalFileProvider` currently.
It's recommended to use completion handlers for error handling or result processing.
#### Controlling file operations
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(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: In `LocalFileProvider`, these methods will be called for files in a directory and its subfolders recursively.**
### Directory contents and file attributes
There is a `FileObject` class which holds file attributes like size and creation date. You can retrieve information of files inside a directory or get information of a file directly
There is a `FileObject` class which holds file attributes like size and creation date. You can retrieve information of files inside a directory or get information of a file directly.
documentsFileProvider.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)")
}
)
For a single file:
documentsFileProvider.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.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)")
}
})
```
### Change current directory
To get list of files in a directory:
documentsFileProvider.currentPath = "/New Folder"
// now path is ~/Documents/New Folder
```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)")
}
})
```
To get size of strage and used/free space:
```swift
func storageProperties(completionHandler: { total, used in
print("Total Storage Space: \(total)")
print("Used Space: \(used)")
print("Free Space: \(total - used)")
})
```
* 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:
documentsFileProvider.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)
documentsFileProvider.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
documentsFileProvider.copyItemAtPath(path: "new folder/old.txt", toPath: "new.txt", overwrite: false, completionHandler: nil)
Copy file old.txt to new.txt in current path:
// Move file old.txt to new.txt in current path
documentsFileProvider.moveItemAtPath(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:
```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
documentsFileProvider.removeItemAtPath(path: "new.txt", completionHandler: nil)
```swift
documentsProvider.removeItem(path: "new.txt", completionHandler: nil)
```
***Caution:*** This method will not delete directories with content.
### Fetching Contents 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.
### Retrieve Content 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.
documentsFileProvider.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.
documentsFileProvider.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)
documentsFileProvider.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.
```swift
// to register a new notification handler
documentsProvider.registerNotifcation(path: "/") {
// calling functions to update UI
}
// 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`
- [ ] 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)
* [File Manager - PDF Reader & Music Player](https://itunes.apple.com/us/app/file-manager-pdf-reader-music/id1017809685?ls=1&mt=8)
If you used this library in your project, you can open an issue to inform us.
## Meta
Amir-Abbas Mousavia [@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.3-green.svg
[cocoapods]: https://cocoapods.org/pods/FilesProvider
[cocoapods-old]: https://cocoapods.org/pods/FileProvider
[swift-image]: https://img.shields.io/badge/swift-3.0,%203.1-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
<!---
[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
--->
[codebeat-image]: https://codebeat.co/badges/c19b47ea-2f9d-45df-8458-b2d952fe9dad
[codebeat-url]: https://codebeat.co/projects/github-com-vsouza-awesomeios-com
[codebeat-image]: https://codebeat.co/badges/7b359f48-78eb-4647-ab22-56262a827517
[codebeat-url]: https://codebeat.co/projects/github-com-amosavian-fileprovider
[travis-image]: https://img.shields.io/travis/amosavian/FileProvider/master.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/
-492
View File
@@ -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: "&amp;", options: .LiteralSearch)
// replace the other four special characters
let escapeChars = ["<" : "&lt;", ">" : "&gt;", "'" : "&apos;", "\"" : "&quot;"]
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
}
}
-248
View File
@@ -1,248 +0,0 @@
//
// FileProvider.swift
// FileProvider
//
// Created by Amir Abbas Mousavian.
// Copyright © 2016 Mousavian. Distributed under MIT license.
//
import Foundation
#if (iOS)
import UIKit
#endif
#if (OSX)
import AppKit
#endif
public enum FileType: String {
case Directory
case Regular
case SymbolicLink
case Socket
case CharacterSpecial
case BlockSpecial
case NamedPipe
case Unknown
init(urlResourceTypeValue: String) {
switch urlResourceTypeValue {
case NSURLFileResourceTypeNamedPipe: self = .NamedPipe
case NSURLFileResourceTypeCharacterSpecial: self = .CharacterSpecial
case NSURLFileResourceTypeDirectory: self = .Directory
case NSURLFileResourceTypeBlockSpecial: self = .BlockSpecial
case NSURLFileResourceTypeRegular: self = .Regular
case NSURLFileResourceTypeSymbolicLink: self = .SymbolicLink
case NSURLFileResourceTypeSocket: self = .Socket
case NSURLFileResourceTypeUnknown: self = .Unknown
default: self = .Unknown
}
}
init(fileTypeValue: String) {
switch fileTypeValue {
case NSFileTypeCharacterSpecial: self = .CharacterSpecial
case NSFileTypeDirectory: self = .Directory
case NSFileTypeBlockSpecial: self = .BlockSpecial
case NSFileTypeRegular: self = .Regular
case NSFileTypeSymbolicLink: self = .SymbolicLink
case NSFileTypeSocket: self = .Socket
case NSFileTypeUnknown: self = .Unknown
default: self = .Unknown
}
}
}
protocol FoundationErrorEnum {
init? (rawValue: Int)
var rawValue: Int { get }
}
extension NSURLError: FoundationErrorEnum {}
extension NSCocoaError: FoundationErrorEnum {}
public class FileObject {
let absoluteURL: NSURL?
let name: String
let size: Int64
let createdDate: NSDate?
let modifiedDate: NSDate?
let fileType: FileType
let isHidden: Bool
let isReadOnly: Bool
init(absoluteURL: NSURL, name: String, size: Int64, createdDate: NSDate?, modifiedDate: NSDate?, fileType: FileType, isHidden: Bool, isReadOnly: Bool) {
self.absoluteURL = absoluteURL
self.name = name
self.size = size
self.createdDate = createdDate
self.modifiedDate = modifiedDate
self.fileType = fileType
self.isHidden = isHidden
self.isReadOnly = isReadOnly
}
init(name: String, createdDate: NSDate?, modifiedDate: NSDate?, isHidden: Bool, isReadOnly: Bool) {
self.absoluteURL = NSURL()
self.name = name
self.size = -1
self.createdDate = createdDate
self.modifiedDate = modifiedDate
self.fileType = .Regular
self.isHidden = isHidden
self.isReadOnly = isReadOnly
}
}
public typealias SimpleCompletionHandler = ((error: ErrorType?) -> Void)?
public protocol FileProviderBasic: class {
var type: String { get }
var isPathRelative: Bool { get }
var baseURL: NSURL? { get }
var currentPath: String { get set }
var dispatch_queue: dispatch_queue_t { get set }
var delegate: FileProviderDelegate? { get set }
var credential: NSURLCredential? { get }
/**
*
*/
func contentsOfDirectoryAtPath(path: String, completionHandler: ((contents: [FileObject], error: ErrorType?) -> Void))
func attributesOfItemAtPath(path: String, completionHandler: ((attributes: FileObject?, error: ErrorType?) -> Void))
}
public protocol FileProviderOperations: FileProviderBasic {
func createFolder(folderName: String, atPath: String, completionHandler: SimpleCompletionHandler)
func createFile(fileAttribs: FileObject, atPath: String, contents data: NSData?, completionHandler: SimpleCompletionHandler)
func moveItemAtPath(path: String, toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler)
func copyItemAtPath(path: String, toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler)
func removeItemAtPath(path: String, completionHandler: SimpleCompletionHandler)
func copyLocalFileToPath(localFile: NSURL, toPath: String, completionHandler: SimpleCompletionHandler)
func copyPathToLocalFile(path: String, toLocalURL: NSURL, completionHandler: SimpleCompletionHandler)
}
public protocol FileProviderReadWrite: FileProviderBasic {
func contentsAtPath(path: String, completionHandler: ((contents: NSData?, error: ErrorType?) -> Void))
func contentsAtPath(path: String, offset: Int64, length: Int, completionHandler: ((contents: NSData?, error: ErrorType?) -> Void))
func writeContentsAtPath(path: String, contents data: NSData, atomically: Bool, completionHandler: SimpleCompletionHandler)
func searchFilesAtPath(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: ((files: [FileObject], error: ErrorType?) -> Void))
}
public protocol FileProviderMonitor: FileProviderBasic {
func registerNotifcation(path: String, eventHandler: (() -> Void))
func unregisterNotifcation(path: String)
}
public protocol FileProvider: FileProviderBasic, FileProviderOperations, FileProviderReadWrite {
}
extension FileProviderBasic {
var bareCurrentPath: String {
return currentPath.stringByTrimmingCharactersInSet(NSCharacterSet(charactersInString: ". /"))
}
func absoluteURL(path: String? = nil) -> NSURL {
let rpath: String
if let path = path {
rpath = path
} else {
rpath = self.currentPath
}
if isPathRelative, let baseURL = baseURL {
if rpath.hasPrefix("/") && baseURL.absoluteString.hasSuffix("/") {
var npath = rpath
npath.removeAtIndex(npath.startIndex)
return baseURL.URLByAppendingPathComponent(npath)
} else {
return baseURL.URLByAppendingPathComponent(rpath)
}
} else {
return NSURL(fileURLWithPath: rpath)
}
}
internal func throwError(path: String, code: FoundationErrorEnum) -> NSError {
let fileURL = self.absoluteURL(path)
let domain: String
switch code {
case is NSURLError:
domain = NSURLErrorDomain
default:
domain = NSCocoaErrorDomain
}
return NSError(domain: domain, code: code.rawValue, userInfo: [NSURLErrorFailingURLErrorKey: fileURL, NSURLErrorFailingURLStringErrorKey: fileURL.absoluteString])
}
internal func NotImplemented() {
assert(false, "method not implemented")
}
internal func resolveRFCDate(httpDateString: String) -> NSDate? {
let dateFor: NSDateFormatter = NSDateFormatter()
dateFor.locale = NSLocale(localeIdentifier: "en_US")
dateFor.dateFormat = "EEE',' dd' 'MMM' 'yyyy HH':'mm':'ss zzz"
if let rfc1123 = dateFor.dateFromString(httpDateString) {
return rfc1123
}
dateFor.dateFormat = "EEEE',' dd'-'MMM'-'yy HH':'mm':'ss z"
if let rfc850 = dateFor.dateFromString(httpDateString) {
return rfc850
}
dateFor.dateFormat = "EEE MMM d HH':'mm':'ss yyyy"
if let asctime = dateFor.dateFromString(httpDateString) {
return asctime
}
//self.init()
return nil
}
internal func jsonToDictionary(jsonString: String) -> [String: AnyObject]? {
guard let data = jsonString.dataUsingEncoding(NSUTF8StringEncoding) else {
return nil
}
if let dic = try? NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions()) as? [String: AnyObject] {
return dic
}
return nil
}
internal func dictionaryToJSON(dictionary: [String: AnyObject]) -> String? {
if let data = try? NSJSONSerialization.dataWithJSONObject(dictionary, options: NSJSONWritingOptions()) {
return String(data: data, encoding: NSUTF8StringEncoding)
}
return nil
}
}
#if (iOS)
public protocol ExtendedFileProvider: FileProvider {
func thumbnailOfFileAtPath(path: String, dimension: CGSize, completionHandler: ((image: UIImage?, error: ErrorType?) -> Void))
func propertiesOfFileAtPath(path: String, completionHandler: ((propertiesDictionary: [String: AnyObject], keys: [String], error: ErrorType?) -> Void))
}
#elseif (OSX)
public protocol ExtendedFileProvider: FileProvider {
func thumbnailOfFileAtPath(path: String, dimension: CGSize, completionHandler: ((image: NSImage?, error: ErrorType?) -> Void))
func propertiesOfFileAtPath(path: String, completionHandler: ((propertiesDictionary: [String: AnyObject], keys: [String], error: ErrorType?) -> Void))
}
#endif
public enum FileOperation {
case Create (path: String)
case Copy (source: String, destination: String)
case Move (source: String, destination: String)
case Modify (path: String)
case Remove (path: String)
case Link (link: String, target: String)
}
public protocol FileProviderDelegate {
func fileproviderSucceed(fileProvider: FileProviderOperations, operation: FileOperation)
func fileproviderFailed(fileProvider: FileProviderOperations, operation: FileOperation)
func fileproviderProgress(fileProvider: FileProviderOperations, operation: FileOperation, progress: Float)
}
-373
View File
@@ -1,373 +0,0 @@
//
// LocalFileProvider.swift
// FileProvider
//
// Created by Amir Abbas Mousavian.
// Copyright © 2016 Mousavian. Distributed under MIT license.
//
import Foundation
public final class LocalFileObject: FileObject {
let allocatedSize: Int64
init(absoluteURL: NSURL, name: String, size: Int64, allocatedSize: Int64, createdDate: NSDate?, modifiedDate: NSDate?, fileType: FileType, isHidden: Bool, isReadOnly: Bool) {
self.allocatedSize = allocatedSize
super.init(absoluteURL: absoluteURL, name: name, size: size, createdDate: createdDate, modifiedDate: modifiedDate, fileType: fileType, isHidden: isHidden, isReadOnly: isReadOnly)
}
}
public class LocalFileProvider: FileProvider, FileProviderMonitor {
public let type = "NSFileManager"
public var isPathRelative: Bool = true
public var baseURL: NSURL? = LocalFileProvider.defaultBaseURL()
public var currentPath: String = ""
public var dispatch_queue: dispatch_queue_t
public var delegate: FileProviderDelegate?
public let credential: NSURLCredential? = nil
let fileManager = NSFileManager()
init () {
dispatch_queue = dispatch_queue_create("FileProvider.\(type)", DISPATCH_QUEUE_SERIAL)
}
init (baseURL: NSURL) {
self.baseURL = baseURL
dispatch_queue = dispatch_queue_create("FileProvider.\(type)", DISPATCH_QUEUE_SERIAL)
}
private static func defaultBaseURL() -> NSURL {
let paths = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true);
return NSURL(fileURLWithPath: paths[0])
}
public func contentsOfDirectoryAtPath(path: String, completionHandler: ((contents: [FileObject], error: ErrorType?) -> Void)) {
dispatch_async(dispatch_queue) {
do {
let contents = try self.fileManager.contentsOfDirectoryAtURL(self.absoluteURL(path), includingPropertiesForKeys: [NSURLNameKey, NSURLFileSizeKey, NSURLFileAllocatedSizeKey, NSURLCreationDateKey, NSURLContentModificationDateKey, NSURLIsHiddenKey, NSURLVolumeIsReadOnlyKey, NSFileGroupOwnerAccountName], options: NSDirectoryEnumerationOptions.SkipsSubdirectoryDescendants)
let filesAttributes = contents.map({ (fileURL) -> LocalFileObject in
return self.attributesOfItemAtURL(fileURL)
})
completionHandler(contents: filesAttributes, error: nil)
} catch let e as NSError {
completionHandler(contents: [], error: e)
}
}
}
private func attributesOfItemAtURL(fileURL: NSURL) -> LocalFileObject {
var namev, sizev, allocated, filetypev, creationDatev, modifiedDatev, hiddenv, readonlyv: AnyObject?
_ = try? fileURL.getResourceValue(&namev, forKey: NSURLNameKey)
_ = try? fileURL.getResourceValue(&sizev, forKey: NSURLFileSizeKey)
_ = try? fileURL.getResourceValue(&allocated, forKey: NSURLFileAllocatedSizeKey)
_ = try? fileURL.getResourceValue(&creationDatev, forKey: NSURLCreationDateKey)
_ = try? fileURL.getResourceValue(&modifiedDatev, forKey: NSURLContentModificationDateKey)
_ = try? fileURL.getResourceValue(&filetypev, forKey: NSURLFileResourceTypeKey)
_ = try? fileURL.getResourceValue(&hiddenv, forKey: NSURLIsHiddenKey)
_ = try? fileURL.getResourceValue(&readonlyv, forKey: NSURLVolumeIsReadOnlyKey)
let fileAttr = LocalFileObject(absoluteURL: fileURL, name: namev as! String, size: sizev?.longLongValue ?? -1, allocatedSize: allocated?.longLongValue ?? -1, createdDate: creationDatev as? NSDate, modifiedDate: modifiedDatev as? NSDate, fileType: FileType(urlResourceTypeValue: filetypev as? String ?? ""), isHidden: hiddenv?.boolValue ?? false, isReadOnly: readonlyv?.boolValue ?? false)
return fileAttr
}
public func attributesOfItemAtPath(path: String, completionHandler: ((attributes: FileObject?, error: ErrorType?) -> Void)) {
dispatch_async(dispatch_queue) {
completionHandler(attributes: self.attributesOfItemAtURL(self.absoluteURL(path)), error: nil)
}
}
public func createFolder(folderName: String, atPath: String, completionHandler: SimpleCompletionHandler) {
dispatch_async(dispatch_queue) {
do {
try self.fileManager.createDirectoryAtURL(self.absoluteURL(atPath).URLByAppendingPathComponent(folderName), withIntermediateDirectories: true, attributes: [:])
completionHandler?(error: nil)
dispatch_async(dispatch_get_main_queue(), {
self.delegate?.fileproviderSucceed(self, operation: .Create(path: (atPath as NSString).stringByAppendingPathComponent(folderName) + "/"))
})
} catch let e as NSError {
completionHandler?(error: e)
dispatch_async(dispatch_get_main_queue(), {
self.delegate?.fileproviderFailed(self, operation: .Create(path: (atPath as NSString).stringByAppendingPathComponent(folderName) + "/"))
})
}
}
}
public func createFile(fileAttribs: FileObject, atPath: String, contents data: NSData?, completionHandler: SimpleCompletionHandler) {
dispatch_async(dispatch_queue) {
let fileURL = self.absoluteURL(atPath).URLByAppendingPathComponent(fileAttribs.name)
var attributes = [String : AnyObject]()
if let createdDate = fileAttribs.createdDate {
attributes[NSFileCreationDate] = createdDate
}
if let modDate = fileAttribs.modifiedDate {
attributes[NSFileModificationDate] = modDate
}
if fileAttribs.isReadOnly {
attributes[NSFilePosixPermissions] = NSNumber(short: 365 /*555 o*/)
}
let success = self.fileManager.createFileAtPath(fileURL.path!, contents: data, attributes: attributes)
if success {
do {
try fileURL.setResourceValue(fileAttribs.isHidden, forKey: NSURLIsHiddenKey)
} catch _ {}
completionHandler?(error: nil)
dispatch_async(dispatch_get_main_queue(), {
self.delegate?.fileproviderSucceed(self, operation: .Create(path: (atPath as NSString).stringByAppendingPathComponent(fileAttribs.name)))
})
} else {
completionHandler?(error: self.throwError(atPath, code: NSURLError.CannotCreateFile))
dispatch_async(dispatch_get_main_queue(), {
self.delegate?.fileproviderFailed(self, operation: .Create(path: (atPath as NSString).stringByAppendingPathComponent(fileAttribs.name)))
})
}
}
}
public func moveItemAtPath(path: String, toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) {
// FIXME: progress
dispatch_async(dispatch_queue) {
if !overwrite && self.fileManager.fileExistsAtPath(self.absoluteURL(toPath).path ?? "") {
completionHandler?(error: self.throwError(toPath, code: NSURLError.CannotMoveFile))
return
}
do {
try self.fileManager.moveItemAtURL(self.absoluteURL(path), toURL: self.absoluteURL(toPath))
completionHandler?(error: nil)
dispatch_async(dispatch_get_main_queue(), {
self.delegate?.fileproviderSucceed(self, operation: .Move(source: path, destination: toPath))
})
} catch let e as NSError {
completionHandler?(error: e)
dispatch_async(dispatch_get_main_queue(), {
self.delegate?.fileproviderFailed(self, operation: .Move(source: path, destination: toPath))
})
}
}
}
public func copyItemAtPath(path: String, toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) {
// FIXME: progress, for files > 100mb, monitor file by another thread, for dirs check copied items count
dispatch_async(dispatch_queue) {
if !overwrite && self.fileManager.fileExistsAtPath(self.absoluteURL(toPath).path ?? "") {
completionHandler?(error: self.throwError(toPath, code: NSURLError.CannotWriteToFile))
return
}
do {
try self.fileManager.copyItemAtURL(self.absoluteURL(path), toURL: self.absoluteURL(toPath))
completionHandler?(error: nil)
dispatch_async(dispatch_get_main_queue(), {
self.delegate?.fileproviderSucceed(self, operation: .Copy(source: path, destination: toPath))
})
} catch let e as NSError {
completionHandler?(error: e)
dispatch_async(dispatch_get_main_queue(), {
self.delegate?.fileproviderFailed(self, operation: .Copy(source: path, destination: toPath))
})
}
}
}
public func removeItemAtPath(path: String, completionHandler: SimpleCompletionHandler) {
dispatch_async(dispatch_queue) {
do {
try self.fileManager.removeItemAtURL(self.absoluteURL(path))
completionHandler?(error: nil)
dispatch_async(dispatch_get_main_queue(), {
self.delegate?.fileproviderSucceed(self, operation: .Remove(path: path))
})
} catch let e as NSError {
completionHandler?(error: e)
dispatch_async(dispatch_get_main_queue(), {
self.delegate?.fileproviderFailed(self, operation: .Remove(path: path))
})
}
}
}
public func copyLocalFileToPath(localFile: NSURL, toPath: String, completionHandler: SimpleCompletionHandler) {
dispatch_async(dispatch_queue) {
do {
try self.fileManager.copyItemAtURL(localFile, toURL: self.absoluteURL(toPath))
completionHandler?(error: nil)
dispatch_async(dispatch_get_main_queue(), {
self.delegate?.fileproviderSucceed(self, operation: .Copy(source: localFile.absoluteString, destination: toPath))
})
} catch let e as NSError {
completionHandler?(error: e)
dispatch_async(dispatch_get_main_queue(), {
self.delegate?.fileproviderFailed(self, operation: .Copy(source: localFile.absoluteString, destination: toPath))
})
}
}
}
public func copyPathToLocalFile(path: String, toLocalURL: NSURL, completionHandler: SimpleCompletionHandler) {
dispatch_async(dispatch_queue) {
do {
try self.fileManager.copyItemAtURL(self.absoluteURL(path), toURL: toLocalURL)
completionHandler?(error: nil)
dispatch_async(dispatch_get_main_queue(), {
self.delegate?.fileproviderSucceed(self, operation: .Copy(source: path, destination: toLocalURL.absoluteString))
})
} catch let e as NSError {
completionHandler?(error: e)
dispatch_async(dispatch_get_main_queue(), {
self.delegate?.fileproviderFailed(self, operation: .Copy(source: path, destination: toLocalURL.absoluteString))
})
}
}
}
public func contentsAtPath(path: String, completionHandler: ((contents: NSData?, error: ErrorType?) -> Void)) {
dispatch_async(dispatch_queue) {
let data = self.fileManager.contentsAtPath(self.absoluteURL(path).path!)
completionHandler(contents: data, error: nil)
}
}
public func contentsAtPath(path: String, offset: Int64, length: Int, completionHandler: ((contents: NSData?, error: ErrorType?) -> Void)) {
// Unfortunatlely there is no method provided in NSFileManager to read a segment of file.
// So we have to fallback to POSIX provided methods
dispatch_async(dispatch_queue) {
let aPath = self.absoluteURL(path).path!
if self.attributesOfItemAtURL(self.absoluteURL(path)).fileType == .Directory {
self.throwError(path, code: NSURLError.FileIsDirectory)
}
if !self.fileManager.fileExistsAtPath(aPath) {
self.throwError(path, code: NSURLError.FileDoesNotExist)
}
let fd_from = open(aPath, O_RDONLY)
if fd_from < 0 {
completionHandler(contents: nil, error: self.throwError(path, code: NSURLError.CannotOpenFile))
}
defer { precondition(close(fd_from) >= 0) }
lseek(fd_from, offset, SEEK_SET)
var buf = [UInt8](count: length, repeatedValue: 0)
let nread = read(fd_from, &buf, buf.count)
if nread < 0 { self.throwError(path, code: NSURLError.NoPermissionsToReadFile) }
if nread == 0 {
completionHandler(contents: nil, error: nil)
} else {
let data = NSData(bytesNoCopy: &buf, length: nread, freeWhenDone: true)
completionHandler(contents: data, error: nil)
}
}
}
public func writeContentsAtPath(path: String, contents data: NSData, atomically: Bool, completionHandler: SimpleCompletionHandler) {
dispatch_async(dispatch_queue) {
data.writeToURL(self.absoluteURL(path), atomically: atomically)
dispatch_async(dispatch_get_main_queue(), {
self.delegate?.fileproviderSucceed(self, operation: .Modify(path: path))
})
}
}
public func searchFilesAtPath(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: ((files: [FileObject], error: ErrorType?) -> Void)) {
dispatch_async(dispatch_queue) {
let iterator = self.fileManager.enumeratorAtURL(self.absoluteURL(path), includingPropertiesForKeys: nil, options: recursive ? NSDirectoryEnumerationOptions() : .SkipsSubdirectoryDescendants) { (url, e) -> Bool in
completionHandler(files: [], error: e)
return true
}
var result = [LocalFileObject]()
while let fileURL = iterator?.nextObject() as? NSURL {
if fileURL.lastPathComponent?.lowercaseString.containsString(query.lowercaseString) ?? false {
let fileObject = self.attributesOfItemAtURL(fileURL)
result.append(self.attributesOfItemAtURL(fileURL))
foundItemHandler?(fileObject)
}
}
completionHandler(files: result, error: nil)
}
}
private var monitorDictionary = [String : LocalFolderMonitor]()
public func registerNotifcation(path: String, eventHandler: (() -> Void)) {
self.unregisterNotifcation(path)
var isdirv: AnyObject?
do {
try absoluteURL(path).getResourceValue(&isdirv, forKey: NSURLIsDirectoryKey)
} catch _ {
}
if !(isdirv?.boolValue ?? false) {
return
}
let monitor = LocalFolderMonitor(url: absoluteURL(path)) {
eventHandler()
}
monitor.start()
monitorDictionary[path] = monitor
}
public func unregisterNotifcation(path: String) {
if let prevMonitor = monitorDictionary[path] {
prevMonitor.stop()
monitorDictionary.removeValueForKey(path)
}
}
}
extension LocalFileProvider {
public func createSymbolicLinkAtPath(path: String, withDestinationPath destPath: String, completionHandler: SimpleCompletionHandler) {
dispatch_async(dispatch_queue) {
do {
try self.fileManager.createSymbolicLinkAtURL(self.absoluteURL(path), withDestinationURL: self.absoluteURL(destPath))
completionHandler?(error: nil)
dispatch_async(dispatch_get_main_queue(), {
self.delegate?.fileproviderSucceed(self, operation: .Link(link: path, target: destPath))
})
} catch let e as NSError {
completionHandler?(error: e)
dispatch_async(dispatch_get_main_queue(), {
self.delegate?.fileproviderFailed(self, operation: .Link(link: path, target: destPath))
})
}
}
}
}
private class LocalFolderMonitor {
private let source: dispatch_source_t
private let descriptor: CInt
private let qq: dispatch_queue_t = dispatch_get_main_queue()
private var state: Bool = false
/// Creates a folder monitor object with monitoring enabled.
init(url: NSURL, handler: ()->Void) {
descriptor = open(url.fileSystemRepresentation, O_EVTONLY)
source = dispatch_source_create(
DISPATCH_SOURCE_TYPE_VNODE,
UInt(descriptor),
DISPATCH_VNODE_WRITE,
qq
)
dispatch_source_set_event_handler(source, handler)
start()
}
/// Starts sending notifications if currently stopped
func start() {
if !state {
state = true
dispatch_resume(source)
}
}
/// Stops sending notifications if currently enabled
func stop() {
if state {
state = false
dispatch_suspend(source)
}
}
deinit {
close(descriptor)
dispatch_source_cancel(source)
}
}
File diff suppressed because it is too large Load Diff
-630
View File
@@ -1,630 +0,0 @@
//
// SMBTransmitter.swift
// FileProvider
//
// Created by Amir Abbas Mousavian.
// Copyright © 2016 Mousavian. Distributed under MIT license.
//
import Foundation
protocol SMBRequest {
func data() -> NSData
}
protocol SMBResponse {
init? (data: NSData)
}
internal func encode<T>(inout value: T) -> NSData {
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
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
}
func sessionSetupForSMB2() -> SMB2.SessionSetupResponse? {
return nil
}
func messageId() -> UInt64 {
defer {
currentMessageID += 1
}
return currentMessageID
}
// MARK: create and analyse messages
class func determineSMBVersion(data: NSData) -> Float {
var smbverChar: Int8 = 0
data.getBytes(&smbverChar, length: 1)
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)
}
class func digestSMB2Message(data: NSData) throws -> (header: SMB2.Header, message: SMBResponse?)? {
guard data.length > 65 else {
throw NSURLError.BadServerResponse
}
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)
switch header.command {
case .NEGOTIATE:
return (header, SMB2.NegotiateResponse(data: messageData))
case .SESSION_SETUP:
return (header, SMB2.SessionSetupResponse(data: messageData))
case .LOGOFF:
return (header, SMB2.LogOff(data: messageData))
case .TREE_CONNECT:
return (header, SMB2.TreeConnectResponse(data: messageData))
case .TREE_DISCONNECT:
return (header, SMB2.TreeDisconnect(data: messageData))
case .CREATE:
return (header, SMB2.CreateResponse(data: messageData))
case .CLOSE:
return (header, SMB2.CloseResponse(data: messageData))
case .FLUSH:
return (header, SMB2.FlushResponse(data: messageData))
case .READ:
return (header, nil) // FIXME:
case .WRITE:
return (header, nil) // FIXME:
case .LOCK:
return (header, nil) // FIXME:
case .IOCTL:
return (header, nil)
case .CANCEL:
return (header, nil)
case .ECHO:
return (header, SMB2.Echo(data: messageData))
case .QUERY_DIRECTORY:
return (header, nil) // FIXME:
case .CHANGE_NOTIFY:
return (header, nil) // FIXME:
case .QUERY_INFO:
return (header, nil) // FIXME:
case .SET_INFO:
return (header, nil) // FIXME:
case .OPLOCK_BREAK:
return (header, nil) // FIXME:
case .INVALID:
throw SMBFileProviderError.InvalidCommand
}
}
class func createSMBMessage(header: SMB1.Header, blocks: [(params: NSData?, message: NSData?)]) -> NSData {
var headerv = header
let result = NSMutableData(data: encode(&headerv))
for block in blocks {
var paramWordsCount = UInt8(block.params?.length ?? 0)
result.appendBytes(&paramWordsCount, length: sizeofValue(paramWordsCount))
if let params = block.params {
result.appendData(params)
}
var messageLen = UInt16(block.message?.length ?? 0)
result.appendBytes(&messageLen, length: sizeofValue(messageLen))
if let message = block.message {
result.appendData(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
}
}
struct SMBTime {
var time: UInt64
init(time: UInt64) {
self.time = time
}
init(unixTime: UInt) {
self.time = (UInt64(unixTime) + 11644473600) * 10000000
}
init(timeIntervalSince1970: NSTimeInterval) {
self.time = UInt64((timeIntervalSince1970 + 11644473600) * 10000000)
}
init(date: NSDate) {
self.init(timeIntervalSince1970: date.timeIntervalSince1970)
}
var unixTime: UInt {
return UInt(self.time / 10000000 - 11644473600)
}
var date: NSDate {
return NSDate(timeIntervalSince1970: Double(self.time) / 10000000 - 11644473600)
}
}
protocol FileProviderSMBHeader {
var protocolID: UInt32 { get }
static var protocolConst: UInt32 { get }
}
// 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
var command: Command {
get {
return Command(rawValue: _command) ?? .INVALID
}
set {
_command = newValue.rawValue
}
}
// error messages from the server to the client
private var _status: (UInt8, UInt8, UInt8, UInt8)
var error: (Class: UInt8, code: UInt16) {
get {
return (_status.0, UInt16(_status.2) + (UInt16(_status.3) << 8))
}
set {
_status = (newValue.Class, 0, UInt8(newValue.code & 0xff), UInt8(newValue.code >> 8))
}
}
var ntStatus: UInt32 {
get {
let statusLo = UInt32(_status.0) + UInt32(_status.1) << 8
let statusHi = UInt32(_status.2) + UInt32(_status.3) << 8
return statusHi << 16 + statusLo
}
set {
_status = (UInt8(newValue & 0xff), UInt8(newValue >> 8 & 0xff), UInt8(newValue >> 16 & 0xff), UInt8(newValue >> 24 & 0xff))
}
}
var flags: Flags
var flags2: Flags2
var pidHigh: UInt16
// encryption key used for validating messages over connectionless transports
private var _securityKey: (UInt16, UInt16)
var securityKey: UInt32 {
get {
return UInt32(_securityKey.1) << 16 + UInt32(_securityKey.0)
}
set {
_securityKey = (UInt16(newValue & 0xffff), UInt16(newValue >> 16))
}
}
/// Connection identifier
var securityCID: UInt16
/// Identifier of the sequence of a message over connectionless transports
var securitySequenceNumber: UInt16
private var ununsed: UInt16
var treeId: UInt16
var pidLow: UInt16
var userId: UInt16
var multiplexId: UInt16
var pid: UInt32 {
get {
return UInt32(pidLow) + UInt32(pidHigh) << 16
}
set {
pidLow = UInt16(newValue & 0xffff)
pidHigh = UInt16(newValue >> 16)
}
}
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) {
self.protocolID = Header.protocolConst
self._command = command.rawValue
_status = (UInt8(ntStatus & 0xff), UInt8(ntStatus >> 8 & 0xff), UInt8(ntStatus >> 16 & 0xff), UInt8(ntStatus >> 24 & 0xff))
self.flags = flags
self.flags2 = flags2
self._securityKey = (UInt16(securityKey & 0xffff), UInt16(securityKey >> 16))
self.securityCID = securityCID
self.securitySequenceNumber = securitySequenceNumber
self.ununsed = 0
self.treeId = treeId
self.pidLow = UInt16(pid & 0xffff)
self.pidHigh = UInt16(pid >> 16)
self.userId = userId
self.multiplexId = multiplexId
}
}
struct Flags: OptionSetType {
let rawValue: UInt8
init(rawValue: UInt8) {
self.rawValue = rawValue
}
/** This bit is set (1) in the NEGOTIATE (0x72) Response if the server supports
* LOCK_AND_READ (0x13) and WRITE_AND_UNLOCK (0x14) commands. */
static let LOCK_AND_READ_OK = Flags(rawValue: 0x01)
static let BUF_AVAIL = Flags(rawValue: 0x02)
static let CASE_INSENSITIVE = Flags(rawValue: 0x08)
static let CANONICALIZED_PATHS = Flags(rawValue: 0x10)
static let OPLOCK = Flags(rawValue: 0x20)
static let OPBATCH = Flags(rawValue: 0x40)
/** When on, this message is being sent from the server in response to a client request. */
static let REPLY = Flags(rawValue: 0x80)
}
struct Flags2: OptionSetType {
let rawValue: UInt16
init(rawValue: UInt16) {
self.rawValue = rawValue
}
/** Client: the message MAY contain long file names. */
static let LONG_NAMES = Flags2(rawValue: 0x0001)
/** Client: the client is aware of extended attributes (EAs). */
static let EAS = Flags2(rawValue: 0x0002)
/** Client: the client is requesting signing (if signing is not yet active) or the message
* being sent is signed. This bit is used on the SMB header of an SESSION_SETUP_ANDX */
static let SMB_SECURITY_SIGNATURE = Flags2(rawValue: 0x0004)
/// Reserved but not implemented.
static let IS_LONG_NAME = Flags2(rawValue: 0x0040)
/** client aware of Extended Security negotiation */
static let EXT_SEC = Flags2(rawValue: 0x0800)
/** any pathnames in this SMB SHOULD be resolved in the Distributed File System (DFS) */
static let DFS = Flags2(rawValue: 0x1000)
/**
* This flag is useful only on a read request. If the bit is set, then the client MAY read
* the file if the client does not have read permission but does have execute permission. */
static let PAGING_IO = Flags2(rawValue: 0x2000) // READ_IF_EXECUTE
/**
* Client: the server MUST return errors as 32-bit NTSTATUS codes in the response
* Server: the Status field in the header is formatted as an NTSTATUS cod */
static let ERR_STATUS = Flags2(rawValue: 0x4000)
/**
* Each field that contains a string in this SMB message MUST be encoded
* as an array of 16-bit Unicode characters */
static let UNICODE = Flags2(rawValue: 0x8000)
}
enum Command: UInt8 {
case CREATE_DIRECTORY = 0x00
case DELETE_DIRECTORY = 0x01
case OPEN = 0x02
case CREATE = 0x03
case CLOSE = 0x04
case FLUSH = 0x05
case DELETE = 0x06
case RENAME = 0x07
case QUERY_INFORMATION = 0x08
case SET_INFORMATION = 0x09
case READ = 0x0A
case WRITE = 0x0B
case LOCK_BYTE_RANGE = 0x0C
case UNLOCK_BYTE_RANGE = 0x0D
case CREATE_TEMPORARY = 0x0E
case CREATE_NEW = 0x0F
case CHECK_DIRECTORY = 0x10
case PROCESS_EXIT = 0x11
case SEEK = 0x12
case LOCK_AND_READ = 0x13
case WRITE_AND_UNLOCK = 0x14
case READ_RAW = 0x1A
case READ_MPX = 0x1B
case READ_MPX_SECONDARY = 0x1C
case WRITE_RAW = 0x1D
case WRITE_MPX = 0x1E
case WRITE_COMPLETE = 0x20
case SET_INFORMATION2 = 0x22
case QUERY_INFORMATION2 = 0x23
case LOCKING_ANDX = 0x24
case TRANSACTION = 0x25
case TRANSACTION_SECONDARY = 0x26
case IOCTL = 0x27
case IOCTL_SECONDARY = 0x28
case COPY = 0x29
case MOVE = 0x2A
case ECHO = 0x2B
case WRITE_AND_CLOSE = 0x2C
case OPEN_ANDX = 0x2D
case READ_ANDX = 0x2E
case WRITE_ANDX = 0x2F
case CLOSE_AND_TREE_DISC = 0x31
case TRANSACTION2 = 0x32
case TRANSACTION2_SECONDARY = 0x33
case FIND_CLOSE2 = 0x34
case FIND_NOTIFY_CLOSE = 0x35
case TREE_CONNECT = 0x70
case TREE_DISCONNECT = 0x71
case NEGOTIATE = 0x72
case SESSION_SETUP_ANDX = 0x73
case LOGOFF_ANDX = 0x74
case TREE_CONNECT_ANDX = 0x75
case QUERY_INFORMATION_DISK = 0x80
case SEARCH = 0x81
case FIND = 0x82
case FIND_UNIQUE = 0x83
case NT_TRANSACT = 0xA0
case NT_TRANSACT_SECONDARY = 0xA1
case NT_CREATE_ANDX = 0xA2
case NT_CANCEL = 0xA4
case OPEN_PRINT_FILE = 0xC0
case WRITE_PRINT_FILE = 0xC1
case CLOSE_PRINT_FILE = 0xC2
case GET_PRINT_QUEUE = 0xC3
case READ_BULK = 0xD8
case WRITE_BULK = 0xD9
case WRITE_BULK_DATA = 0xDA
case INVALID = 0xFE
}
}
/// Error Types and Description
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
var description: String {
switch self {
case NOT_IMPLEMENTED, INVALID_DEVICE_REQUEST, ILLEGAL_FUNCTION:
return "Invalid Function."
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:
return "A component in the path prefix is not a directory."
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:
return "Access denied."
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:
return "Insufficient server memory to perform the requested operation."
case OS2_INVALID_ACCESS:
return "Invalid open mode."
case DATA_ERROR:
return "Bad data. (May be generated by IOCTL calls on the server.)"
case DIRECTORY_NOT_EMPTY:
return "Remove of directory failed because it was not empty."
case NOT_SAME_DEVICE:
return "A file system operation (such as a rename) across two devices was attempted."
case NO_MORE_FILES:
return "No (more) files found following a file search command."
case UNSUCCESSFUL:
return "General error."
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:
return "A lock request specified an invalid locking mode, or conflicted with an existing file lock."
case END_OF_FILE:
return "Attempted to read beyond the end of the file."
case NOT_SUPPORTED:
return "This command is not supported by the server."
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:
return "A parameter supplied with the message is invalid."
case OS2_INVALID_LEVEL:
return "Invalid information level."
case OS2_NEGATIVE_SEEK:
return "An attempt was made to seek to a negative absolute offset within a file."
case RANGE_NOT_LOCKED:
return "The byte range specified in an unlock request was not locked."
case OS2_NO_MORE_SIDS:
return "Maximum number of searches has been exhausted."
case OS2_CANCEL_VIOLATION:
return "No lock request was outstanding for the supplied cancel region."
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:
return "Invalid named pipe."
case OS2_CANNOT_COPY:
return "The copy functions cannot be used."
case INSTANCE_NOT_AVAILABLE, PIPE_NOT_AVAILABLE, PIPE_BUSY:
return "All instances of the designated named pipe are busy."
case PIPE_CLOSING, PIPE_EMPTY:
return "The designated named pipe is in the process of being closed."
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:
return "There is more data available to read on the designated named pipe."
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:
return "The server file system does not support Extended Attributes."
case OS2_EA_ACCESS_DENIED:
return "Access to the extended attribute was denied."
default:
return ""
}
}
}
-116
View File
@@ -1,116 +0,0 @@
//
// SambaFileProvider.swift
// FileProvider
//
// Created by Amir Abbas Mousavian.
// Copyright © 2016 Mousavian. Distributed under MIT license.
//
import Foundation
class SMBFileProvider: FileProvider, FileProviderMonitor {
var type: String = "Samba"
var isPathRelative: Bool = true
var baseURL: NSURL?
var currentPath: String = ""
var dispatch_queue: dispatch_queue_t
var delegate: FileProviderDelegate?
let credential: NSURLCredential?
typealias FileObjectClass = FileObject
init? (baseURL: NSURL, credential: NSURLCredential, afterInitialized: SimpleCompletionHandler) {
guard baseURL.scheme.lowercaseString == "smb" else {
return nil
}
self.baseURL = baseURL
dispatch_queue = dispatch_queue_create("FileProvider.\(type)", DISPATCH_QUEUE_CONCURRENT)
//let url = baseURL.absoluteString
self.credential = credential
}
func contentsOfDirectoryAtPath(path: String, completionHandler: ((contents: [FileObjectClass], error: ErrorType?) -> Void)) {
NotImplemented()
dispatch_async(dispatch_queue) {
}
}
func attributesOfItemAtPath(path: String, completionHandler: ((attributes: FileObjectClass?, error: ErrorType?) -> Void)) {
NotImplemented()
}
func createFolder(folderName: String, atPath: String, completionHandler: SimpleCompletionHandler) {
NotImplemented()
}
func createFile(fileAttribs: FileObject, atPath: String, contents data: NSData?, completionHandler: SimpleCompletionHandler) {
NotImplemented()
}
func moveItemAtPath(path: String, toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) {
NotImplemented()
}
func copyItemAtPath(path: String, toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) {
NotImplemented()
}
func removeItemAtPath(path: String, completionHandler: SimpleCompletionHandler) {
NotImplemented()
}
func copyLocalFileToPath(localFile: NSURL, toPath: String, completionHandler: SimpleCompletionHandler) {
NotImplemented()
}
func copyPathToLocalFile(path: String, toLocalURL: NSURL, completionHandler: SimpleCompletionHandler) {
NotImplemented()
}
func contentsAtPath(path: String, completionHandler: ((contents: NSData?, error: ErrorType?) -> Void)) {
NotImplemented()
}
func contentsAtPath(path: String, offset: Int64, length: Int, completionHandler: ((contents: NSData?, error: ErrorType?) -> Void)) {
NotImplemented()
}
func writeContentsAtPath(path: String, contents data: NSData, atomically: Bool, completionHandler: SimpleCompletionHandler) {
NotImplemented()
}
func searchFilesAtPath(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObjectClass) -> Void)?, completionHandler: ((files: [FileObjectClass], error: ErrorType?) -> Void)) {
NotImplemented()
}
func registerNotifcation(path: String, eventHandler: (() -> Void)) {
NotImplemented()
}
func unregisterNotifcation(path: String) {
NotImplemented()
}
}
// MARK: basic CIFS interactivity
enum SMBFileProviderError: Int, ErrorType, CustomStringConvertible {
case BadHeader
case IncompatibleHeader
case IncorrectParamsLength
case IncorrectMessageLength
case InvalidCommand
var description: String {
return "SMB message structure is invalid"
}
}
extension SMBFileProvider {
private func getPID() -> UInt32 {
return UInt32(NSProcessInfo.processInfo().processIdentifier)
}
}
-249
View File
@@ -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
*/
init?(baseURL: NSURL, secure: Bool = false) {
self.baseURL = baseURL
self.secureConnection = secure
let scheme = baseURL.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
}
}
-551
View File
@@ -1,551 +0,0 @@
//
// WebDAVFileProvider.swift
// FileProvider
//
// Created by Amir Abbas Mousavian.
// Copyright © 2016 Mousavian. Distributed under MIT license.
//
import Foundation
public enum FileProviderWebDavErrorCode: Int {
case OK = 200
case Created = 201
case NoContent = 204
case MultiStatus = 207
case Forbidden = 403
case MethodNotAllowed = 405
case Conflict = 409
case PreconditionFailed = 412
case UnsupportedMediaType = 415
case Locked = 423
case FailedDependency = 424
case BadGateway = 502
case InsufficientStorage = 507
}
public struct FileProviderWebDavError: ErrorType, CustomStringConvertible {
let code: FileProviderWebDavErrorCode
let url: NSURL
public var description: String {
switch code {
case .OK: return "OK"
case .Created: return "Created"
case .NoContent: return "No Content"
case .MultiStatus: return ""
case .Forbidden: return "Forbidden"
case .MethodNotAllowed: return "Method Not Allowed"
case .Conflict: return "Conflict"
case .PreconditionFailed: return "Precondition Failed"
case .UnsupportedMediaType: return "Unsupported Media Type"
case .Locked: return "Locked"
case .FailedDependency: return "Failed Dependency"
case .BadGateway: return "Bad Gateway"
case .InsufficientStorage: return "Insufficient Storage"
}
}
}
public final class WebDavFileObject: FileObject {
let contentType: String
let entryTag: String?
init(absoluteURL: NSURL, name: String, size: Int64, contentType: String, createdDate: NSDate?, modifiedDate: NSDate?, fileType: FileType, isHidden: Bool, isReadOnly: Bool, entryTag: String?) {
self.contentType = contentType
self.entryTag = entryTag
super.init(absoluteURL: absoluteURL, name: name, 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 WebDAVFileProvider: 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 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!
}
init? (baseURL: NSURL, credential: NSURLCredential?) {
if !["http", "https"].contains(baseURL.scheme.lowercaseString) {
return nil
}
self.baseURL = baseURL
dispatch_queue = dispatch_queue_create("FileProvider.\(type)", DISPATCH_QUEUE_CONCURRENT)
//let url = baseURL.absoluteString
self.credential = credential
}
deinit {
_session?.invalidateAndCancel()
}
public func contentsOfDirectoryAtPath(path: String, completionHandler: ((contents: [FileObject], error: ErrorType?) -> Void)) {
let url = absoluteURL(path)
let request = NSMutableURLRequest(URL: url)
request.HTTPMethod = "PROPFIND"
request.setValue(baseURL?.absoluteString, forHTTPHeaderField: "Host")
request.setValue("1", forHTTPHeaderField: "Depth")
request.setValue("text/xml; charset=\"utf-8\"", forHTTPHeaderField: "Content-Type")
request.HTTPBody = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n<D:allprop/></D:propfind>".dataUsingEncoding(NSUTF8StringEncoding)
request.setValue(String(request.HTTPBody!.length), forHTTPHeaderField: "Content-Length")
let task = session.dataTaskWithRequest(request) { (data, response, error) in
if let data = data {
let xresponse = self.parseXMLResponse(data)
var fileObjects = [WebDavFileObject]()
for attr in xresponse {
if attr.href.path == url.path {
continue
}
fileObjects.append(self.mapToFileObject(attr))
}
completionHandler(contents: fileObjects, error: error)
return
}
completionHandler(contents: [], error: error)
}
task.resume()
}
public func attributesOfItemAtPath(path: String, completionHandler: ((attributes: FileObject?, error: ErrorType?) -> Void)) {
let request = NSMutableURLRequest(URL: absoluteURL(path))
request.HTTPMethod = "PROPFIND"
request.setValue(baseURL?.absoluteString, forHTTPHeaderField: "Host")
request.setValue("1", forHTTPHeaderField: "Depth")
request.setValue("text/xml; charset=\"utf-8\"", forHTTPHeaderField: "Content-Type")
request.HTTPBody = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n<D:allprop/></D:propfind>".dataUsingEncoding(NSUTF8StringEncoding)
request.setValue(String(request.HTTPBody!.length), forHTTPHeaderField: "Content-Length")
let task = session.dataTaskWithRequest(request) { (data, response, error) in
if let data = data {
let xresponse = self.parseXMLResponse(data)
if let attr = xresponse.first {
completionHandler(attributes: self.mapToFileObject(attr), error: error)
return
}
}
completionHandler(attributes: nil, error: error)
}
task.resume()
}
}
extension WebDAVFileProvider: FileProviderOperations {
public func createFolder(folderName: String, atPath: String, completionHandler: SimpleCompletionHandler) {
let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
let url = absoluteURL((atPath as NSString).stringByAppendingPathComponent(folderName) + "/")
let request = NSMutableURLRequest(URL: url)
request.HTTPMethod = "MKCOL"
request.setValue(baseURL?.absoluteString, forHTTPHeaderField: "Host")
let task = session.dataTaskWithRequest(request) { (data, response, error) in
if let response = response as? NSHTTPURLResponse, let code = FileProviderWebDavErrorCode(rawValue: response.statusCode) where code != .OK {
completionHandler?(error: FileProviderWebDavError(code: code, url: url))
return
}
completionHandler?(error: error)
self.delegateNotify(.Create(path: (atPath as NSString).stringByAppendingPathComponent(folderName) + "/"), error: error)
}
task.resume()
}
public func createFile(fileAttribs: FileObject, atPath path: String, contents data: NSData?, completionHandler: SimpleCompletionHandler) {
let request = NSMutableURLRequest(URL: absoluteURL(path))
request.HTTPMethod = "PUT"
let task = session.uploadTaskWithRequest(request, fromData: data) { (data, response, error) in
completionHandler?(error: error)
self.delegateNotify(.Create(path: (path as NSString).stringByAppendingPathComponent(fileAttribs.name)), error: error)
}
task.taskDescription = self.dictionaryToJSON(["type": "Create", "source": (path as NSString).stringByAppendingPathComponent(fileAttribs.name)])
task.resume()
}
public func moveItemAtPath(path: String, toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) {
let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
let url = absoluteURL(path)
let request = NSMutableURLRequest(URL: url)
request.HTTPMethod = "MOVE"
request.setValue(baseURL?.absoluteString, forHTTPHeaderField: "Host")
request.setValue(absoluteURL(path).absoluteString, forHTTPHeaderField: "Destination")
if !overwrite {
request.setValue("F", forHTTPHeaderField: "Overwrite")
}
let task = session.dataTaskWithRequest(request) { (data, response, error) in
if let response = response as? NSHTTPURLResponse, let code = FileProviderWebDavErrorCode(rawValue: response.statusCode) {
defer {
self.delegateNotify(.Move(source: path, destination: toPath), error: error)
}
if code == .MultiStatus, let data = data {
let xresponses = self.parseXMLResponse(data)
for xresponse in xresponses {
if xresponse.status >= 300 {
completionHandler?(error: FileProviderWebDavError(code: code, url: url))
}
}
} else {
completionHandler?(error: FileProviderWebDavError(code: code, url: url))
}
return
}
completionHandler?(error: error)
}
task.resume()
}
public func copyItemAtPath(path: String, toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) {
let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
let url = absoluteURL(path)
let request = NSMutableURLRequest(URL: url)
request.HTTPMethod = "COPY"
request.setValue(baseURL?.absoluteString, forHTTPHeaderField: "Host")
request.setValue(absoluteURL(path).absoluteString, forHTTPHeaderField: "Destination")
if !overwrite {
request.setValue("F", forHTTPHeaderField: "Overwrite")
}
let task = session.dataTaskWithRequest(request) { (data, response, error) in
if let response = response as? NSHTTPURLResponse, let code = FileProviderWebDavErrorCode(rawValue: response.statusCode) {
defer {
self.delegateNotify(.Copy(source: path, destination: toPath), error: error)
}
if code == .MultiStatus, let data = data {
let xresponses = self.parseXMLResponse(data)
for xresponse in xresponses {
if xresponse.status >= 300 {
completionHandler?(error: FileProviderWebDavError(code: code, url: url))
}
}
} else {
completionHandler?(error: FileProviderWebDavError(code: code, url: url))
}
return
}
completionHandler?(error: error)
}
task.resume()
}
public func removeItemAtPath(path: String, completionHandler: SimpleCompletionHandler) {
let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
let url = absoluteURL(path)
let request = NSMutableURLRequest(URL: url)
request.HTTPMethod = "DELETE"
request.setValue(baseURL?.absoluteString, forHTTPHeaderField: "Host")
let task = session.dataTaskWithRequest(request) { (data, response, error) in
if let response = response as? NSHTTPURLResponse, let code = FileProviderWebDavErrorCode(rawValue: response.statusCode) {
defer {
self.delegateNotify(.Remove(path: path), error: error)
}
if code == .MultiStatus, let data = data {
let xresponses = self.parseXMLResponse(data)
for xresponse in xresponses {
if xresponse.status >= 300 {
completionHandler?(error: FileProviderWebDavError(code: code, url: url))
}
}
} else {
completionHandler?(error: FileProviderWebDavError(code: code, url: url))
}
return
}
completionHandler?(error: error)
}
task.resume()
}
public func copyLocalFileToPath(localFile: NSURL, toPath: String, completionHandler: SimpleCompletionHandler) {
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.absoluteString, destination: toPath), error: error)
}
task.taskDescription = self.dictionaryToJSON(["type": "Copy", "source": localFile.absoluteString, "dest": toPath])
task.resume()
}
public func copyPathToLocalFile(path: String, toLocalURL: NSURL, completionHandler: SimpleCompletionHandler) {
let request = NSMutableURLRequest(URL: absoluteURL(path))
let task = session.downloadTaskWithRequest(request) { (sourceFileURL, response, error) in
if let sourceFileURL = sourceFileURL {
do {
try NSFileManager.defaultManager().copyItemAtURL(sourceFileURL, toURL: toLocalURL)
} catch let e {
completionHandler?(error: e)
return
}
}
completionHandler?(error: error)
}
task.taskDescription = self.dictionaryToJSON(["type": "Copy", "source": path, "dest": toLocalURL.absoluteString])
task.resume()
}
}
extension WebDAVFileProvider: FileProviderReadWrite {
public func contentsAtPath(path: String, completionHandler: ((contents: NSData?, error: ErrorType?) -> Void)) {
let request = NSMutableURLRequest(URL: absoluteURL(path))
request.HTTPMethod = "GET"
let task = session.dataTaskWithRequest(request) { (data, response, error) in
completionHandler(contents: data, error: error)
}
task.resume()
}
public func contentsAtPath(path: String, offset: Int64, length: Int, completionHandler: ((contents: NSData?, error: ErrorType?) -> Void)) {
let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
let request = NSMutableURLRequest(URL: absoluteURL(path))
request.HTTPMethod = "GET"
request.setValue("bytes=\(offset)-\(offset + length)", forHTTPHeaderField: "Range")
let task = session.dataTaskWithRequest(request) { (data, response, error) in
completionHandler(contents: data, error: error)
}
task.resume()
}
public func writeContentsAtPath(path: String, contents data: NSData, atomically: Bool = false, completionHandler: SimpleCompletionHandler) {
// FIXME: lock destination before writing process
let url = atomically ? absoluteURL(path).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()
}
public func searchFilesAtPath(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: ((files: [FileObject], error: ErrorType?) -> Void)) {
let url = absoluteURL(path)
let request = NSMutableURLRequest(URL: url)
request.HTTPMethod = "PROPFIND"
request.setValue(baseURL?.absoluteString, forHTTPHeaderField: "Host")
//request.setValue("1", forHTTPHeaderField: "Depth")
request.setValue("text/xml; charset=\"utf-8\"", forHTTPHeaderField: "Content-Type")
request.HTTPBody = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n<D:allprop/></D:propfind>".dataUsingEncoding(NSUTF8StringEncoding)
request.setValue(String(request.HTTPBody!.length), forHTTPHeaderField: "Content-Length")
let task = session.dataTaskWithRequest(request) { (data, response, error) in
// FIXME: paginating results
if let data = data {
let xresponse = self.parseXMLResponse(data)
var fileObjects = [WebDavFileObject]()
for attr in xresponse {
if let path = attr.href.path where !((path as NSString).lastPathComponent.containsString(query)) {
continue
}
let fileObject = self.mapToFileObject(attr)
fileObjects.append(fileObject)
foundItemHandler?(fileObject)
}
completionHandler(files: fileObjects, error: error)
return
}
completionHandler(files: [], error: error)
}
task.resume()
}
private func registerNotifcation(path: String, eventHandler: (() -> Void)) {
/* There is no unified api for monitoring WebDAV server content change/update
* Microsoft Exchange uses SUBSCRIBE method, Apple uses push notification system.
* while both is unavailable in a mobile platform.
* A messy approach is listing a directory with an interval period and compare
* with previous results
*/
}
private func unregisterNotifcation(path: String) {
}
// TODO: implements methods for lock mechanism
}
// MARK: WEBDAV XML response implementation
internal extension WebDAVFileProvider {
struct DavResponse {
let href: NSURL
let hrefString: String
let status: Int?
let prop: [String: String]
}
private func parseXMLResponse(response: NSData) -> [DavResponse] {
var result = [DavResponse]()
do {
let xml = try AEXMLDocument(xmlData: response)
var rootnode = xml.root
var responsetag = "response"
for node in rootnode.all ?? [] {
if node.name.lowercaseString.hasSuffix("multistatus") {
rootnode = node
}
}
for node in rootnode.children ?? [] {
if node.name.lowercaseString.hasSuffix("response") {
responsetag = node.name
break
}
}
for responseNode in rootnode[responsetag].all ?? [] {
var hreftag = "href"
var statustag = "status"
var propstattag = "propstat"
for node in responseNode.children ?? [] {
if node.name.lowercaseString.hasSuffix("href") {
hreftag = node.name
}
if node.name.lowercaseString.hasSuffix("status") {
statustag = node.name
}
if node.name.lowercaseString.hasSuffix("propstat") {
propstattag = node.name
}
}
let href = responseNode[hreftag].value
if let href = href, hrefURL = NSURL(string: href) {
var status: Int?
let statusDesc = (responseNode[statustag].stringValue).componentsSeparatedByString(" ")
if statusDesc.count > 2 {
status = Int(statusDesc[1])
}
var propDic = [String: String]()
let propStatNode = responseNode[propstattag]
for node in propStatNode.children ?? [] {
if node.name.lowercaseString.hasSuffix("status") {
statustag = node.name
break
}
}
let statusDesc2 = (propStatNode[statustag].stringValue).componentsSeparatedByString(" ")
if statusDesc2.count > 2 {
status = Int(statusDesc2[1])
}
var proptag = "prop"
for tnode in propStatNode.children ?? [] {
if tnode.name.lowercaseString.hasSuffix("prop") {
proptag = tnode.name
}
break
}
for propItemNode in propStatNode[proptag].children ?? [] {
propDic[propItemNode.name.componentsSeparatedByString(":").last!.lowercaseString] = propItemNode.value
if propItemNode.name.hasSuffix("resourcetype") {
if propItemNode.xmlStringCompact.containsString("collection") {
propDic["getcontenttype"] = "httpd/unix-directory"
}
}
}
result.append(DavResponse(href: hrefURL, hrefString: href, status: status, prop: propDic))
}
}
} catch _ {
}
return result
}
private func mapToFileObject(davResponse: DavResponse) -> WebDavFileObject {
var href = davResponse.href
if href.baseURL == nil {
href = absoluteURL(href.path ?? "")
}
let name = davResponse.prop["displayname"] ?? (davResponse.hrefString.stringByRemovingPercentEncoding! as NSString).lastPathComponent
let size = Int64(davResponse.prop["getcontentlength"] ?? "-1") ?? NSURLSessionTransferSizeUnknown
let createdDate = self.resolveRFCDate(davResponse.prop["creationdate"] ?? "")
let modifiedDate = self.resolveRFCDate(davResponse.prop["getlastmodified"] ?? "")
let contentType = davResponse.prop["getcontenttype"] ?? "octet/stream"
let isDirectory = contentType == "httpd/unix-directory"
let entryTag = davResponse.prop["getetag"]
return WebDavFileObject(absoluteURL: href, name: name, size: size, contentType: contentType, createdDate: createdDate, modifiedDate: modifiedDate, fileType: isDirectory ? .Directory : .Regular, isHidden: false, isReadOnly: false, entryTag: entryTag)
}
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)
}
})
}
}
// MARK: URLSession delegate
extension WebDAVFileProvider: 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)
default:
return
}
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))
}
public func URLSession(session: NSURLSession, task: NSURLSessionTask, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {
completionHandler(NSURLSessionAuthChallengeDisposition.UseCredential, credential)
}
public func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {
completionHandler(NSURLSessionAuthChallengeDisposition.UseCredential, credential)
}
}
+128
View File
@@ -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
}
}
+285
View File
@@ -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: "&amp;", options: .literal)
// replace the other four special characters
let escapeChars = ["<" : "&lt;", ">" : "&gt;", "'" : "&apos;", "\"" : "&quot;"]
for (char, echar) in escapeChars {
escaped = escaped.replacingOccurrences(of: char, with: echar, options: .literal)
}
return escaped
}
}
+37
View File
@@ -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
}
+19
View File
@@ -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.
+52
View File
@@ -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() {}
}
+101
View File
@@ -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
}
}
+777
View File
@@ -0,0 +1,777 @@
//
// 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 apps 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(forKey: "containerId") as? String,
let scopeString = aDecoder.decodeObject(forKey: "scope") as? String,
let scope = UbiquitousScope(rawValue: scopeString) {
self.init(containerId: containerId, scope: scope)
} else if let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL {
self.init(baseURL: baseURL)
} else {
return nil
}
self.isCoorinating = aDecoder.decodeBool(forKey: "isCoorinating")
}
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, foundItemHandler: nil, 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) {
dispatch_queue.async {
let pathURL = self.url(of: path)
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: "%K LIKE[CD] %@", NSMetadataItemPathKey, pathURL.path)
query.valueListAttributes = [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]
query.searchScopes = [self.scope.rawValue]
var finishObserver: NSObjectProtocol?
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
defer {
query.stop()
NotificationCenter.default.removeObserver(finishObserver!)
}
query.disableUpdates()
guard let result = (query.results as? [NSMetadataItem])?.first, let attribs = result.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
let error = self.cocoaError(path, code: .fileNoSuchFile)
self.dispatch_queue.async {
completionHandler(nil, error)
}
return
}
if let file = self.mapFileObject(attributes: attribs) {
self.dispatch_queue.async {
completionHandler(file, nil)
}
} else {
let noFileError = self.cocoaError(path, code: .fileNoSuchFile)
self.dispatch_queue.async {
completionHandler(nil, noFileError)
}
}
})
DispatchQueue.main.async {
if !query.start() {
self.dispatch_queue.async {
completionHandler(nil, self.cocoaError(path, code: .fileReadNoPermission))
}
}
}
}
}
/**
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.
*/
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 (Bool) -> Void) {
dispatch_queue.async {
completionHandler(self.fileManager.ubiquityIdentityToken != 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)
monitorFile(path: toPath, operation: operation, progress: progress)
operation_queue.addOperation {
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 self.opFileManager.fileExists(atPath: tmpFile.path) {
try? self.opFileManager.removeItem(at: tmpFile)
}
completionHandler?(error)
self.delegateNotify(operation, error: error)
}
}
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
}
lazy fileprivate var observer: KVOObserver = KVOObserver()
fileprivate func monitorFile(path: String, operation: FileOperationType, progress: Progress?) {
let pathURL = self.url(of: path).standardizedFileURL
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: "%K LIKE[CD] %@", NSMetadataItemPathKey, pathURL.path)
query.valueListAttributes = [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataUbiquitousItemPercentDownloadedKey, NSMetadataUbiquitousItemPercentUploadedKey, NSMetadataUbiquitousItemDownloadingStatusKey, NSMetadataItemFSSizeKey]
query.searchScopes = [self.scope.rawValue]
var context = QueryProgressWrapper(provider: self, progress: progress, operation: operation)
query.addObserver(self.observer, forKeyPath: "results", options: [.initial, .new, .old], context: &context)
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 apps 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 apps 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
}
}
}
struct QueryProgressWrapper {
weak var provider: CloudFileProvider?
weak var progress: Progress?
let operation: FileOperationType
}
fileprivate class KVOObserver: NSObject {
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let query = object as? NSMetadataQuery else {
return
}
guard let wrapper = context?.load(as: QueryProgressWrapper.self) else {
query.stop()
query.removeObserver(self, forKeyPath: "results")
return
}
let provider = wrapper.provider
let progress = wrapper.progress
let operation = wrapper.operation
guard let results = change?[.newKey], let item = (results as? [NSMetadataItem])?.first else {
return
}
query.disableUpdates()
var size = progress?.totalUnitCount ?? -1
if size < 0, let size_d = item.value(forAttribute: NSMetadataItemFSSizeKey) as? Int64 {
size = size_d
progress?.totalUnitCount = size
}
let downloadStatus = item.value(forAttribute: NSMetadataUbiquitousItemPercentDownloadedKey) as? String ?? ""
let downloaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentDownloadedKey) as? Double ?? 0
let uploaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentUploadedKey) as? Double ?? 0
if (downloaded == 0 || downloaded == 100) && (uploaded > 0 && uploaded < 100) {
progress?.completedUnitCount = Int64(uploaded / 100 * Double(size))
provider?.delegateNotify(operation, progress: uploaded / 100)
} else if (uploaded == 0 || uploaded == 100) && downloadStatus != NSMetadataUbiquitousItemDownloadingStatusCurrent {
progress?.completedUnitCount = Int64(downloaded / 100 * Double(size))
provider?.delegateNotify(operation, progress: downloaded / 100)
} else if uploaded == 100 || downloadStatus == NSMetadataUbiquitousItemDownloadingStatusCurrent {
progress?.completedUnitCount = size
query.stop()
query.removeObserver(self, forKeyPath: "results")
provider?.delegateNotify(operation)
}
query.enableUpdates()
}
}
/*
func getMetadataItem(url: URL) -> NSMetadataItem? {
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: "(%K LIKE %@)", NSMetadataItemPathKey, url.path)
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope, NSMetadataQueryUbiquitousDataScope]
var item: NSMetadataItem?
let group = DispatchGroup()
group.enter()
var finishObserver: NSObjectProtocol?
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
defer {
query.stop()
group.leave()
NotificationCenter.default.removeObserver(finishObserver!)
}
if query.resultCount > 0 {
item = query.result(at: 0) as? NSMetadataItem
}
query.disableUpdates()
})
DispatchQueue.main.async {
query.start()
}
_ = group.wait(timeout: .now() + 30)
return item
}
*/
+507
View File
@@ -0,0 +1,507 @@
//
// DropboxFileProvider.swift
// FileProvider
//
// Created by Amir Abbas Mousavian.
// Copyright © 2016 Mousavian. Distributed under MIT license.
//
import Foundation
import CoreGraphics
/**
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" }
/// 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
/**
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 required convenience init?(coder aDecoder: NSCoder) {
self.init(credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential)
self.useCache = aDecoder.decodeBool(forKey: "useCache")
self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache")
}
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
}
/**
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)
}
/**
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.set(httpAuthentication: credential, with: .oAuth2)
request.set(httpContentType: .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 {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) }
if let json = data?.deserializeJSON(), let file = DropboxFileObject(json: json) {
fileObject = file
}
}
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: "users/get_space_usage", relativeTo: apiURL)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.set(httpAuthentication: 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()
}
/**
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.
*/
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)
}
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.set(httpAuthentication: credential, with: .oAuth2)
request.set(httpContentType: .stream)
request.set(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.set(httpAuthentication: credential, with: .oAuth2)
request.set(dropboxArgKey: ["path": correctPath(path)! as NSString])
return request
}
// content operations
switch operation {
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)
}
}
func apiRequest(for operation: FileOperationType, overwrite: Bool = false) -> URLRequest {
let url: String
let sourcePath = operation.source
let destPath = operation.destination
var requestDictionary = [String: AnyObject]()
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)")
}
var request = URLRequest(url: URL(string: url, relativeTo: apiURL)!)
request.httpMethod = "POST"
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(httpContentType: .json)
if let dest = correctPath(destPath) as NSString? {
requestDictionary["from_path"] = correctPath(sourcePath) as NSString?
requestDictionary["to_path"] = dest
} else {
requestDictionary["path"] = correctPath(sourcePath) as NSString?
}
request.httpBody = Data(jsonDictionary: requestDictionary)
return request
}
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) })
}
return FileProviderDropboxError(code: code, path: path ?? "", errorDescription: errorDesc)
}
override var maxUploadSimpleSupported: Int64 {
return 157_286_400 // 150MB
}
/*
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!
* You can implemnt your own webhook service and replace this method accordingly.
*/
NotImplemented()
}
fileprivate func unregisterNotifcation(path: String) {
NotImplemented()
}
*/
// TODO: Implement /get_account & /get_current_account
open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
let url = URL(string: "files/get_temporary_link", relativeTo: apiURL)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(httpContentType: .json)
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString]
request.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() {
if let linkStr = json["link"] as? String {
link = URL(string: linkStr)
}
if let attribDic = json["metadata"] as? [String: AnyObject] {
fileObject = DropboxFileObject(json: attribDic)
}
}
}
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.set(httpAuthentication: credential, with: .oAuth2)
request.set(httpContentType: .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
if let attribDic = json["metadata"] as? [String: AnyObject] {
fileObject = DropboxFileObject(json: attribDic)
}
}
}
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.set(httpAuthentication: credential, with: .oAuth2)
request.set(httpContentType: .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: ExtendedFileProvider {
open func thumbnailOfFileSupported(path: String) -> Bool {
switch (path as NSString).pathExtension.lowercased() {
case "jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff":
return true
case "doc", "docx", "docm", "xls", "xlsx", "xlsm":
return true
case "ppt", "pps", "ppsx", "ppsm", "pptx", "pptm":
return true
case "rtf":
return true
default:
return false
}
}
open func propertiesOfFileSupported(path: String) -> Bool {
let fileExt = (path as NSString).pathExtension.lowercased()
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 false
}
}
/// Default value for dimension is 64x64, according to Dropbox documentation
open func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) {
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
}
var request = URLRequest(url: url)
request.set(httpAuthentication: 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.set(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){
if let dimension = dimension {
image = DropboxFileProvider.scaleDown(image: fetchedimage, toSize: dimension)
} else {
image = fetchedimage
}
}
}
completionHandler(image, error)
})
task.resume()
}
open func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) {
let url = URL(string: "files/get_metadata", relativeTo: apiURL)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(httpContentType: .json)
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString, "include_media_info": NSNumber(value: true)]
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderHTTPError?
var dic = [String: Any]()
var keys = [String]()
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) }
if let json = data?.deserializeJSON(), let properties = (json["media_info"] as? [String: Any])?["metadata"] as? [String: Any] {
(dic, keys) = self.mapMediaInfo(properties)
}
}
completionHandler(dic, keys, serverError ?? error)
})
task.resume()
}
}
+158
View File
@@ -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 errorDescription: 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.set(httpAuthentication: self.credential, with: .oAuth2)
request.set(httpContentType: .json)
var requestDictionary: [String: AnyObject] = ["path": self.correctPath(path) as NSString!]
requestDictionary["query"] = queryStr as NSString
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.set(httpAuthentication: self.credential, with: .oAuth2)
request.set(httpContentType: .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)
}
}
+506
View File
@@ -0,0 +1,506 @@
//
// ExtendedLocalFileProvider.swift
// FileProvider
//
// Created by Amir Abbas Mousavian.
// Copyright © 2017 Mousavian. Distributed under MIT license.
//
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
}
}
open func thumbnailOfFile(path: String, dimension: CGSize? = nil, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) {
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)
}
}
}
open func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String: Any], _ keys: [String], _ error: Error?) -> Void)) {
(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)
}
}
}
/// 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 = CMTimeMake(asset.duration.value / 3, 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 ?? [:]
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 make", value: tiffDict[kCGImagePropertyTIFFMake as String])
add(key: "Device model", value: tiffDict[kCGImagePropertyTIFFModel as String])
add(key: "Lens model", value: exifDict[kCGImagePropertyExifLensModel as String])
add(key: "Artist", value: tiffDict[kCGImagePropertyTIFFArtist as String] as? String)
add(key: "Copyright", value: tiffDict[kCGImagePropertyTIFFCopyright as String] as? String)
add(key: "Date taken", value: tiffDict[kCGImagePropertyTIFFDateTime as String] as? String)
if let latitude = tiffDict[kCGImagePropertyGPSLatitude as String] as? NSNumber, let longitude = tiffDict[kCGImagePropertyGPSLongitude as String] as? NSNumber {
add(key: "Location", value: "\(latitude), \(longitude)")
}
add(key: "Altitude", value: tiffDict[kCGImagePropertyGPSAltitude as String] as? NSNumber)
add(key: "Area", value: tiffDict[kCGImagePropertyGPSAreaInformation as String])
add(key: "Color space", value: imageDict[kCGImagePropertyColorModel as String])
add(key: "Focal length", value: exifDict[kCGImagePropertyExifFocalLength 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 makeDescription(_ 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
}
guard FileManager.default.fileExists(atPath: fileURL.path) else {
return (dic, keys)
}
let playerItem = AVPlayerItem(url: fileURL)
let metadataList = playerItem.asset.commonMetadata
for item in metadataList {
#if swift(>=4.0)
let commonKey = item.commonKey?.rawValue
#else
let commonKey = item.commonKey
#endif
if let description = makeDescription(commonKey) {
if let value = item.stringValue {
keys.append(description)
dic[description] = value
}
}
}
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()
dateFormatter.dateFormat = "yyyyMMddHHmmssTZ"
if let result = dateFormatter.date(from: dateStr) {
return result
}
dateFormatter.dateFormat = "yyyyMMddHHmmssZZZZZ"
if let result = dateFormatter.date(from: dateStr) {
return result
}
dateFormatter.dateFormat = "yyyyMMddHHmmssZ"
if let result = dateFormatter.date(from: dateStr) {
return result
}
dateFormatter.dateFormat = "yyyyMMddHHmmss"
if let result = dateFormatter.date(from: dateStr) {
return result
}
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
}
+502
View File
@@ -0,0 +1,502 @@
//
// 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 {
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 self.isFileURL && FileManager.default.fileExists(atPath: self.path)
}
}
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
}
}
struct Quality<T> {
let value: T
let quality: Float
var stringifed: String {
var representaion: String = String(describing: value)
let quality: Float = min(1, max(self.quality, 0))
if let value = value as? Locale {
representaion = value.identifier.replacingOccurrences(of: "_", with: "-")
}
if let value = value as? String.Encoding {
let cfEncoding = CFStringConvertNSStringEncodingToEncoding(value.rawValue)
representaion = CFStringConvertEncodingToIANACharSetName(cfEncoding) as String? ?? "*"
}
let qualityDesc = String(format: "%.1f", quality)
return "\(representaion); q=\(qualityDesc)"
}
}
internal extension URLRequest {
mutating func set(httpAuthentication 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 set(httpAcceptCharset acceptCharset: String.Encoding) {
let cfEncoding = CFStringConvertNSStringEncodingToEncoding(acceptCharset.rawValue)
if let charsetString = CFStringConvertEncodingToIANACharSetName(cfEncoding) as String? {
self.addValue(charsetString, forHTTPHeaderField: "Accept-Charset")
}
}
mutating func set(httpAcceptCharset acceptCharset: Quality<String.Encoding>) {
self.addValue(acceptCharset.stringifed, forHTTPHeaderField: "Accept-Charset")
}
mutating func set(httpAcceptCharsets acceptCharsets: [String.Encoding]) {
self.setValue(nil, forHTTPHeaderField: "Accept-Charset")
for charset in acceptCharsets {
let cfEncoding = CFStringConvertNSStringEncodingToEncoding(charset.rawValue)
if let charsetString = CFStringConvertEncodingToIANACharSetName(cfEncoding) as String? {
self.addValue(charsetString, forHTTPHeaderField: "Accept-Charset")
}
}
}
mutating func set(httpAcceptCharsets acceptCharsets: [Quality<String.Encoding>]) {
self.setValue(nil, forHTTPHeaderField: "Accept-Charset")
for charset in acceptCharsets.sorted(by: { $0.quality > $1.quality }) {
self.addValue(charset.stringifed, forHTTPHeaderField: "Accept-Charset")
}
}
enum Encoding: String {
case all = "*"
case identity
case gzip
case deflate
}
mutating func set(httpAcceptEncoding acceptEncoding: Encoding) {
self.addValue(acceptEncoding.rawValue, forHTTPHeaderField: "Accept-Encoding")
}
mutating func set(httpAcceptEncoding acceptEncoding: Quality<Encoding>) {
self.addValue(acceptEncoding.stringifed, forHTTPHeaderField: "Accept-Encoding")
}
mutating func set(httpAcceptEncodings acceptEncodings: [Encoding]) {
self.setValue(nil, forHTTPHeaderField: "Accept-Encoding")
for encoding in acceptEncodings {
self.addValue(encoding.rawValue, forHTTPHeaderField: "Accept-Encoding")
}
}
mutating func set(httpAcceptEncodings acceptEncodings: [Quality<Encoding>]) {
self.setValue(nil, forHTTPHeaderField: "Accept-Encoding")
for encoding in acceptEncodings.sorted(by: { $0.quality > $1.quality }) {
self.addValue(encoding.stringifed, forHTTPHeaderField: "Accept-Encoding")
}
}
mutating func set(httpAcceptLanguage acceptLanguage: Locale) {
let langCode = acceptLanguage.identifier.replacingOccurrences(of: "_", with: "-")
self.addValue(langCode, forHTTPHeaderField: "Accept-Language")
}
mutating func set(httpAcceptLanguage acceptLanguage: Quality<Locale>) {
self.addValue(acceptLanguage.stringifed, forHTTPHeaderField: "Accept-Language")
}
mutating func set(httpAcceptLanguages acceptLanguages: [Locale]) {
self.setValue(nil, forHTTPHeaderField: "Accept-Language")
for lang in acceptLanguages {
let langCode = lang.identifier.replacingOccurrences(of: "_", with: "-")
self.addValue(langCode, forHTTPHeaderField: "Accept-Language")
}
}
mutating func set(httpAcceptLanguages acceptLanguages: [Quality<Locale>]) {
self.setValue(nil, forHTTPHeaderField: "Accept-Language")
for lang in acceptLanguages.sorted(by: { $0.quality > $1.quality} ) {
self.addValue(lang.stringifed, forHTTPHeaderField: "Accept-Language")
}
}
mutating func set(httpRangeWithOffset 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 set(httpRange 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")
}
}
struct ContentMIMEType: RawRepresentable {
public var rawValue: String
public typealias RawValue = String
public init(rawValue: String) {
self.rawValue = rawValue
}
static let javascript = ContentMIMEType(rawValue: "application/javascript")
static let json = ContentMIMEType(rawValue: "application/json")
static let pdf = ContentMIMEType(rawValue: "application/pdf")
static let stream = ContentMIMEType(rawValue: "application/octet-stream")
static let zip = ContentMIMEType(rawValue: "application/zip")
// Texts
static let css = ContentMIMEType(rawValue: "text/css")
static let html = ContentMIMEType(rawValue: "text/html")
static let plainText = ContentMIMEType(rawValue: "text/plain")
static let xml = ContentMIMEType(rawValue: "text/xml")
// Images
static let gif = ContentMIMEType(rawValue: "image/gif")
static let jpeg = ContentMIMEType(rawValue: "image/jpeg")
static let png = ContentMIMEType(rawValue: "image/png")
}
mutating func set(httpContentType contentType: ContentMIMEType, charset: String.Encoding? = nil) {
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 set(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 let data = try? JSONSerialization.data(withJSONObject: dictionary, options: []) else {
return nil
}
self = data
}
func deserializeJSON() -> [String: AnyObject]? {
if let dic = try? JSONSerialization.jsonObject(with: self, options: []) as? [String: AnyObject] {
return dic
}
return nil
}
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)
}
static func mapMemory<T, U>(from: T) -> U? {
guard MemoryLayout<T>.size >= MemoryLayout<U>.size else { return nil }
let data = Data(value: from)
return data.scanValue()
}
}
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
}
}
#if swift(>=4.0)
#else
extension String {
var count: Int {
return self.characters.count
}
}
#endif
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 {
/// Date format defined by usenet, commonly used in old implementations.
case rfc850 = "EEEE',' dd'-'MMM'-'yy HH':'mm':'ss z"
/// Date format defined by RFC 1132 for http.
case rfc1123 = "EEE',' dd' 'MMM' 'yyyy HH':'mm':'ss z"
/// Date format defined by ISO 8601, also defined in RFC 3339. Used by Dropbox.
case iso8601 = "yyyy'-'MM'-'dd'T'HH':'mm':'ssZ"
/// Date string returned by asctime() function.
case asctime = "EEE MMM d HH':'mm':'ss yyyy"
/// Equivalent to and defined by RFC 1123.
public static let http = RFCStandards.rfc1123
/// Equivalent to and defined by ISO 8610.
public static let rfc3339 = RFCStandards.iso8601
/// Equivalent to and defined by RFC 850.
public static let usenet = RFCStandards.rfc850
// Sorted by commonness
fileprivate static let allValues: [RFCStandards] = [.rfc1123, .rfc850, .iso8601, .asctime]
}
/// Checks date string against various RFC standards and returns `Date`.
public init?(rfcString: String) {
let dateFor: DateFormatter = DateFormatter()
dateFor.locale = Locale(identifier: "en_US")
for standard in RFCStandards.allValues {
dateFor.dateFormat = standard.rawValue
if let date = dateFor.date(from: rfcString) {
self = date
return
}
}
return nil
}
/// Formats date according to RFCs standard.
public func format(with standard: RFCStandards, locale: Locale? = nil, timeZone: TimeZone? = nil) -> String {
let fm = DateFormatter()
fm.dateFormat = standard.rawValue
fm.timeZone = timeZone ?? TimeZone(identifier: "UTC")
fm.locale = locale ?? Locale(identifier: "en_US_POSIX")
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)
}
}
+445
View File
@@ -0,0 +1,445 @@
// 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()
valuePointer.deallocate(capacity: 1)
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))
}
}
+743
View File
@@ -0,0 +1,743 @@
//
// FPSStreamTask.swift
// FileProvider
//
// Created by Amir Abbas Mousavian.
// Copyright © 2016 Mousavian. Distributed under MIT license.
//
import Foundation
private var lasttaskIdAssociated = 1_000_000_000
// 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?
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 apps 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 taskactive, 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 init(session: URLSession, host: String, port: Int, useURLSession: Bool = true) {
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 = true) {
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
}
/**
* 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._state = .canceling
inputStream?.setValue(kCFBooleanTrue, forKey: kCFStreamPropertyShouldCloseNativeSocket as String)
outputStream?.setValue(kCFBooleanTrue, forKey: kCFStreamPropertyShouldCloseNativeSocket as String)
self.inputStream?.close()
self.outputStream?.close()
self.inputStream?.remove(from: RunLoop.main, forMode: .defaultRunLoopMode)
self.outputStream?.remove(from: RunLoop.main, forMode: .defaultRunLoopMode)
self.inputStream?.delegate = nil
self.outputStream?.delegate = nil
self.inputStream = nil
self.outputStream = nil
self._state = .completed
self._countOfBytesSent = 0
self._countOfBytesRecieved = 0
}
var _error: Error? = nil
/**
* An error object that indicates why the task failed.
*
* This value is `NULL` if the task is still active or if the transfer completed successfully.
*/
override open var error: Error? {
if #available(iOS 9.0, macOS 10.11, *) {
if useURLSession {
return _underlyingTask!.error
}
}
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 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
}
streamDelegate?.urlSession?(self._underlyingSession, streamTask: self, didBecome: inputStream, outputStream: outputStream)
}
guard let inputStream = inputStream, let outputStream = outputStream else {
return
}
inputStream.delegate = self
outputStream.delegate = self
operation_queue.addOperation {
inputStream.schedule(in: RunLoop.main, forMode: .defaultRunLoopMode)
outputStream.schedule(in: RunLoop.main, forMode: .defaultRunLoopMode)
}
inputStream.open()
outputStream.open()
operation_queue.isSuspended = false
_state = .running
}
fileprivate var dataToBeSent: Data = Data()
fileprivate var dataReceived: Data = Data()
/**
* Asynchronously reads a number of bytes from the stream, and calls a handler upon completion.
*
* - 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 {
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.replaceSubrange(range, with: Data())
} else {
if self.dataReceived.count > 0 {
dR = self.dataReceived
self.dataReceived.count = 0
}
}
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()) {
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)
}
}
/**
* 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
}
}
operation_queue.addOperation {
self.inputStream!.setProperty(StreamSocketSecurityLevel.negotiatedSSL.rawValue, forKey: .socketSecurityLevelKey)
self.outputStream!.setProperty(StreamSocketSecurityLevel.negotiatedSSL.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
}
}
operation_queue.addOperation {
self.inputStream!.setProperty(StreamSocketSecurityLevel.none.rawValue, forKey: .socketSecurityLevelKey)
self.outputStream!.setProperty(StreamSocketSecurityLevel.none.rawValue, forKey: .socketSecurityLevelKey)
}
}
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: 2048)
let len = inputStream!.read(&buffer, maxLength: buffer.count)
if len > 0 {
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]
+867
View File
@@ -0,0 +1,867 @@
//
// 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: FileProviderBasicRemote, FileProviderOperations, FileProviderReadWrite {
open class var type: String { return "FTP" }
open let baseURL: URL?
/// **OBSOLETED** Current active path used in `contentsOfDirectory(path:completionHandler:)` method.
@available(*, obsoleted: 0.22, message: "This property is redundant with almost no use internally.")
open var currentPath: String = ""
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 passiveMode: Bool
/// Force to use URLSessionDownloadTask/URLSessionDataTask when possible
public var useAppleImplementation = true
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 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)
*/
public init? (baseURL: URL, passive: Bool = true, credential: URLCredential? = nil, cache: URLCache? = nil) {
guard (baseURL.scheme ?? "ftp").lowercased().hasPrefix("ftp") else { return nil }
guard baseURL.host != nil else { return nil }
var urlComponents = URLComponents(url: baseURL, resolvingAgainstBaseURL: true)!
let defaultPort: Int = baseURL.scheme == "ftps" ? 990 : 21
urlComponents.port = urlComponents.port ?? defaultPort
urlComponents.scheme = urlComponents.scheme ?? "ftp"
urlComponents.path = urlComponents.path.hasSuffix("/") ? urlComponents.path : urlComponents.path + "/"
self.baseURL = urlComponents.url!.absoluteURL
self.passiveMode = passive
self.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"
}
public required convenience init?(coder aDecoder: NSCoder) {
guard let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL else { return nil }
self.init(baseURL: baseURL, passive: aDecoder.decodeBool(forKey: "passiveMode"), credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential)
self.useCache = aDecoder.decodeBool(forKey: "useCache")
self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache")
self.useAppleImplementation = aDecoder.decodeBool(forKey: "useAppleImplementation")
}
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.useAppleImplementation, forKey: "useAppleImplementation")
aCoder.encode(self.passiveMode, forKey: "passiveMode")
}
public static var supportsSecureCoding: Bool {
return true
}
open func copy(with zone: NSZone? = nil) -> Any {
let copy = FTPFileProvider(baseURL: self.baseURL!, credential: self.credential, cache: self.cache)!
copy.delegate = self.delegate
copy.fileOperationDelegate = self.fileOperationDelegate
copy.useCache = self.useCache
copy.validatingCache = self.validatingCache
copy.useAppleImplementation = self.useAppleImplementation
return copy
}
deinit {
if let sessionuuid = _session?.sessionDescription {
removeSessionHandler(for: sessionuuid)
}
if fileProviderCancelTasksOnInvalidating {
_session?.invalidateAndCancel()
} else {
_session?.finishTasksAndInvalidate()
}
}
internal var serverSupportsRFC3659: Bool = true
open func contentsOfDirectory(path: String, completionHandler: @escaping ([FileObject], Error?) -> Void) {
self.contentsOfDirectory(path: path, rfc3659enabled: serverSupportsRFC3659, 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 ((error as NSError).domain == URLError.errorDomain && (error as NSError).code == URLError.unsupportedURL.rawValue) {
self.contentsOfDirectory(path: path, rfc3659enabled: false, completionHandler: completionHandler)
return
}
self.dispatch_queue.async {
completionHandler([], error)
}
return
}
let files: [FileObject] = contents.flatMap {
rfc3659enabled ? self.parseMLST($0, in: path) : self.parseUnixList($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: serverSupportsRFC3659, 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.serverSupportsRFC3659 = false
self.attributesOfItem(path: path, rfc3659enabled: false, completionHandler: completionHandler)
}
let lines = response.components(separatedBy: "\n").flatMap { $0.isEmpty ? nil : $0.trimmingCharacters(in: .whitespacesAndNewlines) }
guard lines.count > 2 else {
throw self.urlError(path, code: .badServerResponse)
}
let file: FileObject? = rfc3659enabled ? self.parseMLST(lines[1], in: path) : self.parseUnixList(lines[1], in: path)
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)
}
}
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 (Bool) -> Void) {
self.attributesOfItem(path: "/") { (file, error) in
completionHandler(file != nil)
}
}
open weak var fileOperationDelegate: FileOperationDelegate?
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)
}
open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler)
}
open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
return doOperation(.copy(source: path, destination: toPath), completionHandler: completionHandler)
}
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
return doOperation(.remove(path: path), completionHandler: completionHandler)
}
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.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
}
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.copy(source: path, destination: destURL.absoluteString)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
return nil
}
var progress = Progress(totalUnitCount: 0)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
if self.useAppleImplementation {
self.attributesOfItem(path: path, completionHandler: { (file, error) in
do {
if let error = error {
throw error
}
if file?.isDirectory ?? false {
throw self.urlError(path, code: .fileIsDirectory)
}
} catch {
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(operation, error: error)
}
return
}
progress.totalUnitCount = file?.size ?? 0
let task = self.session.downloadTask(with: self.url(of: path))
completionHandlersForTasks[self.session.sessionDescription!]?[task.taskIdentifier] = completionHandler
downloadCompletionHandlersForTasks[self.session.sessionDescription!]?[task.taskIdentifier] = { tempURL in
var error: NSError?
NSFileCoordinator().coordinate(writingItemAt: tempURL, options: .forMoving, writingItemAt: destURL, options: .forReplacing, error: &error, byAccessor: { (tempURL, destURL) in
do {
try FileManager.default.moveItem(at: tempURL, to: destURL)
completionHandler?(nil)
} catch {
completionHandler?(error)
}
})
if let error = error {
completionHandler?(error)
}
}
task.taskDescription = operation.json
task.addObserver(self.sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: &progress)
task.addObserver(self.sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: &progress)
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
})
} else {
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
self.ftpLogin(task) { (error) in
if let error = error {
self.dispatch_queue.async {
completionHandler?(error)
}
return
}
self.ftpRetrieveFile(task, filePath: self.ftpPath(path), onTask: { task in
weak var weakTask = task
progress.cancellationHandler = {
weakTask?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
}, onProgress: { recevied, totalReceived, totalSize in
progress.totalUnitCount = totalSize
progress.completedUnitCount = totalReceived
self.delegateNotify(operation, progress: progress.fractionCompleted)
}) { (tmpurl, error) in
if let error = error {
progress.cancel()
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(operation, error: error)
}
return
}
if let tmpurl = tmpurl {
try? FileManager.default.moveItem(at: tmpurl, to: destURL)
self.dispatch_queue.async {
completionHandler?(nil)
self.delegateNotify(operation)
}
}
}
}
}
return progress
}
open func contents(path: String, completionHandler: @escaping ((Data?, Error?) -> Void)) -> Progress? {
let operation = FileOperationType.fetch(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
return nil
}
if self.useAppleImplementation {
var progress = Progress(totalUnitCount: 0)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
let task = session.downloadTask(with: url(of: path))
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
if error != nil {
progress.cancel()
}
completionHandler(nil, error)
}
downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { tempURL in
do {
let data = try Data(contentsOf: tempURL)
completionHandler(data, nil)
} catch {
completionHandler(nil, error)
}
}
task.taskDescription = operation.json
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: &progress)
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: &progress)
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return progress
} else {
return self.contents(path: path, offset: 0, length: -1, completionHandler: completionHandler)
}
}
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.ftpRetrieveData(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
}
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
}
/**
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).flatMap({ $0.isEmpty ? nil : $0})
.flatMap {
let code = $0.components(separatedBy: .whitespaces).flatMap({ $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 { }
+957
View File
@@ -0,0 +1,957 @@
//
// FTPHelper.swift
// FileProvider
//
// Created by Amir Abbas Mousavian.
// Copyright © 2017 Mousavian. Distributed under MIT license.
//
import Foundation
internal extension FTPFileProvider {
func readDataUntilEOF(of task: FileProviderStreamTask, minLength: Int, receivedData: Data? = nil, timeout: TimeInterval, completionHandler: @escaping (_ data: Data?, _ errror:Error?) -> Void) {
task.readData(ofMinLength: minLength, maxLength: 65535, timeout: timeout) { (data, eof, error) in
if let error = error {
completionHandler(nil, error)
return
}
var receivedData = receivedData
if let data = data {
if receivedData != nil {
receivedData!.append(data)
} else {
receivedData = data
}
}
if eof {
completionHandler(receivedData, nil)
} else {
self.readDataUntilEOF(of: task, minLength: 0, receivedData: receivedData, timeout: timeout, completionHandler: completionHandler)
}
}
}
func execute(command: String, on task: FileProviderStreamTask, minLength: Int = 4, afterSend: ((_ error: Error?) -> Void)? = nil, completionHandler: @escaping (_ response: String?, _ error: Error?) -> Void) {
let timeout = session.configuration.timeoutIntervalForRequest
let terminalcommand = command + "\r\n"
task.write(terminalcommand.data(using: .utf8)!, timeout: timeout) { (error) in
if let error = error {
completionHandler(nil, error)
return
}
afterSend?(error)
if task.state == .suspended {
task.resume()
}
task.readData(ofMinLength: minLength, maxLength: 1024, timeout: timeout) { (data, eof, error) in
if let error = error {
completionHandler(nil, error)
return
}
if let data = data, let response = String(data: data, encoding: .utf8) {
completionHandler(response.trimmingCharacters(in: .whitespacesAndNewlines), nil)
} else {
completionHandler(nil, self.urlError("", code: .cannotParseResponse))
return
}
}
}
}
func ftpLogin(_ task: FileProviderStreamTask, completionHandler: @escaping (_ error: Error?) -> Void) {
let timeout = session.configuration.timeoutIntervalForRequest
if task.state == .suspended {
task.resume()
}
var isSecure = false
// Implicit FTP Connection
if self.baseURL?.port == 990 || self.baseURL?.scheme == "ftps" {
task.startSecureConnection()
isSecure = true
}
let credential = self.credential
task.readData(ofMinLength: 4, maxLength: 2048, timeout: timeout) { (data, eof, error) in
do {
if let error = error {
throw error
}
guard let data = data, let response = String(data: data, encoding: .utf8) else {
throw self.urlError("", code: .cannotParseResponse)
}
guard response.hasPrefix("22") else {
throw FileProviderFTPError(message: response)
}
} catch {
completionHandler(error)
return
}
let loginHandle: () -> Void = {
self.execute(command: "USER \(credential?.user ?? "anonymous")", on: task) { (response, error) in
if let error = error {
completionHandler(error)
return
}
guard let response = response else {
completionHandler(self.urlError("", code: .badServerResponse))
return
}
// successfully logged in
if response.hasPrefix("23") {
completionHandler(nil)
return
}
// needs password
if FileProviderFTPError(message: response).code == 331 {
self.execute(command: "PASS \(credential?.password ?? "fileprovider@")", on: task) { (response, error) in
if response?.hasPrefix("23") ?? false {
completionHandler(nil)
} else {
completionHandler(self.urlError("", code: .userAuthenticationRequired))
}
}
return
}
let error = FileProviderFTPError(message: response)
completionHandler(error)
}
}
if !isSecure && self.baseURL?.scheme == "ftpes" {
// Explicit FTP Connection, by upgrading connection to FTP/SSL
self.execute(command: "AUTH TLS", on: task, minLength: 0, completionHandler: { (response, error) in
if let error = error {
completionHandler(error)
return
}
if let response = response, response.hasPrefix("23") {
task.startSecureConnection()
isSecure = true
self.execute(command: "PBSZ 0\r\nPROT P", on: task, completionHandler: { (response, error) in
if let error = error {
completionHandler(error)
return
}
loginHandle()
})
}
})
} else if isSecure {
self.execute(command: "PBSZ 0\r\nPROT P", on: task, completionHandler: { (response, error) in
if let error = error {
completionHandler(error)
return
}
loginHandle()
})
} else {
loginHandle()
}
}
}
func ftpCwd(_ task: FileProviderStreamTask, to path: String, completionHandler: @escaping (_ error: Error?) -> Void) {
self.execute(command: "CWD \(path)", on: task) { (response, error) in
do {
if let error = error {
throw error
}
guard let response = response else {
throw self.urlError(path, code: .badServerResponse)
}
// successfully logged in
if response.hasPrefix("25") {
completionHandler(nil)
}
// not logged in
else if response.hasPrefix("55") {
throw FileProviderFTPError(message: response)
}
} catch {
completionHandler(error)
}
}
}
func ftpPassive(_ task: FileProviderStreamTask, completionHandler: @escaping (_ dataTask: FileProviderStreamTask?, _ error: Error?) -> Void) {
func trimmedNumber(_ s : String) -> String {
return s.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
}
self.execute(command: "PASV", on: task) { (response, error) in
do {
if let error = error {
throw error
}
guard let response = response, let destString = response.components(separatedBy: " ").flatMap({ $0 }).last.flatMap(String.init) else {
throw self.urlError("", code: .badServerResponse)
}
let destArray = destString.components(separatedBy: ",").flatMap({ UInt32(trimmedNumber($0)) })
guard destArray.count == 6 else {
throw self.urlError("", code: .badServerResponse)
}
// first 4 elements are ip, 2 next are port, as byte
var host = destArray.prefix(4).flatMap(String.init).joined(separator: ".")
let portHi = Int(destArray[4]) << 8
let portLo = Int(destArray[5])
let port = portHi + portLo
// IPv6 workaround
if host == "127.555.555.555" {
host = self.baseURL!.host!
}
let passiveTask = self.session.fpstreamTask(withHostName: host, port: port)
passiveTask.resume()
if self.baseURL?.scheme == "ftps" || self.baseURL?.scheme == "ftpes" || self.baseURL?.port == 990 {
passiveTask.startSecureConnection()
}
completionHandler(passiveTask, nil)
} catch {
completionHandler(nil, error)
return
}
}
}
func ftpActive(_ task: FileProviderStreamTask, completionHandler: @escaping (_ dataTask: FileProviderStreamTask?, _ error: Error?) -> Void) {
let service = NetService(domain: "", type: "_tcp.", name: "", port: 0)
service.publish(options: .listenForConnections)
let startTime = Date()
while service.port < 1 && startTime.timeIntervalSinceNow > -self.session.configuration.timeoutIntervalForRequest {
usleep(100_000)
}
let activeTask = self.session.fpstreamTask(withNetService: service)
activeTask.resume()
if self.baseURL?.scheme == "ftps" || self.baseURL?.port == 990 {
activeTask.startSecureConnection()
}
self.execute(command: "PORT \(service.port)", on: task) { (response, error) in
do {
if let error = error {
throw error
}
guard let response = response else {
throw self.urlError("", code: .badServerResponse)
}
guard !response.hasPrefix("5") else {
throw self.urlError("", code: .cannotConnectToHost)
}
completionHandler(activeTask, nil)
} catch {
activeTask.cancel()
completionHandler(nil, error)
}
}
}
func ftpDataConnect(_ task: FileProviderStreamTask, completionHandler: @escaping (_ dataTask: FileProviderStreamTask?, _ error: Error?) -> Void) {
if self.passiveMode {
self.ftpPassive(task, completionHandler: completionHandler)
} else {
dispatch_queue.async {
self.ftpActive(task, completionHandler: completionHandler)
}
}
}
func ftpRest(_ task: FileProviderStreamTask, startPosition: Int64, completionHandler: @escaping (_ error: Error?) -> Void) {
self.execute(command: "REST \(startPosition)", on: task) { (response, error) in
do {
if let error = error {
throw error
}
// Successful
guard let response = response else {
throw self.urlError("", code: .badServerResponse)
}
if response.hasPrefix("35") {
completionHandler(nil)
} else {
throw FileProviderFTPError(message: response, path: "")
}
} catch {
completionHandler(error)
}
}
}
func ftpList(_ task: FileProviderStreamTask, of path: String, useMLST: Bool, completionHandler: @escaping (_ contents: [String], _ error: Error?) -> Void) {
self.ftpDataConnect(task) { (dataTask, error) in
if let error = error {
completionHandler([], error)
return
}
guard let dataTask = dataTask else {
completionHandler([], self.urlError(path, code: .badServerResponse))
return
}
var success = false
let command = useMLST ? "MLSD \(path)" : "LIST \(path)"
self.execute(command: command, on: task, minLength: 20, afterSend: { error in
// starting passive task
let timeout = self.session.configuration.timeoutIntervalForRequest
DispatchQueue.global().async {
var finalData = Data()
var eof = false
var error: Error?
do {
while !eof {
let group = DispatchGroup()
group.enter()
dataTask.readData(ofMinLength: 1, maxLength: 65535, timeout: timeout, completionHandler: { (data, seof, serror) in
if let data = data {
finalData.append(data)
}
eof = seof
error = serror
group.leave()
})
let waitResult = group.wait(timeout: .now() + timeout)
if let error = error {
if !((error as NSError).domain == URLError.errorDomain && (error as NSError).code == URLError.cancelled.rawValue) {
throw error
}
return
}
if waitResult == .timedOut {
throw self.urlError(path, code: .timedOut)
}
}
guard let response = String(data: finalData, encoding: .utf8) else {
throw self.urlError(path, code: .badServerResponse)
}
let contents: [String] = response.components(separatedBy: "\n").flatMap({ $0.trimmingCharacters(in: .whitespacesAndNewlines) })
success = true
completionHandler(contents, nil)
} catch {
completionHandler([], error)
}
}
}) { (response, error) in
do {
if let error = error {
throw error
}
guard let response = response else {
throw self.urlError(path, code: .cannotParseResponse)
}
if response.hasPrefix("500") && useMLST {
dataTask.cancel()
self.serverSupportsRFC3659 = false
throw self.urlError(path, code: .unsupportedURL)
}
if !success && !(response.hasPrefix("25") || response.hasPrefix("15")) {
throw FileProviderFTPError(message: response, path: path)
}
} catch {
self.dispatch_queue.async {
completionHandler([], error)
}
}
}
}
}
func recursiveList(path: String, useMLST: Bool, foundItemsHandler: ((_ contents: [FileObject]) -> Void)? = nil, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) -> Progress? {
let progress = Progress(totalUnitCount: -1)
let queue = DispatchQueue(label: "\(self.type).recursiveList")
queue.async {
let group = DispatchGroup()
var result = [FileObject]()
var success = true
group.enter()
self.contentsOfDirectory(path: path, completionHandler: { (files, error) in
success = success && (error == nil)
if let error = error {
completionHandler([], error)
group.leave()
return
}
result.append(contentsOf: files)
progress.completedUnitCount = Int64(files.count)
foundItemsHandler?(files)
let directories: [FileObject] = files.filter { $0.isDirectory }
progress.becomeCurrent(withPendingUnitCount: Int64(directories.count))
for dir in directories {
group.enter()
_=self.recursiveList(path: dir.path, useMLST: useMLST, foundItemsHandler: foundItemsHandler, completionHandler: { (contents, error) in
success = success && (error == nil)
if let error = error {
completionHandler([], error)
group.leave()
return
}
foundItemsHandler?(files)
result.append(contentsOf: contents)
group.leave()
})
}
progress.resignCurrent()
group.leave()
})
group.wait()
if success {
self.dispatch_queue.async {
completionHandler(result, nil)
}
}
}
return progress
}
func ftpRetrieveData(_ task: FileProviderStreamTask, filePath: String, from position: Int64 = 0, length: Int = -1, onTask: ((_ task: FileProviderStreamTask) -> Void)?, onProgress: ((_ bytesReceived: Int64, _ totalReceived: Int64, _ expectedBytes: Int64) -> Void)?, completionHandler: @escaping (_ data: Data?, _ error: Error?) -> Void) {
// Check cache
if useCache, let url = URL(string: filePath.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? filePath, relativeTo: self.baseURL!)?.absoluteURL, let cachedResponse = self.cache?.cachedResponse(for: URLRequest(url: url)), cachedResponse.data.count > 0 {
dispatch_queue.async {
completionHandler(cachedResponse.data, nil)
}
return
}
self.attributesOfItem(path: filePath) { (file, error) in
let totalSize = file?.size ?? -1
// Retreive data from server
self.ftpDataConnect(task) { (dataTask, error) in
if let error = error {
completionHandler(nil, error)
return
}
guard let dataTask = dataTask else {
completionHandler(nil, self.urlError(filePath, code: .badServerResponse))
return
}
// Send retreive command
let len = 19 /* TYPE response */ + 65 + String(position).count /* REST Response */ + 53 + filePath.count + String(totalSize).count /* RETR open response */ + 26 /* RETR Transfer complete message. */
self.execute(command: "TYPE I" + "\r\n" + "REST \(position)" + "\r\n" + "RETR \(filePath)", on: task, minLength: len, afterSend: { error in
// starting passive task
onTask?(dataTask)
let timeout = self.session.configuration.timeoutIntervalForRequest
DispatchQueue.global().async {
var finalData = Data()
var eof = false
var error: Error?
while !eof {
let group = DispatchGroup()
group.enter()
dataTask.readData(ofMinLength: 0, maxLength: 65535, timeout: timeout, completionHandler: { (data, seof, serror) in
if let data = data {
finalData.append(data)
onProgress?(Int64(data.count), Int64(finalData.count), totalSize)
}
eof = seof || (length > 0 && finalData.count >= length)
if length > 0 && finalData.count > length {
finalData.count = length
}
error = serror
group.leave()
})
let waitResult = group.wait(timeout: .now() + timeout)
if let error = error {
completionHandler(nil, error)
return
}
if waitResult == .timedOut {
completionHandler(nil, self.urlError(filePath, code: .timedOut))
return
}
}
if let url = URL(string: filePath.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? filePath, relativeTo: self.baseURL!)?.absoluteURL {
let urlresponse = URLResponse(url: url, mimeType: nil, expectedContentLength: finalData.count, textEncodingName: nil)
let cachedResponse = CachedURLResponse(response: urlresponse, data: finalData)
let request = URLRequest(url: url)
self.cache?.storeCachedResponse(cachedResponse, for: request)
}
completionHandler(finalData, nil)
return
}
}) { (response, error) in
do {
if let error = error {
throw error
}
guard let response = response else {
throw self.urlError(filePath, code: .cannotParseResponse)
}
if !(response.hasPrefix("1") || !response.hasPrefix("2")) {
throw FileProviderFTPError(message: response)
}
} catch {
self.dispatch_queue.async {
completionHandler(nil, error)
}
}
}
}
}
}
func ftpRetrieveFile(_ task: FileProviderStreamTask, filePath: String, from position: Int64 = 0, length: Int = -1, onTask: ((_ task: FileProviderStreamTask) -> Void)?, onProgress: ((_ bytesReceived: Int64, _ totalReceived: Int64, _ expectedBytes: Int64) -> Void)?, completionHandler: @escaping (_ file: URL?, _ error: Error?) -> Void) {
let tempURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString).appendingPathExtension("tmp")
// Check cache
if useCache, let url = URL(string: filePath.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? filePath, relativeTo: self.baseURL!)?.absoluteURL, let cachedResponse = self.cache?.cachedResponse(for: URLRequest(url: url)), cachedResponse.data.count > 0 {
dispatch_queue.async {
do {
try cachedResponse.data.write(to: tempURL)
completionHandler(tempURL, nil)
} catch {
completionHandler(nil, error)
}
try? FileManager.default.removeItem(at: tempURL)
}
return
}
self.attributesOfItem(path: filePath) { (file, error) in
let totalSize = file?.size ?? -1
// Retreive data from server
self.ftpDataConnect(task) { (dataTask, error) in
if let error = error {
completionHandler(nil, error)
return
}
guard let dataTask = dataTask else {
completionHandler(nil, self.urlError(filePath, code: .badServerResponse))
return
}
// Send retreive command
let len = 19 /* TYPE response */ + 65 + String(position).count /* REST Response */ + 53 + filePath.count + String(totalSize).count /* RETR open response */ + 26 /* RETR Transfer complete message. */
self.execute(command: "TYPE I" + "\r\n" + "REST \(position)" + "\r\n" + "RETR \(filePath)", on: task, minLength: len, afterSend: { error in
// starting passive task
onTask?(dataTask)
let timeout = self.session.configuration.timeoutIntervalForRequest
DispatchQueue.global().async {
var finalData = Data()
var eof = false
var error: Error?
while !eof {
let group = DispatchGroup()
group.enter()
dataTask.readData(ofMinLength: 0, maxLength: 65535, timeout: timeout, completionHandler: { (data, seof, serror) in
if let data = data {
finalData.append(data)
onProgress?(Int64(data.count), Int64(finalData.count), totalSize)
}
eof = seof || (length > 0 && finalData.count >= length)
if length > 0 && finalData.count > length {
finalData.count = length
}
error = serror
group.leave()
})
let waitResult = group.wait(timeout: .now() + timeout)
if let error = error {
completionHandler(nil, error)
return
}
if waitResult == .timedOut {
error = self.urlError("", code: .timedOut)
completionHandler(nil, error)
return
}
}
if let url = URL(string: filePath.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? filePath, relativeTo: self.baseURL!)?.absoluteURL {
let urlresponse = URLResponse(url: url, mimeType: nil, expectedContentLength: finalData.count, textEncodingName: nil)
let cachedResponse = CachedURLResponse(response: urlresponse, data: finalData)
let request = URLRequest(url: url)
self.cache?.storeCachedResponse(cachedResponse, for: request)
}
self.dispatch_queue.async {
do {
try finalData.write(to: tempURL)
completionHandler(tempURL, nil)
// Removing temporary file after coordinating
NSFileCoordinator().coordinate(writingItemAt: tempURL, options: .forDeleting, error: nil, byAccessor: { (tempURL) in
try? FileManager.default.removeItem(at: tempURL)
})
} catch {
completionHandler(nil, error)
}
}
}
}) { (response, error) in
do {
if let error = error {
throw error
}
guard let response = response else {
throw self.urlError(filePath, code: .cannotParseResponse)
}
if !(response.hasPrefix("1") || response.hasPrefix("2")) {
throw FileProviderFTPError(message: response)
}
} catch {
self.dispatch_queue.async {
completionHandler(nil, error)
}
}
}
}
}
}
func ftpStore(_ task: FileProviderStreamTask, filePath: String, fromData: Data?, fromFile: URL?, onTask: ((_ task: FileProviderStreamTask) -> Void)?, onProgress: ((_ bytesSent: Int64, _ totalSent: Int64, _ expectedBytes: Int64) -> Void)?, completionHandler: @escaping (_ error: Error?) -> Void) {
let timeout = self.session.configuration.timeoutIntervalForRequest
operation_queue.addOperation {
guard let size: Int64 = (fromData != nil ? Int64(fromData!.count) : nil) ?? fromFile?.fileSize else { return }
var error: Error?
let chunkSize: Int
switch size {
case 0..<262_144: chunkSize = 32_768 // 0KB To 256KB, chunk size is 32KB
case 262_144..<1_048_576: chunkSize = 65_536 // 256KB To 1MB, chunk size is 64KB
case 1_048_576..<10_485_760: chunkSize = 131_072 // 1MB To 10MB, chunk size is 128KB
case 10_048_576..<33_554_432: chunkSize = 262_144 // 10MB To 32MB, chunk size is 256KB
default: chunkSize = 524_288 // Larger than 32MB, chunk size is 512KB
}
var fileHandle: FileHandle?
if let file = fromFile {
fileHandle = FileHandle(forReadingAtPath: file.path)
}
defer {
fileHandle?.closeFile()
}
var eof = false
var sent: Int64 = 0
while !eof {
let subdata: Data
if let data = fromData {
let endIndex = min(data.count, Int(sent) + chunkSize)
eof = endIndex == data.count
subdata = data.subdata(in: Int(sent)..<endIndex)
}else if let fileHandle = fileHandle {
subdata = fileHandle.readData(ofLength: chunkSize)
eof = Int64(fileHandle.offsetInFile) == size
} else {
return
}
if subdata.count == 0 { continue }
let group = DispatchGroup()
group.enter()
self.ftpStore(task, data: subdata, to: filePath, from: sent, onTask: onTask, completionHandler: { (serror) in
error = serror
sent += Int64(subdata.count)
group.leave()
onProgress?(Int64(subdata.count), sent, size)
})
let waitResult = group.wait(timeout: .now() + timeout)
if let error = error {
completionHandler(error)
return
}
if waitResult == .timedOut {
error = self.urlError(filePath, code: .timedOut)
completionHandler(error)
return
}
}
completionHandler(nil)
}
}
func ftpStore(_ task: FileProviderStreamTask, data: Data, to filePath: String, from position: Int64, onTask: ((_ task: FileProviderStreamTask) -> Void)?, completionHandler: @escaping (_ error: Error?) -> Void) {
self.ftpDataConnect(task) { (dataTask, error) in
if let error = error {
completionHandler(error)
return
}
guard let dataTask = dataTask else {
completionHandler(self.urlError(filePath, code: .badServerResponse))
return
}
// Send retreive command
var success = false
let len = 19 /* TYPE response */ + 65 + String(position).count /* REST Response */ + 44 + filePath.count /* STOR open response */ + 10 /* RETR Transfer complete message. */
self.execute(command: "TYPE I" + "\r\n" + "REST \(position)" + "\r\n" + "STOR \(filePath)", on: task, minLength: len, afterSend: { error in
// starting passive task
let timeout = self.session.configuration.timeoutIntervalForRequest
onTask?(dataTask)
if data.count == 0 { return }
dataTask.write(data, timeout: timeout, completionHandler: { (error) in
if let error = error {
completionHandler(error)
return
}
success = true
dataTask.closeRead()
dataTask.closeWrite()
})
}) { (response, error) in
guard success else { return }
do {
if let error = error {
throw error
}
guard let response = response else {
throw self.urlError(filePath, code: .cannotParseResponse)
}
if !(response.hasPrefix("1") || response.hasPrefix("2")) {
throw FileProviderFTPError(message: response)
}
completionHandler(nil)
} catch {
self.dispatch_queue.async {
completionHandler(error)
}
}
}
}
}
func ftpQuit(_ task: FileProviderStreamTask) {
self.execute(command: "QUIT", on: task) { (_, _) in
//task.closeRead()
//task.closeWrite()
}
}
func ftpPath(_ apath: String) -> String {
// path of base url should be concreted into file path! And remove final slash
var path = baseURL!.appendingPathComponent(apath).path.replacingOccurrences(of: "/", with: "", options: [.anchored, .backwards])
// Fixing slashes
if !path.hasPrefix("/") {
path = "/" + path
}
return path
}
func parseUnixList(_ text: String, in path: String) -> FileObject? {
let gregorian = Calendar(identifier: .gregorian)
let nearDateFormatter = DateFormatter()
nearDateFormatter.calendar = gregorian
nearDateFormatter.locale = Locale(identifier: "en_US_POSIX")
nearDateFormatter.dateFormat = "MMM dd hh:ss yyyy"
let farDateFormatter = DateFormatter()
farDateFormatter.calendar = gregorian
farDateFormatter.locale = Locale(identifier: "en_US_POSIX")
farDateFormatter.dateFormat = "MMM dd yyyy"
let thisYear = gregorian.component(.year, from: Date())
let components = text.components(separatedBy: " ").flatMap { $0.isEmpty ? nil : $0 }
guard components.count >= 9 else { return nil }
let posixPermission = components[0]
let linksCount = Int(components[1]) ?? 0
//let owner = components[2]
//let groupOwner = components[3]
let size = Int64(components[4]) ?? -1
let date = components[5..<8].joined(separator: " ")
let name = components[8..<components.count].joined(separator: " ")
guard name != "." && name != ".." else { return nil }
let path = (path as NSString).appendingPathComponent(name).replacingOccurrences(of: "/", with: "", options: .anchored)
let file = FileObject(url: url(of: path), name: name, path: path)
#if swift(>=4.0)
let typeChar = posixPermission.first ?? Character(" ")
#else
let typeChar = posixPermission.characters.first ?? Character(" ")
#endif
switch String(typeChar) {
case "d": file.type = .directory
case "l": file.type = .symbolicLink
default: file.type = .regular
}
file.isReadOnly = !posixPermission.contains("w")
file.size = file.isDirectory ? -1 : size
file.allValues[.linkCountKey] = linksCount
if let parsedDate = nearDateFormatter.date(from: date + " " + String(thisYear)) {
if parsedDate > Date() {
file.modifiedDate = gregorian.date(byAdding: .year, value: -1, to: parsedDate)
} else {
file.modifiedDate = parsedDate
}
} else if let parsedDate = farDateFormatter.date(from: date) {
file.modifiedDate = parsedDate
}
return file
}
func parseMLST(_ text: String, in path: String) -> FileObject? {
var components = text.components(separatedBy: ";").flatMap { $0.isEmpty ? nil : $0 }
guard components.count > 1 else { return nil }
let nameOrPath = components.removeLast().trimmingCharacters(in: .whitespacesAndNewlines)
var correctedPath: String
let name: String
if nameOrPath.hasPrefix("/") {
correctedPath = nameOrPath.replacingOccurrences(of: baseURL!.path, with: "", options: .anchored)
name = (nameOrPath as NSString).lastPathComponent
} else {
name = nameOrPath
correctedPath = (path as NSString).appendingPathComponent(nameOrPath)
}
correctedPath = correctedPath.replacingOccurrences(of: "/", with: "", options: .anchored)
var attributes = [String: String]()
for component in components {
let keyValue = component.components(separatedBy: "=") .flatMap { $0.isEmpty ? nil : $0 }
guard keyValue.count >= 2, !keyValue[0].isEmpty else { continue }
attributes[keyValue[0].lowercased()] = keyValue.dropFirst().joined(separator: "=")
}
let file = FileObject(url: url(of: correctedPath), name: name, path: correctedPath)
let dateFormatter = DateFormatter()
dateFormatter.calendar = Calendar(identifier: .gregorian)
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyyMMddhhmmss"
for (key, attribute) in attributes {
switch key {
case "type":
switch attribute.lowercased() {
case "file": file.type = .regular
case "dir": file.type = .directory
case "link": file.type = .symbolicLink
case "os.unix=block": file.type = .blockSpecial
case "cdir", "pdir": return nil // . and .. files are redundant in listing
default: file.type = .unknown
}
case "unique":
file.allValues[.fileResourceIdentifierKey] = attribute
case "modify":
file.modifiedDate = dateFormatter.date(from: attribute)
case "create":
file.creationDate = dateFormatter.date(from: attribute)
case "perm":
file.allValues[.isReadableKey] = attribute.contains("r") || attribute.contains("l")
file.allValues[.isWritableKey] = attribute.contains("w") || attribute.contains("a")
case "size":
file.size = Int64(attribute) ?? -1
case "media-type":
file.allValues[.mimeTypeKey] = attribute
default:
break
}
}
return file
}
}
/// Contains error code and description returned by FTP/S provider.
public struct FileProviderFTPError: Error {
/// HTTP status code returned for error by server.
public let code: Int
/// Path of file/folder casued that error
public let path: String
/// Contents returned by server as error description
public let errorDescription: String?
init(code: Int, path: String, errorDescription: String?) {
self.code = code
self.path = path
self.errorDescription = errorDescription
}
init(message response: String, path: String = "") {
let message = response.components(separatedBy: .newlines).last ?? "No Response"
#if swift(>=4.0)
let startIndex = (message.index(of: "-") ?? message.index(of: " ")) ?? message.startIndex
self.code = Int(message[..<startIndex].trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
#else
let startIndex = (message.characters.index(of: "-") ?? message.characters.index(of: " ")) ?? message.startIndex
self.code = Int(message.substring(to: startIndex).trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
#endif
self.path = path
if code > 0 {
#if swift(>=4.0)
self.errorDescription = message[startIndex...].trimmingCharacters(in: .whitespacesAndNewlines)
#else
self.errorDescription = message.substring(from: startIndex).trimmingCharacters(in: .whitespacesAndNewlines)
#endif
} else {
self.errorDescription = message
}
}
}
+402
View File
@@ -0,0 +1,402 @@
//
// 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: Equatable {
/// 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]()
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
}
/// 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
}
return rhs.path == lhs.path && rhs.size == lhs.size && rhs.modifiedDate == lhs.modifiedDate
}
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["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 {
/// 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 resources 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 volumes 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 volumes 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 volumes 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
}
}
}
}
+38
View File
@@ -0,0 +1,38 @@
//
// FileProvider iOS.h
// FileProvider iOS
//
// 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.
FOUNDATION_EXPORT double FileProvider_iOSVersionNumber;
//! Project version string for FileProvider iOS.
FOUNDATION_EXPORT const unsigned char FileProvider_iOSVersionString[];
#elif defined TARGET_OS_TV
#import <UIKit/UIKit.h>
//! Project version number for FileProvider tvOS.
FOUNDATION_EXPORT double FileProvider_tvOSVersionNumber;
//! Project version string for FileProvider tvOS.
FOUNDATION_EXPORT const unsigned char FileProvider_tvOSVersionString[];
#elif defined TARGET_OS_MAC
#import <Cocoa/Cocoa.h>
//! Project version number for FileProvider OSX.
FOUNDATION_EXPORT double FileProvider_OSXVersionNumber;
//! Project version string for FileProvider OSX.
FOUNDATION_EXPORT const unsigned char FileProvider_OSXVersionString[];
#else
// Unsupported platform
#endif
// In this header, you should import all the public headers of your framework using statements like #import <FileProvider_iOS/PublicHeader.h>
File diff suppressed because it is too large Load Diff
+550
View File
@@ -0,0 +1,550 @@
//
// 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: FileProviderBasicRemote, FileProviderOperations, FileProviderReadWrite {
open class var type: String { fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.") }
open let baseURL: URL?
/// **OBSOLETED** Current active path used in `contentsOfDirectory(path:completionHandler:)` method.
@available(*, obsoleted: 0.22, message: "This property is redundant with almost no use internally.")
open var currentPath: String = ""
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 WebDAV 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"
}
public required convenience init?(coder aDecoder: NSCoder) {
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
}
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.")
}
deinit {
if let sessionuuid = _session?.sessionDescription {
removeSessionHandler(for: sessionuuid)
}
if fileProviderCancelTasksOnInvalidating {
_session?.invalidateAndCancel()
} else {
_session?.finishTasksAndInvalidate()
}
}
open func contentsOfDirectory(path: String, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) {
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
}
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.")
}
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? {
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
}
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
self.storageProperties { volume in
completionHandler(volume != nil)
}
}
open weak var fileOperationDelegate: FileOperationDelegate?
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)
}
open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
return doOperation(.move(source: path, destination: toPath), overwrite: overwrite, completionHandler: completionHandler)
}
open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
return doOperation(.copy(source: path, destination: toPath), overwrite: overwrite, completionHandler: completionHandler)
}
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
return doOperation(.remove(path: path), completionHandler: completionHandler)
}
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_simple(toPath, request: request, localFile: localFile, operation: operation, completionHandler: completionHandler)
}
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.
*/
open func contents(path: String, progressHandler: @escaping (_ position: Int64, _ data: Data) -> Void, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.fetch(path: path)
let request = self.request(for: operation)
var position: Int64 = 0
return download_progressive(path: path, request: request, operation: operation, progressHandler: { data in
progressHandler(position, data)
position += Int64(data.count)
}, completionHandler: (completionHandler ?? { _ in return }))
}
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.set(httpRangeWithOffset: 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)
}
})
}
public 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_simple(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
}
fileprivate func doOperation(_ operation: FileOperationType, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
return nil
}
let 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, response.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) {
serverError = self.serverError(with: code, path: operation.source, data: data)
}
if let response = response as? HTTPURLResponse, FileProviderHTTPErrorCode(rawValue: response.statusCode) == .multiStatus, let data = data {
self.multiStatusHandler(source: operation.source, data: data, completionHandler: completionHandler)
}
if serverError == nil && error == nil {
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
}
/// This method should be used in subclasses to fetch directory content from servers which support paginated results.
/// Almost all HTTP based provider, except WebDAV, supports this method.
///
/// - Important: Please use `[weak self]` when implementing handlers to prevent retain cycles. In these cases,
/// return `nil` as the result of handler as the operation will be aborted.
///
/// - Parameters:
/// - path: path of directory which enqueued for listing, for informational use like errpr reporting.
/// - requestHandler: Get token of next page and returns appropriate `URLRequest` to be sent to server.
/// handler can return `nil` to cancel entire operation.
/// - token: Token of the page which `URLRequest` is needed, token will be `nil` for initial page. .
/// - pageHandler: Handler which is called after fetching results of a page to parse data. will return parse result as
/// array of `FileObject` or error if data is nil or parsing is failed. Method will not continue to next page if
/// `error` is returned, otherwise `nextToken` will be used for next page. `nil` value for `newToken` will indicate
/// last page of directory contents.
/// - data: Raw data returned from server. Handler should parse them and return files.
/// - progress: `Progress` object that `completedUnits` will be increased when a new `FileObject` is parsed in method.
/// - completionHandler: All file objects returned by `pageHandler` will be passed to this handler, or error if occured.
/// This handler will be called when `pageHandler` returns `nil for `newToken`.
/// - contents: all files parsed via `pageHandler` will be return aggregated.
/// - error: `Error` returned by server. `nil` means success. If exists, it means `contents` are incomplete.
internal func paginated(_ path: String, requestHandler: @escaping (_ token: String?) -> URLRequest?, pageHandler: @escaping (_ data: Data?, _ progress: Progress) -> (files: [FileObject], error: Error?, newToken: String?), completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) -> Progress {
let progress = Progress(totalUnitCount: -1)
self.paginated(path, startToken: nil, currentProgress: progress, previousResult: [], requestHandler: requestHandler, pageHandler: pageHandler, completionHandler: completionHandler)
return progress
}
// codebeat:disable[ARITY]
private func paginated(_ path: String, startToken: String?, currentProgress progress: Progress, previousResult: [FileObject], requestHandler: @escaping (_ token: String?) -> URLRequest?, pageHandler: @escaping (_ data: Data?, _ progress: Progress) -> (files: [FileObject], error: Error?, newToken: String?), completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) {
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 }
internal func upload_simple(_ targetPath: String, request: URLRequest, data: Data? = nil, localFile: URL? = nil, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? {
let size: Int64
if let data = data {
size = Int64(data.count)
} else if let localFile = localFile {
let fSize = (try? localFile.resourceValues(forKeys: [.fileSizeKey]))?.fileSize
size = Int64(fSize ?? -1)
} else {
return nil
}
if size > maxUploadSimpleSupported {
let error = self.serverError(with: .payloadTooLarge, path: targetPath, data: nil)
completionHandler?(error)
self.delegateNotify(operation, error: error)
return nil
}
var progress = Progress(totalUnitCount: -1)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
progress.totalUnitCount = size
let taskHandler = { (task: URLSessionTask) -> Void in
completionHandlersForTasks[self.session.sessionDescription!]?[task.taskIdentifier] = { [weak self] error in
var responseError: FileProviderHTTPError?
if let code = (task.response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
// We can't fetch server result from delegate!
responseError = self?.serverError(with: rCode, path: targetPath, data: nil)
}
if !(responseError == nil && error == nil) {
progress.cancel()
}
completionHandler?(responseError ?? error)
self?.delegateNotify(operation, error: responseError ?? error)
}
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()
}
if let data = data {
let task = session.uploadTask(with: request, from: data)
taskHandler(task)
} else if let localFile = localFile {
var error: NSError?
NSFileCoordinator().coordinate(readingItemAt: localFile, options: .forUploading, error: &error, byAccessor: { (url) in
let task = self.session.uploadTask(with: request, fromFile: localFile)
taskHandler(task)
})
if let error = error {
completionHandler?(error)
}
}
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)
responseCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { response in
responseHandler?(response)
}
dataCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { data in
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 { }
+661
View File
@@ -0,0 +1,661 @@
//
// LocalFileProvider.swift
// FileProvider
//
// Created by Amir Abbas Mousavian.
// Copyright © 2016 Mousavian. Distributed under MIT license.
//
import Foundation
/**
This provider class allows interacting with local files placed in user disk. It also allows an
easy way to use `NSFileCoordintaing` to coordinate read and write when neccessary.
it uses `FileManager` foundation class with some additions like searching and reading a portion of file.
*/
open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndoable {
open class var type: String { return "Local" }
open fileprivate(set) var baseURL: URL?
/// **OBSOLETED** Current active path used in `contentsOfDirectory(path:completionHandler:)` method.
@available(*, obsoleted: 0.21, message: "This property is redundant with almost no use internally.")
open var currentPath: String = ""
open var dispatch_queue: DispatchQueue
open var operation_queue: OperationQueue
open weak var delegate: FileProviderDelegate?
open var credential: URLCredential?
/// Underlying `FileManager` object for listing and metadata fetching.
open private(set) var fileManager = FileManager()
/// Underlying `FileManager` object for operationa like copying, moving, etc.
open private(set) var opFileManager = FileManager()
fileprivate var fileProviderManagerDelegate: LocalFileProviderManagerDelegate? = nil
open var undoManager: UndoManager? = nil
/**
Forces file operations to use `NSFileCoordinating`, should be set `true` if:
- Files are on ubiquity (iCloud) container.
- Multiple processes are accessing same file, recommended when accessing a shared/public
user document in macOS and when using app extensions in iOS/tvOS (shared container).
By default it's `true` when using iCloud or shared container (App Group) initializers,
otherwise it's `false` to accelerate operations.
*/
open var isCoorinating: Bool
/**
Initializes provider for the specified common directory in the requested domains.
default values are `directory: .documentDirectory, domainMask: .userDomainMask`.
- Parameters:
- for: The search path directory. The supported values are described in `FileManager.SearchPathDirectory`.
- in: Base locations for directory to search. The value for this parameter is one or more of the constants described in `FileManager.SearchPathDomainMask`.
*/
public convenience init (for directory: FileManager.SearchPathDirectory = .documentDirectory, in domainMask: FileManager.SearchPathDomainMask = .userDomainMask) {
self.init(baseURL: FileManager.default.urls(for: directory, in: domainMask).first!)
}
/**
Failable initializer for the specified shared container directory, allows data and files to be shared among app
and extensions regarding sandbox requirements. Container ID is same with app group specified in project `Capabilities`
tab under `App Group` item. If you don't have enough privilage to access container or the app group imply does't exist,
initialing will fail.
default values are `directory: .documentDirectory`.
- Parameters:
- sharedContainerId: Same with `App Group` identifier defined in project settings.
- directory: The search path directory. The supported values are described in `FileManager.SearchPathDirectory`.
*/
public convenience init? (sharedContainerId: String, directory: FileManager.SearchPathDirectory = .documentDirectory) {
guard let baseURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: sharedContainerId) else {
return nil
}
var finalBaseURL = baseURL.absoluteURL
switch directory {
case .documentDirectory:
finalBaseURL = baseURL.appendingPathComponent("Documents")
case .libraryDirectory:
finalBaseURL = baseURL.appendingPathComponent("Library")
case .cachesDirectory:
finalBaseURL = baseURL.appendingPathComponent("Library/Caches")
case .applicationSupportDirectory:
finalBaseURL = baseURL.appendingPathComponent("Library/Application support")
default:
break
}
self.init(baseURL: finalBaseURL)
self.isCoorinating = true
try? fileManager.createDirectory(at: finalBaseURL, withIntermediateDirectories: true)
}
/// Initializes provider for the specified local URL.
///
/// - Parameter baseURL: Local URL location for base directory.
public init (baseURL: URL) {
guard baseURL.isFileURL else {
fatalError("Cannot initialize a Local provider from remote URL.")
}
self.baseURL = URL(fileURLWithPath: baseURL.path, isDirectory: true)
self.credential = nil
self.isCoorinating = false
#if swift(>=3.1)
let queueLabel = "FileProvider.\(Swift.type(of: self).type)"
#else
let queueLabel = "FileProvider.\(type(of: self).type)"
#endif
dispatch_queue = DispatchQueue(label: queueLabel, attributes: .concurrent)
operation_queue = OperationQueue()
operation_queue.name = "\(queueLabel).Operation"
fileProviderManagerDelegate = LocalFileProviderManagerDelegate(provider: self)
opFileManager.delegate = fileProviderManagerDelegate
}
public required convenience init?(coder aDecoder: NSCoder) {
guard let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL else {
return nil
}
self.init(baseURL: baseURL)
self.isCoorinating = aDecoder.decodeBool(forKey: "isCoorinating")
}
open func encode(with aCoder: NSCoder) {
aCoder.encode(self.baseURL, forKey: "currentPath")
aCoder.encode(self.isCoorinating, forKey: "isCoorinating")
}
public static var supportsSecureCoding: Bool {
return true
}
public func copy(with zone: NSZone? = nil) -> Any {
let copy = LocalFileProvider(baseURL: self.baseURL!)
copy.undoManager = self.undoManager
copy.isCoorinating = self.isCoorinating
copy.delegate = self.delegate
copy.fileOperationDelegate = self.fileOperationDelegate
return copy
}
open func contentsOfDirectory(path: String, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) {
dispatch_queue.async {
do {
let contents = try self.fileManager.contentsOfDirectory(at: self.url(of: path), includingPropertiesForKeys: nil, options: .skipsSubdirectoryDescendants)
let filesAttributes = contents.flatMap({ (fileURL) -> LocalFileObject? in
let path = self.relativePathOf(url: fileURL)
return LocalFileObject(fileWithPath: path, relativeTo: self.baseURL)
})
completionHandler(filesAttributes, nil)
} catch {
completionHandler([], error)
}
}
}
open func attributesOfItem(path: String, completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) {
dispatch_queue.async {
completionHandler(LocalFileObject(fileWithPath: path, relativeTo: self.baseURL), nil)
}
}
public func storageProperties(completionHandler: @escaping (_ volumeInfo: VolumeObject?) -> Void) {
dispatch_queue.async {
var keys: Set<URLResourceKey> = [.volumeTotalCapacityKey, .volumeAvailableCapacityKey, .volumeURLKey, .volumeNameKey, .volumeIsReadOnlyKey, .volumeCreationDateKey]
if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) {
keys.insert(.isEncryptedKey)
}
let values: URLResourceValues? = self.baseURL.flatMap { try? $0.resourceValues(forKeys: keys) }
completionHandler(values.flatMap({ VolumeObject(allValues: $0.allValues) }))
}
}
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? {
let progress = Progress(totalUnitCount: -1)
progress.setUserInfoObject(self.url(of: path), forKey: .fileURLKey)
dispatch_queue.async {
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
let iterator = self.fileManager.enumerator(at: self.url(of: path), includingPropertiesForKeys: nil, options: recursive ? [] : [.skipsSubdirectoryDescendants, .skipsPackageDescendants]) { (url, e) -> Bool in
completionHandler([], e)
return true
}
var result = [LocalFileObject]()
while let fileURL = iterator?.nextObject() as? URL {
if progress.isCancelled {
break
}
let path = self.relativePathOf(url: fileURL)
if let fileObject = LocalFileObject(fileWithPath: path, relativeTo: self.baseURL), query.evaluate(with: fileObject.mapPredicate()) {
result.append(fileObject)
progress.completedUnitCount = Int64(result.count)
foundItemHandler?(fileObject)
}
}
completionHandler(result, nil)
}
return progress
}
open func isReachable(completionHandler: @escaping (_ success: Bool) -> Void) {
dispatch_queue.async {
completionHandler(self.fileManager.isReadableFile(atPath: self.baseURL!.path))
}
}
open weak var fileOperationDelegate : FileOperationDelegate?
@discardableResult
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(folderName) + "/")
return self.doOperation(operation, completionHandler: completionHandler)
}
@discardableResult
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.move(source: path, destination: toPath)
let fileExists = ((try? self.url(of: toPath).checkResourceIsReachable()) ?? false) ||
((try? self.url(of: toPath).checkPromisedItemIsReachable()) ?? false)
if !overwrite && fileExists {
let e = self.cocoaError(toPath, code: .fileWriteFileExists)
dispatch_queue.async {
completionHandler?(e)
}
self.delegateNotify(operation, error: e)
return nil
}
return self.doOperation(operation, completionHandler: completionHandler)
}
@discardableResult
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.copy(source: path, destination: toPath)
let fileExists = ((try? self.url(of: toPath).checkResourceIsReachable()) ?? false) ||
((try? self.url(of: toPath).checkPromisedItemIsReachable()) ?? false)
if !overwrite && fileExists {
let e = self.cocoaError(toPath, code: .fileWriteFileExists)
dispatch_queue.async {
completionHandler?(e)
}
self.delegateNotify(operation, error: e)
return nil
}
return self.doOperation(operation, completionHandler: completionHandler)
}
@discardableResult
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.remove(path: path)
return self.doOperation(operation, completionHandler: completionHandler)
}
@discardableResult
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
let fileExists = ((try? self.url(of: toPath).checkResourceIsReachable()) ?? false) ||
((try? self.url(of: toPath).checkPromisedItemIsReachable()) ?? false)
if !overwrite && fileExists {
let e = self.cocoaError(toPath, code: .fileWriteFileExists)
dispatch_queue.async {
completionHandler?(e)
}
self.delegateNotify(operation, error: e)
return nil
}
return self.doOperation(operation, forUploading: true, completionHandler: completionHandler)
}
@discardableResult
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
return self.doOperation(operation, completionHandler: completionHandler)
}
@objc dynamic func doSimpleOperation(_ box: UndoBox) {
guard let _ = self.undoManager else { return }
_ = self.doOperation(box.undoOperation) { (_) in
return
}
}
@discardableResult
fileprivate func doOperation(_ operation: FileOperationType, data: Data? = nil, atomically: Bool = false, forUploading: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
let progress = Progress(totalUnitCount: -1)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.isCancellable = false
progress.setUserInfoObject(Progress.FileOperationKind.receiving, forKey: .fileOperationKindKey)
func urlofpath(path: String) -> URL {
if path.hasPrefix("file://") {
let removedSchemePath = path.replacingOccurrences(of: "file://", with: "", options: .anchored)
let pDecodedPath = removedSchemePath.removingPercentEncoding ?? removedSchemePath
return URL(fileURLWithPath: pDecodedPath)
} else {
return self.url(of: path)
}
}
let sourcePath = operation.source
let destPath = operation.destination
let source: URL = urlofpath(path: sourcePath)
progress.setUserInfoObject(source, forKey: .fileURLKey)
let dest: URL?
if let destPath = destPath {
dest = urlofpath(path: destPath)
} else {
dest = nil
}
if let undoManager = self.undoManager, let undoOp = self.undoOperation(for: operation) {
let undoBox = UndoBox(provider: self, operation: operation, undoOperation: undoOp)
undoManager.beginUndoGrouping()
undoManager.registerUndo(withTarget: self, selector: #selector(LocalFileProvider.doSimpleOperation(_:)), object: undoBox)
undoManager.setActionName(operation.actionDescription)
undoManager.endUndoGrouping()
}
var successfulSecurityScopedResourceAccess = false
let operationHandler: (URL, URL?) -> Void = { source, dest in
do {
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
switch operation {
case .create:
if sourcePath.hasSuffix("/") {
progress.totalUnitCount = 1
try self.opFileManager.createDirectory(at: source, withIntermediateDirectories: true, attributes: [:])
} else {
progress.totalUnitCount = Int64(data?.count ?? -1)
try data?.write(to: source, options: .atomic)
}
case .modify:
progress.totalUnitCount = Int64(data?.count ?? -1)
try data?.write(to: source, options: atomically ? [.atomic] : [])
case .copy:
guard let dest = dest else { return }
progress.setUserInfoObject(Progress.FileOperationKind.copying, forKey: .fileOperationKindKey)
progress.totalUnitCount = abs(source.fileSize)
try self.opFileManager.copyItem(at: source, to: dest)
case .move:
progress.setUserInfoObject(Progress.FileOperationKind.copying, forKey: .fileOperationKindKey)
guard let dest = dest else { return }
progress.totalUnitCount = abs(source.fileSize)
try self.opFileManager.moveItem(at: source, to: dest)
case.remove:
progress.totalUnitCount = abs(source.fileSize)
try self.opFileManager.removeItem(at: source)
default:
return
}
if successfulSecurityScopedResourceAccess {
source.stopAccessingSecurityScopedResource()
}
progress.completedUnitCount = progress.totalUnitCount
self.dispatch_queue.async {
completionHandler?(nil)
}
self.delegateNotify(operation)
} catch {
if successfulSecurityScopedResourceAccess {
source.stopAccessingSecurityScopedResource()
}
progress.cancel()
self.dispatch_queue.async {
completionHandler?(error)
}
self.delegateNotify(operation, error: error)
}
}
if isCoorinating {
successfulSecurityScopedResourceAccess = source.startAccessingSecurityScopedResource()
var intents = [NSFileAccessIntent]()
switch operation {
case .create, .modify:
intents.append(NSFileAccessIntent.writingIntent(with: source, options: .forReplacing))
case .copy:
guard let dest = dest else { return nil }
intents.append(NSFileAccessIntent.readingIntent(with: source, options: forUploading ? .forUploading : .withoutChanges))
intents.append(NSFileAccessIntent.writingIntent(with: dest, options: .forReplacing))
case .move:
guard let dest = dest else { return nil }
intents.append(NSFileAccessIntent.writingIntent(with: source, options: .forMoving))
intents.append(NSFileAccessIntent.writingIntent(with: dest, options: .forReplacing))
case .remove:
intents.append(NSFileAccessIntent.writingIntent(with: source, options: .forDeleting))
default:
return nil
}
self.coordinated(intents: intents, moving: true, operationHandler: operationHandler, errorHandler: { error in
self.dispatch_queue.async {
completionHandler?(error)
}
self.delegateNotify(operation, error: error)
})
} else {
operation_queue.addOperation {
operationHandler(source, dest)
}
}
return progress
}
@discardableResult
open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
let operation = FileOperationType.fetch(path: path)
let url = self.url(of: path)
let progress = Progress(totalUnitCount: url.fileSize)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.isCancellable = false
progress.setUserInfoObject(Progress.FileOperationKind.receiving, forKey: .fileOperationKindKey)
progress.setUserInfoObject(url, forKey: .fileURLKey)
let operationHandler: (URL) -> Void = { url in
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
do {
let data = try Data(contentsOf: url)
progress.completedUnitCount = progress.totalUnitCount
self.dispatch_queue.async {
completionHandler(data, nil)
}
self.delegateNotify(operation)
} catch {
progress.cancel()
self.dispatch_queue.async {
completionHandler(nil, error)
}
self.delegateNotify(operation, error: error)
}
}
if isCoorinating {
let intent = NSFileAccessIntent.readingIntent(with: url, options: .withoutChanges)
coordinated(intents: [intent], operationHandler: operationHandler, errorHandler: { error in
self.dispatch_queue.async {
completionHandler(nil, error)
}
self.delegateNotify(operation, error: error)
})
} else {
dispatch_queue.async {
operationHandler(url)
}
}
return progress
}
@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
}
if offset == 0 && length < 0 {
return self.contents(path: path, completionHandler: completionHandler)
}
let operation = FileOperationType.fetch(path: path)
let url = self.url(of: path)
let progress = Progress(totalUnitCount: -1)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.isCancellable = false
progress.setUserInfoObject(url, forKey: .fileURLKey)
progress.setUserInfoObject(Progress.FileOperationKind.receiving, forKey: .fileOperationKindKey)
let operationHandler: (URL) -> Void = { url in
do {
guard let handle = FileHandle(forReadingAtPath: url.path) else {
throw self.cocoaError(path, code: .fileNoSuchFile)
}
defer {
handle.closeFile()
}
let size = LocalFileObject(fileWithURL: url)?.size ?? -1
progress.totalUnitCount = size
guard size > offset else {
progress.cancel()
throw self.cocoaError(path, code: .fileReadTooLarge)
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
handle.seek(toFileOffset: UInt64(offset))
guard Int64(handle.offsetInFile) == offset else {
progress.cancel()
throw self.cocoaError(path, code: .fileReadTooLarge)
}
let data = handle.readData(ofLength: length)
progress.completedUnitCount = progress.totalUnitCount
self.dispatch_queue.async {
completionHandler(data, nil)
self.delegateNotify(operation)
}
}
catch {
self.dispatch_queue.async {
completionHandler(nil, error)
self.delegateNotify(operation, error: error)
}
}
}
if isCoorinating {
let intent = NSFileAccessIntent.readingIntent(with: url, options: .withoutChanges)
coordinated(intents: [intent], operationHandler: operationHandler, errorHandler: { error in
completionHandler(nil, error)
self.delegateNotify(operation, error: error)
})
} else {
dispatch_queue.async {
operationHandler(url)
}
}
return progress
}
@discardableResult
open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
let fileExists = ((try? self.url(of: path).checkResourceIsReachable()) ?? false) ||
((try? self.url(of: path).checkPromisedItemIsReachable()) ?? false)
if !overwrite && fileExists {
let e = self.cocoaError(path, code: .fileWriteFileExists)
dispatch_queue.async {
completionHandler?(e)
}
self.delegateNotify(.modify(path: path), error: e)
return nil
}
let operation: FileOperationType = fileExists ? .modify(path: path) : .create(path: path)
return self.doOperation(operation, data: data ?? Data(), atomically: atomically, completionHandler: completionHandler)
}
fileprivate var monitors = [LocalFolderMonitor]()
open func registerNotifcation(path: String, eventHandler: @escaping (() -> Void)) {
self.unregisterNotifcation(path: path)
let dirurl = self.url(of: path)
let isdir = (try? dirurl.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory ?? false
if !isdir {
return
}
let monitor = LocalFolderMonitor(url: dirurl) {
eventHandler()
}
monitor.start()
monitors.append(monitor)
}
open func unregisterNotifcation(path: String) {
var removedMonitor: LocalFolderMonitor?
for (i, monitor) in monitors.enumerated() {
if self.relativePathOf(url: monitor.url) == path {
removedMonitor = monitors.remove(at: i)
break
}
}
removedMonitor?.stop()
}
open func isRegisteredForNotification(path: String) -> Bool {
return monitors.map( { self.relativePathOf(url: $0.url) } ).contains(path.trimmingCharacters(in: CharacterSet(charactersIn: "/")))
}
/**
Creates a symbolic link at the specified path that points to an item at the given path.
This method does not traverse symbolic links contained in destination path, making it possible
to create symbolic links to locations that do not yet exist.
Also, if the final path component is a symbolic link, that link is not followed.
- Parameters:
- symbolicLink: The file path at which to create the new symbolic link. The last component of the path issued as the name of the link.
- withDestinationPath: The path that contains the item to be pointed to by the link. In other words, this is the destination of the link.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
*/
open func create(symbolicLink path: String, withDestinationPath destPath: String, completionHandler: SimpleCompletionHandler) {
operation_queue.addOperation {
let operation = FileOperationType.link(link: path, target: destPath)
do {
try self.opFileManager.createSymbolicLink(at: self.url(of: path), withDestinationURL: self.url(of: destPath))
completionHandler?(nil)
self.delegateNotify(operation)
} catch {
completionHandler?(error)
self.delegateNotify(operation, error: error)
}
}
}
/// Returns the path of the item pointed to by a symbolic link.
///
/// - Parameters:
/// - path: The path of a file or directory.
/// - completionHandler: Returns destination url of given symbolic link, or an `Error` object if it fails.
open func destination(ofSymbolicLink path: String, completionHandler: @escaping (_ url: URL?, _ error: Error?) -> Void) {
dispatch_queue.async {
do {
let destPath = try self.opFileManager.destinationOfSymbolicLink(atPath: self.url(of: path).path)
let destUrl = URL(fileURLWithPath: destPath)
completionHandler(destUrl, nil)
} catch {
completionHandler(nil, error)
}
}
}
}
internal extension LocalFileProvider {
func coordinated(intents: [NSFileAccessIntent], operationHandler: @escaping (_ url: URL) -> Void, errorHandler: ((_ error: Error) -> Void)? = nil) {
let coordinator = NSFileCoordinator(filePresenter: nil)
coordinator.coordinate(with: intents, queue: operation_queue) { (error) in
if let error = error {
errorHandler?(error)
return
}
operationHandler(intents.first!.url)
}
}
func coordinated(intents: [NSFileAccessIntent], moving: Bool = false, operationHandler: @escaping (_ sourceUrl: URL, _ destURL: URL?) -> Void, errorHandler: ((_ error: Error) -> Void)? = nil) {
let coordinator = NSFileCoordinator(filePresenter: nil)
coordinator.coordinate(with: intents, queue: operation_queue) { (error) in
if let error = error {
errorHandler?(error)
return
}
guard let newSource: URL = intents.first?.url else { return }
let newDest: URL? = intents.dropFirst().first?.url
if moving, let newDest = newDest {
coordinator.item(at: newSource, willMoveTo: newDest)
}
operationHandler(newSource, newDest)
if moving, let newDest = newDest {
coordinator.item(at: newSource, didMoveTo: newDest)
}
}
}
}
+237
View File
@@ -0,0 +1,237 @@
//
// 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 {
//
}
}
}
internal final class LocalFolderMonitor {
fileprivate let source: DispatchSourceFileSystemObject
fileprivate let descriptor: CInt
fileprivate let qq: DispatchQueue = DispatchQueue.global(qos: .default)
fileprivate var state: Bool = false
fileprivate var monitoredTime: TimeInterval = Date().timeIntervalSinceReferenceDate
var url: URL
/// Creates a folder monitor object with monitoring enabled.
init(url: URL, handler: @escaping ()->Void) {
self.url = url
descriptor = open((url as NSURL).fileSystemRepresentation, O_EVTONLY)
source = DispatchSource.makeFileSystemObjectSource(fileDescriptor: descriptor, eventMask: .write, queue: qq)
// 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
func start() {
if !state {
state = true
source.resume()
}
}
/// Stops sending notifications if currently enabled
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))
}
}
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
}
}
+514
View File
@@ -0,0 +1,514 @@
//
// OneDriveFileProvider.swift
// FileProvider
//
// Created by Amir Abbas Mousavian.
// Copyright © 2017 Mousavian. Distributed under MIT license.
//
import Foundation
import CoreGraphics
/**
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"
}
}
}
/// 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 init(credential: URLCredential?, serverURL: URL? = nil, drive: String?, cache: URLCache? = nil) {
let baseURL = serverURL?.absoluteURL ?? URL(string: "https://api.onedrive.com/")!
let refinedBaseURL = baseURL.absoluteString.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")
self.route = drive.flatMap({ UUID(uuidString: $0) }).flatMap({ Route.drive(uuid: $0) }) ?? .me
super.init(baseURL: refinedBaseURL, credential: credential, cache: cache)
}
/**
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 ?? URL(string: "https://api.onedrive.com/")!
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(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(forKey: "credential") as? URLCredential,
serverURL: aDecoder.decodeObject(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
}
open override func contentsOfDirectory(path: String, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) {
_ = paginated(path, requestHandler: { [weak self] (token) -> URLRequest? in
guard let `self` = self else { return nil }
let url = token.flatMap(URL.init(string:)) ?? self.url(of: path, modifier: "children")
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.set(httpAuthentication: self.credential, with: .oAuth2)
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)
}
open override func attributesOfItem(path: String, completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) {
var request = URLRequest(url: url(of: path))
request.httpMethod = "GET"
request.set(httpAuthentication: credential, with: .oAuth2)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderHTTPError?
var fileObject: OneDriveFileObject?
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 file = OneDriveFileObject(baseURL: self.baseURL, route: self.route, json: json) {
fileObject = file
}
}
completionHandler(fileObject, serverError ?? error)
})
task.resume()
}
open override func storageProperties(completionHandler: @escaping (_ volumeInfo: VolumeObject?) -> Void) {
var request = URLRequest(url: url(of: ""))
request.httpMethod = "GET"
request.set(httpAuthentication: 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.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()
}
open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? {
let queryStr = query.findValue(forKey: "name") as? String ?? query.findAllValues(forKey: nil).flatMap { $0.value as? String }.first
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)
}
open func url(of path: String, modifier: String? = nil) -> URL {
var url: URL = baseURL!
var rpath: String = path
let isId = path.hasPrefix("id:")
url.appendPathComponent(route.drivePath)
if isId {
url.appendPathComponent("root:")
} else {
url.appendPathComponent("items")
}
rpath = rpath.trimmingCharacters(in: pathTrimSet)
switch (modifier == nil, rpath.isEmpty, isId) {
case (true, false, _):
url.appendPathComponent(rpath)
case (true, true, _):
break
case (false, true, _):
url.appendPathComponent(modifier!)
case (false, false, true):
url.appendPathComponent(rpath)
url.appendPathComponent(modifier!)
case (false, false, false):
url.appendPathComponent(rpath + ":")
url.appendPathComponent(modifier!)
}
return url
}
open override func isReachable(completionHandler: @escaping (Bool) -> Void) {
var request = URLRequest(url: url(of: ""))
request.httpMethod = "HEAD"
request.set(httpAuthentication: credential, with: .oAuth2)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
let status = (response as? HTTPURLResponse)?.statusCode ?? 400
completionHandler(status == 200)
})
task.resume()
}
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 .modify(path: let path):
method = "PUT"
let queryStr = overwrite ? "" : "?@name.conflictBehavior=fail"
url = self.url(of: path, modifier: "content\(queryStr)")
case .create(path: let path):
method = "CREATE"
url = self.url(of: path)
case .copy(let source, let dest) where !source.hasPrefix("file://") && !dest.hasPrefix("file://"):
method = "POST"
url = self.url(of: source)
case .copy(let source, let dest) where source.hasPrefix("file://"):
method = "PUT"
let queryStr = overwrite ? "" : "?@name.conflictBehavior=fail"
url = self.url(of: dest, modifier: "content\(queryStr)")
case .copy(let source, let dest) where dest.hasPrefix("file://"):
method = "GET"
url = self.url(of: source, modifier: "content")
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.set(httpAuthentication: credential, with: .oAuth2)
// Remove gzip to fix availability of progress per (Oleg Marchik)[https://github.com/evilutioner] PR (#61)
request.set(httpAcceptEncodings: [.deflate, .identity])
switch operation {
case .copy(let source, let dest) where !source.hasPrefix("file://") && !dest.hasPrefix("file://"),
.move(source: let source, destination: let dest):
request.set(httpContentType: .json)
let cdest = correctPath(dest) as NSString
var parentRefrence: [String: AnyObject] = [:]
if cdest.hasPrefix("id:") {
parentRefrence["id"] = cdest.components(separatedBy: "/").first as NSString?
switch self.route {
case .drive(uuid: let uuid):
parentRefrence["driveId"] = uuid.uuidString as NSString
default:
break
}
} else {
parentRefrence["path"] = cdest.deletingLastPathComponent as NSString
}
var requestDictionary = [String: AnyObject]()
requestDictionary["parentReference"] = parentRefrence 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 ?? "", errorDescription: errorDesc)
}
override var maxUploadSimpleSupported: Int64 {
return 104_857_600 // 100MB
}
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: "action.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 {
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 thumbnailOfFileSupported(path: String) -> Bool {
return true
}
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", "wma":
return true
case "mp4", "mpg", "3gp", "mov", "avi", "wmv":
return true
default:
return false
}
}
open func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) {
let url: URL
if let dimension = dimension {
url = self.url(of: path, modifier: "thumbnails/0/=c\(dimension.width)x\(dimension.height)/content")
} else {
url = self.url(of: path, modifier: "thumbnails/0/small/content")
}
var request = URLRequest(url: url)
request.set(httpAuthentication: credential, with: .oAuth2)
let task = self.session.dataTask(with: request, completionHandler: { (data, response, error) in
var image: ImageClass? = nil
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
let responseError = self.serverError(with: rCode, path: path, data: data)
completionHandler(nil, responseError)
return
}
if let data = data {
image = ImageClass(data: data)
}
completionHandler(image, error)
})
task.resume()
}
open func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) {
var request = URLRequest(url: url(of: path))
request.httpMethod = "GET"
request.set(httpAuthentication: credential, with: .oAuth2)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderHTTPError?
var dic = [String: Any]()
var keys = [String]()
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) }
if let json = data?.deserializeJSON() {
(dic, keys) = self.mapMediaInfo(json)
}
}
completionHandler(dic, keys, serverError ?? error)
})
task.resume()
}
}
+166
View File
@@ -0,0 +1,166 @@
//
// 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 errorDescription: String?
}
/// Containts path, url and attributes of a OneDrive file or resource.
public final class OneDriveFileObject: FileObject {
internal init(baseURL: URL?, name: String, path: String) {
let rpath = (URL(string:path)?.appendingPathComponent(name).absoluteString)!.replacingOccurrences(of: "/", with: "", options: .anchored)
let url = URL(string: rpath, relativeTo: baseURL) ?? URL(string: rpath)!
super.init(url: url, name: name, path: rpath.removingPercentEncoding ?? path)
}
internal convenience init? (baseURL: URL?, route: OneDriveFileProvider.Route, jsonStr: String) {
guard let json = jsonStr.deserializeJSON() else { return nil }
self.init(baseURL: baseURL, route: route, json: json)
}
internal convenience init? (baseURL: URL?, route: OneDriveFileProvider.Route, json: [String: AnyObject]) {
guard let name = json["name"] as? String else { return nil }
guard let path = json["parentReference"]?["path"] as? String else { return nil }
var lPath = path.replacingOccurrences(of: route.drivePath, with: "", options: .anchored, range: nil)
lPath = lPath.replacingOccurrences(of: "/:", with: "", options: .anchored)
lPath = lPath.replacingOccurrences(of: "//", with: "", options: .anchored)
self.init(baseURL: baseURL, name: name, path: lPath)
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 ?? "application/octet-stream"
self.id = json["id"] as? String
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.hash = (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: String {
get {
return allValues[.mimeTypeKey] as? String ?? "application/octet-stream"
}
set {
allValues[.mimeTypeKey] = newValue
}
}
/// 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 hash: String? {
get {
return allValues[.documentIdentifierKey] as? String
}
set {
allValues[.documentIdentifierKey] = newValue
}
}
}
internal extension OneDriveFileProvider {
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 {
if key == "bitrate" || key == "isVariableBitrate" { continue }
let casedKey = spaceCamelCase(key)
add(key: casedKey, value: value)
}
}
add(key: "Bitrate", value: (json["video"] as? NSDictionary)?["bitrate"] as? Int)
return (dic, keys)
}
}
+376
View File
@@ -0,0 +1,376 @@
//
// 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: Error, 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 errorDescription: String? { get }
}
extension FileProviderHTTPError {
public var description: String {
return code.description
}
public var localizedDescription: String {
return 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] = [:]
}
internal func removeSessionHandler(for uuid: String) {
_ = completionHandlersForTasks.removeValue(forKey: uuid)
_ = downloadCompletionHandlersForTasks.removeValue(forKey: uuid)
_ = dataCompletionHandlersForTasks.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):
progress.completedUnitCount = newVal
if let startTime = progress.userInfo[ProgressUserInfoKey.startingTimeKey] as? Date, let task = object as? URLSessionTask {
let elapsed = Date().timeIntervalSince(startTime)
let throughput = Double(newVal) / elapsed
progress.setUserInfoObject(NSNumber(value: throughput), forKey: .throughputKey)
if task.countOfBytesExpectedToSend > 0 {
let remain = task.countOfBytesExpectedToSend - task.countOfBytesSent
let estimatedTimeRemaining = Double(remain) / elapsed
progress.setUserInfoObject(NSNumber(value: estimatedTimeRemaining), forKey: .estimatedTimeRemainingKey)
}
}
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
}
fileProvider.delegateNotify(op, progress: Double(totalBytesSent) / Double(totalBytesExpectedToSend))
}
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"
}
}
}
+234
View File
@@ -0,0 +1,234 @@
//
// SMBTransmitter.swift
// FileProvider
//
// Created by Amir Abbas Mousavian.
// Copyright © 2016 Mousavian. Distributed under MIT license.
//
import Foundation
// This client implementation is for little-endian platform, namely x86, x64 & arm
// For big-endian platforms like PowerPC, there must be a huge overhaul
protocol FileProviderSMBTaskDelegate: class {
func receivedResponse(client: FileProviderSMBTask, response: SMBResponse, for: SMBRequest)
}
class FileProviderSMBTask: FileProviderStreamTask {
var timeout: TimeInterval = 30
private(set) var lastMessageID: UInt64 = 0
private(set) var sessionId: UInt64 = 0
private func messageId() -> UInt64 {
defer {
lastMessageID += 1
}
return lastMessageID
}
private(set) var establishedTrees = Array<SMB2.TreeConnectResponse>()
private(set) var requestStack = [Int: SMBRequest]()
private(set) var responseStack = [Int: SMBResponse]()
weak var delegate: FileProviderSMBTaskDelegate?
func sendNegotiate(completionHandler: SimpleCompletionHandler) -> UInt64 {
let mId = messageId()
let smbHeader = SMB2.Header(command: .NEGOTIATE, creditRequestResponse: UInt16(126), messageId: mId, treeId: UInt32(0), sessionId: UInt64(0))
let msg = SMB2.NegotiateRequest()
let data = createSMB2Message(header: smbHeader, message: msg)
self.write(data, timeout: timeout, completionHandler: { (e) in
completionHandler?(e)
})
return mId
}
func sendSessionSetup(completionHandler: SimpleCompletionHandler) -> UInt64 {
let mId = messageId()
let credit = UInt16(sessionId > 0 ? 124 : 125)
let smbHeader = SMB2.Header(command: SMB2.Command.SESSION_SETUP, creditRequestResponse: credit, messageId: mId, treeId: UInt32(0), sessionId: sessionId)
let msg = SMB2.SessionSetupRequest(singing: [])
let data = createSMB2Message(header: smbHeader, message: msg)
self.write(data, timeout: timeout, completionHandler: { (e) in
if self.sessionId == 0 {
self.readData(ofMinLength: 64, maxLength: 65536, timeout: self.timeout, completionHandler: { (data, eof, e2) in
// TODO: set session id
completionHandler?(e2 ?? e)
})
}
})
return mId
}
func sendTreeConnect(completionHandler: SimpleCompletionHandler) -> UInt64 {
let req = self.currentRequest ?? self.originalRequest
guard let url = req?.url, let host = url.host else {
return 0
}
let mId = messageId()
let smbHeader = SMB2.Header(command: .TREE_CONNECT, creditRequestResponse: 123, messageId: mId, treeId: 0, sessionId: sessionId)
let cmp = url.pathComponents
let share = cmp.first ?? ""
let tcHeader = SMB2.TreeConnectRequest.Header(flags: [])
let msg = SMB2.TreeConnectRequest(header: tcHeader, host: host, share: share)
let data = createSMB2Message(header: smbHeader, message: msg!)
self.write(data, timeout: timeout, completionHandler: { (e) in
completionHandler?(e)
})
return mId
}
func sendTreeDisconnect(id treeId: UInt32, completionHandler: SimpleCompletionHandler) -> UInt64 {
let mId = messageId()
let smbHeader = SMB2.Header(command: .TREE_DISCONNECT, creditRequestResponse: 111, messageId: mId, treeId: treeId, sessionId: sessionId)
let msg = SMB2.TreeDisconnect()
let data = createSMB2Message(header: smbHeader, message: msg)
self.write(data, timeout: timeout, completionHandler: { (e) in
completionHandler?(e)
})
return mId
}
func sendLogoff(id treeId: UInt32, completionHandler: SimpleCompletionHandler) -> UInt64 {
let mId = messageId()
let smbHeader = SMB2.Header(command: .LOGOFF, creditRequestResponse: 0, messageId: mId, treeId: 0, sessionId: sessionId)
let msg = SMB2.LogOff()
let data = createSMB2Message(header: smbHeader, message: msg)
self.write(data, timeout: timeout, completionHandler: { (e) in
completionHandler?(e)
})
return mId
}
func reset() {
}
}
// MARK: create and analyse messages
extension FileProviderSMBTask {
func determineSMBVersion(_ data: Data) -> Float {
let smbverChar: Int8 = Int8(bitPattern: data.first ?? 0)
let version = 0 - smbverChar
return Float(version)
}
func digestSMBMessage(_ data: Data) throws -> (header: SMB1.Header, blocks: [(params: [UInt16], message: Data?)]) {
guard data.count > 30 else {
throw URLError(.badServerResponse)
}
var buffer = [UInt8](repeating: 0, count: data.count)
guard determineSMBVersion(data) == 1 else {
throw SMBFileProviderError.incompatibleHeader
}
let headersize = MemoryLayout<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)
}
func digestSMB2Message(_ data: Data) throws -> SMBResponse? {
guard data.count > 65 else {
throw URLError(.badServerResponse)
}
guard determineSMBVersion(data) == 2 else {
throw SMBFileProviderError.incompatibleHeader
}
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))
case .SESSION_SETUP:
return (header, SMB2.SessionSetupResponse(data: messageData))
case .LOGOFF:
return (header, SMB2.LogOff(data: messageData))
case .TREE_CONNECT:
return (header, SMB2.TreeConnectResponse(data: messageData))
case .TREE_DISCONNECT:
return (header, SMB2.TreeDisconnect(data: messageData))
case .CREATE:
return (header, SMB2.CreateResponse(data: messageData))
case .CLOSE:
return (header, SMB2.CloseResponse(data: messageData))
case .FLUSH:
return (header, SMB2.FlushResponse(data: messageData))
case .READ:
return (header, SMB2.ReadRespone(data: messageData))
case .WRITE:
return (header, SMB2.WriteResponse(data: messageData))
case .LOCK:
return (header, SMB2.LockResponse(data: messageData))
case .IOCTL:
return (header, SMB2.IOCtlResponse(data: messageData))
case .CANCEL:
return (header, nil)
case .ECHO:
return (header, SMB2.Echo(data: messageData))
case .QUERY_DIRECTORY:
return (header, SMB2.QueryDirectoryResponse(data: messageData))
case .CHANGE_NOTIFY:
return (header, SMB2.ChangeNotifyResponse(data: messageData))
case .QUERY_INFO:
return (header, SMB2.QueryInfoResponse(data: messageData))
case .SET_INFO:
return (header, SMB2.SetInfoResponse(data: messageData))
case .OPLOCK_BREAK:
return (header, nil) // FIXME:
case .INVALID:
throw SMBFileProviderError.invalidCommand
}
}
func createSMBMessage(header: SMB1.Header, blocks: [(params: Data?, message: Data?)]) -> Data {
var result = Data(value: header)
for block in blocks {
var paramWordsCount = UInt8(block.params?.count ?? 0)
result.append(&paramWordsCount, count: MemoryLayout.size(ofValue: paramWordsCount))
if let params = block.params {
result.append(params)
}
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.append(message)
}
}
return result
}
func createSMB2Message(header: SMB2.Header, message: SMBRequestBody) -> Data {
var result = Data(value: header)
result.append(message.data())
return result
}
}
+167
View File
@@ -0,0 +1,167 @@
//
// SambaFileProvider.swift
// FileProvider
//
// Created by Amir Abbas Mousavian.
// Copyright © 2016 Mousavian. Distributed under MIT license.
//
import Foundation
class SMBFileProvider: FileProvider, FileProviderMonitor {
open class var type: String { return "SMB" }
open var baseURL: URL?
open var currentPath: String = ""
open var dispatch_queue: DispatchQueue
open var operation_queue: OperationQueue
open weak var delegate: FileProviderDelegate?
open var credential: URLCredential?
public typealias FileObjectClass = FileObject
public init? (baseURL: URL, credential: URLCredential?) {
guard baseURL.uw_scheme.lowercased() == "smb" else {
return nil
}
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 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 ?? ""
}
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()
}
open func attributesOfItem(path: String, completionHandler: @escaping (_ attributes: FileObjectClass?, _ error: Error?) -> Void) {
NotImplemented()
}
open func storageProperties(completionHandler: @escaping (_ volume: VolumeObject?) -> Void) {
NotImplemented()
}
func isReachable(completionHandler: @escaping (Bool) -> Void) {
NotImplemented()
}
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()
}
open func unregisterNotifcation(path: String) {
NotImplemented()
}
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
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"
}
}
private extension SMBFileProvider {
func getPID() -> UInt32 {
return UInt32(ProcessInfo.processInfo.processIdentifier)
}
}
+221
View File
@@ -0,0 +1,221 @@
//
// CIFSTypes.swift
// ExtDownloader
//
// Created by Amir Abbas Mousavian on 4/30/95.
// Copyright © 1395 Mousavian. All rights reserved.
//
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
fileprivate var _command: UInt8
var command: Command {
get {
return Command(rawValue: _command) ?? .INVALID
}
set {
_command = newValue.rawValue
}
}
// error messages from the server to the client
fileprivate var _status: (UInt8, UInt8, UInt8, UInt8)
var error: (Class: UInt8, code: UInt16) {
get {
return (_status.0, UInt16(_status.2) + (UInt16(_status.3) << 8))
}
set {
_status = (newValue.Class, 0, UInt8(newValue.code & 0xff), UInt8(newValue.code >> 8))
}
}
var ntStatus: UInt32 {
get {
let statusLo = UInt32(_status.0) + UInt32(_status.1) << 8
let statusHi = UInt32(_status.2) + UInt32(_status.3) << 8
return statusHi << 16 + statusLo
}
set {
_status = (UInt8(newValue & 0xff), UInt8(newValue >> 8 & 0xff), UInt8(newValue >> 16 & 0xff), UInt8(newValue >> 24 & 0xff))
}
}
var flags: Flags
var flags2: Flags2
var pidHigh: UInt16
// encryption key used for validating messages over connectionless transports
fileprivate var _securityKey: (UInt16, UInt16)
var securityKey: UInt32 {
get {
return UInt32(_securityKey.1) << 16 + UInt32(_securityKey.0)
}
set {
_securityKey = (UInt16(newValue & 0xffff), UInt16(newValue >> 16))
}
}
/// Connection identifier
var securityCID: UInt16
/// Identifier of the sequence of a message over connectionless transports
var securitySequenceNumber: UInt16
fileprivate var ununsed: UInt16
var treeId: UInt16
var pidLow: UInt16
var userId: UInt16
var multiplexId: UInt16
var pid: UInt32 {
get {
return UInt32(pidLow) + UInt32(pidHigh) << 16
}
set {
pidLow = UInt16(newValue & 0xffff)
pidHigh = UInt16(newValue >> 16)
}
}
// 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))
self.flags = flags
self.flags2 = flags2
self._securityKey = (UInt16(securityKey & 0xffff), UInt16(securityKey >> 16))
self.securityCID = securityCID
self.securitySequenceNumber = securitySequenceNumber
self.ununsed = 0
self.treeId = treeId
self.pidLow = UInt16(pid & 0xffff)
self.pidHigh = UInt16(pid >> 16)
self.userId = userId
self.multiplexId = multiplexId
}
// codebeat:enable[ARITY]
}
struct Flags: OptionSet {
let rawValue: UInt8
init(rawValue: UInt8) {
self.rawValue = rawValue
}
/** This bit is set (1) in the NEGOTIATE (0x72) Response if the server supports
* LOCK_AND_READ (0x13) and WRITE_AND_UNLOCK (0x14) commands. */
static let LOCK_AND_READ_OK = Flags(rawValue: 0x01)
static let BUF_AVAIL = Flags(rawValue: 0x02)
static let CASE_INSENSITIVE = Flags(rawValue: 0x08)
static let CANONICALIZED_PATHS = Flags(rawValue: 0x10)
static let OPLOCK = Flags(rawValue: 0x20)
static let OPBATCH = Flags(rawValue: 0x40)
/** When on, this message is being sent from the server in response to a client request. */
static let REPLY = Flags(rawValue: 0x80)
}
struct Flags2: OptionSet {
let rawValue: UInt16
init(rawValue: UInt16) {
self.rawValue = rawValue
}
/** Client: the message MAY contain long file names. */
static let LONG_NAMES = Flags2(rawValue: 0x0001)
/** Client: the client is aware of extended attributes (EAs). */
static let EAS = Flags2(rawValue: 0x0002)
/** Client: the client is requesting signing (if signing is not yet active) or the message
* being sent is signed. This bit is used on the SMB header of an SESSION_SETUP_ANDX */
static let SMB_SECURITY_SIGNATURE = Flags2(rawValue: 0x0004)
/// Reserved but not implemented.
static let IS_LONG_NAME = Flags2(rawValue: 0x0040)
/** client aware of Extended Security negotiation */
static let EXT_SEC = Flags2(rawValue: 0x0800)
/** any pathnames in this SMB SHOULD be resolved in the Distributed File System (DFS) */
static let DFS = Flags2(rawValue: 0x1000)
/**
* This flag is useful only on a read request. If the bit is set, then the client MAY read
* the file if the client does not have read permission but does have execute permission. */
static let PAGING_IO = Flags2(rawValue: 0x2000) // READ_IF_EXECUTE
/**
* Client: the server MUST return errors as 32-bit NTSTATUS codes in the response
* Server: the Status field in the header is formatted as an NTSTATUS cod */
static let ERR_STATUS = Flags2(rawValue: 0x4000)
/**
* Each field that contains a string in this SMB message MUST be encoded
* as an array of 16-bit Unicode characters */
static let UNICODE = Flags2(rawValue: 0x8000)
}
enum Command: UInt8 {
case CREATE_DIRECTORY = 0x00
case DELETE_DIRECTORY = 0x01
case OPEN = 0x02
case CREATE = 0x03
case CLOSE = 0x04
case FLUSH = 0x05
case DELETE = 0x06
case RENAME = 0x07
case QUERY_INFORMATION = 0x08
case SET_INFORMATION = 0x09
case READ = 0x0A
case WRITE = 0x0B
case LOCK_BYTE_RANGE = 0x0C
case UNLOCK_BYTE_RANGE = 0x0D
case CREATE_TEMPORARY = 0x0E
case CREATE_NEW = 0x0F
case CHECK_DIRECTORY = 0x10
case PROCESS_EXIT = 0x11
case SEEK = 0x12
case LOCK_AND_READ = 0x13
case WRITE_AND_UNLOCK = 0x14
case READ_RAW = 0x1A
case READ_MPX = 0x1B
case READ_MPX_SECONDARY = 0x1C
case WRITE_RAW = 0x1D
case WRITE_MPX = 0x1E
case WRITE_COMPLETE = 0x20
case SET_INFORMATION2 = 0x22
case QUERY_INFORMATION2 = 0x23
case LOCKING_ANDX = 0x24
case TRANSACTION = 0x25
case TRANSACTION_SECONDARY = 0x26
case IOCTL = 0x27
case IOCTL_SECONDARY = 0x28
case COPY = 0x29
case MOVE = 0x2A
case ECHO = 0x2B
case WRITE_AND_CLOSE = 0x2C
case OPEN_ANDX = 0x2D
case READ_ANDX = 0x2E
case WRITE_ANDX = 0x2F
case CLOSE_AND_TREE_DISC = 0x31
case TRANSACTION2 = 0x32
case TRANSACTION2_SECONDARY = 0x33
case FIND_CLOSE2 = 0x34
case FIND_NOTIFY_CLOSE = 0x35
case TREE_CONNECT = 0x70
case TREE_DISCONNECT = 0x71
case NEGOTIATE = 0x72
case SESSION_SETUP_ANDX = 0x73
case LOGOFF_ANDX = 0x74
case TREE_CONNECT_ANDX = 0x75
case QUERY_INFORMATION_DISK = 0x80
case SEARCH = 0x81
case FIND = 0x82
case FIND_UNIQUE = 0x83
case NT_TRANSACT = 0xA0
case NT_TRANSACT_SECONDARY = 0xA1
case NT_CREATE_ANDX = 0xA2
case NT_CANCEL = 0xA4
case OPEN_PRINT_FILE = 0xC0
case WRITE_PRINT_FILE = 0xC1
case CLOSE_PRINT_FILE = 0xC2
case GET_PRINT_QUEUE = 0xC3
case READ_BULK = 0xD8
case WRITE_BULK = 0xD9
case WRITE_BULK_DATA = 0xDA
case INVALID = 0xFE
}
}
// codebeat:enable[TOO_MANY_IVARS]
+68
View File
@@ -0,0 +1,68 @@
//
// SMB2DataTypes.swift
// ExtDownloader
//
// Created by Amir Abbas Mousavian on 4/30/95.
// Copyright © 1395 Mousavian. All rights reserved.
//
import Foundation
protocol SMBRequestBody {
func data() -> Data
}
extension SMBRequestBody {
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: Int64
init(time: Int64) {
self.time = time
}
init(unixTime: UInt) {
self.time = (Int64(unixTime) + 11644473600) * 10000000
}
init(timeIntervalSince1970: TimeInterval) {
self.time = Int64((timeIntervalSince1970 + 11644473600) * 10000000)
}
init(date: Date) {
self.init(timeIntervalSince1970: date.timeIntervalSince1970)
}
var unixTime: UInt {
return UInt(self.time / 10000000 - 11644473600)
}
var date: Date {
return Date(timeIntervalSince1970: Double(self.time) / 10000000 - 11644473600)
}
}
+426
View File
@@ -0,0 +1,426 @@
//
// SMB2CreateClose.swift
// ExtDownloader
//
// Created by Amir Abbas Mousavian on 4/30/95.
// Copyright © 1395 Mousavian. All rights reserved.
//
import Foundation
extension SMB2 {
// MARK: SMB2 Create
struct CreateRequest: SMBRequestBody {
let header: CreateRequest.Header
let name: String?
let contexts: [CreateContext]
init (header: CreateRequest.Header, name: String? = nil, contexts: [CreateContext] = []) {
self.header = header
self.name = name
self.contexts = contexts
}
func data() -> Data {
var header = self.header
var offset = 0x78 //UInt16(sizeof(SMB2.Header.self) + sizeof(CreateContext.Header.self) - 1)
var body = Data()
if let name = self.name, let nameData = name.data(using: .utf16) {
header.nameOffset = UInt16(offset)
header.nameLength = UInt16(nameData.count)
offset += nameData.count
body.append(nameData)
}
if contexts.count > 0 {
// TODO: Context CreateRequest implementation, 8 bit allign offset
header.contextOffset = UInt32(offset)
header.contextLength = 0
//result.appendData(nameData)
}
var result = Data(value: header)
result.append(body)
return result
}
struct Header {
let size: UInt16
fileprivate let securityFlags: UInt8
fileprivate var _requestedOplockLevel: UInt8
var requestedOplockLevel: OplockLevel {
get {
return OplockLevel(rawValue: _requestedOplockLevel)!
}
set {
_requestedOplockLevel = newValue.rawValue
}
}
fileprivate var _impersonationLevel: UInt32
var impersonationLevel: ImpersonationLevel {
get {
return ImpersonationLevel(rawValue: _impersonationLevel)!
}
set {
_impersonationLevel = newValue.rawValue
}
}
fileprivate let flags: UInt64
fileprivate let reserved: UInt64
let access: FileAccessMask
let fileAttributes: FileAttributes
let shareAccess: ShareAccess
fileprivate var _desposition: UInt32
var desposition: CreateDisposition {
get {
return CreateDisposition(rawValue: _desposition)!
}
set {
_desposition = newValue.rawValue
}
}
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 = []) {
self.size = 57
self.securityFlags = 0
self._requestedOplockLevel = requestedOplockLevel.rawValue
self._impersonationLevel = impersonationLevel.rawValue
self.flags = 0
self.reserved = 0
self.access = access
self.fileAttributes = fileAttributes
self.shareAccess = shareAccess
self._desposition = desposition.rawValue
self.options = options
self.nameOffset = 0
self.nameLength = 0
self.contextOffset = 0
self.contextLength = 0
}
}
struct CreateOptions: OptionSet {
let rawValue: UInt32
init(rawValue: UInt32) {
self.rawValue = rawValue
}
static let DIRECTORY_FILE = CreateOptions(rawValue: 0x00000001)
static let WRITE_THROUGH = CreateOptions(rawValue: 0x00000002)
static let SEQUENTIAL_ONLY = CreateOptions(rawValue: 0x00000004)
static let NO_INTERMEDIATE_BUFFERING = CreateOptions(rawValue: 0x00000008)
static let NON_DIRECTORY_FILE = CreateOptions(rawValue: 0x00000040)
static let NO_EA_KNOWLEDGE = CreateOptions(rawValue: 0x00000200)
static let RANDOM_ACCESS = CreateOptions(rawValue: 0x00000800)
static let DELETE_ON_CLOSE = CreateOptions(rawValue: 0x00001000)
static let OPEN_BY_FILE_ID = CreateOptions(rawValue: 0x00002000)
static let OPEN_FOR_BACKUP_INTENT = CreateOptions(rawValue: 0x00004000)
static let NO_COMPRESSION = CreateOptions(rawValue: 0x00008000)
static let OPEN_REPARSE_POINT = CreateOptions(rawValue: 0x00200000)
static let OPEN_NO_RECALL = CreateOptions(rawValue: 0x00400000)
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 {
/// If the file already exists, supersede it. Otherwise, create the file.
case SUPERSEDE = 0x00000000
/// If the file already exists, return success; otherwise, fail the operation.
case OPEN = 0x00000001
/// If the file already exists, fail the operation; otherwise, create the file.
case CREATE = 0x00000002
/// Open the file if it already exists; otherwise, create the file.
case OPEN_IF = 0x00000003
/// Overwrite the file if it already exists; otherwise, fail the operation.
case OVERWRITE = 0x00000004
/// Overwrite the file if it already exists; otherwise, create the file.
case OVERWRITE_IF = 0x00000005
}
enum ImpersonationLevel: UInt32 {
case anonymous = 0x00000000
case identification = 0x00000001
case impersonation = 0x00000002
case delegate = 0x00000003
}
}
struct CreateResponse: SMBResponseBody {
struct Header {
let size: UInt16
fileprivate let _oplockLevel: UInt8
var oplockLevel: OplockLevel {
return OplockLevel(rawValue: _oplockLevel)!
}
fileprivate let reserved: UInt32
let creationTime: SMBTime
let lastAccessTime: SMBTime
let lastWriteTime: SMBTime
let changeTime: SMBTime
let allocationSize: UInt64
let endOfFile: UInt64
let fileAttributes: FileAttributes
fileprivate let reserved2: UInt32
let fileId: FileId
let contextsOffset: UInt32
let ContextsLength: UInt32
}
let header: CreateResponse.Header
let contexts: [CreateContext]
init? (data: Data) {
guard data.count >= MemoryLayout<CreateResponse.Header>.size else {
return nil
}
self.header = data.scanValue()!
if self.header.contextsOffset > 0 {
var contexts = [CreateContext]()
var contextOffset = Int(self.header.contextsOffset) - MemoryLayout<SMB2.Header>.size
while contextOffset > 0 {
guard contextOffset < data.count else {
self.contexts = contexts
return
}
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
} else {
self.contexts = []
}
}
}
struct CreateContext {
struct Header {
var next: UInt32
let nameOffset: UInt16
let nameLength: UInt16
fileprivate let reserved: UInt16
let dataOffset: UInt16
let dataLength: UInt32
}
var header: CreateContext.Header
let buffer: Data
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: 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: Data) {
let headersize = MemoryLayout<Header>.size
guard data.count > headersize else {
return nil
}
self.header = data.scanValue()!
self.buffer = data.subdata(in: headersize..<data.count)
}
func data() -> Data {
var result = Data(value: header)
result.append(buffer)
return result
}
enum ContextNames: String {
/// Request Create Context: Extended attributes
case EA_BUFFER = "ExtA"
/// Request Create Context: Security descriptor
case SD_BUFFER = "SecD"
/// Request & Response Create Context: Open to be durable
case DURABLE_HANDLE = "DHnQ"
case DURABLE_HANDLE_RESPONSE_V2 = "DH2Q"
/// Request Create Context: Reconnect to a durable open after being disconnected
case DURABLE_HANDLE_RECONNECT = "DHnC"
/// Request Create Context: Required allocation size of the newly created file
case ALLOCATION_SIZE = "AISi"
/// Request & Response Create Context: Maximal access information
case QUERY_MAXIMAL_ACCESS = "MxAc"
case TIMEWARP_TOKEN = "TWrp"
/// Response Create Context: DiskID of the open file in a volume.
case QUERY_ON_DISK_ID = "QFid"
/// Response Create Context: A lease. This value is only supported for the SMB 2.1 and 3.x dialect family.
case LEASE = "RqLs"
}
}
enum OplockLevel: UInt8 {
case NONE = 0x00
case LEVEL_II = 0x01
case EXCLUSIVE = 0x08
case BATCH = 0x09
case LEASE = 0xFF
}
struct ShareAccess: OptionSet {
let rawValue: UInt32
init(rawValue: UInt32) {
self.rawValue = rawValue
}
static let READ = ShareAccess(rawValue: 0x00000001)
static let WRITE = ShareAccess(rawValue: 0x00000002)
static let DELETE = ShareAccess(rawValue: 0x00000004)
}
struct FileAccessMask: OptionSet {
let rawValue: UInt32
init(rawValue: UInt32) {
self.rawValue = rawValue
}
// File and Printer/Pipe Accesses
static let FILE_READ_DATA = FileAccessMask(rawValue: 0x00000001)
static let FILE_WRITE_DATA = FileAccessMask(rawValue: 0x00000002)
static let FILE_APPEND_DATA = FileAccessMask(rawValue: 0x00000004)
static let FILE_EXECUTE = FileAccessMask(rawValue: 0x00000020)
// Directory
static let FILE_LIST_DIRECTORY = FileAccessMask(rawValue: 0x00000001)
static let FILE_ADD_FILE = FileAccessMask(rawValue: 0x00000002)
static let FILE_ADD_SUBDIRECTORY = FileAccessMask(rawValue: 0x00000004)
static let FILE_TRAVERSE = FileAccessMask(rawValue: 0x00000020)
// Generic
static let FILE_READ_EA = FileAccessMask(rawValue: 0x00000008)
static let FILE_WRITE_EA = FileAccessMask(rawValue: 0x00000010)
static let FILE_DELETE_CHILD = FileAccessMask(rawValue: 0x00000040)
static let FILE_READ_ATTRIBUTES = FileAccessMask(rawValue: 0x00000080)
static let FILE_WRITE_ATTRIBUTES = FileAccessMask(rawValue: 0x00000100)
static let DELETE = FileAccessMask(rawValue: 0x00010000)
static let READ_CONTROL = FileAccessMask(rawValue: 0x00020000)
static let WRITE_DAC = FileAccessMask(rawValue: 0x00040000)
static let WRITE_OWNER = FileAccessMask(rawValue: 0x00080000)
static let SYNCHRONIZE = FileAccessMask(rawValue: 0x00100000)
static let ACCESS_SYSTEM_SECURITY = FileAccessMask(rawValue: 0x01000000)
static let MAXIMUM_ALLOWED = FileAccessMask(rawValue: 0x02000000)
static let GENERIC_ALL = FileAccessMask(rawValue: 0x10000000)
static let GENERIC_EXECUTE = FileAccessMask(rawValue: 0x20000000)
static let GENERIC_WRITE = FileAccessMask(rawValue: 0x40000000)
static let GENERIC_READ = FileAccessMask(rawValue: 0x80000000)
}
struct FileAttributes: OptionSet {
let rawValue: UInt32
init(rawValue: UInt32) {
self.rawValue = rawValue
}
static let READONLY = FileAttributes(rawValue: 0x00000001)
static let HIDDEN = FileAttributes(rawValue: 0x00000002)
static let SYSTEM = FileAttributes(rawValue: 0x00000004)
static let DIRECTORY = FileAttributes(rawValue: 0x00000010)
static let ARCHIVE = FileAttributes(rawValue: 0x00000020)
static let NORMAL = FileAttributes(rawValue: 0x00000080)
static let TEMPORARY = FileAttributes(rawValue: 0x00000100)
static let SPARSE_FILE = FileAttributes(rawValue: 0x00000200)
static let REPARSE_POINT = FileAttributes(rawValue: 0x00000400)
static let COMPRESSED = FileAttributes(rawValue: 0x00000800)
static let OFFLINE = FileAttributes(rawValue: 0x00001000)
static let NOT_CONTENT_INDEXED = FileAttributes(rawValue: 0x00002000)
static let ENCRYPTED = FileAttributes(rawValue: 0x00004000)
static let INTEGRITY_STREAM = FileAttributes(rawValue: 0x00008000)
static let NO_SCRUB_DATA = FileAttributes(rawValue: 0x00020000)
}
struct FileId {
let persistent: UInt64
let volatile: UInt64
}
// MARK: SMB2 Close
struct CloseRequest: SMBRequestBody {
let size: UInt16
let flags: CloseFlags
fileprivate let reserved2: UInt32
let filePersistantId: UInt64
let fileVolatileId: UInt64
init(filePersistantId: UInt64, fileVolatileId: UInt64) {
self.size = 24
self.filePersistantId = filePersistantId
self.fileVolatileId = fileVolatileId
self.flags = []
self.reserved2 = 0
}
}
struct CloseResponse: SMBResponseBody {
let size: UInt16
let flags: CloseFlags
fileprivate let reserved: UInt32
let creationTime: SMBTime
let lastAccessTime: SMBTime
let lastWriteTime: SMBTime
let changeTime: SMBTime
let allocationSize: UInt64
let endOfFile: UInt64
let fileAttributes: FileAttributes
}
struct CloseFlags: OptionSet {
let rawValue: UInt16
init(rawValue: UInt16) {
self.rawValue = rawValue
}
static let POSTQUERY_ATTRIB = Flags(rawValue: 0x0001)
}
// MARK: SMB2 Flush
struct FlushRequest: SMBRequestBody {
let size: UInt16
fileprivate let reserved: UInt16
fileprivate let reserved2: UInt32
let filePersistantId: UInt64
let fileVolatileId: UInt64
init(filePersistantId: UInt64, fileVolatileId: UInt64) {
self.size = 24
self.filePersistantId = filePersistantId
self.fileVolatileId = fileVolatileId
self.reserved = 0
self.reserved2 = 0
}
}
struct FlushResponse: SMBResponseBody {
let size: UInt16
let reserved: UInt16
init() {
self.size = 4
self.reserved = 0
}
}
}
+228
View File
@@ -0,0 +1,228 @@
//
// SMB2FileOperation.swift
// ExtDownloader
//
// Created by Amir Abbas Mousavian on 4/30/95.
// Copyright © 1395 Mousavian. All rights reserved.
//
import Foundation
extension SMB2 {
// MARK: SMB2 Read
struct ReadRequest: SMBRequestBody {
let size: UInt16
fileprivate let padding: UInt8
let flags: ReadRequest.Flags
let length: UInt32
let offset: UInt64
let fileId: FileId
let minimumLength: UInt32
fileprivate let _channel: UInt32
var channel: Channel {
return Channel(rawValue: _channel) ?? .NONE
}
let remainingBytes: UInt32
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
self.padding = 0
self.flags = flags
self.length = length
self.offset = offset
self.fileId = fileId
self.minimumLength = minimumLength
self._channel = channel.rawValue
self.remainingBytes = remainingBytes
self.channelInfoOffset = 0
self.channelInfoLength = 0
self.channelBuffer = 0
}
struct Flags: OptionSet {
let rawValue: UInt8
init(rawValue: UInt8) {
self.rawValue = rawValue
}
static let UNBUFFERED = Flags(rawValue: 0x01)
}
}
struct ReadRespone: SMBResponseBody {
struct Header {
let size: UInt16
let offset: UInt8
fileprivate let reserved: UInt8
let length: UInt32
let remaining: UInt32
fileprivate let reserved2: UInt32
}
let header: ReadRespone.Header
let buffer: Data
init?(data: Data) {
guard data.count > 16 else {
return nil
}
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
}
// MARK: SMB2 Write
struct WriteRequest: SMBRequestBody {
let header: WriteRequest.Header
let channelInfo: ChannelInfo?
let fileData: Data
struct Header {
let size: UInt16
let dataOffset: UInt16
let length: UInt32
let offset: UInt64
let fileId: FileId
fileprivate let _channel: UInt32
var channel: Channel {
return Channel(rawValue: _channel) ?? .NONE
}
let remainingBytes: UInt32
let channelInfoOffset: UInt16
let channelInfoLength: UInt16
let 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 {
channelInfoOffset = UInt16(MemoryLayout<SMB2.Header>.size + MemoryLayout<WriteRequest.Header>.size)
channelInfoLength = UInt16(MemoryLayout<SMB2.ChannelInfo>.size)
}
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.rawValue, remainingBytes: remainingBytes, channelInfoOffset: channelInfoOffset, channelInfoLength: channelInfoLength, flags: flags)
self.channelInfo = channelInfo
self.fileData = data
}
// codebeat:enable[ARITY]
func data() -> Data {
var result = Data(value: self.header)
if let channelInfo = channelInfo {
result.append(channelInfo.data())
}
result.append(fileData)
return result
}
struct Flags: OptionSet {
let rawValue: UInt32
init(rawValue: UInt32) {
self.rawValue = rawValue
}
static let THROUGH = Flags(rawValue: 0x00000001)
static let UNBUFFERED = Flags(rawValue: 0x00000002)
}
}
struct WriteResponse: SMBResponseBody {
let size: UInt16
fileprivate let reserved: UInt16
let writtenBytes: UInt32
fileprivate let remaining: UInt32
fileprivate let channelInfoOffset: UInt16
fileprivate let channelInfoLength: UInt16
}
struct ChannelInfo: SMBRequestBody {
let offset: UInt64
let token: UInt32
let length: UInt32
}
// MARK: SMB2 Lock
struct LockElement: SMBRequestBody {
let offset: UInt64
let length: UInt64
let flags: LockElement.Flags
fileprivate let reserved: UInt32
struct Flags: OptionSet {
let rawValue: UInt32
init(rawValue: UInt32) {
self.rawValue = rawValue
}
static let SHARED_LOCK = Flags(rawValue: 0x00000001)
static let EXCLUSIVE_LOCK = Flags(rawValue: 0x00000002)
static let UNLOCK = Flags(rawValue: 0x00000004)
static let FAIL_IMMEDIATELY = Flags(rawValue: 0x00000010)
}
}
struct LockRequest: SMBRequestBody {
let header: LockRequest.Header
let locks: [LockElement]
init(fileId: FileId,locks: [LockElement], lockSequenceNumber : Int8 = 0, lockSequenceIndex: UInt32 = 0) {
self.header = LockRequest.Header(size: 48, lockCount: UInt16(locks.count), lockSequence: UInt32(lockSequenceNumber << 28) + lockSequenceIndex, fileId: fileId)
self.locks = locks
}
func data() -> Data {
var result = Data(value: header)
for lock in locks {
result.append(Data(value: lock))
}
return result
}
struct Header {
let size: UInt16
fileprivate let lockCount: UInt16
let lockSequence: UInt32
let fileId : FileId
}
}
struct LockResponse: SMBResponseBody {
let size: UInt16
let reserved: UInt16
init() {
self.size = 4
self.reserved = 0
}
}
// MARK: SMB2 Cancel
struct CancelRequest: SMBRequestBody {
let size: UInt16
let reserved: UInt16
init() {
self.size = 4
self.reserved = 0
}
}
}
+356
View File
@@ -0,0 +1,356 @@
//
// SMB2IOCtl.swift
// ExtDownloader
//
// Created by Amir Abbas Mousavian on 4/30/95.
// Copyright © 1395 Mousavian. All rights reserved.
//
import Foundation
extension SMB2 {
// MARK: SMB2 IOCTL
/**
* IOCtl usage is usually limited in SMB to pipe requests and duplicating file inside server
*/
struct IOCtlRequest: SMBRequestBody {
let header: Header
let requestData: IOCtlRequestProtocol?
init(fileId: FileId ,ctlCode: IOCtlCode, requestData: IOCtlRequestProtocol?, flags: IOCtlRequest.Flags = []) {
let offset = requestData != nil ? UInt32(MemoryLayout<SMB2.Header>.size + MemoryLayout<IOCtlRequest.Header>.size) : 0
self.header = Header(size: 57, reserved: 0, _ctlCode: ctlCode.rawValue, fileId: fileId, inputOffset: offset, inputCount: UInt32((requestData?.data().count ?? 0)), maxInputResponse: 0, outputOffset: offset, outputCount: 0, maxOutputResponse: UInt32(Int32.max), flags: flags, reserved2: 0)
self.requestData = requestData
}
func data() -> Data {
var result = Data(value: self.header)
if let reqData = requestData?.data() {
result.append(reqData)
}
return result
}
struct Header {
let size: UInt16
fileprivate let reserved: UInt16
fileprivate let _ctlCode: UInt32
var ctlCode: IOCtlCode {
return IOCtlCode(rawValue: _ctlCode)!
}
let fileId: FileId
let inputOffset: UInt32
let inputCount: UInt32
let maxInputResponse: UInt32
let outputOffset: UInt32
let outputCount: UInt32
let maxOutputResponse: UInt32
let flags: IOCtlRequest.Flags
fileprivate let reserved2: UInt32
}
struct Flags: OptionSet {
let rawValue: UInt32
init(rawValue: UInt32) {
self.rawValue = rawValue
}
static let IOCTL = Flags(rawValue: 0x00000000)
static let FSCTL = Flags(rawValue: 0x00000001)
}
}
struct IOCtlResponse: SMBResponseBody {
let header: Header
let responseData: IOCtlResponseProtocol?
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)
case .SRV_ENUMERATE_SNAPSHOTS:
self.responseData = IOCtlResponseData.SrvSnapshots(data: response)
case .SRV_REQUEST_RESUME_KEY:
self.responseData = IOCtlResponseData.ResumeKey(data: response)
case .SRV_READ_HASH:
self.responseData = IOCtlResponseData.ReadHash(data: response)
case .QUERY_NETWORK_INTERFACE_INFO:
self.responseData = IOCtlResponseData.NetworkInterfaceInfo(data: response)
case .VALIDATE_NEGOTIATE_INFO:
self.responseData = IOCtlResponseData.ValidateNegotiateInfo(data: response)
default:
self.responseData = nil
}
}
struct Header {
let size: UInt16
fileprivate let reserved: UInt16
fileprivate let _ctlCode: UInt32
var ctlCode: IOCtlCode {
return IOCtlCode(rawValue: _ctlCode)!
}
let fileId: FileId
let inputOffset: UInt32
let inputCount: UInt32
let outputOffset: UInt32
let outputCount: 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
/// PIPE_TRANSCEIVE is valid only on a named pipe with mode set to FILE_PIPE_MESSAGE_MODE.
case PIPE_TRANSCEIVE = 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
/// Get all the revision time-stamps that are associated with the Tree Connect share in which the open resides
case SRV_ENUMERATE_SNAPSHOTS = 0x00144064
/// Reads a chunk of file for performing server side copy operations.
case SRV_COPYCHUNK = 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
/// Writes the chunk of file for performing server side copy operations.
case SRV_COPYCHUNK_WRITE = 0x001480F2
/// Request resiliency for a specified open file, not valid for the SMB 2.0.2 dialect.
case LMR_REQUEST_RESILIENCY = 0x001401D4
/// Get server network interface info e.g. link speed and socket address information
case QUERY_NETWORK_INTERFACE_INFO = 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
}
struct IOCtlRequestData {
struct CopyChunk: IOCtlRequestProtocol {
let sourceKey: (UInt64, UInt64, UInt64)
let chunkCount: UInt32
let chunks: [Chunk]
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
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
}
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.length = length
self.offset = offset
}
}
struct ResilencyRequest: IOCtlRequestProtocol {
let timeout: 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
}
}
struct ValidateNegotiateInfo: IOCtlRequestProtocol {
let header: ValidateNegotiateInfo.Header
let dialects: [UInt16]
init(dialects: [UInt16], guid: uuid_t, capabilities: IOCtlCapabilities, securityMode: UInt16) {
self.header = Header(capabilities: capabilities, guid: guid, securityMode: securityMode, dialectCount: UInt16(dialects.count))
self.dialects = dialects
}
func data() -> Data {
var result = Data(value: self.header)
dialects.forEach { result.append(Data(value: $0)) }
return result
}
struct Header {
let capabilities: IOCtlCapabilities
/// Client's GUID
let guid: uuid_t
let securityMode: UInt16
let dialectCount: UInt16
}
}
}
struct IOCtlResponseData {
// SRV_COPYCHUNK, SRV_COPYCHUNK_WRITE
struct SrvCopyChunk: IOCtlResponseProtocol {
let chunksCount: UInt32
let chunksBytesWritten: UInt32
let totalBytesWriiten: UInt32
}
// SRV_ENUMERATE_SNAPSHOTS
struct SrvSnapshots: IOCtlResponseProtocol {
let count: UInt32
let returnedCount: UInt32
let snapshots: [SMBTime]
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 = DateFormatter()
dateFormatter.dateFormat = "'@GMT-'yyyy'.'MM'.'dd'-'HH'.'mm'.'ss"
for i in 0..<Int(returnedCount) {
let offset = 24 + i * 48
if data.count < offset + 48 {
return nil
}
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))
}
}
self.snapshots = snapshots
}
}
struct ResumeKey: IOCtlResponseProtocol {
let key: (UInt64, UInt64, UInt64)
fileprivate let contextLength: UInt32
fileprivate let context: UInt32
}
struct ReadHash: IOCtlResponseProtocol {
// TODO: Implement IOCTL READ_HASH
}
struct NetworkInterfaceInfo: IOCtlResponseProtocol {
let items: [NetworkInterfaceInfo.Item]
init?(data: Data) {
var items = [Item]()
var offset = 0
while let item: Item = data.scanValue(start: offset) {
items.append(item)
offset += MemoryLayout<Item>.size
}
self.items = items
}
struct Item {
/// The offset, in bytes, from the beginning of this structure to the beginning of a subsequent 8-byte aligned network interface.
let next: UInt32
/// specifies the network interface index.
let ifIndex: UInt32
let capability: IOCtlCapabilities
fileprivate let reserved: UInt32
/// Speed of the network interface in bits per second
let linkSpeed: UInt64
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,
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,
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,
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)
var family: sa_family_t {
return sockaddrStorage.1
}
static let ipv4: sa_family_t = 0x02
static let ipv6: sa_family_t = 0x17
var sockaddr: sockaddr_in {
return Data.mapMemory(from: self.sockaddrStorage)!
}
var sockaddr6: sockaddr_in6 {
return Data.mapMemory(from: self.sockaddrStorage)!
}
}
}
struct ValidateNegotiateInfo: IOCtlResponseProtocol {
let capabilities: IOCtlCapabilities
let guid: uuid_t
let securityMode: UInt16
fileprivate let _dialect: UInt16
var dialect: (major: Int, minor: Int) {
return (major: Int(_dialect & 0xFF), minor: Int(_dialect >> 8))
}
}
}
struct IOCtlCapabilities: OptionSet {
let rawValue: UInt32
init(rawValue: UInt32) {
self.rawValue = rawValue
}
static let RSS_CAPABLE = IOCtlCapabilities(rawValue: 0x00000001)
static let RDMA_CAPABLE = IOCtlCapabilities(rawValue: 0x00000002)
}
enum IOCtlHashType: UInt32 {
case PEER_DIST = 0x00000001
}
enum IOCtlHashVersion: UInt32 {
case VER_1 = 0x00000001
case VER_2 = 0x00000002
}
enum IOCtlHashRetrievalType: UInt32 {
case HASH_BASED = 0x00000001
case FILE_BASED = 0x00000002
}
}
+133
View File
@@ -0,0 +1,133 @@
//
// SMB2Notification.swift
// FileProvider
//
// Created by Amir Abbas Mousavian on 5/18/95.
//
//
import Foundation
extension SMB2 {
// MARK: SMB2 Change Notify
struct ChangeNotifyRequest: SMBRequestBody {
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
guard let action = FileNotifyAction(rawValue: actionValue) else {
continue
}
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
}
}
enum FileNotifyAction: UInt32 {
/// The file was added to the directory.
case ADDED = 0x00000001
/// The file was removed from the directory.
case REMOVED = 0x00000002
/// The file was modified. This can be a change to the data or attributes of the file.
case MODIFIED = 0x00000003
/// The file was renamed, and this is the old name. If the new name resides outside of the directory being monitored, the client will not receive the FILE_ACTION_RENAMED_NEW_NAME bit value.
case RENAMED_OLD_NAME = 0x00000004
/// The file was renamed, and this is the new name. If the old name resides outside of the directory being monitored, the client will not receive the FILE_ACTION_RENAME_OLD_NAME bit value.
case RENAMED_NEW_NAME = 0x00000005
/// The file was added to a named stream.
case ADDED_STREAM = 0x00000006
/// The file was removed from the named stream.
case REMOVED_STREAM = 0x00000007
/// The file was modified. This can be a change to the data or attributes of the file.
case MODIFIED_STREAM = 0x00000008
/// An object ID was removed because the file the object ID referred to was deleted. This notification is only sent when the directory being monitored is the special directory "\$Extend\$ObjId:$O:$INDEX_ALLOCATION".
case REMOVED_BY_DELETE = 0x00000009
/// An attempt to tunnel object ID information to a file being created or renamed failed because the object ID is in use by another file on the same volume. This notification is only sent when the directory being monitored is the special directory "\$Extend\$ObjId:$O:$INDEX_ALLOCATION".
case NOT_TUNNELLED = 0x0000000A
/// An attempt to tunnel object ID information to a file being renamed failed because the file already has an object ID. This notification is only sent when the directory being monitored is the special directory "\$Extend\$ObjId:$O:$INDEX_ALLOCATION".
case TUNNELLED_ID_COLLISION = 0x0000000B
}
}
+325
View File
@@ -0,0 +1,325 @@
//
// SMB2Query.swift
// ExtDownloader
//
// Created by Amir Abbas Mousavian on 4/31/95.
// Copyright © 1395 Mousavian. All rights reserved.
//
import Foundation
extension SMB2 {
// MARK: SMB2 Query Directory
struct QueryDirectoryRequest: SMBRequestBody {
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)
}
}
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:
header = buffer.scanValue(start: offset) as FileDirectoryInformationHeader!
case .fileFullDirectoryInformation:
header = buffer.scanValue(start: offset) as FileFullDirectoryInformationHeader!
case .fileIdFullDirectoryInformation:
header = buffer.scanValue(start: offset) as FileIdFullDirectoryInformationHeader!
case .fileBothDirectoryInformation:
header = buffer.scanValue(start: offset) as FileBothDirectoryInformationHeader!
case .fileIdBothDirectoryInformation:
header = buffer.scanValue(start: offset) as FileIdBothDirectoryInformationHeader!
case .fileNamesInformation:
header = buffer.scanValue(start: offset) as FileNamesInformationHeader!
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 offset = Int(data.scanValue(start: 2) as UInt16!)
let length = Int(data.scanValue(start: 4) as UInt32!)
guard data.count > offset + length else {
return nil
}
self.buffer = data.subdata(in: offset..<(offset + length))
}
}
// MARK: SMB2 Query Info
struct QueryInfoRequest: SMBRequestBody {
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 length = Int(data.scanValue(start: 4) as UInt32!)
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()!
}
}
}
+597
View File
@@ -0,0 +1,597 @@
//
// 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 {
enum FileInformationEnum: UInt8 {
case none = 0x00
case fileDirectoryInformation = 0x01
case fileFullDirectoryInformation = 0x02
case fileBothDirectoryInformation = 0x03
case fileBasicInformation = 0x04
case fileStandardInformation = 0x05
case fileInternalInformation = 0x06
case fileEaInformation = 0x07
case fileAccessInformation = 0x08
case fileNameInformation = 0x09
case fileRenameInformation = 0x0A
case fileLinkInformation = 0x0B
case fileNamesInformation = 0x0C
case fileDispositionInformation = 0x0D
case filePositionInformation = 0x0E
case fileFullEaInformation = 0x0F
case fileModeInformation = 0x10
case fileAlignmentInformation = 0x11
case fileAllInformation = 0x12
case fileAllocationInformation = 0x13
case fileEndOfFileInformation = 0x14
case fileAlternateNameInformation = 0x15
case fileStreamInformation = 0x16
case filePipeInformation = 0x17
case filePipeLocalInformation = 0x18
case filePipeRemoteInformation = 0x19
case fileMailslotQueryInformation = 0x1A
case fileMailslotSetInformation = 0x1B
case fileCompressionInformation = 0x1C
case fileObjectIdInformation = 0x1D
case fileCompletionInformation = 0x1E
case fileMoveClusterInformation = 0x1F
case fileQuotaInformation = 0x20
case fileReparsePointInformation = 0x21
case fileNetworkOpenInformation = 0x22
case fileAttributeTagInformation = 0x23
case fileTrackingInformation = 0x24
case fileIdBothDirectoryInformation = 0x25
case fileIdFullDirectoryInformation = 0x26
case fileValidDataLengthInformation = 0x27
case fileShortNameInformation = 0x28
case fileIoCompletionNotificationInformation = 0x29
case fileIoStatusBlockRangeInformation = 0x2A
case fileIoPriorityHintInformation = 0x2B
case fileSfioReserveInformation = 0x2C
case fileSfioVolumeInformation = 0x2D
case fileHardLinkInformation = 0x2E
case fileProcessIdsUsingFileInformation = 0x2F
case fileNormalizedNameInformation = 0x30
case fileNetworkPhysicalNameInformation = 0x31
case fileIdGlobalTxDirectoryInformation = 0x32
case fileIsRemoteDeviceInformation = 0x33
case fileUnusedInformation = 0x34
case fileNumaNodeInformation = 0x35
case fileStandardLinkInformation = 0x36
case fileRemoteProtocolInformation = 0x37
case fileRenameInformationBypassAccessCheck = 0x38
case fileLinkInformationBypassAccessCheck = 0x39
case fileVolumeNameInformation = 0x3A
case fileIdInformation = 0x3B
case fileIdExtdDirectoryInformation = 0x3C
case fileReplaceCompletionInformation = 0x3D
case fileHardLinkFullIdInformation = 0x3E
case fileIdExtdBothDirectoryInformation = 0x3F
case fileMaximumInformation = 0x40
static let queryDirectory: [FileInformationEnum] = [.fileDirectoryInformation, .fileFullDirectoryInformation, .fileIdFullDirectoryInformation, .fileBothDirectoryInformation, .fileIdBothDirectoryInformation, .fileNamesInformation]
static let queryInfoFile: [FileInformationEnum] = [.fileAccessInformation, .fileAlignmentInformation, .fileAllInformation, .fileAlternateNameInformation, .fileAttributeTagInformation, .fileBasicInformation, .fileCompressionInformation, fileEaInformation, .fileFullEaInformation, .fileInternalInformation, .fileModeInformation, .fileNetworkOpenInformation, .filePipeInformation, .filePipeLocalInformation, .filePipeRemoteInformation, .filePositionInformation, .fileStandardInformation, .fileStreamInformation]
}
enum FileSystemInformationEnum: UInt8 {
case none = 0
case fileFsAttributeInformation
case fileFsControlInformation
case fileFsDeviceInformation
case fileFsFullSizeInformation
case fileFsObjectIdInformation
case fileFsSectorSizeInformation
case fileFsSizeInformation
case fileFsVolumeInformation
}
struct 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 {
fileprivate let _readMode: UInt32
var readMode: ReadMode {
return ReadMode(rawValue: _readMode) ?? .BYTE_STREAM_MODE
}
fileprivate let _completionMode: UInt32
var completionMode: CompletionMode {
return CompletionMode(rawValue: _completionMode) ?? .QUEUE_OPERATION
}
enum ReadMode: UInt32 {
case BYTE_STREAM_MODE = 0x00000000
case MESSAGE_MODE = 0x00000001
}
enum CompletionMode: UInt32 {
case QUEUE_OPERATION = 0x00000000
case COMPLETE_OPERATION = 0x00000001
}
}
struct FilePipeLocalInformation {
fileprivate let _namedPipeType: UInt32
var namedPipeType: Type {
return Type(rawValue: _namedPipeType) ?? .BYTE_STREAM_TYPE
}
fileprivate let _namedPipeConfiguration: UInt32
var namedPipeConfiguration: Configuration {
return Configuration(rawValue: _namedPipeConfiguration) ?? .INBOUND
}
let maximumInstances: UInt32
let currentInstances: UInt32
let inboundQuota: UInt32
let readDataAvailable: UInt32
let outboundQuota: UInt32
let writeQuotaAvailable: UInt32
fileprivate let _namedPipeState: UInt32
var namedPipeState: State {
return State(rawValue: _namedPipeState) ?? .DISCONNECTED_STATE
}
fileprivate let _namedPipeEnd: UInt32
var namedPipeEnd: End {
return End(rawValue: _namedPipeEnd) ?? .CLIENT_END
}
enum `Type`: UInt32 {
case BYTE_STREAM_TYPE = 0x00000000
case MESSAGE_TYPE = 0x00000001
}
enum Configuration: UInt32 {
case INBOUND = 0x00000000
case OUTBOUND = 0x00000001
case FULL_DUPLEX = 0x00000002
}
enum State: UInt32 {
case DISCONNECTED_STATE = 0x00000001
case LISTENING_STATE = 0x00000002
case CONNECTED_STATE = 0x00000003
case CLOSING_STATE = 0x00000004
}
enum End: UInt32 {
case CLIENT_END = 0x00000000
case SERVER_END = 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 {
fileprivate let _deviceType: UInt32
var deviceType: DeviceType {
return DeviceType(rawValue: _deviceType) ?? .DISK
}
let charactristics: Charactristics
enum DeviceType: UInt32 {
case CD_ROM = 0x00000002
case DISK = 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 case 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)
}
}
}
+314
View File
@@ -0,0 +1,314 @@
//
// SMB2NegotiationTypes.swift
// ExtDownloader
//
// Created by Amir Abbas Mousavian on 4/30/95.
// Copyright © 1395 Mousavian. All rights reserved.
//
import Foundation
extension SMB2 {
// MARK: SMB2 Negotiating
struct NegotiateRequest: SMBRequestBody {
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
}
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.append(UnsafeBufferPointer(start: &dialect, count: 2))
}
let pad = ((1024 - dialectData.count) % 8)
dialectData.count += pad
header.contextOffset = UInt32(MemoryLayout<NegotiateRequest.Header>.size) + UInt32(dialectData.count)
header.contextCount = UInt16(contexts.count)
var contextData = Data()
for context in contexts {
contextData.append(Data(value: context.type.rawValue))
contextData.count += 4
contextData.append(Data(value: UInt16(context.data.count)))
}
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 signing: NegotiateSinging
fileprivate let reserved: UInt16
let capabilities: GlobalCapabilities
let guid: uuid_t
var contextOffset: UInt32
var contextCount: UInt16
fileprivate let reserved2: UInt16
var clientStartTime: SMBTime {
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(capabilities: GlobalCapabilities, clientStartTime: SMBTime? = nil, guid: uuid_t? = nil, signing: NegotiateSinging = [.ENABLED]) {
self.size = 36
self.dialectCount = 0
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)
if let clientStartTime = clientStartTime {
let time = clientStartTime.time
self.contextOffset = UInt32(time & 0xffffffff)
self.contextCount = UInt16(time & 0x0000ffff00000000 >> 32)
self.reserved2 = UInt16(time >> 48)
} else {
self.contextOffset = 0
self.contextCount = 0
self.reserved2 = 0
}
}
}
}
struct NegotiateResponse: SMBResponseBody {
let header: NegotiateResponse.Header
let buffer: Data?
let contexts: [(type: NegotiateContextType, data: Data)]
init? (data: Data) {
guard data.count >= 64 else {
return nil
}
self.header = data.scanValue()!
if Int(header.size) != 65 {
return nil
}
let bufOffset = Int(self.header.bufferOffset) - MemoryLayout<SMB2.Header>.size
let bufLen = Int(self.header.bufferLength)
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) - MemoryLayout<SMB2.Header>.size
if contextCount > 0 && contextOffset > 0 {
// TODO: NegotiateResponse context support for SMB3
self.contexts = []
} else {
self.contexts = []
}
}
struct Header {
let size: UInt16
let singing: NegotiateSinging
let dialect: UInt16
let contextCount: UInt16
let serverGuid: uuid_t
let capabilities: GlobalCapabilities
let maxTransactSize: UInt32
let maxReadSize: UInt32
let maxWriteSize: UInt32
let systemTime: SMBTime
let serverStartTime: SMBTime
let bufferOffset: UInt16
let bufferLength: UInt16
let contextOffset: UInt32
}
}
struct NegotiateSinging: OptionSet {
let rawValue: UInt16
init(rawValue: UInt16) {
self.rawValue = rawValue
}
static let ENABLED = NegotiateSinging(rawValue: 0x0001)
static let REQUIRED = NegotiateSinging(rawValue: 0x0002)
}
struct NegotiateContextType: OptionSet {
let rawValue: UInt16
init(rawValue: UInt16) {
self.rawValue = rawValue
}
static let PREAUTH_INTEGRITY_CAPABILITIES = NegotiateContextType(rawValue: 0x0001)
static let ENCRYPTION_CAPABILITIES = NegotiateContextType(rawValue: 0x0002)
}
struct GlobalCapabilities: OptionSet {
let rawValue: UInt32
init(rawValue: UInt32) {
self.rawValue = rawValue
}
static let DFS = GlobalCapabilities(rawValue: 0x00000001)
static let LEASING = GlobalCapabilities(rawValue: 0x00000002)
static let LARGE_MTU = GlobalCapabilities(rawValue: 0x00000004)
static let MULTI_CHANNEL = GlobalCapabilities(rawValue: 0x00000008)
static let PERSISTENT_HANDLES = GlobalCapabilities(rawValue: 0x00000010)
static let DIRECTORY_LEASING = GlobalCapabilities(rawValue: 0x00000020)
static let ENCRYPTION = GlobalCapabilities(rawValue: 0x00000040)
}
// MARK: SMB2 Session Setup
struct SessionSetupRequest: SMBRequestBody {
let header: SessionSetupRequest.Header
let buffer: Data?
init(header: SessionSetupRequest.Header, buffer: Data) {
self.header = header
self.buffer = buffer
}
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(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.append(buffer)
}
return result
}
struct Header {
let size: UInt16
let flags: SessionSetupRequest.Flags
let signing: SessionSetupSinging
let capabilities: GlobalCapabilities
fileprivate let channel: UInt32
var bufferOffset: UInt16
var bufferLength: UInt16
let sessionId: UInt64
init(sessionId: UInt64, flags: SessionSetupRequest.Flags = [], singing: SessionSetupSinging, capabilities: GlobalCapabilities) {
self.size = 25
self.flags = flags
self.signing = singing
self.capabilities = capabilities
self.channel = 0
self.bufferOffset = 0
self.bufferLength = 0
self.sessionId = sessionId
}
}
/// Works the client implements the SMB 3.x dialect family
struct Flags: OptionSet {
let rawValue: UInt8
init(rawValue: UInt8) {
self.rawValue = rawValue
}
static let BINDING = NegotiateSinging(rawValue: 0x01)
}
}
struct SessionSetupResponse: SMBResponseBody {
let header: SessionSetupResponse.Header
let buffer: Data?
init? (data: Data) {
guard data.count >= 64 else {
return nil
}
self.header = data.scanValue()!
if Int(header.size) != 9 {
return nil
}
let bufOffset = Int(self.header.bufferOffset) - MemoryLayout<SMB2.Header>.size
let bufLen = Int(self.header.bufferLength)
if bufOffset > 0 && bufLen > 0 && data.count >= bufOffset + bufLen {
self.buffer = data.subdata(in: bufOffset..<(bufOffset + bufLen))
} else {
self.buffer = nil
}
}
struct Header {
let size: UInt16
let flags: SessionSetupResponse.Flags
let bufferOffset: UInt16
let bufferLength: UInt16
}
struct Flags: OptionSet {
let rawValue: UInt16
init(rawValue: UInt16) {
self.rawValue = rawValue
}
static let IS_GUEST = Flags(rawValue: 0x0001)
static let IS_NULL = Flags(rawValue: 0x0002)
static let ENCRYPT_DATA = Flags(rawValue: 0x0004)
}
}
struct SessionSetupSinging: OptionSet {
let rawValue: UInt8
init(rawValue: UInt8) {
self.rawValue = rawValue
}
static let ENABLED = SessionSetupSinging(rawValue: 0x01)
static let REQUIRED = SessionSetupSinging(rawValue: 0x02)
}
// MARK: SMB2 Log off
struct LogOff: SMBRequestBody, SMBResponseBody {
let size: UInt16
let reserved: UInt16
init() {
self.size = 4
self.reserved = 0
}
}
// MARK: SMB2 Echo
struct Echo: SMBRequestBody, SMBResponseBody {
let size: UInt16
let reserved: UInt16
init() {
self.size = 4
self.reserved = 0
}
}
}
+42
View File
@@ -0,0 +1,42 @@
//
// SMB2SetInfo.swift
// ExtDownloader
//
// Created by Amir Abbas Mousavian on 4/31/95.
// Copyright © 1395 Mousavian. All rights reserved.
//
import Foundation
extension SMB2 {
// MARK: SMB2 Set Info
struct SetInfoRequest: SMBRequestBody {
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
}
}
}
+136
View File
@@ -0,0 +1,136 @@
//
// SMB2Tree.swift
// ExtDownloader
//
// Created by Amir Abbas Mousavian on 4/30/95.
// Copyright © 1395 Mousavian. All rights reserved.
//
import Foundation
extension SMB2 {
// MARK: SMB2 Tree Connect
struct TreeConnectRequest: SMBRequestBody {
let header: TreeConnectRequest.Header
let buffer: Data?
var path: String {
return ""
}
var share: String {
return ""
}
init? (header: TreeConnectRequest.Header, host: String, share: String) {
guard !host.contains("/") && !share.contains("/") else {
return nil
}
self.header = header
let path = "\\\\" + host + "\\" + share
self.buffer = path.data(using: .utf16)
}
func data() -> Data {
var header = self.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.append(buffer)
}
return result
}
struct Header {
let size: UInt16
let flags: TreeConnectRequest.Flags
var pathOffset: UInt16
var pathLength: UInt16
init(flags: TreeConnectRequest.Flags) {
self.size = 9
self.flags = flags
self.pathOffset = 0
self.pathLength = 0
}
}
struct Flags: OptionSet {
let rawValue: UInt16
init(rawValue: UInt16) {
self.rawValue = rawValue
}
static let SHAREFLAG_CLUSTER_RECONNECT = Flags(rawValue: 0x0001)
}
}
struct TreeConnectResponse: SMBResponseBody {
let size: UInt16 // = 16
fileprivate let _type: UInt8
var type: ShareType {
return ShareType(rawValue: _type) ?? .UNKNOWN
}
fileprivate let reserved: UInt8
let flags: TreeConnectResponse.ShareFlags
let capabilities: TreeConnectResponse.Capabilities
let maximalAccess: FileAccessMask
enum ShareType: UInt8 {
case UNKNOWN = 0x00
case DISK = 0x01
case PIPE = 0x02
case PRINT = 0x03
}
struct ShareFlags: OptionSet {
let rawValue: UInt32
init(rawValue: UInt32) {
self.rawValue = rawValue
}
static let DFS = ShareFlags(rawValue: 0x00000001)
static let DFS_ROOT = ShareFlags(rawValue: 0x00000002)
static let MANUAL_CACHING = ShareFlags(rawValue: 0x00000000)
static let AUTO_CACHING = ShareFlags(rawValue: 0x00000010)
static let VDO_CACHING = ShareFlags(rawValue: 0x00000020)
static let NO_CACHING = ShareFlags(rawValue: 0x00000030)
static let RESTRICT_EXCLUSIVE_OPENS = ShareFlags(rawValue: 0x00000100)
static let FORCE_SHARED_DELETE = ShareFlags(rawValue: 0x00000200)
static let ALLOW_NAMESPACE_CACHING = ShareFlags(rawValue: 0x00000400)
static let ACCESS_BASED_DIRECTORY_ENUM = ShareFlags(rawValue: 0x00000800)
static let FORCE_LEVELII_OPLOCK = ShareFlags(rawValue: 0x00001000)
static let ENABLE_HASH_V1 = ShareFlags(rawValue: 0x00002000)
static let ENABLE_HASH_V2 = ShareFlags(rawValue: 0x00004000)
static let ENCRYPT_DATA = ShareFlags(rawValue: 0x00008000)
}
struct Capabilities: OptionSet {
let rawValue: UInt32
init(rawValue: UInt32) {
self.rawValue = rawValue
}
static let DFS = Capabilities(rawValue: 0x00000008)
static let CONTINUOUS_AVAILABILITY = Capabilities(rawValue: 0x00000010)
static let SCALEOUT = Capabilities(rawValue: 0x00000020)
static let CLUSTER = Capabilities(rawValue: 0x00000040)
static let ASYMMETRIC = Capabilities(rawValue: 0x00000080)
}
}
// MARK: SMB2 Tree Disconnect
struct TreeDisconnect: SMBRequestBody, SMBResponseBody {
let size: UInt16
let reserved: UInt16
init() {
self.size = 4
self.reserved = 0
}
}
}
+139
View File
@@ -0,0 +1,139 @@
//
// SMB2Types.swift
// FileProvider
//
// Created by Amir Abbas Mousavian.
// Copyright © 2016 Mousavian. Distributed under MIT license.
//
import Foundation
protocol FileProviderSMBHeader {
var protocolID: UInt32 { get }
static var protocolConst: UInt32 { get }
}
// SMB2 Types
struct SMB2 {
struct Header: FileProviderSMBHeader { // 64 bytes
// header is always \u{fe}SMB
let protocolID: UInt32
static let protocolConst: UInt32 = 0x424d53fe
let size: UInt16
let creditCharge: UInt16
// error messages from the server to the client
let status: UInt32
enum StatusSeverity: UInt8 {
case success = 0, information, warning, error
}
var statusDetails: (severity: StatusSeverity, customer: Bool, facility: UInt16, code: UInt16) {
let severity = StatusSeverity(rawValue: UInt8(status >> 30))!
return (severity, status & 0x20000000 != 0, UInt16((status & 0x0FFF0000) >> 16), UInt16(status & 0x0000FFFF))
}
fileprivate let _command: UInt16
var command: Command {
get {
return Command(rawValue: _command) ?? .INVALID
}
}
let creditRequestResponse: UInt16
let flags: Flags
var nextCommand: UInt32
let messageId: UInt64
fileprivate let reserved: UInt32
let treeId: UInt32
var asyncId: UInt64 {
get {
return UInt64(reserved) + (UInt64(treeId) << 32)
}
}
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 = type(of: self).protocolConst
self.size = 64
self.status = status.rawValue
self._command = command.rawValue
self.creditCharge = creditCharge
self.creditRequestResponse = creditRequestResponse
self.flags = flags
self.nextCommand = nextCommand
self.messageId = messageId
self.reserved = 0
self.treeId = treeId
self.sessionId = sessionId
self.signature = signature
}
init(asyncCommand: Command, status: NTStatus = .SUCCESS, creditCharge: UInt16 = 0, creditRequestResponse: UInt16, flags: Flags = [.ASYNC_COMMAND], nextCommand: UInt32 = 0, messageId: UInt64, asyncId: UInt64, sessionId: UInt64, signature: (UInt64, UInt64) = (0, 0)) {
self.protocolID = type(of: self).protocolConst
self.size = 64
self.status = status.rawValue
self._command = asyncCommand.rawValue
self.creditCharge = creditCharge
self.creditRequestResponse = creditRequestResponse
self.flags = flags.union([Flags.ASYNC_COMMAND])
self.nextCommand = nextCommand
self.messageId = messageId
self.reserved = UInt32(asyncId & 0xffffffff)
self.treeId = UInt32(asyncId >> 32)
self.sessionId = sessionId
self.signature = signature
}
// codebeat:enable[ARITY]
}
struct Flags: OptionSet {
var rawValue: UInt32
init(rawValue: UInt32) {
self.rawValue = rawValue
}
var priorityMask: UInt8 {
get {
return UInt8((rawValue & Flags.PRIORITY_MASK.rawValue) >> 4)
}
set {
rawValue = (rawValue & 0xffffff8f) | (UInt32(newValue & 0x7) << 4)
}
}
static let SERVER_TO_REDIR = Flags(rawValue: 0x00000001)
static let ASYNC_COMMAND = Flags(rawValue: 0x00000002)
static let RELATED_OPERATIONS = Flags(rawValue: 0x00000004)
static let SIGNED = Flags(rawValue: 0x00000008)
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
}
// MARK: SMB2 Oplock Break
}
+208
View File
@@ -0,0 +1,208 @@
//
// SMBErrorType.swift
// ExtDownloader
//
// Created by Amir Abbas Mousavian on 4/30/95.
// Copyright © 1395 Mousavian. All rights reserved.
//
import Foundation
/// Error Types and Description
enum NTStatus: UInt32, Error, CustomStringConvertible {
case SUCCESS = 0x00000000
case NOT_IMPLEMENTED = 0xC0000002
case INVALID_DEVICE_REQUEST = 0xC0000010
case ILLEGAL_FUNCTION = 0xC00000AF
case NO_SUCH_FILE = 0xC000000F
case NO_SUCH_DEVICE = 0xC000000E
case OBJECT_NAME_NOT_FOUND = 0xC0000034
case OBJECT_PATH_INVALID = 0xC0000039
case OBJECT_PATH_NOT_FOUND = 0xC000003A
case OBJECT_PATH_SYNTAX_BAD = 0xC000003B
case DFS_EXIT_PATH_FOUND = 0xC000009B
case REDIRECTOR_NOT_STARTED = 0xC00000FB
case TOO_MANY_OPENED_FILES = 0xC000011F
case ACCESS_DENIED = 0xC0000022
case INVALID_LOCK_SEQUENCE = 0xC000001E
case INVALID_VIEW_SIZE = 0xC000001F
case ALREADY_COMMITTED = 0xC0000021
case PORT_CONNECTION_REFUSED = 0xC0000041
case THREAD_IS_TERMINATING = 0xC000004B
case DELETE_PENDING = 0xC0000056
case PRIVILEGE_NOT_HELD = 0xC0000061
case LOGON_FAILURE = 0xC000006D
case FILE_IS_A_DIRECTORY = 0xC00000BA
case FILE_RENAMED = 0xC00000D5
case PROCESS_IS_TERMINATING = 0xC000010A
case DIRECTORY_NOT_EMPTY = 0xC0000101
case CANNOT_DELETE = 0xC0000121
case FILE_NOT_AVAILABLE = 0xC0000467
case FILE_DELETED = 0xC0000123
case SMB_BAD_FID = 0x00060001
case INVALID_HANDLE = 0xC0000008
case OBJECT_TYPE_MISMATCH = 0xC0000024
case PORT_DISCONNECTED = 0xC0000037
case INVALID_PORT_HANDLE = 0xC0000042
case FILE_CLOSED = 0xC0000128
case HANDLE_NOT_CLOSABLE = 0xC0000235
case SECTION_TOO_BIG = 0xC0000040
case TOO_MANY_PAGING_FILES = 0xC0000097
case INSUFF_SERVER_RESOURCES = 0xC0000205
case OS2_INVALID_ACCESS = 0x000C0001
case ACCESS_DENIED_2 = 0xC00000CA
case DATA_ERROR = 0xC000009C
case NOT_SAME_DEVICE = 0xC00000D4
case NO_MORE_FILES = 0x80000006
case NO_MORE_ENTRIES = 0x8000001A
case UNSUCCESSFUL = 0xC0000001
case SHARING_VIOLATION = 0xC0000043
case FILE_LOCK_CONFLICT = 0xC0000054
case LOCK_NOT_GRANTED = 0xC0000055
case END_OF_FILE = 0xC0000011
case NOT_SUPPORTED = 0xC00000BB
case OBJECT_NAME_COLLISION = 0xC0000035
case INVALID_PARAMETER = 0xC000000D
case OS2_INVALID_LEVEL = 0x007C0001
case OS2_NEGATIVE_SEEK = 0x00830001
case RANGE_NOT_LOCKED = 0xC000007E
case OS2_NO_MORE_SIDS = 0x00710001
case OS2_CANCEL_VIOLATION = 0x00AD0001
case OS2_ATOMIC_LOCKS_NOT_SUPPORTED = 0x00AE0001
case INVALID_INFO_CLASS = 0xC0000003
case INVALID_PIPE_STATE = 0xC00000AD
case INVALID_READ_MODE = 0xC00000B4
case OS2_CANNOT_COPY = 0x010A0001
case STOPPED_ON_SYMLINK = 0x8000002D
case INSTANCE_NOT_AVAILABLE = 0xC00000AB
case PIPE_NOT_AVAILABLE = 0xC00000AC
case PIPE_BUSY = 0xC00000AE
case PIPE_CLOSING = 0xC00000B1
case PIPE_EMPTY = 0xC00000D9
case PIPE_DISCONNECTED = 0xC00000B0
case BUFFER_OVERFLOW = 0x80000005
case MORE_PROCESSING_REQUIRED = 0xC0000016
case EA_TOO_LARGE = 0xC0000050
case OS2_EAS_DIDNT_FIT = 0x01130001
case EAS_NOT_SUPPORTED = 0xC000004F
case EA_LIST_INCONSISTENT = 0x80000014
case OS2_EA_ACCESS_DENIED = 0x03E20001
case NOTIFY_ENUM_DIR = 0x0000010C
case INVALID_SMB = 0x00010002
case WRONG_PASSWORD = 0xC000006A
case PATH_NOT_COVERED = 0xC0000257
case NETWORK_NAME_DELETED = 0xC00000C9
case SMB_BAD_TID = 0x00050002
case BAD_NETWORK_NAME = 0xC00000CC
case BAD_DEVICE_TYPE = 0xC00000CB
case SMB_BAD_COMMAND = 0x00160002
case PRINT_QUEUE_FULL = 0xC00000C6
case NO_SPOOL_SPACE = 0xC00000C7
case PRINT_CANCELLED = 0xC00000C8
case UNEXPECTED_NETWORK_ERROR = 0xC00000C4
case IO_TIMEOUT = 0xC00000B5
case REQUEST_NOT_ACCEPTED = 0xC00000D0
case TOO_MANY_SESSIONS = 0xC00000CE
case SMB_BAD_UID = 0x005B0002
case SMB_USE_MPX = 0x00FA0002
case SMB_USE_STANDARD = 0x00FB0002
case SMB_CONTINUE_MPX = 0x00FC0002
case ACCOUNT_DISABLED = 0xC0000072
case ACCOUNT_EXPIRED = 0xC0000193
case INVALID_WORKSTATION = 0xC0000070
case INVALID_LOGON_HOURS = 0xC000006F
case PASSWORD_EXPIRED = 0xC0000071
case PASSWORD_MUST_CHANGE = 0xC0000224
case SMB_NO_SUPPORT = 0xFFFF0002
case MEDIA_WRITE_PROTECTED = 0xC00000A2
case NO_MEDIA_IN_DEVICE = 0xC0000013
case INVALID_DEVICE_STATE = 0xC0000184
case DATA_ERROR_2 = 0xC000003E
case CRC_ERROR = 0xC000003F
case DISK_CORRUPT_ERROR = 0xC0000032
case NONEXISTENT_SECTOR = 0xC0000015
case DEVICE_PAPER_EMPTY = 0x8000000E
case WRONG_VOLUME = 0xC0000012
case DISK_FULL = 0xC000007F
case BUFFER_TOO_SMALL = 0xC0000023
case BAD_IMPERSONATION_LEVEL = 0xC00000A5
case USER_SESSION_DELETED = 0xC0000203
case NETWORK_SESSION_EXPIRED = 0xC000035C
case SMB_TOO_MANY_UIDS = 0xC000205A
public var description: String {
switch self {
case .NOT_IMPLEMENTED, .INVALID_DEVICE_REQUEST, .ILLEGAL_FUNCTION:
return "Invalid Function."
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:
return "A component in the path prefix is not a directory."
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:
return "Access denied."
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:
return "Insufficient server memory to perform the requested operation."
case .OS2_INVALID_ACCESS:
return "Invalid open mode."
case .DATA_ERROR:
return "Bad data. (May be generated by IOCTL calls on the server.)"
case .DIRECTORY_NOT_EMPTY:
return "Remove of directory failed because it was not empty."
case .NOT_SAME_DEVICE:
return "A file system operation (such as a rename) across two devices was attempted."
case .NO_MORE_FILES:
return "No (more) files found following a file search command."
case .UNSUCCESSFUL:
return "General error."
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:
return "A lock request specified an invalid locking mode, or conflicted with an existing file lock."
case .END_OF_FILE:
return "Attempted to read beyond the end of the file."
case .NOT_SUPPORTED:
return "This command is not supported by the server."
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:
return "A parameter supplied with the message is invalid."
case .OS2_INVALID_LEVEL:
return "Invalid information level."
case .OS2_NEGATIVE_SEEK:
return "An attempt was made to seek to a negative absolute offset within a file."
case .RANGE_NOT_LOCKED:
return "The byte range specified in an unlock request was not locked."
case .OS2_NO_MORE_SIDS:
return "Maximum number of searches has been exhausted."
case .OS2_CANCEL_VIOLATION:
return "No lock request was outstanding for the supplied cancel region."
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:
return "Invalid named pipe."
case .OS2_CANNOT_COPY:
return "The copy functions cannot be used."
case .INSTANCE_NOT_AVAILABLE, .PIPE_NOT_AVAILABLE, .PIPE_BUSY:
return "All instances of the designated named pipe are busy."
case .PIPE_CLOSING, .PIPE_EMPTY:
return "The designated named pipe is in the process of being closed."
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:
return "There is more data available to read on the designated named pipe."
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:
return "The server file system does not support Extended Attributes."
case .OS2_EA_ACCESS_DENIED:
return "Access to the extended attribute was denied."
default:
return ""
}
}
}
+579
View File
@@ -0,0 +1,579 @@
//
// WebDAVFileProvider.swift
// FileProvider
//
// Created by Amir Abbas Mousavian.
// Copyright © 2016 Mousavian. Distributed under MIT license.
//
import Foundation
import CoreGraphics
/**
Allows accessing to WebDAV server files. This provider doesn't cache or save files internally, however you can
set `useCache` and `cache` properties to use Foundation `NSURLCache` system.
WebDAV system supported by many cloud services including [Box.com](https://www.box.com/home)
and [Yandex disk](https://disk.yandex.com) and [ownCloud](https://owncloud.org).
- Important: Because this class uses `URLSession`, it's necessary to disable App Transport Security
in case of using this class with unencrypted HTTP connection.
[Read this to know how](http://iosdevtips.co/post/121756573323/ios-9-xcode-7-http-connect-server-error).
*/
open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing {
override open class var type: String { return "WebDAV" }
/// An enum which defines HTTP Authentication method, usually you should it default `.digest`.
/// If the server uses OAuth authentication, credential must be set with token as `password`, like Dropbox.
public var credentialType: URLRequest.AuthenticationType = .digest
/**
Initializes WebDAV provider.
- Parameters:
- baseURL: Location of WebDAV 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? = nil) {
if !["http", "https"].contains(baseURL.uw_scheme.lowercased()) {
return nil
}
let refinedBaseURL = (baseURL.absoluteString.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent(""))
super.init(baseURL: refinedBaseURL.absoluteURL, credential: credential, cache: cache)
}
public required convenience init?(coder aDecoder: NSCoder) {
guard let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL else {
return nil
}
self.init(baseURL: baseURL,
credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential)
self.useCache = aDecoder.decodeBool(forKey: "useCache")
self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache")
}
override open func copy(with zone: NSZone? = nil) -> Any {
let copy = WebDAVFileProvider(baseURL: self.baseURL!, credential: self.credential, cache: self.cache)!
copy.delegate = self.delegate
copy.fileOperationDelegate = self.fileOperationDelegate
copy.useCache = self.useCache
copy.validatingCache = self.validatingCache
return copy
}
override open func contentsOfDirectory(path: String, completionHandler: @escaping (([FileObject], Error?) -> Void)) {
let query = NSPredicate(format: "TRUEPREDICATE")
_ = searchFiles(path: path, recursive: false, query: query, including: [], foundItemHandler: nil, completionHandler: completionHandler)
}
/**
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 including: An array which determines which file properties should be considered to fetch.
- Parameter completionHandler: a closure with result of directory entries or error.
- Parameter contents: An array of `FileObject` identifying the the directory entries.
- Parameter error: Error returned by system.
*/
open func contentsOfDirectory(path: String, including: [URLResourceKey], completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) {
let query = NSPredicate(format: "TRUEPREDICATE")
_ = searchFiles(path: path, recursive: false, query: query, including: including, foundItemHandler: nil, completionHandler: completionHandler)
}
override open func attributesOfItem(path: String, completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) {
self.attributesOfItem(path: path, including: [], 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 including: An array which determines which file properties should be considered to fetch.
- Parameter completionHandler: a closure with result of directory entries or error.
- Parameter attributes: A `FileObject` containing the attributes of the item.
- Parameter error: Error returned by system.
*/
open func attributesOfItem(path: String, including: [URLResourceKey], completionHandler: @escaping (_ attributes: FileObject?, _ error: Error?) -> Void) {
let url = self.url(of: path)
var request = URLRequest(url: url)
request.httpMethod = "PROPFIND"
request.setValue("0", forHTTPHeaderField: "Depth")
request.set(httpAuthentication: credential, with: credentialType)
request.set(httpContentType: .xml, charset: .utf8)
request.httpBody = WebDavFileObject.xmlProp(including)
runDataTask(with: request, completionHandler: { (data, response, error) in
var responseError: FileProviderHTTPError?
if let code = (response as? HTTPURLResponse)?.statusCode, code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = self.serverError(with: rCode, path: path, data: data)
}
if let data = data {
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
if let attr = xresponse.first {
completionHandler(WebDavFileObject(attr), responseError ?? error)
return
}
}
completionHandler(nil, responseError ?? error)
})
}
override open func storageProperties(completionHandler: @escaping (_ volumeInfo: VolumeObject?) -> Void) {
// Not all WebDAV clients implements RFC2518 which allows geting storage quota.
// In this case you won't get error. totalSize is NSURLSessionTransferSizeUnknown
// and used space is zero.
guard let baseURL = baseURL else {
return
}
var request = URLRequest(url: baseURL)
request.httpMethod = "PROPFIND"
request.setValue("0", forHTTPHeaderField: "Depth")
request.set(httpAuthentication: credential, with: credentialType)
request.set(httpContentType: .xml, charset: .utf8)
request.httpBody = WebDavFileObject.xmlProp([.volumeTotalCapacityKey, .volumeAvailableCapacityKey, .creationDateKey])
runDataTask(with: request, completionHandler: { (data, response, error) in
guard let data = data, let attr = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL).first else {
completionHandler(nil)
return
}
let volume = VolumeObject(allValues: [:])
volume.creationDate = attr.prop["creationdate"].flatMap { Date(rfcString: $0) }
volume.availableCapacity = attr.prop["quota-available-bytes"].flatMap({ Int64($0) }) ?? 0
if let usage = attr.prop["quota-used-bytes"].flatMap({ Int64($0) }) {
volume.totalCapacity = volume.availableCapacity + usage
}
completionHandler(volume)
})
}
open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ([FileObject], Error?) -> Void) -> Progress? {
return searchFiles(path: path, recursive: recursive, query: query, including: [], foundItemHandler: foundItemHandler, completionHandler: completionHandler)
}
/**
Search files inside directory using query asynchronously.
- Note: Query string is limited to file name, to search based on other file attributes, use NSPredicate version.
- Parameters:
- path: location of directory to start search
- recursive: Searching subdirectories of path
- query: Simple string that file name begins with to be search, case-insensitive.
- including: An array which determines which file properties should be considered to fetch.
- foundItemHandler: Closure which is called when a file is found
- completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured.
- files: all files meat the `query` criteria.
- error: `Error` returned by server if occured.
*/
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, including: [URLResourceKey], foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? {
let url = self.url(of: path)
var request = URLRequest(url: url)
request.httpMethod = "PROPFIND"
// Depth infinity is disabled on some servers. Implement workaround?!
request.setValue(recursive ? "infinity" : "1", forHTTPHeaderField: "Depth")
request.set(httpAuthentication: credential, with: credentialType)
request.set(httpContentType: .xml, charset: .utf8)
request.httpBody = WebDavFileObject.xmlProp([])
let progress = Progress(totalUnitCount: -1)
progress.setUserInfoObject(url, forKey: .fileURLKey)
let queryIsTruePredicate = query.predicateFormat == "TRUEPREDICATE"
let task = session.dataTask(with: request) { (data, response, error) in
// FIXME: paginating results
var responseError: FileProviderHTTPError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = self.serverError(with: rCode, path: path, data: data)
}
guard let data = data else {
completionHandler([], responseError ?? error)
return
}
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
var fileObjects = [WebDavFileObject]()
for attr in xresponse where attr.href.path != url.path {
let fileObject = WebDavFileObject(attr)
if !queryIsTruePredicate && !query.evaluate(with: fileObject.mapPredicate()) {
continue
}
fileObjects.append(fileObject)
progress.completedUnitCount = Int64(fileObjects.count)
foundItemHandler?(fileObject)
}
completionHandler(fileObjects, responseError ?? error)
}
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return progress
}
override open func isReachable(completionHandler: @escaping (Bool) -> Void) {
var request = URLRequest(url: baseURL!)
request.httpMethod = "PROPFIND"
request.setValue("0", forHTTPHeaderField: "Depth")
request.set(httpAuthentication: credential, with: credentialType)
request.set(httpContentType: .xml, charset: .utf8)
request.httpBody = WebDavFileObject.xmlProp([.volumeTotalCapacityKey, .volumeAvailableCapacityKey])
runDataTask(with: request, completionHandler: { (data, response, error) in
let status = (response as? HTTPURLResponse)?.statusCode ?? 400
completionHandler(status < 300)
})
}
open func publicLink(to path: String, completionHandler: @escaping ((URL?, FileObject?, Date?, Error?) -> Void)) {
guard self.baseURL?.host?.contains("dav.yandex.") ?? false else {
dispatch_queue.async {
completionHandler(nil, nil, nil, self.urlError(path, code: .resourceUnavailable))
}
return
}
let url = self.url(of: path)
var request = URLRequest(url: url)
request.httpMethod = "PROPPATCH"
request.set(httpAuthentication: credential, with: credentialType)
request.set(httpContentType: .xml, charset: .utf8)
let body = "<propertyupdate xmlns=\"DAV:\">\n<set><prop>\n<public_url xmlns=\"urn:yandex:disk:meta\">true</public_url>\n</prop></set>\n</propertyupdate>"
request.httpBody = body.data(using: .utf8)
runDataTask(with: request, completionHandler: { (data, response, error) in
var responseError: FileProviderHTTPError?
if let code = (response as? HTTPURLResponse)?.statusCode, code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = self.serverError(with: rCode, path: path, data: data)
}
if let data = data {
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
if let urlStr = xresponse.first?.prop["public_url"], let url = URL(string: urlStr) {
completionHandler(url, nil, nil, nil)
return
}
}
completionHandler(nil, nil, nil, responseError ?? error)
})
}
override func request(for operation: FileOperationType, overwrite: Bool = true, attributes: [URLResourceKey: Any] = [:]) -> URLRequest {
let method: String
let url: URL
let sourceURL = self.url(of: operation.source)
switch operation {
case .fetch:
method = "GET"
url = sourceURL
case .create:
if sourceURL.absoluteString.hasSuffix("/") {
method = "MKCOL"
url = sourceURL
} else {
fallthrough
}
case .modify:
method = "PUT"
url = sourceURL
break
case .copy(let source, let dest):
if source.hasPrefix("file://") {
method = "PUT"
url = self.url(of: dest)
} else if dest.hasPrefix("file://") {
method = "GET"
url = sourceURL
} else {
method = "COPY"
url = sourceURL
}
case .move:
method = "MOVE"
url = sourceURL
case .remove:
method = "DELETE"
url = sourceURL
default:
fatalError("Unimplemented operation \(operation.description) in \(#file)")
}
var request = URLRequest(url: url)
request.httpMethod = method
request.set(httpAuthentication: credential, with: credentialType)
request.setValue(overwrite ? "T" : "F", forHTTPHeaderField: "Overwrite")
if let dest = operation.destination, !dest.hasPrefix("file://") {
request.setValue(self.url(of:dest).absoluteString, forHTTPHeaderField: "Destination")
}
return request
}
override func serverError(with code: FileProviderHTTPErrorCode, path: String?, data: Data?) -> FileProviderHTTPError {
return FileProviderWebDavError(code: code, path: path ?? "", errorDescription: data.flatMap({ String(data: $0, encoding: .utf8) }), url: self.url(of: path ?? ""))
}
override func multiStatusHandler(source: String, data: Data, completionHandler: SimpleCompletionHandler) {
let xresponses = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
for xresponse in xresponses where (xresponse.status ?? 0) >= 300 {
let code = xresponse.status.flatMap { FileProviderHTTPErrorCode(rawValue: $0) } ?? .internalServerError
let error = self.serverError(with: code, path: source, data: data)
completionHandler?(error)
}
}
/*
fileprivate func registerNotifcation(path: String, eventHandler: (() -> Void)) {
/* There is no unified api for monitoring WebDAV server content change/update
* Microsoft Exchange uses SUBSCRIBE method, Apple uses push notification system.
* while both is unavailable in a mobile platform.
* A messy approach is listing a directory with an interval period and compare
* with previous results
*/
NotImplemented()
}
fileprivate func unregisterNotifcation(path: String) {
NotImplemented()
}*/
// TODO: implements methods for lock mechanism
}
extension WebDAVFileProvider: ExtendedFileProvider {
open func thumbnailOfFileSupported(path: String) -> Bool {
guard self.baseURL?.host?.contains("dav.yandex.") ?? false else {
return false
}
let supportedExt: [String] = ["jpg", "jpeg", "png", "gif"]
return supportedExt.contains((path as NSString).pathExtension)
}
open func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((ImageClass?, Error?) -> Void)) {
guard self.baseURL?.host?.contains("dav.yandex.") ?? false else {
dispatch_queue.async {
completionHandler(nil, self.urlError(path, code: .resourceUnavailable))
}
return
}
let dimension = dimension ?? CGSize(width: 64, height: 64)
let url = URL(string: self.url(of: path).absoluteString + "?preview&size=\(dimension.width)x\(dimension.height)")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.set(httpAuthentication: credential, with: credentialType)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var responseError: FileProviderHTTPError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = self.serverError(with: rCode, path: url.relativePath, data: data)
completionHandler(nil, responseError ?? error)
return
}
completionHandler(data.flatMap({ ImageClass(data: $0) }), nil)
})
task.resume()
}
open func propertiesOfFileSupported(path: String) -> Bool {
return false
}
open func propertiesOfFile(path: String, completionHandler: @escaping (([String : Any], [String], Error?) -> Void)) {
dispatch_queue.async {
completionHandler([:], [], self.urlError(path, code: .resourceUnavailable))
}
}
}
// MARK: WEBDAV XML response implementation
struct DavResponse {
let href: URL
let hrefString: String
let status: Int?
let prop: [String: String]
init? (_ node: AEXMLElement, baseURL: URL?) {
func standardizePath(_ str: String) -> String {
#if swift(>=4.0)
let trimmedStr = str.hasPrefix("/") ? String(str[str.index(after: str.startIndex)...]) : str
#else
let trimmedStr = str.hasPrefix("/") ? str.substring(from: str.index(after: str.startIndex)) : str
#endif
return trimmedStr.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? str
}
// find node names with namespace
var hreftag = "href"
var statustag = "status"
var propstattag = "propstat"
for node in node.children {
if node.name.lowercased().hasSuffix("href") {
hreftag = node.name
}
if node.name.lowercased().hasSuffix("status") {
statustag = node.name
}
if node.name.lowercased().hasSuffix("propstat") {
propstattag = node.name
}
}
guard let hrefString = node[hreftag].value else { return nil }
// trying to figure out relative path out of href
let hrefAbsolute = URL(string: hrefString, relativeTo: baseURL)?.absoluteURL
let relativePath: String
if hrefAbsolute?.host?.replacingOccurrences(of: "www.", with: "", options: .anchored) == baseURL?.host?.replacingOccurrences(of: "www.", with: "", options: .anchored) {
relativePath = hrefAbsolute?.path.replacingOccurrences(of: baseURL?.absoluteURL.path ?? "", with: "", options: .anchored, range: nil) ?? hrefString
} else {
relativePath = hrefAbsolute?.absoluteString.replacingOccurrences(of: baseURL?.absoluteString ?? "", with: "", options: .anchored, range: nil) ?? hrefString
}
let hrefURL = URL(string: standardizePath(relativePath), relativeTo: baseURL) ?? baseURL
guard let href = hrefURL?.standardized else { return nil }
// reading status and properties
var status: Int?
let statusDesc = (node[statustag].string).components(separatedBy: " ")
if statusDesc.count > 2 {
status = Int(statusDesc[1])
}
var propDic = [String: String]()
let propStatNode = node[propstattag]
for node in propStatNode.children where node.name.lowercased().hasSuffix("status"){
statustag = node.name
break
}
let statusDesc2 = (propStatNode[statustag].string).components(separatedBy: " ")
if statusDesc2.count > 2 {
status = Int(statusDesc2[1])
}
var proptag = "prop"
for tnode in propStatNode.children where tnode.name.lowercased().hasSuffix("prop") {
proptag = tnode.name
break
}
for propItemNode in propStatNode[proptag].children {
propDic[propItemNode.name.components(separatedBy: ":").last!.lowercased()] = propItemNode.value
if propItemNode.name.hasSuffix("resourcetype") && propItemNode.xml.contains("collection") {
propDic["getcontenttype"] = "httpd/unix-directory"
}
}
self.href = href
self.hrefString = hrefString
self.status = status
self.prop = propDic
}
static func parse(xmlResponse: Data, baseURL: URL?) -> [DavResponse] {
guard let xml = try? AEXMLDocument(xml: xmlResponse) else { return [] }
var result = [DavResponse]()
var rootnode = xml.root
var responsetag = "response"
for node in rootnode.all ?? [] where node.name.lowercased().hasSuffix("multistatus") {
rootnode = node
}
for node in rootnode.children where node.name.lowercased().hasSuffix("response") {
responsetag = node.name
break
}
for responseNode in rootnode[responsetag].all ?? [] {
if let davResponse = DavResponse(responseNode, baseURL: baseURL) {
result.append(davResponse)
}
}
return result
}
}
/// Containts path, url and attributes of a WebDAV file or resource.
public final class WebDavFileObject: FileObject {
internal init(_ davResponse: DavResponse) {
let href = davResponse.href
let name = davResponse.prop["displayname"] ?? davResponse.href.lastPathComponent
let relativePath = href.relativePath
let path = relativePath.hasPrefix("/") ? relativePath : ("/" + relativePath)
super.init(url: href, name: name, path: path)
self.size = Int64(davResponse.prop["getcontentlength"] ?? "-1") ?? NSURLSessionTransferSizeUnknown
self.creationDate = davResponse.prop["creationdate"].flatMap { Date(rfcString: $0) }
self.modifiedDate = davResponse.prop["getlastmodified"].flatMap { Date(rfcString: $0) }
self.contentType = davResponse.prop["getcontenttype"] ?? "application/octet-stream"
self.isHidden = (Int(davResponse.prop["ishidden"] ?? "0") ?? 0) > 0
self.type = self.contentType == "httpd/unix-directory" ? .directory : .regular
self.entryTag = davResponse.prop["getetag"]
}
/// MIME type of the file.
open internal(set) var contentType: String {
get {
return allValues[.mimeTypeKey] as? String ?? "application/octet-stream"
}
set {
allValues[.mimeTypeKey] = newValue
}
}
/// 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
}
}
internal class func resourceKeyToDAVProp(_ key: URLResourceKey) -> String? {
switch key {
case URLResourceKey.fileSizeKey:
return "getcontentlength"
case URLResourceKey.creationDateKey:
return "creationdate"
case URLResourceKey.contentModificationDateKey:
return "getlastmodified"
case URLResourceKey.fileResourceTypeKey, URLResourceKey.mimeTypeKey:
return "getcontenttype"
case URLResourceKey.isHiddenKey:
return "ishidden"
case URLResourceKey.entryTagKey:
return "getetag"
case URLResourceKey.volumeTotalCapacityKey:
// WebDAV doesn't have total capacity, but it's can be calculated via used capacity
return "quota-used-bytes"
case URLResourceKey.volumeAvailableCapacityKey:
return "quota-available-bytes"
default:
return nil
}
}
internal class func propString(_ keys: [URLResourceKey]) -> String {
var propKeys = ""
for item in keys {
if let prop = WebDavFileObject.resourceKeyToDAVProp(item) {
propKeys += "<D:prop><D:\(prop)/></D:prop>"
}
}
if propKeys.isEmpty {
propKeys = "<D:allprop/>"
}
return propKeys
}
internal class func xmlProp(_ keys: [URLResourceKey]) -> Data {
return "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n\(WebDavFileObject.propString(keys))\n</D:propfind>".data(using: .utf8)!
}
}
/// Error returned by WebDAV server when trying to access or do operations on a file or folder.
public struct FileProviderWebDavError: FileProviderHTTPError {
public let code: FileProviderHTTPErrorCode
public let path: String
public let errorDescription: String?
/// URL of resource caused error.
public let url: URL
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB