Compare commits

...

39 Commits

Author SHA1 Message Date
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
26 changed files with 1481 additions and 826 deletions
+3 -3
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)
@@ -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,8 +15,8 @@ Pod::Spec.new do |s|
# summary should be tweet-length, and the description more in depth.
#
s.name = "FileProvider"
s.version = "0.15.0"
s.name = "FilesProvider"
s.version = "0.18.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.
@@ -58,7 +58,7 @@ Pod::Spec.new do |s|
s.author = { "Amir Abbas Mousavian" => "a.mosavian@gmail.com" }
# Or just: s.author = "Amir Abbas Mousavian"
# s.authors = { "Amir Abbas Mousavian" => "a.mosavian@gmail.com" }
s.social_media_url = "https://twitter.com/amosavian"
# s.social_media_url = "https://twitter.com/amosavian"
# ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
#
@@ -116,9 +116,9 @@
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 */; };
@@ -146,9 +146,9 @@
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>"; };
798654321E8874BC002FA550 /* FTPHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTPHelper.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; };
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>"; };
@@ -182,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 */
@@ -269,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>";
@@ -304,7 +304,7 @@
794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */,
7936BC111E880F5700A6C81C /* FTPFileProvider.swift */,
798654321E8874BC002FA550 /* FTPHelper.swift */,
79BD63C31E2D17880035128C /* OneDriveFileProvide.swift */,
79BD63C31E2D17880035128C /* OneDriveFileProvider.swift */,
79BD63C41E2D17880035128C /* OneDriveHelper.swift */,
7924B1A81D89F79200589DB7 /* FPSStreamTask.swift */,
799396971D48C02300086753 /* SMBClient.swift */,
@@ -368,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 */,
@@ -381,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 */,
@@ -399,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 */,
@@ -417,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 */
@@ -442,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;
@@ -454,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 */
@@ -498,7 +498,7 @@
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 */,
@@ -541,7 +541,7 @@
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 */,
@@ -584,7 +584,7 @@
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 */,
@@ -621,7 +621,7 @@
799396601D48B7BF00086753 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_VERSION_STRING = 0.15.0;
BUNDLE_VERSION_STRING = 0.18.0;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_EMPTY_BODY = YES;
@@ -631,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;
@@ -644,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;
@@ -651,7 +653,7 @@
799396611D48B7BF00086753 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_VERSION_STRING = 0.15.0;
BUNDLE_VERSION_STRING = 0.18.0;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_EMPTY_BODY = YES;
@@ -671,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;
};
@@ -717,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";
@@ -764,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";
@@ -821,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";
@@ -872,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";
@@ -924,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";
@@ -974,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;
@@ -989,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 */,
@@ -998,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 */,
@@ -1007,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 */,
@@ -1016,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>
+16 -6
View File
@@ -28,10 +28,9 @@ 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, supported by some cloud services like `Box.com` and `Yandex.disk`.
- [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.
* Recursive directory removing & searching is not implemented yet.
* Active mode is not implemented yet (and probably won`t).
* Active mode is not implemented yet.
- [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.
@@ -52,12 +51,15 @@ 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:
@@ -69,7 +71,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
@@ -95,7 +97,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
@@ -106,6 +108,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:
@@ -119,6 +123,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
```
@@ -128,6 +134,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)
```
@@ -135,6 +143,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)
```
+68 -37
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!)
@@ -374,8 +379,7 @@ open class CloudFileProvider: LocalFileProvider {
*/
@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)
return super.create(folder: folderName, at: atPath, completionHandler: completionHandler)
}
/**
@@ -392,8 +396,7 @@ open class CloudFileProvider: LocalFileProvider {
*/
@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)
return super.moveItem(path: path, to: toPath, overwrite: overwrite, completionHandler: completionHandler)
}
/**
@@ -410,8 +413,7 @@ open class CloudFileProvider: LocalFileProvider {
*/
@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)
return super.copyItem(path: path, to: toPath, overwrite: overwrite, completionHandler: completionHandler)
}
/**
@@ -429,8 +431,7 @@ open class CloudFileProvider: LocalFileProvider {
*/
@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)
return super.removeItem(path: path, completionHandler: completionHandler)
}
/**
@@ -448,6 +449,7 @@ open class CloudFileProvider: LocalFileProvider {
open override func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
// TODO: Make use of overwrite parameter
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
let operationHandle = CloudOperationHandle(operationType: opType, baseURL: self.baseURL)
operation_queue.addOperation {
let tempFolder: URL
if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) {
@@ -475,7 +477,7 @@ open class CloudFileProvider: LocalFileProvider {
})
}
}
return CloudOperationHandle(operationType: opType, baseURL: self.baseURL)
return operationHandle
}
/**
@@ -501,9 +503,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 handle = super.copyItem(path: path, toLocalURL: toLocalURL, completionHandler: completionHandler)
return handle
}
/**
@@ -519,8 +520,7 @@ open class CloudFileProvider: LocalFileProvider {
*/
@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)
return super.contents(path: path, completionHandler: completionHandler)
}
/**
@@ -538,8 +538,7 @@ open class CloudFileProvider: LocalFileProvider {
*/
@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)
return super.contents(path: path, offset: offset, length: length, completionHandler: completionHandler)
}
/**
@@ -555,8 +554,7 @@ open class CloudFileProvider: 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)
return super.writeContents(path: path, contents: data, atomically: atomically, overwrite: overwrite, completionHandler: completionHandler)
}
fileprivate var monitors = [String: (NSMetadataQuery, NSObjectProtocol)]()
@@ -628,7 +626,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
@@ -641,6 +639,48 @@ open class CloudFileProvider: LocalFileProvider {
return file
}
func monitorFile(path: String, opType: FileOperationType) {
dispatch_queue.async {
let pathURL = self.url(of: path)
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: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
query.disableUpdates()
guard let item = (query.results as? [NSMetadataItem])?.first else {
return
}
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) {
DispatchQueue.main.async {
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(uploaded / 100))
}
} else if (uploaded == 0 || uploaded == 100) && (downloaded > 0 && downloaded < 100) {
DispatchQueue.main.async {
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(downloaded / 100))
}
} else if uploaded == 100 || downloaded == 100 {
query.stop()
NotificationCenter.default.removeObserver(updateObserver!)
DispatchQueue.main.async {
self.delegate?.fileproviderSucceed(self, operation: opType)
}
}
query.enableUpdates()
})
DispatchQueue.main.async {
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.
@@ -655,19 +695,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 {
@@ -685,6 +712,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
@@ -717,8 +745,11 @@ public enum UbiquitousScope: RawRepresentable {
}
}
/// Get progress of CloudFileProvider operations
open class CloudOperationHandle: OperationHandle {
/// Url of file which operation is doing on
public let baseURL: URL?
/// Type of operation
public let operationType: FileOperationType
init (operationType: FileOperationType, baseURL: URL?) {
+109 -85
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,7 +34,11 @@ open class DropboxFileProvider: FileProviderBasicRemote {
}
open weak var delegate: FileProviderDelegate?
open var 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
@@ -42,14 +46,28 @@ open class DropboxFileProvider: FileProviderBasicRemote {
fileprivate var _session: URLSession?
fileprivate 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,11 +140,16 @@ 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)) {
@@ -134,8 +162,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 +185,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
@@ -245,8 +273,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?
@@ -269,6 +297,14 @@ extension DropboxFileProvider: FileProviderOperations {
}
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
// 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
@@ -283,14 +319,11 @@ extension DropboxFileProvider: FileProviderOperations {
}
let url = URL(string: "files/download", relativeTo: contentURL)!
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")
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(dropboxArgKey: ["path": path as NSString])
let task = session.downloadTask(with: request)
completionHandlersForTasks[task.taskIdentifier] = completionHandler
downloadCompletionHandlersForTasks[task.taskIdentifier] = { tempURL in
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = completionHandler
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
@@ -323,23 +356,28 @@ extension DropboxFileProvider: FileProviderReadWrite {
let opType = FileOperationType.fetch(path: path)
let url = URL(string: "files/download", relativeTo: contentURL)!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
if length > 0 {
request.setValue("bytes=\(offset)-\(offset + Int64(length) - 1)", forHTTPHeaderField: "Range")
} else if offset > 0 && length < 0 {
request.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
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
completionHandler(nil, error)
}
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))
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(nil, serverError)
return
}
let filedata = serverError ?? error == nil ? data : nil
completionHandler(filedata, serverError ?? error)
})
do {
let data = try Data(contentsOf: tempURL)
completionHandler(data, nil)
} catch let e {
completionHandler(nil, e)
}
}
task.taskDescription = opType.json
task.resume()
return RemoteOperationHandle(operationType: opType, tasks: [task])
@@ -371,42 +409,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
@@ -451,8 +460,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
@@ -486,8 +495,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
@@ -535,26 +544,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() {
@@ -567,8 +587,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)
@@ -580,8 +604,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
@@ -591,7 +615,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)
}
}
+27 -57
View File
@@ -17,19 +17,15 @@ 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 = Date(rfcString: json["server_modified"] as? String ?? "")
self.modifiedDate = Date(rfcString: json["client_modified"] 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
}
}
@@ -88,8 +84,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?
@@ -121,42 +117,8 @@ internal extension DropboxFileProvider {
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"] = 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")
request.httpBody = data
let task = session.uploadTask(with: request, from: data)
completionHandlersForTasks[task.taskIdentifier] = completionHandler
dataCompletionHandlersForTasks[task.taskIdentifier] = { [weak self] data in
var responseError: FileProviderDropboxError?
if let code = (task.response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderDropboxError(code: rCode, path: targetPath, errorDescription: String(data: data, encoding: .utf8))
}
completionHandler?(responseError)
self?.delegateNotify(.create(path: targetPath), error: responseError)
}
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) -> OperationHandle? {
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)
@@ -171,18 +133,26 @@ internal extension DropboxFileProvider {
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)
completionHandlersForTasks[task.taskIdentifier] = completionHandler
dataCompletionHandlersForTasks[task.taskIdentifier] = { [weak self] data 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 = (task.response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderDropboxError(code: rCode, path: targetPath, errorDescription: String(data: data, encoding: .utf8))
// We can't fetch server result from delegate!
responseError = FileProviderDropboxError(code: rCode, path: targetPath, errorDescription: nil)
}
completionHandler?(responseError)
self?.delegateNotify(.create(path: targetPath), error: responseError)
completionHandler?(responseError ?? error)
self?.delegateNotify(.create(path: targetPath), error: responseError ?? error)
}
task.taskDescription = operation.json
task.resume()
@@ -193,8 +163,8 @@ internal extension DropboxFileProvider {
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
+26 -8
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,10 +324,10 @@ 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
}
@@ -330,7 +335,12 @@ public struct LocalFileInformationGenerator {
let playerItem = AVPlayerItem(url: fileURL)
let metadataList = playerItem.asset.commonMetadata
for item in metadataList {
if let description = makeDescription(item.commonKey) {
#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
@@ -366,7 +376,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 +394,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 {
+28
View File
@@ -23,6 +23,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
return (_underlyingSession.delegate as? FPSStreamDelegate)
}
fileprivate var _taskIdentifier: Int
fileprivate var _taskDescription: String?
/// Force using `URLSessionStreamTask` for iOS 9 and later
public var useURLSession = true
@@ -50,6 +51,33 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
return _taskIdentifier
}
/// An app-provided description of the current task.
///
/// This value may be nil. It is intended to contain human-readable strings that you can
/// then display to the user as part of your apps user interface.
open override var taskDescription: String? {
get {
if #available(iOS 9.0, OSX 10.11, *) {
if self.useURLSession {
return _underlyingTask!.taskDescription
}
}
return _taskDescription
}
@objc(setTaskDescription:)
set {
if #available(iOS 9.0, OSX 10.11, *) {
if self.useURLSession {
_underlyingTask!.taskDescription = newValue
return
}
}
_taskDescription = newValue
}
}
fileprivate var _state: URLSessionTask.State = .suspended
/**
* The current state of the taskactive, suspended, in the process
+207 -115
View File
@@ -25,15 +25,17 @@ open class FTPFileProvider: FileProviderBasicRemote {
}
open weak var delegate: FileProviderDelegate?
open var 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
/// Determine either FTP session is in passive or active mode.
/// - Note: Due to `URLSessionStreamTask` restrictions for determining listening port,
/// only passive sessions are available in current implementation.
public let passiveMode = true
public let passiveMode: Bool
/// Force to use URLSessionDownloadTask/URLSessionDataTask when possible
public var useAppleImplementation = true
@@ -41,66 +43,69 @@ open class FTPFileProvider: FileProviderBasicRemote {
fileprivate var _session: URLSession?
internal 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)
sessionDelegate?.didReceivedData = { [weak self] (session: URLSession, downloadTask: URLSessionDownloadTask, bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) -> Void in
guard let `self` = self else { return }
guard let opDic = downloadTask.taskDescription?.deserializeJSON(),
let opType = FileOperationType(json: opDic) else { return }
DispatchQueue.main.async {
let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(progress))
}
}
sessionDelegate?.didSendDataHandler = { [weak self] (session: Foundation.URLSession, task: URLSessionTask, bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) -> Void in
guard let `self` = self else { return }
guard let opDic = task.taskDescription?.deserializeJSON(),
let opType = FileOperationType(json: opDic) else { return }
DispatchQueue.main.async {
let progress = Double(totalBytesSent) / Double(totalBytesExpectedToSend)
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(progress))
}
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!
}
/**
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, credential: URLCredential? = nil, cache: URLCache? = 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)!
urlComponents.port = urlComponents.port ?? 21
urlComponents.scheme = urlComponents.scheme ?? "ftp"
self.baseURL = urlComponents.url!
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
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: [])
#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) {
guard let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL else { return nil }
self.init(baseURL: baseURL, credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential)
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")
@@ -114,6 +119,7 @@ open class FTPFileProvider: FileProviderBasicRemote {
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 {
@@ -127,10 +133,15 @@ open class FTPFileProvider: FileProviderBasicRemote {
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 {
@@ -138,8 +149,10 @@ open class FTPFileProvider: FileProviderBasicRemote {
}
}
public func contentsOfDirectory(path: String, completionHandler: @escaping (([FileObject], Error?) -> Void)) {
self.contentsOfDirectory(path: path, rfc3659enabled: true, completionHandler: completionHandler)
internal var serverSupportsRFC3659: Bool = true
open func contentsOfDirectory(path: String, completionHandler: @escaping (([FileObject], Error?) -> Void)) {
self.contentsOfDirectory(path: path, rfc3659enabled: serverSupportsRFC3659, completionHandler: completionHandler)
}
/**
@@ -153,7 +166,6 @@ open class FTPFileProvider: FileProviderBasicRemote {
`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)
@@ -171,6 +183,11 @@ open class FTPFileProvider: FileProviderBasicRemote {
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)
}
@@ -189,8 +206,8 @@ open class FTPFileProvider: FileProviderBasicRemote {
}
}
public func attributesOfItem(path: String, completionHandler: @escaping ((FileObject?, Error?) -> Void)) {
self.attributesOfItem(path: path, rfc3659enabled: true, completionHandler: completionHandler)
open func attributesOfItem(path: String, completionHandler: @escaping ((FileObject?, Error?) -> Void)) {
self.attributesOfItem(path: path, rfc3659enabled: serverSupportsRFC3659, completionHandler: completionHandler)
}
/**
@@ -229,22 +246,21 @@ open class FTPFileProvider: FileProviderBasicRemote {
}
guard let response = response, response.hasPrefix("250") || (response.hasPrefix("50") && rfc3659enabled) else {
let error = NSError(domain: URLError.errorDomain, code: URLError.badServerResponse.rawValue, userInfo: nil)
self.dispatch_queue.async {
completionHandler(nil, error)
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 {
let error = NSError(domain: URLError.errorDomain, code: URLError.badServerResponse.rawValue, userInfo: nil)
self.dispatch_queue.async {
completionHandler(nil, error)
completionHandler(nil, self.throwError(path, code: URLError.badServerResponse))
}
return
}
@@ -263,7 +279,21 @@ open class FTPFileProvider: FileProviderBasicRemote {
}
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
NotImplemented()
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)
}
}
}, completionHandler: {files, error in
if let error = error {
completionHandler([], error)
return
}
let foundFiles = files.filter { query.evaluate(with: $0.mapPredicate()) }
completionHandler(foundFiles, nil)
})
}
public func url(of path: String?) -> URL {
@@ -272,7 +302,21 @@ open class FTPFileProvider: FileProviderBasicRemote {
var baseUrlComponent = URLComponents(url: self.baseURL!, resolvingAgainstBaseURL: true)
baseUrlComponent?.user = credential?.user
baseUrlComponent?.password = credential?.password
return URL(string: path, relativeTo: baseURL) ?? baseURL!
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) {
@@ -346,10 +390,9 @@ extension FTPFileProvider: FileProviderOperations {
}
guard let response = response else {
let error = NSError(domain: URLError.errorDomain, code: URLError.badServerResponse.rawValue, userInfo: nil)
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
self.delegateNotify(opType, error: self.throwError(sourcePath, code: URLError.badServerResponse))
}
return
}
@@ -381,9 +424,7 @@ extension FTPFileProvider: FileProviderOperations {
default:
errorCode = URLError.cannotOpenFile
}
let escapedPath = sourcePath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? sourcePath
let url = NSURL(string: escapedPath, relativeTo: self.baseURL) ?? self.baseURL! as NSURL
let error = NSError(domain: URLError.errorDomain, code: errorCode.rawValue, userInfo: [NSURLErrorFailingURLErrorKey: url])
let error = self.throwError(sourcePath, code: errorCode)
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
@@ -417,56 +458,93 @@ extension FTPFileProvider: FileProviderOperations {
return
}
let secondOp = self.copyItem(localFile: localURL, to: destPath, completionHandler: completionHandler) as? RemoteOperationHandle
let secondOp = self.copyItem(localFile: localURL, to: destPath, completionHandler: { error in
completionHandler?(nil)
self.delegateNotify(opType, error: nil)
}) as? RemoteOperationHandle
operationHandle.tasks = secondOp?.tasks ?? []
}) as? RemoteOperationHandle
operationHandle.tasks = firstOp?.tasks ?? []
return operationHandle
}
private func fallbackRemove(_ opType: FileOperationType, on task: FileProviderStreamTask, recursive: Bool = false, completionHandler: SimpleCompletionHandler) {
private func fallbackRemove(_ opType: FileOperationType, on task: FileProviderStreamTask, completionHandler: SimpleCompletionHandler) {
guard let sourcePath = opType.source else { return }
switch recursive {
case true:
NotImplemented()
case false:
self.execute(command: "SITE RMDIR \(ftpPath(sourcePath))", on: task) { (response, error) in
if let error = error {
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
}
return
}
guard let response = response else {
let error = NSError(domain: URLError.errorDomain, code: URLError.badServerResponse.rawValue, userInfo: nil)
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
}
return
}
if response.hasPrefix("50") {
self.fallbackRemove(opType, on: task, recursive: true, completionHandler: completionHandler)
return
}
let escapedPath = sourcePath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? sourcePath
let url = NSURL(string: escapedPath, relativeTo: self.baseURL) ?? self.baseURL! as NSURL
let error = NSError(domain: URLError.errorDomain, code: URLError.cannotRemoveFile.rawValue, userInfo: [NSURLErrorFailingURLErrorKey: url])
self.execute(command: "SITE RMDIR \(ftpPath(sourcePath))", on: task) { (response, error) in
if let error = error {
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
}
return
}
guard let response = response else {
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, 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)
}
}
return
}
private func fallbackRecursiveRemove(_ opType: FileOperationType, 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 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
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) -> OperationHandle? {
// 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
@@ -485,6 +563,10 @@ extension FTPFileProvider: FileProviderOperations {
self.ftpStore(task, filePath: self.ftpPath(toPath), fromData: nil, fromFile: localFile, onTask: {
operation.add(task: $0)
}, onProgress: { bytesSent, totalSent, expectedBytes in
DispatchQueue.main.async {
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(Double(totalSent) / Double(expectedBytes)))
}
}, completionHandler: { (error) in
self.ftpQuit(task)
self.dispatch_queue.async {
@@ -505,29 +587,38 @@ extension FTPFileProvider: FileProviderOperations {
let operation = RemoteOperationHandle(operationType: opType, tasks: [])
if self.useAppleImplementation {
let task = session.downloadTask(with: url(of: path)) { (tempDest, response, error) in
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 let tempDest = tempDest {
if file?.isDirectory ?? false {
self.dispatch_queue.async {
let error = self.throwError(path, code: URLError.fileIsDirectory)
completionHandler?(error)
self.delegateNotify(opType, error: error)
}
return
}
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: tempDest, to: destURL)
self.dispatch_queue.async {
completionHandler?(nil)
}
} catch let error {
self.dispatch_queue.async {
completionHandler?(error)
}
try FileManager.default.moveItem(at: tempURL, to: destURL)
completionHandler?(nil)
} catch let e {
completionHandler?(e)
}
}
}
operation.add(task: task)
task.resume()
operation.add(task: task)
task.taskDescription = opType.json
task.resume()
})
} else {
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
self.ftpLogin(task) { (error) in
@@ -567,29 +658,26 @@ extension FTPFileProvider: FileProviderOperations {
}
extension FTPFileProvider: FileProviderReadWrite {
public func contents(path: String, completionHandler: @escaping ((Data?, Error?) -> Void)) -> OperationHandle? {
open func contents(path: String, completionHandler: @escaping ((Data?, Error?) -> Void)) -> OperationHandle? {
let opType = FileOperationType.fetch(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
if self.useAppleImplementation {
let task = session.dataTask(with: url(of: path)) { (data, response, error) in
if let error = error {
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)
}
let task = session.downloadTask(with: url(of: path))
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
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.resume()
return RemoteOperationHandle(operationType: opType, tasks: [task])
} else {
@@ -643,7 +731,7 @@ extension FTPFileProvider: FileProviderReadWrite {
return operation
}
public 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) -> OperationHandle? {
let opType = FileOperationType.modify(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
@@ -663,6 +751,10 @@ extension FTPFileProvider: FileProviderReadWrite {
let storeHandler = {
self.ftpStore(task, filePath: self.ftpPath(path), fromData: data ?? Data(), fromFile: nil, onTask: {
operation.add(task: $0)
}, onProgress: { bytesSent, totalSent, expectedBytes in
DispatchQueue.main.async {
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(Double(totalSent) / Double(expectedBytes)))
}
}, completionHandler: { (error) in
self.ftpQuit(task)
self.dispatch_queue.async {
@@ -687,7 +779,7 @@ extension FTPFileProvider: FileProviderReadWrite {
}
}
extension FTPFileProvider {
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
+244 -153
View File
@@ -8,9 +8,7 @@
import Foundation
extension FTPFileProvider {
private static let carriage = "\r\n"
internal extension FTPFileProvider {
func delegateNotify(_ operation: FileOperationType, error: Error?) {
DispatchQueue.main.async(execute: {
if error == nil {
@@ -48,7 +46,7 @@ extension FTPFileProvider {
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 + FTPFileProvider.carriage
let terminalcommand = command + "\r\n"
task.write(terminalcommand.data(using: .utf8)!, timeout: timeout) { (error) in
if let error = error {
completionHandler(nil, error)
@@ -67,10 +65,9 @@ extension FTPFileProvider {
}
if let data = data, let response = String(data: data, encoding: .utf8) {
completionHandler(response.trimmingCharacters(in: CharacterSet(charactersIn: FTPFileProvider.carriage)), nil)
completionHandler(response.trimmingCharacters(in: .whitespacesAndNewlines), nil)
} else {
let badResponseError = NSError(domain: URLError.errorDomain, code: URLError.cannotParseResponse.rawValue, userInfo: nil)
completionHandler(nil, badResponseError)
completionHandler(nil, self.throwError("", code: URLError.cannotParseResponse))
return
}
}
@@ -92,16 +89,12 @@ extension FTPFileProvider {
}
guard let data = data, let response = String(data: data, encoding: .utf8) else {
let error = NSError(domain: URLError.errorDomain, code: URLError.cannotParseResponse.rawValue, userInfo: nil)
completionHandler(error)
completionHandler(self.throwError("", code: URLError.cannotParseResponse))
return
}
guard response.hasPrefix("22") else {
let spaceIndex = response.characters.index(of: "-") ?? response.startIndex
let code = Int(response.substring(to: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
let description = response.substring(from: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)
let error = FileProviderFTPError(code: code, path: "", errorDescription: description)
let error = FileProviderFTPError(message: response)
completionHandler(error)
return
}
@@ -114,8 +107,7 @@ extension FTPFileProvider {
}
guard let response = response else {
let error = NSError(domain: URLError.errorDomain, code: URLError.badServerResponse.rawValue, userInfo: nil)
completionHandler(error)
completionHandler(self.throwError("", code: URLError.badServerResponse))
return
}
@@ -128,20 +120,16 @@ extension FTPFileProvider {
// needs password
if response.hasPrefix("33") {
self.execute(command: "PASS \(credential?.password ?? "fileprovider@")", on: task) { (response, error) in
if response?.hasPrefix("2") ?? false {
if response?.hasPrefix("23") ?? false {
completionHandler(nil)
} else {
let error = NSError(domain: URLError.errorDomain, code: URLError.userAuthenticationRequired.rawValue, userInfo: nil)
completionHandler(error)
completionHandler(self.throwError("", code: URLError.userAuthenticationRequired))
}
}
return
}
let spaceIndex = response.characters.index(of: "-") ?? response.startIndex
let code = Int(response.substring(to: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
let description = response.substring(from: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)
let error = FileProviderFTPError(code: code, path: "", errorDescription: description)
let error = FileProviderFTPError(message: response)
completionHandler(error)
return
}
@@ -149,8 +137,23 @@ extension FTPFileProvider {
if self.baseURL?.scheme == "ftps" || self.baseURL?.port == 990 {
self.execute(command: "AUTH TLS", on: task, minLength: 0, completionHandler: { (response, error) in
task.startSecureConnection()
loginHandle()
if let error = error {
completionHandler(error)
return
}
if let response = response, response.hasPrefix("23") {
task.startSecureConnection()
self.execute(command: "PBSZ 0\r\nPROT P", on: task, completionHandler: { (response, error) in
if let error = error {
completionHandler(error)
return
}
loginHandle()
})
}
})
} else {
loginHandle()
@@ -166,8 +169,7 @@ extension FTPFileProvider {
}
guard let response = response else {
let error = NSError(domain: URLError.errorDomain, code: URLError.badServerResponse.rawValue, userInfo: nil)
completionHandler(error)
completionHandler(self.throwError(path, code: URLError.badServerResponse))
return
}
@@ -177,10 +179,7 @@ extension FTPFileProvider {
}
// not logged in
else if response.hasPrefix("55") {
let spaceIndex = response.characters.index(of: "-") ?? response.startIndex
let code = Int(response.substring(to: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
let description = response.substring(from: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)
let error = FileProviderFTPError(code: code, path: "", errorDescription: description)
let error = FileProviderFTPError(message: response)
completionHandler(error)
return
}
@@ -199,16 +198,14 @@ extension FTPFileProvider {
return
}
guard let response = response, let destString = response.components(separatedBy: " ").flatMap({ $0 }).last else {
let error = NSError(domain: URLError.errorDomain, code: URLError.badServerResponse.rawValue, userInfo: nil)
completionHandler(nil, error)
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 {
let error = NSError(domain: URLError.errorDomain, code: URLError.badServerResponse.rawValue, userInfo: nil)
completionHandler(nil, error)
completionHandler(nil, self.throwError("", code: URLError.badServerResponse))
return
}
@@ -223,38 +220,43 @@ extension FTPFileProvider {
let passiveTask = self.session.fpstreamTask(withHostName: host, port: port)
passiveTask.resume()
if self.baseURL?.scheme == "ftps" || self.baseURL?.port == 990 {
task.startSecureConnection()
passiveTask.startSecureConnection()
}
completionHandler(passiveTask, nil)
}
}
func ftpActive(_ task: FileProviderStreamTask, completionHandler: @escaping (_ dataTask: FileProviderStreamTask?, _ error: Error?) -> Void) {
NotImplemented()
let port = 0
self.execute(command: "PORT \(port)", on: task) { (response, error) in
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 {
let error = NSError(domain: URLError.errorDomain, code: URLError.badServerResponse.rawValue, userInfo: nil)
completionHandler(nil, error)
activeTask.cancel()
completionHandler(nil, self.throwError("", code: URLError.badServerResponse))
return
}
guard !response.hasPrefix("5") else {
let error = NSError(domain: URLError.errorDomain, code: URLError.cannotConnectToHost.rawValue, userInfo: nil)
completionHandler(nil, error)
activeTask.cancel()
completionHandler(nil, self.throwError("", code: URLError.cannotConnectToHost))
return
}
let activeTask = self.session.fpstreamTask(withHostName: self.baseURL!.host!, port: 20)
activeTask.resume()
if self.baseURL?.scheme == "ftps" || self.baseURL?.port == 990 {
task.startSecureConnection()
}
completionHandler(activeTask, nil)
}
}
@@ -263,7 +265,9 @@ extension FTPFileProvider {
if self.passiveMode {
self.ftpPassive(task, completionHandler: completionHandler)
} else {
self.ftpActive(task, completionHandler: completionHandler)
dispatch_queue.async {
self.ftpActive(task, completionHandler: completionHandler)
}
}
}
@@ -275,13 +279,15 @@ extension FTPFileProvider {
}
// Successful
if response?.hasPrefix("35") ?? false {
guard let response = response else {
completionHandler(self.throwError("", code: URLError.badServerResponse))
return
}
if response.hasPrefix("35") {
completionHandler(nil)
} else {
let spaceIndex = response?.characters.index(of: "-") ?? response?.startIndex
let code = Int((response?.substring(to: spaceIndex!).trimmingCharacters(in: .whitespacesAndNewlines))!) ?? -1
let description = response?.substring(from: spaceIndex!).trimmingCharacters(in: .whitespacesAndNewlines)
let error = FileProviderFTPError(code: code, path: "", errorDescription: description)
let error = FileProviderFTPError(message: response, path: "")
completionHandler(error)
return
}
@@ -296,13 +302,13 @@ extension FTPFileProvider {
}
guard let dataTask = dataTask else {
let error = NSError(domain: URLError.errorDomain, code: URLError.badServerResponse.rawValue, userInfo: nil)
completionHandler([], error)
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: 70, afterSend: { error in
self.execute(command: command, on: task, minLength: 20, afterSend: { error in
// starting passive task
let timeout = self.session.configuration.timeoutIntervalForRequest
@@ -313,7 +319,7 @@ extension FTPFileProvider {
while !eof {
let group = DispatchGroup()
group.enter()
dataTask.readData(ofMinLength: 0, maxLength: 65535, timeout: timeout, completionHandler: { (data, seof, serror) in
dataTask.readData(ofMinLength: 1, maxLength: 65535, timeout: timeout, completionHandler: { (data, seof, serror) in
if let data = data {
finalData.append(data)
}
@@ -324,25 +330,25 @@ extension FTPFileProvider {
let waitResult = group.wait(timeout: .now() + timeout)
if let error = error {
completionHandler([], error)
if !((error as NSError).domain == URLError.errorDomain && (error as NSError).code == URLError.cancelled.rawValue) {
completionHandler([], error)
}
return
}
if waitResult == .timedOut {
error = NSError(domain: URLError.errorDomain, code: URLError.timedOut.rawValue, userInfo: nil)
completionHandler([], error)
completionHandler([], self.throwError(path, code: URLError.timedOut))
return
}
}
guard let response = String(data: finalData, encoding: .utf8) else {
error = NSError(domain: URLError.errorDomain, code: URLError.badServerResponse.rawValue, userInfo: nil)
completionHandler([], error)
completionHandler([], self.throwError(path, code: URLError.badServerResponse))
return
}
let contents = response.components(separatedBy: "\n").flatMap({ $0.trimmingCharacters(in: .whitespacesAndNewlines) })
let contents: [String] = response.components(separatedBy: "\n").flatMap({ $0.trimmingCharacters(in: .whitespacesAndNewlines) })
success = true
completionHandler(contents, nil)
return
}
@@ -353,22 +359,19 @@ extension FTPFileProvider {
}
guard let response = response else {
let badResponseError = NSError(domain: URLError.errorDomain, code: URLError.cannotParseResponse.rawValue, userInfo: nil)
completionHandler([], badResponseError)
completionHandler([], self.throwError(path, code: URLError.cannotParseResponse))
return
}
if response.hasPrefix("50") && useMLST {
self.ftpList(task, of: path, useMLST: false, completionHandler: completionHandler)
if response.hasPrefix("500") && useMLST {
dataTask.cancel()
self.serverSupportsRFC3659 = false
completionHandler([], self.throwError(path, code: URLError.unsupportedURL))
return
}
if !response.hasPrefix("25") {
let spaceIndex = response.characters.index(of: "-") ?? response.startIndex
let code = Int(response.substring(to: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
let description = response.substring(from: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)
let error = FileProviderFTPError(code: code, path: "", errorDescription: description)
if !success && !(response.hasPrefix("25") || response.hasPrefix("15")) {
let error = FileProviderFTPError(message: response, path: path)
self.dispatch_queue.async {
completionHandler([], error)
}
@@ -378,8 +381,50 @@ extension FTPFileProvider {
}
}
func ftpRecursiveList(_ task: FileProviderStreamTask, of path: String, useMLST: Bool, completionHandler: @escaping (_ contents: [String], _ error: Error?) -> Void) {
// TODO: Implement recursive listing for search and removing function
func recursiveList(path: String, useMLST: Bool, foundItemsHandler: ((_ contents: [FileObject]) -> Void)? = nil, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) {
let queue = DispatchQueue(label: "test")
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)
foundItemsHandler?(files)
let directories: [FileObject] = files.filter { $0.isDirectory }
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()
})
}
group.leave()
})
group.wait()
if success {
self.dispatch_queue.async {
completionHandler(result, nil)
}
}
}
}
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) {
@@ -402,13 +447,13 @@ extension FTPFileProvider {
}
guard let dataTask = dataTask else {
let error = NSError(domain: URLError.errorDomain, code: URLError.badServerResponse.rawValue, userInfo: nil)
completionHandler(nil, error)
completionHandler(nil, self.throwError(filePath, code: URLError.badServerResponse))
return
}
// Send retreive command
self.execute(command: "TYPE L" + FTPFileProvider.carriage + "REST \(position)" + FTPFileProvider.carriage + "RETR \(filePath)", on: task, minLength: 75, afterSend: { error in
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)
@@ -440,8 +485,7 @@ extension FTPFileProvider {
}
if waitResult == .timedOut {
error = NSError(domain: URLError.errorDomain, code: URLError.timedOut.rawValue, userInfo: nil)
completionHandler(nil, error)
completionHandler(nil, self.throwError(filePath, code: URLError.timedOut))
return
}
}
@@ -463,16 +507,12 @@ extension FTPFileProvider {
}
guard let response = response else {
let badResponseError = NSError(domain: URLError.errorDomain, code: URLError.cannotParseResponse.rawValue, userInfo: nil)
completionHandler(nil, badResponseError)
completionHandler(nil, self.throwError(filePath, code: URLError.cannotParseResponse))
return
}
if !(response.hasPrefix("1") || !response.hasPrefix("2")) {
let spaceIndex = response.characters.index(of: "-") ?? response.startIndex
let code = Int(response.substring(to: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
let description = response.substring(from: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)
let error = FileProviderFTPError(code: code, path: "", errorDescription: description)
let error = FileProviderFTPError(message: response)
self.dispatch_queue.async {
completionHandler(nil, error)
@@ -511,13 +551,13 @@ extension FTPFileProvider {
}
guard let dataTask = dataTask else {
let error = NSError(domain: URLError.errorDomain, code: URLError.badServerResponse.rawValue, userInfo: nil)
completionHandler(nil, error)
completionHandler(nil, self.throwError(filePath, code: URLError.badServerResponse))
return
}
// Send retreive command
self.execute(command: "TYPE I" + FTPFileProvider.carriage + "REST \(position)" + FTPFileProvider.carriage + "RETR \(filePath)", on: task, minLength: 75, afterSend: { error in
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)
@@ -549,7 +589,7 @@ extension FTPFileProvider {
}
if waitResult == .timedOut {
error = NSError(domain: URLError.errorDomain, code: URLError.timedOut.rawValue, userInfo: nil)
error = self.throwError("", code: URLError.timedOut)
completionHandler(nil, error)
return
}
@@ -580,16 +620,12 @@ extension FTPFileProvider {
}
guard let response = response else {
let badResponseError = NSError(domain: URLError.errorDomain, code: URLError.cannotParseResponse.rawValue, userInfo: nil)
completionHandler(nil, badResponseError)
completionHandler(nil, self.throwError(filePath, code: URLError.cannotParseResponse))
return
}
if !(response.hasPrefix("1") || !response.hasPrefix("2")) {
let spaceIndex = response.characters.index(of: "-") ?? response.startIndex
let code = Int(response.substring(to: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
let description = response.substring(from: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)
let error = FileProviderFTPError(code: code, path: "", errorDescription: description)
if !(response.hasPrefix("1") || response.hasPrefix("2")) {
let error = FileProviderFTPError(message: response)
self.dispatch_queue.async {
completionHandler(nil, error)
@@ -601,8 +637,71 @@ extension FTPFileProvider {
}
}
func ftpStore(_ task: FileProviderStreamTask, filePath: String, fromData: Data?, fromFile: URL?, onTask: ((_ task: FileProviderStreamTask) -> Void)?, completionHandler: @escaping (_ error: Error?) -> Void) {
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)
@@ -610,13 +709,14 @@ extension FTPFileProvider {
}
guard let dataTask = dataTask else {
let error = NSError(domain: URLError.errorDomain, code: URLError.badServerResponse.rawValue, userInfo: nil)
completionHandler(error)
completionHandler(self.throwError(filePath, code: URLError.badServerResponse))
return
}
// Send retreive command
self.execute(command: "TYPE L" + FTPFileProvider.carriage + "STOR \(filePath)", on: task, minLength: 75, afterSend: { error in
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 {
@@ -624,72 +724,41 @@ extension FTPFileProvider {
}
onTask?(dataTask)
DispatchQueue.global().async {
var error: Error?
if let data = fromData {
dataTask.write(data, timeout: timeout, completionHandler: { (error) in
completionHandler(error)
})
dataTask.closeWrite()
if data.count == 0 { return }
dataTask.write(data, timeout: timeout, completionHandler: { (error) in
if let error = error {
completionHandler(error)
return
}
success = true
guard let file = fromFile, let fileHandle = FileHandle(forReadingAtPath: file.path) else { return }
fileHandle.seek(toFileOffset: 0)
var eof = false
while !eof {
let group = DispatchGroup()
group.enter()
let data = fileHandle.readData(ofLength: 65536)
eof = data.count < 65536
dataTask.write(data, timeout: timeout, completionHandler: { (serror) in
error = serror
group.leave()
})
let waitResult = group.wait(timeout: .now() + timeout)
if let error = error {
completionHandler(error)
return
}
if waitResult == .timedOut {
error = NSError(domain: URLError.errorDomain, code: URLError.timedOut.rawValue, userInfo: nil)
completionHandler(error)
return
}
}
dataTask.closeRead()
dataTask.closeWrite()
completionHandler(nil)
return
}
})
}) { (response, error) in
guard success else { return }
if let error = error {
completionHandler(error)
return
}
guard let response = response else {
let badResponseError = NSError(domain: URLError.errorDomain, code: URLError.cannotParseResponse.rawValue, userInfo: nil)
completionHandler(badResponseError)
completionHandler(self.throwError(filePath, code: URLError.cannotParseResponse))
return
}
if !(response.hasPrefix("1") || !response.hasPrefix("2")) {
let spaceIndex = response.characters.index(of: "-") ?? response.startIndex
let code = Int(response.substring(to: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
let description = response.substring(from: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)
let error = FileProviderFTPError(code: code, path: "", errorDescription: description)
if !(response.hasPrefix("1") || response.hasPrefix("2")) {
let error = FileProviderFTPError(message: response)
self.dispatch_queue.async {
completionHandler(error)
}
return
}
completionHandler(nil)
}
}
}
@@ -778,7 +847,8 @@ extension FTPFileProvider {
guard components.count > 1 else { return nil }
let nameOrPath = components.removeLast().trimmingCharacters(in: .whitespacesAndNewlines)
let correctedPath: String, name: String
var correctedPath: String
let name: String
if nameOrPath.hasPrefix("/") {
correctedPath = nameOrPath.replacingOccurrences(of: baseURL!.path, with: "", options: .anchored)
name = (nameOrPath as NSString).lastPathComponent
@@ -786,6 +856,9 @@ extension FTPFileProvider {
name = nameOrPath
correctedPath = (path as NSString).appendingPathComponent(nameOrPath)
}
if correctedPath.hasPrefix("/") {
correctedPath.characters.removeFirst()
}
var attributes = [String: String]()
for component in components {
@@ -794,7 +867,7 @@ extension FTPFileProvider {
attributes[keyValue[0].lowercased()] = keyValue.dropFirst().joined(separator: "=")
}
let file = FileObject(url: url(of: path), name: name, path: correctedPath)
let file = FileObject(url: url(of: correctedPath), name: name, path: correctedPath)
let dateFormatter = DateFormatter()
dateFormatter.calendar = Calendar(identifier: .gregorian)
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
@@ -828,7 +901,7 @@ extension FTPFileProvider {
file.size = Int64(attribute) ?? -1
case "media-type":
file.allValues[.mimeType] = attribute
file.allValues[.mimeTypeKey] = attribute
default:
break
@@ -847,4 +920,22 @@ public struct FileProviderFTPError: 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
}
}
}
+26 -18
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 {
@@ -172,9 +180,9 @@ open class FileObject: Equatable {
/// Converts macOS spotlight query for searching files to a query that can be used for `searchFiles()` method
static public func convertPredicate(fromSpotlight query: NSPredicate) -> NSPredicate {
let mapDict: [String: URLResourceKey] = [NSMetadataItemURLKey: .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) }
+66 -25
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 }
@@ -132,6 +132,15 @@ 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)
}
@@ -195,6 +204,12 @@ public protocol FileProviderBasicRemote: FileProviderBasic {
var validatingCache: Bool { get set }
}
internal protocol FileProviderBasicRemoteInternal: FileProviderBasic {
var completionHandlersForTasks: [Int: SimpleCompletionHandler] { get set }
var downloadCompletionHandlersForTasks: [Int: (URL) -> Void] { get set }
var dataCompletionHandlersForTasks: [Int: (Data) -> Void] { get set }
}
internal extension FileProviderBasicRemote {
func returnCachedDate(with request: URLRequest, validatingCache: Bool, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Swift.Void) -> Bool {
guard let cache = self.cache else { return false }
@@ -596,21 +611,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 +657,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 +714,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 +734,8 @@ extension FileProviderBasic {
let domain: String
switch code {
case is URLError:
fallthrough
case is URLError.Code:
domain = NSURLErrorDomain
default:
domain = NSCocoaErrorDomain
@@ -829,19 +867,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 +977,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":
+68 -10
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,17 @@ 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")
}
internal extension URL {
@@ -62,6 +68,58 @@ 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")
}
mutating func set(dropboxArgKey requestDictionary: [String: AnyObject]) {
if let requestJson = String(jsonDictionary: requestDictionary) {
self.setValue(requestJson, forHTTPHeaderField: "Dropbox-API-Arg")
}
}
}
internal extension Data {
internal var isPDF: Bool {
return self.count > 4 && self.scanString(length: 4, using: .ascii) == "%PDF"
@@ -151,7 +209,7 @@ internal extension TimeInterval {
}
}
extension Date {
internal extension Date {
init?(rfcString: String) {
let dateFor: DateFormatter = DateFormatter()
dateFor.locale = Locale(identifier: "en_US")
@@ -188,7 +246,7 @@ extension Date {
}
}
extension NSPredicate {
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
+30 -22
View File
@@ -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
@@ -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 {
@@ -256,7 +255,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
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
@@ -265,7 +264,8 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
@discardableResult
fileprivate func doOperation(_ opType: FileOperationType, data: Data? = nil, atomically: Bool = false, forUploading: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let localOperationHandle = LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
func urlofpath(path: String) -> URL {
if path.hasPrefix("file://") {
let removedSchemePath = path.replacingOccurrences(of: "file://", with: "", options: .anchored)
@@ -299,6 +299,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
let operationHandler: (URL, URL?) -> Void = { source, dest in
do {
localOperationHandle.inProgress = true
switch opType {
case .create:
if sourcePath.hasSuffix("/") {
@@ -322,7 +323,8 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
if successfulSecurityScopedResourceAccess {
source.stopAccessingSecurityScopedResource()
}
localOperationHandle.inProgress = false
self.dispatch_queue.async {
completionHandler?(nil)
}
@@ -374,18 +376,20 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
operationHandler(source, dest)
}
}
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
return localOperationHandle
}
@discardableResult
open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
let opType = FileOperationType.fetch(path: path)
let localOperationHandle = LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
let url = self.url(of: path)
let operationHandler: (URL) -> Void = { url in
do {
localOperationHandle.inProgress = true
let data = try Data(contentsOf: url)
localOperationHandle.inProgress = false
self.dispatch_queue.async {
completionHandler(data, nil)
}
@@ -411,8 +415,8 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
operationHandler(url)
}
}
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
return localOperationHandle
}
@discardableResult
@@ -423,12 +427,13 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
}
return nil
}
if offset == 0 && length < 0 {
return self.contents(path: path, completionHandler: completionHandler)
}
let opType = FileOperationType.fetch(path: path)
let localOperationHandle = LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
let url = self.url(of: path)
let operationHandler: (URL) -> Void = { url in
@@ -442,9 +447,11 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
defer {
handle.closeFile()
}
localOperationHandle.inProgress = true
let size = LocalFileObject(fileWithURL: url)?.size ?? -1
guard size > offset else {
localOperationHandle.inProgress = false
self.dispatch_queue.async {
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadTooLarge as FoundationErrorEnum))
}
@@ -452,6 +459,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
}
handle.seek(toFileOffset: UInt64(offset))
guard Int64(handle.offsetInFile) == offset else {
localOperationHandle.inProgress = false
self.dispatch_queue.async {
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadTooLarge as FoundationErrorEnum))
}
@@ -459,7 +467,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
}
let data = handle.readData(ofLength: length)
localOperationHandle.inProgress = false
self.dispatch_queue.async {
completionHandler(data, nil)
}
@@ -479,7 +487,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
}
}
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
return localOperationHandle
}
@discardableResult
+11 -8
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)
}
@@ -24,7 +24,8 @@ public final class LocalFileObject: FileObject {
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,14 +217,17 @@ internal class LocalFileProviderManagerDelegate: NSObject, FileManagerDelegate {
}
}
/// Local operation handling is limited. Please don't use as much as possible.
/// - Note: Local operation handling is limited. Please don't use as much as possible.
open class LocalOperationHandle: OperationHandle {
/// Url of file which operation is doing on
public let baseURL: URL
/// Type of operation
public let operationType: FileOperationType
init (operationType: FileOperationType, baseURL: URL?) {
self.baseURL = baseURL ?? URL(fileURLWithPath: "/")
self.operationType = operationType
inProgress = false
}
private var sourceURL: URL? {
@@ -277,10 +281,9 @@ open class LocalOperationHandle: OperationHandle {
}
/// Not usable in local provider
open var inProgress: Bool {
return false
}
open var inProgress: Bool
/// Not usable in local provider
open func cancel() -> Bool{
return false
@@ -33,7 +33,11 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
}
open weak var delegate: FileProviderDelegate?
open var 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
@@ -41,16 +45,30 @@ open class OneDriveFileProvider: 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 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!
}
/**
@@ -69,16 +87,22 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
*/
public init(credential: URLCredential?, serverURL: URL? = nil, drive: String = "root", cache: URLCache? = nil) {
let baseURL = serverURL?.absoluteURL ?? URL(string: "https://api.onedrive.com/")!
self.baseURL = baseURL.path.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")
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
@@ -206,7 +234,7 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
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)
@@ -257,10 +285,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)
@@ -279,6 +307,14 @@ extension OneDriveFileProvider: FileProviderOperations {
}
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
// 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
@@ -292,11 +328,10 @@ extension OneDriveFileProvider: FileProviderOperations {
return nil
}
var request = URLRequest(url: self.url(of: path, modifier: "content"))
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.set(httpAuthentication: credential, with: .oAuth2)
let task = session.downloadTask(with: request)
completionHandlersForTasks[task.taskIdentifier] = completionHandler
downloadCompletionHandlersForTasks[task.taskIdentifier] = { tempURL in
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = completionHandler
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
@@ -329,20 +364,27 @@ extension OneDriveFileProvider: FileProviderReadWrite {
let opType = FileOperationType.fetch(path: path)
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 + Int64(length) - 1)", forHTTPHeaderField: "Range")
} else if offset > 0 && length < 0 {
request.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
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
completionHandler(nil, error)
}
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))
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(nil, serverError)
return
}
let filedata = serverError ?? error == nil ? data : nil
completionHandler(filedata, serverError ?? error)
})
do {
let data = try Data(contentsOf: tempURL)
completionHandler(data, nil)
} catch let e {
completionHandler(nil, e)
}
}
task.taskDescription = opType.json
task.resume()
return RemoteOperationHandle(operationType: opType, tasks: [task])
@@ -369,19 +411,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]
@@ -434,7 +467,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) {
@@ -453,7 +486,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]()
+27 -48
View File
@@ -18,11 +18,11 @@ 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("/") {
var rpath = path.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? path
if rpath.hasPrefix("/") {
rpath.remove(at: rpath.startIndex)
}
let url = URL(string: rpath, relativeTo: baseURL) ?? URL(string: path)!
let url = URL(string: rpath, relativeTo: baseURL) ?? URL(string: rpath)!
super.init(url: url, name: name, path: path)
}
@@ -61,20 +61,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 +85,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,37 +113,8 @@ 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)
completionHandlersForTasks[task.taskIdentifier] = completionHandler
dataCompletionHandlersForTasks[task.taskIdentifier] = { [weak self] data in
var responseError: FileProviderOneDriveError?
if let code = (task.response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderOneDriveError(code: rCode, path: targetPath, errorDescription: String(data: data, encoding: .utf8))
}
completionHandler?(responseError)
self?.delegateNotify(.create(path: targetPath), error: responseError)
}
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) -> OperationHandle? {
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)
@@ -154,17 +125,25 @@ internal extension OneDriveFileProvider {
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)
completionHandlersForTasks[task.taskIdentifier] = completionHandler
dataCompletionHandlersForTasks[task.taskIdentifier] = { [weak self] data 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 = (task.response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderOneDriveError(code: rCode, path: targetPath, errorDescription: String(data: data, encoding: .utf8))
// We can't fetch server result from delegate!
responseError = FileProviderOneDriveError(code: rCode, path: targetPath, errorDescription: nil)
}
completionHandler?(responseError)
self?.delegateNotify(.create(path: targetPath), error: responseError)
completionHandler?(responseError ?? error)
self?.delegateNotify(.create(path: targetPath), error: responseError ?? error)
}
task.taskDescription = operation.json
task.resume()
@@ -177,7 +156,7 @@ internal extension OneDriveFileProvider {
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?
+89 -35
View File
@@ -72,6 +72,8 @@ open class RemoteOperationHandle: OperationHandle {
/// 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
@@ -90,58 +92,101 @@ extension FileProviderHTTPError {
}
}
internal var completionHandlersForTasks = [Int: SimpleCompletionHandler]()
internal var downloadCompletionHandlersForTasks = [Int: (URL) -> Void]()
internal var dataCompletionHandlersForTasks = [Int: (Data) -> Void]()
/// 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
}
class SessionDelegate: NSObject, URLSessionDataDelegate, URLSessionDownloadDelegate, URLSessionStreamDelegate {
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: URLSession, _ downloadTask: URLSessionDownloadTask, _ didFinishDownloadingToURL: URL) -> Void)?
var didSendDataHandler: ((_ session: URLSession, _ task: URLSessionTask, _ bytesSent: Int64, _ totalBytesSent: Int64, _ totalBytesExpectedToSend: Int64) -> Void)?
var didReceivedData: ((_ session: URLSession, _ downloadTask: URLSessionDownloadTask, _ bytesWritten: Int64, _ totalBytesWritten: Int64, _ totalBytesExpectedToWrite: Int64) -> Void)?
var didBecomeStream :((_ session: URLSession, _ taskId: Int, _ didBecome: InputStream, _ outputStream: OutputStream) -> 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
}
// codebeat:disable[ARITY]
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if error != nil {
let completionHandler = completionHandlersForTasks[task.taskIdentifier] ?? nil
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if !(error == nil && task is URLSessionDownloadTask) {
let completionHandler = completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] ?? nil
completionHandler?(error)
completionHandlersForTasks.removeValue(forKey: task.taskIdentifier)
_ = completionHandlersForTasks[session.sessionDescription!]?.removeValue(forKey: task.taskIdentifier)
}
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
let completionHandler = dataCompletionHandlersForTasks[dataTask.taskIdentifier] ?? nil
completionHandler?(data)
completionHandlersForTasks.removeValue(forKey: dataTask.taskIdentifier)
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
self.finishDownloadHandler?(session, downloadTask, location)
let dcompletionHandler = downloadCompletionHandlersForTasks[downloadTask.taskIdentifier]
dcompletionHandler?(location)
completionHandlersForTasks.removeValue(forKey: downloadTask.taskIdentifier)
guard let json = downloadTask.taskDescription?.deserializeJSON(),
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 {
fileProvider.delegate?.fileproviderSucceed(fileProvider, operation: op)
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(),
@@ -149,6 +194,15 @@ 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)
DispatchQueue.main.async {
@@ -156,7 +210,7 @@ class SessionDelegate: NSObject, URLSessionDataDelegate, URLSessionDownloadDeleg
}
}
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(),
@@ -169,11 +223,11 @@ class SessionDelegate: NSObject, URLSessionDataDelegate, URLSessionDownloadDeleg
}
}
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)
}
@@ -189,7 +243,7 @@ class SessionDelegate: NSObject, URLSessionDataDelegate, URLSessionDownloadDeleg
}
@available(iOS 9.0, macOS 10.11, *)
func urlSession(_ session: URLSession, streamTask: URLSessionStreamTask, didBecome inputStream: InputStream, outputStream: OutputStream) {
public func urlSession(_ session: URLSession, streamTask: URLSessionStreamTask, didBecome inputStream: InputStream, outputStream: OutputStream) {
self.didBecomeStream?(session, streamTask.taskIdentifier, inputStream, outputStream)
}
}
+10 -10
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: FileProviderStreamTask {
class FileProviderSMBTask: FileProviderStreamTask {
var timeout: TimeInterval = 30
private(set) var lastMessageID: UInt64 = 0
@@ -31,14 +31,14 @@ class SMB2ProtocolClient: FileProviderStreamTask {
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.write(data, timeout: 0, completionHandler: { (e) in
self.write(data, timeout: timeout, completionHandler: { (e) in
completionHandler?(e)
})
return mId
@@ -50,7 +50,7 @@ class SMB2ProtocolClient: FileProviderStreamTask {
let smbHeader = SMB2.Header(command: SMB2.Command.SESSION_SETUP, creditRequestResponse: credit, messageId: mId, treeId: UInt32(0), sessionId: sessionId)
let msg = SMB2.SessionSetupRequest(singing: [])
let data = createSMB2Message(header: smbHeader, message: msg)
self.write(data, timeout: 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
// TODO: set session id
@@ -73,7 +73,7 @@ class SMB2ProtocolClient: FileProviderStreamTask {
let tcHeader = SMB2.TreeConnectRequest.Header(flags: [])
let msg = SMB2.TreeConnectRequest(header: tcHeader, host: host, share: share)
let data = createSMB2Message(header: smbHeader, message: msg!)
self.write(data, timeout: 0, completionHandler: { (e) in
self.write(data, timeout: timeout, completionHandler: { (e) in
completionHandler?(e)
})
@@ -85,7 +85,7 @@ class SMB2ProtocolClient: FileProviderStreamTask {
let smbHeader = SMB2.Header(command: .TREE_DISCONNECT, creditRequestResponse: 111, messageId: mId, treeId: treeId, sessionId: sessionId)
let msg = SMB2.TreeDisconnect()
let data = createSMB2Message(header: smbHeader, message: msg)
self.write(data, timeout: 0, completionHandler: { (e) in
self.write(data, timeout: timeout, completionHandler: { (e) in
completionHandler?(e)
})
return mId
@@ -96,7 +96,7 @@ class SMB2ProtocolClient: FileProviderStreamTask {
let smbHeader = SMB2.Header(command: .LOGOFF, creditRequestResponse: 0, messageId: mId, treeId: 0, sessionId: sessionId)
let msg = SMB2.LogOff()
let data = createSMB2Message(header: smbHeader, message: msg)
self.write(data, timeout: 0, completionHandler: { (e) in
self.write(data, timeout: timeout, completionHandler: { (e) in
completionHandler?(e)
})
return mId
@@ -108,7 +108,7 @@ class SMB2ProtocolClient: FileProviderStreamTask {
}
// 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
+8 -2
View File
@@ -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
}
+254 -56
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,6 +33,7 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
}
public weak var delegate: FileProviderDelegate?
public var credentialType: HTTPAuthenticationType = .digest
open var credential: URLCredential? {
didSet {
sessionDelegate?.credential = credential
@@ -44,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!
}
/**
@@ -68,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("")).absoluteURL
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) {
@@ -94,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 {
@@ -113,6 +135,10 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
}
deinit {
if let sessionuuid = _session?.sessionDescription {
removeSessionHandler(for: sessionuuid)
}
if fileProviderCancelTasksOnInvalidating {
_session?.invalidateAndCancel()
} else {
@@ -120,14 +146,30 @@ 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
var responseError: FileProviderWebDavError?
@@ -149,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?
@@ -182,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
@@ -204,7 +263,8 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
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
// FIXME: paginating results
@@ -235,7 +295,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
@@ -254,9 +315,10 @@ extension WebDAVFileProvider: FileProviderOperations {
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
let url = self.url(of: atPath).appendingPathComponent(folderName.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? folderName, isDirectory: true)
let url = self.url(of: atPath).appendingPathComponent(folderName, isDirectory: true)
var request = URLRequest(url: url)
request.httpMethod = "MKCOL"
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) {
@@ -297,7 +359,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) -> OperationHandle? {
let source = opType.source!
let sourceURL = self.url(of: source)
var request = URLRequest(url: sourceURL)
@@ -315,6 +377,7 @@ extension WebDAVFileProvider: FileProviderOperations {
return nil
}
request.set(httpAuthentication: credential, with: credentialType)
if let overwrite = overwrite, !overwrite {
request.setValue("F", forHTTPHeaderField: "Overwrite")
}
@@ -345,6 +408,14 @@ extension WebDAVFileProvider: FileProviderOperations {
@discardableResult
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
// 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
@@ -355,15 +426,16 @@ extension WebDAVFileProvider: FileProviderOperations {
request.setValue("F", forHTTPHeaderField: "Overwrite")
}
request.httpMethod = "PUT"
request.set(httpAuthentication: credential, with: credentialType)
let task = session.uploadTask(with: request, fromFile: localFile)
completionHandlersForTasks[task.taskIdentifier] = completionHandler
dataCompletionHandlersForTasks[task.taskIdentifier] = { [weak self] data in
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { [weak self] error in
var responseError: FileProviderWebDavError?
if let code = (task.response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderWebDavError(code: rCode, path: toPath, errorDescription: String(data: data, encoding: .utf8), url: url)
// We can't fetch server result from delegate!
responseError = FileProviderWebDavError(code: rCode, path: toPath, errorDescription: nil, url: url)
}
completionHandler?(responseError)
self?.delegateNotify(.create(path: toPath), error: responseError)
completionHandler?(responseError ?? error)
self?.delegateNotify(.create(path: toPath), error: responseError ?? error)
}
task.taskDescription = opType.json
task.resume()
@@ -377,10 +449,11 @@ extension WebDAVFileProvider: FileProviderOperations {
return nil
}
let url = self.url(of:path)
let request = URLRequest(url: url)
var request = URLRequest(url: url)
request.set(httpAuthentication: credential, with: credentialType)
let task = session.downloadTask(with: request)
completionHandlersForTasks[task.taskIdentifier] = completionHandler
downloadCompletionHandlersForTasks[task.taskIdentifier] = { tempURL in
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = completionHandler
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
@@ -413,20 +486,32 @@ extension WebDAVFileProvider: FileProviderReadWrite {
let opType = FileOperationType.fetch(path: path)
let url = self.url(of: path)
var request = URLRequest(url: url)
request.httpMethod = "GET"
if length > 0 {
request.setValue("bytes=\(offset)-\(offset + Int64(length) - 1)", forHTTPHeaderField: "Range")
} else if offset > 0 && length < 0 {
request.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
request.set(httpAuthentication: credential, with: credentialType)
request.set(rangeWithOffset: offset, length: length)
let task = session.downloadTask(with: request)
let handle = RemoteOperationHandle(operationType: opType, tasks: [task])
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
completionHandler(nil, error)
}
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)
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(nil, serverError)
return
}
completionHandler(data, responseError ?? error)
})
do {
let data = try Data(contentsOf: tempURL)
self.dispatch_queue.async {
completionHandler(data, nil)
}
} catch let e {
completionHandler(nil, e)
}
}
task.taskDescription = opType.json
task.resume()
return handle
}
@@ -440,18 +525,19 @@ extension WebDAVFileProvider: FileProviderReadWrite {
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 ?? Data())
completionHandlersForTasks[task.taskIdentifier] = completionHandler
dataCompletionHandlersForTasks[task.taskIdentifier] = { [weak self] data in
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { [weak self] error in
var responseError: FileProviderWebDavError?
if let code = (task.response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data, encoding: .utf8), url: url)
// We can't fetch server result from delegate!
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: nil, url: url)
}
completionHandler?(responseError)
self?.delegateNotify(.create(path: path), error: responseError)
completionHandler?(responseError ?? error)
self?.delegateNotify(opType, error: responseError ?? error)
}
task.taskDescription = opType.json
task.resume()
@@ -474,6 +560,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
@@ -500,7 +666,7 @@ struct DavResponse {
func standardizePath(_ str: String) -> String {
let trimmedStr = str.hasPrefix("/") ? str.substring(from: str.index(after: str.startIndex)) : str
return trimmedStr.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? str
return trimmedStr.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlPathAllowed.subtracting(CharacterSet(charactersIn: ":"))) ?? str
}
// find node names with namespace
@@ -591,7 +757,7 @@ 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)
@@ -607,22 +773,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.