Compare commits

..

74 Commits

Author SHA1 Message Date
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
28 changed files with 4338 additions and 1562 deletions
+5 -4
View File
@@ -12,8 +12,8 @@ env:
- 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="YES" CARTHAGEDEPLOY="NO"
- DESTINATION="OS=9.0,name=iPhone 6" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="NO" CARTHAGEDEPLOY="YES"
- 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"
@@ -56,7 +56,7 @@ script:
after_success:
# Run `pod trunk push` if specified
- if [ $POD == "YES" ] && [ -n "$TRAVIS_TAG" ]; then
pod trunk push;
pod trunk push --allow-warnings;
fi
# - bash <(curl -s https://codecov.io/bash)
@@ -75,6 +75,7 @@ deploy:
file: $FRAMEWORK_NAME.zip
skip_cleanup: true
on:
repo: amosavian/$PROJECTNAME
# repo: amosavian/$PROJECTNAME
repo: amosavian/FileProvider
tags: true
condition: "$CARTHAGEDEPLOY = YES"
@@ -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.14.5"
s.summary = "FileManager replacement for Local and Remote (WebDAV/Dropbox/OneDrive/SMB2) files on iOS and macOS."
s.name = "FilesProvider"
s.version = "0.19.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?
@@ -35,6 +35,9 @@
792572411DF23BDA006A1526 /* LocalHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792572401DF23BDA006A1526 /* LocalHelper.swift */; };
792572421DF23BDA006A1526 /* LocalHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792572401DF23BDA006A1526 /* LocalHelper.swift */; };
792572431DF23BDA006A1526 /* LocalHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792572401DF23BDA006A1526 /* LocalHelper.swift */; };
7936BC121E880F5700A6C81C /* FTPFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7936BC111E880F5700A6C81C /* FTPFileProvider.swift */; };
7936BC131E880F5700A6C81C /* FTPFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7936BC111E880F5700A6C81C /* FTPFileProvider.swift */; };
7936BC141E880F5700A6C81C /* FTPFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7936BC111E880F5700A6C81C /* FTPFileProvider.swift */; };
79480FF61E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79480FF51E3ABDD0007E7275 /* CloudFileProvider.swift */; };
79480FF71E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79480FF51E3ABDD0007E7275 /* CloudFileProvider.swift */; };
79480FF81E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79480FF51E3ABDD0007E7275 /* CloudFileProvider.swift */; };
@@ -50,6 +53,7 @@
796807551E7BF17E00BBB87B /* FileProviderExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796807541E7BF17E00BBB87B /* FileProviderExtensions.swift */; };
796807561E7BF17E00BBB87B /* FileProviderExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796807541E7BF17E00BBB87B /* FileProviderExtensions.swift */; };
796807571E7BF17E00BBB87B /* FileProviderExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796807541E7BF17E00BBB87B /* FileProviderExtensions.swift */; };
798654331E8874BC002FA550 /* FTPHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 798654321E8874BC002FA550 /* FTPHelper.swift */; };
799396AA1D48C02300086753 /* DropboxFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396931D48C02300086753 /* DropboxFileProvider.swift */; };
799396AB1D48C02300086753 /* DropboxFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396931D48C02300086753 /* DropboxFileProvider.swift */; };
799396AC1D48C02300086753 /* DropboxFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396931D48C02300086753 /* DropboxFileProvider.swift */; };
@@ -112,12 +116,14 @@
79BD63BE1E2CC3C20035128C /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63BD1E2CC3C20035128C /* ImageIO.framework */; };
79BD63C01E2CC3CD0035128C /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63BF1E2CC3CD0035128C /* CoreGraphics.framework */; };
79BD63C21E2CC3D30035128C /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63C11E2CC3D30035128C /* AVFoundation.framework */; };
79BD63C51E2D17880035128C /* OneDriveFileProvide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C31E2D17880035128C /* OneDriveFileProvide.swift */; };
79BD63C61E2D17880035128C /* OneDriveFileProvide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C31E2D17880035128C /* OneDriveFileProvide.swift */; };
79BD63C71E2D17880035128C /* OneDriveFileProvide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C31E2D17880035128C /* OneDriveFileProvide.swift */; };
79BD63C51E2D17880035128C /* OneDriveFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C31E2D17880035128C /* OneDriveFileProvider.swift */; };
79BD63C61E2D17880035128C /* OneDriveFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C31E2D17880035128C /* OneDriveFileProvider.swift */; };
79BD63C71E2D17880035128C /* OneDriveFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C31E2D17880035128C /* OneDriveFileProvider.swift */; };
79BD63C81E2D17880035128C /* OneDriveHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C41E2D17880035128C /* OneDriveHelper.swift */; };
79BD63C91E2D17880035128C /* OneDriveHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C41E2D17880035128C /* OneDriveHelper.swift */; };
79BD63CA1E2D17880035128C /* OneDriveHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C41E2D17880035128C /* OneDriveHelper.swift */; };
79F4678B1E8B80F200C91A85 /* FTPHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 798654321E8874BC002FA550 /* FTPHelper.swift */; };
79F4678C1E8B80F200C91A85 /* FTPHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 798654321E8874BC002FA550 /* FTPHelper.swift */; };
79F5745B1DFDB10B00179ABF /* FileObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79F5745A1DFDB10A00179ABF /* FileObject.swift */; };
79F5745C1DFDB10B00179ABF /* FileObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79F5745A1DFDB10A00179ABF /* FileObject.swift */; };
79F5745D1DFDB10B00179ABF /* FileObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79F5745A1DFDB10A00179ABF /* FileObject.swift */; };
@@ -133,14 +139,16 @@
7924B1921D89DAE000589DB7 /* Parser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = "<group>"; };
7924B1A81D89F79200589DB7 /* FPSStreamTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FPSStreamTask.swift; sourceTree = "<group>"; };
792572401DF23BDA006A1526 /* LocalHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalHelper.swift; sourceTree = "<group>"; };
7936BC111E880F5700A6C81C /* FTPFileProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTPFileProvider.swift; sourceTree = "<group>"; };
79480FF51E3ABDD0007E7275 /* CloudFileProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudFileProvider.swift; sourceTree = "<group>"; };
794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DropboxHelper.swift; sourceTree = "<group>"; };
794C22091D5893F800EC49B8 /* SMB2Notification.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMB2Notification.swift; sourceTree = "<group>"; };
794C220D1D591A4B00EC49B8 /* SMB2QueryTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMB2QueryTypes.swift; sourceTree = "<group>"; };
796807541E7BF17E00BBB87B /* FileProviderExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileProviderExtensions.swift; sourceTree = "<group>"; };
799396671D48B7F600086753 /* FileProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FileProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; };
799396751D48B80D00086753 /* FileProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FileProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; };
799396821D48B82700086753 /* FileProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FileProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; };
798654321E8874BC002FA550 /* FTPHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTPHelper.swift; sourceTree = "<group>"; };
799396671D48B7F600086753 /* FilesProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FilesProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; };
799396751D48B80D00086753 /* FilesProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FilesProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; };
799396821D48B82700086753 /* FilesProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FilesProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; };
7993968B1D48B8C700086753 /* Info-iOS.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Info-iOS.plist"; sourceTree = "<group>"; };
7993968C1D48B8C700086753 /* Info-MacOS.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Info-MacOS.plist"; sourceTree = "<group>"; };
7993968D1D48B8C700086753 /* Info-tvOS.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Info-tvOS.plist"; sourceTree = "<group>"; };
@@ -174,7 +182,7 @@
79BD63BD1E2CC3C20035128C /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/System/Library/Frameworks/ImageIO.framework; sourceTree = DEVELOPER_DIR; };
79BD63BF1E2CC3CD0035128C /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/System/Library/Frameworks/CoreGraphics.framework; sourceTree = DEVELOPER_DIR; };
79BD63C11E2CC3D30035128C /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/System/Library/Frameworks/AVFoundation.framework; sourceTree = DEVELOPER_DIR; };
79BD63C31E2D17880035128C /* OneDriveFileProvide.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OneDriveFileProvide.swift; sourceTree = "<group>"; };
79BD63C31E2D17880035128C /* OneDriveFileProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OneDriveFileProvider.swift; sourceTree = "<group>"; };
79BD63C41E2D17880035128C /* OneDriveHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OneDriveHelper.swift; sourceTree = "<group>"; };
79F5745A1DFDB10A00179ABF /* FileObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileObject.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -261,9 +269,9 @@
799396681D48B7F600086753 /* Products */ = {
isa = PBXGroup;
children = (
799396671D48B7F600086753 /* FileProvider.framework */,
799396751D48B80D00086753 /* FileProvider.framework */,
799396821D48B82700086753 /* FileProvider.framework */,
799396671D48B7F600086753 /* FilesProvider.framework */,
799396751D48B80D00086753 /* FilesProvider.framework */,
799396821D48B82700086753 /* FilesProvider.framework */,
);
name = Products;
sourceTree = "<group>";
@@ -294,7 +302,9 @@
7902C0851D61B56D00564440 /* RemoteSession.swift */,
799396931D48C02300086753 /* DropboxFileProvider.swift */,
794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */,
79BD63C31E2D17880035128C /* OneDriveFileProvide.swift */,
7936BC111E880F5700A6C81C /* FTPFileProvider.swift */,
798654321E8874BC002FA550 /* FTPHelper.swift */,
79BD63C31E2D17880035128C /* OneDriveFileProvider.swift */,
79BD63C41E2D17880035128C /* OneDriveHelper.swift */,
7924B1A81D89F79200589DB7 /* FPSStreamTask.swift */,
799396971D48C02300086753 /* SMBClient.swift */,
@@ -358,9 +368,9 @@
/* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget section */
799396661D48B7F600086753 /* FileProvider iOS */ = {
799396661D48B7F600086753 /* FilesProvider iOS */ = {
isa = PBXNativeTarget;
buildConfigurationList = 7993966F1D48B7F600086753 /* Build configuration list for PBXNativeTarget "FileProvider iOS" */;
buildConfigurationList = 7993966F1D48B7F600086753 /* Build configuration list for PBXNativeTarget "FilesProvider iOS" */;
buildPhases = (
799396621D48B7F600086753 /* Sources */,
799396631D48B7F600086753 /* Frameworks */,
@@ -371,14 +381,14 @@
);
dependencies = (
);
name = "FileProvider iOS";
name = "FilesProvider iOS";
productName = "FileProvider iOS";
productReference = 799396671D48B7F600086753 /* FileProvider.framework */;
productReference = 799396671D48B7F600086753 /* FilesProvider.framework */;
productType = "com.apple.product-type.framework";
};
799396741D48B80D00086753 /* FileProvider OSX */ = {
799396741D48B80D00086753 /* FilesProvider OSX */ = {
isa = PBXNativeTarget;
buildConfigurationList = 7993967A1D48B80D00086753 /* Build configuration list for PBXNativeTarget "FileProvider OSX" */;
buildConfigurationList = 7993967A1D48B80D00086753 /* Build configuration list for PBXNativeTarget "FilesProvider OSX" */;
buildPhases = (
799396701D48B80D00086753 /* Sources */,
799396711D48B80D00086753 /* Frameworks */,
@@ -389,14 +399,14 @@
);
dependencies = (
);
name = "FileProvider OSX";
name = "FilesProvider OSX";
productName = "FileProvider OSX";
productReference = 799396751D48B80D00086753 /* FileProvider.framework */;
productReference = 799396751D48B80D00086753 /* FilesProvider.framework */;
productType = "com.apple.product-type.framework";
};
799396811D48B82700086753 /* FileProvider tvOS */ = {
799396811D48B82700086753 /* FilesProvider tvOS */ = {
isa = PBXNativeTarget;
buildConfigurationList = 799396871D48B82700086753 /* Build configuration list for PBXNativeTarget "FileProvider tvOS" */;
buildConfigurationList = 799396871D48B82700086753 /* Build configuration list for PBXNativeTarget "FilesProvider tvOS" */;
buildPhases = (
7993967D1D48B82700086753 /* Sources */,
7993967E1D48B82700086753 /* Frameworks */,
@@ -407,9 +417,9 @@
);
dependencies = (
);
name = "FileProvider tvOS";
name = "FilesProvider tvOS";
productName = "FileProvider tvOS";
productReference = 799396821D48B82700086753 /* FileProvider.framework */;
productReference = 799396821D48B82700086753 /* FilesProvider.framework */;
productType = "com.apple.product-type.framework";
};
/* End PBXNativeTarget section */
@@ -432,7 +442,7 @@
};
};
};
buildConfigurationList = 7993965F1D48B7BF00086753 /* Build configuration list for PBXProject "FileProvider" */;
buildConfigurationList = 7993965F1D48B7BF00086753 /* Build configuration list for PBXProject "FilesProvider" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
hasScannedForEncodings = 0;
@@ -444,9 +454,9 @@
projectDirPath = "";
projectRoot = "";
targets = (
799396661D48B7F600086753 /* FileProvider iOS */,
799396741D48B80D00086753 /* FileProvider OSX */,
799396811D48B82700086753 /* FileProvider tvOS */,
799396661D48B7F600086753 /* FilesProvider iOS */,
799396741D48B80D00086753 /* FilesProvider OSX */,
799396811D48B82700086753 /* FilesProvider tvOS */,
);
};
/* End PBXProject section */
@@ -485,12 +495,14 @@
799396D41D48C02300086753 /* SMB2Tree.swift in Sources */,
7924B1A21D89DAE000589DB7 /* Options.swift in Sources */,
792572411DF23BDA006A1526 /* LocalHelper.swift in Sources */,
7936BC121E880F5700A6C81C /* FTPFileProvider.swift in Sources */,
79480FF61E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */,
79F5745B1DFDB10B00179ABF /* FileObject.swift in Sources */,
79BD63C51E2D17880035128C /* OneDriveFileProvide.swift in Sources */,
79BD63C51E2D17880035128C /* OneDriveFileProvider.swift in Sources */,
7924B1991D89DAE000589DB7 /* Element.swift in Sources */,
799396C81D48C02300086753 /* SMB2IOCtl.swift in Sources */,
799396D71D48C02300086753 /* SMB2Types.swift in Sources */,
798654331E8874BC002FA550 /* FTPHelper.swift in Sources */,
7924B1B21D89FCDA00589DB7 /* FPSStreamTask.swift in Sources */,
799396C51D48C02300086753 /* SMB2FileOperation.swift in Sources */,
79BD63C81E2D17880035128C /* OneDriveHelper.swift in Sources */,
@@ -526,12 +538,14 @@
799396D51D48C02300086753 /* SMB2Tree.swift in Sources */,
7924B1A31D89DAE000589DB7 /* Options.swift in Sources */,
792572421DF23BDA006A1526 /* LocalHelper.swift in Sources */,
7936BC131E880F5700A6C81C /* FTPFileProvider.swift in Sources */,
79480FF71E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */,
79F5745C1DFDB10B00179ABF /* FileObject.swift in Sources */,
79BD63C61E2D17880035128C /* OneDriveFileProvide.swift in Sources */,
79BD63C61E2D17880035128C /* OneDriveFileProvider.swift in Sources */,
7924B1B01D89F7DE00589DB7 /* FPSStreamTask.swift in Sources */,
7924B19A1D89DAE000589DB7 /* Element.swift in Sources */,
799396C91D48C02300086753 /* SMB2IOCtl.swift in Sources */,
79F4678B1E8B80F200C91A85 /* FTPHelper.swift in Sources */,
799396D81D48C02300086753 /* SMB2Types.swift in Sources */,
799396C61D48C02300086753 /* SMB2FileOperation.swift in Sources */,
79BD63C91E2D17880035128C /* OneDriveHelper.swift in Sources */,
@@ -567,12 +581,14 @@
799396D61D48C02300086753 /* SMB2Tree.swift in Sources */,
7924B1A41D89DAE000589DB7 /* Options.swift in Sources */,
792572431DF23BDA006A1526 /* LocalHelper.swift in Sources */,
7936BC141E880F5700A6C81C /* FTPFileProvider.swift in Sources */,
79480FF81E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */,
79F5745D1DFDB10B00179ABF /* FileObject.swift in Sources */,
79BD63C71E2D17880035128C /* OneDriveFileProvide.swift in Sources */,
79BD63C71E2D17880035128C /* OneDriveFileProvider.swift in Sources */,
7924B1B11D89F7DF00589DB7 /* FPSStreamTask.swift in Sources */,
7924B19B1D89DAE000589DB7 /* Element.swift in Sources */,
799396CA1D48C02300086753 /* SMB2IOCtl.swift in Sources */,
79F4678C1E8B80F200C91A85 /* FTPHelper.swift in Sources */,
799396D91D48C02300086753 /* SMB2Types.swift in Sources */,
799396C71D48C02300086753 /* SMB2FileOperation.swift in Sources */,
79BD63CA1E2D17880035128C /* OneDriveHelper.swift in Sources */,
@@ -605,7 +621,7 @@
799396601D48B7BF00086753 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_VERSION_STRING = 0.14.5;
BUNDLE_VERSION_STRING = 0.19.0;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_EMPTY_BODY = YES;
@@ -615,6 +631,7 @@
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_BITCODE = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -628,6 +645,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
ONLY_ACTIVE_ARCH = YES;
PRODUCT_NAME = FilesProvider;
SWIFT_VERSION = 3.0;
};
name = Debug;
@@ -635,7 +653,7 @@
799396611D48B7BF00086753 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_VERSION_STRING = 0.14.5;
BUNDLE_VERSION_STRING = 0.19.0;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_EMPTY_BODY = YES;
@@ -655,6 +673,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
PRODUCT_NAME = FilesProvider;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_VERSION = 3.0;
};
@@ -701,8 +720,7 @@
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FileProvider-iOS";
PRODUCT_NAME = FileProvider;
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FilesProvider-iOS";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -748,8 +766,7 @@
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FileProvider-iOS";
PRODUCT_NAME = FileProvider;
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FilesProvider-iOS";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -805,8 +822,7 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks";
MACOSX_DEPLOYMENT_TARGET = 10.10;
MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FileProvider-OSX";
PRODUCT_NAME = FileProvider;
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FilesProvider-OSX";
SDKROOT = macosx;
SKIP_INSTALL = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -856,8 +872,7 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks";
MACOSX_DEPLOYMENT_TARGET = 10.10;
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FileProvider-OSX";
PRODUCT_NAME = FileProvider;
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FilesProvider-OSX";
SDKROOT = macosx;
SKIP_INSTALL = YES;
VERSIONING_SYSTEM = "apple-generic";
@@ -908,8 +923,7 @@
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FileProvider-tvOS";
PRODUCT_NAME = FileProvider;
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FilesProvider-tvOS";
SDKROOT = appletvos;
SKIP_INSTALL = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -958,8 +972,7 @@
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FileProvider-tvOS";
PRODUCT_NAME = FileProvider;
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FilesProvider-tvOS";
SDKROOT = appletvos;
SKIP_INSTALL = YES;
TARGETED_DEVICE_FAMILY = 3;
@@ -973,7 +986,7 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
7993965F1D48B7BF00086753 /* Build configuration list for PBXProject "FileProvider" */ = {
7993965F1D48B7BF00086753 /* Build configuration list for PBXProject "FilesProvider" */ = {
isa = XCConfigurationList;
buildConfigurations = (
799396601D48B7BF00086753 /* Debug */,
@@ -982,7 +995,7 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
7993966F1D48B7F600086753 /* Build configuration list for PBXNativeTarget "FileProvider iOS" */ = {
7993966F1D48B7F600086753 /* Build configuration list for PBXNativeTarget "FilesProvider iOS" */ = {
isa = XCConfigurationList;
buildConfigurations = (
7993966D1D48B7F600086753 /* Debug */,
@@ -991,7 +1004,7 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
7993967A1D48B80D00086753 /* Build configuration list for PBXNativeTarget "FileProvider OSX" */ = {
7993967A1D48B80D00086753 /* Build configuration list for PBXNativeTarget "FilesProvider OSX" */ = {
isa = XCConfigurationList;
buildConfigurations = (
7993967B1D48B80D00086753 /* Debug */,
@@ -1000,7 +1013,7 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
799396871D48B82700086753 /* Build configuration list for PBXNativeTarget "FileProvider tvOS" */ = {
799396871D48B82700086753 /* Build configuration list for PBXNativeTarget "FilesProvider tvOS" */ = {
isa = XCConfigurationList;
buildConfigurations = (
799396881D48B82700086753 /* Debug */,
@@ -15,9 +15,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "799396741D48B80D00086753"
BuildableName = "FileProvider.framework"
BlueprintName = "FileProvider OSX"
ReferencedContainer = "container:FileProvider.xcodeproj">
BuildableName = "FilesProvider.framework"
BlueprintName = "FilesProvider OSX"
ReferencedContainer = "container:FilesProvider.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
@@ -26,6 +26,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
@@ -36,6 +37,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
@@ -46,9 +48,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "799396741D48B80D00086753"
BuildableName = "FileProvider.framework"
BlueprintName = "FileProvider OSX"
ReferencedContainer = "container:FileProvider.xcodeproj">
BuildableName = "FilesProvider.framework"
BlueprintName = "FilesProvider OSX"
ReferencedContainer = "container:FilesProvider.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
@@ -64,9 +66,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "799396741D48B80D00086753"
BuildableName = "FileProvider.framework"
BlueprintName = "FileProvider OSX"
ReferencedContainer = "container:FileProvider.xcodeproj">
BuildableName = "FilesProvider.framework"
BlueprintName = "FilesProvider OSX"
ReferencedContainer = "container:FilesProvider.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
@@ -15,9 +15,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "799396661D48B7F600086753"
BuildableName = "FileProvider.framework"
BlueprintName = "FileProvider iOS"
ReferencedContainer = "container:FileProvider.xcodeproj">
BuildableName = "FilesProvider.framework"
BlueprintName = "FilesProvider iOS"
ReferencedContainer = "container:FilesProvider.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
@@ -26,6 +26,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
@@ -36,6 +37,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
@@ -46,9 +48,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "799396661D48B7F600086753"
BuildableName = "FileProvider.framework"
BlueprintName = "FileProvider iOS"
ReferencedContainer = "container:FileProvider.xcodeproj">
BuildableName = "FilesProvider.framework"
BlueprintName = "FilesProvider iOS"
ReferencedContainer = "container:FilesProvider.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
@@ -64,9 +66,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "799396661D48B7F600086753"
BuildableName = "FileProvider.framework"
BlueprintName = "FileProvider iOS"
ReferencedContainer = "container:FileProvider.xcodeproj">
BuildableName = "FilesProvider.framework"
BlueprintName = "FilesProvider iOS"
ReferencedContainer = "container:FilesProvider.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
@@ -15,9 +15,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "799396811D48B82700086753"
BuildableName = "FileProvider.framework"
BlueprintName = "FileProvider tvOS"
ReferencedContainer = "container:FileProvider.xcodeproj">
BuildableName = "FilesProvider.framework"
BlueprintName = "FilesProvider tvOS"
ReferencedContainer = "container:FilesProvider.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
@@ -26,6 +26,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
@@ -36,6 +37,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
@@ -46,9 +48,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "799396811D48B82700086753"
BuildableName = "FileProvider.framework"
BlueprintName = "FileProvider tvOS"
ReferencedContainer = "container:FileProvider.xcodeproj">
BuildableName = "FilesProvider.framework"
BlueprintName = "FilesProvider tvOS"
ReferencedContainer = "container:FilesProvider.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
@@ -64,9 +66,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "799396811D48B82700086753"
BuildableName = "FileProvider.framework"
BlueprintName = "FileProvider tvOS"
ReferencedContainer = "container:FileProvider.xcodeproj">
BuildableName = "FilesProvider.framework"
BlueprintName = "FilesProvider tvOS"
ReferencedContainer = "container:FilesProvider.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
+2 -2
View File
@@ -1,5 +1,5 @@
import PackageDescription
let package = Package(
name: "FileProvider"
)
name: "FilesProvider"
)
+80 -52
View File
@@ -2,25 +2,32 @@
>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]
[![Release versin][release-image]][release-url]
[![CocoaPods version](https://img.shields.io/cocoapods/v/FileProvider.svg)][cocoapods]
[![Carthage compatible][carthage-image]](https://github.com/Carthage/Carthage)
[![Build Status][travis-image]][travis-url]
[![Codebeat Badge][codebeat-image]][codebeat-url]
[![Cocoapods Docs][docs-image]][docs-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>
<!---
[![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, Dropbox, OneDrive and SMB2 (incomplete) and local files.
This library provides implementaion of WebDav, FTP, Dropbox, OneDrive and SMB2 (incomplete) and local files.
All functions do async calls and it wont block your main thread.
@@ -28,7 +35,8 @@ All functions do async calls and it wont block your main thread.
- [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, used by many cloud services like Yandex.
- [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.
@@ -37,25 +45,27 @@ All functions do async calls and it wont block your main thread.
- [ ] **AmazonS3FileProvider** Amazon storage backend. Used by many sites.
- [ ] **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!*.
* SMB1/CIFS is deprecated and very tricky to be implemented.
- [ ] **FTPFileProvider** while deprecated in 1990s, it's still in use on some Web hosts.
* SMBv1/CIFS is insecure, deprecated and kinda tricky to be implemented due to strict memory allignment in Swift.
## Requirements
- **Swift 3.0 +**
- **Swift 3.0 or higher**
- iOS 8.0 , OSX 10.10
- XCode 8.0
Legacy version is available in swift-2 branch
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
Add this line to your pods file:
```ruby
pod "FileProvider"
pod "FilesProvider"
```
Or add this to Cartfile:
@@ -67,7 +77,7 @@ github "amosavian/FileProvider"
Or to use in Swift Package Manager add this line in `Dependencies`:
```swift
.Package(url: "https://github.com/amosavian/FileProvider.git", majorVersion: 0, minorVersion: 12)
.Package(url: "https://github.com/amosavian/FileProvider.git", majorVersion: 0)
```
### Manually
@@ -93,7 +103,7 @@ Then you can do either:
* Copy Source folder to your project and Voila!
* Drop `FileProvider.xcodeproj` to you Xcode workspace and add the framework to your Embeded Binaries in target.
* Drop `FilesProvider.xcodeproj` to you Xcode workspace and add the framework to your Embeded Binaries in target.
## Usage
@@ -104,6 +114,8 @@ Each provider has a specific class which conforms to FileProvider protocol and s
For LocalFileProvider if you want to deal with `Documents` folder
``` swift
import FilesProvider
let documentsProvider = LocalFileProvider()
// Equals with:
@@ -117,6 +129,8 @@ 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
```
@@ -126,6 +140,8 @@ You can't change the base url later. and all paths are related to this base url
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)
```
@@ -133,6 +149,8 @@ let documentsProvider = CloudFileProvider(containerId: nil)
For remote file providers authentication may be necessary:
``` 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)
```
@@ -228,10 +246,10 @@ To get list of files in a directory:
documentsProvider.contentsOfDirectory(path: "/", completionHandler: {
contents, error in
for file in contents {
print("Name: \(attributes.name)")
print("Size: \(attributes.size)")
print("Creation Date: \(attributes.creationDate)")
print("Modification Date: \(attributes.modifiedDate)")
print("Name: \(file.name)")
print("Size: \(file.size)")
print("Creation Date: \(file.creationDate)")
print("Modification Date: \(file.modifiedDate)")
}
})
```
@@ -248,15 +266,6 @@ func storageProperties(completionHandler: { total, used in
* if this function is unavailable on provider or an error has been occurred, total space will be reported `-1` and used space `0`
### Change current directory
```swift
documentsProvider.currentPath = "/New Folder"
// now path is ~/Documents/New Folder
```
You can then pass "" (empty string) to `contentsOfDirectory` method to list files in current directory.
### Creating File and Folders
Creating new directory:
@@ -271,12 +280,7 @@ documentsProvider.create(folder: "new folder", at: "/", completionHandler: { err
})
```
Creating new file from data:
```swift
let data = "hello world!".data(encoding: .utf8)
documentsProvider.create(file: "newFile.txt", at: "/", contents: data, completionHandler: nil)
```
To create a file, use `writeContents(path:, content:, atomically:, completionHandler:)` method.
### Copy and Move/Rename Files
@@ -300,8 +304,6 @@ documentsProvider.moveItem(path: "new folder/old.txt", to: "new.txt", overwrite:
documentsProvider.removeItem(path: "new.txt", completionHandler: nil)
```
***Caution:*** This method will delete directories with all it's contents recursively.
### 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.
@@ -333,6 +335,33 @@ let data = "What's up Newyork!".data(encoding: .utf8)
documentsProvider.writeContents(path: "old.txt", content: data, atomically: true, completionHandler: nil)
```
### 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:
@@ -376,12 +405,6 @@ class ViewController: UIViewController
}
```
### Operation Handle
Creating/Copying/Deleting functions return a `OperationHandle` for remote operations. It provides operation type, progress and a `.cancel()` method which allows you to cancel operation in midst.
It's not supported by native `(NS)FileManager` so `LocalFileProvider`, but this functionality will be added to future `PosixFileProvider` class.
### 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.
@@ -413,9 +436,10 @@ To check either file thumbnail is supported or not and fetch thumbnail, use (and
```swift
let path = "/newImage.jpg"
let thumbSize = CGSize(width: 64, height: 64)
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
}
@@ -443,9 +467,10 @@ if documentsProvider..propertiesOfFile(path: file.path, completionHandler: { (pr
We would love for you to contribute to **FileProvider**, check the `LICENSE` file for more info.
Things to do:
Things you may consider to help us:
- [ ] Implement Test-case (XCTest)
- [ ] Implement request/response stack for `SMBClient`
- [ ] Implement Test-case (`XCTest`)
- [ ] Add Sample project for iOS
- [ ] Add Sample project for macOS
@@ -466,10 +491,11 @@ Distributed under the MIT license. See `LICENSE` for more information.
[https://github.com/amosavian/](https://github.com/amosavian/)
[cocoapods]: https://cocoapods.org/pods/FileProvider
[swift-image]: https://img.shields.io/badge/swift-3.0-orange.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/
[platform-image]: https://img.shields.io/cocoapods/p/FileProvider.svg
[platform-image]: https://img.shields.io/cocoapods/p/FilesProvider.svg
[license-image]: https://img.shields.io/github/license/amosavian/FileProvider.svg
[license-url]: LICENSE
[codebeat-image]: https://codebeat.co/badges/7b359f48-78eb-4647-ab22-56262a827517
@@ -479,7 +505,9 @@ Distributed under the MIT license. See `LICENSE` for more information.
[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]: https://img.shields.io/cocoapods/dt/FileProvider.svg
[cocoapods-apps]: https://img.shields.io/cocoapods/at/FileProvider.svg
[docs-image]: https://img.shields.io/cocoapods/metrics/doc-percent/FileProvider.svg
[docs-url]: http://cocoadocs.org/docsets/FileProvider/
[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/
+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.
+167 -159
View File
@@ -15,7 +15,7 @@ import Foundation
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 {
open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
/// An string to identify type of provider.
open override class var type: String { return "iCloudDrive" }
@@ -71,9 +71,14 @@ open class CloudFileProvider: LocalFileProvider {
super.init(baseURL: baseURL)
self.isCoorinating = true
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: .concurrent)
#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 = "FileProvider.\(type(of: self).type).Operation"
operation_queue.name = "\(queueLabel).Operation"
fileManager.url(forUbiquityContainerIdentifier: containerId)
opFileManager.url(forUbiquityContainerIdentifier: containerId)
@@ -128,7 +133,7 @@ open class CloudFileProvider: LocalFileProvider {
query.valueListAttributes = [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]
query.searchScopes = [self.scope.rawValue]
var finishObserver: NSObjectProtocol?
finishObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
defer {
query.stop()
NotificationCenter.default.removeObserver(finishObserver!)
@@ -195,7 +200,7 @@ open class CloudFileProvider: LocalFileProvider {
query.valueListAttributes = [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]
query.searchScopes = [self.scope.rawValue]
var finishObserver: NSObjectProtocol?
finishObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
defer {
query.stop()
NotificationCenter.default.removeObserver(finishObserver!)
@@ -238,13 +243,13 @@ open class CloudFileProvider: LocalFileProvider {
- Note: For now only it's limited to file names. `query` parameter may take `NSPredicate` format in near future.
- Parameters:
- path: location of directory to start search
- recursive: Searching subdirectories of path
- query: Simple string of file name to be search (for now).
- 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.
- path: location of directory to start search
- recursive: Searching subdirectories of path
- query: Simple string of file name to be search (for now).
- 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.
*/
open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? {
let mapDict: [String: String] = ["url": NSMetadataItemURLKey, "name": NSMetadataItemFSNameKey, "path": NSMetadataItemPathKey, "filesize": NSMetadataItemFSSizeKey, "modifiedDate": NSMetadataItemFSContentChangeDateKey, "creationDate": NSMetadataItemFSCreationDateKey, "contentType": NSMetadataItemContentTypeKey]
@@ -277,6 +282,8 @@ open class CloudFileProvider: LocalFileProvider {
}
}
let progress = Progress(parent: nil, userInfo: nil)
dispatch_queue.async {
let pathURL = self.url(of: path)
let mdquery = NSMetadataQuery()
@@ -285,6 +292,10 @@ open class CloudFileProvider: LocalFileProvider {
var lastReportedCount = 0
progress.cancellationHandler = { [weak mdquery] in
mdquery?.stop()
}
if let foundItemHandler = foundItemHandler {
var updateObserver: NSObjectProtocol?
@@ -309,6 +320,7 @@ open class CloudFileProvider: LocalFileProvider {
}
}
lastReportedCount = mdquery.resultCount
progress.totalUnitCount = Int64(lastReportedCount)
mdquery.enableUpdates()
})
@@ -341,12 +353,14 @@ open class CloudFileProvider: LocalFileProvider {
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.throwError(path, code: CocoaError.fileReadNoPermission))
@@ -354,6 +368,8 @@ open class CloudFileProvider: LocalFileProvider {
}
}
}
return progress
}
open override func isReachable(completionHandler: @escaping (Bool) -> Void) {
@@ -370,29 +386,11 @@ open class CloudFileProvider: LocalFileProvider {
- folder: Directory name.
- at: Parent path of new directory.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
- Returns: A `Progress` object to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
*/
@discardableResult
open override func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
guard let r = super.create(folder: folderName, at: atPath, completionHandler: completionHandler) else { return nil }
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
}
/**
Creates an new file with data passed to method asynchronously.
Returns error via completionHandler if file is already exists.
- Parameters:
- file: New file name with extension separated by period.
- at: Parent path of new file.
- data: Data of new files. Pass nil or `Data()` to create empty file.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
*/
@discardableResult
open override func create(file fileName: String, at atPath: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
guard let r = super.create(file: fileName, at: atPath, contents: data, completionHandler: completionHandler) else { return nil }
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
open override func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? {
return super.create(folder: folderName, at: atPath, completionHandler: completionHandler)
}
/**
@@ -405,12 +403,11 @@ open class CloudFileProvider: LocalFileProvider {
- to: destination path of file or directory, including file/directory name.
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
- Returns: A `Progress` object to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
*/
@discardableResult
open override func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
guard let r = super.moveItem(path: path, to: toPath, overwrite: overwrite, completionHandler: completionHandler) else { return nil }
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
open override func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
return super.moveItem(path: path, to: toPath, overwrite: overwrite, completionHandler: completionHandler)
}
/**
@@ -423,12 +420,11 @@ open class CloudFileProvider: LocalFileProvider {
- to: destination path of file or directory, including file/directory name.
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
- Returns: A `Progress` object to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
*/
@discardableResult
open override func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
guard let r = super.copyItem(path: path, to: toPath, overwrite: overwrite, completionHandler: completionHandler) else { return nil }
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
open override func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
return super.copyItem(path: path, to: toPath, overwrite: overwrite, completionHandler: completionHandler)
}
/**
@@ -441,13 +437,11 @@ open class CloudFileProvider: LocalFileProvider {
- Parameters:
- path: file or directory path.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
- Returns: A `Progress` object to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
*/
@discardableResult
open override func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
guard let r = super.removeItem(path: path, completionHandler: completionHandler) else { return nil }
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
open override func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
return super.removeItem(path: path, completionHandler: completionHandler)
}
/**
@@ -459,12 +453,18 @@ open class CloudFileProvider: LocalFileProvider {
- to: destination path of file, including file/directory name.
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress.
- Returns: A `Progress` object to get progress or cancel progress.
*/
@discardableResult
open override func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open override func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
// TODO: Make use of overwrite parameter
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
let progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.isCancellable = false
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
monitorFile(path: toPath, opType: opType, progress: progress)
operation_queue.addOperation {
let tempFolder: URL
if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) {
@@ -492,7 +492,7 @@ open class CloudFileProvider: LocalFileProvider {
})
}
}
return CloudOperationHandle(operationType: opType, baseURL: self.baseURL)
return progress
}
/**
@@ -503,12 +503,17 @@ open class CloudFileProvider: LocalFileProvider {
- 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: An `OperationHandle` to get progress or cancel progress.
- Returns: A `Progress` object to get progress or cancel progress.
*/
@discardableResult
open override func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open override func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
let progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.isCancellable = false
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
monitorFile(path: path, opType: opType, progress: progress)
do {
try self.opFileManager.startDownloadingUbiquitousItem(at: self.url(of: path))
} catch let e {
@@ -518,9 +523,8 @@ open class CloudFileProvider: LocalFileProvider {
})
return nil
}
guard let r = super.copyItem(path: path, toLocalURL: toLocalURL, completionHandler: completionHandler) else { return nil }
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
let _ = super.copyItem(path: path, toLocalURL: toLocalURL, completionHandler: completionHandler)
return progress
}
/**
@@ -532,12 +536,18 @@ open class CloudFileProvider: LocalFileProvider {
- completionHandler: a closure with result of file contents or error.
`contents`: contents of file in a `Data` object.
`error`: Error returned by system.
- Returns: An `OperationHandle` to get progress or cancel progress.
- Returns: A `Progress` object to get progress or cancel progress.
*/
@discardableResult
open override func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
guard let r = super.contents(path: path, completionHandler: completionHandler) else { return nil }
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
open override func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
let operation = FileOperationType.fetch(path: path)
let progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
monitorFile(path: path, opType: operation, progress: progress)
_ = super.contents(path: path, completionHandler: completionHandler)
return progress
}
/**
@@ -551,12 +561,18 @@ open class CloudFileProvider: LocalFileProvider {
- completionHandler: a closure with result of file contents or error.
`contents`: contents of file in a `Data` object.
`error`: Error returned by system.
- Returns: An `OperationHandle` to get progress or cancel progress.
- 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)) -> OperationHandle? {
guard let r = super.contents(path: path, offset: offset, length: length, completionHandler: completionHandler) else { return nil }
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
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 = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
monitorFile(path: path, opType: operation, progress: progress)
_ = super.contents(path: path, offset: offset, length: length, completionHandler: completionHandler)
return progress
}
/**
@@ -568,12 +584,18 @@ open class CloudFileProvider: LocalFileProvider {
- 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: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- 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) -> OperationHandle? {
guard let r = super.writeContents(path: path, contents: data, atomically: atomically, overwrite: overwrite, completionHandler: completionHandler) else { return nil }
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
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(parent: nil, userInfo: nil)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
monitorFile(path: path, opType: operation, progress: progress)
_ = super.writeContents(path: path, contents: data, atomically: atomically, overwrite: overwrite, completionHandler: completionHandler)
return progress
}
fileprivate var monitors = [String: (NSMetadataQuery, NSObjectProtocol)]()
@@ -645,7 +667,7 @@ open class CloudFileProvider: LocalFileProvider {
let path = self.relativePathOf(url: url)
let rpath = path.hasPrefix("/") ? path.substring(from: path.index(after: path.startIndex)) : path
let relativeUrl = URL(string: rpath, relativeTo: self.baseURL)
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
@@ -658,6 +680,57 @@ open class CloudFileProvider: LocalFileProvider {
return file
}
func monitorFile(path: String, opType: FileOperationType, progress: Progress?) {
dispatch_queue.async {
let pathURL = self.url(of: path)
let size = pathURL.fileSize
progress?.totalUnitCount = size > 0 ? size : 0
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: "%K LIKE %@", NSMetadataItemPathKey, pathURL.path)
query.valueListAttributes = [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataUbiquitousItemPercentDownloadedKey, NSMetadataUbiquitousItemPercentUploadedKey, NSMetadataItemFSSizeKey]
query.searchScopes = [self.scope.rawValue]
var updateObserver: NSObjectProtocol?
updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidUpdate, object: query, queue: nil, using: { (notification) in
query.disableUpdates()
guard let item = (query.results as? [NSMetadataItem])?.first else {
return
}
if progress?.totalUnitCount == 0, let size = item.value(forAttribute: NSMetadataItemFSSizeKey) as? Int64 {
progress?.totalUnitCount = size
}
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(progress?.totalUnitCount ?? 0))
DispatchQueue.main.async {
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(uploaded / 100))
}
} else if (uploaded == 0 || uploaded == 100) && (downloaded > 0 && downloaded < 100) {
progress?.completedUnitCount = Int64(downloaded / 100 * Double(progress?.totalUnitCount ?? 0))
DispatchQueue.main.async {
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(downloaded / 100))
}
} else if uploaded == 100 || downloaded == 100 {
progress?.completedUnitCount = progress?.totalUnitCount ?? 0
query.stop()
NotificationCenter.default.removeObserver(updateObserver!)
DispatchQueue.main.async {
self.delegate?.fileproviderSucceed(self, operation: opType)
}
}
query.enableUpdates()
})
DispatchQueue.main.async {
progress?.setUserInfoObject(Date(), forKey: .startingTimeKey)
query.start()
}
}
}
/// Removes local copy of file, but spares cloud copy/
/// - Parameter path: Path of file or directory to be remoed from local
/// - Parameter completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
@@ -672,19 +745,6 @@ open class CloudFileProvider: LocalFileProvider {
}
}
/**
Genrates a public url to a file to be shared with other users and can be downloaded without authentication.
- Important: URL will be available for a limitied time, determined in `expiration` argument.
- Parameters:
- to: path of file, including file/directory name.
- completionHandler: a closure with result of directory entries or error.
`link`: a url returned by Dropbox to share.
`attribute`: a `FileObject` containing the attributes of the item.
`expiration`: a `Date` object, determines when the public url will expires.
`error`: Error returned by Dropbox.
*/
open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
operation_queue.addOperation {
do {
@@ -702,6 +762,7 @@ open class CloudFileProvider: LocalFileProvider {
}
}
/// 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
@@ -734,89 +795,36 @@ public enum UbiquitousScope: RawRepresentable {
}
}
open class CloudOperationHandle: OperationHandle {
public let baseURL: URL?
public let operationType: FileOperationType
/*
func getMetadataItem(url: URL) -> NSMetadataItem? {
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: "(%K LIKE %@)", NSMetadataItemPathKey, url.path)
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope, NSMetadataQueryUbiquitousDataScope]
init (operationType: FileOperationType, baseURL: URL?) {
self.baseURL = baseURL
self.operationType = operationType
}
var item: NSMetadataItem?
private var sourceURL: URL? {
guard let source = operationType.source, let baseURL = baseURL else { return nil }
return source.hasPrefix("file://") ? URL(fileURLWithPath: source) : baseURL.appendingPathComponent(source)
}
private var destURL: URL? {
guard let dest = operationType.destination, let baseURL = baseURL else { return nil }
return dest.hasPrefix("file://") ? URL(fileURLWithPath: dest) : baseURL.appendingPathComponent(dest)
}
open var bytesSoFar: Int64 {
assert(!Thread.isMainThread, "Don't run \(#function) method on main thread")
guard let url = destURL ?? sourceURL, let item = CloudOperationHandle.getMetadataItem(url: url) else { return 0 }
let downloaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentDownloadedKey) as? Double ?? 0
let uploaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentUploadedKey) as? Double ?? 0
guard let size = item.value(forAttribute: NSMetadataItemFSSizeKey) as? Int64 else { return -1 }
if (downloaded == 0 || downloaded == 100) && (uploaded > 0 && uploaded < 100) {
return Int64(uploaded * (Double(size) / 100))
} else if (uploaded == 0 || uploaded == 100) && (downloaded > 0 && downloaded < 100) {
return Int64(downloaded * (Double(size) / 100))
} else if uploaded == 100 || downloaded == 100 {
return size
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!)
}
return 0
}
open var totalBytes: Int64 {
assert(!Thread.isMainThread, "Don't run \(#function) method on main thread")
guard let url = destURL ?? sourceURL, let item = CloudOperationHandle.getMetadataItem(url: url) else { return -1 }
return item.value(forAttribute: NSMetadataItemFSSizeKey) as? Int64 ?? -1
}
open var inProgress: Bool {
guard let url = destURL ?? sourceURL, let item = CloudOperationHandle.getMetadataItem(url: url) else { return false }
let downloadStatus = item.value(forAttribute: NSMetadataUbiquitousItemDownloadingStatusKey) as? String ?? NSMetadataUbiquitousItemDownloadingStatusNotDownloaded
let isUploading = item.value(forAttribute: NSMetadataUbiquitousItemIsUploadingKey) as? Bool ?? false
return downloadStatus == NSMetadataUbiquitousItemDownloadingStatusCurrent || isUploading
}
/// Not usable in local provider
open func cancel() -> Bool {
return false
}
fileprivate static 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()
if query.resultCount > 0 {
item = query.result(at: 0) as? NSMetadataItem
}
_ = group.wait(timeout: .now() + 30)
return item
query.disableUpdates()
})
DispatchQueue.main.async {
query.start()
}
_ = group.wait(timeout: .now() + 30)
return item
}
*/
+193 -116
View File
@@ -17,7 +17,7 @@ import CoreGraphics
- Note: Uploading files and data are limited to 150MB, for now.
*/
open class DropboxFileProvider: FileProviderBasicRemote {
open class var type: String { return "DropBox" }
open class var type: String { return "Dropbox" }
open let baseURL: URL?
open var currentPath: String
@@ -34,22 +34,40 @@ open class DropboxFileProvider: FileProviderBasicRemote {
}
open weak var delegate: FileProviderDelegate?
open let credential: URLCredential?
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?
fileprivate var sessionDelegate: SessionDelegate?
internal fileprivate(set) var sessionDelegate: SessionDelegate?
public var session: URLSession {
if _session == nil {
self.sessionDelegate = SessionDelegate(fileProvider: self, credential: credential)
let config = URLSessionConfiguration.default
config.urlCache = cache
config.requestCachePolicy = .returnCacheDataElseLoad
_session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDelegate?, delegateQueue: self.operation_queue)
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!)
}
return _session!
}
fileprivate var _longpollSession: URLSession?
@@ -83,9 +101,14 @@ open class DropboxFileProvider: FileProviderBasicRemote {
self.apiURL = URL(string: "https://api.dropboxapi.com/2/")!
self.contentURL = URL(string: "https://content.dropboxapi.com/2/")!
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: .concurrent)
#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 = "FileProvider.\(type(of: self).type).Operation"
operation_queue.name = "\(queueLabel).Operation"
}
public required convenience init?(coder aDecoder: NSCoder) {
@@ -117,15 +140,21 @@ open class DropboxFileProvider: FileProviderBasicRemote {
}
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)) {
list(path) { (contents, cursor, error) in
let progress = Progress(parent: nil, userInfo: nil)
list(path, progress: progress) { (contents, cursor, error) in
completionHandler(contents, error)
}
}
@@ -134,8 +163,8 @@ open class DropboxFileProvider: FileProviderBasicRemote {
let url = URL(string: "files/get_metadata", relativeTo: apiURL)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .json)
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString]
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
@@ -157,7 +186,7 @@ open class DropboxFileProvider: FileProviderBasicRemote {
let url = URL(string: "users/get_space_usage", relativeTo: apiURL)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.set(httpAuthentication: credential, with: .oAuth2)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var totalSize: Int64 = -1
var usedSize: Int64 = 0
@@ -170,12 +199,13 @@ open class DropboxFileProvider: FileProviderBasicRemote {
task.resume()
}
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? {
let progress = Progress(parent: nil, userInfo: nil)
var foundFiles = [DropboxFileObject]()
if let queryStr = query.findValue(forKey: "name", operator: .beginsWith) as? String {
// Dropbox only support searching for file names begin with query in non-enterprise accounts.
// We will use it if there is a `name BEGINSWITH[c] "query"` in predicate, then filter to form final result.
search(path, query: queryStr, foundItem: { (file) in
search(path, query: queryStr, progress: progress, foundItem: { (file) in
if query.evaluate(with: file.mapPredicate()) {
foundFiles.append(file)
foundItemHandler?(file)
@@ -186,7 +216,7 @@ open class DropboxFileProvider: FileProviderBasicRemote {
} else {
// Dropbox doesn't support searching attributes natively. The workaround is to fallback to listing all files
// and filter it locally. It may have a network burden in case there is many files in Dropbox, so please use it concisely.
list(path, recursive: true, progressHandler: { (files, _, error) in
list(path, recursive: true, progress: progress, progressHandler: { (files, _, error) in
for file in files where query.evaluate(with: file.mapPredicate()) {
foundItemHandler?(file)
}
@@ -195,6 +225,7 @@ open class DropboxFileProvider: FileProviderBasicRemote {
completionHandler(predicatedFiles, error)
})
}
return progress
}
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
@@ -207,32 +238,33 @@ open class DropboxFileProvider: FileProviderBasicRemote {
}
extension DropboxFileProvider: FileProviderOperations {
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
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 create(file fileName: String, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let filePath = (path as NSString).appendingPathComponent(fileName)
return self.writeContents(path: filePath, contents: data ?? Data(), completionHandler: completionHandler)
}
open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
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) -> OperationHandle? {
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) -> OperationHandle? {
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
return doOperation(.remove(path: path), completionHandler: completionHandler)
}
fileprivate func doOperation(_ operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
fileprivate func doOperation(_ operation: FileOperationType, 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 url: String
guard let sourcePath = operation.source else { return nil }
let destPath = operation.destination
@@ -250,8 +282,8 @@ extension DropboxFileProvider: FileProviderOperations {
}
var request = URLRequest(url: URL(string: url, relativeTo: apiURL)!)
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .json)
var requestDictionary = [String: AnyObject]()
if let dest = correctPath(destPath) as NSString? {
requestDictionary["from_path"] = correctPath(sourcePath) as NSString?
@@ -265,15 +297,32 @@ extension DropboxFileProvider: FileProviderOperations {
if let response = response as? HTTPURLResponse, response.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) {
serverError = FileProviderDropboxError(code: code, path: sourcePath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
}
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 RemoteOperationHandle(operationType: operation, tasks: [task])
return progress
}
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
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.throwError(localFile.path, code: URLError.fileIsDirectory))
}
return nil
}
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
@@ -281,41 +330,60 @@ extension DropboxFileProvider: FileProviderOperations {
return upload_simple(toPath, localFile: localFile, overwrite: overwrite, operation: opType, completionHandler: completionHandler)
}
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.copy(source: path, destination: destURL.absoluteString)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
let url = URL(string: "files/download", relativeTo: contentURL)!
var progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
let requestDictionary: [String: AnyObject] = ["path": path as NSString]
let requestJson = String(jsonDictionary: requestDictionary) ?? ""
request.setValue(requestJson, forHTTPHeaderField: "Dropbox-API-Arg")
let task = session.downloadTask(with: request, completionHandler: { (cacheURL, response, error) in
guard let cacheURL = cacheURL, let httpResponse = response as? HTTPURLResponse , httpResponse.statusCode < 300 else {
let code = FileProviderHTTPErrorCode(rawValue: (response as? HTTPURLResponse)?.statusCode ?? -1)
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(dropboxArgKey: ["path": path as NSString])
let task = session.downloadTask(with: request)
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
if error != nil {
progress.cancel()
}
completionHandler?(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? = nil //Data(contentsOf:cacheURL) // TODO: Figure out how to get error response data for the error description
let serverError : FileProviderDropboxError? = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: errorData ?? Data(), encoding: .utf8)) : nil
completionHandler?(serverError ?? error)
if serverError != nil {
progress.cancel()
}
completionHandler?(serverError)
return
}
do {
try FileManager.default.moveItem(at: cacheURL, to: destURL)
try FileManager.default.moveItem(at: tempURL, to: destURL)
completionHandler?(nil)
} catch let e {
completionHandler?(e)
}
})
}
task.taskDescription = opType.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 RemoteOperationHandle(operationType: opType, tasks: [task])
return progress
}
}
extension DropboxFileProvider: FileProviderReadWrite {
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
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)
@@ -325,36 +393,59 @@ extension DropboxFileProvider: FileProviderReadWrite {
let opType = FileOperationType.fetch(path: path)
let url = URL(string: "files/download", relativeTo: contentURL)!
var progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
if length > 0 {
request.setValue("bytes=\(offset)-\(offset + length - 1)", forHTTPHeaderField: "Range")
} else if offset > 0 && length < 0 {
request.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
}
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString]
request.setValue(String(jsonDictionary: requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderDropboxError?
if let httpResponse = response as? HTTPURLResponse , httpResponse.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: httpResponse.statusCode) {
serverError = FileProviderDropboxError(code: code, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8))
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(rangeWithOffset: offset, length: length)
request.set(dropboxArgKey: ["path": correctPath(path)! as NSString])
let task = session.downloadTask(with: request)
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
if error != nil {
progress.cancel()
}
let filedata = serverError ?? error == nil ? data : nil
completionHandler(filedata, serverError ?? error)
})
completionHandler(nil, 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? = nil //Data(contentsOf:cacheURL) // TODO: Figure out how to get error response data for the error description
let serverError : FileProviderDropboxError? = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: errorData ?? Data(), encoding: .utf8)) : nil
if serverError != nil {
progress.cancel()
}
completionHandler(nil, serverError)
return
}
do {
let data = try Data(contentsOf: tempURL)
completionHandler(data, nil)
} catch let e {
completionHandler(nil, e)
}
}
task.taskDescription = opType.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 RemoteOperationHandle(operationType: opType, tasks: [task])
return progress
}
public func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
public func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.modify(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
// FIXME: remove 150MB restriction
return upload_simple(path, data: data, overwrite: overwrite, operation: opType, completionHandler: completionHandler)
return upload_simple(path, data: data ?? Data(), overwrite: overwrite, operation: opType, completionHandler: completionHandler)
}
/*
@@ -374,42 +465,13 @@ extension DropboxFileProvider: FileProviderReadWrite {
// TODO: Implement /get_account & /get_current_account
}
extension DropboxFileProvider {
/// *OBSOLETED:* Use `publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?))` function instead.
@available(*, obsoleted: 1.0, renamed: "publicLink(to:completionHandler:)", message: "Use publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?)) function instead.")
open func temporaryLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: DropboxFileObject?, _ error: Error?) -> Void)) {
self.publicLink(to: path) { (url, file, _, error) in
completionHandler(url, file, error)
}
}
/// *OBSOLETED:* Use `publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?))` function instead.
@available(*, obsoleted: 1.0, renamed: "publicLink(to:completionHandler:)", message: "Use publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?)) function instead.")
open func temporaryLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: DropboxFileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
self.publicLink(to: path) { (url, file, expiration, error) in
completionHandler(url, file, expiration, error)
}
}
/**
Genrates a public url to a file to be shared with other users and can be downloaded without authentication.
- Important: URL will be available for a limitied time (4 hours according to Dropbox documentation).
- Parameters:
- to: path of file, including file/directory name.
- completionHandler: a closure with result of directory entries or error.
`link`: a url returned by Dropbox to share.
`attribute`: a `FileObject` containing the attributes of the item.
`expiration`: a `Date` object, determines when the public url will expires.
`error`: Error returned by Dropbox.
*/
open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: DropboxFileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
extension DropboxFileProvider: FileProviderSharing {
open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
let url = URL(string: "files/get_temporary_link", relativeTo: apiURL)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .json)
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString]
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
@@ -442,9 +504,9 @@ extension DropboxFileProvider {
- 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.
- `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 {
@@ -454,8 +516,8 @@ extension DropboxFileProvider {
let url = URL(string: "files/save_url", relativeTo: apiURL)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .json)
let requestDictionary: [String: AnyObject] = ["path": correctPath(toPath)! as NSString, "url" : remoteURL.absoluteString as NSString]
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
@@ -489,8 +551,8 @@ extension DropboxFileProvider {
let url = URL(string: "files/copy_reference/save", relativeTo: apiURL)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .json)
let requestDictionary: [String: AnyObject] = ["path": correctPath(toPath)! as NSString, "copy_reference" : reference as NSString]
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
@@ -538,26 +600,37 @@ extension DropboxFileProvider: ExtendedFileProvider {
/// 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.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.set(httpAuthentication: credential, with: .oAuth2)
var requestDictionary: [String: AnyObject] = ["path": path as NSString]
requestDictionary["format"] = "jpeg" as NSString
if let dimension = dimension {
requestDictionary["size"] = "w\(Int(dimension.width))h\(Int(dimension.height))" as NSString
if thumbAPI {
requestDictionary["format"] = "jpeg" as NSString
let size: String
switch dimension?.height ?? 64 {
case 0...32: size = "w32h32"
case 33...64: size = "w64h64"
case 65...128: size = "w128h128"
case 129...480: size = "w640h480"
default: size = "w1024h768"
}
requestDictionary["size"] = size as NSString
}
request.setValue(String(jsonDictionary: requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
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() {
@@ -570,8 +643,12 @@ extension DropboxFileProvider: ExtendedFileProvider {
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 {
image = ImageClass(data: data)
} else if let fetchedimage = ImageClass(data: data){
if let dimension = dimension {
image = DropboxFileProvider.scaleDown(image: fetchedimage, toSize: dimension)
} else {
image = fetchedimage
}
}
}
completionHandler(image, error)
@@ -583,8 +660,8 @@ extension DropboxFileProvider: ExtendedFileProvider {
let url = URL(string: "files/get_metadata", relativeTo: apiURL)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .json)
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString, "include_media_info": NSNumber(value: true)]
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
@@ -594,7 +671,7 @@ extension DropboxFileProvider: ExtendedFileProvider {
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
serverError = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
if let json = data?.deserializeJSON(), let properties = json["media_info"] as? [String: Any] {
if let json = data?.deserializeJSON(), let properties = (json["media_info"] as? [String: Any])?["metadata"] as? [String: Any] {
(dic, keys) = self.mapMediaInfo(properties)
}
}
+73 -68
View File
@@ -17,22 +17,18 @@ public struct FileProviderDropboxError: FileProviderHTTPError {
/// Containts path, url and attributes of a Dropbox file or resource.
public final class DropboxFileObject: FileObject {
internal init(name: String, path: String) {
super.init(url: URL(string: path) ?? URL(string: "/")!, name: name, path: path)
}
internal convenience init? (jsonStr: String) {
guard let json = jsonStr.deserializeJSON() else { return nil }
self.init(json: json)
}
internal convenience init? (json: [String: AnyObject]) {
internal init? (json: [String: AnyObject]) {
guard let name = json["name"] as? String else { return nil }
guard let path = json["path_display"] as? String else { return nil }
self.init(name: name, path: path)
super.init(url: nil, name: name, path: path)
self.size = (json["size"] as? NSNumber)?.int64Value ?? -1
self.serverTime = resolve(dateString: json["server_modified"] as? String ?? "")
self.modifiedDate = resolve(dateString: json["client_modified"] as? String ?? "")
self.serverTime = Date(rfcString: json["server_modified"] as? String ?? "")
self.modifiedDate = Date(rfcString: json["client_modified"] as? String ?? "")
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
@@ -42,10 +38,10 @@ public final class DropboxFileObject: FileObject {
/// The time contents of file has been modified on server, returns nil if not set
open internal(set) var serverTime: Date? {
get {
return allValues[.serverDate] as? Date
return allValues[.serverDateKey] as? Date
}
set {
allValues[.serverDate] = newValue
allValues[.serverDateKey] = newValue
}
}
@@ -75,7 +71,11 @@ public final class DropboxFileObject: FileObject {
// codebeat:disable[ARITY]
internal extension DropboxFileProvider {
func list(_ path: String, cursor: String? = nil, prevContents: [DropboxFileObject] = [], recursive: Bool = false, session: URLSession? = nil, progressHandler: ((_ contents: [FileObject], _ nextCursor: String?, _ error: Error?) -> Void)? = nil, completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) {
func list(_ path: String, cursor: String? = nil, prevContents: [DropboxFileObject] = [], recursive: Bool = false, session: URLSession? = nil, progress: Progress, progressHandler: ((_ contents: [FileObject], _ nextCursor: String?, _ error: Error?) -> Void)? = nil, completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) {
if progress.isCancelled { return }
var requestDictionary = [String: AnyObject]()
let url: URL
if let cursor = cursor {
@@ -88,8 +88,8 @@ internal extension DropboxFileProvider {
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .json)
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = (session ?? self.session).dataTask(with: request, completionHandler: { (data, response, error) in
var responseError: FileProviderDropboxError?
@@ -103,13 +103,14 @@ internal extension DropboxFileProvider {
for entry in entries {
if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry) {
files.append(file)
progress.totalUnitCount = Int64(files.count)
}
}
let ncursor = json["cursor"] as? String
let hasmore = (json["has_more"] as? NSNumber)?.boolValue ?? false
if hasmore {
if hasmore && !progress.isCancelled {
progressHandler?(files, ncursor, responseError ?? error)
self.list(path, cursor: ncursor, prevContents: prevContents + files, completionHandler: completionHandler)
self.list(path, cursor: ncursor, prevContents: prevContents + files, progress: progress, completionHandler: completionHandler)
return
}
}
@@ -117,80 +118,79 @@ internal extension DropboxFileProvider {
progressHandler?(files, nil, responseError ?? error)
completionHandler(prevContents + files, nil, responseError ?? error)
})
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.taskDescription = FileOperationType.fetch(path: path).json
task.resume()
}
func upload_simple(_ targetPath: String, data: Data, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
if data.count > 150 * 1024 * 1024 {
let error = FileProviderDropboxError(code: .payloadTooLarge, path: targetPath, errorDescription: nil)
completionHandler?(error)
self.delegateNotify(.create(path: targetPath), error: error)
return nil
}
var requestDictionary = [String: AnyObject]()
let url: URL
url = URL(string: "files/upload", relativeTo: contentURL)!
requestDictionary["path"] = correctPath(targetPath) as NSString?
requestDictionary["mode"] = (overwrite ? "overwrite" : "add") as NSString
requestDictionary["client_modified"] = rfc3339utc(of: modifiedDate) as NSString
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
request.setValue(String(jsonDictionary: requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
request.httpBody = data
let task = session.uploadTask(with: request, from: data, completionHandler: { (data, response, error) in
var responseError: FileProviderDropboxError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderDropboxError(code: rCode, path: targetPath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
}
completionHandler?(responseError ?? error)
self.delegateNotify(.create(path: targetPath), error: responseError ?? error)
})
task.taskDescription = operation.json
task.resume()
return RemoteOperationHandle(operationType: operation, tasks: [task])
}
func upload_simple(_ targetPath: String, localFile: URL, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let size = (try? localFile.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? -1
func upload_simple(_ targetPath: String, data: Data? = nil, localFile: URL? = nil, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? {
let size = data?.count ?? Int((try? localFile?.resourceValues(forKeys: [.fileSizeKey]))??.fileSize ?? -1)
if size > 150 * 1024 * 1024 {
let error = FileProviderDropboxError(code: .payloadTooLarge, path: targetPath, errorDescription: nil)
completionHandler?(error)
self.delegateNotify(.create(path: targetPath), error: error)
return nil
}
var progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
progress.totalUnitCount = Int64(size)
var requestDictionary = [String: AnyObject]()
let url: URL
url = URL(string: "files/upload", relativeTo: contentURL)!
requestDictionary["path"] = correctPath(targetPath) as NSString?
requestDictionary["mode"] = (overwrite ? "overwrite" : "add") as NSString
requestDictionary["client_modified"] = rfc3339utc(of: modifiedDate) as NSString
requestDictionary["client_modified"] = modifiedDate.rfc3339utc() as NSString
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
request.setValue(String(jsonDictionary: requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
let task = session.uploadTask(with: request, fromFile: localFile, completionHandler: { (data, response, error) in
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .stream)
request.set(dropboxArgKey: requestDictionary)
let task: URLSessionUploadTask
if let data = data {
task = session.uploadTask(with: request, from: data)
} else if let localFile = localFile {
task = session.uploadTask(with: request, fromFile: localFile)
} else {
return nil
}
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { [weak self] error in
var responseError: FileProviderDropboxError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderDropboxError(code: rCode, path: targetPath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
if let code = (task.response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
// We can't fetch server result from delegate!
responseError = FileProviderDropboxError(code: rCode, path: targetPath, errorDescription: nil)
}
if !(responseError == nil && error == nil) {
progress.cancel()
}
completionHandler?(responseError ?? error)
self.delegateNotify(.create(path: targetPath), error: responseError ?? error)
})
self?.delegateNotify(.modify(path: targetPath), error: responseError ?? error)
}
task.taskDescription = operation.json
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent), options: .new, context: &progress)
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return RemoteOperationHandle(operationType: operation, tasks: [task])
return progress
}
func search(_ startPath: String = "", query: String, start: Int = 0, maxResultPerPage: Int = 25, maxResults: Int = -1, foundItem:@escaping ((_ file: DropboxFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) {
func search(_ startPath: String = "", query: String, start: Int = 0, maxResultPerPage: Int = 25, maxResults: Int = -1, progress: Progress, foundItem:@escaping ((_ file: DropboxFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) {
if progress.isCancelled { return }
let url = URL(string: "files/search", relativeTo: apiURL)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .json)
var requestDictionary: [String: AnyObject] = ["path": startPath as NSString]
requestDictionary["query"] = query as NSString
requestDictionary["start"] = start as NSNumber
@@ -206,12 +206,13 @@ internal extension DropboxFileProvider {
for entry in entries {
if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry) {
foundItem(file)
progress.completedUnitCount += 1
}
}
let rstart = json["start"] as? Int
let hasmore = (json["more"] as? NSNumber)?.boolValue ?? false
if hasmore, let rstart = rstart {
self.search(startPath, query: query, start: rstart + entries.count, maxResultPerPage: maxResultPerPage, foundItem: foundItem, completionHandler: completionHandler)
if hasmore && !progress.isCancelled, let rstart = rstart {
self.search(startPath, query: query, start: rstart + entries.count, maxResultPerPage: maxResultPerPage, progress: progress, foundItem: foundItem, completionHandler: completionHandler)
} else {
completionHandler(responseError ?? error)
}
@@ -219,7 +220,11 @@ internal extension DropboxFileProvider {
}
}
completionHandler(responseError ?? error)
})
})
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
}
}
@@ -241,11 +246,11 @@ internal extension DropboxFileProvider {
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))
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 = resolve(dateString: timeTakenStr) {
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)
+88 -47
View File
@@ -12,7 +12,7 @@ import CoreGraphics
import AVFoundation
extension LocalFileProvider: ExtendedFileProvider {
public func thumbnailOfFileSupported(path: String) -> Bool {
open func thumbnailOfFileSupported(path: String) -> Bool {
switch (path as NSString).pathExtension.lowercased() {
case LocalFileInformationGenerator.imageThumbnailExtensions:
return true
@@ -31,7 +31,7 @@ extension LocalFileProvider: ExtendedFileProvider {
}
}
public func propertiesOfFileSupported(path: String) -> Bool {
open func propertiesOfFileSupported(path: String) -> Bool {
let fileExt = (path as NSString).pathExtension.lowercased()
switch fileExt {
case LocalFileInformationGenerator.imagePropertiesExtensions:
@@ -54,7 +54,7 @@ extension LocalFileProvider: ExtendedFileProvider {
}
}
public func thumbnailOfFile(path: String, dimension: CGSize? = nil, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) {
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
@@ -86,7 +86,7 @@ extension LocalFileProvider: ExtendedFileProvider {
}
}
public func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String: Any], _ keys: [String], _ error: Error?) -> Void)) {
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]))?
@@ -197,8 +197,13 @@ public struct LocalFileInformationGenerator {
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 == AVMetadataCommonKeyArtwork {
if item.commonKey == commonKeyArtwork {
if let data = item.dataValue {
return ImageClass(data: data)
}
@@ -319,28 +324,34 @@ public struct LocalFileInformationGenerator {
guard let key = key else {
return nil
}
guard let regex = try? NSRegularExpression(pattern: "([a-z])([A-Z])" , options: NSRegularExpression.Options()) else {
guard let regex = try? NSRegularExpression(pattern: "([a-z])([A-Z])" , options: []) else {
return nil
}
let newKey = regex.stringByReplacingMatches(in: key, options: NSRegularExpression.MatchingOptions(), range: NSMakeRange(0, (key as NSString).length) , withTemplate: "$1 $2")
let newKey = regex.stringByReplacingMatches(in: key, options: [], range: NSRange(location: 0, length: (key as NSString).length) , withTemplate: "$1 $2")
return newKey.capitalized
}
if FileManager.default.fileExists(atPath: fileURL.path) {
let playerItem = AVPlayerItem(url: fileURL)
let metadataList = playerItem.asset.commonMetadata
for item in metadataList {
if let description = makeDescription(item.commonKey) {
if let value = item.stringValue {
keys.append(description)
dic[description] = value
}
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)
}
}
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)
}
@@ -366,7 +377,11 @@ public struct LocalFileInformationGenerator {
}
}
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)
@@ -380,7 +395,11 @@ public struct LocalFileInformationGenerator {
add(key: "Duration", value: TimeInterval(duration).formatshort)
add(key: "Video Bitrate", value: "\(Int(ceil(bitrate / 1000))) kbps")
}
#if swift(>=4.0)
let audioTracks = asset.tracks(withMediaType: AVMediaType.audio)
#else
let audioTracks = asset.tracks(withMediaType: AVMediaTypeAudio)
#endif
// dic["Audio channels"] = audioTracks.count
var bitrate: Float = 0
for track in audioTracks {
@@ -403,21 +422,40 @@ public struct LocalFileInformationGenerator {
}
func getKey(_ key: String, from dict: CGPDFDictionaryRef) -> String? {
var cfValue: CGPDFStringRef? = nil
if (CGPDFDictionaryGetString(dict, key, &cfValue)), let value = CGPDFStringCopyTextString(cfValue!) {
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 }
var dateStr = date
var dateStr = date.replacingOccurrences(of: "'", with: "")
if dateStr.hasPrefix("D:") {
dateStr = date.substring(from: date.characters.index(date.startIndex, offsetBy: 2))
dateStr.characters.removeFirst(2)
}
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMddHHmmssTZD"
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
}
@@ -428,29 +466,32 @@ public struct LocalFileInformationGenerator {
return nil
}
if let provider = CGDataProvider(url: fileURL as CFURL), let reference = CGPDFDocument(provider), let dict = reference.info {
add(key: "Title", value: getKey("Title", from: dict))
add(key: "Author", value: getKey("Author", from: dict))
add(key: "Subject", value: getKey("Subject", 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 ? "Present" : "None")
add(key: "Allows printing", value: reference.allowsPrinting ? "Yes" : "No")
add(key: "Allows copying", value: reference.allowsCopying ? "Yes" : "No")
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)
}
File diff suppressed because it is too large Load Diff
+887
View File
@@ -0,0 +1,887 @@
//
// 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 {
open class var type: String { return "FTP" }
open let baseURL: URL?
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
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!)
}
}
/**
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"
self.baseURL = (urlComponents.url!.path.hasSuffix("/") ? urlComponents.url! : urlComponents.url!.appendingPathComponent("")).absoluteURL
self.passiveMode = passive
self.currentPath = ""
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.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? ""
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.currentPath, forKey: "currentPath")
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.currentPath = self.currentPath
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, `currentPath` value will be used.
- 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.
`contents`: An array of `FileObject` identifying the the directory entries.
`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, `currentPath` value will be used.
- 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)
}
if let error = error {
self.dispatch_queue.async {
completionHandler(nil, error)
}
return
}
guard let response = response, response.hasPrefix("250") || (response.hasPrefix("50") && rfc3659enabled) else {
self.dispatch_queue.async {
completionHandler(nil, self.throwError(path, code: URLError.badServerResponse))
}
return
}
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 {
self.dispatch_queue.async {
completionHandler(nil, self.throwError(path, code: URLError.badServerResponse))
}
return
}
let file = rfc3659enabled ? self.parseMLST(lines[1], in: path) : self.parseUnixList(lines[1], in: path)
self.dispatch_queue.async {
completionHandler(file, nil)
}
})
}
}
open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) {
dispatch_queue.async {
completionHandler(-1, 0)
}
}
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? {
let progress = Progress(parent: nil, userInfo: nil)
_ = 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)
})
return progress
}
public func url(of path: String?) -> URL {
let path = (path ?? self.currentPath).trimmingCharacters(in: CharacterSet(charactersIn: "/ ")).addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? (path ?? self.currentPath)
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!
}
public 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?
}
extension FTPFileProvider: FileProviderOperations {
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)
}
fileprivate func doOperation(_ opType: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? {
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
guard let sourcePath = opType.source else { return nil }
let destPath = opType.destination
let command: String
switch opType {
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: // modify, fetch
return nil
}
let progress = Progress(totalUnitCount: 1)
progress.setUserInfoObject(opType, 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(opType, error: error)
}
return
}
self.execute(command: command, on: task, completionHandler: { (response, error) in
if let error = error {
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
}
return
}
guard let response = response else {
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: self.throwError(sourcePath, code: URLError.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 opType {
case .create:
errorCode = URLError.cannotCreateFile
case .modify:
errorCode = URLError.cannotWriteToFile
case .copy:
self.fallbackCopy(opType, progress: progress, completionHandler: completionHandler)
return
case .move:
errorCode = URLError.cannotMoveFile
case .remove:
self.fallbackRemove(opType, progress: progress, on: task, completionHandler: completionHandler)
return
case .link:
errorCode = URLError.cannotWriteToFile
default:
errorCode = URLError.cannotOpenFile
}
let error = self.throwError(sourcePath, code: errorCode)
progress.cancel()
self.dispatch_queue.async {
completionHandler?(error)
}
self.delegateNotify(opType, error: error)
return
}
progress.completedUnitCount = progress.totalUnitCount
self.dispatch_queue.async {
completionHandler?(nil)
}
self.delegateNotify(opType, error: nil)
})
}
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
return progress
}
private func fallbackCopy(_ opType: FileOperationType, progress: Progress, completionHandler: SimpleCompletionHandler) {
guard let sourcePath = opType.source else { return }
guard let destPath = opType.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(opType, error: error)
}
return
}
progress.becomeCurrent(withPendingUnitCount: 1)
_ = self.copyItem(localFile: localURL, to: destPath) { error in
completionHandler?(nil)
self.delegateNotify(opType, error: nil)
}
progress.resignCurrent()
}
progress.resignCurrent()
return
}
private func fallbackRemove(_ opType: FileOperationType, progress: Progress, on task: FileProviderStreamTask, completionHandler: SimpleCompletionHandler) {
guard let sourcePath = opType.source else { return }
self.execute(command: "SITE RMDIR \(ftpPath(sourcePath))", on: task) { (response, error) in
if let error = error {
progress.cancel()
self.dispatch_queue.async {
completionHandler?(error)
}
self.delegateNotify(opType, error: error)
return
}
guard let response = response else {
progress.cancel()
let error = self.throwError(sourcePath, code: URLError.badServerResponse)
self.dispatch_queue.async {
completionHandler?(error)
}
self.delegateNotify(opType, error: error)
return
}
if response.hasPrefix("50") {
self.fallbackRecursiveRemove(opType, progress: progress, on: task, completionHandler: completionHandler)
return
}
var error: Error?
if !response.hasPrefix("2") {
error = self.throwError(sourcePath, code: URLError.cannotRemoveFile)
}
self.dispatch_queue.async {
completionHandler?(error)
}
self.delegateNotify(opType, error: error)
}
}
private func fallbackRecursiveRemove(_ opType: FileOperationType, progress: Progress, on task: FileProviderStreamTask, completionHandler: SimpleCompletionHandler) {
guard let sourcePath = opType.source else { return }
_ = self.recursiveList(path: sourcePath, useMLST: true, completionHandler: { (contents, error) in
if let error = error {
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
}
return
}
let recursiveProgress = Progress(parent: progress, userInfo: nil)
recursiveProgress.totalUnitCount = Int64(contents.count)
let sortedContents = contents.sorted(by: {
$0.path.localizedStandardCompare($1.path) == .orderedDescending
})
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(opType, error: error)
}
// TODO: Digest response
})
})
}
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.throwError(localFile.path, code: URLError.fileIsDirectory))
}
return nil
}
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
let progress = Progress(totalUnitCount: 0)
progress.setUserInfoObject(opType, 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(opType, 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
DispatchQueue.main.async {
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(progress.fractionCompleted))
}
}, completionHandler: { (error) in
if error != nil {
progress.cancel()
}
self.ftpQuit(task)
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
}
})
}
return progress
}
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.copy(source: path, destination: destURL.absoluteString)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
var progress = Progress(totalUnitCount: 0)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
if self.useAppleImplementation {
self.attributesOfItem(path: path, completionHandler: { (file, error) in
if let error = error {
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
}
return
}
if file?.isDirectory ?? false {
self.dispatch_queue.async {
let error = self.throwError(path, code: URLError.fileIsDirectory)
completionHandler?(error)
self.delegateNotify(opType, 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
do {
try FileManager.default.moveItem(at: tempURL, to: destURL)
completionHandler?(nil)
} catch let e {
completionHandler?(e)
}
}
task.taskDescription = opType.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
DispatchQueue.main.async {
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(progress.fractionCompleted))
}
}) { (tmpurl, error) in
if let error = error {
progress.cancel()
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
}
return
}
if let tmpurl = tmpurl {
try? FileManager.default.moveItem(at: tmpurl, to: destURL)
self.dispatch_queue.async {
completionHandler?(nil)
self.delegateNotify(opType, error: nil)
}
}
}
}
}
return progress
}
}
extension FTPFileProvider: FileProviderReadWrite {
open func contents(path: String, completionHandler: @escaping ((Data?, Error?) -> Void)) -> Progress? {
let opType = FileOperationType.fetch(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
if self.useAppleImplementation {
var progress = Progress(totalUnitCount: 0)
progress.setUserInfoObject(opType, 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 let e {
completionHandler(nil, e)
}
}
task.taskDescription = opType.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 opType = FileOperationType.fetch(path: path)
if length == 0 || offset < 0 {
dispatch_queue.async {
completionHandler(Data(), nil)
self.delegateNotify(opType, error: nil)
}
return nil
}
let progress = Progress(totalUnitCount: 0)
progress.setUserInfoObject(opType, 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
DispatchQueue.main.async {
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(progress.fractionCompleted))
}
}) { (data, error) in
if let error = error {
progress.cancel()
self.dispatch_queue.async {
completionHandler(nil, error)
self.delegateNotify(opType, error: error)
}
return
}
if let data = data {
self.dispatch_queue.async {
completionHandler(data, nil)
self.delegateNotify(opType, error: nil)
}
}
}
}
return progress
}
open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.modify(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
let progress = Progress(totalUnitCount: Int64(data?.count ?? 0))
progress.setUserInfoObject(opType, 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(opType, 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
DispatchQueue.main.async {
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(progress.fractionCompleted))
}
}, completionHandler: { (error) in
if error != nil {
progress.cancel()
}
self.ftpQuit(task)
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
}
})
}
if overwrite {
storeHandler()
} else {
self.attributesOfItem(path: path, completionHandler: { (file, erroe) in
if file == nil {
storeHandler()
}
})
}
}
return progress
}
}
public extension FTPFileProvider {
/**
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.
*/
public func create(symbolicLink path: String, withDestinationPath destPath: String, completionHandler: SimpleCompletionHandler) {
let opType = FileOperationType.link(link: path, target: destPath)
_=self.doOperation(opType, completionHandler: completionHandler)
}
}
extension FTPFileProvider: FileProvider { }
+965
View File
@@ -0,0 +1,965 @@
//
// FTPHelper.swift
// FileProvider
//
// Created by Amir Abbas Mousavian.
// Copyright © 2017 Mousavian. Distributed under MIT license.
//
import Foundation
internal extension FTPFileProvider {
func delegateNotify(_ operation: FileOperationType, error: Error?) {
DispatchQueue.main.async(execute: {
if error == nil {
self.delegate?.fileproviderSucceed(self, operation: operation)
} else {
self.delegate?.fileproviderFailed(self, operation: operation)
}
})
}
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.throwError("", code: URLError.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
if let error = error {
completionHandler(error)
return
}
guard let data = data, let response = String(data: data, encoding: .utf8) else {
completionHandler(self.throwError("", code: URLError.cannotParseResponse))
return
}
guard response.hasPrefix("22") else {
let error = FileProviderFTPError(message: response)
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.throwError("", code: URLError.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.throwError("", code: URLError.userAuthenticationRequired))
}
}
return
}
let error = FileProviderFTPError(message: response)
completionHandler(error)
return
}
}
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
if let error = error {
completionHandler(error)
return
}
guard let response = response else {
completionHandler(self.throwError(path, code: URLError.badServerResponse))
return
}
// successfully logged in
if response.hasPrefix("25") {
completionHandler(nil)
}
// not logged in
else if response.hasPrefix("55") {
let error = FileProviderFTPError(message: response)
completionHandler(error)
return
}
}
}
func ftpPassive(_ task: FileProviderStreamTask, completionHandler: @escaping (_ dataTask: FileProviderStreamTask?, _ error: Error?) -> Void) {
func trimmedNumber(_ s : String) -> String {
let characterSet = Set("+*#0123456789".characters)
return String(s.characters.lazy.filter(characterSet.contains))
}
self.execute(command: "PASV", on: task) { (response, error) in
if let error = error {
completionHandler(nil, error)
return
}
guard let response = response, let destString = response.components(separatedBy: " ").flatMap({ $0 }).last.flatMap({ String($0) }) else {
completionHandler(nil, self.throwError("", code: URLError.badServerResponse))
return
}
let destArray = destString.components(separatedBy: ",").flatMap({ UInt32(trimmedNumber($0)) })
guard destArray.count == 6 else {
completionHandler(nil, self.throwError("", code: URLError.badServerResponse))
return
}
// first 4 elements are ip, 2 next are port, as byte
var host = destArray.prefix(4).flatMap({ String($0) }).joined(separator: ".")
let port = Int(destArray[4] << 8 + destArray[5])
// 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)
}
}
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
if let error = error {
activeTask.cancel()
completionHandler(nil, error)
return
}
guard let response = response else {
activeTask.cancel()
completionHandler(nil, self.throwError("", code: URLError.badServerResponse))
return
}
guard !response.hasPrefix("5") else {
activeTask.cancel()
completionHandler(nil, self.throwError("", code: URLError.cannotConnectToHost))
return
}
completionHandler(activeTask, nil)
}
}
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
if let error = error {
completionHandler(error)
return
}
// Successful
guard let response = response else {
completionHandler(self.throwError("", code: URLError.badServerResponse))
return
}
if response.hasPrefix("35") {
completionHandler(nil)
} else {
let error = FileProviderFTPError(message: response, path: "")
completionHandler(error)
return
}
}
}
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.throwError(path, code: URLError.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?
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) {
completionHandler([], error)
}
return
}
if waitResult == .timedOut {
completionHandler([], self.throwError(path, code: URLError.timedOut))
return
}
}
guard let response = String(data: finalData, encoding: .utf8) else {
completionHandler([], self.throwError(path, code: URLError.badServerResponse))
return
}
let contents: [String] = response.components(separatedBy: "\n").flatMap({ $0.trimmingCharacters(in: .whitespacesAndNewlines) })
success = true
completionHandler(contents, nil)
return
}
}) { (response, error) in
if let error = error {
completionHandler([], error)
return
}
guard let response = response else {
completionHandler([], self.throwError(path, code: URLError.cannotParseResponse))
return
}
if response.hasPrefix("500") && useMLST {
dataTask.cancel()
self.serverSupportsRFC3659 = false
completionHandler([], self.throwError(path, code: URLError.unsupportedURL))
return
}
if !success && !(response.hasPrefix("25") || response.hasPrefix("15")) {
let error = FileProviderFTPError(message: response, path: path)
self.dispatch_queue.async {
completionHandler([], error)
}
return
}
}
}
}
func recursiveList(path: String, useMLST: Bool, foundItemsHandler: ((_ contents: [FileObject]) -> Void)? = nil, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) -> Progress? {
let progress = Progress(totalUnitCount: 0)
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: .urlPathAllowed) ?? 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.throwError(filePath, code: URLError.badServerResponse))
return
}
// Send retreive command
let len = 19 /* TYPE response */ + 65 + String(position).characters.count /* REST Response */ + 53 + filePath.characters.count + String(totalSize).characters.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.throwError(filePath, code: URLError.timedOut))
return
}
}
if let url = URL(string: filePath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? 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
if let error = error {
completionHandler(nil, error)
return
}
guard let response = response else {
completionHandler(nil, self.throwError(filePath, code: URLError.cannotParseResponse))
return
}
if !(response.hasPrefix("1") || !response.hasPrefix("2")) {
let error = FileProviderFTPError(message: response)
self.dispatch_queue.async {
completionHandler(nil, error)
}
return
}
}
}
}
}
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: .urlPathAllowed) ?? 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.throwError(filePath, code: URLError.badServerResponse))
return
}
// Send retreive command
let len = 19 /* TYPE response */ + 65 + String(position).characters.count /* REST Response */ + 53 + filePath.characters.count + String(totalSize).characters.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.throwError("", code: URLError.timedOut)
completionHandler(nil, error)
return
}
}
if let url = URL(string: filePath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? 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)
} catch {
completionHandler(nil, error)
}
try? FileManager.default.removeItem(at: tempURL)
}
return
}
}) { (response, error) in
if let error = error {
completionHandler(nil, error)
return
}
guard let response = response else {
completionHandler(nil, self.throwError(filePath, code: URLError.cannotParseResponse))
return
}
if !(response.hasPrefix("1") || response.hasPrefix("2")) {
let error = FileProviderFTPError(message: response)
self.dispatch_queue.async {
completionHandler(nil, error)
}
return
}
}
}
}
}
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, page size is 32KB
case 262_144..<1_048_576: chunkSize = 65_536 // 256KB To 1MB, page size is 64KB
case 1_048_576..<10_485_760: chunkSize = 131_072 // 1MB To 10MB, page size is 128KB
case 10_048_576..<33_554_432: chunkSize = 262_144 // 1MB To 10MB, page size is 256KB
default: chunkSize = 524_288 // Larger than 32MB, page 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 waitResult == .timedOut {
error = self.throwError(filePath, code: URLError.timedOut)
completionHandler(error)
return
}
if let error = error {
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.throwError(filePath, code: URLError.badServerResponse))
return
}
// Send retreive command
var success = false
let len = 19 /* TYPE response */ + 65 + String(position).characters.count /* REST Response */ + 44 + filePath.characters.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
if self.baseURL?.scheme == "ftps" || self.baseURL?.port == 990 {
task.startSecureConnection()
}
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 }
if let error = error {
completionHandler(error)
return
}
guard let response = response else {
completionHandler(self.throwError(filePath, code: URLError.cannotParseResponse))
return
}
if !(response.hasPrefix("1") || response.hasPrefix("2")) {
let error = FileProviderFTPError(message: response)
self.dispatch_queue.async {
completionHandler(error)
}
return
}
completionHandler(nil)
}
}
}
func ftpQuit(_ task: FileProviderStreamTask) {
self.execute(command: "QUIT", on: task) { (_, _) in
//task.closeRead()
//task.closeWrite()
}
}
func ftpPath(_ apath: String) -> String {
var path = apath.isEmpty ? self.currentPath : apath
// path of base url should be concreted into file path!
path = baseURL!.appendingPathComponent(path).path
// Fixing slashes
if !path.hasPrefix("/") {
path = "/" + path
}
if path.hasSuffix("/"){
path.characters.removeLast()
}
if path.isEmpty {
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 }
var path = (path as NSString).appendingPathComponent(name)
if path.hasPrefix("/") {
path.characters.removeFirst()
}
let file = FileObject(url: url(of: path), name: name, path: path)
switch String(posixPermission.characters.first!) {
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)
}
if correctedPath.hasPrefix("/") {
correctedPath.characters.removeFirst()
}
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"
let spaceIndex = message.characters.index(of: "-") ?? message.characters.index(of: " ") ?? message.startIndex
self.code = Int(message.substring(to: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
self.path = path
if code > 0 {
self.errorDescription = message.substring(from: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)
} else {
self.errorDescription = message
}
}
}
+27 -49
View File
@@ -13,31 +13,32 @@ open class FileObject: Equatable {
/// A `Dictionary` contains file information, using `URLResourceKey` keys.
open internal(set) var allValues: [URLResourceKey: Any]
internal init(allValues: [URLResourceKey: Any]) {
public init(allValues: [URLResourceKey: Any]) {
self.allValues = allValues
}
internal init(url: URL, name: String, path: String) {
internal init(url: URL?, name: String, path: String) {
self.allValues = [URLResourceKey: Any]()
self.url = url
if let url = url {
self.url = url
}
self.name = name
self.path = path
}
/// url to access the resource, not supported by Dropbox provider
@available(*, obsoleted: 1.0, renamed: "url", message: "Use url.absoluteURL instead.")
open var absoluteURL: URL? {
return url?.absoluteURL
}
/// URL to access the resource, can be a relative URL against base URL.
/// not supported by Dropbox provider.
open internal(set) var url: URL? {
open internal(set) var url: URL {
get {
return allValues[.fileURL] as? URL
if let url = allValues[.fileURLKey] as? URL {
return url
} else {
let path = self.path.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? self.path
return URL(string: path) ?? URL(string: "/")!
}
}
set {
allValues[.fileURL] = newValue
allValues[.fileURLKey] = newValue
}
}
@@ -139,21 +140,28 @@ open class FileObject: Equatable {
/// Check `FileObject` equality
public static func ==(lhs: FileObject, rhs: FileObject) -> Bool {
if rhs === lhs {
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
}
if let rurl = rhs.url, let lurl = lhs.url {
#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] = [.fileURL: "url", .nameKey: "name", .pathKey: "path", .fileSizeKey: "filesize", .creationDateKey: "creationDate",
.contentModificationDateKey: "modifiedDate", .isHiddenKey: "isHidden", .isWritableKey: "isWritable", .serverDate: "serverDate", .entryTag: "entryTag", .mimeType: "mimeType"]
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 {
@@ -170,10 +178,11 @@ open class FileObject: Equatable {
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: .fileURL, NSMetadataItemFSNameKey: .nameKey, NSMetadataItemPathKey: .pathKey,
let mapDict: [String: URLResourceKey] = [NSMetadataItemURLKey: .fileURLKey, NSMetadataItemFSNameKey: .nameKey, NSMetadataItemPathKey: .pathKey,
NSMetadataItemFSSizeKey: .fileSizeKey, NSMetadataItemFSCreationDateKey: .creationDateKey,
NSMetadataItemFSContentChangeDateKey: .contentModificationDateKey, "kMDItemFSInvisible": .isHiddenKey, "kMDItemFSIsWriteable": .isWritableKey, "kMDItemKind": .mimeType]
NSMetadataItemFSContentChangeDateKey: .contentModificationDateKey, "kMDItemFSInvisible": .isHiddenKey, "kMDItemFSIsWriteable": .isWritableKey, "kMDItemKind": .mimeTypeKey]
if let cQuery = query as? NSCompoundPredicate {
let newSub = cQuery.subpredicates.map { convertPredicate(fromSpotlight: $0 as! NSPredicate) }
@@ -198,37 +207,6 @@ open class FileObject: Equatable {
}
}
internal func resolve(dateString: String) -> Date? {
let dateFor: DateFormatter = DateFormatter()
dateFor.locale = Locale(identifier: "en_US")
dateFor.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ssZ"
if let rfc3339 = dateFor.date(from: dateString) {
return rfc3339
}
dateFor.dateFormat = "EEE',' dd' 'MMM' 'yyyy HH':'mm':'ss z"
if let rfc1123 = dateFor.date(from: dateString) {
return rfc1123
}
dateFor.dateFormat = "EEEE',' dd'-'MMM'-'yy HH':'mm':'ss z"
if let rfc850 = dateFor.date(from: dateString) {
return rfc850
}
dateFor.dateFormat = "EEE MMM d HH':'mm':'ss yyyy"
if let asctime = dateFor.date(from: dateString) {
return asctime
}
return nil
}
internal func rfc3339utc(of date:Date) -> String {
let fm = DateFormatter()
fm.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
fm.timeZone = TimeZone(identifier:"UTC")
fm.locale = Locale(identifier:"en_US_POSIX")
return fm.string(from:date)
}
/// Sorting FileObject array by given criteria, **not thread-safe**
public struct FileObjectSorting {
+158 -126
View File
@@ -19,7 +19,7 @@ public typealias ImageClass = NSImage
public typealias SimpleCompletionHandler = ((_ error: Error?) -> Void)?
/// This protocol defines FileProvider neccesary functions and properties to connect and get contents list
public protocol FileProviderBasic: class, NSCoding, NSSecureCoding {
public protocol FileProviderBasic: class, NSSecureCoding {
/// An string to identify type of provider.
static var type: String { get }
@@ -51,13 +51,13 @@ public protocol FileProviderBasic: class, NSCoding, NSSecureCoding {
**Example initialization:**
````
let credential = URLCredential(user: "user", password: "password", persistence: .forSeession)
provider.credential = URLCredential(user: "user", password: "password", persistence: .forSeession)
````
- Note: In OAuth based providers like `DropboxFileProvider` and `OneDriveFileProvider`, password is Token.
use [OAuthSwift](https://github.com/OAuthSwift/OAuthSwift) library to fetch clientId and Token of user.
*/
var credential: URLCredential? { get }
var credential: URLCredential? { get set }
/**
Returns an Array of `FileObject`s identifying the the directory entries via asynchronous completion handler.
@@ -66,8 +66,8 @@ public protocol FileProviderBasic: class, NSCoding, NSSecureCoding {
- Parameter path: path to target directory. If empty, `currentPath` value will be used.
- Parameter 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.
- `contents`: An array of `FileObject` identifying the the directory entries.
- `error`: Error returned by system.
*/
func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void))
@@ -78,8 +78,8 @@ public protocol FileProviderBasic: class, NSCoding, NSSecureCoding {
- Parameter path: path to target directory. If empty, `currentPath` value will be used.
- 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.
- `attributes`: A `FileObject` containing the attributes of the item.
- `error`: Error returned by system.
*/
func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void))
@@ -99,7 +99,8 @@ public protocol FileProviderBasic: class, NSCoding, NSSecureCoding {
- 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.
*/
func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void))
@discardableResult
func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress?
/**
Search files inside directory using query asynchronously.
@@ -121,8 +122,10 @@ public protocol FileProviderBasic: class, NSCoding, NSSecureCoding {
- 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.
- Returns: An `Progress` to get progress or cancel progress.
*/
func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void))
@discardableResult
func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress?
/**
Returns an independent url to access the file. Some providers like `Dropbox` due to their nature.
@@ -132,14 +135,23 @@ public protocol FileProviderBasic: class, NSCoding, NSSecureCoding {
*/
func url(of path: String?) -> URL
/// Returns the relative path of url, wothout percent encoding. Even if url is absolute or
/// retrieved from another provider, it will try to resolve the url against `baseURL` of
/// current provider. It's highly recomended to use this method for displaying purposes.
///
/// - Parameter url: Absolute url to file or directory.
/// - Returns: A `String` contains relative path of url against base url.
func relativePathOf(url: URL) -> String
/// Checks the connection to server or permission on local
func isReachable(completionHandler: @escaping(_ success: Bool) -> Void)
}
extension FileProviderBasic {
public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? {
let predicate = NSPredicate(format: "name BEGINSWITH[c] %@", query)
self.searchFiles(path: path, recursive: recursive, query: predicate, foundItemHandler: foundItemHandler, completionHandler: completionHandler)
return self.searchFiles(path: path, recursive: recursive, query: predicate, foundItemHandler: foundItemHandler, completionHandler: completionHandler)
}
/// The maximum number of queued operations that can execute at the same time.
@@ -153,8 +165,6 @@ extension FileProviderBasic {
operation_queue.maxConcurrentOperationCount = newValue
}
}
}
/// Checking equality of two file provider, regardless of current path queues and delegates.
@@ -195,7 +205,13 @@ public protocol FileProviderBasicRemote: FileProviderBasic {
var validatingCache: Bool { get set }
}
internal extension FileProviderBasicRemote {
internal protocol FileProviderBasicRemoteInternal: FileProviderBasic {
var completionHandlersForTasks: [Int: SimpleCompletionHandler] { get set }
var downloadCompletionHandlersForTasks: [Int: (URL) -> Void] { get set }
var dataCompletionHandlersForTasks: [Int: (Data) -> Void] { get set }
}
internal extension FileProviderBasicRemote {
func returnCachedDate(with request: URLRequest, validatingCache: Bool, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Swift.Void) -> Bool {
guard let cache = self.cache else { return false }
if let response = cache.cachedResponse(for: request) {
@@ -226,7 +242,7 @@ internal extension FileProviderBasicRemote {
return false
}
func runDataTask(with request: URLRequest, operationHandle: RemoteOperationHandle? = nil, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Swift.Void) {
func runDataTask(with request: URLRequest, operation: FileOperationType? = nil, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Swift.Void) {
let useCache = self.useCache
let validatingCache = self.validatingCache
dispatch_queue.async {
@@ -236,8 +252,7 @@ internal extension FileProviderBasicRemote {
}
}
let task = self.session.dataTask(with: request, completionHandler: completionHandler)
task.taskDescription = operationHandle?.operationType.json
operationHandle?.add(task: task)
task.taskDescription = operation?.json
task.resume()
}
}
@@ -256,24 +271,10 @@ public protocol FileProviderOperations: FileProviderBasic {
- folder: Directory name.
- at: Parent path of new directory.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func create(folder: String, at: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
Creates an new file with data passed to method asynchronously.
Returns error via completionHandler if file is already exists.
- Parameters:
- file: New file name with extension separated by period.
- at: Parent path of new file.
- data: Data of new files. Pass nil or `Data()` to create empty file.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func create(file: String, at: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func create(folder: String, at: String, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Moves a file or directory from `path` to designated path asynchronously.
@@ -284,10 +285,10 @@ public protocol FileProviderOperations: FileProviderBasic {
- path: original file or directory path.
- to: destination path of file or directory, including file/directory name.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func moveItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func moveItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Moves a file or directory from `path` to designated path asynchronously.
@@ -299,10 +300,10 @@ public protocol FileProviderOperations: FileProviderBasic {
- to: destination path of file or directory, including file/directory name.
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func moveItem(path: String, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func moveItem(path: String, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Copies a file or directory from `path` to designated path asynchronously.
@@ -313,10 +314,10 @@ public protocol FileProviderOperations: FileProviderBasic {
- path: original file or directory path.
- to: destination path of file or directory, including file/directory name.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func copyItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func copyItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Copies a file or directory from `path` to designated path asynchronously.
@@ -328,10 +329,10 @@ public protocol FileProviderOperations: FileProviderBasic {
- to: destination path of file or directory, including file/directory name.
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func copyItem(path: String, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func copyItem(path: String, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Removes the file or directory at the specified path.
@@ -339,66 +340,80 @@ public protocol FileProviderOperations: FileProviderBasic {
- Parameters:
- path: file or directory path.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Uploads a file from local file url to designated path asynchronously.
Method will fail if source is not a local url with `file://` scheme.
- Note: It's safe to assume that this method only works on individual files and **won't** copy folders recursively.
- Parameters:
- localFile: a file url to file.
- to: destination path of file, including file/directory name.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress.
- Returns: An `Progress` to get progress or cancel progress.
*/
@discardableResult
func copyItem(localFile: URL, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func copyItem(localFile: URL, to: String, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Uploads a file from local file url to designated path asynchronously.
Method will fail if source is not a local url with `file://` scheme.
- Note: It's safe to assume that this method only works on individual files and **won't** copy folders recursively.
- Parameters:
- localFile: a file url to file.
- to: destination path of file, including file/directory name.
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress.
- Returns: An `Progress` to get progress or cancel progress.
*/
@discardableResult
func copyItem(localFile: URL, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func copyItem(localFile: URL, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> 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.
- Note: It's safe to assume that this method only works on individual files and **won't** copy folders recursively.
- 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: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> Progress?
}
extension FileProviderOperations {
public extension FileProviderOperations {
/// *DEPRECATED:* Use Use FileProviderReadWrite.writeContents(path:, data:, completionHandler:) method instead.
@available(*, deprecated, message: "Use FileProviderReadWrite.writeContents(path:, data:, completionHandler:) method instead.")
@discardableResult
public func moveItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
public func create(file: String, at: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> Progress? {
let path = (at as NSString).appendingPathComponent(file)
return (self as? FileProviderReadWrite)?.writeContents(path: path, contents: data, completionHandler: completionHandler)
}
@discardableResult
public func moveItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> Progress? {
return self.moveItem(path: path, to: to, overwrite: false, completionHandler: completionHandler)
}
@discardableResult
public func copyItem(localFile: URL, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
public func copyItem(localFile: URL, to: String, completionHandler: SimpleCompletionHandler) -> Progress? {
return self.copyItem(localFile: localFile, to: to, overwrite: false, completionHandler: completionHandler)
}
@discardableResult
public func copyItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
public func copyItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> Progress? {
return self.copyItem(path: path, to: to, overwrite: false, completionHandler: completionHandler)
}
}
@@ -412,12 +427,12 @@ public protocol FileProviderReadWrite: FileProviderBasic {
- 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: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- `contents`: contents of file in a `Data` object.
- `error`: Error returned by system.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle?
func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress?
/**
Retreives a `Data` object with a portion contents of the file asynchronously vis contents argument of completion handler.
@@ -428,12 +443,12 @@ public protocol FileProviderReadWrite: FileProviderBasic {
- 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: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- `contents`: contents of file in a `Data` object.
- `error`: Error returned by system.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle?
func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress?
/**
Write the contents of the `Data` to a location asynchronously.
@@ -442,12 +457,12 @@ public protocol FileProviderReadWrite: FileProviderBasic {
- Parameters:
- path: Path of target file.
- contents: Data to be written into file.
- contents: Data to be written into file, pass nil to create empty file.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func writeContents(path: String, contents: Data, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func writeContents(path: String, contents: Data?, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Write the contents of the `Data` to a location asynchronously.
@@ -455,13 +470,13 @@ public protocol FileProviderReadWrite: FileProviderBasic {
- Parameters:
- path: Path of target file.
- contents: Data to be written into file.
- contents: Data to be written into file, pass nil to create empty file.
- 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: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func writeContents(path: String, contents: Data, atomically: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func writeContents(path: String, contents: Data?, atomically: Bool, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Write the contents of the `Data` to a location asynchronously.
@@ -469,47 +484,47 @@ public protocol FileProviderReadWrite: FileProviderBasic {
- Parameters:
- path: Path of target file.
- contents: Data to be written into file.
- contents: Data to be written into file, pass nil to create empty file.
- overwrite: Destination file should be overwritten if file is already exists. Default is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func writeContents(path: String, contents: Data, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func writeContents(path: String, contents: Data?, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Write the contents of the `Data` to a location asynchronously.
- Parameters:
- path: Path of target file.
- contents: Data to be written into file.
- contents: Data to be written into file, pass nil to create empty 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: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func writeContents(path: String, contents: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func writeContents(path: String, contents: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress?
}
extension FileProviderReadWrite {
@discardableResult
public func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle?{
public func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
return self.contents(path: path, offset: 0, length: -1, completionHandler: completionHandler)
}
@discardableResult
public func writeContents(path: String, contents: Data, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
public func writeContents(path: String, contents: Data?, completionHandler: SimpleCompletionHandler) -> Progress? {
return self.writeContents(path: path, contents: contents, atomically: false, overwrite: false, completionHandler: completionHandler)
}
@discardableResult
public func writeContents(path: String, contents: Data, atomically: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
public func writeContents(path: String, contents: Data?, atomically: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
return self.writeContents(path: path, contents: contents, atomically: atomically, overwrite: false, completionHandler: completionHandler)
}
@discardableResult
public func writeContents(path: String, contents: Data, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
public func writeContents(path: String, contents: Data?, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
return self.writeContents(path: path, contents: contents, atomically: false, overwrite: overwrite, completionHandler: completionHandler)
}
}
@@ -555,7 +570,7 @@ public protocol FileProvideUndoable: FileProviderOperations {
var undoManager: UndoManager? { get set }
/// UndoManager supports undoing this file operation
func canUndo(handle: OperationHandle) -> Bool
func canUndo(handle: Progress) -> Bool
/// UndoManager supports undoing this operation
func canUndo(operation: FileOperationType) -> Bool
}
@@ -565,8 +580,11 @@ public extension FileProvideUndoable {
return undoOperation(for: operation) != nil
}
public func canUndo(handle: OperationHandle) -> Bool {
return canUndo(operation: handle.operationType)
public func canUndo(handle: Progress) -> Bool {
if let operationType = handle.userInfo[.fileProvderOperationTypeKey] as? FileOperationType {
return canUndo(operation: operationType)
}
return false
}
internal func undoOperation(for operation: FileOperationType) -> FileOperationType? {
@@ -596,21 +614,39 @@ public extension FileProvideUndoable {
}
}
/// This protocol defines method to share a public link with other users
public protocol FileProviderSharing {
/**
Genrates a public url to a file to be shared with other users and can be downloaded without authentication.
- Important: In some providers url will be available for a limitied time, determined in `expiration` argument.
e.g. Dropbox links will be expired after 4 hours.
- Parameters:
- to: path of file, including file/directory name.
- completionHandler: a closure with result of directory entries or error.
- `link`: a url returned by Dropbox to share.
- `attribute`: a `FileObject` containing the attributes of the item.
- `expiration`: a `Date` object, determines when the public url will expires.
- `error`: Error returned by server.
*/
func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void))
}
/// Defines protocol for provider allows all common operations.
public protocol FileProvider: FileProviderBasic, FileProviderOperations, FileProviderReadWrite, NSCopying {
public protocol FileProvider: FileProviderOperations, FileProviderReadWrite, NSCopying {
}
internal let pathTrimSet = CharacterSet(charactersIn: " /")
extension FileProviderBasic {
public var type: String {
#if swift(>=3.1)
return Swift.type(of: self).type
#else
return type(of: self).type
#endif
}
/// **OBSOLETED** This property never worked as expected and is redundant as only supported by `LocalFileProvider`.
/// To simulate `false` value, assign `URL(fileURLWithPath: "/")` to `baseURL`.
@available(*, obsoleted: 1.0, message: "Redundant property, now is always true.")
var isPathRelative: Bool { return true }
public func url(of path: String? = nil) -> URL {
var rpath: String = path ?? self.currentPath
rpath = rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? rpath
@@ -624,26 +660,29 @@ extension FileProviderBasic {
}
}
/// Returns the relative path of url, wothout percent encoding. Even if url is absolute or
/// retrieved from another provider, it will try to resolve the url against `baseURL` of
/// current provider. It's highly recomended to use this method for displaying purposes.
///
/// - Parameter url: Absolute url to file or directory.
/// - Returns: A `String` contains relative path of url against base url.
public 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
return (relativePath.removingPercentEncoding ?? relativePath).replacingOccurrences(of: "/", with: "", options: .anchored)
}
// resolve url string against baseurl
guard let baseURL = self.baseURL?.standardizedFileURL else { return url.absoluteString }
let standardPath = url.absoluteString.replacingOccurrences(of: "file:///private/var/", with: "file:///var/", options: .anchored)
let standardBase = baseURL.absoluteString.replacingOccurrences(of: "file:///private/var/", with: "file:///var/", options: .anchored)
let standardRelativePath = standardPath.replacingOccurrences(of: standardBase, with: "/")
return standardRelativePath.removingPercentEncoding ?? standardRelativePath
if baseURL?.isFileURL ?? false {
guard let baseURL = self.baseURL?.standardizedFileURL else { return url.absoluteString }
let standardPath = url.absoluteString.replacingOccurrences(of: "file:///private/var/", with: "file:///var/", options: .anchored)
let standardBase = baseURL.absoluteString.replacingOccurrences(of: "file:///private/var/", with: "file:///var/", options: .anchored)
let standardRelativePath = standardPath.replacingOccurrences(of: standardBase, with: "/").replacingOccurrences(of: "/", with: "", options: .anchored)
return standardRelativePath.removingPercentEncoding ?? standardRelativePath
} else {
guard let baseURL = self.baseURL else { return url.absoluteString }
let standardRelativePath = url.absoluteString.replacingOccurrences(of: baseURL.absoluteString, with: "/").replacingOccurrences(of: "/", with: "", options: .anchored)
if URLComponents(string: standardRelativePath)?.host?.isEmpty ?? true {
return standardRelativePath.removingPercentEncoding ?? standardRelativePath
} else {
return relativePath.replacingOccurrences(of: "/", with: "", options: .anchored)
}
}
}
internal func correctPath(_ path: String?) -> String? {
@@ -678,7 +717,7 @@ extension FileProviderBasic {
}
var i = number ?? 2
let similiar = contents.map {
$0.url?.lastPathComponent ?? $0.name
$0.url.lastPathComponent.isEmpty ? $0.name : $0.url.lastPathComponent
}.filter {
$0.hasPrefix(result)
}
@@ -698,6 +737,8 @@ extension FileProviderBasic {
let domain: String
switch code {
case is URLError:
fallthrough
case is URLError.Code:
domain = NSURLErrorDomain
default:
domain = NSCocoaErrorDomain
@@ -732,8 +773,8 @@ public protocol ExtendedFileProvider: FileProviderBasic {
- Parameters:
- path: path of file.
- completionHandler: a closure with result of preview image or error.
`image`: `NSImage`/`UIImage` object contains preview.
`error`: Error returned by system.
- `image`: `NSImage`/`UIImage` object contains preview.
- `error`: Error returned by system.
*/
func thumbnailOfFile(path: String, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void))
@@ -748,8 +789,8 @@ public protocol ExtendedFileProvider: FileProviderBasic {
- path: path of file.
- dimension: width and height of result preview image.
- completionHandler: a closure with result of preview image or error.
`image`: `NSImage`/`UIImage` object contains preview.
`error`: Error returned by system.
- `image`: `NSImage`/`UIImage` object contains preview.
- `error`: Error returned by system.
*/
func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void))
@@ -763,9 +804,9 @@ public protocol ExtendedFileProvider: FileProviderBasic {
- Parameters:
- path: path of file.
- completionHandler: a closure with result of preview image or error.
`propertiesDictionary`: A `Dictionary` of proprty keys and values.
`keys`: An `Array` contains ordering of keys.
`error`: Error returned by system.
- `propertiesDictionary`: A `Dictionary` of proprty keys and values.
- `keys`: An `Array` contains ordering of keys.
- `error`: Error returned by system.
*/
func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String: Any], _ keys: [String], _ error: Error?) -> Void))
}
@@ -829,19 +870,20 @@ extension ExtendedFileProvider {
return resultingImage
#else
let ppp = Int(UIScreen.main.scale) // fetch device is retina or not
guard let context = UIGraphicsGetCurrentContext() else {
return nil
}
size.width *= CGFloat(ppp)
size.height *= CGFloat(ppp)
UIGraphicsBeginImageContext(size)
guard let context = UIGraphicsGetCurrentContext() else {
return nil
}
context.saveGState()
let transform = pdfPage.getDrawingTransform(CGPDFBox.mediaBox, rect: rect, rotate: 0, preserveAspectRatio: true)
context.concatenate(transform)
context.translateBy(x: 0, y: size.height)
context.scaleBy(x: CGFloat(ppp), y: CGFloat(-ppp))
context.setFillColor(UIColor.white.cgColor)
context.fill(rect)
context.drawPDFPage(pdfPage)
context.restoreGState()
@@ -938,6 +980,8 @@ public enum FileOperationType: CustomStringConvertible {
}
let dest = json["dest"] as? String
switch type {
case "Fetch":
self = .fetch(path: source)
case "Create":
self = .create(path: source)
case "Modify":
@@ -967,6 +1011,7 @@ public enum FileOperationType: CustomStringConvertible {
}
/// Allows to get progress or cancel an in-progress operation, useful for remote providers
@available(*, obsoleted: 1.0, message: "Use Progress class class instead.")
public protocol OperationHandle {
/// Operation supposed to be done on files. Contains file paths as associated value.
var operationType: FileOperationType { get }
@@ -987,14 +1032,6 @@ public protocol OperationHandle {
func cancel() -> Bool
}
public extension OperationHandle {
public var progress: Float {
let bytesSoFar = self.bytesSoFar
let totalBytes = self.totalBytes
return totalBytes > 0 ? Float(Double(bytesSoFar) / Double(totalBytes)) : Float.nan
}
}
/// Delegate methods for reporting provider's operation result and progress, when it's ready to update
/// user interface.
/// All methods are called in main thread to avoids UI bugs.
@@ -1021,16 +1058,11 @@ public protocol FileOperationDelegate: class {
func fileProvider(_ fileProvider: FileProviderOperations, shouldProceedAfterError error: Error, operation: FileOperationType) -> Bool
}
internal class Weak<T: AnyObject> {
weak var value : T?
init (_ value: T) {
self.value = value
}
}
/// For internal use in `FileProvider` framework
public protocol FoundationErrorEnum {
/// Init from error code
init? (rawValue: Int)
// Raw error code
var rawValue: Int { get }
}
+127 -9
View File
@@ -8,7 +8,7 @@
import Foundation
extension Array where Element: FileObject {
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)
@@ -21,8 +21,8 @@ extension Array where Element: FileObject {
}
}
extension URLFileResourceType {
/// Returns corresponding `URLFileResourceType` of a `FileAttributeType` value
public extension URLFileResourceType {
/// **FileProvider** returns corresponding `URLFileResourceType` of a `FileAttributeType` value
public init(fileTypeValue: FileAttributeType) {
switch fileTypeValue {
case FileAttributeType.typeCharacterSpecial: self = .characterSpecial
@@ -37,11 +37,22 @@ extension URLFileResourceType {
}
}
internal extension URLResourceKey {
static let fileURL = URLResourceKey(rawValue: "NSURLFileURLKey")
static let serverDate = URLResourceKey(rawValue: "NSURLServerDateKey")
static let entryTag = URLResourceKey(rawValue: "NSURLEntryTagKey")
static let mimeType = URLResourceKey(rawValue: "NSURLMIMETypeIdentifierKey")
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")
}
public extension ProgressUserInfoKey {
public static let fileProvderOperationTypeKey = ProgressUserInfoKey("FilesProviderOperationTypeKey")
public static let startingTimeKey = ProgressUserInfoKey("NSProgressstartingTimeKey")
}
internal extension URL {
@@ -62,6 +73,76 @@ internal extension URL {
}
}
internal extension URLRequest {
mutating func set(httpAuthentication credential: URLCredential?, with type: HTTPAuthenticationType) {
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 authStr = "\(credential.user ?? ""):\(credential.password ?? "")"
self.setValue("Basic \(authStr)", 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(rangeWithOffset offset: Int64, length: Int) {
if length > 0 {
self.setValue("bytes=\(offset)-\(offset + Int64(length) - 1)", forHTTPHeaderField: "Range")
} else if offset > 0 && length < 0 {
self.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
}
}
enum ContentType: String {
case json = "application/json"
case stream = "application/octet-stream"
case xml = "text/xml; charset=\"utf-8\""
}
mutating func set(contentType: ContentType) {
self.setValue(contentType.rawValue, forHTTPHeaderField: "Content-Type")
}
private func asciiEscape(string:String) -> String {
var res = ""
for char in string.unicodeScalars {
let substring = String(char)
if substring.canBeConverted(to: .ascii) {
res.append(substring)
} else {
res = res.appendingFormat("\\u%04x", char.value)
}
}
return res
}
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 = self.asciiEscape(string: jsonString)
jsonString = jsonString.replacingOccurrences(of: "\\/", with: "/")
self.setValue(jsonString, forHTTPHeaderField: "Dropbox-API-Arg")
}
}
internal extension Data {
internal var isPDF: Bool {
return self.count > 4 && self.scanString(length: 4, using: .ascii) == "%PDF"
@@ -151,7 +232,44 @@ internal extension TimeInterval {
}
}
extension NSPredicate {
internal extension Date {
init?(rfcString: String) {
let dateFor: DateFormatter = DateFormatter()
dateFor.locale = Locale(identifier: "en_US")
dateFor.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ssZ"
if let rfc3339 = dateFor.date(from: rfcString) {
self = rfc3339
return
}
dateFor.dateFormat = "EEE',' dd' 'MMM' 'yyyy HH':'mm':'ss z"
if let rfc1123 = dateFor.date(from: rfcString) {
self = rfc1123
return
}
dateFor.dateFormat = "EEEE',' dd'-'MMM'-'yy HH':'mm':'ss z"
if let rfc850 = dateFor.date(from: rfcString) {
self = rfc850
return
}
dateFor.dateFormat = "EEE MMM d HH':'mm':'ss yyyy"
if let asctime = dateFor.date(from: rfcString) {
self = asctime
return
}
return nil
}
internal func rfc3339utc() -> String {
let fm = DateFormatter()
fm.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
fm.timeZone = TimeZone(identifier: "UTC")
fm.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
+85 -51
View File
@@ -21,7 +21,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
open var dispatch_queue: DispatchQueue
open var operation_queue: OperationQueue
open weak var delegate: FileProviderDelegate?
open internal(set) var credential: URLCredential?
open var credential: URLCredential?
/// Underlying `FileManager` object for listing and metadata fetching.
open private(set) var fileManager = FileManager()
@@ -70,7 +70,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
return nil
}
var finalBaseURL = baseURL
var finalBaseURL = baseURL.absoluteURL
switch directory {
case .documentDirectory:
@@ -98,14 +98,19 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
guard baseURL.isFileURL else {
fatalError("Cannot initialize a Local provider from remote URL.")
}
self.baseURL = baseURL
self.baseURL = (baseURL.absoluteString.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")).absoluteURL
self.currentPath = ""
self.credential = nil
self.isCoorinating = false
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: .concurrent)
#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 = "FileProvider.\(type(of: self).type).Operation"
operation_queue.name = "\(queueLabel).Operation"
fileProviderManagerDelegate = LocalFileProviderManagerDelegate(provider: self)
opFileManager.delegate = fileProviderManagerDelegate
@@ -116,7 +121,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
return nil
}
self.init(baseURL: baseURL)
self.currentPath = aDecoder.decodeObject(of: NSString.self, forKey: "currentPath") as? String ?? ""
self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? ""
self.isCoorinating = aDecoder.decodeBool(forKey: "isCoorinating")
}
@@ -140,12 +145,6 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
return copy
}
/// **OBSOLETED:** No longer is in use and overriding this method has no effect anymore.
@available(*, obsoleted: 1.0, message: "Overriding this method has no effect anymore.")
open class func defaultBaseURL() -> URL {
return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
}
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
dispatch_queue.async {
do {
@@ -174,22 +173,31 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
completionHandler(totalSize, totalSize - freeSize)
}
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? {
let progress = Progress(parent: nil, userInfo: nil)
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 (Bool) -> Void) {
@@ -201,22 +209,13 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
open weak var fileOperationDelegate : FileOperationDelegate?
@discardableResult
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(folderName) + "/")
return self.doOperation(opType, completionHandler: completionHandler)
}
@discardableResult
open func create(file fileName: String, at atPath: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let fileName = fileName.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
let path = (atPath as NSString).appendingPathComponent(fileName)
let opType = FileOperationType.create(path: path)
return self.doOperation(opType, data: data, atomically: true, completionHandler: completionHandler)
}
@discardableResult
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.move(source: path, destination: toPath)
if !overwrite && self.fileManager.fileExists(atPath: self.url(of: toPath).path) {
@@ -228,7 +227,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
}
@discardableResult
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.copy(source: path, destination: toPath)
if !overwrite && self.fileManager.fileExists(atPath: self.url(of: toPath).path) {
@@ -242,13 +241,13 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
}
@discardableResult
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.remove(path: path)
return self.doOperation(opType, completionHandler: completionHandler)
}
@discardableResult
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
if !overwrite && self.fileManager.fileExists(atPath: self.url(of: toPath).path) {
self.dispatch_queue.async {
completionHandler?(self.throwError(toPath, code: CocoaError.fileWriteFileExists as FoundationErrorEnum))
@@ -260,12 +259,12 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
}
@discardableResult
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
return self.doOperation(opType, completionHandler: completionHandler)
}
dynamic func doSimpleOperation(_ box: UndoBox) {
@objc dynamic func doSimpleOperation(_ box: UndoBox) {
guard let _ = self.undoManager else { return }
_ = self.doOperation(box.undoOperation) { (_) in
return
@@ -273,8 +272,12 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
}
@discardableResult
fileprivate func doOperation(_ opType: FileOperationType, data: Data? = nil, atomically: Bool = false, forUploading: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
fileprivate func doOperation(_ opType: FileOperationType, data: Data? = nil, atomically: Bool = false, forUploading: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
let progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, 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)
@@ -308,22 +311,31 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
let operationHandler: (URL, URL?) -> Void = { source, dest in
do {
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
switch opType {
case .create:
if sourcePath.hasSuffix("/") {
progress.totalUnitCount = 1
try self.opFileManager.createDirectory(at: source, withIntermediateDirectories: true, attributes: [:])
} else {
progress.totalUnitCount = Int64(data?.count ?? 0)
try data?.write(to: source, options: .atomic)
}
case .modify:
progress.totalUnitCount = Int64(data?.count ?? 0)
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
@@ -331,7 +343,8 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
if successfulSecurityScopedResourceAccess {
source.stopAccessingSecurityScopedResource()
}
progress.completedUnitCount = progress.totalUnitCount
self.dispatch_queue.async {
completionHandler?(nil)
}
@@ -342,6 +355,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
if successfulSecurityScopedResourceAccess {
source.stopAccessingSecurityScopedResource()
}
progress.cancel()
self.dispatch_queue.async {
completionHandler?(e)
}
@@ -383,22 +397,31 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
operationHandler(source, dest)
}
}
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
return progress
}
@discardableResult
open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
let opType = FileOperationType.fetch(path: path)
let progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.isCancellable = false
progress.setUserInfoObject(Progress.FileOperationKind.receiving, forKey: .fileOperationKindKey)
let url = self.url(of: path)
progress.totalUnitCount = url.fileSize
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)
}
} catch let e {
progress.cancel()
self.dispatch_queue.async {
completionHandler(nil, e)
}
@@ -420,24 +443,30 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
operationHandler(url)
}
}
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
return progress
}
@discardableResult
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
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 opType = FileOperationType.fetch(path: path)
let progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.isCancellable = false
progress.setUserInfoObject(Progress.FileOperationKind.receiving, forKey: .fileOperationKindKey)
let url = self.url(of: path)
let operationHandler: (URL) -> Void = { url in
@@ -451,16 +480,20 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
defer {
handle.closeFile()
}
let size = LocalFileObject(fileWithURL: url)?.size ?? -1
progress.totalUnitCount = size
guard size > offset else {
progress.cancel()
self.dispatch_queue.async {
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadTooLarge as FoundationErrorEnum))
}
return
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
handle.seek(toFileOffset: UInt64(offset))
guard Int64(handle.offsetInFile) == offset else {
progress.cancel()
self.dispatch_queue.async {
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadTooLarge as FoundationErrorEnum))
}
@@ -468,7 +501,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
}
let data = handle.readData(ofLength: length)
progress.completedUnitCount = progress.totalUnitCount
self.dispatch_queue.async {
completionHandler(data, nil)
}
@@ -488,13 +521,14 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
}
}
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
return progress
}
@discardableResult
open func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.modify(path: path)
return self.doOperation(opType, data: data, atomically: atomically, completionHandler: completionHandler)
open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
let fileExists = fileManager.fileExists(atPath: url(of: path).path)
let opType: FileOperationType = fileExists ? .modify(path: path) : .create(path: path)
return self.doOperation(opType, data: data ?? Data(), atomically: atomically, completionHandler: completionHandler)
}
fileprivate var monitors = [LocalFolderMonitor]()
@@ -532,13 +566,13 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
public extension LocalFileProvider {
/**
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 destURL, making it possible
to create symbolic links to locations that do not yet exist.
Also, if the final path component in url is a symbolic link, that link is not followed.
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:
- path: The file path at which to create the new symbolic link. The last component of the path issued as the name of the link.
- destPath: The path that contains the item to be pointed to by the link. In other words, this is the destination of the link.
- 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.
*/
public func create(symbolicLink path: String, withDestinationPath destPath: String, completionHandler: SimpleCompletionHandler) {
+5 -100
View File
@@ -10,7 +10,7 @@ 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) {
internal override init(url: URL?, name: String, path: String) {
super.init(url: url, name: name, path: path)
}
@@ -19,12 +19,13 @@ public final class LocalFileObject: FileObject {
var fileURL: URL?
var rpath = path.replacingOccurrences(of: relativeURL?.path ?? "", with: "", options: .anchored)
if relativeURL != nil && rpath.hasPrefix("/") {
rpath.remove(at: rpath.startIndex)
_=rpath.characters.removeFirst()
}
if #available(iOS 9.0, macOS 10.11, tvOS 9.0, *) {
fileURL = URL(fileURLWithPath: rpath, relativeTo: relativeURL)
} else {
fileURL = URL(string: rpath.isEmpty ? "./" : rpath, relativeTo: relativeURL)
rpath = rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? rpath
fileURL = URL(string: rpath, relativeTo: relativeURL) ?? relativeURL
}
if let fileURL = fileURL {
@@ -37,7 +38,7 @@ public final class LocalFileObject: FileObject {
/// Initiates a `LocalFileObject` with attributes of file in url.
public convenience init?(fileWithURL fileURL: URL) {
do {
let values = try fileURL.resourceValues(forKeys: [.nameKey, .fileSizeKey, .fileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .fileResourceTypeKey, .isHiddenKey, .isWritableKey, .typeIdentifierKey, .generationIdentifierKey, .documentIdentifierKey])
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)
@@ -216,102 +217,6 @@ internal class LocalFileProviderManagerDelegate: NSObject, FileManagerDelegate {
}
}
/// Local operation handling is limited. Please don't use as much as possible.
open class LocalOperationHandle: OperationHandle {
public let baseURL: URL
public let operationType: FileOperationType
init (operationType: FileOperationType, baseURL: URL?) {
self.baseURL = baseURL ?? URL(fileURLWithPath: "/")
self.operationType = operationType
}
private var sourceURL: URL? {
guard let source = operationType.source else { return nil }
return source.hasPrefix("file://") ? URL(fileURLWithPath: source) : baseURL.appendingPathComponent(source)
}
private var destURL: URL? {
guard let dest = operationType.destination else { return nil }
return dest.hasPrefix("file://") ? URL(fileURLWithPath: dest) : baseURL.appendingPathComponent(dest)
}
/// Caution: may put pressure on CPU, may have latency
open var bytesSoFar: Int64 {
assert(!Thread.isMainThread, "Don't run \(#function) method on main thread")
switch operationType {
case .modify:
guard let url = sourceURL, url.isFileURL else { return 0 }
if url.fileIsDirectory {
return iterateDirectory(url, deep: true).totalsize
} else {
return url.fileSize
}
case .copy, .move:
guard let url = destURL, url.isFileURL else { return 0 }
if url.fileIsDirectory {
return iterateDirectory(url, deep: true).totalsize
} else {
return url.fileSize
}
default:
return 0
}
}
/// Caution: may put pressure on CPU, may have latency
open var totalBytes: Int64 {
assert(!Thread.isMainThread, "Don't run \(#function) method on main thread")
switch operationType {
case .copy, .move:
guard let url = sourceURL, url.isFileURL else { return 0 }
if url.fileIsDirectory {
return iterateDirectory(url, deep: true).totalsize
} else {
return url.fileSize
}
default:
return 0
}
}
/// Not usable in local provider
open var inProgress: Bool {
return false
}
/// Not usable in local provider
open func cancel() -> Bool{
return false
}
func iterateDirectory(_ pathURL: URL, deep: Bool) -> (folders: Int, files: Int, totalsize: Int64) {
var folders = 0
var files = 0
var totalsize: Int64 = 0
let keys: [URLResourceKey] = [.isDirectoryKey, .fileSizeKey]
let enumOpt: FileManager.DirectoryEnumerationOptions = !deep ? [.skipsSubdirectoryDescendants, .skipsPackageDescendants] : []
let fp = FileManager()
let filesList = fp.enumerator(at: pathURL, includingPropertiesForKeys: keys, options: enumOpt, errorHandler: nil)
while let fileURL = filesList?.nextObject() as? URL {
guard let values = try? fileURL.resourceValues(forKeys: [.isDirectoryKey, .fileSizeKey]) else { continue }
let isdir = values.isDirectory ?? false
let size = Int64(values.fileSize ?? 0)
if isdir {
folders += 1
} else {
files += 1
}
totalsize += size
}
return (folders, files, totalsize)
}
}
class UndoBox: NSObject {
weak var provider: FileProvideUndoable?
let operation: FileOperationType
@@ -33,24 +33,42 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
}
open weak var delegate: FileProviderDelegate?
open let credential: URLCredential?
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?
fileprivate var sessionDelegate: SessionDelegate?
internal fileprivate(set) var sessionDelegate: SessionDelegate?
public var session: URLSession {
if _session == nil {
self.sessionDelegate = SessionDelegate(fileProvider: self, credential: credential)
let queue = OperationQueue()
//queue.underlyingQueue = dispatch_queue
let config = URLSessionConfiguration.default
config.urlCache = cache
config.requestCachePolicy = .returnCacheDataElseLoad
_session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDelegate?, delegateQueue: queue)
get {
if _session == nil {
self.sessionDelegate = SessionDelegate(fileProvider: self)
let queue = OperationQueue()
//queue.underlyingQueue = dispatch_queue
let config = URLSessionConfiguration.default
config.urlCache = cache
config.requestCachePolicy = .returnCacheDataElseLoad
_session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDelegate?, delegateQueue: 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!)
}
return _session!
}
/**
@@ -68,17 +86,23 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
- cache: A URLCache to cache downloaded files and contents.
*/
public init(credential: URLCredential?, serverURL: URL? = nil, drive: String = "root", cache: URLCache? = nil) {
let baseURL = serverURL ?? URL(string: "https://api.onedrive.com/")!
self.baseURL = baseURL.path.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")
let baseURL = serverURL?.absoluteURL ?? URL(string: "https://api.onedrive.com/")!
self.baseURL = baseURL.absoluteString.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")
self.drive = drive
self.currentPath = ""
self.useCache = false
self.validatingCache = true
self.cache = cache
self.credential = credential
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: .concurrent)
#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 = "FileProvider.\(type(of: self).type).Operation"
operation_queue.name = "\(queueLabel).Operation"
}
public required convenience init?(coder aDecoder: NSCoder) {
@@ -114,6 +138,10 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
}
deinit {
if let sessionuuid = _session?.sessionDescription {
removeSessionHandler(for: sessionuuid)
}
if fileProviderCancelTasksOnInvalidating {
_session?.invalidateAndCancel()
} else {
@@ -130,7 +158,7 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
var request = URLRequest(url: url(of: path))
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.set(httpAuthentication: credential, with: .oAuth2)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderOneDriveError?
var fileObject: OneDriveFileObject?
@@ -149,7 +177,7 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) {
var request = URLRequest(url: url())
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.set(httpAuthentication: credential, with: .oAuth2)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var totalSize: Int64 = -1
var usedSize: Int64 = 0
@@ -162,12 +190,13 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
task.resume()
}
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? {
var foundFiles = [OneDriveFileObject]()
var queryStr: String?
queryStr = query.findValue(forKey: "name") as? String ?? query.findAllValues(forKey: nil).flatMap { $0.value as? String }.first
guard let finalQueryStr = queryStr else { return }
search(path, query: finalQueryStr, foundItem: { (file) in
guard let finalQueryStr = queryStr else { return nil }
let progress = Progress(parent: nil, userInfo: nil)
search(path, query: finalQueryStr, progress: progress, foundItem: { (file) in
if query.evaluate(with: file.mapPredicate()) {
foundFiles.append(file)
foundItemHandler?(file)
@@ -175,6 +204,7 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
}, completionHandler: { (error) in
completionHandler(foundFiles, error)
})
return progress
}
open func url(of path: String? = nil, modifier: String? = nil) -> URL {
@@ -185,28 +215,30 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
rpath = self.currentPath
}
let driveURL = baseURL!.appendingPathComponent("drive/\(drive):/")
if rpath.hasPrefix("/") {
rpath.remove(at: rpath.startIndex)
_=rpath.characters.removeFirst()
}
if rpath.isEmpty {
if let modifier = modifier {
return baseURL!.appendingPathComponent("drive/\(drive)/\(modifier)")
return driveURL.appendingPathComponent(modifier)
}
return baseURL!.appendingPathComponent("drive/\(drive)")
return driveURL
}
let driveURL = baseURL!.appendingPathComponent("drive/\(drive):/")
rpath = (rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? rpath)
rpath = rpath.trimmingCharacters(in: pathTrimSet)
if let modifier = modifier {
rpath = rpath + ":/" + modifier
}
return URL(string: rpath, relativeTo: driveURL) ?? driveURL
return driveURL.appendingPathComponent(rpath)
}
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
var request = URLRequest(url: url())
request.httpMethod = "HEAD"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
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)
@@ -219,33 +251,33 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
extension OneDriveFileProvider: FileProviderOperations {
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
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 create(file fileName: String, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let filePath = (path as NSString).appendingPathComponent(fileName)
return self.writeContents(path: filePath, contents: data ?? Data(), completionHandler: completionHandler)
}
open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
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) -> OperationHandle? {
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) -> OperationHandle? {
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
return doOperation(.remove(path: path), completionHandler: completionHandler)
}
fileprivate func doOperation(_ operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
fileprivate func doOperation(_ operation: FileOperationType, 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)
guard let sourcePath = operation.source else { return nil }
let destPath = operation.destination
var request = URLRequest(url: url(of: sourcePath))
@@ -262,10 +294,10 @@ extension OneDriveFileProvider: FileProviderOperations {
return nil
}
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.set(httpAuthentication: credential, with: .oAuth2)
var requestDictionary = [String: AnyObject]()
if let dest = correctPath(destPath) as NSString? {
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.set(contentType: .json)
requestDictionary["parentReference"] = ("/drive/\(drive):" + dest.deletingLastPathComponent) as NSString
requestDictionary["name"] = dest.lastPathComponent as NSString
request.httpBody = Data(jsonDictionary: requestDictionary)
@@ -275,15 +307,32 @@ extension OneDriveFileProvider: FileProviderOperations {
if let response = response as? HTTPURLResponse, response.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) {
serverError = FileProviderOneDriveError(code: code, path: sourcePath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
}
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 RemoteOperationHandle(operationType: operation, tasks: [task])
return progress
}
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
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.throwError(localFile.path, code: URLError.fileIsDirectory))
}
return nil
}
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
@@ -291,37 +340,58 @@ extension OneDriveFileProvider: FileProviderOperations {
return upload_simple(toPath, localFile: localFile, overwrite: overwrite, operation: opType, completionHandler: completionHandler)
}
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.copy(source: path, destination: destURL.absoluteString)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
var progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
var request = URLRequest(url: self.url(of: path, modifier: "content"))
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
let task = session.downloadTask(with: request, completionHandler: { (cacheURL, response, error) in
guard let cacheURL = cacheURL, let httpResponse = response as? HTTPURLResponse , httpResponse.statusCode < 300 else {
let code = FileProviderHTTPErrorCode(rawValue: (response as? HTTPURLResponse)?.statusCode ?? -1)
let errorData : Data? = nil //Data(contentsOf:cacheURL) // TODO: Figure out how to get error response data for the error description
request.set(httpAuthentication: credential, with: .oAuth2)
let task = session.downloadTask(with: request)
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
if error != nil {
progress.cancel()
}
completionHandler?(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? = nil //Data(contentsOf: cacheURL) // TODO: Figure out how to get error response data for the error description
let serverError : FileProviderOneDriveError? = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: errorData ?? Data(), encoding: .utf8)) : nil
completionHandler?(serverError ?? error)
if serverError != nil {
progress.cancel()
}
completionHandler?(serverError)
return
}
do {
try FileManager.default.moveItem(at: cacheURL, to: destURL)
try FileManager.default.moveItem(at: tempURL, to: destURL)
completionHandler?(nil)
} catch let e {
completionHandler?(e)
}
})
}
task.taskDescription = opType.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 RemoteOperationHandle(operationType: opType, tasks: [task])
return progress
}
}
extension OneDriveFileProvider: FileProviderReadWrite {
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
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)
@@ -330,34 +400,59 @@ extension OneDriveFileProvider: FileProviderReadWrite {
}
let opType = FileOperationType.fetch(path: path)
var progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
var request = URLRequest(url: self.url(of: path, modifier: "content"))
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
if length > 0 {
request.setValue("bytes=\(offset)-\(offset + length - 1)", forHTTPHeaderField: "Range")
} else if offset > 0 && length < 0 {
request.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
}
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderOneDriveError?
if let httpResponse = response as? HTTPURLResponse , httpResponse.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: httpResponse.statusCode) {
serverError = FileProviderOneDriveError(code: code, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8))
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(rangeWithOffset: offset, length: length)
let task = session.downloadTask(with: request)
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
if error != nil {
progress.cancel()
}
let filedata = serverError ?? error == nil ? data : nil
completionHandler(filedata, serverError ?? error)
})
completionHandler(nil, 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? = nil //Data(contentsOf: cacheURL) // TODO: Figure out how to get error response data for the error description
let serverError : FileProviderOneDriveError? = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: errorData ?? Data(), encoding: .utf8)) : nil
if serverError != nil {
progress.cancel()
}
completionHandler(nil, serverError)
return
}
do {
let data = try Data(contentsOf: tempURL)
completionHandler(data, nil)
} catch let e {
completionHandler(nil, e)
}
}
task.taskDescription = opType.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 RemoteOperationHandle(operationType: opType, tasks: [task])
return progress
}
open func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.modify(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
// FIXME: remove 150MB restriction
return upload_simple(path, data: data, overwrite: overwrite, operation: opType, completionHandler: completionHandler)
return upload_simple(path, data: data ?? Data(), overwrite: overwrite, operation: opType, completionHandler: completionHandler)
}
fileprivate func registerNotifcation(path: String, eventHandler: (() -> Void)) {
@@ -372,19 +467,10 @@ extension OneDriveFileProvider: FileProviderReadWrite {
fileprivate func unregisterNotifcation(path: String) {
NotImplemented()
}
/**
Genrates a public url to a file to be shared with other users and can be downloaded without authentication.
- Parameters:
- to: path of file, including file/directory name.
- completionHandler: a closure with result of directory entries or error.
`link`: a url returned by OneDrive to share.
`attribute`: `nil` for OneDrive.
`expiration`: `nil` for OneDrive, as it doesn't expires.
`error`: Error returned by OneDrive.
*/
open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: OneDriveFileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
}
extension OneDriveFileProvider: FileProviderSharing {
open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
var request = URLRequest(url: self.url(of: path, modifier: "action.createLink"))
request.httpMethod = "POST"
let requestDictionary: [String: AnyObject] = ["type": "view" as NSString]
@@ -437,7 +523,7 @@ extension OneDriveFileProvider: ExtendedFileProvider {
}
var request = URLRequest(url: url)
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
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) {
@@ -456,7 +542,7 @@ extension OneDriveFileProvider: ExtendedFileProvider {
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.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.set(httpAuthentication: credential, with: .oAuth2)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderOneDriveError?
var dic = [String: Any]()
+66 -59
View File
@@ -18,12 +18,13 @@ public struct FileProviderOneDriveError: FileProviderHTTPError {
/// Containts path, url and attributes of a OneDrive file or resource.
public final class OneDriveFileObject: FileObject {
internal init(baseURL: URL?, name: String, path: String) {
var rpath = path
if path.hasPrefix("/") {
rpath.remove(at: rpath.startIndex)
var rpath = (URL(string:path)?.appendingPathComponent(name).absoluteString)!
if rpath.hasPrefix("/") {
_=rpath.characters.removeFirst()
}
let url = URL(string: rpath, relativeTo: baseURL) ?? URL(string: path)!
super.init(url: url, name: name, path: path)
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?, drive: String, jsonStr: String) {
@@ -34,14 +35,14 @@ public final class OneDriveFileObject: FileObject {
internal convenience init? (baseURL: URL?, drive: String, json: [String: AnyObject]) {
guard let name = json["name"] as? String else { return nil }
guard let path = (json["parentReference"] as? NSDictionary)?["path"] as? String else { return nil }
var lPath = path.replacingOccurrences(of: "/drive/\(drive)", with: "/", options: .anchored, range: nil)
var lPath = path.replacingOccurrences(of: "/drive/\(drive):", 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.modifiedDate = resolve(dateString: json["lastModifiedDateTime"] as? String ?? "")
self.creationDate = resolve(dateString: json["createdDateTime"] as? String ?? "")
self.type = (json["folder"] as? String) != nil ? .directory : .regular
self.modifiedDate = Date(rfcString: json["lastModifiedDateTime"] as? String ?? "")
self.creationDate = Date(rfcString: json["createdDateTime"] as? String ?? "")
self.type = json["folder"] != nil ? .directory : .regular
self.id = json["id"] as? String
self.entryTag = json["eTag"] as? String
}
@@ -61,20 +62,20 @@ public final class OneDriveFileObject: FileObject {
/// MIME type of file contents returned by OneDrive server.
open internal(set) var contentType: String {
get {
return allValues[.mimeType] as? String ?? ""
return allValues[.mimeTypeKey] as? String ?? ""
}
set {
allValues[.mimeType] = newValue
allValues[.mimeTypeKey] = newValue
}
}
/// HTTP E-Tag, can be used to mark changed files.
open internal(set) var entryTag: String? {
get {
return allValues[.entryTag] as? String
return allValues[.entryTagKey] as? String
}
set {
allValues[.entryTag] = newValue
allValues[.entryTagKey] = newValue
}
}
}
@@ -85,7 +86,7 @@ internal extension OneDriveFileProvider {
let url = cursor ?? self.url(of: path, modifier: "children")
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.set(httpAuthentication: credential, with: .oAuth2)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var responseError: FileProviderOneDriveError?
var files = prevContents
@@ -113,67 +114,69 @@ internal extension OneDriveFileProvider {
task.resume()
}
func upload_simple(_ targetPath: String, data: Data, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
if data.count > 100 * 1024 * 1024 {
let error = FileProviderOneDriveError(code: .payloadTooLarge, path: targetPath, errorDescription: nil)
completionHandler?(error)
self.delegateNotify(.create(path: targetPath), error: error)
return nil
}
let queryStr = overwrite ? "" : "?@name.conflictBehavior=fail"
let url = self.url(of: targetPath, modifier: "content\(queryStr)")
var request = URLRequest(url: url)
request.httpMethod = "PUT"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
request.httpBody = data
let task = session.uploadTask(with: request, from: data, completionHandler: { (data, response, error) in
var responseError: FileProviderOneDriveError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderOneDriveError(code: rCode, path: targetPath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
}
completionHandler?(responseError ?? error)
self.delegateNotify(operation, error: responseError ?? error)
})
task.taskDescription = operation.json
task.resume()
return RemoteOperationHandle(operationType: operation, tasks: [task])
}
func upload_simple(_ targetPath: String, localFile: URL, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let size = (try? localFile.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? -1
func upload_simple(_ targetPath: String, data: Data? = nil, localFile: URL? = nil, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? {
let size = data?.count ?? (try? localFile?.resourceValues(forKeys: [.fileSizeKey]))??.fileSize ?? -1
if size > 100 * 1024 * 1024 {
let error = FileProviderOneDriveError(code: .payloadTooLarge, path: targetPath, errorDescription: nil)
completionHandler?(error)
self.delegateNotify(.create(path: targetPath), error: error)
return nil
}
var progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
progress.totalUnitCount = Int64(size)
let queryStr = overwrite ? "" : "?@name.conflictBehavior=fail"
let url = self.url(of: targetPath, modifier: "content\(queryStr)")
var request = URLRequest(url: url)
request.httpMethod = "PUT"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
let task = session.uploadTask(with: request, fromFile: localFile, completionHandler: { (data, response, error) in
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .stream)
let task: URLSessionUploadTask
if let data = data {
task = session.uploadTask(with: request, from: data)
} else if let localFile = localFile {
task = session.uploadTask(with: request, fromFile: localFile)
} else {
return nil
}
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { [weak self] error in
var responseError: FileProviderOneDriveError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderOneDriveError(code: rCode, path: targetPath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
if let code = (task.response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
// We can't fetch server result from delegate!
responseError = FileProviderOneDriveError(code: rCode, path: targetPath, errorDescription: nil)
}
if !(responseError == nil && error == nil) {
progress.cancel()
}
completionHandler?(responseError ?? error)
self.delegateNotify(operation, error: responseError ?? error)
})
self?.delegateNotify(.create(path: targetPath), error: responseError ?? error)
}
task.taskDescription = operation.json
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent), options: .new, context: &progress)
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return RemoteOperationHandle(operationType: operation, tasks: [task])
return progress
}
func search(_ startPath: String = "", query: String, next: URL? = nil, foundItem:@escaping ((_ file: OneDriveFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) {
func search(_ startPath: String = "", query: String, next: URL? = nil, progress: Progress, foundItem: @escaping ((_ file: OneDriveFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) {
if progress.isCancelled {
return
}
let url: URL
let q = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
url = next ?? self.url(of: startPath, modifier: "view.search?q=\(q)")
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.set(httpAuthentication: credential, with: .oAuth2)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var responseError: FileProviderOneDriveError?
@@ -188,8 +191,8 @@ internal extension OneDriveFileProvider {
}
}
let next: URL? = (json["@odata.nextLink"] as? String).flatMap { URL(string: $0) }
if let next = next {
self.search(startPath, query: query, next: next, foundItem: foundItem, completionHandler: completionHandler)
if !progress.isCancelled, let next = next {
self.search(startPath, query: query, next: next, progress: progress, foundItem: foundItem, completionHandler: completionHandler)
} else {
completionHandler(responseError ?? error)
}
@@ -197,7 +200,11 @@ internal extension OneDriveFileProvider {
}
}
completionHandler(responseError ?? error)
})
})
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
}
}
@@ -240,14 +247,14 @@ internal extension OneDriveFileProvider {
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))
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 = resolve(dateString: timeTakenStr) {
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))
}
+150 -69
View File
@@ -8,63 +8,11 @@
import Foundation
/// Allows to get progress or cancel an in-progress operation, for remote, `URLSession` based providers.
open class RemoteOperationHandle: OperationHandle {
internal var tasks: [Weak<URLSessionTask>]
open private(set) var operationType: FileOperationType
init(operationType: FileOperationType, tasks: [URLSessionTask]) {
self.operationType = operationType
self.tasks = tasks.map { Weak<URLSessionTask>($0) }
}
internal func add(task: URLSessionTask) {
tasks.append(Weak<URLSessionTask>(task))
}
private func reape() {
self.tasks = tasks.filter { $0.value != nil }
}
open var bytesSoFar: Int64 {
return tasks.reduce(0) {
if let task = $1.value as? URLSessionUploadTask {
return $0 + task.countOfBytesSent
} else {
return $0 + ($1.value?.countOfBytesReceived ?? 0)
}
}
}
open var totalBytes: Int64 {
return tasks.reduce(0) {
if let task = $1.value as? URLSessionUploadTask {
return $0 + task.countOfBytesExpectedToSend
} else {
return $0 + ($1.value?.countOfBytesExpectedToReceive ?? 0)
}
}
}
open func cancel() -> Bool {
var canceled = false
for taskbox in tasks {
taskbox.value?.cancel()
canceled = true
}
return canceled
}
open var inProgress: Bool {
return tasks.reduce(false) { $0 || $1.value?.state ?? .canceling == .running }
}
}
/// 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
@@ -83,27 +31,142 @@ extension FileProviderHTTPError {
}
}
class SessionDelegate: NSObject, URLSessionDataDelegate, URLSessionDownloadDelegate {
/// Defines HTTP Authentication method required to access
public enum HTTPAuthenticationType {
/// 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
}
internal var completionHandlersForTasks = [String: [Int: SimpleCompletionHandler]]()
internal var downloadCompletionHandlersForTasks = [String: [Int: (URL) -> Void]]()
internal var dataCompletionHandlersForTasks = [String: [Int: (Data) -> 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?
var finishDownloadHandler: ((_ session: Foundation.URLSession, _ downloadTask: URLSessionDownloadTask, _ didFinishDownloadingToURL: URL) -> Void)?
var didSendDataHandler: ((_ session: Foundation.URLSession, _ task: URLSessionTask, _ bytesSent: Int64, _ totalBytesSent: Int64, _ totalBytesExpectedToSend: Int64) -> Void)?
var didReceivedData: ((_ session: Foundation.URLSession, _ downloadTask: URLSessionDownloadTask, _ bytesWritten: Int64, _ totalBytesWritten: Int64, _ totalBytesExpectedToWrite: Int64) -> Void)?
/// Forwardng URLSessionDownloadTaskDelegate call
public var finishDownloadHandler: ((_ session: URLSession, _ downloadTask: URLSessionDownloadTask, _ didFinishDownloadingToURL: URL) -> Void)?
/// Forwardng URLSessionTaskDelegate call
public var didSendDataHandler: ((_ session: URLSession, _ task: URLSessionTask, _ bytesSent: Int64, _ totalBytesSent: Int64, _ totalBytesExpectedToSend: Int64) -> Void)?
/// Forwardng URLSessionDownloadTaskDelegate call
public var didReceivedData: ((_ session: URLSession, _ downloadTask: URLSessionDownloadTask, _ bytesWritten: Int64, _ totalBytesWritten: Int64, _ totalBytesExpectedToWrite: Int64) -> Void)?
/// Forwardng URLSessionStreamTaskDelegate call
public var didBecomeStream :((_ session: URLSession, _ taskId: Int, _ didBecome: InputStream, _ outputStream: OutputStream) -> Void)?
init(fileProvider: FileProviderBasicRemote & FileProviderOperations, credential: URLCredential?) {
public init(fileProvider: FileProviderBasicRemote & FileProviderOperations) {
self.fileProvider = fileProvider
self.credential = credential
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:
break
}
}
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
// codebeat:disable[ARITY]
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
self.finishDownloadHandler?(session, downloadTask, location)
return
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived))
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive))
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent))
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived))
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
}
if !(task is URLSessionDownloadTask), case FileOperationType.fetch = op {
return
}
if #available(iOS 9.0, macOS 10.11, *) {
if task is URLSessionStreamTask {
return
}
}
DispatchQueue.main.async {
if error != nil {
fileProvider.delegate?.fileproviderFailed(fileProvider, operation: op)
} else {
fileProvider.delegate?.fileproviderSucceed(fileProvider, operation: op)
}
}
}
func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
let completionHandler = dataCompletionHandlersForTasks[session.sessionDescription!]?[dataTask.taskIdentifier] ?? nil
completionHandler?(data)
_ = dataCompletionHandlersForTasks[session.sessionDescription!]?.removeValue(forKey: dataTask.taskIdentifier)
}
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
self.finishDownloadHandler?(session, downloadTask, location)
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, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
self.didSendDataHandler?(session, task, bytesSent, totalBytesSent, totalBytesExpectedToSend)
guard let json = task.taskDescription?.deserializeJSON(),
@@ -111,12 +174,23 @@ class SessionDelegate: NSObject, URLSessionDataDelegate, URLSessionDownloadDeleg
return
}
switch op {
case .create(path: let path):
if path.hasSuffix("/") { return }
case .modify:
break
default:
return
}
let progress = Float(totalBytesSent) / Float(totalBytesExpectedToSend)
fileProvider.delegate?.fileproviderProgress(fileProvider, operation: op, progress: progress)
DispatchQueue.main.async {
fileProvider.delegate?.fileproviderProgress(fileProvider, operation: op, progress: progress)
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
self.didReceivedData?(session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
guard let json = downloadTask.taskDescription?.deserializeJSON(),
@@ -124,14 +198,16 @@ class SessionDelegate: NSObject, URLSessionDataDelegate, URLSessionDownloadDeleg
return
}
fileProvider.delegate?.fileproviderProgress(fileProvider, operation: op, progress: Float(totalBytesWritten) / Float(totalBytesExpectedToWrite))
DispatchQueue.main.async {
fileProvider.delegate?.fileproviderProgress(fileProvider, operation: op, progress: Float(totalBytesWritten) / Float(totalBytesExpectedToWrite))
}
}
func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
public func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
authenticate(didReceive: challenge, completionHandler: completionHandler)
}
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
authenticate(didReceive: challenge, completionHandler: completionHandler)
}
@@ -145,6 +221,11 @@ class SessionDelegate: NSObject, URLSessionDataDelegate, URLSessionDownloadDeleg
completionHandler(.performDefaultHandling, nil)
}
}
@available(iOS 9.0, macOS 10.11, *)
public func urlSession(_ session: URLSession, streamTask: URLSessionStreamTask, didBecome inputStream: InputStream, outputStream: OutputStream) {
self.didBecomeStream?(session, streamTask.taskIdentifier, inputStream, outputStream)
}
}
/// HTTP status codes as an enum.
+11 -11
View File
@@ -11,11 +11,11 @@ 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 SMBProtocolClientDelegate: class {
func receivedResponse(client: SMB2ProtocolClient, response: SMBResponse, for: SMBRequest)
protocol FileProviderSMBTaskDelegate: class {
func receivedResponse(client: FileProviderSMBTask, response: SMBResponse, for: SMBRequest)
}
class SMB2ProtocolClient: FPSStreamTask {
class FileProviderSMBTask: FileProviderStreamTask {
var timeout: TimeInterval = 30
private(set) var lastMessageID: UInt64 = 0
@@ -31,14 +31,14 @@ class SMB2ProtocolClient: FPSStreamTask {
private(set) var requestStack = [Int: SMBRequest]()
private(set) var responseStack = [Int: SMBResponse]()
weak var delegate: SMBProtocolClientDelegate?
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.writeData(data, timeout: 0, completionHandler: { (e) in
self.write(data, timeout: timeout, completionHandler: { (e) in
completionHandler?(e)
})
return mId
@@ -50,9 +50,9 @@ class SMB2ProtocolClient: FPSStreamTask {
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.writeData(data, timeout: 0, completionHandler: { (e) in
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
self.readData(ofMinLength: 64, maxLength: 65536, timeout: self.timeout, completionHandler: { (data, eof, e2) in
// TODO: set session id
completionHandler?(e2 ?? e)
})
@@ -73,7 +73,7 @@ class SMB2ProtocolClient: FPSStreamTask {
let tcHeader = SMB2.TreeConnectRequest.Header(flags: [])
let msg = SMB2.TreeConnectRequest(header: tcHeader, host: host, share: share)
let data = createSMB2Message(header: smbHeader, message: msg!)
self.writeData(data, timeout: 0, completionHandler: { (e) in
self.write(data, timeout: timeout, completionHandler: { (e) in
completionHandler?(e)
})
@@ -85,7 +85,7 @@ class SMB2ProtocolClient: FPSStreamTask {
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.writeData(data, timeout: 0, completionHandler: { (e) in
self.write(data, timeout: timeout, completionHandler: { (e) in
completionHandler?(e)
})
return mId
@@ -96,7 +96,7 @@ class SMB2ProtocolClient: FPSStreamTask {
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.writeData(data, timeout: 0, completionHandler: { (e) in
self.write(data, timeout: timeout, completionHandler: { (e) in
completionHandler?(e)
})
return mId
@@ -108,7 +108,7 @@ class SMB2ProtocolClient: FPSStreamTask {
}
// MARK: create and analyse messages
extension SMB2ProtocolClient {
extension FileProviderSMBTask {
func determineSMBVersion(_ data: Data) -> Float {
let smbverChar: Int8 = Int8(bitPattern: data.first ?? 0)
let version = 0 - smbverChar
+19 -17
View File
@@ -15,7 +15,7 @@ class SMBFileProvider: FileProvider, FileProviderMonitor {
open var dispatch_queue: DispatchQueue
open var operation_queue: OperationQueue
open weak var delegate: FileProviderDelegate?
open let credential: URLCredential?
open var credential: URLCredential?
public typealias FileObjectClass = FileObject
@@ -24,9 +24,15 @@ class SMBFileProvider: FileProvider, FileProviderMonitor {
return nil
}
self.baseURL = baseURL.appendingPathComponent("")
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: .concurrent)
#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 = "FileProvider.\(type(of: self).type).Operation"
operation_queue.name = "\(queueLabel).Operation"
self.credential = credential
}
@@ -68,60 +74,56 @@ class SMBFileProvider: FileProvider, FileProviderMonitor {
open weak var fileOperationDelegate: FileOperationDelegate?
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? {
NotImplemented()
return nil
}
open func create(file fileName: String, at atPath: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
NotImplemented()
return nil
}
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(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) -> OperationHandle? {
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
NotImplemented()
return nil
}
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
NotImplemented()
return nil
}
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
NotImplemented()
return nil
}
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
NotImplemented()
return nil
}
open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
open func contents(path: String, offset: Int64, length: Int, 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)) -> OperationHandle? {
open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
NotImplemented()
return nil
}
open func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
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 searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler:((FileObjectClass) -> Void)?, completionHandler: @escaping ((_ files: [FileObjectClass], _ error: Error?) -> Void)) {
NotImplemented()
}
open func registerNotifcation(path: String, eventHandler: @escaping (() -> Void)) {
NotImplemented()
}
+387 -139
View File
@@ -7,12 +7,13 @@
//
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.net](https://www.box.com/home)
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
@@ -32,7 +33,12 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
}
public weak var delegate: FileProviderDelegate?
open let credential: URLCredential?
public var credentialType: HTTPAuthenticationType = .digest
open var credential: URLCredential? {
didSet {
sessionDelegate?.credential = credential
}
}
open private(set) var cache: URLCache?
public var useCache: Bool
public var validatingCache: Bool
@@ -40,16 +46,30 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
fileprivate var _session: URLSession?
fileprivate var sessionDelegate: SessionDelegate?
public var session: URLSession {
if _session == nil {
self.sessionDelegate = SessionDelegate(fileProvider: self, credential: credential)
let queue = OperationQueue()
//queue.underlyingQueue = dispatch_queue
let config = URLSessionConfiguration.default
config.urlCache = cache
config.requestCachePolicy = .returnCacheDataElseLoad
_session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDownloadDelegate?, delegateQueue: queue)
get {
if _session == nil {
self.sessionDelegate = SessionDelegate(fileProvider: self)
let queue = OperationQueue()
//queue.underlyingQueue = dispatch_queue
let config = URLSessionConfiguration.default
config.urlCache = cache
config.requestCachePolicy = .returnCacheDataElseLoad
_session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDownloadDelegate?, delegateQueue: 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!)
}
return _session!
}
/**
@@ -64,15 +84,21 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
if !["http", "https"].contains(baseURL.uw_scheme.lowercased()) {
return nil
}
self.baseURL = baseURL.path.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")
self.baseURL = (baseURL.absoluteString.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")).absoluteURL
self.currentPath = ""
self.useCache = false
self.validatingCache = true
self.cache = cache
self.credential = credential
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: .concurrent)
#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 = "FileProvider.\(type(of: self).type).Operation"
operation_queue.name = "\(queueLabel).Operation"
}
public required convenience init?(coder aDecoder: NSCoder) {
@@ -90,8 +116,8 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
aCoder.encode(self.baseURL, forKey: "baseURL")
aCoder.encode(self.credential, forKey: "credential")
aCoder.encode(self.currentPath, forKey: "currentPath")
aCoder.encode(self.useCache, forKey: "isCoorinating")
aCoder.encode(self.validatingCache, forKey: "undoManager")
aCoder.encode(self.useCache, forKey: "useCache")
aCoder.encode(self.validatingCache, forKey: "validatingCache")
}
public static var supportsSecureCoding: Bool {
@@ -109,6 +135,10 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
}
deinit {
if let sessionuuid = _session?.sessionDescription {
removeSessionHandler(for: sessionuuid)
}
if fileProviderCancelTasksOnInvalidating {
_session?.invalidateAndCancel()
} else {
@@ -116,16 +146,32 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
}
}
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
open func contentsOfDirectory(path: String, completionHandler: @escaping (([FileObject], Error?) -> Void)) {
self.contentsOfDirectory(path: path, including: [], 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, `currentPath` value will be used.
- 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.
- `contents`: An array of `FileObject` identifying the the directory entries.
- `error`: Error returned by system.
*/
open func contentsOfDirectory(path: String, including: [URLResourceKey], completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
let opType = FileOperationType.fetch(path: path)
let url = self.url(of: path).appendingPathComponent("")
var request = URLRequest(url: url)
request.httpMethod = "PROPFIND"
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>".data(using: .utf8)
request.set(httpAuthentication: credential, with: credentialType)
request.set(contentType: .xml)
request.httpBody = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n\(WebDavFileObject.propString(including))\n</D:propfind>".data(using: .utf8)
request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length")
runDataTask(with: request, operationHandle: RemoteOperationHandle(operationType: opType, tasks: []), completionHandler: { (data, response, error) in
runDataTask(with: request, operation: opType, completionHandler: { (data, response, error) in
var responseError: FileProviderWebDavError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
@@ -145,12 +191,28 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
}
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, `currentPath` value will be used.
- 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.
- `attributes`: A `FileObject` containing the attributes of the item.
- `error`: Error returned by system.
*/
open func attributesOfItem(path: String, including: [URLResourceKey], completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
let url = self.url(of: path)
var request = URLRequest(url: url)
request.httpMethod = "PROPFIND"
request.setValue("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>".data(using: .utf8)
request.set(httpAuthentication: credential, with: credentialType)
request.set(contentType: .xml)
request.httpBody = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n\(WebDavFileObject.propString(including))\n</D:propfind>".data(using: .utf8)
request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length")
runDataTask(with: request, completionHandler: { (data, response, error) in
var responseError: FileProviderWebDavError?
@@ -178,7 +240,8 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
var request = URLRequest(url: baseURL)
request.httpMethod = "PROPFIND"
request.setValue("0", forHTTPHeaderField: "Depth")
request.setValue("text/xml; charset=\"utf-8\"", forHTTPHeaderField: "Content-Type")
request.set(httpAuthentication: credential, with: credentialType)
request.set(contentType: .xml)
request.httpBody = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n<D:prop><D:quota-available-bytes/><D:quota-used-bytes/></D:prop>\n</D:propfind>".data(using: .utf8)
request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length")
runDataTask(with: request, completionHandler: { (data, response, error) in
@@ -195,14 +258,16 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
})
}
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, 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"
//request.setValue("1", forHTTPHeaderField: "Depth")
request.setValue("text/xml; charset=\"utf-8\"", forHTTPHeaderField: "Content-Type")
request.set(httpAuthentication: credential, with: credentialType)
request.set(contentType: .xml)
request.httpBody = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n<D:allprop/></D:propfind>".data(using: .utf8)
runDataTask(with: request, completionHandler: { (data, response, error) in
let progress = Progress(parent: nil, userInfo: nil)
let task = session.dataTask(with: request) { (data, response, error) in
// FIXME: paginating results
var responseError: FileProviderWebDavError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
@@ -218,20 +283,28 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
}
fileObjects.append(fileObject)
progress.completedUnitCount = Int64(fileObjects.count)
foundItemHandler?(fileObject)
}
completionHandler(fileObjects, responseError ?? error)
return
}
completionHandler([], responseError ?? error)
})
}
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return progress
}
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
var request = URLRequest(url: baseURL!)
request.httpMethod = "PROPFIND"
request.setValue("0", forHTTPHeaderField: "Depth")
request.setValue("text/xml; charset=\"utf-8\"", forHTTPHeaderField: "Content-Type")
request.set(httpAuthentication: credential, with: credentialType)
request.set(contentType: .xml)
request.httpBody = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n<D:prop><D:quota-available-bytes/><D:quota-used-bytes/></D:prop>\n</D:propfind>".data(using: .utf8)
request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length")
runDataTask(with: request, completionHandler: { (data, response, error) in
@@ -245,51 +318,16 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
extension WebDAVFileProvider: FileProviderOperations {
@discardableResult
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(folderName) + "/")
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
let url = self.url(of: atPath).appendingPathComponent(folderName, isDirectory: true)
var request = URLRequest(url: url)
request.httpMethod = "MKCOL"
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var responseError: FileProviderWebDavError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderWebDavError(code: rCode, path: url.relativePath, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
}
completionHandler?(responseError ?? error)
self.delegateNotify(opType, error: responseError ?? error)
})
task.taskDescription = opType.json
task.resume()
return RemoteOperationHandle(operationType: opType, tasks: [task])
return self.doOperation(operation: opType, overwrite: false, completionHandler: completionHandler)
}
@discardableResult
open func create(file fileName: String, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.create(path: (path as NSString).appendingPathComponent(fileName))
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
let url = self.url(of: path).appendingPathComponent(fileName)
var request = URLRequest(url: url)
request.httpMethod = "PUT"
let task = session.uploadTask(with: request, from: data, completionHandler: { (data, response, error) in
var responseError: FileProviderWebDavError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
}
completionHandler?(responseError ?? error)
self.delegateNotify(opType, error: responseError ?? error)
})
task.taskDescription = opType.json
task.resume()
return RemoteOperationHandle(operationType: opType, tasks: [task])
}
@discardableResult
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.move(source: path, destination: toPath)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
@@ -298,7 +336,7 @@ extension WebDAVFileProvider: FileProviderOperations {
}
@discardableResult
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.copy(source: path, destination: toPath)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
@@ -307,7 +345,7 @@ extension WebDAVFileProvider: FileProviderOperations {
}
@discardableResult
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.remove(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
@@ -315,7 +353,7 @@ extension WebDAVFileProvider: FileProviderOperations {
return self.doOperation(operation: opType, completionHandler: completionHandler)
}
func doOperation(operation opType: FileOperationType, overwrite: Bool? = nil, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
fileprivate func doOperation(operation opType: FileOperationType, overwrite: Bool? = nil, completionHandler: SimpleCompletionHandler) -> Progress? {
let source = opType.source!
let sourceURL = self.url(of: source)
var request = URLRequest(url: sourceURL)
@@ -323,6 +361,8 @@ extension WebDAVFileProvider: FileProviderOperations {
request.setValue(url(of:dest).absoluteString, forHTTPHeaderField: "Destination")
}
switch opType {
case .create:
request.httpMethod = "MKCOL"
case .copy:
request.httpMethod = "COPY"
case .move:
@@ -333,6 +373,12 @@ extension WebDAVFileProvider: FileProviderOperations {
return nil
}
let progress = Progress(totalUnitCount: 1)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
request.set(httpAuthentication: credential, with: credentialType)
if let overwrite = overwrite, !overwrite {
request.setValue("F", forHTTPHeaderField: "Overwrite")
}
@@ -347,9 +393,17 @@ extension WebDAVFileProvider: FileProviderOperations {
for xresponse in xresponses where (xresponse.status ?? 0) >= 300 {
let error = FileProviderWebDavError(code: code, path: source, errorDescription: String(data: data, encoding: .utf8), url: sourceURL)
completionHandler?(error)
progress.cancel()
}
}
}
if responseError == nil && error == nil {
progress.completedUnitCount = 1
} else {
progress.cancel()
}
if (response as? HTTPURLResponse)?.statusCode ?? 0 != FileProviderHTTPErrorCode.multiStatus.rawValue {
completionHandler?(responseError ?? error)
}
@@ -357,68 +411,116 @@ extension WebDAVFileProvider: FileProviderOperations {
self.delegateNotify(opType, error: responseError ?? error)
})
task.taskDescription = opType.json
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return RemoteOperationHandle(operationType: opType, tasks: [task])
return progress
}
@discardableResult
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
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.throwError(localFile.path, code: URLError.fileIsDirectory))
}
return nil
}
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
var progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
progress.totalUnitCount = localFile.fileSize
let url = self.url(of:toPath)
var request = URLRequest(url: url)
if !overwrite {
request.setValue("F", forHTTPHeaderField: "Overwrite")
}
request.httpMethod = "PUT"
let task = session.uploadTask(with: request, fromFile: localFile, completionHandler: { (data, response, error) in
request.set(httpAuthentication: credential, with: credentialType)
let task = session.uploadTask(with: request, fromFile: localFile)
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { [weak self] error in
var responseError: FileProviderWebDavError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderWebDavError(code: rCode, path: toPath, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
if let code = (task.response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
// We can't fetch server result from delegate!
responseError = FileProviderWebDavError(code: rCode, path: toPath, errorDescription: nil, url: url)
}
if !(responseError == nil && error == nil) {
progress.cancel()
}
completionHandler?(responseError ?? error)
self.delegateNotify(opType, error: responseError ?? error)
})
self?.delegateNotify(.create(path: toPath), error: responseError ?? error)
}
task.taskDescription = opType.json
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent), options: .new, context: &progress)
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return RemoteOperationHandle(operationType: opType, tasks: [task])
return progress
}
@discardableResult
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.copy(source: path, destination: destURL.absoluteString)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
var progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
let url = self.url(of:path)
let request = URLRequest(url: url)
let task = session.downloadTask(with: request, completionHandler: { (sourceFileURL, response, error) in
var responseError: FileProviderWebDavError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: nil, url: url)
var request = URLRequest(url: url)
request.set(httpAuthentication: credential, with: credentialType)
let task = session.downloadTask(with: request)
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
if error != nil {
progress.cancel()
}
if let sourceFileURL = sourceFileURL {
do {
try FileManager.default.copyItem(at: sourceFileURL, to: toLocalURL)
} catch let e {
completionHandler?(e)
return
}
completionHandler?(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 serverError : FileProviderWebDavError? = code != nil ? FileProviderWebDavError(code: code!, path: path, errorDescription: code?.description, url: url) : nil
completionHandler?(serverError)
return
}
completionHandler?(responseError ?? error)
self.delegateNotify(opType, error: responseError ?? error)
})
do {
try FileManager.default.moveItem(at: tempURL, to: destURL)
completionHandler?(nil)
} catch let e {
completionHandler?(e)
}
}
task.taskDescription = opType.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 RemoteOperationHandle(operationType: opType, tasks: [task])
return progress
}
}
extension WebDAVFileProvider: FileProviderReadWrite {
@discardableResult
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
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)
@@ -427,57 +529,94 @@ extension WebDAVFileProvider: FileProviderReadWrite {
}
let opType = FileOperationType.fetch(path: path)
var progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
let url = self.url(of: path)
var request = URLRequest(url: url)
request.httpMethod = "GET"
if length > 0 {
request.setValue("bytes=\(offset)-\(offset + length - 1)", forHTTPHeaderField: "Range")
} else if offset > 0 && length < 0 {
request.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
}
let handle = RemoteOperationHandle(operationType: opType, tasks: [])
runDataTask(with: request, operationHandle: handle, completionHandler: { (data, response, error) in
var responseError: FileProviderWebDavError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
request.set(httpAuthentication: credential, with: credentialType)
request.set(rangeWithOffset: offset, length: length)
let task = session.downloadTask(with: request)
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
if error != nil {
progress.cancel()
}
completionHandler(data, responseError ?? error)
})
return handle
completionHandler(nil, error)
}
downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { [weak self] tempURL in
guard let httpResponse = task.response as? HTTPURLResponse , httpResponse.statusCode < 300 else {
let code = FileProviderHTTPErrorCode(rawValue: (task.response as? HTTPURLResponse)?.statusCode ?? -1)
let serverError : FileProviderWebDavError? = code != nil ? FileProviderWebDavError(code: code!, path: path, errorDescription: code?.description, url: url) : nil
completionHandler(nil, serverError)
return
}
do {
let data = try Data(contentsOf: tempURL)
(self?.dispatch_queue ?? DispatchQueue.global()).async {
completionHandler(data, nil)
}
} catch let e {
completionHandler(nil, e)
}
}
task.taskDescription = opType.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
}
@discardableResult
open func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.modify(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
var progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
progress.totalUnitCount = Int64(data?.count ?? 0)
// FIXME: lock destination before writing process
let url = atomically ? self.url(of: path).appendingPathExtension("tmp") : self.url(of: path)
var request = URLRequest(url: url)
request.httpMethod = "PUT"
request.set(httpAuthentication: credential, with: credentialType)
if !overwrite {
request.setValue("F", forHTTPHeaderField: "Overwrite")
}
let task = session.uploadTask(with: request, from: data, completionHandler: { (data, response, error) in
let task = session.uploadTask(with: request, from: data ?? Data())
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { [weak self] error in
var responseError: FileProviderWebDavError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: self.url(of: path))
if let code = (task.response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
// We can't fetch server result from delegate!
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: nil, url: url)
}
defer {
self.delegateNotify(opType, error: responseError ?? error)
if !(responseError == nil && error == nil) {
progress.cancel()
}
if let error = error {
completionHandler?(error)
return
}
if atomically {
self.moveItem(path: (path as NSString).appendingPathExtension("tmp")!, to: path, completionHandler: completionHandler)
}
})
completionHandler?(responseError ?? error)
self?.delegateNotify(opType, error: responseError ?? error)
}
task.taskDescription = opType.json
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent), options: .new, context: &progress)
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return RemoteOperationHandle(operationType: opType, tasks: [task])
return progress
}
/*
@@ -496,6 +635,86 @@ extension WebDAVFileProvider: FileProviderReadWrite {
// 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.throwError(path, code: URLError.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: FileProviderWebDavError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderWebDavError(code: rCode, path: url.relativePath, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
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.throwError(path, code: URLError.resourceUnavailable))
}
}
}
extension WebDAVFileProvider: FileProviderSharing {
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.throwError(path, code: URLError.resourceUnavailable))
}
return
}
let url = self.url(of: path)
var request = URLRequest(url: url)
request.httpMethod = "PROPPATCH"
request.set(httpAuthentication: credential, with: credentialType)
request.set(contentType: .xml)
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)
request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length")
runDataTask(with: request, completionHandler: { (data, response, error) in
var responseError: FileProviderWebDavError?
if let code = (response as? HTTPURLResponse)?.statusCode, code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
}
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)
})
}
}
extension WebDAVFileProvider: FileProvider { }
// MARK: WEBDAV XML response implementation
@@ -520,12 +739,9 @@ struct DavResponse {
init? (_ node: AEXMLElement, baseURL: URL?) {
func removeSlash(_ str: String) -> String {
if str.hasPrefix("/") {
return str.substring(from: str.index(after: str.startIndex))
} else {
return str
}
func standardizePath(_ str: String) -> String {
let trimmedStr = str.hasPrefix("/") ? str.substring(from: str.index(after: str.startIndex)) : str
return trimmedStr.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlPathAllowed.subtracting(CharacterSet(charactersIn: ":"))) ?? str
}
// find node names with namespace
@@ -554,7 +770,7 @@ struct DavResponse {
} else {
relativePath = hrefAbsolute?.absoluteString.replacingOccurrences(of: baseURL?.absoluteString ?? "", with: "", options: .anchored, range: nil) ?? hrefString
}
let hrefURL = URL(string: removeSlash(relativePath), relativeTo: baseURL) ?? baseURL
let hrefURL = URL(string: standardizePath(relativePath), relativeTo: baseURL) ?? baseURL
guard let href = hrefURL?.standardized else { return nil }
@@ -616,13 +832,13 @@ struct DavResponse {
public final class WebDavFileObject: FileObject {
internal init(_ davResponse: DavResponse) {
let href = davResponse.href
let name = davResponse.prop["displayname"] ?? (davResponse.hrefString.removingPercentEncoding! as NSString).lastPathComponent
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 = resolve(dateString: davResponse.prop["creationdate"] ?? "")
self.modifiedDate = resolve(dateString: davResponse.prop["getlastmodified"] ?? "")
self.creationDate = Date(rfcString: davResponse.prop["creationdate"] ?? "")
self.modifiedDate = Date(rfcString: davResponse.prop["getlastmodified"] ?? "")
self.contentType = davResponse.prop["getcontenttype"] ?? "octet/stream"
self.isHidden = (Int(davResponse.prop["ishidden"] ?? "0") ?? 0) > 0
self.type = self.contentType == "httpd/unix-directory" ? .directory : .regular
@@ -632,22 +848,54 @@ public final class WebDavFileObject: FileObject {
/// MIME type of the file.
open internal(set) var contentType: String {
get {
return allValues[.mimeType] as? String ?? ""
return allValues[.mimeTypeKey] as? String ?? ""
}
set {
allValues[.mimeType] = newValue
allValues[.mimeTypeKey] = newValue
}
}
/// HTTP E-Tag, can be used to mark changed files.
open internal(set) var entryTag: String? {
get {
return allValues[.entryTag] as? String
return allValues[.entryTagKey] as? String
}
set {
allValues[.entryTag] = newValue
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"
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
}
}
/// Error returned by WebDAV server when trying to access or do operations on a file or folder.