Compare commits

..

48 Commits

Author SHA1 Message Date
Amir Abbas b2a7800f7c Fixed typo 2017-08-15 15:08:06 +04:30
Amir Abbas 96c102d156 Updated readme and podspec to ver 0.19.0 2017-08-15 14:57:36 +04:30
Amir Abbas 8aedd8e72a Replaced OperationHandle with (NS)Progress 2017-08-15 13:42:41 +04:30
Amir Abbas 5b55debe8a Fix warning in OneDrive provider 2017-08-12 17:50:00 +04:30
Amir Abbas Mousavian 0fa062a946 Merge pull request #59 from evilutioner/master
OneDrive: list folder issue fixed, removed escape symbols in path
2017-08-11 20:43:28 +04:30
Oleg Marchik 879f86c1f9 Merge branch 'master' into master 2017-08-11 18:07:25 +03:00
Oleg Marchik 80d5f02bbd OneDrive: list folder issue fixed, removed escape symbols in path 2017-08-11 18:04:18 +03:00
Amir Abbas Mousavian b8a7721b2f Merge pull request #58 from evilutioner/master
Dropbox bug fixing: added unicode symbols support in http header path
2017-08-10 22:29:08 +04:30
Oleg Marchik 44b4784cd3 Important dropbox bug fixing: added unicode symbols support in path 2017-08-10 11:36:15 +03:00
Amir Abbas 7d9e2247f2 Fixed Compile error, removed redundant comments 2017-08-04 00:30:46 +04:30
Amir Abbas Mousavian 0ace562442 Merge pull request #57 from evilutioner/master
Fixed outdated OneDrive API
2017-08-04 00:27:36 +04:30
evilutioner 63d831ef90 Merge branch 'master' into master 2017-08-01 16:08:51 +03:00
Oleg Marchik d6b91348a3 Fixed outdated OneDrive API 2017-08-01 12:55:39 +03:00
Amir Abbas fd9d4c1ab4 Probable fix for # 55 (OneDrive url issue) 2017-07-31 20:10:40 +04:30
Amir Abbas 41e266c2a9 Fix #54(FTP login), Refactored ExtendedLocalFileProvider 2017-07-17 05:36:30 +04:30
Amir Abbas 6127f4a7d9 Fixed Readme errors 2017-07-14 19:28:41 +04:30
Amir Abbas faca943beb Updated Readme fot FTP and SMB description 2017-07-14 16:49:55 +04:30
Amir Abbas 9345436fa2 Enhanced PDF Meta-info, Better FTPS handling 2017-07-14 16:49:24 +04:30
Amir Abbas 6228d88d41 Adjusting readme badges 2017-07-05 21:35:15 +04:30
Amir Abbas 5b395915a9 Updated Readme for Pods and SPM file 2017-07-02 18:26:34 +04:30
Amir Abbas e4f12a502b Fixed repo name and Readme 2017-07-01 16:57:23 +04:30
Amir Abbas 8faad0ec65 Fixed project name in schemes 2017-07-01 14:14:45 +04:30
Amir Abbas bd5569d213 Updated scheme names 2017-07-01 14:09:39 +04:30
Amir Abbas 14ed279879 Renamed framework for compatibility with iOS 11 2017-07-01 13:50:22 +04:30
Amir Abbas fd293f7bdb [Amend] Fixed swift 3.0 compile error for type(of:) 2017-07-01 11:08:49 +04:30
Amir Abbas 3d9625e243 Fixed swift 3.0 compile error for type(of:) 2017-07-01 11:03:22 +04:30
Amir Abbas 0d017ebfc4 Fix #52 (Swift 4.0 source compatibility) 2017-07-01 10:47:48 +04:30
Amir Abbas fd36df67f3 FTP passive property encoding
- removed redundant protocol conforming
2017-07-01 09:43:45 +04:30
Amir Abbas 5be8c4ef5e podspec and version update 2017-06-27 14:02:00 +04:30
Amir Abbas 0374fd7688 Making ftp active data task creation async 2017-06-24 12:09:16 +04:30
Amir Abbas 5ddfa43555 Fix #49 (Workaround colon in url path bug in NSURL relative url) 2017-06-24 12:04:50 +04:30
Amir Abbas 21850bb548 Probable fix for #51 2017-06-24 11:44:12 +04:30
Amir Abbas b166e111e0 FTP Active mode, FTPS bugfix, possible fix for #47 2017-05-25 17:33:56 +04:30
Amir Abbas 1dd7561215 Caching ftp server support for RFC3659, Possible fix for #47 2017-05-23 19:10:04 +04:30
Amir Abbas f94719deb0 Fixes #39 (FTP listing), Error domain determination 2017-05-05 13:02:07 +04:30
Amir Abbas b13df0a977 Fixes #38 (Creating folder on WebDAV) 2017-05-05 10:06:17 +04:30
Amir Abbas 24af7aa4c2 Fixed LocalFileProvider init baseURL issue 2017-04-20 01:27:59 +04:30
Amir Abbas 06039ad993 CloudFileProvider operation handle improvements 2017-04-20 01:06:36 +04:30
Amir Abbas Mousavian d8fec3e346 Merge pull request #37 from hansvdam/master
Making `inProgress` of LocalFileProvider working
2017-04-18 14:22:08 +04:30
Hans van Dam 5c93bc8731 making 'inProgress' of LocalFileProvider work more consistently 2017-04-18 11:03:35 +02:00
Hans van Dam 61ba245189 making 'inProgress' of LocalFileProvider work 2017-04-17 18:16:04 +02:00
Amir Abbas 02e6cd37dd WebDAV OAuth 1,2 support 2017-04-16 19:19:17 +04:30
Amir Abbas 55608fb8d0 New FileProviderSharing protocol for publicLinks
- fixed DropboxFileProvider.propertiesOfFile() bug
- minor URLRequest refactors
- Added documentation and scope declaration
2017-04-16 19:07:45 +04:30
Amir Abbas 3e3582f6fa Changed DropboxFileProvider.type constant to “Dropbox” 2017-04-15 21:31:47 +04:30
Amir Abbas dd7a9d20b6 Fixed macOS build error 2017-04-14 23:26:40 +04:30
Amir Abbas d4a9b4a34f Fixed Dropbox thumbnail issue 2017-04-14 22:07:35 +04:30
Amir Abbas 34c663e62c FileObject.url is unwraped. fixed url initializing from path 2017-04-14 18:57:50 +04:30
Amir Abbas 1415dda987 Fixed #36 (FTP Uploading bug) 2017-04-14 18:55:07 +04:30
26 changed files with 1723 additions and 1015 deletions
+4 -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"
@@ -75,6 +75,7 @@ deploy:
file: $FRAMEWORK_NAME.zip
skip_cleanup: true
on:
repo: amosavian/$PROJECTNAME
# repo: amosavian/$PROJECTNAME
repo: amosavian/FileProvider
tags: true
condition: "$CARTHAGEDEPLOY = YES"
@@ -1,5 +1,5 @@
#
# Be sure to run `pod spec lint FileProvider.podspec' to ensure this is a
# Be sure to run `pod spec lint FilesProvider.podspec' to ensure this is a
# valid spec and to remove all comments including this before submitting the spec.
#
# To learn more about Podspec attributes see http://docs.cocoapods.org/specification.html
@@ -15,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.5"
s.name = "FilesProvider"
s.version = "0.19.0"
s.summary = "FileManager replacement for Local and Remote (WebDAV/FTP/Dropbox/OneDrive/SMB2) files on iOS and macOS."
# This description is used to generate tags and improve search results.
@@ -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>"; };
@@ -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>";
@@ -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 */
@@ -621,7 +621,7 @@
799396601D48B7BF00086753 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_VERSION_STRING = 0.15.5;
BUNDLE_VERSION_STRING = 0.19.0;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_EMPTY_BODY = YES;
@@ -645,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;
@@ -652,7 +653,7 @@
799396611D48B7BF00086753 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_VERSION_STRING = 0.15.5;
BUNDLE_VERSION_STRING = 0.19.0;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_EMPTY_BODY = YES;
@@ -672,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;
};
@@ -718,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";
@@ -765,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";
@@ -822,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";
@@ -873,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";
@@ -925,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";
@@ -975,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;
@@ -990,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 */,
@@ -999,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 */,
@@ -1008,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 */,
@@ -1017,7 +1013,7 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
799396871D48B82700086753 /* Build configuration list for PBXNativeTarget "FileProvider tvOS" */ = {
799396871D48B82700086753 /* Build configuration list for PBXNativeTarget "FilesProvider tvOS" */ = {
isa = XCConfigurationList;
buildConfigurations = (
799396881D48B82700086753 /* Debug */,
@@ -15,9 +15,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "799396741D48B80D00086753"
BuildableName = "FileProvider.framework"
BlueprintName = "FileProvider OSX"
ReferencedContainer = "container:FileProvider.xcodeproj">
BuildableName = "FilesProvider.framework"
BlueprintName = "FilesProvider OSX"
ReferencedContainer = "container:FilesProvider.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
@@ -26,6 +26,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
@@ -36,6 +37,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
@@ -46,9 +48,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "799396741D48B80D00086753"
BuildableName = "FileProvider.framework"
BlueprintName = "FileProvider OSX"
ReferencedContainer = "container:FileProvider.xcodeproj">
BuildableName = "FilesProvider.framework"
BlueprintName = "FilesProvider OSX"
ReferencedContainer = "container:FilesProvider.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
@@ -64,9 +66,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "799396741D48B80D00086753"
BuildableName = "FileProvider.framework"
BlueprintName = "FileProvider OSX"
ReferencedContainer = "container:FileProvider.xcodeproj">
BuildableName = "FilesProvider.framework"
BlueprintName = "FilesProvider OSX"
ReferencedContainer = "container:FilesProvider.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
@@ -15,9 +15,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "799396661D48B7F600086753"
BuildableName = "FileProvider.framework"
BlueprintName = "FileProvider iOS"
ReferencedContainer = "container:FileProvider.xcodeproj">
BuildableName = "FilesProvider.framework"
BlueprintName = "FilesProvider iOS"
ReferencedContainer = "container:FilesProvider.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
@@ -26,6 +26,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
@@ -36,6 +37,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
@@ -46,9 +48,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "799396661D48B7F600086753"
BuildableName = "FileProvider.framework"
BlueprintName = "FileProvider iOS"
ReferencedContainer = "container:FileProvider.xcodeproj">
BuildableName = "FilesProvider.framework"
BlueprintName = "FilesProvider iOS"
ReferencedContainer = "container:FilesProvider.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
@@ -64,9 +66,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "799396661D48B7F600086753"
BuildableName = "FileProvider.framework"
BlueprintName = "FileProvider iOS"
ReferencedContainer = "container:FileProvider.xcodeproj">
BuildableName = "FilesProvider.framework"
BlueprintName = "FilesProvider iOS"
ReferencedContainer = "container:FilesProvider.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
@@ -15,9 +15,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "799396811D48B82700086753"
BuildableName = "FileProvider.framework"
BlueprintName = "FileProvider tvOS"
ReferencedContainer = "container:FileProvider.xcodeproj">
BuildableName = "FilesProvider.framework"
BlueprintName = "FilesProvider tvOS"
ReferencedContainer = "container:FilesProvider.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
@@ -26,6 +26,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
@@ -36,6 +37,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
@@ -46,9 +48,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "799396811D48B82700086753"
BuildableName = "FileProvider.framework"
BlueprintName = "FileProvider tvOS"
ReferencedContainer = "container:FileProvider.xcodeproj">
BuildableName = "FilesProvider.framework"
BlueprintName = "FilesProvider tvOS"
ReferencedContainer = "container:FilesProvider.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
@@ -64,9 +66,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "799396811D48B82700086753"
BuildableName = "FileProvider.framework"
BlueprintName = "FileProvider tvOS"
ReferencedContainer = "container:FileProvider.xcodeproj">
BuildableName = "FilesProvider.framework"
BlueprintName = "FilesProvider tvOS"
ReferencedContainer = "container:FilesProvider.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
+2 -2
View File
@@ -1,5 +1,5 @@
import PackageDescription
let package = Package(
name: "FileProvider"
)
name: "FilesProvider"
)
+49 -39
View File
@@ -2,21 +2,28 @@
>This Swift library provide a swifty way to deal with local and remote files and directories in a unified way.
<center>
[![Swift Version][swift-image]][swift-url]
[![Platform][platform-image]](#)
[![License][license-image]][license-url]
[![Release versin][release-image]][release-url]
[![CocoaPods version](https://img.shields.io/cocoapods/v/FileProvider.svg)][cocoapods]
[![Carthage compatible][carthage-image]](https://github.com/Carthage/Carthage)
[![Build Status][travis-image]][travis-url]
[![Codebeat Badge][codebeat-image]][codebeat-url]
[![Cocoapods Docs][docs-image]][docs-url]
[![Release version][release-image]][release-url]
[![CocoaPods version](https://img.shields.io/cocoapods/v/FilesProvider.svg)][cocoapods]
[![Carthage compatible][carthage-image]](https://github.com/Carthage/Carthage)
[![Cocoapods Downloads][cocoapods-downloads]][cocoapods]
[![Cocoapods Apps][cocoapods-apps]][cocoapods]
Old Cocoapods repo stats:
[![Cocoapods Downloads][cocoapods-downloads-old]][cocoapods-old]
[![Cocoapods Apps][cocoapods-apps-old]][cocoapods-old]
</center>
<!---
[![Cocoapods Doc][docs-image]][docs-url]
[![codecov](https://codecov.io/gh/amosavian/FileProvider/branch/master/graph/badge.svg)](https://codecov.io/gh/amosavian/FileProvider)
--->
@@ -30,7 +37,6 @@ All functions do async calls and it wont block your main thread.
- [x] **CloudFileProvider** A wrapper around app's ubiquitous container API of iCloud Drive.
- [x] **WebDAVFileProvider** WebDAV protocol is defacto file transmission standard, supported by some cloud services like `ownCloud`, `Box.com` and `Yandex.disk`.
- [x] **FTPFileProvider** While deprecated in 1990s due to serious security concerns, it's still in use on some Web hosts.
* 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.
@@ -39,7 +45,7 @@ All functions do async calls and it wont block your main thread.
- [ ] **AmazonS3FileProvider** Amazon storage backend. Used by many sites.
- [ ] **SMBFileProvider** SMB2/3 introduced in 2006, which is a file and printer sharing protocol originated from Microsoft Windows and now is replacing AFP protocol on macOS.
* Data types and some basic functions are implemented but *main interface is not implemented yet!*.
* SMB1/CIFS is deprecated and very tricky to be implemented due to strict memory allignment in Swift.
* SMBv1/CIFS is insecure, deprecated and kinda tricky to be implemented due to strict memory allignment in Swift.
## Requirements
@@ -51,12 +57,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:
@@ -94,7 +103,7 @@ Then you can do either:
* Copy Source folder to your project and Voila!
* Drop `FileProvider.xcodeproj` to you Xcode workspace and add the framework to your Embeded Binaries in target.
* Drop `FilesProvider.xcodeproj` to you Xcode workspace and add the framework to your Embeded Binaries in target.
## Usage
@@ -105,6 +114,8 @@ Each provider has a specific class which conforms to FileProvider protocol and s
For LocalFileProvider if you want to deal with `Documents` folder
``` swift
import FilesProvider
let documentsProvider = LocalFileProvider()
// Equals with:
@@ -118,6 +129,8 @@ let documentsProvider = LocalFileProvider(baseURL: documentsURL)
Also for using group shared container:
```swift
import FilesProvider
let documentsProvider = LocalFileProvider(sharedContainerId: "group.yourcompany.appContainer")
// Replace your group identifier
```
@@ -127,6 +140,8 @@ You can't change the base url later. and all paths are related to this base url
To initialize an iCloud Container provider look at [here](https://medium.com/ios-os-x-development/icloud-drive-documents-1a46b5706fe1) to see how to update project settings then use below code, This will automatically manager creating Documents folder in container:
```swift
import FilesProvider
let documentsProvider = CloudFileProvider(containerId: nil)
```
@@ -134,6 +149,8 @@ let documentsProvider = CloudFileProvider(containerId: nil)
For remote file providers authentication may be necessary:
``` swift
import FilesProvider
let credential = URLCredential(user: "user", password: "pass", persistence: .permanent)
let webdavProvider = WebDAVFileProvider(baseURL: URL(string: "http://www.example.com/dav")!, credential: credential)
```
@@ -229,10 +246,10 @@ To get list of files in a directory:
documentsProvider.contentsOfDirectory(path: "/", completionHandler: {
contents, error in
for file in contents {
print("Name: \(attributes.name)")
print("Size: \(attributes.size)")
print("Creation Date: \(attributes.creationDate)")
print("Modification Date: \(attributes.modifiedDate)")
print("Name: \(file.name)")
print("Size: \(file.size)")
print("Creation Date: \(file.creationDate)")
print("Modification Date: \(file.modifiedDate)")
}
})
```
@@ -249,15 +266,6 @@ func storageProperties(completionHandler: { total, used in
* if this function is unavailable on provider or an error has been occurred, total space will be reported `-1` and used space `0`
### Change current directory
```swift
documentsProvider.currentPath = "/New Folder"
// now path is ~/Documents/New Folder
```
You can then pass "" (empty string) to `contentsOfDirectory` method to list files in current directory.
### Creating File and Folders
Creating new directory:
@@ -296,8 +304,6 @@ documentsProvider.moveItem(path: "new folder/old.txt", to: "new.txt", overwrite:
documentsProvider.removeItem(path: "new.txt", completionHandler: nil)
```
***Caution:*** This method will delete directories with all it's contents recursively except for FTP providers that don't support `SITE RMDIR` command, this will be fixed later.
### Fetching Contents of File
There is two method for this purpose, one of them loads entire file into `Data` and another can load a portion of file.
@@ -329,7 +335,7 @@ let data = "What's up Newyork!".data(encoding: .utf8)
documentsProvider.writeContents(path: "old.txt", content: data, atomically: true, completionHandler: nil)
```
### Copying Files to and From Local URL
### Copying Files to and From Local Storage
There are two methods to download and upload files between provider's and local storage. These methods use `URLSessionDownloadTask` and `URLSessionUploadTask` classes and allows to use background session and provide progress via delegate.
@@ -350,6 +356,12 @@ documentsProvider.copyItem(path: "/download/image.jpg", toLocalURL: fileURL, ove
* It's safe only to assume these methods **won't** handle directories to upload/download recursively. If you need, you can list directories, create directories on target and copy files using these methods.
* FTP provider allows developer to either use apple implemented `URLSessionDownloadTask` or custom implemented method based on stream task via `useAppleImplementation` property. FTP protocol is not supported by background session.
### Operation Progress
Creating/Copying/Deleting/Searching functions return a `(NS)Progress`. It provides operation type, progress and a `.cancel()` method which allows you to cancel operation in midst. You can check `cancellable` property to check either you can cancel operation via this object or not.
- **Note:** Progress reporting is not supported by native `(NS)FileManager` so `LocalFileProvider`.
### Undo Operations
Providers conform to `FileProviderUndoable` can perform undo for **some** operations like moving/renaming, copying and creating (file or folder). **Now, only `LocalFileProvider` supports this feature.** To implement:
@@ -393,12 +405,6 @@ class ViewController: UIViewController
}
```
### Operation Handle
Creating/Copying/Deleting functions return a `OperationHandle` for remote operations. It provides operation type, progress and a `.cancel()` method which allows you to cancel operation in midst.
It's not supported by native `(NS)FileManager` so `LocalFileProvider`, but this functionality will be added to future `PosixFileProvider` class.
### File Coordination
`LocalFileProvider` and its descendents has a `isCoordinating` property. By setting this, provider will use `NSFileCoordinating` class to do all file operations. It's mandatory for iCloud, while recommended when using shared container or anywhere that simultaneous operations on a file/folder is common.
@@ -430,9 +436,10 @@ To check either file thumbnail is supported or not and fetch thumbnail, use (and
```swift
let path = "/newImage.jpg"
let thumbSize = CGSize(width: 64, height: 64)
let thumbSize = CGSize(width: 64, height: 64) // or nil which renders to default dimension of provider
if documentsProvider.thumbnailOfFileSupported(path: path {
documentsProvider.thumbnailOfFile(path: file.path, dimension: thumbSize, completionHandler: { (image, error) in
// Interacting with UI must be placed in main thread
DispatchQueue.main.async {
self.previewImage.image = image
}
@@ -484,10 +491,11 @@ Distributed under the MIT license. See `LICENSE` for more information.
[https://github.com/amosavian/](https://github.com/amosavian/)
[cocoapods]: https://cocoapods.org/pods/FileProvider
[cocoapods]: https://cocoapods.org/pods/FilesProvider
[cocoapods-old]: https://cocoapods.org/pods/FileProvider
[swift-image]: https://img.shields.io/badge/swift-3.0,%203.1-orange.svg
[swift-url]: https://swift.org/
[platform-image]: https://img.shields.io/cocoapods/p/FileProvider.svg
[platform-image]: https://img.shields.io/cocoapods/p/FilesProvider.svg
[license-image]: https://img.shields.io/github/license/amosavian/FileProvider.svg
[license-url]: LICENSE
[codebeat-image]: https://codebeat.co/badges/7b359f48-78eb-4647-ab22-56262a827517
@@ -497,7 +505,9 @@ Distributed under the MIT license. See `LICENSE` for more information.
[release-url]: https://github.com/amosavian/FileProvider/releases
[release-image]: https://img.shields.io/github/release/amosavian/FileProvider.svg
[carthage-image]: https://img.shields.io/badge/Carthage-compatible-4BC51D.svg
[cocoapods-downloads]: https://img.shields.io/cocoapods/dt/FileProvider.svg
[cocoapods-apps]: https://img.shields.io/cocoapods/at/FileProvider.svg
[docs-image]: https://img.shields.io/cocoapods/metrics/doc-percent/FileProvider.svg
[docs-url]: http://cocoadocs.org/docsets/FileProvider/
[cocoapods-downloads-old]: https://img.shields.io/cocoapods/dt/FileProvider.svg
[cocoapods-apps-old]: https://img.shields.io/cocoapods/at/FileProvider.svg
[cocoapods-downloads]: https://img.shields.io/cocoapods/dt/FilesProvider.svg
[cocoapods-apps]: https://img.shields.io/cocoapods/at/FilesProvider.svg
[docs-image]: https://img.shields.io/cocoapods/metrics/doc-percent/FilesProvider.svg
[docs-url]: http://cocoadocs.org/docsets/FilesProvider/
+165 -143
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)
@@ -238,13 +243,13 @@ open class CloudFileProvider: LocalFileProvider {
- Note: For now only it's limited to file names. `query` parameter may take `NSPredicate` format in near future.
- Parameters:
- path: location of directory to start search
- recursive: Searching subdirectories of path
- query: Simple string of file name to be search (for now).
- foundItemHandler: Closure which is called when a file is found
- completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured.
- path: location of directory to start search
- recursive: Searching subdirectories of path
- query: Simple string of file name to be search (for now).
- foundItemHandler: Closure which is called when a file is found
- completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured.
*/
open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? {
let mapDict: [String: String] = ["url": NSMetadataItemURLKey, "name": NSMetadataItemFSNameKey, "path": NSMetadataItemPathKey, "filesize": NSMetadataItemFSSizeKey, "modifiedDate": NSMetadataItemFSContentChangeDateKey, "creationDate": NSMetadataItemFSCreationDateKey, "contentType": NSMetadataItemContentTypeKey]
@@ -277,6 +282,8 @@ open class CloudFileProvider: LocalFileProvider {
}
}
let progress = Progress(parent: nil, userInfo: nil)
dispatch_queue.async {
let pathURL = self.url(of: path)
let mdquery = NSMetadataQuery()
@@ -285,6 +292,10 @@ open class CloudFileProvider: LocalFileProvider {
var lastReportedCount = 0
progress.cancellationHandler = { [weak mdquery] in
mdquery?.stop()
}
if let foundItemHandler = foundItemHandler {
var updateObserver: NSObjectProtocol?
@@ -309,6 +320,7 @@ open class CloudFileProvider: LocalFileProvider {
}
}
lastReportedCount = mdquery.resultCount
progress.totalUnitCount = Int64(lastReportedCount)
mdquery.enableUpdates()
})
@@ -341,12 +353,14 @@ open class CloudFileProvider: LocalFileProvider {
contents.append(file)
}
}
progress.completedUnitCount = Int64(contents.count)
self.dispatch_queue.async {
completionHandler(contents, nil)
}
})
DispatchQueue.main.async {
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
if !mdquery.start() {
self.dispatch_queue.async {
completionHandler([], self.throwError(path, code: CocoaError.fileReadNoPermission))
@@ -354,6 +368,8 @@ open class CloudFileProvider: LocalFileProvider {
}
}
}
return progress
}
open override func isReachable(completionHandler: @escaping (Bool) -> Void) {
@@ -370,12 +386,11 @@ open class CloudFileProvider: LocalFileProvider {
- folder: Directory name.
- at: Parent path of new directory.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
- Returns: A `Progress` object to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
*/
@discardableResult
open override func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
guard let r = super.create(folder: folderName, at: atPath, completionHandler: completionHandler) else { return nil }
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
open override func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? {
return super.create(folder: folderName, at: atPath, completionHandler: completionHandler)
}
/**
@@ -388,12 +403,11 @@ open class CloudFileProvider: LocalFileProvider {
- to: destination path of file or directory, including file/directory name.
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
- Returns: A `Progress` object to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
*/
@discardableResult
open override func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
guard let r = super.moveItem(path: path, to: toPath, overwrite: overwrite, completionHandler: completionHandler) else { return nil }
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
open override func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
return super.moveItem(path: path, to: toPath, overwrite: overwrite, completionHandler: completionHandler)
}
/**
@@ -406,12 +420,11 @@ open class CloudFileProvider: LocalFileProvider {
- to: destination path of file or directory, including file/directory name.
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
- Returns: A `Progress` object to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
*/
@discardableResult
open override func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
guard let r = super.copyItem(path: path, to: toPath, overwrite: overwrite, completionHandler: completionHandler) else { return nil }
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
open override func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
return super.copyItem(path: path, to: toPath, overwrite: overwrite, completionHandler: completionHandler)
}
/**
@@ -424,13 +437,11 @@ open class CloudFileProvider: LocalFileProvider {
- Parameters:
- path: file or directory path.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
- Returns: A `Progress` object to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
*/
@discardableResult
open override func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
guard let r = super.removeItem(path: path, completionHandler: completionHandler) else { return nil }
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
open override func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
return super.removeItem(path: path, completionHandler: completionHandler)
}
/**
@@ -442,12 +453,18 @@ open class CloudFileProvider: LocalFileProvider {
- to: destination path of file, including file/directory name.
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress.
- Returns: A `Progress` object to get progress or cancel progress.
*/
@discardableResult
open override func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open override func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
// TODO: Make use of overwrite parameter
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
let progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.isCancellable = false
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
monitorFile(path: toPath, opType: opType, progress: progress)
operation_queue.addOperation {
let tempFolder: URL
if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) {
@@ -475,7 +492,7 @@ open class CloudFileProvider: LocalFileProvider {
})
}
}
return CloudOperationHandle(operationType: opType, baseURL: self.baseURL)
return progress
}
/**
@@ -486,12 +503,17 @@ open class CloudFileProvider: LocalFileProvider {
- path: original file or directory path.
- toLocalURL: destination local url of file, including file/directory name.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress.
- Returns: A `Progress` object to get progress or cancel progress.
*/
@discardableResult
open override func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open override func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
let progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.isCancellable = false
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
monitorFile(path: path, opType: opType, progress: progress)
do {
try self.opFileManager.startDownloadingUbiquitousItem(at: self.url(of: path))
} catch let e {
@@ -501,9 +523,8 @@ open class CloudFileProvider: LocalFileProvider {
})
return nil
}
guard let r = super.copyItem(path: path, toLocalURL: toLocalURL, completionHandler: completionHandler) else { return nil }
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
let _ = super.copyItem(path: path, toLocalURL: toLocalURL, completionHandler: completionHandler)
return progress
}
/**
@@ -515,12 +536,18 @@ open class CloudFileProvider: LocalFileProvider {
- completionHandler: a closure with result of file contents or error.
`contents`: contents of file in a `Data` object.
`error`: Error returned by system.
- Returns: An `OperationHandle` to get progress or cancel progress.
- Returns: A `Progress` object to get progress or cancel progress.
*/
@discardableResult
open override func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
guard let r = super.contents(path: path, completionHandler: completionHandler) else { return nil }
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
open override func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
let operation = FileOperationType.fetch(path: path)
let progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
monitorFile(path: path, opType: operation, progress: progress)
_ = super.contents(path: path, completionHandler: completionHandler)
return progress
}
/**
@@ -534,12 +561,18 @@ open class CloudFileProvider: LocalFileProvider {
- completionHandler: a closure with result of file contents or error.
`contents`: contents of file in a `Data` object.
`error`: Error returned by system.
- Returns: An `OperationHandle` to get progress or cancel progress.
- Returns: A `Progress` object to get progress or cancel progress.
*/
@discardableResult
open override func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
guard let r = super.contents(path: path, offset: offset, length: length, completionHandler: completionHandler) else { return nil }
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
open override func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
let operation = FileOperationType.fetch(path: path)
let progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
monitorFile(path: path, opType: operation, progress: progress)
_ = super.contents(path: path, offset: offset, length: length, completionHandler: completionHandler)
return progress
}
/**
@@ -551,12 +584,18 @@ open class CloudFileProvider: LocalFileProvider {
- overwrite: Destination file should be overwritten if file is already exists. Default is `false`.
- atomically: data will be written to a temporary file before writing to final location. Default is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: A `Progress` object to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
open override func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
guard let r = super.writeContents(path: path, contents: data, atomically: atomically, overwrite: overwrite, completionHandler: completionHandler) else { return nil }
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
open override func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
let operation = FileOperationType.fetch(path: path)
let progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
monitorFile(path: path, opType: operation, progress: progress)
_ = super.writeContents(path: path, contents: data, atomically: atomically, overwrite: overwrite, completionHandler: completionHandler)
return progress
}
fileprivate var monitors = [String: (NSMetadataQuery, NSObjectProtocol)]()
@@ -628,7 +667,7 @@ open class CloudFileProvider: LocalFileProvider {
let path = self.relativePathOf(url: url)
let rpath = path.hasPrefix("/") ? path.substring(from: path.index(after: path.startIndex)) : path
let relativeUrl = URL(string: rpath, relativeTo: self.baseURL)
let relativeUrl = URL(string: rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? rpath, relativeTo: self.baseURL)
let file = FileObject(url: relativeUrl ?? url, name: name, path: path)
file.size = (attribs[NSMetadataItemFSSizeKey] as? NSNumber)?.int64Value ?? -1
@@ -641,6 +680,57 @@ open class CloudFileProvider: LocalFileProvider {
return file
}
func monitorFile(path: String, opType: FileOperationType, progress: Progress?) {
dispatch_queue.async {
let pathURL = self.url(of: path)
let size = pathURL.fileSize
progress?.totalUnitCount = size > 0 ? size : 0
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: "%K LIKE %@", NSMetadataItemPathKey, pathURL.path)
query.valueListAttributes = [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataUbiquitousItemPercentDownloadedKey, NSMetadataUbiquitousItemPercentUploadedKey, NSMetadataItemFSSizeKey]
query.searchScopes = [self.scope.rawValue]
var updateObserver: NSObjectProtocol?
updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidUpdate, object: query, queue: nil, using: { (notification) in
query.disableUpdates()
guard let item = (query.results as? [NSMetadataItem])?.first else {
return
}
if progress?.totalUnitCount == 0, let size = item.value(forAttribute: NSMetadataItemFSSizeKey) as? Int64 {
progress?.totalUnitCount = size
}
let downloaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentDownloadedKey) as? Double ?? 0
let uploaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentUploadedKey) as? Double ?? 0
if (downloaded == 0 || downloaded == 100) && (uploaded > 0 && uploaded < 100) {
progress?.completedUnitCount = Int64(uploaded / 100 * Double(progress?.totalUnitCount ?? 0))
DispatchQueue.main.async {
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(uploaded / 100))
}
} else if (uploaded == 0 || uploaded == 100) && (downloaded > 0 && downloaded < 100) {
progress?.completedUnitCount = Int64(downloaded / 100 * Double(progress?.totalUnitCount ?? 0))
DispatchQueue.main.async {
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(downloaded / 100))
}
} else if uploaded == 100 || downloaded == 100 {
progress?.completedUnitCount = progress?.totalUnitCount ?? 0
query.stop()
NotificationCenter.default.removeObserver(updateObserver!)
DispatchQueue.main.async {
self.delegate?.fileproviderSucceed(self, operation: opType)
}
}
query.enableUpdates()
})
DispatchQueue.main.async {
progress?.setUserInfoObject(Date(), forKey: .startingTimeKey)
query.start()
}
}
}
/// Removes local copy of file, but spares cloud copy/
/// - Parameter path: Path of file or directory to be remoed from local
/// - Parameter completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
@@ -655,19 +745,6 @@ open class CloudFileProvider: LocalFileProvider {
}
}
/**
Genrates a public url to a file to be shared with other users and can be downloaded without authentication.
- Important: URL will be available for a limitied time, determined in `expiration` argument.
- Parameters:
- to: path of file, including file/directory name.
- completionHandler: a closure with result of directory entries or error.
`link`: a url returned by Dropbox to share.
`attribute`: a `FileObject` containing the attributes of the item.
`expiration`: a `Date` object, determines when the public url will expires.
`error`: Error returned by Dropbox.
*/
open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
operation_queue.addOperation {
do {
@@ -685,6 +762,7 @@ open class CloudFileProvider: LocalFileProvider {
}
}
/// Scope of iCloud, wrapper for NSMetadataQueryUbiquitous...Scope constants
public enum UbiquitousScope: RawRepresentable {
/// Search all files not in the Documents directories of the apps iCloud container directories.
/// Use this scope to store user-related data files that your app needs to share
@@ -717,92 +795,36 @@ 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
/*
func getMetadataItem(url: URL) -> NSMetadataItem? {
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: "(%K LIKE %@)", NSMetadataItemPathKey, url.path)
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope, NSMetadataQueryUbiquitousDataScope]
init (operationType: FileOperationType, baseURL: URL?) {
self.baseURL = baseURL
self.operationType = operationType
}
var item: NSMetadataItem?
private var sourceURL: URL? {
guard let source = operationType.source, let baseURL = baseURL else { return nil }
return source.hasPrefix("file://") ? URL(fileURLWithPath: source) : baseURL.appendingPathComponent(source)
}
private var destURL: URL? {
guard let dest = operationType.destination, let baseURL = baseURL else { return nil }
return dest.hasPrefix("file://") ? URL(fileURLWithPath: dest) : baseURL.appendingPathComponent(dest)
}
open var bytesSoFar: Int64 {
assert(!Thread.isMainThread, "Don't run \(#function) method on main thread")
guard let url = destURL ?? sourceURL, let item = CloudOperationHandle.getMetadataItem(url: url) else { return 0 }
let downloaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentDownloadedKey) as? Double ?? 0
let uploaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentUploadedKey) as? Double ?? 0
guard let size = item.value(forAttribute: NSMetadataItemFSSizeKey) as? Int64 else { return -1 }
if (downloaded == 0 || downloaded == 100) && (uploaded > 0 && uploaded < 100) {
return Int64(uploaded * (Double(size) / 100))
} else if (uploaded == 0 || uploaded == 100) && (downloaded > 0 && downloaded < 100) {
return Int64(downloaded * (Double(size) / 100))
} else if uploaded == 100 || downloaded == 100 {
return size
let group = DispatchGroup()
group.enter()
var finishObserver: NSObjectProtocol?
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
defer {
query.stop()
group.leave()
NotificationCenter.default.removeObserver(finishObserver!)
}
return 0
}
open var totalBytes: Int64 {
assert(!Thread.isMainThread, "Don't run \(#function) method on main thread")
guard let url = destURL ?? sourceURL, let item = CloudOperationHandle.getMetadataItem(url: url) else { return -1 }
return item.value(forAttribute: NSMetadataItemFSSizeKey) as? Int64 ?? -1
}
open var inProgress: Bool {
guard let url = destURL ?? sourceURL, let item = CloudOperationHandle.getMetadataItem(url: url) else { return false }
let downloadStatus = item.value(forAttribute: NSMetadataUbiquitousItemDownloadingStatusKey) as? String ?? NSMetadataUbiquitousItemDownloadingStatusNotDownloaded
let isUploading = item.value(forAttribute: NSMetadataUbiquitousItemIsUploadingKey) as? Bool ?? false
return downloadStatus == NSMetadataUbiquitousItemDownloadingStatusCurrent || isUploading
}
/// Not usable in local provider
open func cancel() -> Bool {
return false
}
fileprivate static func getMetadataItem(url: URL) -> NSMetadataItem? {
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: "(%K LIKE %@)", NSMetadataItemPathKey, url.path)
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope, NSMetadataQueryUbiquitousDataScope]
var item: NSMetadataItem?
let group = DispatchGroup()
group.enter()
var finishObserver: NSObjectProtocol?
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
defer {
query.stop()
group.leave()
NotificationCenter.default.removeObserver(finishObserver!)
}
if query.resultCount > 0 {
item = query.result(at: 0) as? NSMetadataItem
}
query.disableUpdates()
})
DispatchQueue.main.async {
query.start()
if query.resultCount > 0 {
item = query.result(at: 0) as? NSMetadataItem
}
_ = group.wait(timeout: .now() + 30)
return item
query.disableUpdates()
})
DispatchQueue.main.async {
query.start()
}
_ = group.wait(timeout: .now() + 30)
return item
}
*/
+126 -70
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
@@ -44,7 +44,7 @@ open class DropboxFileProvider: FileProviderBasicRemote {
public var validatingCache: Bool
fileprivate var _session: URLSession?
fileprivate var sessionDelegate: SessionDelegate?
internal fileprivate(set) var sessionDelegate: SessionDelegate?
public var session: URLSession {
get {
if _session == nil {
@@ -101,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) {
@@ -148,7 +153,8 @@ open class DropboxFileProvider: FileProviderBasicRemote {
}
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
list(path) { (contents, cursor, error) in
let progress = Progress(parent: nil, userInfo: nil)
list(path, progress: progress) { (contents, cursor, error) in
completionHandler(contents, error)
}
}
@@ -157,8 +163,8 @@ open class DropboxFileProvider: FileProviderBasicRemote {
let url = URL(string: "files/get_metadata", relativeTo: apiURL)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .json)
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString]
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
@@ -180,7 +186,7 @@ open class DropboxFileProvider: FileProviderBasicRemote {
let url = URL(string: "users/get_space_usage", relativeTo: apiURL)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.set(httpAuthentication: credential, with: .oAuth2)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var totalSize: Int64 = -1
var usedSize: Int64 = 0
@@ -193,12 +199,13 @@ open class DropboxFileProvider: FileProviderBasicRemote {
task.resume()
}
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? {
let progress = Progress(parent: nil, userInfo: nil)
var foundFiles = [DropboxFileObject]()
if let queryStr = query.findValue(forKey: "name", operator: .beginsWith) as? String {
// Dropbox only support searching for file names begin with query in non-enterprise accounts.
// We will use it if there is a `name BEGINSWITH[c] "query"` in predicate, then filter to form final result.
search(path, query: queryStr, foundItem: { (file) in
search(path, query: queryStr, progress: progress, foundItem: { (file) in
if query.evaluate(with: file.mapPredicate()) {
foundFiles.append(file)
foundItemHandler?(file)
@@ -209,7 +216,7 @@ open class DropboxFileProvider: FileProviderBasicRemote {
} else {
// Dropbox doesn't support searching attributes natively. The workaround is to fallback to listing all files
// and filter it locally. It may have a network burden in case there is many files in Dropbox, so please use it concisely.
list(path, recursive: true, progressHandler: { (files, _, error) in
list(path, recursive: true, progress: progress, progressHandler: { (files, _, error) in
for file in files where query.evaluate(with: file.mapPredicate()) {
foundItemHandler?(file)
}
@@ -218,6 +225,7 @@ open class DropboxFileProvider: FileProviderBasicRemote {
completionHandler(predicatedFiles, error)
})
}
return progress
}
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
@@ -230,27 +238,33 @@ open class DropboxFileProvider: FileProviderBasicRemote {
}
extension DropboxFileProvider: FileProviderOperations {
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? {
let path = (atPath as NSString).appendingPathComponent(folderName) + "/"
return doOperation(.create(path: path), completionHandler: completionHandler)
}
open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler)
}
open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
return doOperation(.copy(source: path, destination: toPath), completionHandler: completionHandler)
}
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
return doOperation(.remove(path: path), completionHandler: completionHandler)
}
fileprivate func doOperation(_ operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
fileprivate func doOperation(_ operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? {
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
return nil
}
let progress = Progress(totalUnitCount: 1)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
let url: String
guard let sourcePath = operation.source else { return nil }
let destPath = operation.destination
@@ -268,8 +282,8 @@ extension DropboxFileProvider: FileProviderOperations {
}
var request = URLRequest(url: URL(string: url, relativeTo: apiURL)!)
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .json)
var requestDictionary = [String: AnyObject]()
if let dest = correctPath(destPath) as NSString? {
requestDictionary["from_path"] = correctPath(sourcePath) as NSString?
@@ -283,15 +297,24 @@ extension DropboxFileProvider: FileProviderOperations {
if let response = response as? HTTPURLResponse, response.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) {
serverError = FileProviderDropboxError(code: code, path: sourcePath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
}
if serverError == nil && error == nil {
progress.completedUnitCount = 1
} else {
progress.cancel()
}
completionHandler?(serverError ?? error)
self.delegateNotify(operation, error: serverError ?? error)
})
task.taskDescription = operation.json
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return RemoteOperationHandle(operationType: operation, tasks: [task])
return progress
}
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
// check file is not a folder
guard (try? localFile.resourceValues(forKeys: [.fileResourceTypeKey]))?.fileResourceType ?? .unknown == .regular else {
dispatch_queue.async {
@@ -307,24 +330,36 @@ extension DropboxFileProvider: FileProviderOperations {
return upload_simple(toPath, localFile: localFile, overwrite: overwrite, operation: opType, completionHandler: completionHandler)
}
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.copy(source: path, destination: destURL.absoluteString)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
let url = URL(string: "files/download", relativeTo: contentURL)!
var progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
var request = URLRequest(url: url)
request.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[session.sessionDescription!]?[task.taskIdentifier] = completionHandler
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
if error != nil {
progress.cancel()
}
completionHandler?(error)
}
downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { tempURL in
guard let httpResponse = task.response as? HTTPURLResponse , httpResponse.statusCode < 300 else {
let code = FileProviderHTTPErrorCode(rawValue: (task.response as? HTTPURLResponse)?.statusCode ?? -1)
let errorData : Data? = nil //Data(contentsOf:cacheURL) // TODO: Figure out how to get error response data for the error description
let serverError : FileProviderDropboxError? = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: errorData ?? Data(), encoding: .utf8)) : nil
if serverError != nil {
progress.cancel()
}
completionHandler?(serverError)
return
}
@@ -336,13 +371,19 @@ extension DropboxFileProvider: FileProviderOperations {
}
}
task.taskDescription = opType.json
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: &progress)
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: &progress)
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return RemoteOperationHandle(operationType: opType, tasks: [task])
return progress
}
}
extension DropboxFileProvider: FileProviderReadWrite {
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
if length == 0 || offset < 0 {
dispatch_queue.async {
completionHandler(Data(), nil)
@@ -352,24 +393,31 @@ extension DropboxFileProvider: FileProviderReadWrite {
let opType = FileOperationType.fetch(path: path)
let url = URL(string: "files/download", relativeTo: contentURL)!
var progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
var request = URLRequest(url: url)
request.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")
}
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString]
request.setValue(String(jsonDictionary: requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(rangeWithOffset: offset, length: length)
request.set(dropboxArgKey: ["path": correctPath(path)! as NSString])
let task = session.downloadTask(with: request)
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
if error != nil {
progress.cancel()
}
completionHandler(nil, error)
}
downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { tempURL in
downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { tempURL in
guard let httpResponse = task.response as? HTTPURLResponse , httpResponse.statusCode < 300 else {
let code = FileProviderHTTPErrorCode(rawValue: (task.response as? HTTPURLResponse)?.statusCode ?? -1)
let errorData : Data? = nil //Data(contentsOf:cacheURL) // TODO: Figure out how to get error response data for the error description
let serverError : FileProviderDropboxError? = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: errorData ?? Data(), encoding: .utf8)) : nil
if serverError != nil {
progress.cancel()
}
completionHandler(nil, serverError)
return
}
@@ -381,11 +429,17 @@ extension DropboxFileProvider: FileProviderReadWrite {
}
}
task.taskDescription = opType.json
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: &progress)
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: &progress)
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return RemoteOperationHandle(operationType: opType, tasks: [task])
return progress
}
public func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
public func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.modify(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
@@ -411,26 +465,13 @@ extension DropboxFileProvider: FileProviderReadWrite {
// TODO: Implement /get_account & /get_current_account
}
extension DropboxFileProvider {
/**
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
@@ -475,8 +516,8 @@ extension DropboxFileProvider {
let url = URL(string: "files/save_url", relativeTo: apiURL)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .json)
let requestDictionary: [String: AnyObject] = ["path": correctPath(toPath)! as NSString, "url" : remoteURL.absoluteString as NSString]
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
@@ -510,8 +551,8 @@ extension DropboxFileProvider {
let url = URL(string: "files/copy_reference/save", relativeTo: apiURL)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .json)
let requestDictionary: [String: AnyObject] = ["path": correctPath(toPath)! as NSString, "copy_reference" : reference as NSString]
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
@@ -559,26 +600,37 @@ extension DropboxFileProvider: ExtendedFileProvider {
/// Default value for dimension is 64x64, according to Dropbox documentation
open func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) {
let url: URL
let thumbAPI: Bool
switch (path as NSString).pathExtension.lowercased() {
case "jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff":
url = URL(string: "files/get_thumbnail", relativeTo: contentURL)!
thumbAPI = true
case "doc", "docx", "docm", "xls", "xlsx", "xlsm":
fallthrough
case "ppt", "pps", "ppsx", "ppsm", "pptx", "pptm":
fallthrough
case "rtf":
url = URL(string: "files/get_preview", relativeTo: contentURL)!
thumbAPI = false
default:
return
}
var request = URLRequest(url: url)
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.set(httpAuthentication: credential, with: .oAuth2)
var requestDictionary: [String: AnyObject] = ["path": path as NSString]
requestDictionary["format"] = "jpeg" as NSString
if let dimension = dimension {
requestDictionary["size"] = "w\(Int(dimension.width))h\(Int(dimension.height))" as NSString
if thumbAPI {
requestDictionary["format"] = "jpeg" as NSString
let size: String
switch dimension?.height ?? 64 {
case 0...32: size = "w32h32"
case 33...64: size = "w64h64"
case 65...128: size = "w128h128"
case 129...480: size = "w640h480"
default: size = "w1024h768"
}
requestDictionary["size"] = size as NSString
}
request.setValue(String(jsonDictionary: requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
request.set(dropboxArgKey: requestDictionary)
let task = self.session.dataTask(with: request, completionHandler: { (data, response, error) in
var image: ImageClass? = nil
if let r = response as? HTTPURLResponse, let result = r.allHeaderFields["Dropbox-API-Result"] as? String, let jsonResult = result.deserializeJSON() {
@@ -591,8 +643,12 @@ extension DropboxFileProvider: ExtendedFileProvider {
image = pageImage
} else if let contentType = (response as? HTTPURLResponse)?.allHeaderFields["Content-Type"] as? String, contentType.contains("text/html") {
// TODO: Implement converting html returned type of get_preview to image
} else {
image = ImageClass(data: data)
} else if let fetchedimage = ImageClass(data: data){
if let dimension = dimension {
image = DropboxFileProvider.scaleDown(image: fetchedimage, toSize: dimension)
} else {
image = fetchedimage
}
}
}
completionHandler(image, error)
@@ -604,8 +660,8 @@ extension DropboxFileProvider: ExtendedFileProvider {
let url = URL(string: "files/get_metadata", relativeTo: apiURL)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .json)
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString, "include_media_info": NSNumber(value: true)]
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
@@ -615,7 +671,7 @@ extension DropboxFileProvider: ExtendedFileProvider {
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
serverError = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
if let json = data?.deserializeJSON(), let properties = json["media_info"] as? [String: Any] {
if let json = data?.deserializeJSON(), let properties = (json["media_info"] as? [String: Any])?["metadata"] as? [String: Any] {
(dic, keys) = self.mapMediaInfo(properties)
}
}
+50 -23
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 ?? "")
@@ -75,7 +71,11 @@ public final class DropboxFileObject: FileObject {
// codebeat:disable[ARITY]
internal extension DropboxFileProvider {
func list(_ path: String, cursor: String? = nil, prevContents: [DropboxFileObject] = [], recursive: Bool = false, session: URLSession? = nil, progressHandler: ((_ contents: [FileObject], _ nextCursor: String?, _ error: Error?) -> Void)? = nil, completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) {
func list(_ path: String, cursor: String? = nil, prevContents: [DropboxFileObject] = [], recursive: Bool = false, session: URLSession? = nil, progress: Progress, progressHandler: ((_ contents: [FileObject], _ nextCursor: String?, _ error: Error?) -> Void)? = nil, completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) {
if progress.isCancelled { return }
var requestDictionary = [String: AnyObject]()
let url: URL
if let cursor = cursor {
@@ -88,8 +88,8 @@ internal extension DropboxFileProvider {
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .json)
request.httpBody = Data(jsonDictionary: requestDictionary)
let task = (session ?? self.session).dataTask(with: request, completionHandler: { (data, response, error) in
var responseError: FileProviderDropboxError?
@@ -103,13 +103,14 @@ internal extension DropboxFileProvider {
for entry in entries {
if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry) {
files.append(file)
progress.totalUnitCount = Int64(files.count)
}
}
let ncursor = json["cursor"] as? String
let hasmore = (json["has_more"] as? NSNumber)?.boolValue ?? false
if hasmore {
if hasmore && !progress.isCancelled {
progressHandler?(files, ncursor, responseError ?? error)
self.list(path, cursor: ncursor, prevContents: prevContents + files, completionHandler: completionHandler)
self.list(path, cursor: ncursor, prevContents: prevContents + files, progress: progress, completionHandler: completionHandler)
return
}
}
@@ -117,11 +118,15 @@ internal extension DropboxFileProvider {
progressHandler?(files, nil, responseError ?? error)
completionHandler(prevContents + files, nil, responseError ?? error)
})
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.taskDescription = FileOperationType.fetch(path: path).json
task.resume()
}
func upload_simple(_ targetPath: String, data: Data? = nil, localFile: URL? = nil, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
func upload_simple(_ targetPath: String, data: Data? = nil, localFile: URL? = nil, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? {
let size = data?.count ?? Int((try? localFile?.resourceValues(forKeys: [.fileSizeKey]))??.fileSize ?? -1)
if size > 150 * 1024 * 1024 {
let error = FileProviderDropboxError(code: .payloadTooLarge, path: targetPath, errorDescription: nil)
@@ -129,6 +134,13 @@ internal extension DropboxFileProvider {
self.delegateNotify(.create(path: targetPath), error: error)
return nil
}
var progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
progress.totalUnitCount = Int64(size)
var requestDictionary = [String: AnyObject]()
let url: URL
url = URL(string: "files/upload", relativeTo: contentURL)!
@@ -137,9 +149,9 @@ 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")
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)
@@ -155,20 +167,30 @@ internal extension DropboxFileProvider {
// We can't fetch server result from delegate!
responseError = FileProviderDropboxError(code: rCode, path: targetPath, errorDescription: nil)
}
if !(responseError == nil && error == nil) {
progress.cancel()
}
completionHandler?(responseError ?? error)
self?.delegateNotify(.create(path: targetPath), error: responseError ?? error)
self?.delegateNotify(.modify(path: targetPath), error: responseError ?? error)
}
task.taskDescription = operation.json
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent), options: .new, context: &progress)
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return RemoteOperationHandle(operationType: operation, tasks: [task])
return progress
}
func search(_ startPath: String = "", query: String, start: Int = 0, maxResultPerPage: Int = 25, maxResults: Int = -1, foundItem:@escaping ((_ file: DropboxFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) {
func search(_ startPath: String = "", query: String, start: Int = 0, maxResultPerPage: Int = 25, maxResults: Int = -1, progress: Progress, foundItem:@escaping ((_ file: DropboxFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) {
if progress.isCancelled { return }
let url = URL(string: "files/search", relativeTo: apiURL)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .json)
var requestDictionary: [String: AnyObject] = ["path": startPath as NSString]
requestDictionary["query"] = query as NSString
requestDictionary["start"] = start as NSNumber
@@ -184,12 +206,13 @@ internal extension DropboxFileProvider {
for entry in entries {
if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry) {
foundItem(file)
progress.completedUnitCount += 1
}
}
let rstart = json["start"] as? Int
let hasmore = (json["more"] as? NSNumber)?.boolValue ?? false
if hasmore, let rstart = rstart {
self.search(startPath, query: query, start: rstart + entries.count, maxResultPerPage: maxResultPerPage, foundItem: foundItem, completionHandler: completionHandler)
if hasmore && !progress.isCancelled, let rstart = rstart {
self.search(startPath, query: query, start: rstart + entries.count, maxResultPerPage: maxResultPerPage, progress: progress, foundItem: foundItem, completionHandler: completionHandler)
} else {
completionHandler(responseError ?? error)
}
@@ -197,7 +220,11 @@ internal extension DropboxFileProvider {
}
}
completionHandler(responseError ?? error)
})
})
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
}
}
+87 -46
View File
@@ -12,7 +12,7 @@ import CoreGraphics
import AVFoundation
extension LocalFileProvider: ExtendedFileProvider {
public func thumbnailOfFileSupported(path: String) -> Bool {
open func thumbnailOfFileSupported(path: String) -> Bool {
switch (path as NSString).pathExtension.lowercased() {
case LocalFileInformationGenerator.imageThumbnailExtensions:
return true
@@ -31,7 +31,7 @@ extension LocalFileProvider: ExtendedFileProvider {
}
}
public func propertiesOfFileSupported(path: String) -> Bool {
open func propertiesOfFileSupported(path: String) -> Bool {
let fileExt = (path as NSString).pathExtension.lowercased()
switch fileExt {
case LocalFileInformationGenerator.imagePropertiesExtensions:
@@ -54,7 +54,7 @@ extension LocalFileProvider: ExtendedFileProvider {
}
}
public func thumbnailOfFile(path: String, dimension: CGSize? = nil, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) {
open func thumbnailOfFile(path: String, dimension: CGSize? = nil, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) {
let dimension = dimension ?? CGSize(width: 64, height: 64)
(dispatch_queue).async {
var thumbnailImage: ImageClass? = nil
@@ -86,7 +86,7 @@ extension LocalFileProvider: ExtendedFileProvider {
}
}
public func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String: Any], _ keys: [String], _ error: Error?) -> Void)) {
open func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String: Any], _ keys: [String], _ error: Error?) -> Void)) {
(dispatch_queue).async {
let fileExt = (path as NSString).pathExtension.lowercased()
var getter: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))?
@@ -197,8 +197,13 @@ public struct LocalFileInformationGenerator {
static public var audioThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in
let playerItem = AVPlayerItem(url: fileURL)
let metadataList = playerItem.asset.commonMetadata
#if swift(>=4.0)
let commonKeyArtwork = AVMetadataKey.commonKeyArtwork
#else
let commonKeyArtwork = AVMetadataCommonKeyArtwork
#endif
for item in metadataList {
if item.commonKey == AVMetadataCommonKeyArtwork {
if item.commonKey == commonKeyArtwork {
if let data = item.dataValue {
return ImageClass(data: data)
}
@@ -319,28 +324,34 @@ public struct LocalFileInformationGenerator {
guard let key = key else {
return nil
}
guard let regex = try? NSRegularExpression(pattern: "([a-z])([A-Z])" , options: NSRegularExpression.Options()) else {
guard let regex = try? NSRegularExpression(pattern: "([a-z])([A-Z])" , options: []) else {
return nil
}
let newKey = regex.stringByReplacingMatches(in: key, options: NSRegularExpression.MatchingOptions(), range: NSMakeRange(0, (key as NSString).length) , withTemplate: "$1 $2")
let newKey = regex.stringByReplacingMatches(in: key, options: [], range: NSRange(location: 0, length: (key as NSString).length) , withTemplate: "$1 $2")
return newKey.capitalized
}
if FileManager.default.fileExists(atPath: fileURL.path) {
let playerItem = AVPlayerItem(url: fileURL)
let metadataList = playerItem.asset.commonMetadata
for item in metadataList {
if let description = makeDescription(item.commonKey) {
if let value = item.stringValue {
keys.append(description)
dic[description] = value
}
guard FileManager.default.fileExists(atPath: fileURL.path) else {
return (dic, keys)
}
let playerItem = AVPlayerItem(url: fileURL)
let metadataList = playerItem.asset.commonMetadata
for item in metadataList {
#if swift(>=4.0)
let commonKey = item.commonKey?.rawValue
#else
let commonKey = item.commonKey
#endif
if let description = makeDescription(commonKey) {
if let value = item.stringValue {
keys.append(description)
dic[description] = value
}
}
if let ap = try? AVAudioPlayer(contentsOf: fileURL) {
add(key: "Duration", value: ap.duration.formatshort)
add(key: "Bitrate", value: ap.settings[AVSampleRateKey] as? Int)
}
}
if let ap = try? AVAudioPlayer(contentsOf: fileURL) {
add(key: "Duration", value: ap.duration.formatshort)
add(key: "Bitrate", value: ap.settings[AVSampleRateKey] as? Int)
}
return (dic, keys)
}
@@ -366,7 +377,11 @@ public struct LocalFileInformationGenerator {
}
}
let asset = AVURLAsset(url: fileURL, options: nil)
#if swift(>=4.0)
let videoTracks = asset.tracks(withMediaType: AVMediaType.video)
#else
let videoTracks = asset.tracks(withMediaType: AVMediaTypeVideo)
#endif
if let videoTrack = videoTracks.first {
var bitrate: Float = 0
let width = Int(videoTrack.naturalSize.width)
@@ -380,7 +395,11 @@ public struct LocalFileInformationGenerator {
add(key: "Duration", value: TimeInterval(duration).formatshort)
add(key: "Video Bitrate", value: "\(Int(ceil(bitrate / 1000))) kbps")
}
#if swift(>=4.0)
let audioTracks = asset.tracks(withMediaType: AVMediaType.audio)
#else
let audioTracks = asset.tracks(withMediaType: AVMediaTypeAudio)
#endif
// dic["Audio channels"] = audioTracks.count
var bitrate: Float = 0
for track in audioTracks {
@@ -403,21 +422,40 @@ public struct LocalFileInformationGenerator {
}
func getKey(_ key: String, from dict: CGPDFDictionaryRef) -> String? {
var cfValue: CGPDFStringRef? = nil
if (CGPDFDictionaryGetString(dict, key, &cfValue)), let value = CGPDFStringCopyTextString(cfValue!) {
var cfStrValue: CGPDFStringRef?
if (CGPDFDictionaryGetString(dict, key, &cfStrValue)), let value = cfStrValue.flatMap({ CGPDFStringCopyTextString($0) }) {
return value as String
}
var cfArrayValue: CGPDFArrayRef?
if (CGPDFDictionaryGetArray(dict, key, &cfArrayValue)), let cfArray = cfArrayValue {
var array = [String]()
for i in 0..<CGPDFArrayGetCount(cfArray) {
var cfItemValue: CGPDFStringRef?
if CGPDFArrayGetString(cfArray, i, &cfItemValue), let item = cfItemValue.flatMap({ CGPDFStringCopyTextString($0) }) {
array.append(item as String)
}
}
return array.joined(separator: ", ")
}
return nil
}
func convertDate(_ date: String?) -> Date? {
guard let date = date else { return nil }
var dateStr = date
var dateStr = date.replacingOccurrences(of: "'", with: "")
if dateStr.hasPrefix("D:") {
dateStr.characters.removeFirst(2)
}
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMddHHmmssTZD"
dateFormatter.dateFormat = "yyyyMMddHHmmssTZ"
if let result = dateFormatter.date(from: dateStr) {
return result
}
dateFormatter.dateFormat = "yyyyMMddHHmmssZZZZZ"
if let result = dateFormatter.date(from: dateStr) {
return result
}
dateFormatter.dateFormat = "yyyyMMddHHmmssZ"
if let result = dateFormatter.date(from: dateStr) {
return result
}
@@ -428,29 +466,32 @@ public struct LocalFileInformationGenerator {
return nil
}
if let provider = CGDataProvider(url: fileURL as CFURL), let reference = CGPDFDocument(provider), let dict = reference.info {
add(key: "Title", value: getKey("Title", from: dict))
add(key: "Author", value: getKey("Author", from: dict))
add(key: "Subject", value: getKey("Subject", from: dict))
var majorVersion: Int32 = 0
var minorVersion: Int32 = 0
reference.getVersion(majorVersion: &majorVersion, minorVersion: &minorVersion)
if majorVersion > 0 {
add(key: "Version", value: String(majorVersion) + "." + String(minorVersion))
}
add(key: "Pages", value: reference.numberOfPages)
if reference.numberOfPages > 0, let pageRef = reference.page(at: 1) {
let size = pageRef.getBoxRect(CGPDFBox.mediaBox).size
add(key: "Resolution", value: "\(Int(size.width))x\(Int(size.height))")
}
add(key: "Content creator", value: getKey("Creator", from: dict))
add(key: "Creation date", value: convertDate(getKey("CreationDate", from: dict)))
add(key: "Modified date", value: convertDate(getKey("ModDate", from: dict)))
add(key: "Security", value: reference.isEncrypted ? "Present" : "None")
add(key: "Allows printing", value: reference.allowsPrinting ? "Yes" : "No")
add(key: "Allows copying", value: reference.allowsCopying ? "Yes" : "No")
guard let provider = CGDataProvider(url: fileURL as CFURL), let reference = CGPDFDocument(provider), let dict = reference.info else {
return (dic, keys)
}
add(key: "Title", value: getKey("Title", from: dict))
add(key: "Author", value: getKey("Author", from: dict))
add(key: "Subject", value: getKey("Subject", from: dict))
add(key: "Producer", value: getKey("Producer", from: dict))
add(key: "Keywords", value: getKey("Keywords", from: dict))
var majorVersion: Int32 = 0
var minorVersion: Int32 = 0
reference.getVersion(majorVersion: &majorVersion, minorVersion: &minorVersion)
if majorVersion > 0 {
add(key: "Version", value: String(majorVersion) + "." + String(minorVersion))
}
add(key: "Pages", value: reference.numberOfPages)
if reference.numberOfPages > 0, let pageRef = reference.page(at: 1) {
let size = pageRef.getBoxRect(CGPDFBox.mediaBox).size
add(key: "Resolution", value: "\(Int(size.width))x\(Int(size.height))")
}
add(key: "Content creator", value: getKey("Creator", from: dict))
add(key: "Creation date", value: convertDate(getKey("CreationDate", from: dict)))
add(key: "Modified date", value: convertDate(getKey("ModDate", from: dict)))
add(key: "Security", value: reference.isEncrypted)
add(key: "Allows printing", value: reference.allowsPrinting)
add(key: "Allows copying", value: reference.allowsCopying)
return (dic, keys)
}
+77 -8
View File
@@ -26,7 +26,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
fileprivate var _taskDescription: String?
/// Force using `URLSessionStreamTask` for iOS 9 and later
public var useURLSession = true
public let useURLSession: Bool
@available(iOS 9.0, OSX 10.11, *)
fileprivate static var streamTasks = [Int: URLSessionStreamTask]()
@@ -65,6 +65,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
return _taskDescription
}
@objc(setTaskDescription:)
set {
if #available(iOS 9.0, OSX 10.11, *) {
if self.useURLSession {
@@ -120,8 +121,31 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
}
}
fileprivate var _countOfBytesSent: Int64 = 0
fileprivate var _countOfBytesRecieved: Int64 = 0
fileprivate var _countOfBytesSent: Int64 = 0 {
willSet {
for observer in observers where observer.keyPath == "countOfBytesSent" {
observer.observer.observeValue(forKeyPath: observer.keyPath, of: self, change: [.oldKey: _countOfBytesSent, .oldKey: newValue], context: observer.context)
}
}
didSet {
for observer in observers where observer.keyPath == "countOfBytesSent" {
observer.observer.observeValue(forKeyPath: observer.keyPath, of: self, change: [.oldKey: oldValue, .oldKey: _countOfBytesSent], context: observer.context)
}
}
}
fileprivate var _countOfBytesRecieved: Int64 = 0 {
willSet {
for observer in observers where observer.keyPath == "countOfBytesRecieved" {
observer.observer.observeValue(forKeyPath: observer.keyPath, of: self, change: [.oldKey: _countOfBytesRecieved, .oldKey: newValue], context: observer.context)
}
}
didSet {
for observer in observers where observer.keyPath == "countOfBytesRecieved" {
observer.observer.observeValue(forKeyPath: observer.keyPath, of: self, change: [.oldKey: oldValue, .oldKey: _countOfBytesRecieved], context: observer.context)
}
}
}
/**
* The number of bytes that the task has sent to the server in the request body.
@@ -132,7 +156,7 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
* `urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)` delegate method.
*/
override open var countOfBytesSent: Int64 {
if #available(iOS 9.0, OSX 10.11, *) {
if #available(iOS 9.0, macOS 10.11, *) {
if self.useURLSession {
return _underlyingTask!.countOfBytesSent
}
@@ -191,6 +215,49 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
return Int64(dataReceived.count)
}
var observers: [(keyPath: String, observer: NSObject, context: UnsafeMutableRawPointer?)] = []
public override func addObserver(_ observer: NSObject, forKeyPath keyPath: String, options: NSKeyValueObservingOptions = [], context: UnsafeMutableRawPointer?) {
if #available(iOS 9.0, macOS 10.11, *) {
if self.useURLSession {
self._underlyingTask?.addObserver(observer, forKeyPath: keyPath, options: options, context: context)
return
}
}
switch keyPath {
case #keyPath(countOfBytesSent):
fallthrough
case #keyPath(countOfBytesReceived):
fallthrough
case #keyPath(countOfBytesExpectedToSend):
fallthrough
case #keyPath(countOfBytesExpectedToReceive):
observers.append((keyPath: keyPath, observer: observer, context: context))
default:
break
}
super.addObserver(observer, forKeyPath: keyPath, options: options, context: context)
}
public override func removeObserver(_ observer: NSObject, forKeyPath keyPath: String) {
var newObservers: [(keyPath: String, observer: NSObject, context: UnsafeMutableRawPointer?)] = []
for observer in observers where observer.keyPath != keyPath {
newObservers.append(observer)
}
self.observers = newObservers
super.removeObserver(observer, forKeyPath: keyPath)
}
public override func removeObserver(_ observer: NSObject, forKeyPath keyPath: String, context: UnsafeMutableRawPointer?) {
var newObservers: [(keyPath: String, observer: NSObject, context: UnsafeMutableRawPointer?)] = []
for observer in observers where observer.keyPath != keyPath || observer.context != context {
newObservers.append(observer)
}
self.observers = newObservers
super.removeObserver(observer, forKeyPath: keyPath, context: context)
}
override public init() {
fatalError("Use NSURLSession.fpstreamTask() method")
}
@@ -198,10 +265,11 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
fileprivate var host: (hostname: String, port: Int)?
fileprivate var service: NetService?
internal init(session: URLSession, host: String, port: Int) {
internal init(session: URLSession, host: String, port: Int, useURLSession: Bool = true) {
self._underlyingSession = session
self.useURLSession = useURLSession
if #available(iOS 9.0, OSX 10.11, *) {
if self.useURLSession {
if useURLSession {
let task = session.streamTask(withHostName: host, port: port)
self._taskIdentifier = task.taskIdentifier
FileProviderStreamTask.streamTasks[_taskIdentifier] = task
@@ -217,10 +285,11 @@ public class FileProviderStreamTask: URLSessionTask, StreamDelegate {
self.operation_queue.maxConcurrentOperationCount = 1
}
internal init(session: URLSession, netService: NetService) {
internal init(session: URLSession, netService: NetService, useURLSession: Bool = true) {
self._underlyingSession = session
self.useURLSession = useURLSession
if #available(iOS 9.0, OSX 10.11, *) {
if self.useURLSession {
if useURLSession {
let task = session.streamTask(with: netService)
self._taskIdentifier = task.taskIdentifier
FileProviderStreamTask.streamTasks[_taskIdentifier] = task
+183 -80
View File
@@ -35,9 +35,7 @@ open class FTPFileProvider: FileProviderBasicRemote {
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
@@ -72,32 +70,43 @@ open class FTPFileProvider: FileProviderBasicRemote {
/**
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
let defaultPort: Int = baseURL.scheme == "ftps" ? 990 : 21
urlComponents.port = urlComponents.port ?? defaultPort
urlComponents.scheme = urlComponents.scheme ?? "ftp"
self.baseURL = (urlComponents.url!.path.hasSuffix("/") ? urlComponents.url! : urlComponents.url!.appendingPathComponent("")).absoluteURL
self.passiveMode = passive
self.currentPath = ""
self.useCache = false
self.validatingCache = true
self.cache = cache
self.credential = credential
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")
@@ -111,6 +120,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 {
@@ -140,8 +150,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)
}
/**
@@ -155,7 +167,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)
@@ -173,6 +184,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)
}
@@ -191,8 +207,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)
}
/**
@@ -238,6 +254,7 @@ open class FTPFileProvider: FileProviderBasicRemote {
}
if response.hasPrefix("500") {
self.serverSupportsRFC3659 = false
self.attributesOfItem(path: path, rfc3659enabled: false, completionHandler: completionHandler)
}
@@ -262,12 +279,14 @@ open class FTPFileProvider: FileProviderBasicRemote {
}
}
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
self.recursiveList(path: path, useMLST: true, foundItemsHandler: { items in
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? {
let progress = Progress(parent: nil, userInfo: nil)
_ = self.recursiveList(path: path, useMLST: true, foundItemsHandler: { items in
if let foundItemHandler = foundItemHandler {
for item in items where query.evaluate(with: item.mapPredicate()) {
foundItemHandler(item)
}
progress.totalUnitCount = Int64(items.count)
}
}, completionHandler: {files, error in
if let error = error {
@@ -278,6 +297,7 @@ open class FTPFileProvider: FileProviderBasicRemote {
let foundFiles = files.filter { query.evaluate(with: $0.mapPredicate()) }
completionHandler(foundFiles, nil)
})
return progress
}
public func url(of path: String?) -> URL {
@@ -313,24 +333,24 @@ open class FTPFileProvider: FileProviderBasicRemote {
}
extension FTPFileProvider: FileProviderOperations {
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? {
let path = (atPath as NSString).appendingPathComponent(folderName) + "/"
return doOperation(.create(path: path), completionHandler: completionHandler)
}
open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler)
}
open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
return doOperation(.copy(source: path, destination: toPath), completionHandler: completionHandler)
}
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
return doOperation(.remove(path: path), completionHandler: completionHandler)
}
fileprivate func doOperation(_ opType: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
fileprivate func doOperation(_ opType: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? {
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
@@ -352,7 +372,10 @@ extension FTPFileProvider: FileProviderOperations {
default: // modify, fetch
return nil
}
let operationHandle = RemoteOperationHandle(operationType: opType, tasks: [])
let progress = Progress(totalUnitCount: 1)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
self.ftpLogin(task) { (error) in
@@ -395,13 +418,12 @@ extension FTPFileProvider: FileProviderOperations {
case .modify:
errorCode = URLError.cannotWriteToFile
case .copy:
let opHandle = self.fallbackCopy(opType, completionHandler: completionHandler) as? RemoteOperationHandle
operationHandle.tasks = opHandle?.tasks ?? []
self.fallbackCopy(opType, progress: progress, completionHandler: completionHandler)
return
case .move:
errorCode = URLError.cannotMoveFile
case .remove:
self.fallbackRemove(opType, on: task, completionHandler: completionHandler)
self.fallbackRemove(opType, progress: progress, on: task, completionHandler: completionHandler)
return
case .link:
errorCode = URLError.cannotWriteToFile
@@ -409,31 +431,37 @@ extension FTPFileProvider: FileProviderOperations {
errorCode = URLError.cannotOpenFile
}
let error = self.throwError(sourcePath, code: errorCode)
progress.cancel()
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
}
self.delegateNotify(opType, error: error)
return
}
progress.completedUnitCount = progress.totalUnitCount
self.dispatch_queue.async {
completionHandler?(nil)
self.delegateNotify(opType, error: nil)
}
self.delegateNotify(opType, error: nil)
})
}
operationHandle.add(task: task)
return operationHandle
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
return progress
}
private func fallbackCopy(_ opType: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
guard let sourcePath = opType.source else { return nil }
guard let destPath = opType.destination else { return nil }
private func fallbackCopy(_ opType: FileOperationType, progress: Progress, completionHandler: SimpleCompletionHandler) {
guard let sourcePath = opType.source else { return }
guard let destPath = opType.destination else { return }
let localURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString).appendingPathExtension("tmp")
let operationHandle = RemoteOperationHandle(operationType: opType, tasks: [])
let firstOp = self.copyItem(path: sourcePath, toLocalURL: localURL, completionHandler: { (error) in
progress.becomeCurrent(withPendingUnitCount: 1)
_ = self.copyItem(path: sourcePath, toLocalURL: localURL) { (error) in
if let error = error {
self.dispatch_queue.async {
completionHandler?(error)
@@ -442,51 +470,60 @@ extension FTPFileProvider: FileProviderOperations {
return
}
let secondOp = self.copyItem(localFile: localURL, to: destPath, completionHandler: completionHandler) as? RemoteOperationHandle
operationHandle.tasks = secondOp?.tasks ?? []
}) as? RemoteOperationHandle
operationHandle.tasks = firstOp?.tasks ?? []
return operationHandle
progress.becomeCurrent(withPendingUnitCount: 1)
_ = self.copyItem(localFile: localURL, to: destPath) { error in
completionHandler?(nil)
self.delegateNotify(opType, error: nil)
}
progress.resignCurrent()
}
progress.resignCurrent()
return
}
private func fallbackRemove(_ opType: FileOperationType, on task: FileProviderStreamTask, completionHandler: SimpleCompletionHandler) {
private func fallbackRemove(_ opType: FileOperationType, progress: Progress, on task: FileProviderStreamTask, completionHandler: SimpleCompletionHandler) {
guard let sourcePath = opType.source else { return }
self.execute(command: "SITE RMDIR \(ftpPath(sourcePath))", on: task) { (response, error) in
if let error = error {
progress.cancel()
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
}
self.delegateNotify(opType, error: error)
return
}
guard let response = response else {
progress.cancel()
let error = self.throwError(sourcePath, code: URLError.badServerResponse)
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
}
self.delegateNotify(opType, error: error)
return
}
if response.hasPrefix("50") {
self.fallbackRecursiveRemove(opType, on: task, completionHandler: completionHandler)
self.fallbackRecursiveRemove(opType, progress: progress, on: task, completionHandler: completionHandler)
return
}
let error = self.throwError(sourcePath, code: URLError.cannotRemoveFile)
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)
}
self.delegateNotify(opType, error: error)
}
}
private func fallbackRecursiveRemove(_ opType: FileOperationType, on task: FileProviderStreamTask, completionHandler: SimpleCompletionHandler) {
private func fallbackRecursiveRemove(_ opType: FileOperationType, progress: Progress, on task: FileProviderStreamTask, completionHandler: SimpleCompletionHandler) {
guard let sourcePath = opType.source else { return }
self.recursiveList(path: sourcePath, useMLST: true, completionHandler: { (contents, error) in
_ = self.recursiveList(path: sourcePath, useMLST: true, completionHandler: { (contents, error) in
if let error = error {
self.dispatch_queue.async {
completionHandler?(error)
@@ -495,6 +532,8 @@ extension FTPFileProvider: FileProviderOperations {
return
}
let recursiveProgress = Progress(parent: progress, userInfo: nil)
recursiveProgress.totalUnitCount = Int64(contents.count)
let sortedContents = contents.sorted(by: {
$0.path.localizedStandardCompare($1.path) == .orderedDescending
})
@@ -505,6 +544,7 @@ extension FTPFileProvider: FileProviderOperations {
command += "RMD \(self.ftpPath(sourcePath))"
self.execute(command: command, on: task, completionHandler: { (response, error) in
recursiveProgress.completedUnitCount += 1
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
@@ -514,7 +554,7 @@ extension FTPFileProvider: FileProviderOperations {
})
}
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
// check file is not a folder
guard (try? localFile.resourceValues(forKeys: [.fileResourceTypeKey]))?.fileResourceType ?? .unknown == .regular else {
dispatch_queue.async {
@@ -527,7 +567,11 @@ extension FTPFileProvider: FileProviderOperations {
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
let operation = RemoteOperationHandle(operationType: opType, tasks: [])
let progress = Progress(totalUnitCount: 0)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
self.ftpLogin(task) { (error) in
@@ -539,14 +583,21 @@ extension FTPFileProvider: FileProviderOperations {
return
}
self.ftpStore(task, filePath: self.ftpPath(toPath), fromData: nil, fromFile: localFile, onTask: {
operation.add(task: $0)
$0.taskDescription = opType.json
self.ftpStore(task, filePath: self.ftpPath(toPath), fromData: nil, fromFile: localFile, onTask: { task in
weak var weakTask = task
progress.cancellationHandler = {
weakTask?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
}, onProgress: { bytesSent, totalSent, expectedBytes in
progress.completedUnitCount = totalSent
DispatchQueue.main.async {
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(Double(totalSent) / Double(expectedBytes)))
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(progress.fractionCompleted))
}
}, completionHandler: { (error) in
if error != nil {
progress.cancel()
}
self.ftpQuit(task)
self.dispatch_queue.async {
completionHandler?(error)
@@ -555,15 +606,18 @@ extension FTPFileProvider: FileProviderOperations {
})
}
return operation
return progress
}
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.copy(source: path, destination: destURL.absoluteString)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
let operation = RemoteOperationHandle(operationType: opType, tasks: [])
var progress = Progress(totalUnitCount: 0)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
if self.useAppleImplementation {
self.attributesOfItem(path: path, completionHandler: { (file, error) in
@@ -584,6 +638,8 @@ extension FTPFileProvider: FileProviderOperations {
return
}
progress.totalUnitCount = file?.size ?? 0
let task = self.session.downloadTask(with: self.url(of: path))
completionHandlersForTasks[self.session.sessionDescription!]?[task.taskIdentifier] = completionHandler
downloadCompletionHandlersForTasks[self.session.sessionDescription!]?[task.taskIdentifier] = { tempURL in
@@ -594,8 +650,13 @@ extension FTPFileProvider: FileProviderOperations {
completionHandler?(e)
}
}
operation.add(task: task)
task.taskDescription = opType.json
task.addObserver(self.sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: &progress)
task.addObserver(self.sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: &progress)
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
})
} else {
@@ -608,14 +669,21 @@ extension FTPFileProvider: FileProviderOperations {
return
}
self.ftpRetrieveFile(task, filePath: self.ftpPath(path), onTask: {
operation.add(task: $0)
$0.taskDescription = opType.json
self.ftpRetrieveFile(task, filePath: self.ftpPath(path), onTask: { task in
weak var weakTask = task
progress.cancellationHandler = {
weakTask?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
}, onProgress: { recevied, totalReceived, totalSize in
let progress = Double(totalReceived) / Double(totalSize)
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(progress))
progress.totalUnitCount = totalSize
progress.completedUnitCount = totalReceived
DispatchQueue.main.async {
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(progress.fractionCompleted))
}
}) { (tmpurl, error) in
if let error = error {
progress.cancel()
self.dispatch_queue.async {
completionHandler?(error)
self.delegateNotify(opType, error: error)
@@ -633,20 +701,28 @@ extension FTPFileProvider: FileProviderOperations {
}
}
}
return operation
return progress
}
}
extension FTPFileProvider: FileProviderReadWrite {
public func contents(path: String, completionHandler: @escaping ((Data?, Error?) -> Void)) -> OperationHandle? {
open func contents(path: String, completionHandler: @escaping ((Data?, Error?) -> Void)) -> Progress? {
let opType = FileOperationType.fetch(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
if self.useAppleImplementation {
var progress = Progress(totalUnitCount: 0)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
let task = session.downloadTask(with: url(of: path))
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
if error != nil {
progress.cancel()
}
completionHandler(nil, error)
}
downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { tempURL in
@@ -658,14 +734,20 @@ extension FTPFileProvider: FileProviderReadWrite {
}
}
task.taskDescription = opType.json
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: &progress)
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: &progress)
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return RemoteOperationHandle(operationType: opType, tasks: [task])
return progress
} else {
return self.contents(path: path, offset: 0, length: -1, completionHandler: completionHandler)
}
}
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
let opType = FileOperationType.fetch(path: path)
if length == 0 || offset < 0 {
dispatch_queue.async {
@@ -674,7 +756,10 @@ extension FTPFileProvider: FileProviderReadWrite {
}
return nil
}
let operation = RemoteOperationHandle(operationType: opType, tasks: [])
let progress = Progress(totalUnitCount: 0)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
self.ftpLogin(task) { (error) in
@@ -685,14 +770,21 @@ extension FTPFileProvider: FileProviderReadWrite {
return
}
self.ftpRetrieveData(task, filePath: self.ftpPath(path), from: offset, length: length, onTask: {
operation.add(task: $0)
$0.taskDescription = opType.json
self.ftpRetrieveData(task, filePath: self.ftpPath(path), from: offset, length: length, onTask: { task in
weak var weakTask = task
progress.cancellationHandler = {
weakTask?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
}, onProgress: { recevied, totalReceived, totalSize in
let progress = Double(totalReceived) / Double(totalSize)
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(progress))
progress.totalUnitCount = totalSize
progress.completedUnitCount = totalReceived
DispatchQueue.main.async {
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(progress.fractionCompleted))
}
}) { (data, error) in
if let error = error {
progress.cancel()
self.dispatch_queue.async {
completionHandler(nil, error)
self.delegateNotify(opType, error: error)
@@ -709,16 +801,20 @@ extension FTPFileProvider: FileProviderReadWrite {
}
}
return operation
return progress
}
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) -> Progress? {
let opType = FileOperationType.modify(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
let operation = RemoteOperationHandle(operationType: opType, tasks: [])
let progress = Progress(totalUnitCount: Int64(data?.count ?? 0))
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
self.ftpLogin(task) { (error) in
if let error = error {
@@ -730,14 +826,21 @@ extension FTPFileProvider: FileProviderReadWrite {
}
let storeHandler = {
self.ftpStore(task, filePath: self.ftpPath(path), fromData: data ?? Data(), fromFile: nil, onTask: {
operation.add(task: $0)
$0.taskDescription = opType.json
self.ftpStore(task, filePath: self.ftpPath(path), fromData: data ?? Data(), fromFile: nil, onTask: { task in
weak var weakTask = task
progress.cancellationHandler = {
weakTask?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
}, onProgress: { bytesSent, totalSent, expectedBytes in
progress.completedUnitCount = totalSent
DispatchQueue.main.async {
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(Double(totalSent) / Double(expectedBytes)))
self.delegate?.fileproviderProgress(self, operation: opType, progress: Float(progress.fractionCompleted))
}
}, completionHandler: { (error) in
if error != nil {
progress.cancel()
}
self.ftpQuit(task)
self.dispatch_queue.async {
completionHandler?(error)
@@ -757,11 +860,11 @@ extension FTPFileProvider: FileProviderReadWrite {
}
}
return operation
return progress
}
}
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
+184 -116
View File
@@ -8,7 +8,7 @@
import Foundation
extension FTPFileProvider {
internal extension FTPFileProvider {
func delegateNotify(_ operation: FileOperationType, error: Error?) {
DispatchQueue.main.async(execute: {
if error == nil {
@@ -65,7 +65,7 @@ extension FTPFileProvider {
}
if let data = data, let response = String(data: data, encoding: .utf8) {
completionHandler(response.trimmingCharacters(in: CharacterSet(charactersIn: "\r\n")), nil)
completionHandler(response.trimmingCharacters(in: .whitespacesAndNewlines), nil)
} else {
completionHandler(nil, self.throwError("", code: URLError.cannotParseResponse))
return
@@ -80,6 +80,13 @@ extension FTPFileProvider {
task.resume()
}
var isSecure = false
// Implicit FTP Connection
if self.baseURL?.port == 990 || self.baseURL?.scheme == "ftps" {
task.startSecureConnection()
isSecure = true
}
let credential = self.credential
task.readData(ofMinLength: 4, maxLength: 2048, timeout: timeout) { (data, eof, error) in
@@ -94,15 +101,12 @@ extension FTPFileProvider {
}
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
}
let loginHandle = {
let loginHandle: () -> Void = {
self.execute(command: "USER \(credential?.user ?? "anonymous")", on: task) { (response, error) in
if let error = error {
completionHandler(error)
@@ -121,9 +125,9 @@ extension FTPFileProvider {
}
// needs password
if response.hasPrefix("33") {
if FileProviderFTPError(message: response).code == 331 {
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 {
completionHandler(self.throwError("", code: URLError.userAuthenticationRequired))
@@ -132,18 +136,40 @@ extension FTPFileProvider {
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
}
}
if self.baseURL?.scheme == "ftps" || self.baseURL?.port == 990 {
if !isSecure && self.baseURL?.scheme == "ftpes" {
// Explicit FTP Connection, by upgrading connection to FTP/SSL
self.execute(command: "AUTH TLS", on: task, minLength: 0, completionHandler: { (response, error) in
task.startSecureConnection()
if let error = error {
completionHandler(error)
return
}
if let response = response, response.hasPrefix("23") {
task.startSecureConnection()
isSecure = true
self.execute(command: "PBSZ 0\r\nPROT P", on: task, completionHandler: { (response, error) in
if let error = error {
completionHandler(error)
return
}
loginHandle()
})
}
})
} else if isSecure {
self.execute(command: "PBSZ 0\r\nPROT P", on: task, completionHandler: { (response, error) in
if let error = error {
completionHandler(error)
return
}
loginHandle()
})
} else {
@@ -170,10 +196,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
}
@@ -192,7 +215,7 @@ extension FTPFileProvider {
return
}
guard let response = response, let destString = response.components(separatedBy: " ").flatMap({ $0 }).last else {
guard let response = response, let destString = response.components(separatedBy: " ").flatMap({ $0 }).last.flatMap({ String($0) }) else {
completionHandler(nil, self.throwError("", code: URLError.badServerResponse))
return
}
@@ -213,27 +236,27 @@ extension FTPFileProvider {
let passiveTask = self.session.fpstreamTask(withHostName: host, port: port)
passiveTask.resume()
if self.baseURL?.scheme == "ftps" || self.baseURL?.port == 990 {
task.startSecureConnection()
if self.baseURL?.scheme == "ftps" || self.baseURL?.scheme == "ftpes" || self.baseURL?.port == 990 {
passiveTask.startSecureConnection()
}
completionHandler(passiveTask, nil)
}
}
func ftpActive(_ task: FileProviderStreamTask, completionHandler: @escaping (_ dataTask: FileProviderStreamTask?, _ error: Error?) -> Void) {
var port: Int32 = 0
var _activeTask: FileProviderStreamTask?
while (_activeTask?.state ?? .suspended) == .suspended {
port = 32000 + Int32(arc4random_uniform(16384))
let service = NetService(domain: "", type: "_tcp.", name: "", port: port)
_activeTask = self.session.fpstreamTask(withNetService: service)
_activeTask?.resume()
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)
}
guard let activeTask = _activeTask else { return }
let activeTask = self.session.fpstreamTask(withNetService: service)
activeTask.resume()
if self.baseURL?.scheme == "ftps" || self.baseURL?.port == 990 {
task.startSecureConnection()
activeTask.startSecureConnection()
}
self.execute(command: "PORT \(port)", on: task) { (response, error) in
self.execute(command: "PORT \(service.port)", on: task) { (response, error) in
if let error = error {
activeTask.cancel()
completionHandler(nil, error)
@@ -241,11 +264,13 @@ extension FTPFileProvider {
}
guard let response = response else {
activeTask.cancel()
completionHandler(nil, self.throwError("", code: URLError.badServerResponse))
return
}
guard !response.hasPrefix("5") else {
activeTask.cancel()
completionHandler(nil, self.throwError("", code: URLError.cannotConnectToHost))
return
}
@@ -258,7 +283,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)
}
}
}
@@ -270,13 +297,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
}
@@ -297,7 +326,7 @@ extension FTPFileProvider {
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
@@ -308,7 +337,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)
}
@@ -319,7 +348,9 @@ 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
}
@@ -334,7 +365,7 @@ extension FTPFileProvider {
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
@@ -350,17 +381,15 @@ extension FTPFileProvider {
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 !success && !(response.hasPrefix("25") || response.hasPrefix("15")) {
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: path, errorDescription: description)
let error = FileProviderFTPError(message: response, path: path)
self.dispatch_queue.async {
completionHandler([], error)
}
@@ -370,8 +399,9 @@ extension FTPFileProvider {
}
}
func recursiveList(path: String, useMLST: Bool, foundItemsHandler: ((_ contents: [FileObject]) -> Void)? = nil, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) {
let queue = DispatchQueue(label: "test")
func recursiveList(path: String, useMLST: Bool, foundItemsHandler: ((_ contents: [FileObject]) -> Void)? = nil, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) -> Progress? {
let progress = Progress(totalUnitCount: 0)
let queue = DispatchQueue(label: "\(self.type).recursiveList")
queue.async {
let group = DispatchGroup()
var result = [FileObject]()
@@ -386,12 +416,14 @@ extension FTPFileProvider {
}
result.append(contentsOf: files)
progress.completedUnitCount = Int64(files.count)
foundItemsHandler?(files)
let directories: [FileObject] = files.filter { $0.isDirectory }
progress.becomeCurrent(withPendingUnitCount: Int64(directories.count))
for dir in directories {
group.enter()
self.recursiveList(path: dir.path, useMLST: useMLST, foundItemsHandler: foundItemsHandler, completionHandler: { (contents, error) in
_=self.recursiveList(path: dir.path, useMLST: useMLST, foundItemsHandler: foundItemsHandler, completionHandler: { (contents, error) in
success = success && (error == nil)
if let error = error {
completionHandler([], error)
@@ -401,9 +433,11 @@ extension FTPFileProvider {
foundItemsHandler?(files)
result.append(contentsOf: contents)
group.leave()
})
}
progress.resignCurrent()
group.leave()
})
group.wait()
@@ -414,6 +448,7 @@ extension FTPFileProvider {
}
}
}
return progress
}
func ftpRetrieveData(_ task: FileProviderStreamTask, filePath: String, from position: Int64 = 0, length: Int = -1, onTask: ((_ task: FileProviderStreamTask) -> Void)?, onProgress: ((_ bytesReceived: Int64, _ totalReceived: Int64, _ expectedBytes: Int64) -> Void)?, completionHandler: @escaping (_ data: Data?, _ error: Error?) -> Void) {
@@ -501,10 +536,7 @@ extension FTPFileProvider {
}
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)
@@ -617,10 +649,7 @@ extension FTPFileProvider {
}
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)
@@ -633,7 +662,70 @@ extension FTPFileProvider {
}
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)
@@ -646,8 +738,9 @@ extension FTPFileProvider {
}
// Send retreive command
let len = 17 /* */
self.execute(command: "TYPE I" + "\r\n" + "REST 0" + "\r\n" + "STOR \(filePath)", on: task, minLength: 90 + filePath.characters.count, 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 {
@@ -655,63 +748,21 @@ extension FTPFileProvider {
}
onTask?(dataTask)
DispatchQueue.global().async {
var error: Error?
let chunkSize = 65536
guard let size: Int64 = (fromData != nil ? Int64(fromData!.count) : nil) ?? fromFile?.fileSize else { return }
var fileHandle: FileHandle?
if let file = fromFile {
fileHandle = FileHandle(forReadingAtPath: file.path)
}
defer {
fileHandle?.closeFile()
}
var eof = false
var sent: Int64 = 0
let group = DispatchGroup()
while !eof {
group.enter()
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 }
dataTask.write(subdata, timeout: timeout, completionHandler: { (serror) in
error = serror
sent += Int64(subdata.count)
onProgress?(Int64(subdata.count), sent, size)
group.leave()
})
}
let waitResult = group.wait(timeout: .now() + timeout)
if data.count == 0 { return }
dataTask.write(data, timeout: timeout, completionHandler: { (error) in
if let error = error {
completionHandler(error)
return
}
if waitResult == .timedOut {
error = self.throwError(fromFile?.relativePath ?? filePath, code: URLError.timedOut)
completionHandler(error)
return
}
success = true
dataTask.closeRead()
dataTask.closeWrite()
completionHandler(nil)
return
}
})
}) { (response, error) in
guard success else { return }
if let error = error {
completionHandler(error)
return
@@ -723,16 +774,15 @@ extension FTPFileProvider {
}
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(error)
}
return
}
completionHandler(nil)
}
}
}
@@ -894,4 +944,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
}
}
}
+21 -7
View File
@@ -13,22 +13,29 @@ 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, 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[.fileURLKey] 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[.fileURLKey] = newValue
@@ -133,13 +140,20 @@ 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
+87 -65
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 }
@@ -99,7 +99,8 @@ public protocol FileProviderBasic: class, NSCoding, NSSecureCoding {
- foundItemHandler: Closure which is called when a file is found
- completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured.
*/
func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void))
@discardableResult
func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress?
/**
Search files inside directory using query asynchronously.
@@ -121,8 +122,10 @@ public protocol FileProviderBasic: class, NSCoding, NSSecureCoding {
- query: An `NSPredicate` object with keys like `FileObject` members, except `size` which becomes `filesize`.
- foundItemHandler: Closure which is called when a file is found
- completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured.
- Returns: An `Progress` to get progress or cancel progress.
*/
func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void))
@discardableResult
func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress?
/**
Returns an independent url to access the file. Some providers like `Dropbox` due to their nature.
@@ -146,9 +149,9 @@ public protocol FileProviderBasic: class, NSCoding, NSSecureCoding {
}
extension FileProviderBasic {
public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? {
let predicate = NSPredicate(format: "name BEGINSWITH[c] %@", query)
self.searchFiles(path: path, recursive: recursive, query: predicate, foundItemHandler: foundItemHandler, completionHandler: completionHandler)
return self.searchFiles(path: path, recursive: recursive, query: predicate, foundItemHandler: foundItemHandler, completionHandler: completionHandler)
}
/// The maximum number of queued operations that can execute at the same time.
@@ -162,8 +165,6 @@ extension FileProviderBasic {
operation_queue.maxConcurrentOperationCount = newValue
}
}
}
/// Checking equality of two file provider, regardless of current path queues and delegates.
@@ -241,7 +242,7 @@ internal extension FileProviderBasicRemote {
return false
}
func runDataTask(with request: URLRequest, operationHandle: RemoteOperationHandle? = nil, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Swift.Void) {
func runDataTask(with request: URLRequest, operation: FileOperationType? = nil, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Swift.Void) {
let useCache = self.useCache
let validatingCache = self.validatingCache
dispatch_queue.async {
@@ -251,8 +252,7 @@ internal extension FileProviderBasicRemote {
}
}
let task = self.session.dataTask(with: request, completionHandler: completionHandler)
task.taskDescription = operationHandle?.operationType.json
operationHandle?.add(task: task)
task.taskDescription = operation?.json
task.resume()
}
}
@@ -271,10 +271,10 @@ public protocol FileProviderOperations: FileProviderBasic {
- folder: Directory name.
- at: Parent path of new directory.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func create(folder: String, at: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func create(folder: String, at: String, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Moves a file or directory from `path` to designated path asynchronously.
@@ -285,10 +285,10 @@ public protocol FileProviderOperations: FileProviderBasic {
- path: original file or directory path.
- to: destination path of file or directory, including file/directory name.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func moveItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func moveItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Moves a file or directory from `path` to designated path asynchronously.
@@ -300,10 +300,10 @@ public protocol FileProviderOperations: FileProviderBasic {
- to: destination path of file or directory, including file/directory name.
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func moveItem(path: String, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func moveItem(path: String, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Copies a file or directory from `path` to designated path asynchronously.
@@ -314,10 +314,10 @@ public protocol FileProviderOperations: FileProviderBasic {
- path: original file or directory path.
- to: destination path of file or directory, including file/directory name.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func copyItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func copyItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Copies a file or directory from `path` to designated path asynchronously.
@@ -329,10 +329,10 @@ public protocol FileProviderOperations: FileProviderBasic {
- to: destination path of file or directory, including file/directory name.
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func copyItem(path: String, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func copyItem(path: String, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Removes the file or directory at the specified path.
@@ -340,11 +340,11 @@ public protocol FileProviderOperations: FileProviderBasic {
- Parameters:
- path: file or directory path.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Uploads a file from local file url to designated path asynchronously.
@@ -356,10 +356,10 @@ public protocol FileProviderOperations: FileProviderBasic {
- localFile: a file url to file.
- to: destination path of file, including file/directory name.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress.
- Returns: An `Progress` to get progress or cancel progress.
*/
@discardableResult
func copyItem(localFile: URL, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func copyItem(localFile: URL, to: String, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Uploads a file from local file url to designated path asynchronously.
@@ -372,10 +372,10 @@ public protocol FileProviderOperations: FileProviderBasic {
- to: destination path of file, including file/directory name.
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress.
- Returns: An `Progress` to get progress or cancel progress.
*/
@discardableResult
func copyItem(localFile: URL, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func copyItem(localFile: URL, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Download a file from `path` to designated local file url asynchronously.
@@ -387,33 +387,33 @@ public protocol FileProviderOperations: FileProviderBasic {
- path: original file or directory path.
- toLocalURL: destination local url of file, including file/directory name.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> Progress?
}
public extension FileProviderOperations {
/// *DEPRECATED:* Use Use FileProviderReadWrite.writeContents(path:, data:, completionHandler:) method instead.
@available(*, deprecated, message: "Use FileProviderReadWrite.writeContents(path:, data:, completionHandler:) method instead.")
@discardableResult
public func create(file: String, at: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
public func create(file: String, at: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> Progress? {
let path = (at as NSString).appendingPathComponent(file)
return (self as? FileProviderReadWrite)?.writeContents(path: path, contents: data, completionHandler: completionHandler)
}
@discardableResult
public func moveItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
public func moveItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> Progress? {
return self.moveItem(path: path, to: to, overwrite: false, completionHandler: completionHandler)
}
@discardableResult
public func copyItem(localFile: URL, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
public func copyItem(localFile: URL, to: String, completionHandler: SimpleCompletionHandler) -> Progress? {
return self.copyItem(localFile: localFile, to: to, overwrite: false, completionHandler: completionHandler)
}
@discardableResult
public func copyItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
public func copyItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> Progress? {
return self.copyItem(path: path, to: to, overwrite: false, completionHandler: completionHandler)
}
}
@@ -429,10 +429,10 @@ public protocol FileProviderReadWrite: FileProviderBasic {
- completionHandler: a closure with result of file contents or error.
- `contents`: contents of file in a `Data` object.
- `error`: Error returned by system.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle?
func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress?
/**
Retreives a `Data` object with a portion contents of the file asynchronously vis contents argument of completion handler.
@@ -445,10 +445,10 @@ public protocol FileProviderReadWrite: FileProviderBasic {
- completionHandler: a closure with result of file contents or error.
- `contents`: contents of file in a `Data` object.
- `error`: Error returned by system.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle?
func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress?
/**
Write the contents of the `Data` to a location asynchronously.
@@ -459,10 +459,10 @@ public protocol FileProviderReadWrite: FileProviderBasic {
- path: Path of target file.
- contents: Data to be written into file, pass nil to create empty file.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func writeContents(path: String, contents: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func writeContents(path: String, contents: Data?, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Write the contents of the `Data` to a location asynchronously.
@@ -473,10 +473,10 @@ public protocol FileProviderReadWrite: FileProviderBasic {
- contents: Data to be written into file, pass nil to create empty file.
- atomically: data will be written to a temporary file before writing to final location. Default is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func writeContents(path: String, contents: Data?, atomically: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func writeContents(path: String, contents: Data?, atomically: Bool, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Write the contents of the `Data` to a location asynchronously.
@@ -487,10 +487,10 @@ public protocol FileProviderReadWrite: FileProviderBasic {
- contents: Data to be written into file, pass nil to create empty file.
- overwrite: Destination file should be overwritten if file is already exists. Default is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func writeContents(path: String, contents: Data?, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func writeContents(path: String, contents: Data?, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress?
/**
Write the contents of the `Data` to a location asynchronously.
@@ -501,30 +501,30 @@ public protocol FileProviderReadWrite: FileProviderBasic {
- overwrite: Destination file should be overwritten if file is already exists. Default is `false`.
- atomically: data will be written to a temporary file before writing to final location. Default is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func writeContents(path: String, contents: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
func writeContents(path: String, contents: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress?
}
extension FileProviderReadWrite {
@discardableResult
public func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle?{
public func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
return self.contents(path: path, offset: 0, length: -1, completionHandler: completionHandler)
}
@discardableResult
public func writeContents(path: String, contents: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
public func writeContents(path: String, contents: Data?, completionHandler: SimpleCompletionHandler) -> Progress? {
return self.writeContents(path: path, contents: contents, atomically: false, overwrite: false, completionHandler: completionHandler)
}
@discardableResult
public func writeContents(path: String, contents: Data?, atomically: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
public func writeContents(path: String, contents: Data?, atomically: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
return self.writeContents(path: path, contents: contents, atomically: atomically, overwrite: false, completionHandler: completionHandler)
}
@discardableResult
public func writeContents(path: String, contents: Data?, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
public func writeContents(path: String, contents: Data?, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
return self.writeContents(path: path, contents: contents, atomically: false, overwrite: overwrite, completionHandler: completionHandler)
}
}
@@ -570,7 +570,7 @@ public protocol FileProvideUndoable: FileProviderOperations {
var undoManager: UndoManager? { get set }
/// UndoManager supports undoing this file operation
func canUndo(handle: OperationHandle) -> Bool
func canUndo(handle: Progress) -> Bool
/// UndoManager supports undoing this operation
func canUndo(operation: FileOperationType) -> Bool
}
@@ -580,8 +580,11 @@ public extension FileProvideUndoable {
return undoOperation(for: operation) != nil
}
public func canUndo(handle: OperationHandle) -> Bool {
return canUndo(operation: handle.operationType)
public func canUndo(handle: Progress) -> Bool {
if let operationType = handle.userInfo[.fileProvderOperationTypeKey] as? FileOperationType {
return canUndo(operation: operationType)
}
return false
}
internal func undoOperation(for operation: FileOperationType) -> FileOperationType? {
@@ -611,14 +614,37 @@ 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
}
public func url(of path: String? = nil) -> URL {
@@ -691,7 +717,7 @@ extension FileProviderBasic {
}
var i = number ?? 2
let similiar = contents.map {
$0.url?.lastPathComponent ?? $0.name
$0.url.lastPathComponent.isEmpty ? $0.name : $0.url.lastPathComponent
}.filter {
$0.hasPrefix(result)
}
@@ -711,6 +737,8 @@ extension FileProviderBasic {
let domain: String
switch code {
case is URLError:
fallthrough
case is URLError.Code:
domain = NSURLErrorDomain
default:
domain = NSCocoaErrorDomain
@@ -842,19 +870,20 @@ extension ExtendedFileProvider {
return resultingImage
#else
let ppp = Int(UIScreen.main.scale) // fetch device is retina or not
guard let context = UIGraphicsGetCurrentContext() else {
return nil
}
size.width *= CGFloat(ppp)
size.height *= CGFloat(ppp)
UIGraphicsBeginImageContext(size)
guard let context = UIGraphicsGetCurrentContext() else {
return nil
}
context.saveGState()
let transform = pdfPage.getDrawingTransform(CGPDFBox.mediaBox, rect: rect, rotate: 0, preserveAspectRatio: true)
context.concatenate(transform)
context.translateBy(x: 0, y: size.height)
context.scaleBy(x: CGFloat(ppp), y: CGFloat(-ppp))
context.setFillColor(UIColor.white.cgColor)
context.fill(rect)
context.drawPDFPage(pdfPage)
context.restoreGState()
@@ -982,6 +1011,7 @@ public enum FileOperationType: CustomStringConvertible {
}
/// Allows to get progress or cancel an in-progress operation, useful for remote providers
@available(*, obsoleted: 1.0, message: "Use Progress class class instead.")
public protocol OperationHandle {
/// Operation supposed to be done on files. Contains file paths as associated value.
var operationType: FileOperationType { get }
@@ -1002,14 +1032,6 @@ public protocol OperationHandle {
func cancel() -> Bool
}
public extension OperationHandle {
public var progress: Float {
let bytesSoFar = self.bytesSoFar
let totalBytes = self.totalBytes
return totalBytes > 0 ? Float(Double(bytesSoFar) / Double(totalBytes)) : Float.nan
}
}
/// Delegate methods for reporting provider's operation result and progress, when it's ready to update
/// user interface.
/// All methods are called in main thread to avoids UI bugs.
+91 -19
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,20 +37,22 @@ extension URLFileResourceType {
}
}
internal extension URLResourceKey {
static let fileURLKey = URLResourceKey(rawValue: "NSURLFileURLKey")
static let serverDateKey = URLResourceKey(rawValue: "NSURLServerDateKey")
static let entryTagKey = URLResourceKey(rawValue: "NSURLEntryTagKey")
static let mimeTypeKey = URLResourceKey(rawValue: "NSURLMIMETypeIdentifierKey")
@available(*, deprecated, renamed: "fileURLKey")
static let fileURL = fileURLKey
@available(*, deprecated, renamed: "serverDateKey")
static let serverDate = serverDateKey
@available(*, deprecated, renamed: "entryTagKey")
static let entryTag = entryTagKey
@available(*, deprecated, renamed: "mimeTypeKey")
static let mimeType = mimeTypeKey
public extension URLResourceKey {
/// **FileProvider** returns url of file object.
public static let fileURLKey = URLResourceKey(rawValue: "NSURLFileURLKey")
/// **FileProvider** returns modification date of file in server
public static let serverDateKey = URLResourceKey(rawValue: "NSURLServerDateKey")
/// **FileProvider** returns HTTP ETag string of remote resource
public static let entryTagKey = URLResourceKey(rawValue: "NSURLEntryTagKey")
/// **FileProvider** returns MIME type of file, if returned by server
public static let mimeTypeKey = URLResourceKey(rawValue: "NSURLMIMETypeIdentifierKey")
/// **FileProvider** returns either file is encrypted or not
public static let isEncryptedKey = URLResourceKey(rawValue: "NSURLIsEncryptedKey")
}
public extension ProgressUserInfoKey {
public static let fileProvderOperationTypeKey = ProgressUserInfoKey("FilesProviderOperationTypeKey")
public static let startingTimeKey = ProgressUserInfoKey("NSProgressstartingTimeKey")
}
internal extension URL {
@@ -71,6 +73,76 @@ internal extension URL {
}
}
internal extension URLRequest {
mutating func set(httpAuthentication credential: URLCredential?, with type: HTTPAuthenticationType) {
func base64(_ str: String) -> String {
let plainData = str.data(using: .utf8)
let base64String = plainData!.base64EncodedString(options: [])
return base64String
}
guard let credential = credential else { return }
switch type {
case .basic:
let authStr = "\(credential.user ?? ""):\(credential.password ?? "")"
self.setValue("Basic \(authStr)", forHTTPHeaderField: "Authorization")
case .digest:
// handled by RemoteSessionDelegate
break
case .oAuth1:
if let oauth = credential.password {
self.setValue("OAuth \(oauth)", forHTTPHeaderField: "Authorization")
}
case .oAuth2:
if let bearer = credential.password {
self.setValue("Bearer \(bearer)", forHTTPHeaderField: "Authorization")
}
}
}
mutating func set(rangeWithOffset offset: Int64, length: Int) {
if length > 0 {
self.setValue("bytes=\(offset)-\(offset + Int64(length) - 1)", forHTTPHeaderField: "Range")
} else if offset > 0 && length < 0 {
self.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
}
}
enum ContentType: String {
case json = "application/json"
case stream = "application/octet-stream"
case xml = "text/xml; charset=\"utf-8\""
}
mutating func set(contentType: ContentType) {
self.setValue(contentType.rawValue, forHTTPHeaderField: "Content-Type")
}
private func asciiEscape(string:String) -> String {
var res = ""
for char in string.unicodeScalars {
let substring = String(char)
if substring.canBeConverted(to: .ascii) {
res.append(substring)
} else {
res = res.appendingFormat("\\u%04x", char.value)
}
}
return res
}
mutating func set(dropboxArgKey requestDictionary: [String: AnyObject]) {
guard let jsonData = try? JSONSerialization.data(withJSONObject: requestDictionary, options: []) else {
return
}
guard var jsonString = String(data: jsonData, encoding: .utf8) else { return }
jsonString = self.asciiEscape(string: jsonString)
jsonString = jsonString.replacingOccurrences(of: "\\/", with: "/")
self.setValue(jsonString, forHTTPHeaderField: "Dropbox-API-Arg")
}
}
internal extension Data {
internal var isPDF: Bool {
return self.count > 4 && self.scanString(length: 4, using: .ascii) == "%PDF"
@@ -160,7 +232,7 @@ internal extension TimeInterval {
}
}
extension Date {
internal extension Date {
init?(rfcString: String) {
let dateFor: DateFormatter = DateFormatter()
dateFor.locale = Locale(identifier: "en_US")
@@ -197,7 +269,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
+74 -26
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
@@ -168,22 +173,31 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
completionHandler(totalSize, totalSize - freeSize)
}
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? {
let progress = Progress(parent: nil, userInfo: nil)
dispatch_queue.async {
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
let iterator = self.fileManager.enumerator(at: self.url(of: path), includingPropertiesForKeys: nil, options: recursive ? [] : [.skipsSubdirectoryDescendants, .skipsPackageDescendants]) { (url, e) -> Bool in
completionHandler([], e)
return true
}
var result = [LocalFileObject]()
while let fileURL = iterator?.nextObject() as? URL {
if progress.isCancelled {
break
}
let path = self.relativePathOf(url: fileURL)
if let fileObject = LocalFileObject(fileWithPath: path, relativeTo: self.baseURL), query.evaluate(with: fileObject.mapPredicate()) {
result.append(fileObject)
progress.completedUnitCount = Int64(result.count)
foundItemHandler?(fileObject)
}
}
completionHandler(result, nil)
}
return progress
}
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
@@ -195,13 +209,13 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
open weak var fileOperationDelegate : FileOperationDelegate?
@discardableResult
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(folderName) + "/")
return self.doOperation(opType, completionHandler: completionHandler)
}
@discardableResult
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.move(source: path, destination: toPath)
if !overwrite && self.fileManager.fileExists(atPath: self.url(of: toPath).path) {
@@ -213,7 +227,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
}
@discardableResult
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.copy(source: path, destination: toPath)
if !overwrite && self.fileManager.fileExists(atPath: self.url(of: toPath).path) {
@@ -227,13 +241,13 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
}
@discardableResult
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.remove(path: path)
return self.doOperation(opType, completionHandler: completionHandler)
}
@discardableResult
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
if !overwrite && self.fileManager.fileExists(atPath: self.url(of: toPath).path) {
self.dispatch_queue.async {
completionHandler?(self.throwError(toPath, code: CocoaError.fileWriteFileExists as FoundationErrorEnum))
@@ -245,12 +259,12 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
}
@discardableResult
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
return self.doOperation(opType, completionHandler: completionHandler)
}
dynamic func doSimpleOperation(_ box: UndoBox) {
@objc dynamic func doSimpleOperation(_ box: UndoBox) {
guard let _ = self.undoManager else { return }
_ = self.doOperation(box.undoOperation) { (_) in
return
@@ -258,8 +272,12 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
}
@discardableResult
fileprivate func doOperation(_ opType: FileOperationType, data: Data? = nil, atomically: Bool = false, forUploading: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
fileprivate func doOperation(_ opType: FileOperationType, data: Data? = nil, atomically: Bool = false, forUploading: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
let progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.isCancellable = false
progress.setUserInfoObject(Progress.FileOperationKind.receiving, forKey: .fileOperationKindKey)
func urlofpath(path: String) -> URL {
if path.hasPrefix("file://") {
let removedSchemePath = path.replacingOccurrences(of: "file://", with: "", options: .anchored)
@@ -293,22 +311,31 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
let operationHandler: (URL, URL?) -> Void = { source, dest in
do {
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
switch opType {
case .create:
if sourcePath.hasSuffix("/") {
progress.totalUnitCount = 1
try self.opFileManager.createDirectory(at: source, withIntermediateDirectories: true, attributes: [:])
} else {
progress.totalUnitCount = Int64(data?.count ?? 0)
try data?.write(to: source, options: .atomic)
}
case .modify:
progress.totalUnitCount = Int64(data?.count ?? 0)
try data?.write(to: source, options: atomically ? [.atomic] : [])
case .copy:
guard let dest = dest else { return }
progress.setUserInfoObject(Progress.FileOperationKind.copying, forKey: .fileOperationKindKey)
progress.totalUnitCount = abs(source.fileSize)
try self.opFileManager.copyItem(at: source, to: dest)
case .move:
progress.setUserInfoObject(Progress.FileOperationKind.copying, forKey: .fileOperationKindKey)
guard let dest = dest else { return }
progress.totalUnitCount = abs(source.fileSize)
try self.opFileManager.moveItem(at: source, to: dest)
case.remove:
progress.totalUnitCount = abs(source.fileSize)
try self.opFileManager.removeItem(at: source)
default:
return
@@ -316,7 +343,8 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
if successfulSecurityScopedResourceAccess {
source.stopAccessingSecurityScopedResource()
}
progress.completedUnitCount = progress.totalUnitCount
self.dispatch_queue.async {
completionHandler?(nil)
}
@@ -327,6 +355,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
if successfulSecurityScopedResourceAccess {
source.stopAccessingSecurityScopedResource()
}
progress.cancel()
self.dispatch_queue.async {
completionHandler?(e)
}
@@ -368,22 +397,31 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
operationHandler(source, dest)
}
}
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
return progress
}
@discardableResult
open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
let opType = FileOperationType.fetch(path: path)
let progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.isCancellable = false
progress.setUserInfoObject(Progress.FileOperationKind.receiving, forKey: .fileOperationKindKey)
let url = self.url(of: path)
progress.totalUnitCount = url.fileSize
let operationHandler: (URL) -> Void = { url in
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
do {
let data = try Data(contentsOf: url)
progress.completedUnitCount = progress.totalUnitCount
self.dispatch_queue.async {
completionHandler(data, nil)
}
} catch let e {
progress.cancel()
self.dispatch_queue.async {
completionHandler(nil, e)
}
@@ -405,24 +443,30 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
operationHandler(url)
}
}
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
return progress
}
@discardableResult
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
if length == 0 || offset < 0 {
dispatch_queue.async {
completionHandler(Data(), nil)
}
return nil
}
if offset == 0 && length < 0 {
return self.contents(path: path, completionHandler: completionHandler)
}
let opType = FileOperationType.fetch(path: path)
let progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.isCancellable = false
progress.setUserInfoObject(Progress.FileOperationKind.receiving, forKey: .fileOperationKindKey)
let url = self.url(of: path)
let operationHandler: (URL) -> Void = { url in
@@ -436,16 +480,20 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
defer {
handle.closeFile()
}
let size = LocalFileObject(fileWithURL: url)?.size ?? -1
progress.totalUnitCount = size
guard size > offset else {
progress.cancel()
self.dispatch_queue.async {
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadTooLarge as FoundationErrorEnum))
}
return
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
handle.seek(toFileOffset: UInt64(offset))
guard Int64(handle.offsetInFile) == offset else {
progress.cancel()
self.dispatch_queue.async {
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadTooLarge as FoundationErrorEnum))
}
@@ -453,7 +501,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
}
let data = handle.readData(ofLength: length)
progress.completedUnitCount = progress.totalUnitCount
self.dispatch_queue.async {
completionHandler(data, nil)
}
@@ -473,11 +521,11 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
}
}
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
return progress
}
@discardableResult
open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
let fileExists = fileManager.fileExists(atPath: url(of: path).path)
let opType: FileOperationType = fileExists ? .modify(path: path) : .create(path: path)
return self.doOperation(opType, data: data ?? Data(), atomically: atomically, completionHandler: completionHandler)
+4 -101
View File
@@ -10,7 +10,7 @@ import Foundation
/// Containts path, url and attributes of a local file or resource.
public final class LocalFileObject: FileObject {
internal override init(url: URL, name: String, path: String) {
internal override init(url: URL?, name: String, path: String) {
super.init(url: url, name: name, path: path)
}
@@ -19,12 +19,13 @@ public final class LocalFileObject: FileObject {
var fileURL: URL?
var rpath = path.replacingOccurrences(of: relativeURL?.path ?? "", with: "", options: .anchored)
if relativeURL != nil && rpath.hasPrefix("/") {
rpath.remove(at: rpath.startIndex)
_=rpath.characters.removeFirst()
}
if #available(iOS 9.0, macOS 10.11, tvOS 9.0, *) {
fileURL = URL(fileURLWithPath: rpath, relativeTo: relativeURL)
} else {
fileURL = URL(string: rpath.isEmpty ? "./" : rpath, relativeTo: relativeURL)
rpath = rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? rpath
fileURL = URL(string: rpath, relativeTo: relativeURL) ?? relativeURL
}
if let fileURL = fileURL {
@@ -216,104 +217,6 @@ internal class LocalFileProviderManagerDelegate: NSObject, FileManagerDelegate {
}
}
/// - 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
}
private var sourceURL: URL? {
guard let source = operationType.source else { return nil }
return source.hasPrefix("file://") ? URL(fileURLWithPath: source) : baseURL.appendingPathComponent(source)
}
private var destURL: URL? {
guard let dest = operationType.destination else { return nil }
return dest.hasPrefix("file://") ? URL(fileURLWithPath: dest) : baseURL.appendingPathComponent(dest)
}
/// Caution: may put pressure on CPU, may have latency
open var bytesSoFar: Int64 {
assert(!Thread.isMainThread, "Don't run \(#function) method on main thread")
switch operationType {
case .modify:
guard let url = sourceURL, url.isFileURL else { return 0 }
if url.fileIsDirectory {
return iterateDirectory(url, deep: true).totalsize
} else {
return url.fileSize
}
case .copy, .move:
guard let url = destURL, url.isFileURL else { return 0 }
if url.fileIsDirectory {
return iterateDirectory(url, deep: true).totalsize
} else {
return url.fileSize
}
default:
return 0
}
}
/// Caution: may put pressure on CPU, may have latency
open var totalBytes: Int64 {
assert(!Thread.isMainThread, "Don't run \(#function) method on main thread")
switch operationType {
case .copy, .move:
guard let url = sourceURL, url.isFileURL else { return 0 }
if url.fileIsDirectory {
return iterateDirectory(url, deep: true).totalsize
} else {
return url.fileSize
}
default:
return 0
}
}
/// Not usable in local provider
open var inProgress: Bool {
return false
}
/// Not usable in local provider
open func cancel() -> Bool{
return false
}
func iterateDirectory(_ pathURL: URL, deep: Bool) -> (folders: Int, files: Int, totalsize: Int64) {
var folders = 0
var files = 0
var totalsize: Int64 = 0
let keys: [URLResourceKey] = [.isDirectoryKey, .fileSizeKey]
let enumOpt: FileManager.DirectoryEnumerationOptions = !deep ? [.skipsSubdirectoryDescendants, .skipsPackageDescendants] : []
let fp = FileManager()
let filesList = fp.enumerator(at: pathURL, includingPropertiesForKeys: keys, options: enumOpt, errorHandler: nil)
while let fileURL = filesList?.nextObject() as? URL {
guard let values = try? fileURL.resourceValues(forKeys: [.isDirectoryKey, .fileSizeKey]) else { continue }
let isdir = values.isDirectory ?? false
let size = Int64(values.fileSize ?? 0)
if isdir {
folders += 1
} else {
files += 1
}
totalsize += size
}
return (folders, files, totalsize)
}
}
class UndoBox: NSObject {
weak var provider: FileProvideUndoable?
let operation: FileOperationType
+104 -55
View File
@@ -43,7 +43,7 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
public var validatingCache: Bool
fileprivate var _session: URLSession?
fileprivate var sessionDelegate: SessionDelegate?
internal fileprivate(set) var sessionDelegate: SessionDelegate?
public var session: URLSession {
get {
if _session == nil {
@@ -87,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) {
@@ -152,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?
@@ -171,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
@@ -184,12 +190,13 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
task.resume()
}
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? {
var foundFiles = [OneDriveFileObject]()
var queryStr: String?
queryStr = query.findValue(forKey: "name") as? String ?? query.findAllValues(forKey: nil).flatMap { $0.value as? String }.first
guard let finalQueryStr = queryStr else { return }
search(path, query: finalQueryStr, foundItem: { (file) in
guard let finalQueryStr = queryStr else { return nil }
let progress = Progress(parent: nil, userInfo: nil)
search(path, query: finalQueryStr, progress: progress, foundItem: { (file) in
if query.evaluate(with: file.mapPredicate()) {
foundFiles.append(file)
foundItemHandler?(file)
@@ -197,6 +204,7 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
}, completionHandler: { (error) in
completionHandler(foundFiles, error)
})
return progress
}
open func url(of path: String? = nil, modifier: String? = nil) -> URL {
@@ -207,28 +215,30 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
rpath = self.currentPath
}
let driveURL = baseURL!.appendingPathComponent("drive/\(drive):/")
if rpath.hasPrefix("/") {
rpath.remove(at: rpath.startIndex)
_=rpath.characters.removeFirst()
}
if rpath.isEmpty {
if let modifier = modifier {
return baseURL!.appendingPathComponent("drive/\(drive)/\(modifier)")
return driveURL.appendingPathComponent(modifier)
}
return baseURL!.appendingPathComponent("drive/\(drive)")
return driveURL
}
let driveURL = baseURL!.appendingPathComponent("drive/\(drive):/")
rpath = (rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? rpath)
rpath = rpath.trimmingCharacters(in: pathTrimSet)
if let modifier = modifier {
rpath = rpath + ":/" + modifier
}
return URL(string: rpath, relativeTo: driveURL) ?? driveURL
return driveURL.appendingPathComponent(rpath)
}
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
var request = URLRequest(url: url())
request.httpMethod = "HEAD"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.set(httpAuthentication: credential, with: .oAuth2)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
let status = (response as? HTTPURLResponse)?.statusCode ?? 400
completionHandler(status == 200)
@@ -241,28 +251,33 @@ open class OneDriveFileProvider: FileProviderBasicRemote {
extension OneDriveFileProvider: FileProviderOperations {
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? {
let path = (atPath as NSString).appendingPathComponent(folderName) + "/"
return doOperation(.create(path: path), completionHandler: completionHandler)
}
open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler)
}
open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
return doOperation(.copy(source: path, destination: toPath), completionHandler: completionHandler)
}
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
return doOperation(.remove(path: path), completionHandler: completionHandler)
}
fileprivate func doOperation(_ operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
fileprivate func doOperation(_ operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? {
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
return nil
}
let progress = Progress(totalUnitCount: 1)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
guard let sourcePath = operation.source else { return nil }
let destPath = operation.destination
var request = URLRequest(url: url(of: sourcePath))
@@ -279,10 +294,10 @@ extension OneDriveFileProvider: FileProviderOperations {
return nil
}
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.set(httpAuthentication: credential, with: .oAuth2)
var requestDictionary = [String: AnyObject]()
if let dest = correctPath(destPath) as NSString? {
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.set(contentType: .json)
requestDictionary["parentReference"] = ("/drive/\(drive):" + dest.deletingLastPathComponent) as NSString
requestDictionary["name"] = dest.lastPathComponent as NSString
request.httpBody = Data(jsonDictionary: requestDictionary)
@@ -292,15 +307,24 @@ extension OneDriveFileProvider: FileProviderOperations {
if let response = response as? HTTPURLResponse, response.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) {
serverError = FileProviderOneDriveError(code: code, path: sourcePath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
}
if serverError == nil && error == nil {
progress.completedUnitCount = 1
} else {
progress.cancel()
}
completionHandler?(serverError ?? error)
self.delegateNotify(operation, error: serverError ?? error)
})
task.taskDescription = operation.json
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return RemoteOperationHandle(operationType: operation, tasks: [task])
return progress
}
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
// check file is not a folder
guard (try? localFile.resourceValues(forKeys: [.fileResourceTypeKey]))?.fileResourceType ?? .unknown == .regular else {
dispatch_queue.async {
@@ -316,20 +340,34 @@ extension OneDriveFileProvider: FileProviderOperations {
return upload_simple(toPath, localFile: localFile, overwrite: overwrite, operation: opType, completionHandler: completionHandler)
}
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.copy(source: path, destination: destURL.absoluteString)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
var progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
var request = URLRequest(url: self.url(of: path, modifier: "content"))
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.set(httpAuthentication: credential, with: .oAuth2)
let task = session.downloadTask(with: request)
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = completionHandler
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
if error != nil {
progress.cancel()
}
completionHandler?(error)
}
downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { tempURL in
guard let httpResponse = task.response as? HTTPURLResponse , httpResponse.statusCode < 300 else {
let code = FileProviderHTTPErrorCode(rawValue: (task.response as? HTTPURLResponse)?.statusCode ?? -1)
let errorData : Data? = nil //Data(contentsOf: cacheURL) // TODO: Figure out how to get error response data for the error description
let serverError : FileProviderOneDriveError? = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: errorData ?? Data(), encoding: .utf8)) : nil
if serverError != nil {
progress.cancel()
}
completionHandler?(serverError)
return
}
@@ -341,13 +379,19 @@ extension OneDriveFileProvider: FileProviderOperations {
}
}
task.taskDescription = opType.json
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: &progress)
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: &progress)
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return RemoteOperationHandle(operationType: opType, tasks: [task])
return progress
}
}
extension OneDriveFileProvider: FileProviderReadWrite {
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
if length == 0 || offset < 0 {
dispatch_queue.async {
completionHandler(Data(), nil)
@@ -356,23 +400,31 @@ extension OneDriveFileProvider: FileProviderReadWrite {
}
let opType = FileOperationType.fetch(path: path)
var progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
var request = URLRequest(url: self.url(of: path, modifier: "content"))
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
if length > 0 {
request.setValue("bytes=\(offset)-\(offset + 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
if error != nil {
progress.cancel()
}
completionHandler(nil, error)
}
downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { tempURL in
downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { tempURL in
guard let httpResponse = task.response as? HTTPURLResponse , httpResponse.statusCode < 300 else {
let code = FileProviderHTTPErrorCode(rawValue: (task.response as? HTTPURLResponse)?.statusCode ?? -1)
let errorData : Data? = nil //Data(contentsOf: cacheURL) // TODO: Figure out how to get error response data for the error description
let serverError : FileProviderOneDriveError? = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: errorData ?? Data(), encoding: .utf8)) : nil
if serverError != nil {
progress.cancel()
}
completionHandler(nil, serverError)
return
}
@@ -384,11 +436,17 @@ extension OneDriveFileProvider: FileProviderReadWrite {
}
}
task.taskDescription = opType.json
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: &progress)
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: &progress)
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return RemoteOperationHandle(operationType: opType, tasks: [task])
return progress
}
open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.modify(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
@@ -409,19 +467,10 @@ extension OneDriveFileProvider: FileProviderReadWrite {
fileprivate func unregisterNotifcation(path: String) {
NotImplemented()
}
/**
Genrates a public url to a file to be shared with other users and can be downloaded without authentication.
- Parameters:
- to: path of file, including file/directory name.
- completionHandler: a closure with result of directory entries or error.
`link`: a url returned by OneDrive to share.
`attribute`: `nil` for OneDrive.
`expiration`: `nil` for OneDrive, as it doesn't expires.
`error`: Error returned by OneDrive.
*/
open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: OneDriveFileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
}
extension OneDriveFileProvider: FileProviderSharing {
open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
var request = URLRequest(url: self.url(of: path, modifier: "action.createLink"))
request.httpMethod = "POST"
let requestDictionary: [String: AnyObject] = ["type": "view" as NSString]
@@ -474,7 +523,7 @@ extension OneDriveFileProvider: ExtendedFileProvider {
}
var request = URLRequest(url: url)
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.set(httpAuthentication: credential, with: .oAuth2)
let task = self.session.dataTask(with: request, completionHandler: { (data, response, error) in
var image: ImageClass? = nil
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
@@ -493,7 +542,7 @@ extension OneDriveFileProvider: ExtendedFileProvider {
open func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) {
var request = URLRequest(url: url(of: path))
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.set(httpAuthentication: credential, with: .oAuth2)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var serverError: FileProviderOneDriveError?
var dic = [String: Any]()
+41 -17
View File
@@ -18,12 +18,13 @@ public struct FileProviderOneDriveError: FileProviderHTTPError {
/// Containts path, url and attributes of a OneDrive file or resource.
public final class OneDriveFileObject: FileObject {
internal init(baseURL: URL?, name: String, path: String) {
var rpath = path
if path.hasPrefix("/") {
rpath.remove(at: rpath.startIndex)
var rpath = (URL(string:path)?.appendingPathComponent(name).absoluteString)!
if rpath.hasPrefix("/") {
_=rpath.characters.removeFirst()
}
let url = URL(string: rpath, relativeTo: baseURL) ?? URL(string: path)!
super.init(url: url, name: name, path: path)
let url = URL(string: rpath, relativeTo: baseURL) ?? URL(string: rpath)!
super.init(url: url, name: name, path: rpath.removingPercentEncoding ?? path)
}
internal convenience init? (baseURL: URL?, drive: String, jsonStr: String) {
@@ -34,14 +35,14 @@ public final class OneDriveFileObject: FileObject {
internal convenience init? (baseURL: URL?, drive: String, json: [String: AnyObject]) {
guard let name = json["name"] as? String else { return nil }
guard let path = (json["parentReference"] as? NSDictionary)?["path"] as? String else { return nil }
var lPath = path.replacingOccurrences(of: "/drive/\(drive)", with: "/", options: .anchored, range: nil)
var lPath = path.replacingOccurrences(of: "/drive/\(drive):", with: "/", options: .anchored, range: nil)
lPath = lPath.replacingOccurrences(of: "/:", with: "", options: .anchored)
lPath = lPath.replacingOccurrences(of: "//", with: "", options: .anchored)
self.init(baseURL: baseURL, name: name, path: lPath)
self.size = (json["size"] as? NSNumber)?.int64Value ?? -1
self.modifiedDate = Date(rfcString: json["lastModifiedDateTime"] as? String ?? "")
self.creationDate = Date(rfcString: json["createdDateTime"] as? String ?? "")
self.type = (json["folder"] as? String) != nil ? .directory : .regular
self.type = json["folder"] != nil ? .directory : .regular
self.id = json["id"] as? String
self.entryTag = json["eTag"] as? String
}
@@ -85,7 +86,7 @@ internal extension OneDriveFileProvider {
let url = cursor ?? self.url(of: path, modifier: "children")
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.set(httpAuthentication: credential, with: .oAuth2)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var responseError: FileProviderOneDriveError?
var files = prevContents
@@ -113,7 +114,7 @@ internal extension OneDriveFileProvider {
task.resume()
}
func upload_simple(_ targetPath: String, data: Data? = nil , localFile: URL? = nil, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
func upload_simple(_ targetPath: String, data: Data? = nil, localFile: URL? = nil, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? {
let size = data?.count ?? (try? localFile?.resourceValues(forKeys: [.fileSizeKey]))??.fileSize ?? -1
if size > 100 * 1024 * 1024 {
let error = FileProviderOneDriveError(code: .payloadTooLarge, path: targetPath, errorDescription: nil)
@@ -121,12 +122,19 @@ internal extension OneDriveFileProvider {
self.delegateNotify(.create(path: targetPath), error: error)
return nil
}
var progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
progress.totalUnitCount = Int64(size)
let queryStr = overwrite ? "" : "?@name.conflictBehavior=fail"
let url = self.url(of: targetPath, modifier: "content\(queryStr)")
var request = URLRequest(url: url)
request.httpMethod = "PUT"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
request.set(httpAuthentication: credential, with: .oAuth2)
request.set(contentType: .stream)
let task: URLSessionUploadTask
if let data = data {
task = session.uploadTask(with: request, from: data)
@@ -142,21 +150,33 @@ internal extension OneDriveFileProvider {
// We can't fetch server result from delegate!
responseError = FileProviderOneDriveError(code: rCode, path: targetPath, errorDescription: nil)
}
if !(responseError == nil && error == nil) {
progress.cancel()
}
completionHandler?(responseError ?? error)
self?.delegateNotify(.create(path: targetPath), error: responseError ?? error)
}
task.taskDescription = operation.json
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent), options: .new, context: &progress)
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return RemoteOperationHandle(operationType: operation, tasks: [task])
return progress
}
func search(_ startPath: String = "", query: String, next: URL? = nil, foundItem:@escaping ((_ file: OneDriveFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) {
func search(_ startPath: String = "", query: String, next: URL? = nil, progress: Progress, foundItem: @escaping ((_ file: OneDriveFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) {
if progress.isCancelled {
return
}
let url: URL
let q = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
url = next ?? self.url(of: startPath, modifier: "view.search?q=\(q)")
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.set(httpAuthentication: credential, with: .oAuth2)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var responseError: FileProviderOneDriveError?
@@ -171,8 +191,8 @@ internal extension OneDriveFileProvider {
}
}
let next: URL? = (json["@odata.nextLink"] as? String).flatMap { URL(string: $0) }
if let next = next {
self.search(startPath, query: query, next: next, foundItem: foundItem, completionHandler: completionHandler)
if !progress.isCancelled, let next = next {
self.search(startPath, query: query, next: next, progress: progress, foundItem: foundItem, completionHandler: completionHandler)
} else {
completionHandler(responseError ?? error)
}
@@ -180,7 +200,11 @@ internal extension OneDriveFileProvider {
}
}
completionHandler(responseError ?? error)
})
})
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
}
}
+61 -61
View File
@@ -8,70 +8,11 @@
import Foundation
/// Allows to get progress or cancel an in-progress operation, for remote, `URLSession` based providers.
/// This class keeps strong reference to tasks.
open class RemoteOperationHandle: OperationHandle {
internal var tasks: [URLSessionTask]
open private(set) var operationType: FileOperationType
init(operationType: FileOperationType, tasks: [URLSessionTask]) {
self.operationType = operationType
self.tasks = tasks
}
internal func add(task: URLSessionTask) {
tasks.append(task)
}
internal func reape() {
self.tasks = tasks.filter { $0.state != .completed }
}
open var bytesSoFar: Int64 {
return tasks.reduce(0) {
switch $1 {
case let task as URLSessionUploadTask:
return $0 + task.countOfBytesSent
case let task as FileProviderStreamTask:
return $0 + task.countOfBytesSent + task.countOfBytesReceived
default:
return $0 + $1.countOfBytesReceived
}
}
}
open var totalBytes: Int64 {
return tasks.reduce(0) {
switch $1 {
case let task as URLSessionUploadTask:
return $0 + task.countOfBytesExpectedToSend
case let task as FileProviderStreamTask:
return $0 + task.countOfBytesExpectedToSend + task.countOfBytesExpectedToReceive
default:
return $0 + $1.countOfBytesExpectedToReceive
}
}
}
open func cancel() -> Bool {
var canceled = false
for taskbox in tasks {
taskbox.cancel()
canceled = true
}
return canceled
}
open var inProgress: Bool {
return tasks.reduce(false) { $0 || $1.state == .running }
}
}
/// A protocol defines properties for errors returned by HTTP/S based providers.
/// Including Dropbox, OneDrive and WebDAV.
public protocol FileProviderHTTPError: Error, CustomStringConvertible {
/// HTTP status codes as an enum.
typealias Code = FileProviderHTTPErrorCode
/// HTTP status code returned for error by server.
var code: FileProviderHTTPErrorCode { get }
/// Path of file/folder casued that error
@@ -90,6 +31,18 @@ extension FileProviderHTTPError {
}
}
/// Defines HTTP Authentication method required to access
public enum HTTPAuthenticationType {
/// Basic method for authentication
case basic
/// Digest method for authentication
case digest
/// OAuth 1.0 method for authentication (OAuth)
case oAuth1
/// OAuth 2.0 method for authentication (Bearer)
case oAuth2
}
internal var completionHandlersForTasks = [String: [Int: SimpleCompletionHandler]]()
internal var downloadCompletionHandlersForTasks = [String: [Int: (URL) -> Void]]()
internal var dataCompletionHandlersForTasks = [String: [Int: (Data) -> Void]]()
@@ -126,8 +79,49 @@ final public class SessionDelegate: NSObject, URLSessionDataDelegate, URLSession
self.credential = fileProvider.credential
}
open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if let progress = context?.load(as: Progress.self), let newVal = change?[.newKey] as? Int64 {
switch keyPath ?? "" {
case #keyPath(URLSessionTask.countOfBytesReceived):
progress.completedUnitCount = newVal
if let startTime = progress.userInfo[ProgressUserInfoKey.startingTimeKey] as? Date, let task = object as? URLSessionTask {
let elapsed = Date().timeIntervalSince(startTime)
let throughput = Double(newVal) / elapsed
progress.setUserInfoObject(NSNumber(value: throughput), forKey: .throughputKey)
if task.countOfBytesExpectedToReceive > 0 {
let remain = task.countOfBytesExpectedToReceive - task.countOfBytesReceived
let estimatedTimeRemaining = Double(remain) / elapsed
progress.setUserInfoObject(NSNumber(value: estimatedTimeRemaining), forKey: .estimatedTimeRemainingKey)
}
}
case #keyPath(URLSessionTask.countOfBytesSent):
progress.completedUnitCount = newVal
if let startTime = progress.userInfo[ProgressUserInfoKey.startingTimeKey] as? Date, let task = object as? URLSessionTask {
let elapsed = Date().timeIntervalSince(startTime)
let throughput = Double(newVal) / elapsed
progress.setUserInfoObject(NSNumber(value: throughput), forKey: .throughputKey)
if task.countOfBytesExpectedToSend > 0 {
let remain = task.countOfBytesExpectedToSend - task.countOfBytesSent
let estimatedTimeRemaining = Double(remain) / elapsed
progress.setUserInfoObject(NSNumber(value: estimatedTimeRemaining), forKey: .estimatedTimeRemainingKey)
}
}
case #keyPath(URLSessionTask.countOfBytesExpectedToReceive), #keyPath(URLSessionTask.countOfBytesExpectedToSend):
progress.totalUnitCount = newVal
default:
break
}
}
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
// codebeat:disable[ARITY]
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived))
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive))
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent))
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived))
if !(error == nil && task is URLSessionDownloadTask) {
let completionHandler = completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] ?? nil
completionHandler?(error)
@@ -142,6 +136,12 @@ final public class SessionDelegate: NSObject, URLSessionDataDelegate, URLSession
if !(task is URLSessionDownloadTask), case FileOperationType.fetch = op {
return
}
if #available(iOS 9.0, macOS 10.11, *) {
if task is URLSessionStreamTask {
return
}
}
DispatchQueue.main.async {
if error != nil {
fileProvider.delegate?.fileproviderFailed(fileProvider, operation: op)
+19 -12
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
}
@@ -68,53 +74,54 @@ class SMBFileProvider: FileProvider, FileProviderMonitor {
open weak var fileOperationDelegate: FileOperationDelegate?
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? {
NotImplemented()
return nil
}
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
NotImplemented()
return nil
}
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
NotImplemented()
return nil
}
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
NotImplemented()
return nil
}
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
NotImplemented()
return nil
}
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
NotImplemented()
return nil
}
open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
NotImplemented()
return nil
}
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
NotImplemented()
return nil
}
open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
NotImplemented()
return nil
}
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler:((FileObjectClass) -> Void)?, completionHandler: @escaping ((_ files: [FileObjectClass], _ error: Error?) -> Void)) {
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler:((FileObjectClass) -> Void)?, completionHandler: @escaping ((_ files: [FileObjectClass], _ error: Error?) -> Void)) -> Progress? {
NotImplemented()
return nil
}
open func registerNotifcation(path: String, eventHandler: @escaping (() -> Void)) {
+222 -52
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
@@ -82,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) {
@@ -159,10 +167,11 @@ 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\(WebDavFileObject.propString(including))\n</D:propfind>".data(using: .utf8)
request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length")
runDataTask(with: request, operationHandle: RemoteOperationHandle(operationType: opType, tasks: []), completionHandler: { (data, response, error) in
runDataTask(with: request, operation: opType, completionHandler: { (data, response, error) in
var responseError: FileProviderWebDavError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
@@ -201,7 +210,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\(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
@@ -230,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
@@ -247,14 +258,16 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
})
}
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? {
let url = self.url(of: path)
var request = URLRequest(url: url)
request.httpMethod = "PROPFIND"
//request.setValue("1", forHTTPHeaderField: "Depth")
request.setValue("text/xml; charset=\"utf-8\"", forHTTPHeaderField: "Content-Type")
request.set(httpAuthentication: credential, with: credentialType)
request.set(contentType: .xml)
request.httpBody = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n<D:allprop/></D:propfind>".data(using: .utf8)
runDataTask(with: request, completionHandler: { (data, response, error) in
let progress = Progress(parent: nil, userInfo: nil)
let task = session.dataTask(with: request) { (data, response, error) in
// FIXME: paginating results
var responseError: FileProviderWebDavError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
@@ -270,20 +283,28 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
}
fileObjects.append(fileObject)
progress.completedUnitCount = Int64(fileObjects.count)
foundItemHandler?(fileObject)
}
completionHandler(fileObjects, responseError ?? error)
return
}
completionHandler([], responseError ?? error)
})
}
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return progress
}
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
var request = URLRequest(url: baseURL!)
request.httpMethod = "PROPFIND"
request.setValue("0", forHTTPHeaderField: "Depth")
request.setValue("text/xml; charset=\"utf-8\"", forHTTPHeaderField: "Content-Type")
request.set(httpAuthentication: credential, with: credentialType)
request.set(contentType: .xml)
request.httpBody = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n<D:prop><D:quota-available-bytes/><D:quota-used-bytes/></D:prop>\n</D:propfind>".data(using: .utf8)
request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length")
runDataTask(with: request, completionHandler: { (data, response, error) in
@@ -297,29 +318,16 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
extension WebDAVFileProvider: FileProviderOperations {
@discardableResult
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(folderName) + "/")
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
let url = self.url(of: atPath).appendingPathComponent(folderName.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? folderName, isDirectory: true)
var request = URLRequest(url: url)
request.httpMethod = "MKCOL"
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var responseError: FileProviderWebDavError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderWebDavError(code: rCode, path: url.relativePath, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
}
completionHandler?(responseError ?? error)
self.delegateNotify(opType, error: responseError ?? error)
})
task.taskDescription = opType.json
task.resume()
return RemoteOperationHandle(operationType: opType, tasks: [task])
return self.doOperation(operation: opType, overwrite: false, completionHandler: completionHandler)
}
@discardableResult
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.move(source: path, destination: toPath)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
@@ -328,7 +336,7 @@ extension WebDAVFileProvider: FileProviderOperations {
}
@discardableResult
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.copy(source: path, destination: toPath)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
@@ -337,7 +345,7 @@ extension WebDAVFileProvider: FileProviderOperations {
}
@discardableResult
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.remove(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
@@ -345,7 +353,7 @@ extension WebDAVFileProvider: FileProviderOperations {
return self.doOperation(operation: opType, completionHandler: completionHandler)
}
func doOperation(operation opType: FileOperationType, overwrite: Bool? = nil, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
fileprivate func doOperation(operation opType: FileOperationType, overwrite: Bool? = nil, completionHandler: SimpleCompletionHandler) -> Progress? {
let source = opType.source!
let sourceURL = self.url(of: source)
var request = URLRequest(url: sourceURL)
@@ -353,6 +361,8 @@ extension WebDAVFileProvider: FileProviderOperations {
request.setValue(url(of:dest).absoluteString, forHTTPHeaderField: "Destination")
}
switch opType {
case .create:
request.httpMethod = "MKCOL"
case .copy:
request.httpMethod = "COPY"
case .move:
@@ -363,6 +373,12 @@ extension WebDAVFileProvider: FileProviderOperations {
return nil
}
let progress = Progress(totalUnitCount: 1)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
request.set(httpAuthentication: credential, with: credentialType)
if let overwrite = overwrite, !overwrite {
request.setValue("F", forHTTPHeaderField: "Overwrite")
}
@@ -377,9 +393,17 @@ extension WebDAVFileProvider: FileProviderOperations {
for xresponse in xresponses where (xresponse.status ?? 0) >= 300 {
let error = FileProviderWebDavError(code: code, path: source, errorDescription: String(data: data, encoding: .utf8), url: sourceURL)
completionHandler?(error)
progress.cancel()
}
}
}
if responseError == nil && error == nil {
progress.completedUnitCount = 1
} else {
progress.cancel()
}
if (response as? HTTPURLResponse)?.statusCode ?? 0 != FileProviderHTTPErrorCode.multiStatus.rawValue {
completionHandler?(responseError ?? error)
}
@@ -387,12 +411,16 @@ extension WebDAVFileProvider: FileProviderOperations {
self.delegateNotify(opType, error: responseError ?? error)
})
task.taskDescription = opType.json
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return RemoteOperationHandle(operationType: opType, tasks: [task])
return progress
}
@discardableResult
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
// check file is not a folder
guard (try? localFile.resourceValues(forKeys: [.fileResourceTypeKey]))?.fileResourceType ?? .unknown == .regular else {
dispatch_queue.async {
@@ -405,12 +433,20 @@ extension WebDAVFileProvider: FileProviderOperations {
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
var progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
progress.totalUnitCount = localFile.fileSize
let url = self.url(of:toPath)
var request = URLRequest(url: url)
if !overwrite {
request.setValue("F", forHTTPHeaderField: "Overwrite")
}
request.httpMethod = "PUT"
request.set(httpAuthentication: credential, with: credentialType)
let task = session.uploadTask(with: request, fromFile: localFile)
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { [weak self] error in
var responseError: FileProviderWebDavError?
@@ -418,24 +454,44 @@ extension WebDAVFileProvider: FileProviderOperations {
// We can't fetch server result from delegate!
responseError = FileProviderWebDavError(code: rCode, path: toPath, errorDescription: nil, url: url)
}
if !(responseError == nil && error == nil) {
progress.cancel()
}
completionHandler?(responseError ?? error)
self?.delegateNotify(.create(path: toPath), error: responseError ?? error)
}
task.taskDescription = opType.json
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent), options: .new, context: &progress)
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return RemoteOperationHandle(operationType: opType, tasks: [task])
return progress
}
@discardableResult
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.copy(source: path, destination: destURL.absoluteString)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
var progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
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[session.sessionDescription!]?[task.taskIdentifier] = completionHandler
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
if error != nil {
progress.cancel()
}
completionHandler?(error)
}
downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { tempURL in
guard let httpResponse = task.response as? HTTPURLResponse , httpResponse.statusCode < 300 else {
let code = FileProviderHTTPErrorCode(rawValue: (task.response as? HTTPURLResponse)?.statusCode ?? -1)
@@ -451,14 +507,20 @@ extension WebDAVFileProvider: FileProviderOperations {
}
}
task.taskDescription = opType.json
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: &progress)
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: &progress)
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return RemoteOperationHandle(operationType: opType, tasks: [task])
return progress
}
}
extension WebDAVFileProvider: FileProviderReadWrite {
@discardableResult
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
if length == 0 || offset < 0 {
dispatch_queue.async {
completionHandler(Data(), nil)
@@ -467,20 +529,26 @@ extension WebDAVFileProvider: FileProviderReadWrite {
}
let opType = FileOperationType.fetch(path: path)
var progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
let url = self.url(of: path)
var request = URLRequest(url: url)
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
if error != nil {
progress.cancel()
}
completionHandler(nil, error)
}
downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { tempURL in
downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { [weak self] tempURL in
guard let httpResponse = task.response as? HTTPURLResponse , httpResponse.statusCode < 300 else {
let code = FileProviderHTTPErrorCode(rawValue: (task.response as? HTTPURLResponse)?.statusCode ?? -1)
let serverError : FileProviderWebDavError? = code != nil ? FileProviderWebDavError(code: code!, path: path, errorDescription: code?.description, url: url) : nil
@@ -489,7 +557,7 @@ extension WebDAVFileProvider: FileProviderReadWrite {
}
do {
let data = try Data(contentsOf: tempURL)
self.dispatch_queue.async {
(self?.dispatch_queue ?? DispatchQueue.global()).async {
completionHandler(data, nil)
}
} catch let e {
@@ -497,20 +565,34 @@ extension WebDAVFileProvider: FileProviderReadWrite {
}
}
task.taskDescription = opType.json
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: &progress)
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: &progress)
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return handle
return progress
}
@discardableResult
open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
let opType = FileOperationType.modify(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
var progress = Progress(parent: nil, userInfo: nil)
progress.setUserInfoObject(opType, forKey: .fileProvderOperationTypeKey)
progress.kind = .file
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
progress.totalUnitCount = Int64(data?.count ?? 0)
// FIXME: lock destination before writing process
let url = atomically ? self.url(of: path).appendingPathExtension("tmp") : self.url(of: path)
var request = URLRequest(url: url)
request.httpMethod = "PUT"
request.set(httpAuthentication: credential, with: credentialType)
if !overwrite {
request.setValue("F", forHTTPHeaderField: "Overwrite")
}
@@ -521,12 +603,20 @@ extension WebDAVFileProvider: FileProviderReadWrite {
// We can't fetch server result from delegate!
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: nil, url: url)
}
if !(responseError == nil && error == nil) {
progress.cancel()
}
completionHandler?(responseError ?? error)
self?.delegateNotify(opType, error: responseError ?? error)
}
task.taskDescription = opType.json
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent), options: .new, context: &progress)
progress.cancellationHandler = { [weak task] in
task?.cancel()
}
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
task.resume()
return RemoteOperationHandle(operationType: opType, tasks: [task])
return progress
}
/*
@@ -545,6 +635,86 @@ extension WebDAVFileProvider: FileProviderReadWrite {
// TODO: implements methods for lock mechanism
}
extension WebDAVFileProvider: ExtendedFileProvider {
open func thumbnailOfFileSupported(path: String) -> Bool {
guard self.baseURL?.host?.contains("dav.yandex.") ?? false else {
return false
}
let supportedExt: [String] = ["jpg", "jpeg", "png", "gif"]
return supportedExt.contains((path as NSString).pathExtension)
}
open func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((ImageClass?, Error?) -> Void)) {
guard self.baseURL?.host?.contains("dav.yandex.") ?? false else {
dispatch_queue.async {
completionHandler(nil, self.throwError(path, code: URLError.resourceUnavailable))
}
return
}
let dimension = dimension ?? CGSize(width: 64, height: 64)
let url = URL(string: self.url(of: path).absoluteString + "?preview&size=\(dimension.width)x\(dimension.height)")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.set(httpAuthentication: credential, with: credentialType)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var responseError: FileProviderWebDavError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderWebDavError(code: rCode, path: url.relativePath, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
completionHandler(nil, responseError ?? error)
return
}
completionHandler(data.flatMap({ ImageClass(data: $0) }), nil)
})
task.resume()
}
open func propertiesOfFileSupported(path: String) -> Bool {
return false
}
open func propertiesOfFile(path: String, completionHandler: @escaping (([String : Any], [String], Error?) -> Void)) {
dispatch_queue.async {
completionHandler([:], [], self.throwError(path, code: URLError.resourceUnavailable))
}
}
}
extension WebDAVFileProvider: FileProviderSharing {
open func publicLink(to path: String, completionHandler: @escaping ((URL?, FileObject?, Date?, Error?) -> Void)) {
guard self.baseURL?.host?.contains("dav.yandex.") ?? false else {
dispatch_queue.async {
completionHandler(nil, nil, nil, self.throwError(path, code: URLError.resourceUnavailable))
}
return
}
let url = self.url(of: path)
var request = URLRequest(url: url)
request.httpMethod = "PROPPATCH"
request.set(httpAuthentication: credential, with: credentialType)
request.set(contentType: .xml)
let body = "<propertyupdate xmlns=\"DAV:\">\n<set><prop>\n<public_url xmlns=\"urn:yandex:disk:meta\">true</public_url>\n</prop></set>\n</propertyupdate>"
request.httpBody = body.data(using: .utf8)
request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length")
runDataTask(with: request, completionHandler: { (data, response, error) in
var responseError: FileProviderWebDavError?
if let code = (response as? HTTPURLResponse)?.statusCode, code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
}
if let data = data {
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
if let urlStr = xresponse.first?.prop["public_url"], let url = URL(string: urlStr) {
completionHandler(url, nil, nil, nil)
return
}
}
completionHandler(nil, nil, nil, responseError ?? error)
})
}
}
extension WebDAVFileProvider: FileProvider { }
// MARK: WEBDAV XML response implementation
@@ -571,7 +741,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