Compare commits
66 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| abf68a6254 | |||
| 9384264d29 | |||
| 90706f1842 | |||
| cb42552573 | |||
| b9c1729733 | |||
| 83eb97a100 | |||
| 7c7763d105 | |||
| d1ac332ace | |||
| 737cd7a7c3 | |||
| d050ceb3bc | |||
| 504ca76077 | |||
| d462b5d17b | |||
| 004966aa69 | |||
| d97f6c0289 | |||
| 967a5a05fd | |||
| 9d5ac1064c | |||
| 8a5edc5700 | |||
| 933ef9b5d4 | |||
| d6d777fd65 | |||
| 9cbe0fc661 | |||
| 59ba5b7817 | |||
| 3f59178b9e | |||
| 92f0970097 | |||
| 1fd724c3dc | |||
| ab1da39fc2 | |||
| 36c600ebb0 | |||
| 26a9e2de1e | |||
| 5c33683a8c | |||
| 08d476654e | |||
| b597244be4 | |||
| 76aee40c0d | |||
| 38fb3fc89a | |||
| e2572d2810 | |||
| 5e3db16401 | |||
| 311fcc5a88 | |||
| fb389c1d69 | |||
| f1f7955b86 | |||
| 7b21605eeb | |||
| cf6e6f96f5 | |||
| 91f2610a72 | |||
| 50f0d33233 | |||
| ba9cad8daf | |||
| 057bf1a663 | |||
| 753055602f | |||
| 7a5cef47b5 | |||
| f5c6403769 | |||
| e2520ff154 | |||
| 7a3c4a297a | |||
| b946d514a0 | |||
| 5e49202ba6 | |||
| 399d755eac | |||
| e72d7ff088 | |||
| ccb7961171 | |||
| e4fc6b24c0 | |||
| f22af8d002 | |||
| e5e5faa4e8 | |||
| f2cd571d7a | |||
| 4202f5e1bd | |||
| 3040215ce3 | |||
| bd2f2b3954 | |||
| 9d19768e1c | |||
| bcc774d3a8 | |||
| ad9768a584 | |||
| a089cbc21c | |||
| 090baa3b61 | |||
| fc75c85b14 |
@@ -1 +0,0 @@
|
||||
4.0
|
||||
+7
-7
@@ -1,5 +1,5 @@
|
||||
language: objective-c
|
||||
osx_image: xcode9
|
||||
osx_image: xcode10.2
|
||||
xcode_project: $PROJECTNAME.xcodeproj
|
||||
env:
|
||||
global:
|
||||
@@ -13,14 +13,14 @@ env:
|
||||
- MACOS_SDK=macosx
|
||||
- TVOS_SDK=appletvsimulator
|
||||
matrix:
|
||||
- DESTINATION="OS=11.0,name=iPad Pro (10.5-inch)" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" CARTHAGEDEPLOY="YES"
|
||||
- DESTINATION="OS=11.0,name=iPhone 8" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" POD="YES"
|
||||
- DESTINATION="OS=12.2,name=iPad Pro (10.5-inch)" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" CARTHAGEDEPLOY="YES"
|
||||
- DESTINATION="OS=12.2,name=iPhone 8" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" POD="YES"
|
||||
- DESTINATION="arch=x86_64" SCHEME="$MACOS_FRAMEWORK_SCHEME" SDK="$MACOS_SDK"
|
||||
- DESTINATION="arch=x86_64" SCHEME="FilesProviderTests" SDK="$MACOS_SDK" RUN_TESTS="YES"
|
||||
- DESTINATION="OS=11.0,name=Apple TV 1080p" SCHEME="$TVOS_FRAMEWORK_SCHEME" SDK="$TVOS_SDK"
|
||||
- DESTINATION="OS=12.2,name=Apple TV 1080p" SCHEME="$TVOS_FRAMEWORK_SCHEME" SDK="$TVOS_SDK"
|
||||
before_install:
|
||||
- gem install xcpretty --no-rdoc --no-ri --no-document --quiet
|
||||
- gem install cocoapods --no-rdoc --no-ri --no-document --quiet
|
||||
- gem install xcpretty --no-document --quiet
|
||||
- gem install cocoapods --no-document --quiet
|
||||
# - gem install xcpretty-travis-formatter
|
||||
|
||||
script:
|
||||
@@ -76,4 +76,4 @@ deploy:
|
||||
# repo: amosavian/$PROJECTNAME
|
||||
repo: amosavian/FileProvider
|
||||
tags: true
|
||||
condition: "$CARTHAGEDEPLOY = YES"
|
||||
condition: "$CARTHAGEDEPLOY = YES"
|
||||
|
||||
@@ -16,7 +16,7 @@ Pod::Spec.new do |s|
|
||||
#
|
||||
|
||||
s.name = "FilesProvider"
|
||||
s.version = "0.24.0"
|
||||
s.version = "0.26.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.
|
||||
@@ -55,9 +55,8 @@ Pod::Spec.new do |s|
|
||||
# profile URL.
|
||||
#
|
||||
|
||||
s.author = { "Amir Abbas Mousavian" => "a.mosavian@gmail.com" }
|
||||
# Or just: s.author = "Amir Abbas Mousavian"
|
||||
# s.authors = { "Amir Abbas Mousavian" => "a.mosavian@gmail.com" }
|
||||
s.authors = { "Amir Abbas Mousavian" => "a.mosavian@gmail.com" }
|
||||
s.social_media_url = "https://twitter.com/amosavian"
|
||||
|
||||
# ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
|
||||
@@ -66,7 +65,7 @@ Pod::Spec.new do |s|
|
||||
# the deployment target. You can optionally include the target after the platform.
|
||||
#
|
||||
|
||||
s.swift_version = "4.0"
|
||||
s.swift_version = "5.0"
|
||||
s.ios.deployment_target = "8.0"
|
||||
s.osx.deployment_target = "10.10"
|
||||
# s.watchos.deployment_target = "2.0"
|
||||
@@ -117,7 +116,10 @@ Pod::Spec.new do |s|
|
||||
#
|
||||
|
||||
# s.framework = "SomeFramework"
|
||||
# s.frameworks = "SomeFramework", "AnotherFramework"
|
||||
s.frameworks = "AVFoundation", "ImageIO", "CoreGraphics"
|
||||
s.ios.framework = "UIKit"
|
||||
s.tvos.framework = "UIKit"
|
||||
s.osx.framework = "AppKit"
|
||||
|
||||
s.library = "xml2"
|
||||
# s.libraries = "iconv", "xml2"
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
7958155B1F478ED9003344DD /* HTTPFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 795815591F478ED9003344DD /* HTTPFileProvider.swift */; };
|
||||
7958155C1F478ED9003344DD /* HTTPFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 795815591F478ED9003344DD /* HTTPFileProvider.swift */; };
|
||||
798654331E8874BC002FA550 /* FTPHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 798654321E8874BC002FA550 /* FTPHelper.swift */; };
|
||||
798D394020D01DA100FFB9EF /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 798D393F20D01DA000FFB9EF /* CoreGraphics.framework */; };
|
||||
799396AA1D48C02300086753 /* DropboxFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396931D48C02300086753 /* DropboxFileProvider.swift */; };
|
||||
799396AB1D48C02300086753 /* DropboxFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396931D48C02300086753 /* DropboxFileProvider.swift */; };
|
||||
799396AC1D48C02300086753 /* DropboxFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396931D48C02300086753 /* DropboxFileProvider.swift */; };
|
||||
@@ -113,7 +114,6 @@
|
||||
79BD63AC1E2CC2C20035128C /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63AB1E2CC2C20035128C /* ImageIO.framework */; };
|
||||
79BD63B01E2CC3300035128C /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63AF1E2CC3300035128C /* libxml2.tbd */; };
|
||||
79BD63B21E2CC3350035128C /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63B11E2CC3350035128C /* ImageIO.framework */; };
|
||||
79BD63B61E2CC3860035128C /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63B51E2CC3860035128C /* CoreFoundation.framework */; };
|
||||
79BD63B81E2CC38D0035128C /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63B71E2CC38D0035128C /* AVFoundation.framework */; };
|
||||
79BD63BA1E2CC39B0035128C /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63B91E2CC39B0035128C /* libxml2.tbd */; };
|
||||
79BD63BE1E2CC3C20035128C /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63BD1E2CC3C20035128C /* ImageIO.framework */; };
|
||||
@@ -132,6 +132,9 @@
|
||||
79F5745B1DFDB10B00179ABF /* FileObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79F5745A1DFDB10A00179ABF /* FileObject.swift */; };
|
||||
79F5745C1DFDB10B00179ABF /* FileObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79F5745A1DFDB10A00179ABF /* FileObject.swift */; };
|
||||
79F5745D1DFDB10B00179ABF /* FileObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79F5745A1DFDB10A00179ABF /* FileObject.swift */; };
|
||||
CE27C49B219BFFA0000BE23C /* ServerTrustPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE27C498219BFFA0000BE23C /* ServerTrustPolicy.swift */; };
|
||||
CE27C49C219C041E000BE23C /* ServerTrustPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE27C498219BFFA0000BE23C /* ServerTrustPolicy.swift */; };
|
||||
CE27C49D219C041E000BE23C /* ServerTrustPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE27C498219BFFA0000BE23C /* ServerTrustPolicy.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -163,6 +166,7 @@
|
||||
794C220D1D591A4B00EC49B8 /* SMB2QueryTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMB2QueryTypes.swift; sourceTree = "<group>"; };
|
||||
795815591F478ED9003344DD /* HTTPFileProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPFileProvider.swift; sourceTree = "<group>"; };
|
||||
798654321E8874BC002FA550 /* FTPHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTPHelper.swift; sourceTree = "<group>"; };
|
||||
798D393F20D01DA000FFB9EF /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; };
|
||||
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; };
|
||||
@@ -205,6 +209,7 @@
|
||||
79D903531FAB647400D61D31 /* FilesProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesProviderTests.swift; sourceTree = "<group>"; };
|
||||
79D903551FAB647400D61D31 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
79F5745A1DFDB10A00179ABF /* FileObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileObject.swift; sourceTree = "<group>"; };
|
||||
CE27C498219BFFA0000BE23C /* ServerTrustPolicy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerTrustPolicy.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -213,8 +218,8 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
79BD63AA1E2CC2BB0035128C /* AVFoundation.framework in Frameworks */,
|
||||
79BD63AC1E2CC2C20035128C /* ImageIO.framework in Frameworks */,
|
||||
79BD63A81E2CC2940035128C /* CoreGraphics.framework in Frameworks */,
|
||||
79BD63AC1E2CC2C20035128C /* ImageIO.framework in Frameworks */,
|
||||
791950F51DE58A5400B4426E /* libxml2.tbd in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -224,7 +229,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
79BD63B81E2CC38D0035128C /* AVFoundation.framework in Frameworks */,
|
||||
79BD63B61E2CC3860035128C /* CoreFoundation.framework in Frameworks */,
|
||||
798D394020D01DA100FFB9EF /* CoreGraphics.framework in Frameworks */,
|
||||
79BD63B21E2CC3350035128C /* ImageIO.framework in Frameworks */,
|
||||
79BD63B01E2CC3300035128C /* libxml2.tbd in Frameworks */,
|
||||
);
|
||||
@@ -255,6 +260,7 @@
|
||||
791950F31DE58A5300B4426E /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
798D393F20D01DA000FFB9EF /* CoreGraphics.framework */,
|
||||
79BD63C11E2CC3D30035128C /* AVFoundation.framework */,
|
||||
79BD63BF1E2CC3CD0035128C /* CoreGraphics.framework */,
|
||||
79BD63BD1E2CC3C20035128C /* ImageIO.framework */,
|
||||
@@ -350,6 +356,7 @@
|
||||
798654321E8874BC002FA550 /* FTPHelper.swift */,
|
||||
799396971D48C02300086753 /* SMBClient.swift */,
|
||||
799396981D48C02300086753 /* SMBFileProvider.swift */,
|
||||
CE27C498219BFFA0000BE23C /* ServerTrustPolicy.swift */,
|
||||
);
|
||||
path = Sources;
|
||||
sourceTree = "<group>";
|
||||
@@ -496,7 +503,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0910;
|
||||
LastUpgradeCheck = 0900;
|
||||
LastUpgradeCheck = 0930;
|
||||
TargetAttributes = {
|
||||
799396661D48B7F600086753 = {
|
||||
CreatedOnToolsVersion = 7.3.1;
|
||||
@@ -504,22 +511,25 @@
|
||||
};
|
||||
799396741D48B80D00086753 = {
|
||||
CreatedOnToolsVersion = 7.3.1;
|
||||
LastSwiftMigration = 1020;
|
||||
};
|
||||
799396811D48B82700086753 = {
|
||||
CreatedOnToolsVersion = 7.3.1;
|
||||
};
|
||||
79D903501FAB647400D61D31 = {
|
||||
CreatedOnToolsVersion = 9.1;
|
||||
LastSwiftMigration = 1020;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 7993965F1D48B7BF00086753 /* Build configuration list for PBXProject "FilesProvider" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = English;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 7993965B1D48B7BF00086753;
|
||||
productRefGroup = 799396681D48B7F600086753 /* Products */;
|
||||
@@ -573,6 +583,7 @@
|
||||
799396B31D48C02300086753 /* LocalFileProvider.swift in Sources */,
|
||||
799396D41D48C02300086753 /* SMB2Tree.swift in Sources */,
|
||||
7924B1A21D89DAE000589DB7 /* Options.swift in Sources */,
|
||||
CE27C49D219C041E000BE23C /* ServerTrustPolicy.swift in Sources */,
|
||||
792572411DF23BDA006A1526 /* LocalHelper.swift in Sources */,
|
||||
7936BC121E880F5700A6C81C /* FTPFileProvider.swift in Sources */,
|
||||
79480FF61E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */,
|
||||
@@ -617,6 +628,7 @@
|
||||
799396B41D48C02300086753 /* LocalFileProvider.swift in Sources */,
|
||||
799396D51D48C02300086753 /* SMB2Tree.swift in Sources */,
|
||||
7924B1A31D89DAE000589DB7 /* Options.swift in Sources */,
|
||||
CE27C49C219C041E000BE23C /* ServerTrustPolicy.swift in Sources */,
|
||||
792572421DF23BDA006A1526 /* LocalHelper.swift in Sources */,
|
||||
7936BC131E880F5700A6C81C /* FTPFileProvider.swift in Sources */,
|
||||
79480FF71E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */,
|
||||
@@ -661,6 +673,7 @@
|
||||
799396B51D48C02300086753 /* LocalFileProvider.swift in Sources */,
|
||||
799396D61D48C02300086753 /* SMB2Tree.swift in Sources */,
|
||||
7924B1A41D89DAE000589DB7 /* Options.swift in Sources */,
|
||||
CE27C49B219BFFA0000BE23C /* ServerTrustPolicy.swift in Sources */,
|
||||
792572431DF23BDA006A1526 /* LocalHelper.swift in Sources */,
|
||||
7936BC141E880F5700A6C81C /* FTPFileProvider.swift in Sources */,
|
||||
79480FF81E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */,
|
||||
@@ -720,16 +733,18 @@
|
||||
799396601D48B7BF00086753 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_VERSION_STRING = 0.24.0;
|
||||
BUNDLE_VERSION_STRING = 0.25.1;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
@@ -738,7 +753,6 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
@@ -759,16 +773,18 @@
|
||||
799396611D48B7BF00086753 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_VERSION_STRING = 0.24.0;
|
||||
BUNDLE_VERSION_STRING = 0.25.1;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
@@ -776,7 +792,6 @@
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
@@ -820,6 +835,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FilesProvider-iOS";
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_VERSION = 4.2;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
@@ -850,6 +866,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FilesProvider-iOS";
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_VERSION = 4.2;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
@@ -873,7 +890,6 @@
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
FRAMEWORK_VERSION = A;
|
||||
@@ -891,6 +907,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FilesProvider-OSX";
|
||||
SDKROOT = macosx;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
};
|
||||
@@ -912,7 +929,6 @@
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
FRAMEWORK_VERSION = A;
|
||||
@@ -925,6 +941,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FilesProvider-OSX";
|
||||
SDKROOT = macosx;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
};
|
||||
@@ -961,6 +978,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FilesProvider-tvOS";
|
||||
SDKROOT = appletvos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_VERSION = 4.2;
|
||||
TARGETED_DEVICE_FAMILY = 3;
|
||||
TVOS_DEPLOYMENT_TARGET = 10.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
@@ -994,6 +1012,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FilesProvider-tvOS";
|
||||
SDKROOT = appletvos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_VERSION = 4.2;
|
||||
TARGETED_DEVICE_FAMILY = 3;
|
||||
TVOS_DEPLOYMENT_TARGET = 10.0;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
@@ -1019,7 +1038,6 @@
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
@@ -1036,6 +1054,7 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -1057,7 +1076,6 @@
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
@@ -1069,6 +1087,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mousavian.FilesProviderTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0920"
|
||||
LastUpgradeVersion = "0930"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -26,10 +26,28 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "79D903501FAB647400D61D31"
|
||||
BuildableName = "FilesProviderTests.xctest"
|
||||
BlueprintName = "FilesProviderTests"
|
||||
ReferencedContainer = "container:FilesProvider.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "799396741D48B80D00086753"
|
||||
BuildableName = "FilesProvider.framework"
|
||||
BlueprintName = "FilesProvider OSX"
|
||||
ReferencedContainer = "container:FilesProvider.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
@@ -37,7 +55,6 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0920"
|
||||
LastUpgradeVersion = "0930"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -26,7 +26,6 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
@@ -37,7 +36,6 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0920"
|
||||
LastUpgradeVersion = "0930"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -26,7 +26,6 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
@@ -37,7 +36,6 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// swift-tools-version:4.0
|
||||
// swift-tools-version:5.0
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
@@ -40,7 +40,7 @@ All functions do async calls and it wont block your main thread.
|
||||
- [ ] **GoogleFileProvider** A wrapper around Goodle Drive REST API.
|
||||
- [ ] **SMBFileProvider** SMB2/3 introduced in 2006, which is a file and printer sharing protocol originated from Microsoft Windows and now is replacing AFP protocol on macOS.
|
||||
* Data types and some basic functions are implemented but *main interface is not implemented yet!*.
|
||||
* SMBv1/CIFS is insecure, deprecated and kinda tricky to be implemented due to strict memory allignment in Swift.
|
||||
* For now, you can use [amosavian/AMSMB2](https://github.com/amosavian/AMSMB2) framework to connect to SMB2.
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -471,7 +471,7 @@ We would love for you to contribute to **FileProvider**, check the `LICENSE` fil
|
||||
Things you may consider to help us:
|
||||
|
||||
- [ ] Implement request/response stack for `SMBClient`
|
||||
- [ ] Implement Test-case (`XCTest`)
|
||||
- [x] Implement Test-case (`XCTest`)
|
||||
- [ ] Add Sample project for iOS
|
||||
- [ ] Add Sample project for macOS
|
||||
|
||||
@@ -512,7 +512,3 @@ Distributed under the MIT license. See `LICENSE` for more information.
|
||||
[cocoapods-apps]: https://img.shields.io/cocoapods/at/FilesProvider.svg
|
||||
[docs-image]: https://img.shields.io/cocoapods/metrics/doc-percent/FilesProvider.svg
|
||||
[docs-url]: http://cocoadocs.org/docsets/FilesProvider/
|
||||
## Support on Beerpay
|
||||
Hey dude! Help me out for a couple of :beers:!
|
||||
|
||||
[](https://beerpay.io/amosavian/FileProvider) [](https://beerpay.io/amosavian/FileProvider?focus=wish)
|
||||
@@ -43,7 +43,7 @@ internal class AEXMLDocument: AEXMLElement {
|
||||
return rootElement
|
||||
}
|
||||
|
||||
open let options: AEXMLOptions
|
||||
public let options: AEXMLOptions
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
|
||||
@@ -192,7 +192,7 @@ internal class AEXMLElement {
|
||||
}
|
||||
|
||||
fileprivate func removeChild(_ child: AEXMLElement) {
|
||||
if let childIndex = children.index(where: { $0 === child }) {
|
||||
if let childIndex = children.firstIndex(where: { $0 === child }) {
|
||||
children.remove(at: childIndex)
|
||||
}
|
||||
}
|
||||
@@ -266,7 +266,7 @@ internal class AEXMLElement {
|
||||
|
||||
}
|
||||
|
||||
public extension String {
|
||||
extension String {
|
||||
|
||||
/// String representation of self with XML special characters escaped.
|
||||
public var xmlEscaped: String {
|
||||
|
||||
+151
-156
@@ -37,7 +37,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
|
||||
open fileprivate(set) var scope: UbiquitousScope
|
||||
|
||||
/// Set this property to ignore initiations asserting to be on secondary thread
|
||||
static open var asserting: Bool = true
|
||||
static public var asserting: Bool = true
|
||||
|
||||
/**
|
||||
Initializes the provider for the iCloud container associated with the specified identifier and
|
||||
@@ -82,22 +82,18 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
|
||||
super.init(baseURL: baseURL)
|
||||
self.isCoorinating = true
|
||||
|
||||
#if swift(>=3.1)
|
||||
let queueLabel = "FileProvider.\(Swift.type(of: self).type)"
|
||||
#else
|
||||
let queueLabel = "FileProvider.\(type(of: self).type)"
|
||||
#endif
|
||||
let queueLabel = "FileProvider.\(Swift.type(of: self).type)"
|
||||
dispatch_queue = DispatchQueue(label: queueLabel, attributes: .concurrent)
|
||||
operation_queue = OperationQueue()
|
||||
operation_queue.name = "\(queueLabel).Operation"
|
||||
}
|
||||
|
||||
public required convenience init?(coder aDecoder: NSCoder) {
|
||||
if let containerId = aDecoder.decodeObject(forKey: "containerId") as? String,
|
||||
let scopeString = aDecoder.decodeObject(forKey: "scope") as? String,
|
||||
if let containerId = aDecoder.decodeObject(of: NSString.self, forKey: "containerId") as String?,
|
||||
let scopeString = aDecoder.decodeObject(of: NSString.self, forKey: "scope") as String?,
|
||||
let scope = UbiquitousScope(rawValue: scopeString) {
|
||||
self.init(containerId: containerId, scope: scope)
|
||||
} else if let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL {
|
||||
} else if let baseURL = aDecoder.decodeObject(of: NSURL.self, forKey: "baseURL") as URL? {
|
||||
self.init(baseURL: baseURL)
|
||||
} else {
|
||||
return nil
|
||||
@@ -275,7 +271,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
if !mdquery.start() {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler([], self.cocoaError(path, code: .fileReadNoPermission))
|
||||
completionHandler([], CocoaError(.fileReadNoPermission, path: path))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -342,7 +338,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
|
||||
try self.opFileManager.copyItem(at: localFile, to: tmpFile)
|
||||
let toUrl = self.url(of: toPath)
|
||||
try self.opFileManager.setUbiquitous(true, itemAt: tmpFile, destinationURL: toUrl)
|
||||
self.monitorFile(path: toPath, operation: operation, progress: progress)
|
||||
self.monitorTransmissionProgress(path: toPath, operation: operation, progress: progress)
|
||||
completionHandler?(nil)
|
||||
self.delegateNotify(operation)
|
||||
} catch {
|
||||
@@ -362,7 +358,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
|
||||
self.operation_queue.addOperation(moveblock)
|
||||
})
|
||||
} else {
|
||||
let e = self.cocoaError(dest.path, code: .fileWriteFileExists)
|
||||
let e = CocoaError(.fileWriteFileExists, path: dest.path)
|
||||
dispatch_queue.async {
|
||||
completionHandler?(e)
|
||||
}
|
||||
@@ -390,7 +386,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
|
||||
open override func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let operation = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
|
||||
let progress = super.copyItem(path: path, toLocalURL: toLocalURL, completionHandler: completionHandler)
|
||||
monitorFile(path: path, operation: operation, progress: progress)
|
||||
monitorTransmissionProgress(path: path, operation: operation, progress: progress)
|
||||
do {
|
||||
try self.opFileManager.startDownloadingUbiquitousItem(at: self.url(of: path))
|
||||
} catch {
|
||||
@@ -416,7 +412,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
|
||||
open override func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
|
||||
let operation = FileOperationType.fetch(path: path)
|
||||
let progress = super.contents(path: path, completionHandler: completionHandler)
|
||||
monitorFile(path: path, operation: operation, progress: progress)
|
||||
monitorTransmissionProgress(path: path, operation: operation, progress: progress)
|
||||
return progress
|
||||
}
|
||||
|
||||
@@ -437,7 +433,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
|
||||
open override func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
|
||||
let operation = FileOperationType.fetch(path: path)
|
||||
let progress = super.contents(path: path, offset: offset, length: length, completionHandler: completionHandler)
|
||||
monitorFile(path: path, operation: operation, progress: progress)
|
||||
monitorTransmissionProgress(path: path, operation: operation, progress: progress)
|
||||
return progress
|
||||
}
|
||||
|
||||
@@ -460,7 +456,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
|
||||
progress.kind = .file
|
||||
progress.setUserInfoObject(self.url(of: path), forKey: .fileURLKey)
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
||||
monitorFile(path: path, operation: operation, progress: progress)
|
||||
monitorTransmissionProgress(path: path, operation: operation, progress: progress)
|
||||
_ = super.writeContents(path: path, contents: data, atomically: atomically, overwrite: overwrite, completionHandler: completionHandler)
|
||||
return progress
|
||||
}
|
||||
@@ -527,6 +523,40 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
|
||||
return monitors[path] != nil
|
||||
}
|
||||
|
||||
open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
|
||||
operation_queue.addOperation {
|
||||
do {
|
||||
var expiration: NSDate?
|
||||
let url = try self.opFileManager.url(forPublishingUbiquitousItemAt: self.url(of: path), expiration: &expiration)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(url, nil, expiration as Date?, nil)
|
||||
}
|
||||
} catch {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, nil, nil, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Removes local copy of file, but spares cloud copy.
|
||||
- Parameter path: Path of file or directory to be removed from local.
|
||||
- Parameter completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
*/
|
||||
open func evictItem(path: String, completionHandler: SimpleCompletionHandler) {
|
||||
operation_queue.addOperation {
|
||||
do {
|
||||
try self.opFileManager.evictUbiquitousItem(at: self.url(of: path))
|
||||
completionHandler?(nil)
|
||||
} catch {
|
||||
completionHandler?(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension CloudFileProvider {
|
||||
fileprivate func updateQueryTypeKeys(_ queryComponent: NSPredicate) -> NSPredicate {
|
||||
let mapDict: [String: String] = ["url": NSMetadataItemURLKey, "name": NSMetadataItemFSNameKey, "path": NSMetadataItemPathKey, "filesize": NSMetadataItemFSSizeKey, "modifiedDate": NSMetadataItemFSContentChangeDateKey, "creationDate": NSMetadataItemFSCreationDateKey, "contentType": NSMetadataItemContentTypeKey]
|
||||
|
||||
@@ -536,6 +566,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
|
||||
case .and: return NSCompoundPredicate(andPredicateWithSubpredicates: newSub)
|
||||
case .not: return NSCompoundPredicate(notPredicateWithSubpredicate: newSub.first!)
|
||||
case .or: return NSCompoundPredicate(orPredicateWithSubpredicates: newSub)
|
||||
@unknown default: fatalError()
|
||||
}
|
||||
} else if let cQuery = queryComponent as? NSComparisonPredicate {
|
||||
var newLeft = cQuery.leftExpression
|
||||
@@ -557,7 +588,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
|
||||
return queryComponent
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
fileprivate func mapFileObject(attributes attribs: [String: Any]) -> FileObject? {
|
||||
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardizedFileURL, let name = attribs[NSMetadataItemFSNameKey] as? String else {
|
||||
@@ -565,11 +596,7 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
|
||||
}
|
||||
|
||||
let path = self.relativePathOf(url: url)
|
||||
#if swift(>=4.0)
|
||||
let rpath = path.hasPrefix("/") ? String(path[path.index(after: path.startIndex)...]) : path
|
||||
#else
|
||||
let rpath = path.hasPrefix("/") ? path.substring(from: path.index(after: path.startIndex)) : path
|
||||
#endif
|
||||
let relativeUrl = URL(string: rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? rpath, relativeTo: self.baseURL)
|
||||
let file = FileObject(url: relativeUrl ?? url, name: name, path: path)
|
||||
|
||||
@@ -583,16 +610,84 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
|
||||
return file
|
||||
}
|
||||
|
||||
lazy fileprivate var observer: KVOObserver = KVOObserver()
|
||||
|
||||
fileprivate func monitorFile(path: String, operation: FileOperationType, progress: Progress?) {
|
||||
fileprivate func monitorTransmissionProgress(path: String, operation: FileOperationType, progress: Progress?) {
|
||||
var isDownloadingOperation: Bool
|
||||
let isUploadingOperation: Bool
|
||||
switch operation {
|
||||
case .copy(_, destination: let dest) where dest.hasPrefix("file://"), .move(_, destination: let dest) where dest.hasPrefix("file://"):
|
||||
fallthrough
|
||||
case .fetch:
|
||||
isDownloadingOperation = true
|
||||
isUploadingOperation = false
|
||||
case .copy(source: let source, _) where source.hasPrefix("file://"), .move(source: let source, _) where source.hasPrefix("file://"):
|
||||
fallthrough
|
||||
case .modify, .create:
|
||||
isDownloadingOperation = false
|
||||
isUploadingOperation = true
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
let pathURL = self.url(of: path).standardizedFileURL
|
||||
let query = NSMetadataQuery()
|
||||
query.predicate = NSPredicate(format: "%K LIKE[CD] %@", NSMetadataItemPathKey, pathURL.path)
|
||||
query.valueListAttributes = [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataUbiquitousItemPercentDownloadedKey, NSMetadataUbiquitousItemPercentUploadedKey, NSMetadataUbiquitousItemDownloadingStatusKey, NSMetadataItemFSSizeKey]
|
||||
query.predicate = NSPredicate(format: "(%K LIKE[CD] %@)", NSMetadataItemPathKey, pathURL.path)
|
||||
query.valueListAttributes = [NSMetadataUbiquitousItemPercentDownloadedKey,
|
||||
NSMetadataUbiquitousItemPercentUploadedKey,
|
||||
NSMetadataUbiquitousItemIsUploadedKey,
|
||||
NSMetadataUbiquitousItemDownloadingStatusKey,
|
||||
NSMetadataItemFSSizeKey]
|
||||
query.searchScopes = [self.scope.rawValue]
|
||||
var context = QueryProgressWrapper(provider: self, progress: progress, operation: operation)
|
||||
query.addObserver(self.observer, forKeyPath: "results", options: [.initial, .new, .old], context: &context)
|
||||
|
||||
var observer: NSObjectProtocol?
|
||||
observer = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidUpdate, object: query, queue: .main) { [weak self] (notification) in
|
||||
guard let items = notification.userInfo?[NSMetadataQueryUpdateChangedItemsKey] as? NSArray,
|
||||
let item = items.firstObject as? NSMetadataItem else {
|
||||
return
|
||||
}
|
||||
|
||||
func terminateAndRemoveObserver() {
|
||||
guard observer != nil else { return }
|
||||
query.stop()
|
||||
observer.flatMap(NotificationCenter.default.removeObserver)
|
||||
observer = nil
|
||||
}
|
||||
|
||||
func updateProgress(_ percent: NSNumber) {
|
||||
let fraction = percent.doubleValue / 100
|
||||
self?.delegateNotify(operation, progress: fraction)
|
||||
if let progress = progress {
|
||||
if progress.totalUnitCount < 1, let size = item.value(forAttribute: NSMetadataItemFSSizeKey) as? NSNumber {
|
||||
progress.totalUnitCount = size.int64Value
|
||||
}
|
||||
progress.completedUnitCount = progress.totalUnitCount > 0 ? Int64(Double(progress.totalUnitCount) * fraction) : 0
|
||||
}
|
||||
if percent.doubleValue == 100.0 {
|
||||
terminateAndRemoveObserver()
|
||||
}
|
||||
}
|
||||
|
||||
for attrName in item.attributes {
|
||||
switch attrName {
|
||||
case NSMetadataUbiquitousItemPercentDownloadedKey:
|
||||
guard isDownloadingOperation, let percent = item.value(forAttribute: attrName) as? NSNumber else { break }
|
||||
updateProgress(percent)
|
||||
case NSMetadataUbiquitousItemPercentUploadedKey:
|
||||
guard isUploadingOperation, let percent = item.value(forAttribute: attrName) as? NSNumber else { break }
|
||||
updateProgress(percent)
|
||||
case NSMetadataUbiquitousItemDownloadingStatusKey:
|
||||
if isDownloadingOperation, let value = item.value(forAttribute: attrName) as? String,
|
||||
value == NSMetadataUbiquitousItemDownloadingStatusDownloaded {
|
||||
terminateAndRemoveObserver()
|
||||
}
|
||||
case NSMetadataUbiquitousItemIsUploadedKey:
|
||||
if isUploadingOperation, let value = item.value(forAttribute: attrName) as? NSNumber, value.boolValue {
|
||||
terminateAndRemoveObserver()
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
query.start()
|
||||
@@ -600,53 +695,37 @@ open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
|
||||
}
|
||||
}
|
||||
|
||||
open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
|
||||
operation_queue.addOperation {
|
||||
do {
|
||||
var expiration: NSDate?
|
||||
let url = try self.opFileManager.url(forPublishingUbiquitousItemAt: self.url(of: path), expiration: &expiration)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(url, nil, expiration as Date?, nil)
|
||||
}
|
||||
} catch {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, nil, nil, error)
|
||||
}
|
||||
fileprivate func metadataItem(for url: URL) -> NSMetadataItem? {
|
||||
assert(!Thread.isMainThread, "CloudFileProvider.metadataItem(for:) is not recommended to be executed on Main Thread.")
|
||||
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!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Removes local copy of file, but spares cloud copy.
|
||||
- Parameter path: Path of file or directory to be removed from local
|
||||
- Parameter completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
*/
|
||||
open func evictItem(path: String, completionHandler: SimpleCompletionHandler) {
|
||||
operation_queue.addOperation {
|
||||
do {
|
||||
try self.opFileManager.evictUbiquitousItem(at: self.url(of: path))
|
||||
completionHandler?(nil)
|
||||
} catch {
|
||||
completionHandler?(error)
|
||||
|
||||
if query.resultCount > 0 {
|
||||
item = query.result(at: 0) as? NSMetadataItem
|
||||
}
|
||||
|
||||
query.disableUpdates()
|
||||
|
||||
})
|
||||
|
||||
DispatchQueue.main.async {
|
||||
query.start()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Returns current version of file on this device and all versions of files in user devices.
|
||||
- Parameter path: Path of file or directory.
|
||||
- Parameter completionHandler: Retrieve current version on this device and all versions available. `currentVersion` will be nil if file doesn't exist. If an error parameter was provided, a presentable `Error` will be returned.
|
||||
*/
|
||||
func versionsOfItem(path: String, completionHandler: @escaping ((_ currentVersion: NSFileVersion?, _ versions: [NSFileVersion], _ error: Error?) -> Void)) {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
/// Resolves conflicts by selecting a version.
|
||||
/// - Parameter path: Path of file or directory.
|
||||
/// - Parameter version: Version than will be choose as main version. `nil` value indicates current version on this device will be selected.
|
||||
/// - Parameter completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
func selectVersionOfItem(path: String, version: NSFileVersion? = nil, completionHandler: SimpleCompletionHandler) {
|
||||
NotImplemented()
|
||||
_ = group.wait(timeout: .now() + 30)
|
||||
return item
|
||||
}
|
||||
}
|
||||
|
||||
@@ -691,87 +770,3 @@ public enum UbiquitousScope: RawRepresentable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct QueryProgressWrapper {
|
||||
weak var provider: CloudFileProvider?
|
||||
weak var progress: Progress?
|
||||
let operation: FileOperationType
|
||||
}
|
||||
|
||||
fileprivate class KVOObserver: NSObject {
|
||||
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
guard let query = object as? NSMetadataQuery else {
|
||||
return
|
||||
}
|
||||
guard let wrapper = context?.load(as: QueryProgressWrapper.self) else {
|
||||
query.stop()
|
||||
query.removeObserver(self, forKeyPath: "results")
|
||||
return
|
||||
}
|
||||
let provider = wrapper.provider
|
||||
let progress = wrapper.progress
|
||||
let operation = wrapper.operation
|
||||
|
||||
guard let results = change?[.newKey], let item = (results as? [NSMetadataItem])?.first else {
|
||||
return
|
||||
}
|
||||
|
||||
query.disableUpdates()
|
||||
var size = progress?.totalUnitCount ?? -1
|
||||
if size < 0, let size_d = item.value(forAttribute: NSMetadataItemFSSizeKey) as? Int64 {
|
||||
size = size_d
|
||||
progress?.totalUnitCount = size
|
||||
}
|
||||
let downloadStatus = item.value(forAttribute: NSMetadataUbiquitousItemPercentDownloadedKey) as? String ?? ""
|
||||
let downloaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentDownloadedKey) as? Double ?? 0
|
||||
let uploaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentUploadedKey) as? Double ?? 0
|
||||
if (downloaded == 0 || downloaded == 100) && (uploaded > 0 && uploaded < 100) {
|
||||
progress?.completedUnitCount = Int64(uploaded / 100 * Double(size))
|
||||
provider?.delegateNotify(operation, progress: uploaded / 100)
|
||||
} else if (uploaded == 0 || uploaded == 100) && downloadStatus != NSMetadataUbiquitousItemDownloadingStatusCurrent {
|
||||
progress?.completedUnitCount = Int64(downloaded / 100 * Double(size))
|
||||
provider?.delegateNotify(operation, progress: downloaded / 100)
|
||||
} else if uploaded == 100 || downloadStatus == NSMetadataUbiquitousItemDownloadingStatusCurrent {
|
||||
progress?.completedUnitCount = size
|
||||
query.stop()
|
||||
query.removeObserver(self, forKeyPath: "results")
|
||||
provider?.delegateNotify(operation)
|
||||
}
|
||||
|
||||
query.enableUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
func getMetadataItem(url: URL) -> NSMetadataItem? {
|
||||
let query = NSMetadataQuery()
|
||||
query.predicate = NSPredicate(format: "(%K LIKE %@)", NSMetadataItemPathKey, url.path)
|
||||
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope, NSMetadataQueryUbiquitousDataScope]
|
||||
|
||||
var item: NSMetadataItem?
|
||||
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
var finishObserver: NSObjectProtocol?
|
||||
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
|
||||
defer {
|
||||
query.stop()
|
||||
group.leave()
|
||||
NotificationCenter.default.removeObserver(finishObserver!)
|
||||
}
|
||||
|
||||
if query.resultCount > 0 {
|
||||
item = query.result(at: 0) as? NSMetadataItem
|
||||
}
|
||||
|
||||
query.disableUpdates()
|
||||
|
||||
})
|
||||
|
||||
DispatchQueue.main.async {
|
||||
query.start()
|
||||
}
|
||||
_ = group.wait(timeout: .now() + 30)
|
||||
return item
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -24,9 +24,9 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
override open class var type: String { return "Dropbox" }
|
||||
|
||||
/// Dropbox RPC API URL, which is equal with [https://api.dropboxapi.com/2/](https://api.dropboxapi.com/2/)
|
||||
open let apiURL: URL
|
||||
public let apiURL: URL
|
||||
/// Dropbox contents download/upload API URL, which is equal with [https://content.dropboxapi.com/2/](https://content.dropboxapi.com/2/)
|
||||
open let contentURL: URL
|
||||
public let contentURL: URL
|
||||
|
||||
/**
|
||||
Initializer for Dropbox provider with given client ID and Token.
|
||||
@@ -44,7 +44,7 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
}
|
||||
|
||||
public required convenience init?(coder aDecoder: NSCoder) {
|
||||
self.init(credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential)
|
||||
self.init(credential: aDecoder.decodeObject(of: URLCredential.self, forKey: "credential"))
|
||||
self.useCache = aDecoder.decodeBool(forKey: "useCache")
|
||||
self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache")
|
||||
}
|
||||
@@ -91,7 +91,7 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
request.httpMethod = "POST"
|
||||
request.setValue(authentication: credential, with: .oAuth2)
|
||||
request.setValue(contentType: .json)
|
||||
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString]
|
||||
let requestDictionary: [String: Any] = ["path": correctPath(path)!]
|
||||
request.httpBody = Data(jsonDictionary: requestDictionary)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderHTTPError?
|
||||
@@ -165,14 +165,14 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
let queryIsTruePredicate = query.predicateFormat == "TRUEPREDICATE"
|
||||
return paginated(path, requestHandler: requestHandler,
|
||||
pageHandler: { [weak self] (data, progress) -> (files: [FileObject], error: Error?, newToken: String?) in
|
||||
guard let json = data?.deserializeJSON(), let entries = (json["entries"] ?? json["matches"]) as? [AnyObject] else {
|
||||
let err = self?.urlError(path, code: .badServerResponse)
|
||||
guard let json = data?.deserializeJSON(), let entries = (json["entries"] ?? json["matches"]) as? [Any] else {
|
||||
let err = URLError(.badServerResponse, url: self?.url(of: path))
|
||||
return ([], err, nil)
|
||||
}
|
||||
|
||||
var files = [FileObject]()
|
||||
for entry in entries {
|
||||
if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry), queryIsTruePredicate || query.evaluate(with: file.mapPredicate()) {
|
||||
if let entry = entry as? [String: Any], let file = DropboxFileObject(json: entry), queryIsTruePredicate || query.evaluate(with: file.mapPredicate()) {
|
||||
files.append(file)
|
||||
progress.completedUnitCount += 1
|
||||
foundItemHandler?(file)
|
||||
@@ -193,11 +193,11 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
override func request(for operation: FileOperationType, overwrite: Bool = false, attributes: [URLResourceKey : Any] = [:]) -> URLRequest {
|
||||
|
||||
func uploadRequest(to path: String) -> URLRequest {
|
||||
var requestDictionary = [String: AnyObject]()
|
||||
var requestDictionary = [String: Any]()
|
||||
let url: URL = URL(string: "files/upload", relativeTo: contentURL)!
|
||||
requestDictionary["path"] = correctPath(path) as NSString?
|
||||
requestDictionary["mode"] = (overwrite ? "overwrite" : "add") as NSString
|
||||
//requestDictionary["client_modified"] = (attributes[.contentModificationDateKey] as? Date)?.format(with: .rfc3339) as NSString?
|
||||
requestDictionary["path"] = correctPath(path)
|
||||
requestDictionary["mode"] = (overwrite ? "overwrite" : "add")
|
||||
//requestDictionary["client_modified"] = (attributes[.contentModificationDateKey] as? Date)?.format(with: .rfc3339)
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue(authentication: credential, with: .oAuth2)
|
||||
@@ -211,7 +211,7 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
var request = URLRequest(url: url)
|
||||
request = URLRequest(url: url)
|
||||
request.setValue(authentication: credential, with: .oAuth2)
|
||||
request.setValue(dropboxArgKey: ["path": correctPath(path)! as NSString])
|
||||
request.setValue(dropboxArgKey: ["path": correctPath(path)!])
|
||||
return request
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
let url: String
|
||||
let sourcePath = operation.source
|
||||
let destPath = operation.destination
|
||||
var requestDictionary = [String: AnyObject]()
|
||||
var requestDictionary = [String: Any]()
|
||||
switch operation {
|
||||
case .create:
|
||||
url = "files/create_folder_v2"
|
||||
@@ -254,11 +254,11 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
request.httpMethod = "POST"
|
||||
request.setValue(authentication: credential, with: .oAuth2)
|
||||
request.setValue(contentType: .json)
|
||||
if let dest = correctPath(destPath) as NSString? {
|
||||
requestDictionary["from_path"] = correctPath(sourcePath) as NSString?
|
||||
if let dest = correctPath(destPath) {
|
||||
requestDictionary["from_path"] = correctPath(sourcePath)
|
||||
requestDictionary["to_path"] = dest
|
||||
} else {
|
||||
requestDictionary["path"] = correctPath(sourcePath) as NSString?
|
||||
requestDictionary["path"] = correctPath(sourcePath)
|
||||
}
|
||||
request.httpBody = Data(jsonDictionary: requestDictionary)
|
||||
return request
|
||||
@@ -267,7 +267,7 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
override func serverError(with code: FileProviderHTTPErrorCode, path: String?, data: Data?) -> FileProviderHTTPError {
|
||||
let errorDesc: String?
|
||||
if let response = data?.deserializeJSON() {
|
||||
errorDesc = (response["user_message"] as? String) ?? (response["error"]?["tag"] as? String)
|
||||
errorDesc = (response["user_message"] as? String) ?? ((response["error"] as? [String: Any])?["tag"] as? String)
|
||||
} else {
|
||||
errorDesc = data.flatMap({ String(data: $0, encoding: .utf8) })
|
||||
}
|
||||
@@ -300,7 +300,7 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
request.httpMethod = "POST"
|
||||
request.setValue(authentication: credential, with: .oAuth2)
|
||||
request.setValue(contentType: .json)
|
||||
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString]
|
||||
let requestDictionary: [String: Any] = ["path": correctPath(path)!]
|
||||
request.httpBody = Data(jsonDictionary: requestDictionary)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderHTTPError?
|
||||
@@ -311,7 +311,7 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) }
|
||||
if let json = data?.deserializeJSON() {
|
||||
link = (json["link"] as? String).flatMap(URL.init(string:))
|
||||
fileObject = (json["metadata"] as? [String: AnyObject]).flatMap(DropboxFileObject.init(json:))
|
||||
fileObject = (json["metadata"] as? [String: Any]).flatMap(DropboxFileObject.init(json:))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,7 +334,7 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
*/
|
||||
open func copyItem(remoteURL: URL, to toPath: String, completionHandler: @escaping ((_ jobId: String?, _ attribute: DropboxFileObject?, _ error: Error?) -> Void)) {
|
||||
if remoteURL.isFileURL {
|
||||
completionHandler(nil, nil, self.urlError(remoteURL.path, code: .badURL))
|
||||
completionHandler(nil, nil, URLError(.badURL, url: remoteURL))
|
||||
return
|
||||
}
|
||||
let url = URL(string: "files/save_url", relativeTo: apiURL)!
|
||||
@@ -342,7 +342,7 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
request.httpMethod = "POST"
|
||||
request.setValue(authentication: credential, with: .oAuth2)
|
||||
request.setValue(contentType: .json)
|
||||
let requestDictionary: [String: AnyObject] = ["path": correctPath(toPath)! as NSString, "url" : remoteURL.absoluteString as NSString]
|
||||
let requestDictionary: [String: Any] = ["path": correctPath(toPath)!, "url" : remoteURL.absoluteString]
|
||||
request.httpBody = Data(jsonDictionary: requestDictionary)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderHTTPError?
|
||||
@@ -353,7 +353,7 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
serverError = code.flatMap { self.serverError(with: $0, path: toPath, data: data) }
|
||||
if let json = data?.deserializeJSON() {
|
||||
jobId = json["async_job_id"] as? String
|
||||
fileObject = (json["metadata"] as? [String: AnyObject]).flatMap(DropboxFileObject.init(json:))
|
||||
fileObject = (json["metadata"] as? [String: Any]).flatMap(DropboxFileObject.init(json:))
|
||||
}
|
||||
}
|
||||
completionHandler(jobId, fileObject, serverError ?? error)
|
||||
@@ -375,7 +375,7 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
request.httpMethod = "POST"
|
||||
request.setValue(authentication: credential, with: .oAuth2)
|
||||
request.setValue(contentType: .json)
|
||||
let requestDictionary: [String: AnyObject] = ["path": correctPath(toPath)! as NSString, "copy_reference" : reference as NSString]
|
||||
let requestDictionary: [String: Any] = ["path": correctPath(toPath)!, "copy_reference" : reference ]
|
||||
request.httpBody = Data(jsonDictionary: requestDictionary)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderHTTPError?
|
||||
@@ -391,7 +391,7 @@ open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
|
||||
extension DropboxFileProvider: ExtendedFileProvider {
|
||||
open func propertiesOfFileSupported(path: String) -> Bool {
|
||||
let fileExt = (path as NSString).pathExtension.lowercased()
|
||||
let fileExt = path.pathExtension.lowercased()
|
||||
switch fileExt {
|
||||
case "jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff":
|
||||
return true
|
||||
@@ -411,7 +411,7 @@ extension DropboxFileProvider: ExtendedFileProvider {
|
||||
request.httpMethod = "POST"
|
||||
request.setValue(authentication: credential, with: .oAuth2)
|
||||
request.setValue(contentType: .json)
|
||||
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString, "include_media_info": NSNumber(value: true)]
|
||||
let requestDictionary: [String: Any] = ["path": correctPath(path)!, "include_media_info": NSNumber(value: true)]
|
||||
request.httpBody = Data(jsonDictionary: requestDictionary)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderHTTPError?
|
||||
@@ -432,15 +432,15 @@ extension DropboxFileProvider: ExtendedFileProvider {
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS)
|
||||
open func thumbnailOfFileSupported(path: String) -> Bool {
|
||||
switch (path as NSString).pathExtension.lowercased() {
|
||||
switch path.pathExtension.lowercased() {
|
||||
case "jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff":
|
||||
return true
|
||||
case "doc", "docx", "docm", "xls", "xlsx", "xlsm":
|
||||
return true
|
||||
return false
|
||||
case "ppt", "pps", "ppsx", "ppsm", "pptx", "pptm":
|
||||
return true
|
||||
return false
|
||||
case "rtf":
|
||||
return true
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@@ -451,7 +451,7 @@ extension DropboxFileProvider: ExtendedFileProvider {
|
||||
open func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) -> Progress? {
|
||||
let url: URL
|
||||
let thumbAPI: Bool
|
||||
switch (path as NSString).pathExtension.lowercased() {
|
||||
switch path.pathExtension.lowercased() {
|
||||
case "jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff":
|
||||
url = URL(string: "files/get_thumbnail", relativeTo: contentURL)!
|
||||
thumbAPI = true
|
||||
@@ -467,9 +467,9 @@ extension DropboxFileProvider: ExtendedFileProvider {
|
||||
}
|
||||
var request = URLRequest(url: url)
|
||||
request.setValue(authentication: credential, with: .oAuth2)
|
||||
var requestDictionary: [String: AnyObject] = ["path": path as NSString]
|
||||
var requestDictionary: [String: Any] = ["path": path]
|
||||
if thumbAPI {
|
||||
requestDictionary["format"] = "jpeg" as NSString
|
||||
requestDictionary["format"] = "jpeg"
|
||||
let size: String
|
||||
switch dimension?.height ?? 64 {
|
||||
case 0...32: size = "w32h32"
|
||||
@@ -478,23 +478,27 @@ extension DropboxFileProvider: ExtendedFileProvider {
|
||||
case 129...480: size = "w640h480"
|
||||
default: size = "w1024h768"
|
||||
}
|
||||
requestDictionary["size"] = size as NSString
|
||||
requestDictionary["size"] = size
|
||||
}
|
||||
request.setValue(dropboxArgKey: requestDictionary)
|
||||
let task = self.session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var image: ImageClass? = nil
|
||||
if let r = response as? HTTPURLResponse, let result = r.allHeaderFields["Dropbox-API-Result"] as? String, let jsonResult = result.deserializeJSON() {
|
||||
if jsonResult["error"] != nil {
|
||||
completionHandler(nil, self.urlError(path, code: .cannotDecodeRawData))
|
||||
completionHandler(nil, URLError(.cannotDecodeRawData, url: self.url(of: path)))
|
||||
}
|
||||
}
|
||||
if let data = data {
|
||||
if data.isPDF, let pageImage = DropboxFileProvider.convertToImage(pdfData: data) {
|
||||
if data.isPDF, let pageImage = DropboxFileProvider.convertToImage(pdfData: data, maxSize: dimension) {
|
||||
image = pageImage
|
||||
} else if let contentType = (response as? HTTPURLResponse)?.allHeaderFields["Content-Type"] as? String, contentType.contains("text/html") {
|
||||
// TODO: Implement converting html returned type of get_preview to image
|
||||
} else if let fetchedimage = ImageClass(data: data) {
|
||||
image = dimension.map({ DropboxFileProvider.scaleDown(image: fetchedimage, toSize: $0) }) ?? fetchedimage
|
||||
} else {
|
||||
if let dimension = dimension {
|
||||
image = DropboxFileProvider.scaleDown(data: data, toSize: dimension)
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
completionHandler(image, error)
|
||||
|
||||
+14
-14
@@ -22,9 +22,9 @@ public final class DropboxFileObject: FileObject {
|
||||
self.init(json: json)
|
||||
}
|
||||
|
||||
internal init? (json: [String: AnyObject]) {
|
||||
internal init? (json: [String: Any]) {
|
||||
var json = json
|
||||
if json["name"] == nil, let metadata = json["metadata"] as? [String: AnyObject] {
|
||||
if json["name"] == nil, let metadata = json["metadata"] as? [String: Any] {
|
||||
json = metadata
|
||||
}
|
||||
guard let name = json["name"] as? String else { return nil }
|
||||
@@ -34,13 +34,13 @@ public final class DropboxFileObject: FileObject {
|
||||
self.serverTime = (json["server_modified"] as? String).flatMap(Date.init(rfcString:))
|
||||
self.modifiedDate = (json["client_modified"] as? String).flatMap(Date.init(rfcString:))
|
||||
self.type = (json[".tag"] as? String) == "folder" ? .directory : .regular
|
||||
self.isReadOnly = (json["sharing_info"]?["read_only"] as? NSNumber)?.boolValue ?? false
|
||||
self.isReadOnly = ((json["sharing_info"] as? [String: Any])?["read_only"] as? NSNumber)?.boolValue ?? false
|
||||
self.id = json["id"] as? String
|
||||
self.rev = json["rev"] as? String
|
||||
}
|
||||
|
||||
/// The time contents of file has been modified on server, returns nil if not set
|
||||
open internal(set) var serverTime: Date? {
|
||||
public internal(set) var serverTime: Date? {
|
||||
get {
|
||||
return allValues[.serverDateKey] as? Date
|
||||
}
|
||||
@@ -51,7 +51,7 @@ public final class DropboxFileObject: FileObject {
|
||||
|
||||
/// The document identifier is a value assigned by the Dropbox to a file.
|
||||
/// This value is used to identify the document regardless of where it is moved on a volume.
|
||||
open internal(set) var id: String? {
|
||||
public internal(set) var id: String? {
|
||||
get {
|
||||
return allValues[.fileResourceIdentifierKey] as? String
|
||||
}
|
||||
@@ -62,7 +62,7 @@ public final class DropboxFileObject: FileObject {
|
||||
|
||||
/// The revision of file, which changes when a file contents are modified.
|
||||
/// Changes to attributes or other file metadata do not change the identifier.
|
||||
open internal(set) var rev: String? {
|
||||
public internal(set) var rev: String? {
|
||||
get {
|
||||
return allValues[.generationIdentifierKey] as? String
|
||||
}
|
||||
@@ -72,8 +72,8 @@ public final class DropboxFileObject: FileObject {
|
||||
}
|
||||
}
|
||||
|
||||
internal extension DropboxFileProvider {
|
||||
internal func correctPath(_ path: String?) -> String? {
|
||||
extension DropboxFileProvider {
|
||||
func correctPath(_ path: String?) -> String? {
|
||||
guard let path = path else { return nil }
|
||||
if path.hasPrefix("id:") || path.hasPrefix("rev:") {
|
||||
return path
|
||||
@@ -85,7 +85,7 @@ internal extension DropboxFileProvider {
|
||||
return p
|
||||
}
|
||||
|
||||
internal func listRequest(path: String, queryStr: String? = nil, recursive: Bool = false) -> ((_ token: String?) -> URLRequest?) {
|
||||
func listRequest(path: String, queryStr: String? = nil, recursive: Bool = false) -> ((_ token: String?) -> URLRequest?) {
|
||||
if let queryStr = queryStr {
|
||||
return { [weak self] (token) -> URLRequest? in
|
||||
guard let `self` = self else { return nil }
|
||||
@@ -94,8 +94,8 @@ internal extension DropboxFileProvider {
|
||||
request.httpMethod = "POST"
|
||||
request.setValue(authentication: self.credential, with: .oAuth2)
|
||||
request.setValue(contentType: .json)
|
||||
var requestDictionary: [String: AnyObject] = ["path": self.correctPath(path) as NSString!]
|
||||
requestDictionary["query"] = queryStr as NSString
|
||||
var requestDictionary: [String: Any] = ["path": self.correctPath(path)!]
|
||||
requestDictionary["query"] = queryStr
|
||||
requestDictionary["start"] = NSNumber(value: (token.flatMap( { Int($0) } ) ?? 0))
|
||||
request.httpBody = Data(jsonDictionary: requestDictionary)
|
||||
return request
|
||||
@@ -103,14 +103,14 @@ internal extension DropboxFileProvider {
|
||||
} else {
|
||||
return { [weak self] (token) -> URLRequest? in
|
||||
guard let `self` = self else { return nil }
|
||||
var requestDictionary = [String: AnyObject]()
|
||||
var requestDictionary = [String: Any]()
|
||||
let url: URL
|
||||
if let token = token {
|
||||
url = URL(string: "files/list_folder/continue", relativeTo: self.apiURL)!
|
||||
requestDictionary["cursor"] = token as NSString?
|
||||
requestDictionary["cursor"] = token
|
||||
} else {
|
||||
url = URL(string: "files/list_folder", relativeTo: self.apiURL)!
|
||||
requestDictionary["path"] = self.correctPath(path) as NSString?
|
||||
requestDictionary["path"] = self.correctPath(path)
|
||||
requestDictionary["recursive"] = NSNumber(value: recursive)
|
||||
}
|
||||
var request = URLRequest(url: url)
|
||||
|
||||
@@ -14,7 +14,7 @@ import AVFoundation
|
||||
|
||||
extension LocalFileProvider: ExtendedFileProvider {
|
||||
open func thumbnailOfFileSupported(path: String) -> Bool {
|
||||
switch (path as NSString).pathExtension.lowercased() {
|
||||
switch path.pathExtension.lowercased() {
|
||||
case LocalFileInformationGenerator.imageThumbnailExtensions.contains:
|
||||
return true
|
||||
case LocalFileInformationGenerator.audioThumbnailExtensions.contains:
|
||||
@@ -33,7 +33,7 @@ extension LocalFileProvider: ExtendedFileProvider {
|
||||
}
|
||||
|
||||
open func propertiesOfFileSupported(path: String) -> Bool {
|
||||
let fileExt = (path as NSString).pathExtension.lowercased()
|
||||
let fileExt = path.pathExtension.lowercased()
|
||||
switch fileExt {
|
||||
case LocalFileInformationGenerator.imagePropertiesExtensions.contains:
|
||||
return LocalFileInformationGenerator.imageProperties != nil
|
||||
@@ -65,26 +65,23 @@ extension LocalFileProvider: ExtendedFileProvider {
|
||||
// Create Thumbnail and cache
|
||||
switch fileURL.pathExtension.lowercased() {
|
||||
case LocalFileInformationGenerator.videoThumbnailExtensions.contains:
|
||||
thumbnailImage = LocalFileInformationGenerator.videoThumbnail(fileURL)
|
||||
thumbnailImage = LocalFileInformationGenerator.videoThumbnail(fileURL, dimension)
|
||||
case LocalFileInformationGenerator.audioThumbnailExtensions.contains:
|
||||
thumbnailImage = LocalFileInformationGenerator.audioThumbnail(fileURL)
|
||||
thumbnailImage = LocalFileInformationGenerator.audioThumbnail(fileURL, dimension)
|
||||
case LocalFileInformationGenerator.imageThumbnailExtensions.contains:
|
||||
thumbnailImage = LocalFileInformationGenerator.imageThumbnail(fileURL)
|
||||
thumbnailImage = LocalFileInformationGenerator.imageThumbnail(fileURL, dimension)
|
||||
case LocalFileInformationGenerator.pdfThumbnailExtensions.contains:
|
||||
thumbnailImage = LocalFileInformationGenerator.pdfThumbnail(fileURL)
|
||||
thumbnailImage = LocalFileInformationGenerator.pdfThumbnail(fileURL, dimension)
|
||||
case LocalFileInformationGenerator.officeThumbnailExtensions.contains:
|
||||
thumbnailImage = LocalFileInformationGenerator.officeThumbnail(fileURL)
|
||||
thumbnailImage = LocalFileInformationGenerator.officeThumbnail(fileURL, dimension)
|
||||
case LocalFileInformationGenerator.customThumbnailExtensions.contains:
|
||||
thumbnailImage = LocalFileInformationGenerator.customThumbnail(fileURL)
|
||||
thumbnailImage = LocalFileInformationGenerator.customThumbnail(fileURL, dimension)
|
||||
default:
|
||||
completionHandler(nil, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if let image = thumbnailImage {
|
||||
let scaledImage = LocalFileProvider.scaleDown(image: image, toSize: dimension)
|
||||
completionHandler(scaledImage, nil)
|
||||
}
|
||||
completionHandler(thumbnailImage, nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -92,7 +89,7 @@ extension LocalFileProvider: ExtendedFileProvider {
|
||||
@discardableResult
|
||||
open func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String: Any], _ keys: [String], _ error: Error?) -> Void)) -> Progress? {
|
||||
(dispatch_queue).async {
|
||||
let fileExt = (path as NSString).pathExtension.lowercased()
|
||||
let fileExt = path.pathExtension.lowercased()
|
||||
var getter: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))?
|
||||
switch fileExt {
|
||||
case LocalFileInformationGenerator.imagePropertiesExtensions.contains:
|
||||
@@ -194,23 +191,19 @@ public struct LocalFileInformationGenerator {
|
||||
static public var customPropertiesExtensions: [String] = []
|
||||
|
||||
/// Thumbnail generator closure for image files.
|
||||
static public var imageThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in
|
||||
return ImageClass(contentsOfFile: fileURL.path)
|
||||
static public var imageThumbnail: (_ fileURL: URL, _ dimension: CGSize?) -> ImageClass? = { fileURL, dimension in
|
||||
return LocalFileProvider.scaleDown(fileURL: fileURL, toSize: dimension)
|
||||
}
|
||||
|
||||
/// Thumbnail generator closure for audio and music files.
|
||||
static public var audioThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in
|
||||
static public var audioThumbnail: (_ fileURL: URL, _ dimension: CGSize?) -> ImageClass? = { fileURL, dimension in
|
||||
let playerItem = AVPlayerItem(url: fileURL)
|
||||
let metadataList = playerItem.asset.commonMetadata
|
||||
#if swift(>=4.0)
|
||||
let commonKeyArtwork = AVMetadataKey.commonKeyArtwork
|
||||
#else
|
||||
let commonKeyArtwork = AVMetadataCommonKeyArtwork
|
||||
#endif
|
||||
for item in metadataList {
|
||||
if item.commonKey == commonKeyArtwork {
|
||||
if let data = item.dataValue {
|
||||
return ImageClass(data: data)
|
||||
return LocalFileProvider.scaleDown(data: data, toSize: dimension)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,9 +211,10 @@ public struct LocalFileInformationGenerator {
|
||||
}
|
||||
|
||||
/// Thumbnail generator closure for video files.
|
||||
static public var videoThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in
|
||||
static public var videoThumbnail: (_ fileURL: URL, _ dimension: CGSize?) -> ImageClass? = { fileURL, dimension in
|
||||
let asset = AVAsset(url: fileURL)
|
||||
let assetImgGenerate = AVAssetImageGenerator(asset: asset)
|
||||
assetImgGenerate.maximumSize = dimension ?? .zero
|
||||
assetImgGenerate.appliesPreferredTrackTransform = true
|
||||
let time = CMTime(value: asset.duration.value / 3, timescale: asset.duration.timescale)
|
||||
if let cgImage = try? assetImgGenerate.copyCGImage(at: time, actualTime: nil) {
|
||||
@@ -234,19 +228,19 @@ public struct LocalFileInformationGenerator {
|
||||
}
|
||||
|
||||
/// Thumbnail generator closure for portable document files files.
|
||||
static public var pdfThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in
|
||||
return LocalFileProvider.convertToImage(pdfURL: fileURL)
|
||||
static public var pdfThumbnail: (_ fileURL: URL, _ dimension: CGSize?) -> ImageClass? = { fileURL, dimension in
|
||||
return LocalFileProvider.convertToImage(pdfURL: fileURL, maxSize: dimension)
|
||||
}
|
||||
|
||||
/// Thumbnail generator closure for office document files.
|
||||
/// - Note: No default implementation is avaiable
|
||||
static public var officeThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in
|
||||
static public var officeThumbnail: (_ fileURL: URL, _ dimension: CGSize?) -> ImageClass? = { fileURL, dimension in
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Thumbnail generator closure for custom type of files.
|
||||
/// - Note: No default implementation is avaiable
|
||||
static public var customThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in
|
||||
static public var customThumbnail: (_ fileURL: URL, _ dimension: CGSize?) -> ImageClass? = { fileURL, dimension in
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -276,32 +270,36 @@ public struct LocalFileInformationGenerator {
|
||||
return(Int(newTopVal), Int(newBottomVal))
|
||||
}
|
||||
|
||||
guard let cgDataRef = CGImageSourceCreateWithURL(fileURL as CFURL, nil), let cfImageDict = CGImageSourceCopyPropertiesAtIndex(cgDataRef, 0, nil) else {
|
||||
guard let source = CGImageSourceCreateWithURL(fileURL as CFURL, nil), let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as NSDictionary? else {
|
||||
return (dic, keys)
|
||||
}
|
||||
let imageDict = cfImageDict as NSDictionary
|
||||
let tiffDict = imageDict[kCGImagePropertyTIFFDictionary as String] as? NSDictionary ?? [:]
|
||||
let exifDict = imageDict[kCGImagePropertyExifDictionary as String] as? NSDictionary ?? [:]
|
||||
if let pixelWidth = imageDict.object(forKey: kCGImagePropertyPixelWidth) as? NSNumber, let pixelHeight = imageDict.object(forKey: kCGImagePropertyPixelHeight) as? NSNumber {
|
||||
let tiffDict = properties[kCGImagePropertyTIFFDictionary as String] as? NSDictionary ?? [:]
|
||||
let exifDict = properties[kCGImagePropertyExifDictionary as String] as? NSDictionary ?? [:]
|
||||
let gpsDict = properties[kCGImagePropertyGPSDictionary as String] as? NSDictionary ?? [:]
|
||||
|
||||
if let pixelWidth = properties.object(forKey: kCGImagePropertyPixelWidth) as? NSNumber, let pixelHeight = properties.object(forKey: kCGImagePropertyPixelHeight) as? NSNumber {
|
||||
add(key: "Dimensions", value: "\(pixelWidth)x\(pixelHeight)")
|
||||
}
|
||||
|
||||
add(key: "DPI", value: imageDict[kCGImagePropertyDPIWidth as String])
|
||||
add(key: "Device make", value: tiffDict[kCGImagePropertyTIFFMake as String])
|
||||
add(key: "DPI", value: properties[kCGImagePropertyDPIWidth as String])
|
||||
add(key: "Device maker", value: tiffDict[kCGImagePropertyTIFFMake as String])
|
||||
add(key: "Device model", value: tiffDict[kCGImagePropertyTIFFModel as String])
|
||||
add(key: "Lens model", value: exifDict[kCGImagePropertyExifLensModel as String])
|
||||
add(key: "Artist", value: tiffDict[kCGImagePropertyTIFFArtist as String] as? String)
|
||||
add(key: "Copyright", value: tiffDict[kCGImagePropertyTIFFCopyright as String] as? String)
|
||||
add(key: "Date taken", value: tiffDict[kCGImagePropertyTIFFDateTime as String] as? String)
|
||||
|
||||
if let latitude = tiffDict[kCGImagePropertyGPSLatitude as String] as? NSNumber, let longitude = tiffDict[kCGImagePropertyGPSLongitude as String] as? NSNumber {
|
||||
add(key: "Location", value: "\(latitude), \(longitude)")
|
||||
if let latitude = gpsDict[kCGImagePropertyGPSLatitude as String] as? NSNumber,
|
||||
let longitude = gpsDict[kCGImagePropertyGPSLongitude as String] as? NSNumber {
|
||||
let altitudeDesc = (gpsDict[kCGImagePropertyGPSAltitude as String] as? NSNumber).map({ " at \($0.format(precision: 0))m" }) ?? ""
|
||||
add(key: "Location", value: "\(latitude.format()), \(longitude.format())\(altitudeDesc)")
|
||||
}
|
||||
add(key: "Altitude", value: tiffDict[kCGImagePropertyGPSAltitude as String] as? NSNumber)
|
||||
add(key: "Area", value: tiffDict[kCGImagePropertyGPSAreaInformation as String])
|
||||
add(key: "Area", value: gpsDict[kCGImagePropertyGPSAreaInformation as String])
|
||||
|
||||
add(key: "Color space", value: imageDict[kCGImagePropertyColorModel as String])
|
||||
add(key: "Color space", value: properties[kCGImagePropertyColorModel as String])
|
||||
add(key: "Color depth", value: (properties[kCGImagePropertyDepth as String] as? NSNumber).map({ "\($0) bits" }))
|
||||
add(key: "Color profile", value: properties[kCGImagePropertyProfileName as String])
|
||||
add(key: "Focal length", value: exifDict[kCGImagePropertyExifFocalLength as String])
|
||||
add(key: "White banance", value: exifDict[kCGImagePropertyExifWhiteBalance as String])
|
||||
add(key: "F number", value: exifDict[kCGImagePropertyExifFNumber as String])
|
||||
add(key: "Exposure program", value: exifDict[kCGImagePropertyExifExposureProgram as String])
|
||||
|
||||
@@ -325,7 +323,7 @@ public struct LocalFileInformationGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
func makeDescription(_ key: String?) -> String? {
|
||||
func makeKeyDescription(_ key: String?) -> String? {
|
||||
guard let key = key else {
|
||||
return nil
|
||||
}
|
||||
@@ -336,21 +334,39 @@ public struct LocalFileInformationGenerator {
|
||||
return newKey.capitalized
|
||||
}
|
||||
|
||||
func parseLocationData(_ value: String) -> (latitude: Double, longitude: Double, height: Double?)? {
|
||||
let scanner = Scanner.init(string: value)
|
||||
var latitude: Double = 0.0, longitude: Double = 0.0, height: Double = 0
|
||||
|
||||
if scanner.scanDouble(&latitude), scanner.scanDouble(&longitude) {
|
||||
scanner.scanDouble(&height)
|
||||
return (latitude, longitude, height)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
guard fileURL.fileExists else {
|
||||
return (dic, keys)
|
||||
}
|
||||
let playerItem = AVPlayerItem(url: fileURL)
|
||||
let metadataList = playerItem.asset.commonMetadata
|
||||
for item in metadataList {
|
||||
#if swift(>=4.0)
|
||||
let commonKey = item.commonKey?.rawValue
|
||||
#else
|
||||
let commonKey = item.commonKey
|
||||
#endif
|
||||
if let description = makeDescription(commonKey) {
|
||||
if let value = item.stringValue {
|
||||
keys.append(description)
|
||||
dic[description] = value
|
||||
let commonKey = item.commonKey?.rawValue
|
||||
if let key = makeKeyDescription(commonKey) {
|
||||
if commonKey == "location", let value = item.stringValue, let loc = parseLocationData(value) {
|
||||
keys.append(key)
|
||||
let heightStr: String = (loc.height as NSNumber?).map({ ", \($0.format(precision: 0))m" }) ?? ""
|
||||
dic[key] = "\((loc.latitude as NSNumber).format())°, \((loc.longitude as NSNumber).format())°\(heightStr)"
|
||||
} else if let value = item.dateValue {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
} else if let value = item.numberValue {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
} else if let value = item.stringValue {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -377,16 +393,12 @@ public struct LocalFileInformationGenerator {
|
||||
dic = audioprops.prop
|
||||
keys = audioprops.keys
|
||||
dic.removeValue(forKey: "Duration")
|
||||
if let index = keys.index(of: "Duration") {
|
||||
if let index = keys.firstIndex(of: "Duration") {
|
||||
keys.remove(at: index)
|
||||
}
|
||||
}
|
||||
let asset = AVURLAsset(url: fileURL, options: nil)
|
||||
#if swift(>=4.0)
|
||||
let videoTracks = asset.tracks(withMediaType: AVMediaType.video)
|
||||
#else
|
||||
let videoTracks = asset.tracks(withMediaType: AVMediaTypeVideo)
|
||||
#endif
|
||||
if let videoTrack = videoTracks.first {
|
||||
var bitrate: Float = 0
|
||||
let width = Int(videoTrack.naturalSize.width)
|
||||
@@ -400,11 +412,7 @@ 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 {
|
||||
@@ -482,7 +490,7 @@ public struct LocalFileInformationGenerator {
|
||||
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: "Security", value: reference.isEncrypted ? (reference.isUnlocked ? "Present" : "Password Protected") : "None")
|
||||
add(key: "Allows printing", value: reference.allowsPrinting)
|
||||
add(key: "Allows copying", value: reference.allowsCopying)
|
||||
return (dic, keys)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Array where Element: FileObject {
|
||||
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)
|
||||
@@ -28,7 +28,7 @@ public extension Sequence where Iterator.Element == UInt8 {
|
||||
}
|
||||
}
|
||||
|
||||
public extension URLFileResourceType {
|
||||
extension URLFileResourceType {
|
||||
/// **FileProvider** returns corresponding `URLFileResourceType` of a `FileAttributeType` value
|
||||
public init(fileTypeValue: FileAttributeType) {
|
||||
switch fileTypeValue {
|
||||
@@ -44,7 +44,33 @@ public extension URLFileResourceType {
|
||||
}
|
||||
}
|
||||
|
||||
public extension URLResourceKey {
|
||||
extension CocoaError {
|
||||
init(_ code: CocoaError.Code, path: String?) {
|
||||
if let path = path {
|
||||
let userInfo: [String: Any] = [NSFilePathErrorKey: path]
|
||||
self.init(code, userInfo: userInfo)
|
||||
} else {
|
||||
self.init(code)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
extension URLError {
|
||||
init(_ code: URLError.Code, url: URL?) {
|
||||
if let url = url {
|
||||
let userInfo: [String: Any] = [NSURLErrorKey: url,
|
||||
NSURLErrorFailingURLErrorKey: url,
|
||||
NSURLErrorFailingURLStringErrorKey: url.absoluteString,
|
||||
]
|
||||
self.init(code, userInfo: userInfo)
|
||||
} else {
|
||||
self.init(code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension URLResourceKey {
|
||||
/// **FileProvider** returns url of file object.
|
||||
public static let fileURLKey = URLResourceKey(rawValue: "NSURLFileURLKey")
|
||||
/// **FileProvider** returns modification date of file in server
|
||||
@@ -59,7 +85,7 @@ public extension URLResourceKey {
|
||||
public static let childrensCount = URLResourceKey(rawValue: "MFPURLChildrensCount")
|
||||
}
|
||||
|
||||
public extension ProgressUserInfoKey {
|
||||
extension ProgressUserInfoKey {
|
||||
/// **FileProvider** returns associated `FileProviderOperationType`
|
||||
public static let fileProvderOperationTypeKey = ProgressUserInfoKey("MFPOperationTypeKey")
|
||||
/// **FileProvider** returns start date/time of operation
|
||||
@@ -76,7 +102,7 @@ internal extension URL {
|
||||
}
|
||||
|
||||
var fileSize: Int64 {
|
||||
return Int64((try? self.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? -1)
|
||||
return (try? self.resourceValues(forKeys: [.fileSizeKey]))?.allValues[.fileSizeKey] as? Int64 ?? -1
|
||||
}
|
||||
|
||||
var fileExists: Bool {
|
||||
@@ -91,7 +117,7 @@ internal extension URL {
|
||||
#endif
|
||||
}
|
||||
|
||||
public extension URLRequest {
|
||||
extension URLRequest {
|
||||
/// Defines HTTP Authentication method required to access
|
||||
public enum AuthenticationType {
|
||||
/// Basic method for authentication
|
||||
@@ -358,7 +384,7 @@ internal extension URLRequest {
|
||||
self.setValue(contentType.rawValue + parameter, forHTTPHeaderField: "Content-Type")
|
||||
}
|
||||
|
||||
mutating func setValue(dropboxArgKey requestDictionary: [String: AnyObject]) {
|
||||
mutating func setValue(dropboxArgKey requestDictionary: [String: Any]) {
|
||||
guard let jsonData = try? JSONSerialization.data(withJSONObject: requestDictionary, options: []) else {
|
||||
return
|
||||
}
|
||||
@@ -373,12 +399,12 @@ internal extension CharacterSet {
|
||||
static let filePathAllowed = CharacterSet.urlPathAllowed.subtracting(CharacterSet(charactersIn: ":"))
|
||||
}
|
||||
|
||||
internal extension Data {
|
||||
internal var isPDF: Bool {
|
||||
extension Data {
|
||||
var isPDF: Bool {
|
||||
return self.count > 4 && self.scanString(length: 4, using: .ascii) == "%PDF"
|
||||
}
|
||||
|
||||
init? (jsonDictionary dictionary: [String: AnyObject]) {
|
||||
init? (jsonDictionary dictionary: [String: Any]) {
|
||||
guard JSONSerialization.isValidJSONObject(dictionary) else { return nil }
|
||||
guard let data = try? JSONSerialization.data(withJSONObject: dictionary, options: []) else {
|
||||
return nil
|
||||
@@ -386,8 +412,8 @@ internal extension Data {
|
||||
self = data
|
||||
}
|
||||
|
||||
func deserializeJSON() -> [String: AnyObject]? {
|
||||
return (try? JSONSerialization.jsonObject(with: self, options: [])) as? [String: AnyObject]
|
||||
func deserializeJSON() -> [String: Any]? {
|
||||
return (try? JSONSerialization.jsonObject(with: self, options: [])) as? [String: Any]
|
||||
}
|
||||
|
||||
init<T>(value: T) {
|
||||
@@ -397,36 +423,39 @@ internal extension Data {
|
||||
|
||||
func scanValue<T>() -> T? {
|
||||
guard MemoryLayout<T>.size <= self.count else { return nil }
|
||||
#if swift(>=5.0)
|
||||
return self.withUnsafeBytes { $0.load(as: T.self) }
|
||||
#else
|
||||
return self.withUnsafeBytes { $0.pointee }
|
||||
#endif
|
||||
}
|
||||
|
||||
func scanValue<T>(start: Int) -> T? {
|
||||
let length = MemoryLayout<T>.size
|
||||
guard self.count >= start + length else { return nil }
|
||||
return self.subdata(in: start..<start+length).withUnsafeBytes { $0.pointee }
|
||||
let subdata = self.subdata(in: start..<start+length)
|
||||
#if swift(>=5.0)
|
||||
return subdata.withUnsafeBytes { $0.load(as: T.self) }
|
||||
#else
|
||||
return subdata.withUnsafeBytes { $0.pointee }
|
||||
#endif
|
||||
}
|
||||
|
||||
func scanString(start: Int = 0, length: Int, using encoding: String.Encoding = .utf8) -> String? {
|
||||
guard self.count >= start + length else { return nil }
|
||||
return String(data: self.subdata(in: start..<start+length), encoding: encoding)
|
||||
}
|
||||
|
||||
static func mapMemory<T, U>(from: T) -> U? {
|
||||
guard MemoryLayout<T>.size >= MemoryLayout<U>.size else { return nil }
|
||||
let data = Data(value: from)
|
||||
return data.scanValue()
|
||||
}
|
||||
}
|
||||
|
||||
internal extension String {
|
||||
init? (jsonDictionary: [String: AnyObject]) {
|
||||
init? (jsonDictionary: [String: Any]) {
|
||||
guard let data = Data(jsonDictionary: jsonDictionary) else {
|
||||
return nil
|
||||
}
|
||||
self.init(data: data, encoding: .utf8)
|
||||
}
|
||||
|
||||
func deserializeJSON(using encoding: String.Encoding = .utf8) -> [String: AnyObject]? {
|
||||
func deserializeJSON(using encoding: String.Encoding = .utf8) -> [String: Any]? {
|
||||
guard let data = self.data(using: encoding) else {
|
||||
return nil
|
||||
}
|
||||
@@ -447,17 +476,35 @@ internal extension String {
|
||||
}
|
||||
}
|
||||
|
||||
#if swift(>=4.0)
|
||||
#else
|
||||
extension String {
|
||||
var count: Int {
|
||||
return self.characters.count
|
||||
extension NSNumber {
|
||||
internal func format(precision: Int = 2, style: NumberFormatter.Style = .decimal) -> String {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.maximumFractionDigits = precision
|
||||
formatter.numberStyle = style
|
||||
return formatter.string(from: self)!
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
internal extension TimeInterval {
|
||||
internal var formatshort: String {
|
||||
extension String {
|
||||
internal var pathExtension: String {
|
||||
return (self as NSString).pathExtension
|
||||
}
|
||||
|
||||
internal func appendingPathComponent(_ pathComponent: String) -> String {
|
||||
return (self as NSString).appendingPathComponent(pathComponent)
|
||||
}
|
||||
|
||||
internal var lastPathComponent: String {
|
||||
return (self as NSString).lastPathComponent
|
||||
}
|
||||
|
||||
internal var deletingLastPathComponent: String {
|
||||
return (self as NSString).deletingLastPathComponent
|
||||
}
|
||||
}
|
||||
|
||||
extension TimeInterval {
|
||||
var formatshort: String {
|
||||
var result = "0:00"
|
||||
if self < TimeInterval(Int32.max) {
|
||||
result = ""
|
||||
@@ -482,7 +529,7 @@ internal extension TimeInterval {
|
||||
}
|
||||
}
|
||||
|
||||
public extension Date {
|
||||
extension Date {
|
||||
/// Date formats used commonly in internet messaging defined by various RFCs.
|
||||
public enum RFCStandards: String {
|
||||
/// Obsolete (2-digit year) date format defined by RFC 822 for http.
|
||||
@@ -550,6 +597,51 @@ public extension Date {
|
||||
}
|
||||
}
|
||||
|
||||
extension InputStream {
|
||||
func readData(ofLength length: Int) throws -> Data {
|
||||
var data = Data(count: length)
|
||||
#if swift(>=5.0)
|
||||
let result = data.withUnsafeMutableBytes { (buf) -> Int in
|
||||
let p = buf.bindMemory(to: UInt8.self).baseAddress!
|
||||
return self.read(p, maxLength: buf.count)
|
||||
}
|
||||
#else
|
||||
let bufcount = data.count
|
||||
let result = data.withUnsafeMutableBytes { (p) -> Int in
|
||||
return self.read(p, maxLength: bufcount)
|
||||
}
|
||||
#endif
|
||||
if result < 0 {
|
||||
throw self.streamError ?? POSIXError(.EIO)
|
||||
} else {
|
||||
data.count = result
|
||||
return data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension OutputStream {
|
||||
func write(data: Data) throws -> Int {
|
||||
#if swift(>=5.0)
|
||||
let result = data.withUnsafeBytes { (buf) -> Int in
|
||||
let p = buf.bindMemory(to: UInt8.self).baseAddress!
|
||||
return self.write(p, maxLength: buf.count)
|
||||
}
|
||||
#else
|
||||
let bufcount = data.count
|
||||
let result = data.withUnsafeBytes { (p) -> Int in
|
||||
return self.write(p, maxLength: bufcount)
|
||||
}
|
||||
#endif
|
||||
if result < 0 {
|
||||
throw self.streamError ?? POSIXError(.EIO)
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 }
|
||||
@@ -592,3 +684,31 @@ func hasSuffix(_ suffix: String) -> (_ value: String) -> Bool {
|
||||
value.hasSuffix(suffix)
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy Swift versions support
|
||||
|
||||
#if swift(>=4.0)
|
||||
#else
|
||||
extension String {
|
||||
var count: Int {
|
||||
return self.characters.count
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if swift(>=4.1)
|
||||
#else
|
||||
extension Array {
|
||||
func compactMap<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult] {
|
||||
return try self.flatMap(transform)
|
||||
}
|
||||
}
|
||||
|
||||
extension ArraySlice {
|
||||
func compactMap<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult] {
|
||||
return try self.flatMap(transform)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
@@ -323,11 +323,19 @@ final class HMAC<Variant: SHA2Variant> {
|
||||
}
|
||||
|
||||
static func authenticate(message: Data, withKey key: Data) -> Data {
|
||||
#if swift(>=5.0)
|
||||
return Data(authenticate(message: Array(message), withKey: Array(key)))
|
||||
#else
|
||||
return Data(bytes: authenticate(message: Array(message), withKey: Array(key)))
|
||||
#endif
|
||||
}
|
||||
|
||||
static func authenticate(message: String, withKey key: Data) -> Data {
|
||||
#if swift(>=5.0)
|
||||
return Data(authenticate(message: [UInt8](message.utf8), withKey: Array(key)))
|
||||
#else
|
||||
return Data(bytes: authenticate(message: [UInt8](message.utf8), withKey: Array(key)))
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,26 +405,18 @@ fileprivate func toUInt64Array(_ slice: ArraySlice<UInt8>) -> Array<UInt64> {
|
||||
return result
|
||||
}
|
||||
|
||||
fileprivate func arrayOfBytes<T>(_ value:T, length:Int? = nil) -> [UInt8] {
|
||||
let totalBytes = length ?? MemoryLayout<T>.size
|
||||
|
||||
let valuePointer = UnsafeMutablePointer<T>.allocate(capacity: 1)
|
||||
|
||||
valuePointer.pointee = value
|
||||
|
||||
let bytesPointer = UnsafeMutableRawPointer(valuePointer).assumingMemoryBound(to: UInt8.self)
|
||||
var bytes = [UInt8](repeating: 0, count: totalBytes)
|
||||
for j in 0..<min(MemoryLayout<T>.size,totalBytes) {
|
||||
bytes[totalBytes - 1 - j] = (bytesPointer + j).pointee
|
||||
fileprivate func arrayOfBytes<T>(_ value: T, length: Int? = nil) -> [UInt8] {
|
||||
var value = value
|
||||
return Swift.withUnsafeBytes(of: &value) { (buffer: UnsafeRawBufferPointer) -> [UInt8] in
|
||||
if let length = length {
|
||||
return Array(buffer.prefix(length))
|
||||
} else {
|
||||
return Array(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
valuePointer.deinitialize()
|
||||
valuePointer.deallocate(capacity: 1)
|
||||
|
||||
return bytes
|
||||
}
|
||||
|
||||
public extension String {
|
||||
extension String {
|
||||
public func fp_sha256() -> [UInt8] {
|
||||
return SHA2<SHA256>.calculate([UInt8](self.utf8))
|
||||
}
|
||||
@@ -430,7 +430,7 @@ public extension String {
|
||||
}
|
||||
}
|
||||
|
||||
public extension Data {
|
||||
extension Data {
|
||||
public func fp_sha256() -> [UInt8] {
|
||||
return SHA2<SHA256>.calculate(Array(self))
|
||||
}
|
||||
|
||||
+689
-462
File diff suppressed because it is too large
Load Diff
+172
-42
@@ -12,7 +12,7 @@ import Foundation
|
||||
Allows accessing to FTP files and directories. This provider doesn't cache or save files internally.
|
||||
It's a complete reimplementation and doesn't use CFNetwork deprecated API.
|
||||
*/
|
||||
open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, FileProviderReadWrite {
|
||||
open class FTPFileProvider: NSObject, FileProviderBasicRemote, FileProviderOperations, FileProviderReadWrite, FileProviderReadWriteProgressive {
|
||||
|
||||
/// FTP data connection mode.
|
||||
public enum Mode: String {
|
||||
@@ -27,7 +27,7 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
|
||||
}
|
||||
|
||||
open class var type: String { return "FTP" }
|
||||
open let baseURL: URL?
|
||||
public let baseURL: URL?
|
||||
|
||||
open var dispatch_queue: DispatchQueue
|
||||
open var operation_queue: OperationQueue {
|
||||
@@ -74,6 +74,10 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS)
|
||||
open var undoManager: UndoManager? = nil
|
||||
#endif
|
||||
|
||||
/**
|
||||
Initializer for FTP provider with given username and password.
|
||||
|
||||
@@ -106,14 +110,12 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
|
||||
self.credential = credential
|
||||
self.supportsRFC3659 = true
|
||||
|
||||
#if swift(>=3.1)
|
||||
let queueLabel = "FileProvider.\(Swift.type(of: self).type)"
|
||||
#else
|
||||
let queueLabel = "FileProvider.\(type(of: self).type)"
|
||||
#endif
|
||||
dispatch_queue = DispatchQueue(label: queueLabel, attributes: .concurrent)
|
||||
operation_queue = OperationQueue()
|
||||
operation_queue.name = "\(queueLabel).Operation"
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,15 +135,21 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
|
||||
}
|
||||
|
||||
public required convenience init?(coder aDecoder: NSCoder) {
|
||||
guard let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL else { return nil }
|
||||
guard let baseURL = aDecoder.decodeObject(of: NSURL.self, forKey: "baseURL") as URL? else {
|
||||
if #available(macOS 10.11, iOS 9.0, tvOS 9.0, *) {
|
||||
aDecoder.failWithError(CocoaError(.coderValueNotFound,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Base URL is not set."]))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
let mode: Mode
|
||||
if let modeStr = aDecoder.decodeObject(forKey: "mode") as? String, let mode_v = Mode(rawValue: modeStr) {
|
||||
if let modeStr = aDecoder.decodeObject(of: NSString.self, forKey: "mode") as String?, let mode_v = Mode(rawValue: modeStr) {
|
||||
mode = mode_v
|
||||
} else {
|
||||
let passiveMode = aDecoder.decodeBool(forKey: "passiveMode")
|
||||
mode = passiveMode ? .passive : .active
|
||||
}
|
||||
self.init(baseURL: baseURL, mode: mode, credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential)
|
||||
self.init(baseURL: baseURL, mode: mode, credential: aDecoder.decodeObject(of: URLCredential.self, forKey: "credential"))
|
||||
self.useCache = aDecoder.decodeBool(forKey: "useCache")
|
||||
self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache")
|
||||
self.supportsRFC3659 = aDecoder.decodeBool(forKey: "supportsRFC3659")
|
||||
@@ -196,7 +204,7 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
|
||||
|
||||
- Note: Disabling this option will increase upload speed.
|
||||
*/
|
||||
public var uploadByREST: Bool = FileProviderStreamTask.defaultUseURLSession
|
||||
public var uploadByREST: Bool = false
|
||||
|
||||
/**
|
||||
Determines data connection must TLS or not. `false` value indicates to use `PROT C` and
|
||||
@@ -204,6 +212,12 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
|
||||
*/
|
||||
public var securedDataConnection: Bool = true
|
||||
|
||||
/**
|
||||
Trust all certificates if `disableEvaluation`, Otherwise validate certificate chain.
|
||||
Default is `performDefaultEvaluation`.
|
||||
*/
|
||||
public var serverTrustPolicy: ServerTrustPolicy = .performDefaultEvaluation(validateHost: true)
|
||||
|
||||
open func contentsOfDirectory(path: String, completionHandler: @escaping ([FileObject], Error?) -> Void) {
|
||||
self.contentsOfDirectory(path: path, rfc3659enabled: supportsRFC3659, completionHandler: completionHandler)
|
||||
}
|
||||
@@ -223,6 +237,8 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
|
||||
let path = ftpPath(apath)
|
||||
|
||||
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
|
||||
task.serverTrustPolicy = serverTrustPolicy
|
||||
task.taskDescription = FileOperationType.fetch(path: path).json
|
||||
self.ftpLogin(task) { (error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
@@ -248,7 +264,7 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
|
||||
}
|
||||
|
||||
|
||||
let files: [FileObject] = contents.flatMap {
|
||||
let files: [FileObject] = contents.compactMap {
|
||||
rfc3659enabled ? self.parseMLST($0, in: path) : (self.parseUnixList($0, in: path) ?? self.parseDOSList($0, in: path))
|
||||
}
|
||||
|
||||
@@ -278,6 +294,8 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
|
||||
let path = ftpPath(apath)
|
||||
|
||||
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
|
||||
task.serverTrustPolicy = serverTrustPolicy
|
||||
task.taskDescription = FileOperationType.fetch(path: path).json
|
||||
self.ftpLogin(task) { (error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
@@ -297,7 +315,7 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
|
||||
}
|
||||
|
||||
guard let response = response, response.hasPrefix("250") || (response.hasPrefix("50") && rfc3659enabled) else {
|
||||
throw self.urlError(path, code: .badServerResponse)
|
||||
throw URLError(.badServerResponse, url: self.url(of: path))
|
||||
}
|
||||
|
||||
if response.hasPrefix("500") {
|
||||
@@ -305,11 +323,11 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
|
||||
self.attributesOfItem(path: path, rfc3659enabled: false, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
let lines = response.components(separatedBy: "\n").flatMap { $0.isEmpty ? nil : $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
let lines = response.components(separatedBy: "\n").compactMap { $0.isEmpty ? nil : $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
guard lines.count > 2 else {
|
||||
throw self.urlError(path, code: .badServerResponse)
|
||||
throw URLError(.badServerResponse, url: self.url(of: path))
|
||||
}
|
||||
let dirPath = (path as NSString).deletingLastPathComponent
|
||||
let dirPath = path.deletingLastPathComponent
|
||||
let file: FileObject? = rfc3659enabled ?
|
||||
self.parseMLST(lines[1], in: dirPath) :
|
||||
(self.parseUnixList(lines[1], in: dirPath) ?? self.parseDOSList(lines[1], in: dirPath))
|
||||
@@ -403,7 +421,7 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
|
||||
|
||||
@discardableResult
|
||||
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let path = (atPath as NSString).appendingPathComponent(folderName) + "/"
|
||||
let path = atPath.appendingPathComponent(folderName) + "/"
|
||||
return doOperation(.create(path: path), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@@ -424,10 +442,17 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
|
||||
|
||||
@discardableResult
|
||||
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
guard (try? localFile.checkResourceIsReachable()) ?? false else {
|
||||
dispatch_queue.async {
|
||||
completionHandler?(URLError(.fileDoesNotExist, url: localFile))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// check file is not a folder
|
||||
guard (try? localFile.resourceValues(forKeys: [.fileResourceTypeKey]))?.fileResourceType ?? .unknown == .regular else {
|
||||
dispatch_queue.async {
|
||||
completionHandler?(self.urlError(localFile.path, code: .fileIsDirectory))
|
||||
completionHandler?(URLError(.fileIsDirectory, url: localFile))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -437,12 +462,17 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
|
||||
return nil
|
||||
}
|
||||
|
||||
let progress = Progress(totalUnitCount: 0)
|
||||
let progress = Progress(totalUnitCount: -1)
|
||||
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
||||
progress.kind = .file
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
||||
|
||||
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
|
||||
task.serverTrustPolicy = serverTrustPolicy
|
||||
task.taskDescription = operation.json
|
||||
progress.cancellationHandler = { [weak task] in
|
||||
task?.cancel()
|
||||
}
|
||||
self.ftpLogin(task) { (error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
@@ -452,7 +482,11 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
|
||||
return
|
||||
}
|
||||
|
||||
self.ftpStore(task, filePath: self.ftpPath(toPath), fromData: nil, fromFile: localFile, onTask: { task in
|
||||
guard let stream = InputStream(url: localFile) else {
|
||||
return
|
||||
}
|
||||
let size = localFile.fileSize
|
||||
self.ftpStore(task, filePath: self.ftpPath(toPath), from: stream, size: size, onTask: { task in
|
||||
weak var weakTask = task
|
||||
progress.cancellationHandler = {
|
||||
weakTask?.cancel()
|
||||
@@ -483,12 +517,17 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
let progress = Progress(totalUnitCount: 0)
|
||||
let progress = Progress(totalUnitCount: -1)
|
||||
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
||||
progress.kind = .file
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
||||
|
||||
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
|
||||
task.serverTrustPolicy = serverTrustPolicy
|
||||
task.taskDescription = operation.json
|
||||
progress.cancellationHandler = { [weak task] in
|
||||
task?.cancel()
|
||||
}
|
||||
self.ftpLogin(task) { (error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
@@ -497,7 +536,12 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
|
||||
return
|
||||
}
|
||||
|
||||
self.ftpDownload(task, filePath: self.ftpPath(path), onTask: { task in
|
||||
let tempURL = URL(fileURLWithPath: NSTemporaryDirectory().appendingPathComponent(UUID().uuidString))
|
||||
guard let stream = OutputStream(url: tempURL, append: false) else {
|
||||
completionHandler?(CocoaError(.fileWriteUnknown, path: destURL.path))
|
||||
return
|
||||
}
|
||||
self.ftpDownload(task, filePath: self.ftpPath(path), to: stream, onTask: { task in
|
||||
weak var weakTask = task
|
||||
progress.cancellationHandler = {
|
||||
weakTask?.cancel()
|
||||
@@ -507,22 +551,24 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
|
||||
progress.totalUnitCount = totalSize
|
||||
progress.completedUnitCount = totalReceived
|
||||
self.delegateNotify(operation, progress: progress.fractionCompleted)
|
||||
}) { (tmpurl, error) in
|
||||
if let error = error {
|
||||
}) { (error) in
|
||||
if error != nil {
|
||||
progress.cancel()
|
||||
}
|
||||
do {
|
||||
try FileManager.default.moveItem(at: tempURL, to: destURL)
|
||||
} catch {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
try? FileManager.default.removeItem(at: tempURL)
|
||||
return
|
||||
}
|
||||
|
||||
if let tmpurl = tmpurl {
|
||||
try? FileManager.default.moveItem(at: tmpurl, to: destURL)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(nil)
|
||||
self.delegateNotify(operation)
|
||||
}
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -539,12 +585,17 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
let progress = Progress(totalUnitCount: 0)
|
||||
let progress = Progress(totalUnitCount: -1)
|
||||
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
||||
progress.kind = .file
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
||||
|
||||
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
|
||||
task.serverTrustPolicy = serverTrustPolicy
|
||||
task.taskDescription = operation.json
|
||||
progress.cancellationHandler = { [weak task] in
|
||||
task?.cancel()
|
||||
}
|
||||
self.ftpLogin(task) { (error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
@@ -553,7 +604,8 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
|
||||
return
|
||||
}
|
||||
|
||||
self.ftpFileData(task, filePath: self.ftpPath(path), from: offset, length: length, onTask: { task in
|
||||
let stream = OutputStream.toMemory()
|
||||
self.ftpDownload(task, filePath: self.ftpPath(path), from: offset, length: length, to: stream, onTask: { task in
|
||||
weak var weakTask = task
|
||||
progress.cancellationHandler = {
|
||||
weakTask?.cancel()
|
||||
@@ -563,7 +615,7 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
|
||||
progress.totalUnitCount = totalSize
|
||||
progress.completedUnitCount = totalReceived
|
||||
self.delegateNotify(operation, progress: progress.fractionCompleted)
|
||||
}) { (data, error) in
|
||||
}) { (error) in
|
||||
if let error = error {
|
||||
progress.cancel()
|
||||
self.dispatch_queue.async {
|
||||
@@ -573,7 +625,7 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
|
||||
return
|
||||
}
|
||||
|
||||
if let data = data {
|
||||
if let data = stream.property(forKey: .dataWrittenToMemoryStreamKey) as? Data {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(data, nil)
|
||||
self.delegateNotify(operation)
|
||||
@@ -592,12 +644,17 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
|
||||
return nil
|
||||
}
|
||||
|
||||
let progress = Progress(totalUnitCount: Int64(data?.count ?? 0))
|
||||
let progress = Progress(totalUnitCount: Int64(data?.count ?? -1))
|
||||
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
||||
progress.kind = .file
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
||||
|
||||
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
|
||||
task.serverTrustPolicy = serverTrustPolicy
|
||||
task.taskDescription = operation.json
|
||||
progress.cancellationHandler = { [weak task] in
|
||||
task?.cancel()
|
||||
}
|
||||
self.ftpLogin(task) { (error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
@@ -608,7 +665,9 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
|
||||
}
|
||||
|
||||
let storeHandler = {
|
||||
self.ftpStore(task, filePath: self.ftpPath(path), fromData: data ?? Data(), fromFile: nil, onTask: { task in
|
||||
let data = data ?? Data()
|
||||
let stream = InputStream(data: data)
|
||||
self.ftpStore(task, filePath: self.ftpPath(path), from: stream, size: Int64(data.count), onTask: { task in
|
||||
weak var weakTask = task
|
||||
progress.cancellationHandler = {
|
||||
weakTask?.cancel()
|
||||
@@ -643,6 +702,65 @@ open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fil
|
||||
return progress
|
||||
}
|
||||
|
||||
public func contents(path: String, offset: Int64, length: Int, responseHandler: ((URLResponse) -> Void)?, progressHandler: @escaping (Int64, Data) -> Void, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let operation = FileOperationType.fetch(path: path)
|
||||
if length == 0 || offset < 0 {
|
||||
dispatch_queue.async {
|
||||
completionHandler?(nil)
|
||||
self.delegateNotify(operation)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
let progress = Progress(totalUnitCount: -1)
|
||||
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
||||
progress.kind = .file
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
||||
|
||||
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
|
||||
task.serverTrustPolicy = serverTrustPolicy
|
||||
task.taskDescription = operation.json
|
||||
progress.cancellationHandler = { [weak task] in
|
||||
task?.cancel()
|
||||
}
|
||||
self.ftpLogin(task) { (error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
self.ftpDownloadData(task, filePath: self.ftpPath(path), from: offset, length: length, onTask: { task in
|
||||
weak var weakTask = task
|
||||
progress.cancellationHandler = {
|
||||
weakTask?.cancel()
|
||||
}
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
}, onProgress: { data, recevied, totalReceived, totalSize in
|
||||
progressHandler(totalReceived - recevied, data)
|
||||
progress.totalUnitCount = totalSize
|
||||
progress.completedUnitCount = totalReceived
|
||||
self.delegateNotify(operation, progress: progress.fractionCompleted)
|
||||
}) { (data, error) in
|
||||
if let error = error {
|
||||
progress.cancel()
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(nil)
|
||||
self.delegateNotify(operation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return progress
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a symbolic link at the specified path that points to an item at the given path.
|
||||
This method does not traverse symbolic links contained in destination path, making it possible
|
||||
@@ -685,6 +803,11 @@ extension FTPFileProvider {
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
||||
|
||||
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
|
||||
task.serverTrustPolicy = serverTrustPolicy
|
||||
task.taskDescription = operation.json
|
||||
progress.cancellationHandler = { [weak task] in
|
||||
task?.cancel()
|
||||
}
|
||||
self.ftpLogin(task) { (error) in
|
||||
if let error = error {
|
||||
completionHandler?(error)
|
||||
@@ -701,13 +824,13 @@ extension FTPFileProvider {
|
||||
|
||||
guard let response = response else {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: self.urlError(sourcePath, code: .badServerResponse))
|
||||
self.delegateNotify(operation, error: URLError(.badServerResponse, url: self.url(of: sourcePath)))
|
||||
return
|
||||
}
|
||||
|
||||
let codes: [Int] = response.components(separatedBy: .newlines).flatMap({ $0.isEmpty ? nil : $0})
|
||||
.flatMap {
|
||||
let code = $0.components(separatedBy: .whitespaces).flatMap({ $0.isEmpty ? nil : $0}).first
|
||||
let codes: [Int] = response.components(separatedBy: .newlines).compactMap({ $0.isEmpty ? nil : $0})
|
||||
.compactMap {
|
||||
let code = $0.components(separatedBy: .whitespaces).compactMap({ $0.isEmpty ? nil : $0}).first
|
||||
return code != nil ? Int(code!) : nil
|
||||
}
|
||||
|
||||
@@ -726,13 +849,16 @@ extension FTPFileProvider {
|
||||
case .link: errorCode = .cannotWriteToFile
|
||||
default: errorCode = .cannotOpenFile
|
||||
}
|
||||
let error = self.urlError(sourcePath, code: errorCode)
|
||||
let error = URLError(errorCode, url: self.url(of: sourcePath))
|
||||
progress.cancel()
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
return
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS)
|
||||
self._registerUndo(operation)
|
||||
#endif
|
||||
progress.completedUnitCount = progress.totalUnitCount
|
||||
completionHandler?(nil)
|
||||
self.delegateNotify(operation)
|
||||
@@ -783,7 +909,7 @@ extension FTPFileProvider {
|
||||
}
|
||||
|
||||
guard let response = response else {
|
||||
throw self.urlError(sourcePath, code: .badServerResponse)
|
||||
throw URLError(.badServerResponse, url: self.url(of: sourcePath))
|
||||
}
|
||||
|
||||
if response.hasPrefix("50") {
|
||||
@@ -792,7 +918,7 @@ extension FTPFileProvider {
|
||||
}
|
||||
|
||||
if !response.hasPrefix("2") {
|
||||
throw self.urlError(sourcePath, code: .cannotRemoveFile)
|
||||
throw URLError(.cannotRemoveFile, url: self.url(of: sourcePath))
|
||||
}
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(nil)
|
||||
@@ -845,3 +971,7 @@ extension FTPFileProvider {
|
||||
}
|
||||
|
||||
extension FTPFileProvider: FileProvider { }
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS)
|
||||
extension FTPFileProvider: FileProvideUndoable { }
|
||||
#endif
|
||||
|
||||
+484
-366
File diff suppressed because it is too large
Load Diff
+40
-24
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
|
||||
/// Containts path, url and attributes of a file or resource.
|
||||
open class FileObject: Equatable {
|
||||
open class FileObject: NSObject {
|
||||
/// A `Dictionary` contains file information, using `URLResourceKey` keys.
|
||||
open internal(set) var allValues: [URLResourceKey: Any]
|
||||
|
||||
@@ -19,6 +19,7 @@ open class FileObject: Equatable {
|
||||
|
||||
internal init(url: URL?, name: String, path: String) {
|
||||
self.allValues = [URLResourceKey: Any]()
|
||||
super.init()
|
||||
if let url = url {
|
||||
self.url = url
|
||||
}
|
||||
@@ -33,7 +34,10 @@ open class FileObject: Equatable {
|
||||
if let url = allValues[.fileURLKey] as? URL {
|
||||
return url
|
||||
} else {
|
||||
let path = self.path.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? self.path
|
||||
var path = self.path.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? self.path
|
||||
if path.hasPrefix("/") {
|
||||
path.remove(at: path.startIndex)
|
||||
}
|
||||
return URL(string: path) ?? URL(string: "/")!
|
||||
}
|
||||
}
|
||||
@@ -147,28 +151,40 @@ open class FileObject: Equatable {
|
||||
open var isSymLink: Bool {
|
||||
return self.type == .symbolicLink
|
||||
}
|
||||
|
||||
/// Check `FileObject` equality
|
||||
public static func ==(lhs: FileObject, rhs: FileObject) -> Bool {
|
||||
if rhs === lhs {
|
||||
return true
|
||||
}
|
||||
#if swift(>=3.1)
|
||||
if Swift.type(of: lhs) != Swift.type(of: rhs) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
extension FileObject {
|
||||
open override var hash: Int {
|
||||
#if swift(>=4.2)
|
||||
var hasher = Hasher()
|
||||
hasher.combine(url)
|
||||
hasher.combine(size)
|
||||
hasher.combine(modifiedDate)
|
||||
return hasher.finalize()
|
||||
#else
|
||||
if type(of: lhs) != type(of: rhs) {
|
||||
return false
|
||||
}
|
||||
let hashURL = self.url.hashValue
|
||||
let hashSize = self.size.hashValue
|
||||
return (hashURL << 7) &+ hashURL &+ hashSize
|
||||
#endif
|
||||
|
||||
if let rurl = rhs.allValues[.fileURLKey] as? URL, let lurl = lhs.allValues[.fileURLKey] as? URL {
|
||||
return rurl == lurl && rhs.size == lhs.size
|
||||
}
|
||||
return rhs.path == lhs.path && rhs.size == lhs.size && rhs.modifiedDate == lhs.modifiedDate
|
||||
}
|
||||
|
||||
open override func isEqual(_ object: Any?) -> Bool {
|
||||
guard let object = object as? FileObject else { return false }
|
||||
if self === object {
|
||||
return true
|
||||
}
|
||||
if Swift.type(of: self) != Swift.type(of: object) {
|
||||
return false
|
||||
}
|
||||
|
||||
if let rurl = self.allValues[.fileURLKey] as? URL, let lurl = object.allValues[.fileURLKey] as? URL {
|
||||
return rurl == lurl && self.size == object.size
|
||||
}
|
||||
return self.path == object.path && self.size == object.size && self.modifiedDate == object.modifiedDate
|
||||
}
|
||||
}
|
||||
|
||||
extension FileObject {
|
||||
internal func mapPredicate() -> [String: Any] {
|
||||
let mapDict: [URLResourceKey: String] = [.fileURLKey: "url", .nameKey: "name", .pathKey: "path",
|
||||
.fileSizeKey: "fileSize", .creationDateKey: "creationDate",
|
||||
@@ -207,6 +223,7 @@ open class FileObject: Equatable {
|
||||
case .and: return NSCompoundPredicate(andPredicateWithSubpredicates: newSub)
|
||||
case .not: return NSCompoundPredicate(notPredicateWithSubpredicate: newSub[0])
|
||||
case .or: return NSCompoundPredicate(orPredicateWithSubpredicates: newSub)
|
||||
@unknown default: fatalError()
|
||||
}
|
||||
} else if let cQuery = query as? NSComparisonPredicate {
|
||||
var newLeft = cQuery.leftExpression
|
||||
@@ -225,7 +242,7 @@ open class FileObject: Equatable {
|
||||
}
|
||||
|
||||
/// Containts attributes of a provider.
|
||||
open class VolumeObject {
|
||||
open class VolumeObject: NSObject {
|
||||
/// A `Dictionary` contains volume information, using `URLResourceKey` keys.
|
||||
open internal(set) var allValues: [URLResourceKey: Any]
|
||||
|
||||
@@ -323,7 +340,6 @@ open class VolumeObject {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Sorting FileObject array by given criteria, **not thread-safe**
|
||||
public struct FileObjectSorting {
|
||||
|
||||
@@ -399,8 +415,8 @@ public struct FileObjectSorting {
|
||||
case .nameCaseInsensitive:
|
||||
return ($0.name).localizedCaseInsensitiveCompare($1.name) == (ascending ? .orderedAscending : .orderedDescending)
|
||||
case .extension:
|
||||
let kind1 = $0.isDirectory ? "folder" : ($0.path as NSString).pathExtension
|
||||
let kind2 = $1.isDirectory ? "folder" : ($1.path as NSString).pathExtension
|
||||
let kind1 = $0.isDirectory ? "folder" : $0.path.pathExtension
|
||||
let kind2 = $1.isDirectory ? "folder" : $1.path.pathExtension
|
||||
return kind1.localizedCaseInsensitiveCompare(kind2) == (ascending ? .orderedAscending : .orderedDescending)
|
||||
case .modifiedDate:
|
||||
let fileMod1 = $0.modifiedDate ?? Date.distantPast
|
||||
|
||||
+237
-118
@@ -9,9 +9,11 @@
|
||||
import Foundation
|
||||
#if os(iOS) || os(tvOS)
|
||||
import UIKit
|
||||
import ImageIO
|
||||
public typealias ImageClass = UIImage
|
||||
#elseif os(macOS)
|
||||
import Cocoa
|
||||
import ImageIO
|
||||
public typealias ImageClass = NSImage
|
||||
#endif
|
||||
|
||||
@@ -19,7 +21,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, NSSecureCoding {
|
||||
public protocol FileProviderBasic: class, NSSecureCoding, CustomDebugStringConvertible {
|
||||
/// An string to identify type of provider.
|
||||
static var type: String { get }
|
||||
|
||||
@@ -201,6 +203,13 @@ extension FileProviderBasic {
|
||||
operation_queue.maxConcurrentOperationCount = newValue
|
||||
}
|
||||
}
|
||||
|
||||
public var debugDescription: String {
|
||||
let typeDesc = "\(Self.type) provider"
|
||||
let urlDesc = self.baseURL.map({ " - " + $0.absoluteString }) ?? ""
|
||||
let credentialDesc = self.credential?.user.map({ " - " + $0.debugDescription }) ?? ""
|
||||
return typeDesc + urlDesc + credentialDesc
|
||||
}
|
||||
}
|
||||
|
||||
/// Checking equality of two file provider, regardless of current path queues and delegates.
|
||||
@@ -423,7 +432,7 @@ public protocol FileProviderOperations: FileProviderBasic {
|
||||
func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> Progress?
|
||||
}
|
||||
|
||||
public extension FileProviderOperations {
|
||||
extension FileProviderOperations {
|
||||
@discardableResult
|
||||
public func moveItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
return self.moveItem(path: path, to: to, overwrite: false, completionHandler: completionHandler)
|
||||
@@ -440,7 +449,7 @@ public extension FileProviderOperations {
|
||||
}
|
||||
}
|
||||
|
||||
internal extension FileProviderOperations {
|
||||
extension FileProviderOperations {
|
||||
internal func delegateNotify(_ operation: FileOperationType, error: Error? = nil) {
|
||||
DispatchQueue.main.async(execute: {
|
||||
if let error = error {
|
||||
@@ -569,6 +578,72 @@ extension FileProviderReadWrite {
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines method for fetching file contents progressivly
|
||||
public protocol FileProviderReadWriteProgressive {
|
||||
/**
|
||||
Retreives a `Data` object with a portion contents of the file asynchronously vis contents argument of completion handler.
|
||||
If path specifies a directory, or if some other error occurs, data will be nil.
|
||||
|
||||
- Parameters:
|
||||
- path: Path of file.
|
||||
- progressHandler: a closure which will be called every time a new data received from server.
|
||||
- position: Start position of returned data, indexed from zero.
|
||||
- data: returned `Data` from server.
|
||||
- completionHandler: a closure which will be called after receiving is completed or an error occureed.
|
||||
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
func contents(path: String, progressHandler: @escaping (_ position: Int64, _ data: Data) -> Void, completionHandler: SimpleCompletionHandler) -> Progress?
|
||||
|
||||
/**
|
||||
Retreives a `Data` object with a portion contents of the file asynchronously vis contents argument of completion handler.
|
||||
If path specifies a directory, or if some other error occurs, data will be nil.
|
||||
|
||||
- Parameters:
|
||||
- path: Path of file.
|
||||
- responseHandler: a closure which will be called after fetching server response.
|
||||
- response: `URLResponse` returned from server.
|
||||
- progressHandler: a closure which will be called every time a new data received from server.
|
||||
- position: Start position of returned data, indexed from zero.
|
||||
- data: returned `Data` from server.
|
||||
- completionHandler: a closure which will be called after receiving is completed or an error occureed.
|
||||
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
func contents(path: String, responseHandler: ((_ response: URLResponse) -> Void)?, progressHandler: @escaping (_ position: Int64, _ data: Data) -> Void, completionHandler: SimpleCompletionHandler) -> Progress?
|
||||
|
||||
/**
|
||||
Retreives a `Data` object with a portion contents of the file asynchronously vis contents argument of completion handler.
|
||||
If path specifies a directory, or if some other error occurs, data will be nil.
|
||||
|
||||
- Parameters:
|
||||
- path: Path of file.
|
||||
- offset: First byte index which should be read. **Starts from 0.**
|
||||
- length: Bytes count of data. Pass `-1` to read until the end of file.
|
||||
- responseHandler: a closure which will be called after fetching server response.
|
||||
- response: `URLResponse` returned from server.
|
||||
- progressHandler: a closure which will be called every time a new data received from server.
|
||||
- position: Start position of returned data, indexed from offset.
|
||||
- data: returned `Data` from server.
|
||||
- completionHandler: a closure which will be called after receiving is completed or an error occureed.
|
||||
- Returns: An `Progress` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
func contents(path: String, offset: Int64, length: Int, responseHandler: ((_ response: URLResponse) -> Void)?, progressHandler: @escaping (_ position: Int64, _ data: Data) -> Void, completionHandler: SimpleCompletionHandler) -> Progress?
|
||||
}
|
||||
|
||||
extension FileProviderReadWriteProgressive {
|
||||
@discardableResult
|
||||
public func contents(path: String, progressHandler: @escaping (_ position: Int64, _ data: Data) -> Void, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
return contents(path: path, offset: 0, length: -1, responseHandler: nil, progressHandler: progressHandler, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func contents(path: String, responseHandler: ((_ response: URLResponse) -> Void)?, progressHandler: @escaping (_ position: Int64, _ data: Data) -> Void, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
return contents(path: path, offset: 0, length: -1, responseHandler: responseHandler, progressHandler: progressHandler, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows a file provider to notify changes occured
|
||||
public protocol FileProviderMonitor: FileProviderBasic {
|
||||
|
||||
@@ -618,7 +693,7 @@ public protocol FileProvideUndoable: FileProviderOperations {
|
||||
func canUndo(operation: FileOperationType) -> Bool
|
||||
}
|
||||
|
||||
public extension FileProvideUndoable {
|
||||
extension FileProvideUndoable {
|
||||
public func canUndo(operation: FileOperationType) -> Bool {
|
||||
return undoOperation(for: operation) != nil
|
||||
}
|
||||
@@ -656,6 +731,43 @@ public extension FileProvideUndoable {
|
||||
self.undoManager = UndoManager()
|
||||
self.undoManager?.levelsOfUndo = 10
|
||||
}
|
||||
|
||||
public func _registerUndo(_ operation: FileOperationType) {
|
||||
#if os(macOS) || os(iOS) || os(tvOS)
|
||||
guard let undoManager = self.undoManager, let undoOp = self.undoOperation(for: operation) else {
|
||||
return
|
||||
}
|
||||
|
||||
let undoBox = UndoBox(provider: self, operation: operation, undoOperation: undoOp)
|
||||
undoManager.beginUndoGrouping()
|
||||
undoManager.registerUndo(withTarget: undoBox, selector: #selector(UndoBox.doSimpleOperation(_:)), object: undoBox)
|
||||
undoManager.setActionName(operation.actionDescription)
|
||||
undoManager.endUndoGrouping()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
class UndoBox: NSObject {
|
||||
weak var provider: FileProvideUndoable?
|
||||
let operation: FileOperationType
|
||||
let undoOperation: FileOperationType
|
||||
|
||||
init(provider: FileProvideUndoable, operation: FileOperationType, undoOperation: FileOperationType) {
|
||||
self.provider = provider
|
||||
self.operation = operation
|
||||
self.undoOperation = undoOperation
|
||||
}
|
||||
|
||||
@objc internal func doSimpleOperation(_ box: UndoBox) {
|
||||
switch self.undoOperation {
|
||||
case .move(source: let source, destination: let dest):
|
||||
_=provider?.moveItem(path: source, to: dest, completionHandler: nil)
|
||||
case .remove(let path):
|
||||
_=provider?.removeItem(path: path, completionHandler: nil)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -678,24 +790,43 @@ public protocol FileProviderSharing {
|
||||
func publicLink(to path: String, completionHandler: @escaping (_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void)
|
||||
}
|
||||
|
||||
//efines protocol for provider allows symbolic link operations.
|
||||
public protocol FileProviderSymbolicLink {
|
||||
/**
|
||||
Creates a symbolic link at the specified path that points to an item at the given path.
|
||||
This method does not traverse symbolic links contained in destination path, making it possible
|
||||
to create symbolic links to locations that do not yet exist.
|
||||
Also, if the final path component is a symbolic link, that link is not followed.
|
||||
|
||||
- Parameters:
|
||||
- symbolicLink: The file path at which to create the new symbolic link. The last component of the path issued as the name of the link.
|
||||
- withDestinationPath: The path that contains the item to be pointed to by the link. In other words, this is the destination of the link.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
*/
|
||||
func create(symbolicLink path: String, withDestinationPath destPath: String, completionHandler: SimpleCompletionHandler)
|
||||
|
||||
/// Returns the path of the item pointed to by a symbolic link.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - path: The path of a file or directory.
|
||||
/// - completionHandler: Returns destination url of given symbolic link, or an `Error` object if it fails.
|
||||
func destination(ofSymbolicLink path: String, completionHandler: @escaping (_ file: FileObject?, _ error: Error?) -> Void)
|
||||
}
|
||||
|
||||
/// Defines protocol for provider allows all common operations.
|
||||
public protocol FileProvider: FileProviderOperations, FileProviderReadWrite, NSCopying {
|
||||
}
|
||||
|
||||
internal let pathTrimSet = CharacterSet(charactersIn: " /")
|
||||
public extension FileProviderBasic {
|
||||
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) -> URL {
|
||||
var rpath: String = path
|
||||
rpath = rpath.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? rpath
|
||||
if let baseURL = baseURL {
|
||||
if let baseURL = baseURL?.absoluteURL {
|
||||
if rpath.hasPrefix("/") {
|
||||
rpath.remove(at: rpath.startIndex)
|
||||
}
|
||||
@@ -759,32 +890,7 @@ public extension FileProviderBasic {
|
||||
}
|
||||
_ = group.wait(timeout: .now() + 5)
|
||||
let finalFile = result + (!fileExt.isEmpty ? "." + fileExt : "")
|
||||
return (dirPath as NSString).appendingPathComponent(finalFile)
|
||||
}
|
||||
|
||||
internal func urlError(_ path: String, code: URLError.Code) -> Error {
|
||||
let fileURL = self.url(of: path)
|
||||
var userInfo: [String: Any] = [NSURLErrorKey: fileURL,
|
||||
NSURLErrorFailingURLErrorKey: fileURL,
|
||||
NSURLErrorFailingURLStringErrorKey: fileURL.absoluteString,
|
||||
]
|
||||
let error = NSError(domain: NSURLErrorDomain, code: code.rawValue, userInfo: nil)
|
||||
for (key, value) in error.userInfo {
|
||||
userInfo[key] = value
|
||||
}
|
||||
return URLError(code, userInfo: userInfo)
|
||||
}
|
||||
|
||||
internal func cocoaError(_ path: String, code: CocoaError.Code) -> Error {
|
||||
let fileURL = self.url(of: path)
|
||||
var userInfo: [String: Any] = [NSFilePathErrorKey: path,
|
||||
NSURLErrorKey: fileURL,
|
||||
]
|
||||
let error = NSError(domain: NSCocoaErrorDomain, code: code.rawValue, userInfo: nil)
|
||||
for (key, value) in error.userInfo {
|
||||
userInfo[key] = value
|
||||
}
|
||||
return cocoaError(fileURL.path, code: code)
|
||||
return dirPath.appendingPathComponent(finalFile)
|
||||
}
|
||||
|
||||
internal func NotImplemented(_ fn: String = #function, file: StaticString = #file) {
|
||||
@@ -864,121 +970,134 @@ extension ExtendedFileProvider {
|
||||
return self.thumbnailOfFile(path: path, dimension: nil, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
internal static func convertToImage(pdfData: Data?, page: Int = 1) -> ImageClass? {
|
||||
internal static func convertToImage(pdfData: Data?, page: Int = 1, maxSize: CGSize?) -> ImageClass? {
|
||||
guard let pdfData = pdfData else { return nil }
|
||||
|
||||
let cfPDFData: CFData = pdfData as CFData
|
||||
if let provider = CGDataProvider(data: cfPDFData), let reference = CGPDFDocument(provider), let pageRef = reference.page(at: page) {
|
||||
return self.convertToImage(pdfPage: pageRef)
|
||||
return self.convertToImage(pdfPage: pageRef, maxSize: maxSize)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
internal static func convertToImage(pdfURL: URL, page: Int = 1) -> ImageClass? {
|
||||
internal static func convertToImage(pdfURL: URL, page: Int = 1, maxSize: CGSize?) -> ImageClass? {
|
||||
// To accelerate, supporting only local file URL
|
||||
guard pdfURL.isFileURL else { return nil }
|
||||
|
||||
if let reference = CGPDFDocument(pdfURL as CFURL), let pageRef = reference.page(at: page) {
|
||||
return self.convertToImage(pdfPage: pageRef)
|
||||
return self.convertToImage(pdfPage: pageRef, maxSize: maxSize)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func convertToImage(pdfPage: CGPDFPage) -> ImageClass? {
|
||||
private static func convertToImage(pdfPage: CGPDFPage, maxSize: CGSize?) -> ImageClass? {
|
||||
let scale: CGFloat
|
||||
let frame = pdfPage.getBoxRect(CGPDFBox.mediaBox)
|
||||
var size = frame.size
|
||||
let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
|
||||
|
||||
if let maxSize = maxSize {
|
||||
scale = min(maxSize.width / frame.width, maxSize.height / frame.height)
|
||||
} else {
|
||||
#if os(macOS)
|
||||
scale = NSScreen.main?.backingScaleFactor ?? 1.0 // fetch device is retina or not
|
||||
#else
|
||||
scale = UIScreen.main.scale // fetch device is retina or not
|
||||
#endif
|
||||
}
|
||||
let rect = CGRect(origin: .zero, size: frame.size)
|
||||
let size = CGSize(width: frame.size.width * scale, height: frame.size.height * scale)
|
||||
let transform = pdfPage.getDrawingTransform(CGPDFBox.mediaBox, rect: rect, rotate: 0, preserveAspectRatio: true)
|
||||
|
||||
#if os(macOS)
|
||||
#if swift(>=3.2)
|
||||
let ppp = Int(NSScreen.main?.backingScaleFactor ?? 1) // fetch device is retina or not
|
||||
#else
|
||||
let ppp = Int(NSScreen.main()?.backingScaleFactor ?? 1) // fetch device is retina or not
|
||||
#endif
|
||||
let rep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(size.width), pixelsHigh: Int(size.height),
|
||||
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: .calibratedRGB,
|
||||
bytesPerRow: 0, bitsPerPixel: 0)
|
||||
|
||||
guard let context = NSGraphicsContext(bitmapImageRep: rep!) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
size.width *= CGFloat(ppp)
|
||||
size.height *= CGFloat(ppp)
|
||||
|
||||
#if swift(>=3.2)
|
||||
let rep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(size.width), pixelsHigh: Int(size.height),
|
||||
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: .calibratedRGB,
|
||||
bytesPerRow: 0, bitsPerPixel: 0)
|
||||
#else
|
||||
let rep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(size.width), pixelsHigh: Int(size.height),
|
||||
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: NSCalibratedRGBColorSpace,
|
||||
bytesPerRow: 0, bitsPerPixel: 0)
|
||||
#endif
|
||||
|
||||
guard let context = NSGraphicsContext(bitmapImageRep: rep!) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
NSGraphicsContext.saveGraphicsState()
|
||||
#if swift(>=4.0)
|
||||
NSGraphicsContext.current = context
|
||||
#else
|
||||
NSGraphicsContext.setCurrent(context)
|
||||
#endif
|
||||
|
||||
let transform = pdfPage.getDrawingTransform(CGPDFBox.mediaBox, rect: rect, rotate: 0, preserveAspectRatio: true)
|
||||
context.cgContext.concatenate(transform)
|
||||
|
||||
context.cgContext.translateBy(x: 0, y: size.height)
|
||||
context.cgContext.scaleBy(x: CGFloat(ppp), y: CGFloat(-ppp))
|
||||
context.cgContext.drawPDFPage(pdfPage)
|
||||
|
||||
let resultingImage = NSImage(size: size)
|
||||
resultingImage.addRepresentation(rep!)
|
||||
return resultingImage
|
||||
NSGraphicsContext.saveGraphicsState()
|
||||
NSGraphicsContext.current = context
|
||||
|
||||
context.cgContext.concatenate(transform)
|
||||
context.cgContext.translateBy(x: 0, y: size.height)
|
||||
context.cgContext.scaleBy(x: scale, y: -scale)
|
||||
context.cgContext.drawPDFPage(pdfPage)
|
||||
|
||||
let resultingImage = NSImage(size: size)
|
||||
resultingImage.addRepresentation(rep!)
|
||||
return resultingImage
|
||||
#else
|
||||
let ppp = Int(UIScreen.main.scale) // fetch device is retina or not
|
||||
size.width *= CGFloat(ppp)
|
||||
size.height *= CGFloat(ppp)
|
||||
|
||||
let handler : (CGContext) -> Void = { context in
|
||||
context.concatenate(transform)
|
||||
context.translateBy(x: 0, y: size.height)
|
||||
context.scaleBy(x: CGFloat(scale), y: CGFloat(-scale))
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.fill(rect)
|
||||
context.drawPDFPage(pdfPage)
|
||||
}
|
||||
|
||||
if #available(iOS 10.0, tvOS 10.0, *) {
|
||||
return UIGraphicsImageRenderer(size: size).image { (rendererContext) in
|
||||
handler(rendererContext.cgContext)
|
||||
}
|
||||
} else {
|
||||
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)
|
||||
|
||||
handler(context)
|
||||
context.restoreGState()
|
||||
let resultingImage = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
return resultingImage
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static func scaleDown(image: ImageClass, toSize maxSize: CGSize) -> ImageClass {
|
||||
let height, width: CGFloat
|
||||
if image.size.width > image.size.height {
|
||||
width = maxSize.width
|
||||
height = (image.size.height / image.size.width) * width
|
||||
internal static func scaleDown(fileURL: URL, toSize maxSize: CGSize?) -> ImageClass? {
|
||||
guard let source = CGImageSourceCreateWithURL(fileURL as CFURL, nil) else {
|
||||
return nil
|
||||
}
|
||||
return scaleDown(source: source, toSize: maxSize)
|
||||
}
|
||||
|
||||
internal static func scaleDown(data: Data, toSize maxSize: CGSize?) -> ImageClass? {
|
||||
guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {
|
||||
return nil
|
||||
}
|
||||
return scaleDown(source: source, toSize: maxSize)
|
||||
}
|
||||
|
||||
internal static func scaleDown(source: CGImageSource, toSize maxSize: CGSize?) -> ImageClass? {
|
||||
let options: [NSString: Any]
|
||||
if let maxSize = maxSize {
|
||||
let pixelSize: CGFloat
|
||||
let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil)
|
||||
|
||||
if let width: CGFloat = ((properties as NSDictionary?)?.object(forKey: kCGImagePropertyPixelWidth) as? CGFloat),
|
||||
let height: CGFloat = ((properties as NSDictionary?)?.object(forKey: kCGImagePropertyPixelHeight) as? CGFloat) {
|
||||
pixelSize = (width / maxSize.width < height / maxSize.height) ? maxSize.width : maxSize.height
|
||||
} else {
|
||||
pixelSize = max(maxSize.width, maxSize.height)
|
||||
}
|
||||
|
||||
options = [
|
||||
kCGImageSourceThumbnailMaxPixelSize: pixelSize,
|
||||
kCGImageSourceCreateThumbnailFromImageAlways: true]
|
||||
} else {
|
||||
height = maxSize.height
|
||||
width = (image.size.width / image.size.height) * height
|
||||
options = [
|
||||
kCGImageSourceCreateThumbnailFromImageAlways: true]
|
||||
}
|
||||
|
||||
let newSize = CGSize(width: width, height: height)
|
||||
|
||||
guard let image = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
|
||||
return nil
|
||||
}
|
||||
#if os(macOS)
|
||||
var imageRect = NSRect(origin: .zero, size: image.size)
|
||||
let imageRef = image.cgImage(forProposedRect: &imageRect, context: nil, hints: nil)
|
||||
|
||||
// Create NSImage from the CGImage using the new size
|
||||
return NSImage(cgImage: imageRef!, size: newSize)
|
||||
return ImageClass(cgImage: image, size: .zero)
|
||||
#else
|
||||
UIGraphicsBeginImageContextWithOptions(newSize, false, 0.0)
|
||||
image.draw(in: CGRect(origin: .zero, size: newSize))
|
||||
let newImage = UIGraphicsGetImageFromCurrentImageContext() ?? image
|
||||
UIGraphicsEndImageContext()
|
||||
return newImage
|
||||
return ImageClass(cgImage: image)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -1037,7 +1156,7 @@ public enum FileOperationType: CustomStringConvertible {
|
||||
return mirror.children.dropFirst().first?.value as? String
|
||||
}
|
||||
|
||||
init? (json: [String: AnyObject]) {
|
||||
init? (json: [String: Any]) {
|
||||
guard let type = json["type"] as? String, let source = json["source"] as? String else {
|
||||
return nil
|
||||
}
|
||||
@@ -1066,9 +1185,9 @@ public enum FileOperationType: CustomStringConvertible {
|
||||
}
|
||||
|
||||
internal var json: String? {
|
||||
var dictionary: [String: AnyObject] = ["type": self.description as NSString]
|
||||
dictionary["source"] = source as NSString?
|
||||
dictionary["dest"] = destination as NSString?
|
||||
var dictionary: [String: Any] = ["type": self.description]
|
||||
dictionary["source"] = source
|
||||
dictionary["dest"] = destination
|
||||
return String(jsonDictionary: dictionary)
|
||||
}
|
||||
}
|
||||
|
||||
+150
-74
@@ -14,9 +14,9 @@ import Foundation
|
||||
|
||||
No instance of this class should (and can) be created. Use derived classes instead. It leads to a crash with `fatalError()`.
|
||||
*/
|
||||
open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, FileProviderReadWrite {
|
||||
open class HTTPFileProvider: NSObject, FileProviderBasicRemote, FileProviderOperations, FileProviderReadWrite, FileProviderReadWriteProgressive {
|
||||
open class var type: String { fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.") }
|
||||
open let baseURL: URL?
|
||||
public let baseURL: URL?
|
||||
open var dispatch_queue: DispatchQueue
|
||||
open var operation_queue: OperationQueue {
|
||||
willSet {
|
||||
@@ -72,6 +72,10 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
|
||||
return _longpollSession!
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS)
|
||||
open var undoManager: UndoManager? = nil
|
||||
#endif
|
||||
|
||||
/**
|
||||
This is parent initializer for subclasses. Using this method on `HTTPFileProvider` will fail as `type` is not implemented.
|
||||
|
||||
@@ -89,14 +93,12 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
|
||||
self.cache = cache
|
||||
self.credential = credential
|
||||
|
||||
#if swift(>=3.1)
|
||||
let queueLabel = "FileProvider.\(Swift.type(of: self).type)"
|
||||
#else
|
||||
let queueLabel = "FileProvider.\(type(of: self).type)"
|
||||
#endif
|
||||
let queueLabel = "FileProvider.\(Swift.type(of: self).type)"
|
||||
dispatch_queue = DispatchQueue(label: queueLabel, attributes: .concurrent)
|
||||
operation_queue = OperationQueue()
|
||||
operation_queue.name = "\(queueLabel).Operation"
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
public required convenience init?(coder aDecoder: NSCoder) {
|
||||
@@ -196,7 +198,7 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
|
||||
|
||||
@discardableResult
|
||||
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let path = (atPath as NSString).appendingPathComponent(folderName) + "/"
|
||||
let path = atPath.appendingPathComponent(folderName) + "/"
|
||||
return doOperation(.create(path: path), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@@ -220,7 +222,7 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
|
||||
// check file is not a folder
|
||||
guard (try? localFile.resourceValues(forKeys: [.fileResourceTypeKey]))?.fileResourceType ?? .unknown == .regular else {
|
||||
dispatch_queue.async {
|
||||
completionHandler?(self.urlError(localFile.path, code: .fileIsDirectory))
|
||||
completionHandler?(URLError(.fileIsDirectory, url: localFile))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -237,8 +239,8 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
|
||||
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let operation = FileOperationType.copy(source: path, destination: destURL.absoluteString)
|
||||
let request = self.request(for: operation)
|
||||
let cantLoadError = urlError(path, code: .cannotLoadFromNetwork)
|
||||
return self.download_simple(path: path, request: request, operation: operation, completionHandler: { [weak self] (tempURL, error) in
|
||||
let cantLoadError = URLError(.cannotLoadFromNetwork, url: self.url(of: path))
|
||||
return self.download_file(path: path, request: request, operation: operation, completionHandler: { [weak self] (tempURL, error) in
|
||||
do {
|
||||
if let error = error {
|
||||
throw error
|
||||
@@ -248,11 +250,11 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
|
||||
throw cantLoadError
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS)
|
||||
var coordError: NSError?
|
||||
NSFileCoordinator().coordinate(writingItemAt: tempURL, options: .forMoving, writingItemAt: destURL, options: .forReplacing, error: &coordError, byAccessor: { (tempURL, destURL) in
|
||||
do {
|
||||
try FileManager.default.moveItem(at: tempURL, to: destURL)
|
||||
|
||||
completionHandler?(nil)
|
||||
self?.delegateNotify(operation)
|
||||
} catch {
|
||||
@@ -264,6 +266,17 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
|
||||
if let error = coordError {
|
||||
throw error
|
||||
}
|
||||
#else
|
||||
do {
|
||||
try FileManager.default.moveItem(at: tempURL, to: destURL)
|
||||
completionHandler?(nil)
|
||||
self?.delegateNotify(operation)
|
||||
} catch {
|
||||
completionHandler?(error)
|
||||
self?.delegateNotify(operation, error: error)
|
||||
}
|
||||
#endif
|
||||
|
||||
} catch {
|
||||
completionHandler?(error)
|
||||
self?.delegateNotify(operation, error: error)
|
||||
@@ -285,12 +298,10 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
|
||||
- Returns: An `Progress` to get progress or cancel progress.
|
||||
*/
|
||||
@discardableResult
|
||||
open func contents(path: String, offset: Int64 = 0, responseHandler: ((_ response: URLResponse) -> Void)? = nil, progressHandler: @escaping (_ position: Int64, _ data: Data) -> Void, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
open func contents(path: String, offset: Int64 = 0, length: Int = -1, responseHandler: ((_ response: URLResponse) -> Void)? = nil, progressHandler: @escaping (_ position: Int64, _ data: Data) -> Void, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let operation = FileOperationType.fetch(path: path)
|
||||
var request = self.request(for: operation)
|
||||
if offset > 0 {
|
||||
request.addValue("bytes \(offset)-", forHTTPHeaderField: "Range")
|
||||
}
|
||||
request.setValue(rangeWithOffset: offset, length: length)
|
||||
var position: Int64 = offset
|
||||
return download_progressive(path: path, request: request, operation: operation, responseHandler: responseHandler, progressHandler: { data in
|
||||
progressHandler(position, data)
|
||||
@@ -309,24 +320,25 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
|
||||
|
||||
let operation = FileOperationType.fetch(path: path)
|
||||
var request = self.request(for: operation)
|
||||
let cantLoadError = urlError(path, code: .cannotLoadFromNetwork)
|
||||
let cantLoadError = URLError(.cannotLoadFromNetwork, url: self.url(of: path))
|
||||
request.setValue(rangeWithOffset: offset, length: length)
|
||||
return self.download_simple(path: path, request: request, operation: operation, completionHandler: { (tempURL, error) in
|
||||
|
||||
let stream = OutputStream.toMemory()
|
||||
return self.download(path: path, request: request, operation: operation, stream: stream) { (error) in
|
||||
do {
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
guard let tempURL = tempURL else {
|
||||
guard let data = stream.property(forKey: .dataWrittenToMemoryStreamKey) as? Data else {
|
||||
throw cantLoadError
|
||||
}
|
||||
|
||||
let data = try Data(contentsOf: tempURL)
|
||||
completionHandler(data, nil)
|
||||
} catch {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
@@ -335,8 +347,10 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
let data = data ?? Data()
|
||||
let request = self.request(for: operation, overwrite: overwrite, attributes: [.contentModificationDateKey: Date()])
|
||||
return upload_data(path, request: request, data: data ?? Data(), operation: operation, completionHandler: completionHandler)
|
||||
let stream = InputStream(data: data)
|
||||
return upload(path, request: request, stream: stream, size: Int64(data.count), operation: operation, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
internal func request(for operation: FileOperationType, overwrite: Bool = false, attributes: [URLResourceKey: Any] = [:]) -> URLRequest {
|
||||
@@ -347,8 +361,9 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
|
||||
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
|
||||
}
|
||||
|
||||
internal func multiStatusHandler(source: String, data: Data, completionHandler: SimpleCompletionHandler) -> Void {
|
||||
internal func multiStatusError(operation: FileOperationType, data: Data) -> FileProviderHTTPError? {
|
||||
// WebDAV will override this function
|
||||
return nil
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -374,25 +389,32 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
|
||||
let request = self.request(for: operation, overwrite: overwrite)
|
||||
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderHTTPError?
|
||||
if let response = response as? HTTPURLResponse {
|
||||
if response.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) {
|
||||
serverError = self.serverError(with: code, path: operation.source, data: data)
|
||||
do {
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
if FileProviderHTTPErrorCode(rawValue: response.statusCode) == .multiStatus, let data = data {
|
||||
self.multiStatusHandler(source: operation.source, data: data, completionHandler: completionHandler)
|
||||
if let response = response as? HTTPURLResponse {
|
||||
if response.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) {
|
||||
throw self.serverError(with: code, path: operation.source, data: data)
|
||||
}
|
||||
|
||||
if FileProviderHTTPErrorCode(rawValue: response.statusCode) == .multiStatus, let data = data,
|
||||
let ms_error = self.multiStatusError(operation: operation, data: data) {
|
||||
throw ms_error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if serverError == nil && error == nil {
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS)
|
||||
self._registerUndo(operation)
|
||||
#endif
|
||||
progress.completedUnitCount = 1
|
||||
} else {
|
||||
progress.cancel()
|
||||
completionHandler?(nil)
|
||||
self.delegateNotify(operation)
|
||||
} catch {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
|
||||
completionHandler?(serverError ?? error)
|
||||
self.delegateNotify(operation, error: serverError ?? error)
|
||||
})
|
||||
task.taskDescription = operation.json
|
||||
progress.cancellationHandler = { [weak task] in
|
||||
@@ -445,28 +467,28 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
|
||||
}
|
||||
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
if let error = error {
|
||||
do {
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
throw self.serverError(with: rCode, path: path, data: data)
|
||||
}
|
||||
|
||||
let (newFiles, err, newToken) = pageHandler(data, progress)
|
||||
if let error = err {
|
||||
throw error
|
||||
}
|
||||
|
||||
let files = previousResult + newFiles
|
||||
if let newToken = newToken, !progress.isCancelled {
|
||||
_ = self.paginated(path, startToken: newToken, currentProgress: progress, previousResult: files, requestHandler: requestHandler, pageHandler: pageHandler, completionHandler: completionHandler)
|
||||
} else {
|
||||
completionHandler(files, nil)
|
||||
}
|
||||
} catch {
|
||||
completionHandler(previousResult, error)
|
||||
return
|
||||
}
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
let responseError = self.serverError(with: rCode, path: path, data: data)
|
||||
completionHandler(previousResult, responseError)
|
||||
return
|
||||
}
|
||||
|
||||
let (newFiles, err, newToken) = pageHandler(data, progress)
|
||||
if let error = err {
|
||||
completionHandler(previousResult, error)
|
||||
return
|
||||
}
|
||||
let files = previousResult + newFiles
|
||||
if let newToken = newToken, !progress.isCancelled {
|
||||
_ = self.paginated(path, startToken: newToken, currentProgress: progress, previousResult: files, requestHandler: requestHandler, pageHandler: pageHandler, completionHandler: completionHandler)
|
||||
} else {
|
||||
completionHandler(files, nil)
|
||||
}
|
||||
|
||||
})
|
||||
progress.cancellationHandler = { [weak task] in
|
||||
task?.cancel()
|
||||
@@ -480,7 +502,6 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
|
||||
|
||||
func upload_task(_ targetPath: String, progress: Progress, task: URLSessionTask, operation: FileOperationType,
|
||||
completionHandler: SimpleCompletionHandler) -> Void {
|
||||
var progress = progress
|
||||
|
||||
var allData = Data()
|
||||
dataCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { data in
|
||||
@@ -499,7 +520,7 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
|
||||
self?.delegateNotify(operation, error: responseError ?? error)
|
||||
}
|
||||
task.taskDescription = operation.json
|
||||
task.addObserver(self.sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent), options: .new, context: &progress)
|
||||
sessionDelegate?.observerProgress(of: task, using: progress, kind: .upload)
|
||||
progress.cancellationHandler = { [weak task] in
|
||||
task?.cancel()
|
||||
}
|
||||
@@ -507,9 +528,8 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
|
||||
task.resume()
|
||||
}
|
||||
|
||||
func upload_data(_ targetPath: String, request: URLRequest, data: Data, operation: FileOperationType,
|
||||
func upload(_ targetPath: String, request: URLRequest, stream: InputStream, size: Int64, operation: FileOperationType,
|
||||
completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let size: Int64 = Int64(data.count)
|
||||
if size > maxUploadSimpleSupported {
|
||||
let error = self.serverError(with: .payloadTooLarge, path: targetPath, data: nil)
|
||||
completionHandler?(error)
|
||||
@@ -522,7 +542,9 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
|
||||
progress.kind = .file
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
||||
|
||||
let task = session.uploadTask(with: request, from: data)
|
||||
var request = request
|
||||
request.httpBodyStream = stream
|
||||
let task = session.uploadTask(withStreamedRequest: request)
|
||||
self.upload_task(targetPath, progress: progress, task: task, operation: operation, completionHandler: completionHandler)
|
||||
|
||||
return progress
|
||||
@@ -530,7 +552,7 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
|
||||
|
||||
func upload_file(_ targetPath: String, request: URLRequest, localFile: URL, operation: FileOperationType,
|
||||
completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let fSize = (try? localFile.resourceValues(forKeys: [.fileSizeKey]))?.fileSize
|
||||
let fSize = (try? localFile.resourceValues(forKeys: [.fileSizeKey]))?.allValues[.fileSizeKey] as? Int64
|
||||
let size = Int64(fSize ?? -1)
|
||||
if size > maxUploadSimpleSupported {
|
||||
let error = self.serverError(with: .payloadTooLarge, path: targetPath, data: nil)
|
||||
@@ -544,6 +566,7 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
|
||||
progress.kind = .file
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS)
|
||||
var error: NSError?
|
||||
NSFileCoordinator().coordinate(readingItemAt: localFile, options: .forUploading, error: &error, byAccessor: { (url) in
|
||||
let task = self.session.uploadTask(with: request, fromFile: localFile)
|
||||
@@ -552,15 +575,66 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
|
||||
if let error = error {
|
||||
completionHandler?(error)
|
||||
}
|
||||
#else
|
||||
self.upload_task(targetPath, progress: progress, task: task, operation: operation, completionHandler: completionHandler)
|
||||
#endif
|
||||
|
||||
return progress
|
||||
}
|
||||
|
||||
internal func download(path: String, request: URLRequest, operation: FileOperationType,
|
||||
responseHandler: ((_ response: URLResponse) -> Void)? = nil,
|
||||
stream: OutputStream,
|
||||
completionHandler: @escaping (_ error: Error?) -> Void) -> Progress? {
|
||||
let progress = Progress(totalUnitCount: -1)
|
||||
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
||||
progress.kind = .file
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
||||
|
||||
let task = session.dataTask(with: request)
|
||||
if let responseHandler = responseHandler {
|
||||
responseCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { response in
|
||||
responseHandler(response)
|
||||
}
|
||||
}
|
||||
|
||||
stream.open()
|
||||
dataCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { [weak task, weak self] data in
|
||||
guard !data.isEmpty else { return }
|
||||
task.flatMap { self?.delegateNotify(operation, progress: Double($0.countOfBytesReceived) / Double($0.countOfBytesExpectedToReceive)) }
|
||||
|
||||
let result = (try? stream.write(data: data)) ?? -1
|
||||
if result < 0 {
|
||||
completionHandler(stream.streamError!)
|
||||
self?.delegateNotify(operation, error: stream.streamError!)
|
||||
task?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
|
||||
if error != nil {
|
||||
progress.cancel()
|
||||
}
|
||||
stream.close()
|
||||
completionHandler(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
|
||||
task.taskDescription = operation.json
|
||||
sessionDelegate?.observerProgress(of: task, using: progress, kind: .download)
|
||||
progress.cancellationHandler = { [weak task] in
|
||||
task?.cancel()
|
||||
}
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
task.resume()
|
||||
return progress
|
||||
}
|
||||
|
||||
internal func download_progressive(path: String, request: URLRequest, operation: FileOperationType,
|
||||
responseHandler: ((_ response: URLResponse) -> Void)? = nil,
|
||||
progressHandler: @escaping (_ data: Data) -> Void,
|
||||
completionHandler: @escaping (_ error: Error?) -> Void) -> Progress? {
|
||||
var progress = Progress(totalUnitCount: -1)
|
||||
let progress = Progress(totalUnitCount: -1)
|
||||
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
||||
progress.kind = .file
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
||||
@@ -586,8 +660,7 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
|
||||
}
|
||||
|
||||
task.taskDescription = operation.json
|
||||
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: &progress)
|
||||
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: &progress)
|
||||
sessionDelegate?.observerProgress(of: task, using: progress, kind: .download)
|
||||
progress.cancellationHandler = { [weak task] in
|
||||
task?.cancel()
|
||||
}
|
||||
@@ -596,20 +669,20 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
|
||||
return progress
|
||||
}
|
||||
|
||||
internal func download_simple(path: String, request: URLRequest, operation: FileOperationType,
|
||||
internal func download_file(path: String, request: URLRequest, operation: FileOperationType,
|
||||
completionHandler: @escaping ((_ tempURL: URL?, _ error: Error?) -> Void)) -> Progress? {
|
||||
var progress = Progress(totalUnitCount: -1)
|
||||
let progress = Progress(totalUnitCount: -1)
|
||||
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
||||
progress.kind = .file
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
||||
|
||||
let task = session.downloadTask(with: request)
|
||||
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
|
||||
if error != nil {
|
||||
if let error = error {
|
||||
progress.cancel()
|
||||
completionHandler(nil, error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
completionHandler(nil, error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { tempURL in
|
||||
guard let httpResponse = task.response as? HTTPURLResponse , httpResponse.statusCode < 300 else {
|
||||
@@ -627,8 +700,7 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
|
||||
completionHandler(tempURL, nil)
|
||||
}
|
||||
task.taskDescription = operation.json
|
||||
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: &progress)
|
||||
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: &progress)
|
||||
sessionDelegate?.observerProgress(of: task, using: progress, kind: .download)
|
||||
progress.cancellationHandler = { [weak task] in
|
||||
task?.cancel()
|
||||
}
|
||||
@@ -639,3 +711,7 @@ open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, Fi
|
||||
}
|
||||
|
||||
extension HTTPFileProvider: FileProvider { }
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS)
|
||||
extension HTTPFileProvider: FileProvideUndoable { }
|
||||
#endif
|
||||
|
||||
@@ -14,7 +14,7 @@ import Foundation
|
||||
|
||||
it uses `FileManager` foundation class with some additions like searching and reading a portion of file.
|
||||
*/
|
||||
open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
open class LocalFileProvider: NSObject, FileProvider, FileProviderMonitor, FileProviderSymbolicLink {
|
||||
open class var type: String { return "Local" }
|
||||
open fileprivate(set) var baseURL: URL?
|
||||
open var dispatch_queue: DispatchQueue
|
||||
@@ -105,21 +105,23 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
self.credential = nil
|
||||
self.isCoorinating = false
|
||||
|
||||
#if swift(>=3.1)
|
||||
let queueLabel = "FileProvider.\(Swift.type(of: self).type)"
|
||||
#else
|
||||
let queueLabel = "FileProvider.\(type(of: self).type)"
|
||||
#endif
|
||||
dispatch_queue = DispatchQueue(label: queueLabel, attributes: .concurrent)
|
||||
operation_queue = OperationQueue()
|
||||
operation_queue.name = "\(queueLabel).Operation"
|
||||
|
||||
super.init()
|
||||
|
||||
fileProviderManagerDelegate = LocalFileProviderManagerDelegate(provider: self)
|
||||
opFileManager.delegate = fileProviderManagerDelegate
|
||||
}
|
||||
|
||||
public required convenience init?(coder aDecoder: NSCoder) {
|
||||
guard let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL else {
|
||||
guard let baseURL = aDecoder.decodeObject(of: NSURL.self, forKey: "baseURL") as URL? else {
|
||||
if #available(macOS 10.11, iOS 9.0, tvOS 9.0, *) {
|
||||
aDecoder.failWithError(CocoaError(.coderValueNotFound,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Base URL is not set."]))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
self.init(baseURL: baseURL)
|
||||
@@ -158,7 +160,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
dispatch_queue.async {
|
||||
do {
|
||||
let contents = try self.fileManager.contentsOfDirectory(at: self.url(of: path), includingPropertiesForKeys: nil, options: .skipsSubdirectoryDescendants)
|
||||
let filesAttributes = contents.flatMap({ (fileURL) -> LocalFileObject? in
|
||||
let filesAttributes = contents.compactMap({ (fileURL) -> LocalFileObject? in
|
||||
let path = self.relativePathOf(url: fileURL)
|
||||
return LocalFileObject(fileWithPath: path, relativeTo: self.baseURL)
|
||||
})
|
||||
@@ -244,7 +246,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
|
||||
@discardableResult
|
||||
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let operation = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(folderName) + "/")
|
||||
let operation = FileOperationType.create(path: atPath.appendingPathComponent(folderName) + "/")
|
||||
return self.doOperation(operation, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@@ -278,15 +280,6 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
return self.doOperation(operation, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS)
|
||||
@objc dynamic func doSimpleOperation(_ box: UndoBox) {
|
||||
guard let _ = self.undoManager else { return }
|
||||
_ = self.doOperation(box.undoOperation) { (_) in
|
||||
return
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@discardableResult
|
||||
fileprivate func doOperation(_ operation: FileOperationType, data: Data? = nil, overwrite: Bool = true, atomically: Bool = false, forUploading: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let progress = Progress(totalUnitCount: -1)
|
||||
@@ -314,7 +307,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
|
||||
if !overwrite, let dest = dest, /* fileExists */ ((try? dest.checkResourceIsReachable()) ?? false) ||
|
||||
((try? dest.checkPromisedItemIsReachable()) ?? false) {
|
||||
let e = self.cocoaError(destPath!, code: .fileWriteFileExists)
|
||||
let e = CocoaError(.fileWriteFileExists, path: destPath!)
|
||||
dispatch_queue.async {
|
||||
completionHandler?(e)
|
||||
}
|
||||
@@ -323,14 +316,6 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS)
|
||||
if let undoManager = self.undoManager, let undoOp = self.undoOperation(for: operation) {
|
||||
let undoBox = UndoBox(provider: self, operation: operation, undoOperation: undoOp)
|
||||
undoManager.beginUndoGrouping()
|
||||
undoManager.registerUndo(withTarget: self, selector: #selector(LocalFileProvider.doSimpleOperation(_:)), object: undoBox)
|
||||
undoManager.setActionName(operation.actionDescription)
|
||||
undoManager.endUndoGrouping()
|
||||
}
|
||||
|
||||
var successfulSecurityScopedResourceAccess = false
|
||||
#endif
|
||||
|
||||
@@ -370,7 +355,10 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
source.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS)
|
||||
self._registerUndo(operation)
|
||||
#endif
|
||||
progress.completedUnitCount = progress.totalUnitCount
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(nil)
|
||||
@@ -508,7 +496,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
let operationHandler: (URL) -> Void = { url in
|
||||
do {
|
||||
guard let handle = FileHandle(forReadingAtPath: url.path) else {
|
||||
throw self.cocoaError(path, code: .fileNoSuchFile)
|
||||
throw CocoaError(.fileNoSuchFile, path: path)
|
||||
}
|
||||
|
||||
defer {
|
||||
@@ -519,13 +507,13 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
progress.totalUnitCount = size
|
||||
guard size > offset else {
|
||||
progress.cancel()
|
||||
throw self.cocoaError(path, code: .fileReadTooLarge)
|
||||
throw CocoaError(.fileReadTooLarge, path: path)
|
||||
}
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
handle.seek(toFileOffset: UInt64(offset))
|
||||
guard Int64(handle.offsetInFile) == offset else {
|
||||
progress.cancel()
|
||||
throw self.cocoaError(path, code: .fileReadTooLarge)
|
||||
throw CocoaError(.fileReadTooLarge, path: path)
|
||||
}
|
||||
|
||||
let data = handle.readData(ofLength: length)
|
||||
@@ -568,7 +556,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
let fileExists = ((try? self.url(of: path).checkResourceIsReachable()) ?? false) ||
|
||||
((try? self.url(of: path).checkPromisedItemIsReachable()) ?? false)
|
||||
if !overwrite && fileExists {
|
||||
let e = self.cocoaError(path, code: .fileWriteFileExists)
|
||||
let e = CocoaError(.fileWriteFileExists, path: path)
|
||||
dispatch_queue.async {
|
||||
completionHandler?(e)
|
||||
}
|
||||
@@ -580,24 +568,19 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
return self.doOperation(operation, data: data ?? Data(), atomically: atomically, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
fileprivate var monitors = [LocalFolderMonitor]()
|
||||
fileprivate var monitors = [LocalFileMonitor]()
|
||||
|
||||
open func registerNotifcation(path: String, eventHandler: @escaping (() -> Void)) {
|
||||
self.unregisterNotifcation(path: path)
|
||||
let dirurl = self.url(of: path)
|
||||
let isdir = (try? dirurl.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory ?? false
|
||||
if !isdir {
|
||||
return
|
||||
let url = self.url(of: path)
|
||||
if let monitor = LocalFileMonitor(url: url, handler: eventHandler) {
|
||||
monitor.start()
|
||||
monitors.append(monitor)
|
||||
}
|
||||
let monitor = LocalFolderMonitor(url: dirurl) {
|
||||
eventHandler()
|
||||
}
|
||||
monitor.start()
|
||||
monitors.append(monitor)
|
||||
}
|
||||
|
||||
open func unregisterNotifcation(path: String) {
|
||||
var removedMonitor: LocalFolderMonitor?
|
||||
var removedMonitor: LocalFileMonitor?
|
||||
for (i, monitor) in monitors.enumerated() {
|
||||
if self.relativePathOf(url: monitor.url) == path {
|
||||
removedMonitor = monitors.remove(at: i)
|
||||
@@ -611,22 +594,20 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
return monitors.map( { self.relativePathOf(url: $0.url) } ).contains(path.trimmingCharacters(in: CharacterSet(charactersIn: "/")))
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a symbolic link at the specified path that points to an item at the given path.
|
||||
This method does not traverse symbolic links contained in destination path, making it possible
|
||||
to create symbolic links to locations that do not yet exist.
|
||||
Also, if the final path component is a symbolic link, that link is not followed.
|
||||
|
||||
- Parameters:
|
||||
- symbolicLink: The file path at which to create the new symbolic link. The last component of the path issued as the name of the link.
|
||||
- withDestinationPath: The path that contains the item to be pointed to by the link. In other words, this is the destination of the link.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
*/
|
||||
open func create(symbolicLink path: String, withDestinationPath destPath: String, completionHandler: SimpleCompletionHandler) {
|
||||
operation_queue.addOperation {
|
||||
let operation = FileOperationType.link(link: path, target: destPath)
|
||||
do {
|
||||
try self.opFileManager.createSymbolicLink(at: self.url(of: path), withDestinationURL: self.url(of: destPath))
|
||||
let url = self.url(of: path)
|
||||
let destURL = self.url(of: destPath)
|
||||
let homePath = NSHomeDirectory()
|
||||
if destURL.path.hasPrefix(homePath) {
|
||||
let canonicalHomePath = "/" + homePath.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||||
let destRelativePath = destURL.path.replacingOccurrences(of: canonicalHomePath, with: "~", options: .anchored)
|
||||
try self.opFileManager.createSymbolicLink(atPath: url.path, withDestinationPath: destRelativePath)
|
||||
} else {
|
||||
try self.opFileManager.createSymbolicLink(at: url, withDestinationURL: destURL)
|
||||
}
|
||||
completionHandler?(nil)
|
||||
self.delegateNotify(operation)
|
||||
} catch {
|
||||
@@ -636,17 +617,13 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the path of the item pointed to by a symbolic link.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - path: The path of a file or directory.
|
||||
/// - completionHandler: Returns destination url of given symbolic link, or an `Error` object if it fails.
|
||||
open func destination(ofSymbolicLink path: String, completionHandler: @escaping (_ url: URL?, _ error: Error?) -> Void) {
|
||||
open func destination(ofSymbolicLink path: String, completionHandler: @escaping (_ file: FileObject?, _ error: Error?) -> Void) {
|
||||
dispatch_queue.async {
|
||||
do {
|
||||
let destPath = try self.opFileManager.destinationOfSymbolicLink(atPath: self.url(of: path).path)
|
||||
let destUrl = URL(fileURLWithPath: destPath)
|
||||
completionHandler(destUrl, nil)
|
||||
let absoluteDestPath = (destPath as NSString).expandingTildeInPath
|
||||
let file = LocalFileObject(fileWithPath: absoluteDestPath, relativeTo: self.baseURL)
|
||||
completionHandler(file, nil)
|
||||
} catch {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
@@ -655,7 +632,6 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS)
|
||||
|
||||
extension LocalFileProvider: FileProvideUndoable { }
|
||||
|
||||
internal extension LocalFileProvider {
|
||||
|
||||
+48
-30
@@ -17,12 +17,19 @@ public final class LocalFileObject: FileObject {
|
||||
/// Initiates a `LocalFileObject` with attributes of file in path.
|
||||
public convenience init? (fileWithPath path: String, relativeTo relativeURL: URL?) {
|
||||
var fileURL: URL?
|
||||
var rpath = path.replacingOccurrences(of: relativeURL?.path ?? "", with: "", options: .anchored).replacingOccurrences(of: "/", with: "", options: .anchored)
|
||||
var rpath = path.replacingOccurrences(of: "/", with: "", options: .anchored)
|
||||
var resolvedRelativeURL: URL?
|
||||
if let relPath = relativeURL?.path.replacingOccurrences(of: "/", with: "", options: .anchored), rpath.hasPrefix(relPath) {
|
||||
rpath = rpath.replacingOccurrences(of: relPath, with: "", options: .anchored).replacingOccurrences(of: "/", with: "", options: .anchored)
|
||||
resolvedRelativeURL = relativeURL
|
||||
} else {
|
||||
resolvedRelativeURL = relativeURL
|
||||
}
|
||||
if #available(iOS 9.0, macOS 10.11, *) {
|
||||
fileURL = URL(fileURLWithPath: rpath, relativeTo: relativeURL)
|
||||
fileURL = URL(fileURLWithPath: rpath, relativeTo: resolvedRelativeURL)
|
||||
} else {
|
||||
rpath = rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? rpath
|
||||
fileURL = URL(string: rpath, relativeTo: relativeURL) ?? relativeURL
|
||||
fileURL = URL(string: rpath, relativeTo: resolvedRelativeURL) ?? resolvedRelativeURL
|
||||
}
|
||||
|
||||
if let fileURL = fileURL {
|
||||
@@ -48,7 +55,7 @@ public final class LocalFileObject: FileObject {
|
||||
}
|
||||
|
||||
/// The total size allocated on disk for the file
|
||||
open internal(set) var allocatedSize: Int64 {
|
||||
public internal(set) var allocatedSize: Int64 {
|
||||
get {
|
||||
return allValues[.fileAllocatedSizeKey] as? Int64 ?? 0
|
||||
}
|
||||
@@ -60,7 +67,7 @@ public final class LocalFileObject: FileObject {
|
||||
/// The document identifier is a value assigned by the kernel/system to a file or directory.
|
||||
/// This value is used to identify the document regardless of where it is moved on a volume.
|
||||
/// The identifier persists across system restarts.
|
||||
open internal(set) var id: Int? {
|
||||
public internal(set) var id: Int? {
|
||||
get {
|
||||
return allValues[.documentIdentifierKey] as? Int
|
||||
}
|
||||
@@ -71,7 +78,7 @@ public final class LocalFileObject: FileObject {
|
||||
|
||||
/// The revision of file, which changes when a file contents are modified.
|
||||
/// Changes to attributes or other file metadata do not change the identifier.
|
||||
open var rev: String? {
|
||||
public var rev: String? {
|
||||
get {
|
||||
let data = allValues[.generationIdentifierKey] as? Data
|
||||
return data?.map { String(format: "%02hhx", $0) }.joined()
|
||||
@@ -79,7 +86,7 @@ public final class LocalFileObject: FileObject {
|
||||
}
|
||||
|
||||
/// Count of children items of a driectory. It costs disk access for local directories.
|
||||
open public(set) override var childrensCount: Int? {
|
||||
public override var childrensCount: Int? {
|
||||
get {
|
||||
return try? FileManager.default.contentsOfDirectory(atPath: self.url.path).count
|
||||
}
|
||||
@@ -89,24 +96,49 @@ public final class LocalFileObject: FileObject {
|
||||
}
|
||||
}
|
||||
|
||||
internal final class LocalFolderMonitor {
|
||||
public final class LocalFileMonitor {
|
||||
fileprivate let source: DispatchSourceFileSystemObject
|
||||
fileprivate let descriptor: CInt
|
||||
fileprivate let qq: DispatchQueue = DispatchQueue.global(qos: .default)
|
||||
fileprivate var state: Bool = false
|
||||
fileprivate var monitoredTime: TimeInterval = Date().timeIntervalSinceReferenceDate
|
||||
var url: URL
|
||||
|
||||
fileprivate var _monitoredTime: TimeInterval = Date().timeIntervalSinceReferenceDate
|
||||
fileprivate let _monitoredTimeLock = NSLock()
|
||||
fileprivate var monitoredTime: TimeInterval {
|
||||
get {
|
||||
_monitoredTimeLock.lock()
|
||||
defer {
|
||||
_monitoredTimeLock.unlock()
|
||||
}
|
||||
return _monitoredTime
|
||||
}
|
||||
set {
|
||||
_monitoredTimeLock.lock()
|
||||
defer {
|
||||
_monitoredTimeLock.unlock()
|
||||
}
|
||||
_monitoredTime = newValue
|
||||
}
|
||||
}
|
||||
|
||||
public var url: URL
|
||||
|
||||
/// Creates a folder monitor object with monitoring enabled.
|
||||
init(url: URL, handler: @escaping ()->Void) {
|
||||
public init?(url: URL, handler: @escaping ()->Void) {
|
||||
self.url = url
|
||||
descriptor = open((url as NSURL).fileSystemRepresentation, O_EVTONLY)
|
||||
source = DispatchSource.makeFileSystemObjectSource(fileDescriptor: descriptor, eventMask: .write, queue: qq)
|
||||
descriptor = url.absoluteURL.withUnsafeFileSystemRepresentation { rep in
|
||||
guard let rep = rep else { return -1 }
|
||||
return open(rep, O_EVTONLY)
|
||||
}
|
||||
guard descriptor >= 0 else { return nil }
|
||||
let event: DispatchSource.FileSystemEvent = url.fileIsDirectory ? [.write] : .all
|
||||
source = DispatchSource.makeFileSystemObjectSource(fileDescriptor: descriptor, eventMask: event, queue: qq)
|
||||
|
||||
// Folder monitoring is recursive and deep. Monitoring a root folder may be very costly
|
||||
// We have a 0.2 second delay to ensure we wont call handler 1000s times when there is
|
||||
// a huge file operation. This ensures app will work smoothly while this 250 milisec won't
|
||||
// affect user experince much
|
||||
let main_handler: ()->Void = { [weak self] in
|
||||
let main_handler: DispatchSourceProtocol.DispatchSourceHandler = { [weak self] in
|
||||
guard let `self` = self else { return }
|
||||
if Date().timeIntervalSinceReferenceDate < self.monitoredTime + 0.2 {
|
||||
return
|
||||
@@ -126,7 +158,7 @@ internal final class LocalFolderMonitor {
|
||||
}
|
||||
|
||||
/// Starts sending notifications if currently stopped
|
||||
func start() {
|
||||
public func start() {
|
||||
if !state {
|
||||
state = true
|
||||
source.resume()
|
||||
@@ -134,7 +166,7 @@ internal final class LocalFolderMonitor {
|
||||
}
|
||||
|
||||
/// Stops sending notifications if currently enabled
|
||||
func stop() {
|
||||
public func stop() {
|
||||
if state {
|
||||
state = false
|
||||
source.suspend()
|
||||
@@ -223,17 +255,3 @@ internal class LocalFileProviderManagerDelegate: NSObject, FileManagerDelegate {
|
||||
return delegate.fileProvider(provider, shouldProceedAfterError: error, operation: .link(link: srcPath, target: dstPath))
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS)
|
||||
class UndoBox: NSObject {
|
||||
weak var provider: FileProvideUndoable?
|
||||
let operation: FileOperationType
|
||||
let undoOperation: FileOperationType
|
||||
|
||||
init(provider: FileProvideUndoable, operation: FileOperationType, undoOperation: FileOperationType) {
|
||||
self.provider = provider
|
||||
self.operation = operation
|
||||
self.undoOperation = undoOperation
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -101,7 +101,7 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
public static var graphVersion = "v1.0"
|
||||
|
||||
/// Route for container, default is `.me`.
|
||||
open let route: Route
|
||||
public let route: Route
|
||||
|
||||
/**
|
||||
Initializer for Onedrive provider with given client ID and Token.
|
||||
@@ -146,13 +146,13 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
|
||||
public required convenience init?(coder aDecoder: NSCoder) {
|
||||
let route: Route
|
||||
if let driveId = aDecoder.decodeObject(forKey: "drive") as? String, let uuid = UUID(uuidString: driveId) {
|
||||
if let driveId = aDecoder.decodeObject(of: NSString.self, forKey: "drive") as String?, let uuid = UUID(uuidString: driveId) {
|
||||
route = .drive(uuid: uuid)
|
||||
} else {
|
||||
route = (aDecoder.decodeObject(forKey: "route") as? String).flatMap({ Route(rawValue: $0) }) ?? .me
|
||||
route = (aDecoder.decodeObject(of: NSString.self, forKey: "route") as String?).flatMap({ Route(rawValue: $0) }) ?? .me
|
||||
}
|
||||
self.init(credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential,
|
||||
serverURL: aDecoder.decodeObject(forKey: "baseURL") as? URL,
|
||||
self.init(credential: aDecoder.decodeObject(of: URLCredential.self, forKey: "credential"),
|
||||
serverURL: aDecoder.decodeObject(of: NSURL.self, forKey: "baseURL") as URL?,
|
||||
route: route)
|
||||
self.useCache = aDecoder.decodeBool(forKey: "useCache")
|
||||
self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache")
|
||||
@@ -194,14 +194,14 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
}, pageHandler: { [weak self] (data, _) -> (files: [FileObject], error: Error?, newToken: String?) in
|
||||
guard let `self` = self else { return ([], nil, nil) }
|
||||
|
||||
guard let json = data?.deserializeJSON(), let entries = json["value"] as? [AnyObject] else {
|
||||
let err = self.urlError(path, code: .badServerResponse)
|
||||
guard let json = data?.deserializeJSON(), let entries = json["value"] as? [Any] else {
|
||||
let err = URLError(.badServerResponse, url: self.url(of: path))
|
||||
return ([], err, nil)
|
||||
}
|
||||
|
||||
var files = [FileObject]()
|
||||
for entry in entries {
|
||||
if let entry = entry as? [String: AnyObject], let file = OneDriveFileObject(baseURL: self.baseURL, route: self.route, json: entry) {
|
||||
if let entry = entry as? [String: Any], let file = OneDriveFileObject(baseURL: self.baseURL, route: self.route, json: entry) {
|
||||
files.append(file)
|
||||
}
|
||||
}
|
||||
@@ -227,12 +227,12 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderHTTPError?
|
||||
var fileObject: OneDriveFileObject?
|
||||
if let response = response as? HTTPURLResponse {
|
||||
if let response = response as? HTTPURLResponse, response.statusCode >= 400 {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
|
||||
serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) }
|
||||
if let json = data?.deserializeJSON(), let file = OneDriveFileObject(baseURL: self.baseURL, route: self.route, json: json) {
|
||||
fileObject = file
|
||||
}
|
||||
}
|
||||
if let json = data?.deserializeJSON(), let file = OneDriveFileObject(baseURL: self.baseURL, route: self.route, json: json) {
|
||||
fileObject = file
|
||||
}
|
||||
completionHandler(fileObject, serverError ?? error)
|
||||
})
|
||||
@@ -257,8 +257,9 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
volume.uuid = json["id"] as? String
|
||||
volume.name = json["name"] as? String
|
||||
volume.creationDate = (json["createdDateTime"] as? String).flatMap { Date(rfcString: $0) }
|
||||
volume.totalCapacity = (json["quota"]?["total"] as? NSNumber)?.int64Value ?? -1
|
||||
volume.availableCapacity = (json["quota"]?["remaining"] as? NSNumber)?.int64Value ?? 0
|
||||
let quota = json["quota"] as? [String: Any]
|
||||
volume.totalCapacity = (quota?["total"] as? NSNumber)?.int64Value ?? -1
|
||||
volume.availableCapacity = (quota?["remaining"] as? NSNumber)?.int64Value ?? 0
|
||||
completionHandler(volume)
|
||||
})
|
||||
task.resume()
|
||||
@@ -290,7 +291,7 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
*/
|
||||
@discardableResult
|
||||
open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? {
|
||||
let queryStr = query.findValue(forKey: "name") as? String ?? query.findAllValues(forKey: nil).flatMap { $0.value as? String }.first
|
||||
let queryStr = query.findValue(forKey: "name") as? String ?? query.findAllValues(forKey: nil).compactMap { $0.value as? String }.first
|
||||
|
||||
return paginated(path, requestHandler: { [weak self] (token) -> URLRequest? in
|
||||
guard let `self` = self else { return nil }
|
||||
@@ -314,14 +315,14 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
return request
|
||||
}, pageHandler: { [weak self] (data, progress) -> (files: [FileObject], error: Error?, newToken: String?) in
|
||||
guard let `self` = self else { return ([], nil, nil) }
|
||||
guard let json = data?.deserializeJSON(), let entries = json["value"] as? [AnyObject] else {
|
||||
let err = self.urlError(path, code: .badServerResponse)
|
||||
guard let json = data?.deserializeJSON(), let entries = json["value"] as? [Any] else {
|
||||
let err = URLError(.badServerResponse, url: self.url(of: path))
|
||||
return ([], err, nil)
|
||||
}
|
||||
|
||||
var foundFiles = [FileObject]()
|
||||
for entry in entries {
|
||||
if let entry = entry as? [String: AnyObject], let file = OneDriveFileObject(baseURL: self.baseURL, route: self.route, json: entry), query.evaluate(with: file.mapPredicate()) {
|
||||
if let entry = entry as? [String: Any], let file = OneDriveFileObject(baseURL: self.baseURL, route: self.route, json: entry), query.evaluate(with: file.mapPredicate()) {
|
||||
foundFiles.append(file)
|
||||
foundItemHandler?(file)
|
||||
}
|
||||
@@ -378,6 +379,33 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
task.resume()
|
||||
}
|
||||
|
||||
/*internal var cachedDriveID: String?
|
||||
|
||||
override func doOperation(_ operation: FileOperationType, overwrite: Bool, progress: Progress?, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
switch operation {
|
||||
case .copy(source: let source, destination: let dest) where !source.hasPrefix("file://") && !dest.hasPrefix("file://"):
|
||||
fallthrough
|
||||
case .move:
|
||||
if self.cachedDriveID != nil {
|
||||
return super.doOperation(operation, overwrite: overwrite, progress: progress, completionHandler: completionHandler)
|
||||
} else {
|
||||
let progress = Progress(totalUnitCount: 1)
|
||||
self.storageProperties(completionHandler: { (volume) in
|
||||
if let volumeId = volume?.uuid {
|
||||
self.cachedDriveID = volumeId
|
||||
_ = super.doOperation(operation, overwrite: overwrite, progress: progress, completionHandler: completionHandler)
|
||||
} else {
|
||||
let error = self.urlError(operation.source, code: .badServerResponse)
|
||||
completionHandler?(error)
|
||||
}
|
||||
})
|
||||
return progress
|
||||
}
|
||||
default:
|
||||
return super.doOperation(operation, overwrite: overwrite, progress: progress, completionHandler: completionHandler)
|
||||
}
|
||||
}*/
|
||||
|
||||
/**
|
||||
Uploads a file from local file url to designated path asynchronously.
|
||||
Method will fail if source is not a local url with `file://` scheme.
|
||||
@@ -396,7 +424,7 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
// check file is not a folder
|
||||
guard (try? localFile.resourceValues(forKeys: [.fileResourceTypeKey]))?.fileResourceType ?? .unknown == .regular else {
|
||||
dispatch_queue.async {
|
||||
completionHandler?(self.urlError(localFile.path, code: .fileIsDirectory))
|
||||
completionHandler?(URLError(.fileIsDirectory, url: localFile))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -449,7 +477,7 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
url = self.url(of: path, modifier: "content")
|
||||
case .create(path: let path) where path.hasSuffix("/"):
|
||||
method = "POST"
|
||||
let parent = (path as NSString).deletingLastPathComponent
|
||||
let parent = path.deletingLastPathComponent
|
||||
url = self.url(of: parent, modifier: "children")
|
||||
case .modify(path: let path):
|
||||
method = "PUT"
|
||||
@@ -486,32 +514,32 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
switch operation {
|
||||
case .create(path: let path) where path.hasSuffix("/"):
|
||||
request.setValue(contentType: .json)
|
||||
var requestDictionary = [String: AnyObject]()
|
||||
let name = (path as NSString).lastPathComponent
|
||||
requestDictionary["name"] = name as NSString
|
||||
requestDictionary["folder"] = NSDictionary()
|
||||
requestDictionary["@microsoft.graph.conflictBehavior"] = "fail" as NSString
|
||||
var requestDictionary = [String: Any]()
|
||||
let name = path.lastPathComponent
|
||||
requestDictionary["name"] = name
|
||||
requestDictionary["folder"] = [String: Any]()
|
||||
requestDictionary["@microsoft.graph.conflictBehavior"] = "fail"
|
||||
request.httpBody = Data(jsonDictionary: requestDictionary)
|
||||
case .copy(let source, let dest) where !source.hasPrefix("file://") && !dest.hasPrefix("file://"),
|
||||
.move(source: let source, destination: let dest):
|
||||
request.setValue(contentType: .json, charset: .utf8)
|
||||
let cdest = correctPath(dest) as NSString
|
||||
var parentReference: [String: AnyObject] = [:]
|
||||
let cdest = correctPath(dest)
|
||||
var parentReference: [String: Any] = [:]
|
||||
if cdest.hasPrefix("id:") {
|
||||
parentReference["id"] = cdest.components(separatedBy: "/").first?.replacingOccurrences(of: "id:", with: "", options: .anchored) as NSString?
|
||||
parentReference["id"] = cdest.components(separatedBy: "/").first?.replacingOccurrences(of: "id:", with: "", options: .anchored)
|
||||
} else {
|
||||
parentReference["path"] = ("/drive/root:" as NSString).appendingPathComponent(cdest.deletingLastPathComponent) as NSString
|
||||
parentReference["path"] = "/drive/root:".appendingPathComponent(cdest.deletingLastPathComponent)
|
||||
}
|
||||
switch self.route {
|
||||
case .drive(uuid: let uuid):
|
||||
parentReference["driveId"] = uuid.uuidString as NSString
|
||||
parentReference["driveId"] = uuid.uuidString
|
||||
default:
|
||||
//parentReference["driveId"] = cachedDriveID as NSString? ?? ""
|
||||
//parentReference["driveId"] = cachedDriveID ?? ""
|
||||
break
|
||||
}
|
||||
var requestDictionary = [String: AnyObject]()
|
||||
requestDictionary["parentReference"] = parentReference as NSDictionary
|
||||
requestDictionary["name"] = (cdest as NSString).lastPathComponent as NSString
|
||||
var requestDictionary = [String: Any]()
|
||||
requestDictionary["parentReference"] = parentReference
|
||||
requestDictionary["name"] = cdest.lastPathComponent
|
||||
request.httpBody = Data(jsonDictionary: requestDictionary)
|
||||
default:
|
||||
break
|
||||
@@ -523,7 +551,7 @@ open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
override func serverError(with code: FileProviderHTTPErrorCode, path: String?, data: Data?) -> FileProviderHTTPError {
|
||||
let errorDesc: String?
|
||||
if let response = data?.deserializeJSON() {
|
||||
errorDesc = response["error"]?["message"] as? String
|
||||
errorDesc = (response["error"] as? [String: Any])?["message"] as? String
|
||||
} else {
|
||||
errorDesc = data.flatMap({ String(data: $0, encoding: .utf8) })
|
||||
}
|
||||
@@ -550,21 +578,20 @@ open class OneDriveFileProvider: HTTPFileProvider, 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: "createLink"))
|
||||
request.httpMethod = "POST"
|
||||
let requestDictionary: [String: AnyObject] = ["type": "view" as NSString]
|
||||
let requestDictionary: [String: Any] = ["type": "view"]
|
||||
request.httpBody = Data(jsonDictionary: requestDictionary)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderHTTPError?
|
||||
var link: URL?
|
||||
if let response = response as? HTTPURLResponse {
|
||||
if let response = response as? HTTPURLResponse, response.statusCode >= 400 {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
|
||||
serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) }
|
||||
if let json = data?.deserializeJSON() {
|
||||
if let linkDic = json["link"] as? NSDictionary, let linkStr = linkDic["webUrl"] as? String {
|
||||
link = URL(string: linkStr)
|
||||
}
|
||||
}
|
||||
if let json = data?.deserializeJSON() {
|
||||
if let linkDic = json["link"] as? [String: Any], let linkStr = linkDic["webUrl"] as? String {
|
||||
link = URL(string: linkStr)
|
||||
}
|
||||
}
|
||||
|
||||
completionHandler(link, nil, nil, serverError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
@@ -589,9 +616,9 @@ extension OneDriveFileProvider: ExtendedFileProvider {
|
||||
if let response = response as? HTTPURLResponse {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
|
||||
serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) }
|
||||
if let json = data?.deserializeJSON() {
|
||||
(dic, keys) = self.mapMediaInfo(json)
|
||||
}
|
||||
}
|
||||
if let json = data?.deserializeJSON() {
|
||||
(dic, keys) = self.mapMediaInfo(json)
|
||||
}
|
||||
completionHandler(dic, keys, serverError ?? error)
|
||||
})
|
||||
@@ -601,7 +628,7 @@ extension OneDriveFileProvider: ExtendedFileProvider {
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS)
|
||||
open func thumbnailOfFileSupported(path: String) -> Bool {
|
||||
let fileExt = (path as NSString).pathExtension.lowercased()
|
||||
let fileExt = path.pathExtension.lowercased()
|
||||
switch fileExt {
|
||||
case "jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff":
|
||||
return true
|
||||
@@ -624,6 +651,11 @@ extension OneDriveFileProvider: ExtendedFileProvider {
|
||||
case 97...176: thumbQuery = "medium"
|
||||
default: thumbQuery = "large"
|
||||
}
|
||||
/*if let dimension = dimension {
|
||||
thumbQuery = "c\(Int(dimension.width))x\(Int(dimension.height))"
|
||||
} else {
|
||||
thumbQuery = "small"
|
||||
}*/
|
||||
let url = self.url(of: path, modifier: "thumbnails")
|
||||
.appendingPathComponent("0").appendingPathComponent(thumbQuery)
|
||||
.appendingPathComponent("content")
|
||||
|
||||
@@ -22,22 +22,18 @@ public final class OneDriveFileObject: FileObject {
|
||||
self.init(baseURL: baseURL, route: route, json: json)
|
||||
}
|
||||
|
||||
internal init? (baseURL: URL?, route: OneDriveFileProvider.Route, json: [String: AnyObject]) {
|
||||
internal init? (baseURL: URL?, route: OneDriveFileProvider.Route, json: [String: Any]) {
|
||||
guard let name = json["name"] as? String else { return nil }
|
||||
guard let id = json["id"] as? String else { return nil }
|
||||
let path: String
|
||||
if let refpath = json["parentReference"]?["path"] as? String {
|
||||
if let refpath = (json["parentReference"] as? [String: Any])?["path"] as? String {
|
||||
let parentPath: String
|
||||
if let colonIndex = refpath.index(of: ":") {
|
||||
#if swift(>=4.0)
|
||||
if let colonIndex = refpath.firstIndex(of: ":") {
|
||||
parentPath = String(refpath[refpath.index(after: colonIndex)...])
|
||||
#else
|
||||
parentPath = refpath.substring(from: refpath.index(after: colonIndex))
|
||||
#endif
|
||||
} else {
|
||||
parentPath = refpath
|
||||
}
|
||||
path = (parentPath as NSString).appendingPathComponent(name)
|
||||
path = parentPath.appendingPathComponent(name)
|
||||
} else {
|
||||
path = "id:\(id)"
|
||||
}
|
||||
@@ -45,20 +41,20 @@ public final class OneDriveFileObject: FileObject {
|
||||
super.init(url: url, name: name, path: path)
|
||||
self.id = id
|
||||
self.size = (json["size"] as? NSNumber)?.int64Value ?? -1
|
||||
self.childrensCount = json["folder"]?["childCount"] as? Int
|
||||
self.childrensCount = (json["folder"] as? [String: Any])?["childCount"] as? Int
|
||||
self.modifiedDate = (json["lastModifiedDateTime"] as? String).flatMap { Date(rfcString: $0) }
|
||||
self.creationDate = (json["createdDateTime"] as? String).flatMap { Date(rfcString: $0) }
|
||||
self.type = json["folder"] != nil ? .directory : .regular
|
||||
self.contentType = (json["file"]?["mimeType"] as? String).flatMap(ContentMIMEType.init(rawValue:)) ?? .stream
|
||||
self.contentType = ((json["file"] as? [String: Any])?["mimeType"] as? String).flatMap(ContentMIMEType.init(rawValue:)) ?? .stream
|
||||
self.entryTag = json["eTag"] as? String
|
||||
let hashes = json["file"]?["hashes"] as? NSDictionary
|
||||
let hashes = (json["file"] as? [String: Any])?["hashes"] as? [String: Any]
|
||||
// checks for both sha1 or quickXor. First is available in personal drives, second in business one.
|
||||
self.hash = (hashes?["sha1Hash"] as? String) ?? (hashes?["quickXorHash"] as? String)
|
||||
self.fileHash = (hashes?["sha1Hash"] as? String) ?? (hashes?["quickXorHash"] as? String)
|
||||
}
|
||||
|
||||
/// The document identifier is a value assigned by the OneDrive to a file.
|
||||
/// This value is used to identify the document regardless of where it is moved on a volume.
|
||||
open internal(set) var id: String? {
|
||||
public internal(set) var id: String? {
|
||||
get {
|
||||
return allValues[.fileResourceIdentifierKey] as? String
|
||||
}
|
||||
@@ -68,7 +64,7 @@ public final class OneDriveFileObject: FileObject {
|
||||
}
|
||||
|
||||
/// MIME type of file contents returned by OneDrive server.
|
||||
open internal(set) var contentType: ContentMIMEType {
|
||||
public internal(set) var contentType: ContentMIMEType {
|
||||
get {
|
||||
return (allValues[.mimeTypeKey] as? String).flatMap(ContentMIMEType.init(rawValue:)) ?? .stream
|
||||
}
|
||||
@@ -78,7 +74,7 @@ public final class OneDriveFileObject: FileObject {
|
||||
}
|
||||
|
||||
/// HTTP E-Tag, can be used to mark changed files.
|
||||
open internal(set) var entryTag: String? {
|
||||
public internal(set) var entryTag: String? {
|
||||
get {
|
||||
return allValues[.entryTagKey] as? String
|
||||
}
|
||||
@@ -88,7 +84,7 @@ public final class OneDriveFileObject: FileObject {
|
||||
}
|
||||
|
||||
/// Calculated hash from OneDrive server. Hex string SHA1 in personal or Base65 string [QuickXOR](https://dev.onedrive.com/snippets/quickxorhash.htm) in business drives.
|
||||
open internal(set) var hash: String? {
|
||||
public internal(set) var fileHash: String? {
|
||||
get {
|
||||
return allValues[.documentIdentifierKey] as? String
|
||||
}
|
||||
@@ -141,7 +137,7 @@ public final class OneDriveFileObject: FileObject {
|
||||
|
||||
switch crudePath {
|
||||
case hasPrefix("items/"):
|
||||
let components = (crudePath as NSString).pathComponents
|
||||
let components = crudePath.components(separatedBy: "/")
|
||||
return components.dropFirst().first.map { "id:\($0)" } ?? ""
|
||||
case hasPrefix("root:"):
|
||||
return crudePath.components(separatedBy: ":").dropFirst().first ?? ""
|
||||
@@ -151,8 +147,8 @@ public final class OneDriveFileObject: FileObject {
|
||||
}
|
||||
}
|
||||
|
||||
internal extension OneDriveFileProvider {
|
||||
internal func upload_multipart_data(_ targetPath: String, data: Data, operation: FileOperationType,
|
||||
extension OneDriveFileProvider {
|
||||
func upload_multipart_data(_ targetPath: String, data: Data, operation: FileOperationType,
|
||||
overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
return self.upload_multipart(targetPath, operation: operation, size: Int64(data.count), overwrite: overwrite, dataProvider: {
|
||||
let range = $0.clamped(to: 0..<Int64(data.count))
|
||||
@@ -160,13 +156,13 @@ internal extension OneDriveFileProvider {
|
||||
}, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
internal func upload_multipart_file(_ targetPath: String, file: URL, operation: FileOperationType,
|
||||
func upload_multipart_file(_ targetPath: String, file: URL, operation: FileOperationType,
|
||||
overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
// upload task can't handle uploading file
|
||||
|
||||
return self.upload_multipart(targetPath, operation: operation, size: file.fileSize, overwrite: overwrite, dataProvider: { range in
|
||||
guard let handle = FileHandle(forReadingAtPath: file.path) else {
|
||||
throw self.cocoaError(targetPath, code: .fileNoSuchFile)
|
||||
throw CocoaError(.fileNoSuchFile, path: targetPath)
|
||||
}
|
||||
|
||||
defer {
|
||||
@@ -176,7 +172,7 @@ internal extension OneDriveFileProvider {
|
||||
let offset = range.lowerBound
|
||||
handle.seek(toFileOffset: UInt64(offset))
|
||||
guard Int64(handle.offsetInFile) == offset else {
|
||||
throw self.cocoaError(targetPath, code: .fileReadTooLarge)
|
||||
throw CocoaError(.fileReadTooLarge, path: targetPath)
|
||||
}
|
||||
|
||||
return handle.readData(ofLength: range.count)
|
||||
@@ -221,7 +217,6 @@ internal extension OneDriveFileProvider {
|
||||
private func upload_multipart(url: URL, operation: FileOperationType, size: Int64, range: Range<Int64>? = nil, uploadedSoFar: Int64 = 0,
|
||||
progress: Progress, dataProvider: @escaping (Range<Int64>) throws -> Data, completionHandler: SimpleCompletionHandler) {
|
||||
guard !progress.isCancelled else { return }
|
||||
var progress = progress
|
||||
|
||||
let maximumSize: Int64 = 10_485_760 // Recommended by OneDrive documentations and divides evenly by 320 KiB, max 60MiB.
|
||||
var request = URLRequest(url: url)
|
||||
@@ -252,13 +247,13 @@ internal extension OneDriveFileProvider {
|
||||
}
|
||||
let task = session.uploadTask(with: request, from: data)
|
||||
|
||||
var dictionary: [String: AnyObject] = ["type": operation.description as NSString]
|
||||
dictionary["source"] = operation.source as NSString?
|
||||
dictionary["dest"] = operation.destination as NSString?
|
||||
dictionary["uploadedBytes"] = uploadedSoFar as NSNumber
|
||||
dictionary["totalBytes"] = data.count as NSNumber
|
||||
var dictionary: [String: Any] = ["type": operation.description]
|
||||
dictionary["source"] = operation.source
|
||||
dictionary["dest"] = operation.destination
|
||||
dictionary["uploadedBytes"] = NSNumber(value: uploadedSoFar)
|
||||
dictionary["totalBytes"] = NSNumber(value: data.count)
|
||||
task.taskDescription = String(jsonDictionary: dictionary)
|
||||
task.addObserver(self.sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent), options: .new, context: &progress)
|
||||
sessionDelegate?.observerProgress(of: task, using: progress, kind: .upload)
|
||||
progress.cancellationHandler = { [weak task, weak self] in
|
||||
task?.cancel()
|
||||
var deleteRequest = URLRequest(url: url)
|
||||
@@ -339,7 +334,7 @@ internal extension OneDriveFileProvider {
|
||||
var keys = [String]()
|
||||
|
||||
func add(key: String, value: Any?) {
|
||||
if let value = value {
|
||||
if let value = value, !((value as? String)?.isEmpty ?? false) {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
}
|
||||
|
||||
+83
-73
@@ -26,8 +26,8 @@ extension FileProviderHTTPError {
|
||||
return "Status \(code.rawValue): \(code.description)"
|
||||
}
|
||||
|
||||
public var errorDescription: String? {
|
||||
return "Status \(code.rawValue): \(code.description)"
|
||||
public var localizedDescription: String {
|
||||
return description
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,57 +61,93 @@ 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)
|
||||
}
|
||||
public enum ObserveKind {
|
||||
case upload
|
||||
case download
|
||||
}
|
||||
|
||||
private let observeProgressesLock = NSLock()
|
||||
private var observeProgresses = [(task: URLSessionTask, progress: Progress, kind: ObserveKind)]()
|
||||
|
||||
public func observerProgress(of task: URLSessionTask, using: Progress, kind: ObserveKind) {
|
||||
switch kind {
|
||||
case .upload:
|
||||
task.addObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent), options: .new, context: nil)
|
||||
case .download:
|
||||
task.addObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: nil)
|
||||
task.addObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: nil)
|
||||
}
|
||||
observeProgressesLock.lock()
|
||||
observeProgresses.append((task, using, kind))
|
||||
observeProgressesLock.unlock()
|
||||
}
|
||||
|
||||
func removeObservers(for task: URLSessionTask) {
|
||||
observeProgressesLock.lock()
|
||||
observeProgresses = observeProgresses.filter { (item) -> Bool in
|
||||
if item.task == task {
|
||||
switch item.kind {
|
||||
case .upload:
|
||||
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent))
|
||||
case .download:
|
||||
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived))
|
||||
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive))
|
||||
}
|
||||
case #keyPath(URLSessionTask.countOfBytesSent):
|
||||
if let startTime = progress.userInfo[ProgressUserInfoKey.startingTimeKey] as? Date, let task = object as? URLSessionTask {
|
||||
let elapsed = Date().timeIntervalSince(startTime)
|
||||
let throughput = Double(newVal) / elapsed
|
||||
progress.setUserInfoObject(NSNumber(value: throughput), forKey: .throughputKey)
|
||||
|
||||
// wokaround for multipart uploading
|
||||
let json = task.taskDescription?.deserializeJSON()
|
||||
let uploadedBytes = ((json?["uploadedBytes"] as? Int64) ?? 0) + newVal
|
||||
let totalBytes = (json?["totalBytes"] as? Int64) ?? task.countOfBytesExpectedToSend
|
||||
progress.completedUnitCount = uploadedBytes
|
||||
if totalBytes > 0 {
|
||||
let remain = totalBytes - uploadedBytes
|
||||
let estimatedTimeRemaining = Double(remain) / elapsed
|
||||
progress.setUserInfoObject(NSNumber(value: estimatedTimeRemaining), forKey: .estimatedTimeRemainingKey)
|
||||
}
|
||||
} else {
|
||||
progress.completedUnitCount = newVal
|
||||
}
|
||||
case #keyPath(URLSessionTask.countOfBytesExpectedToReceive), #keyPath(URLSessionTask.countOfBytesExpectedToSend):
|
||||
progress.totalUnitCount = newVal
|
||||
default:
|
||||
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
observeProgressesLock.unlock()
|
||||
}
|
||||
|
||||
public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
guard let context = context, let keyPath = keyPath, keyPath.contains("countOfBytes") else { return }
|
||||
let progress = context.assumingMemoryBound(to: Progress.self).pointee
|
||||
guard let newVal = change?[.newKey] as? Int64 else { return }
|
||||
|
||||
switch keyPath {
|
||||
case #keyPath(URLSessionTask.countOfBytesReceived):
|
||||
progress.completedUnitCount = newVal
|
||||
if let startTime = progress.userInfo[ProgressUserInfoKey.startingTimeKey] as? Date, let task = object as? URLSessionTask {
|
||||
let elapsed = Date().timeIntervalSince(startTime)
|
||||
let throughput = Double(newVal) / elapsed
|
||||
progress.setUserInfoObject(NSNumber(value: throughput), forKey: .throughputKey)
|
||||
if task.countOfBytesExpectedToReceive > 0 {
|
||||
let remain = task.countOfBytesExpectedToReceive - task.countOfBytesReceived
|
||||
let estimatedTimeRemaining = Double(remain) / elapsed
|
||||
progress.setUserInfoObject(NSNumber(value: estimatedTimeRemaining), forKey: .estimatedTimeRemainingKey)
|
||||
}
|
||||
}
|
||||
case #keyPath(URLSessionTask.countOfBytesSent):
|
||||
if let startTime = progress.userInfo[ProgressUserInfoKey.startingTimeKey] as? Date, let task = object as? URLSessionTask {
|
||||
let elapsed = Date().timeIntervalSince(startTime)
|
||||
let throughput = Double(newVal) / elapsed
|
||||
progress.setUserInfoObject(NSNumber(value: throughput), forKey: .throughputKey)
|
||||
|
||||
// wokaround for multipart uploading
|
||||
let json = task.taskDescription?.deserializeJSON()
|
||||
let uploadedBytes = ((json?["uploadedBytes"] as? Int64) ?? 0) + newVal
|
||||
let totalBytes = (json?["totalBytes"] as? Int64) ?? task.countOfBytesExpectedToSend
|
||||
progress.completedUnitCount = uploadedBytes
|
||||
if totalBytes > 0 {
|
||||
let remain = totalBytes - uploadedBytes
|
||||
let estimatedTimeRemaining = Double(remain) / elapsed
|
||||
progress.setUserInfoObject(NSNumber(value: estimatedTimeRemaining), forKey: .estimatedTimeRemainingKey)
|
||||
}
|
||||
} else {
|
||||
progress.completedUnitCount = newVal
|
||||
}
|
||||
case #keyPath(URLSessionTask.countOfBytesExpectedToReceive), #keyPath(URLSessionTask.countOfBytesExpectedToSend):
|
||||
progress.totalUnitCount = newVal
|
||||
default:
|
||||
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
||||
}
|
||||
}
|
||||
|
||||
// codebeat:disable[ARITY]
|
||||
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
||||
if task is URLSessionUploadTask {
|
||||
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent))
|
||||
//task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToSend))
|
||||
} else if task is URLSessionDownloadTask {
|
||||
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived))
|
||||
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive))
|
||||
}
|
||||
self.removeObservers(for: task)
|
||||
|
||||
_ = dataCompletionHandlersForTasks[session.sessionDescription!]?.removeValue(forKey: task.taskIdentifier)
|
||||
if !(error == nil && task is URLSessionDownloadTask) {
|
||||
@@ -119,32 +155,6 @@ final public class SessionDelegate: NSObject, URLSessionDataDelegate, URLSession
|
||||
completionHandler?(error)
|
||||
_ = completionHandlersForTasks[session.sessionDescription!]?.removeValue(forKey: task.taskIdentifier)
|
||||
}
|
||||
|
||||
guard let json = task.taskDescription?.deserializeJSON(),
|
||||
let op = FileOperationType(json: json), let fileProvider = fileProvider else {
|
||||
return
|
||||
}
|
||||
|
||||
switch op {
|
||||
case .fetch:
|
||||
if task is URLSessionDataTask {
|
||||
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived))
|
||||
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive))
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if !(task is URLSessionDownloadTask), case FileOperationType.fetch = op {
|
||||
return
|
||||
}
|
||||
if #available(iOS 9.0, macOS 10.11, *) {
|
||||
if task is URLSessionStreamTask {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fileProvider.delegateNotify(op, error: error)
|
||||
}
|
||||
|
||||
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
|
||||
@@ -216,7 +226,7 @@ final public class SessionDelegate: NSObject, URLSessionDataDelegate, URLSession
|
||||
authenticate(didReceive: challenge, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
func authenticate(didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
||||
private func authenticate(didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
||||
switch (challenge.previousFailureCount, credential != nil) {
|
||||
case (0...1, true):
|
||||
completionHandler(.useCredential, credential)
|
||||
|
||||
@@ -28,7 +28,7 @@ class SMBClient: NSObject, StreamDelegate {
|
||||
public var timeout: TimeInterval = 30
|
||||
|
||||
internal private(set) var messageId: UInt64 = 0
|
||||
private func createMessageId() -> UInt64 {
|
||||
fileprivate func createMessageId() -> UInt64 {
|
||||
defer {
|
||||
messageId += 1
|
||||
}
|
||||
@@ -36,7 +36,7 @@ class SMBClient: NSObject, StreamDelegate {
|
||||
}
|
||||
|
||||
internal private(set) var credit: UInt16 = 0
|
||||
private func consumeCredit() -> UInt16 {
|
||||
fileprivate func consumeCredit() -> UInt16 {
|
||||
if credit > 0 {
|
||||
credit -= 1
|
||||
return credit
|
||||
@@ -90,8 +90,8 @@ class SMBClient: NSObject, StreamDelegate {
|
||||
|
||||
inputStream.delegate = self
|
||||
outputStream.delegate = self
|
||||
inputStream.schedule(in: RunLoop.main, forMode: .defaultRunLoopMode)
|
||||
outputStream.schedule(in: RunLoop.main, forMode: .defaultRunLoopMode)
|
||||
inputStream.schedule(in: RunLoop.main, forMode: .init("kCFRunLoopDefaultMode"))
|
||||
outputStream.schedule(in: RunLoop.main, forMode: .init("kCFRunLoopDefaultMode"))
|
||||
inputStream.open()
|
||||
outputStream.open()
|
||||
|
||||
@@ -101,8 +101,8 @@ class SMBClient: NSObject, StreamDelegate {
|
||||
fileprivate func close() {
|
||||
self.inputStream?.close()
|
||||
self.outputStream?.close()
|
||||
self.inputStream?.remove(from: RunLoop.main, forMode: .defaultRunLoopMode)
|
||||
self.outputStream?.remove(from: RunLoop.main, forMode: .defaultRunLoopMode)
|
||||
self.inputStream?.remove(from: RunLoop.main, forMode: .init("kCFRunLoopDefaultMode"))
|
||||
self.outputStream?.remove(from: RunLoop.main, forMode: .init("kCFRunLoopDefaultMode"))
|
||||
self.inputStream?.delegate = nil
|
||||
self.outputStream?.delegate = nil
|
||||
|
||||
@@ -119,9 +119,7 @@ class SMBClient: NSObject, StreamDelegate {
|
||||
var data = data
|
||||
var byteSent: Int = 0
|
||||
while data.count > 0 {
|
||||
let bytesWritten = data.withUnsafeBytes {
|
||||
outputStream.write($0, maxLength: data.count)
|
||||
}
|
||||
let bytesWritten: Int = (try? outputStream.write(data: data)) ?? -1
|
||||
|
||||
if bytesWritten > 0 {
|
||||
let range = 0..<bytesWritten
|
||||
|
||||
@@ -11,7 +11,6 @@ import Foundation
|
||||
class SMBFileProvider: FileProvider, FileProviderMonitor {
|
||||
open class var type: String { return "SMB" }
|
||||
open var baseURL: URL?
|
||||
open var currentPath: String = ""
|
||||
open var dispatch_queue: DispatchQueue
|
||||
open var operation_queue: OperationQueue
|
||||
open weak var delegate: FileProviderDelegate?
|
||||
@@ -25,11 +24,7 @@ class SMBFileProvider: FileProvider, FileProviderMonitor {
|
||||
}
|
||||
self.baseURL = baseURL.appendingPathComponent("")
|
||||
|
||||
#if swift(>=3.1)
|
||||
let queueLabel = "FileProvider.\(Swift.type(of: self).type)"
|
||||
#else
|
||||
let queueLabel = "FileProvider.\(type(of: self).type)"
|
||||
#endif
|
||||
dispatch_queue = DispatchQueue(label: queueLabel, attributes: .concurrent)
|
||||
operation_queue = OperationQueue()
|
||||
operation_queue.name = "\(queueLabel).Operation"
|
||||
@@ -38,18 +33,20 @@ class SMBFileProvider: FileProvider, FileProviderMonitor {
|
||||
}
|
||||
|
||||
public required convenience init?(coder aDecoder: NSCoder) {
|
||||
guard let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL else {
|
||||
guard let baseURL = aDecoder.decodeObject(of: NSURL.self, forKey: "baseURL") as URL? else {
|
||||
if #available(macOS 10.11, iOS 9.0, tvOS 9.0, *) {
|
||||
aDecoder.failWithError(CocoaError(.coderValueNotFound,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Base URL is not set."]))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
self.init(baseURL: baseURL,
|
||||
credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential)
|
||||
self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? ""
|
||||
credential: aDecoder.decodeObject(of: URLCredential.self, forKey: "credential"))
|
||||
}
|
||||
|
||||
open func encode(with aCoder: NSCoder) {
|
||||
aCoder.encode(self.baseURL, forKey: "baseURL")
|
||||
aCoder.encode(self.credential, forKey: "credential")
|
||||
aCoder.encode(self.currentPath, forKey: "currentPath")
|
||||
}
|
||||
|
||||
public static var supportsSecureCoding: Bool {
|
||||
@@ -138,7 +135,6 @@ class SMBFileProvider: FileProvider, FileProviderMonitor {
|
||||
|
||||
open func copy(with zone: NSZone? = nil) -> Any {
|
||||
let copy = SMBFileProvider(baseURL: self.baseURL!, credential: self.credential!)!
|
||||
copy.currentPath = self.currentPath
|
||||
copy.delegate = self.delegate
|
||||
copy.fileOperationDelegate = self.fileOperationDelegate
|
||||
return copy
|
||||
|
||||
@@ -31,11 +31,7 @@ protocol SMBRequestBody {
|
||||
|
||||
extension SMBRequestBody {
|
||||
var command: SMB2.Command {
|
||||
#if swift(>=3.1)
|
||||
return Swift.type(of: self).command
|
||||
#else
|
||||
return type(of: self).command
|
||||
#endif
|
||||
return Swift.type(of: self).command
|
||||
}
|
||||
|
||||
func data() -> Data {
|
||||
|
||||
@@ -310,11 +310,11 @@ extension SMB2 {
|
||||
static let ipv6: sa_family_t = 0x17
|
||||
|
||||
var sockaddr: sockaddr_in {
|
||||
return Data.mapMemory(from: self.sockaddrStorage)!
|
||||
return unsafeBitCast(self.sockaddrStorage, to: sockaddr_in.self)
|
||||
}
|
||||
|
||||
var sockaddr6: sockaddr_in6 {
|
||||
return Data.mapMemory(from: self.sockaddrStorage)!
|
||||
return unsafeBitCast(self.sockaddrStorage, to: sockaddr_in6.self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,17 +72,23 @@ extension SMB2 {
|
||||
let header: SMB2FilesInformationHeader
|
||||
switch type {
|
||||
case .fileDirectoryInformation:
|
||||
header = buffer.scanValue(start: offset) as FileDirectoryInformationHeader!
|
||||
let tHeader: FileDirectoryInformationHeader = buffer.scanValue(start: offset)!
|
||||
header = tHeader
|
||||
case .fileFullDirectoryInformation:
|
||||
header = buffer.scanValue(start: offset) as FileFullDirectoryInformationHeader!
|
||||
let tHeader: FileFullDirectoryInformationHeader = buffer.scanValue(start: offset)!
|
||||
header = tHeader
|
||||
case .fileIdFullDirectoryInformation:
|
||||
header = buffer.scanValue(start: offset) as FileIdFullDirectoryInformationHeader!
|
||||
let tHeader: FileIdFullDirectoryInformationHeader = buffer.scanValue(start: offset)!
|
||||
header = tHeader
|
||||
case .fileBothDirectoryInformation:
|
||||
header = buffer.scanValue(start: offset) as FileBothDirectoryInformationHeader!
|
||||
let tHeader: FileBothDirectoryInformationHeader = buffer.scanValue(start: offset)!
|
||||
header = tHeader
|
||||
case .fileIdBothDirectoryInformation:
|
||||
header = buffer.scanValue(start: offset) as FileIdBothDirectoryInformationHeader!
|
||||
let tHeader: FileIdBothDirectoryInformationHeader = buffer.scanValue(start: offset)!
|
||||
header = tHeader
|
||||
case .fileNamesInformation:
|
||||
header = buffer.scanValue(start: offset) as FileNamesInformationHeader!
|
||||
let tHeader: FileNamesInformationHeader = buffer.scanValue(start: offset)!
|
||||
header = tHeader
|
||||
default:
|
||||
return []
|
||||
}
|
||||
@@ -98,8 +104,10 @@ extension SMB2 {
|
||||
}
|
||||
|
||||
init? (data: Data) {
|
||||
let offset = Int(data.scanValue(start: 2) as UInt16!)
|
||||
let length = Int(data.scanValue(start: 4) as UInt32!)
|
||||
let tOffset: UInt16 = data.scanValue(start: 2)!
|
||||
let offset = Int(tOffset)
|
||||
let tLength: UInt32 = data.scanValue(start: 4)!
|
||||
let length = Int(tLength)
|
||||
guard data.count > offset + length else {
|
||||
return nil
|
||||
}
|
||||
@@ -200,7 +208,8 @@ extension SMB2 {
|
||||
/*let offsetData = data.subdataWithRange(NSRange(location: 2, length: 2))
|
||||
let offset: UInt16 = decode(offsetData)*/
|
||||
|
||||
let length = Int(data.scanValue(start: 4) as UInt32!)
|
||||
let tLength: UInt32 = data.scanValue(start: 4)!
|
||||
let length = Int(tLength)
|
||||
|
||||
guard data.count >= 8 + length else {
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
//
|
||||
// ServerTrustPolicy.swift
|
||||
//
|
||||
// Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - ServerTrustPolicy
|
||||
/// The `ServerTrustPolicy` evaluates the server trust generally provided by an `NSURLAuthenticationChallenge` when
|
||||
/// connecting to a server over a secure HTTPS connection. The policy configuration then evaluates the server trust
|
||||
/// with a given set of criteria to determine whether the server trust is valid and the connection should be made.
|
||||
///
|
||||
/// Using pinned certificates or public keys for evaluation helps prevent man-in-the-middle (MITM) attacks and other
|
||||
/// vulnerabilities. Applications dealing with sensitive customer data or financial information are strongly encouraged
|
||||
/// to route all communication over an HTTPS connection with pinning enabled.
|
||||
///
|
||||
/// - performDefaultEvaluation: Uses the default server trust evaluation while allowing you to control whether to
|
||||
/// validate the host provided by the challenge. Applications are encouraged to always
|
||||
/// validate the host in production environments to guarantee the validity of the server's
|
||||
/// certificate chain.
|
||||
///
|
||||
/// - disableEvaluation: Disables all evaluation which in turn will always consider any server trust as valid.
|
||||
///
|
||||
public enum ServerTrustPolicy {
|
||||
case performDefaultEvaluation(validateHost: Bool)
|
||||
case disableEvaluation
|
||||
|
||||
// MARK: - Evaluation
|
||||
/// Evaluates whether the server trust is valid.
|
||||
///
|
||||
/// - returns: Whether the server trust is valid.
|
||||
public func evaluate() -> Bool {
|
||||
var serverTrustIsValid = false
|
||||
|
||||
switch self {
|
||||
case .performDefaultEvaluation(_):
|
||||
break
|
||||
case .disableEvaluation:
|
||||
serverTrustIsValid = true
|
||||
}
|
||||
|
||||
return serverTrustIsValid
|
||||
}
|
||||
|
||||
/// Evaluates whether the server trust is valid for the given host.
|
||||
///
|
||||
/// - parameter serverTrust: The server trust to evaluate.
|
||||
/// - parameter host: The host of the challenge protection space.
|
||||
///
|
||||
/// - returns: Whether the server trust is valid.
|
||||
public func evaluate(_ serverTrust: SecTrust, forHost host: String) -> Bool {
|
||||
var serverTrustIsValid = false
|
||||
|
||||
switch self {
|
||||
case let .performDefaultEvaluation(validateHost):
|
||||
let policy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil)
|
||||
SecTrustSetPolicies(serverTrust, policy)
|
||||
case .disableEvaluation:
|
||||
serverTrustIsValid = true
|
||||
}
|
||||
|
||||
return serverTrustIsValid
|
||||
}
|
||||
}
|
||||
@@ -46,11 +46,15 @@ open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
}
|
||||
|
||||
public required convenience init?(coder aDecoder: NSCoder) {
|
||||
guard let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL else {
|
||||
guard let baseURL = aDecoder.decodeObject(of: NSURL.self, forKey: "baseURL") as URL? else {
|
||||
if #available(macOS 10.11, iOS 9.0, tvOS 9.0, *) {
|
||||
aDecoder.failWithError(CocoaError(.coderValueNotFound,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Base URL is not set."]))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
self.init(baseURL: baseURL,
|
||||
credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential)
|
||||
credential: aDecoder.decodeObject(of: URLCredential.self, forKey: "credential"))
|
||||
self.useCache = aDecoder.decodeBool(forKey: "useCache")
|
||||
self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache")
|
||||
}
|
||||
@@ -289,7 +293,7 @@ open class WebDAVFileProvider: HTTPFileProvider, 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.urlError(path, code: .resourceUnavailable))
|
||||
completionHandler(nil, nil, nil, URLError(.resourceUnavailable, url: self.url(of: path)))
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -373,13 +377,13 @@ open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
return FileProviderWebDavError(code: code, path: path ?? "", serverDescription: data.flatMap({ String(data: $0, encoding: .utf8) }), url: self.url(of: path ?? ""))
|
||||
}
|
||||
|
||||
override func multiStatusHandler(source: String, data: Data, completionHandler: SimpleCompletionHandler) {
|
||||
override func multiStatusError(operation: FileOperationType, data: Data) -> FileProviderHTTPError? {
|
||||
let xresponses = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
|
||||
for xresponse in xresponses where (xresponse.status ?? 0) >= 300 {
|
||||
let code = xresponse.status.flatMap { FileProviderHTTPErrorCode(rawValue: $0) } ?? .internalServerError
|
||||
let error = self.serverError(with: code, path: source, data: data)
|
||||
completionHandler?(error)
|
||||
return self.serverError(with: code, path: operation.source, data: data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -405,14 +409,14 @@ extension WebDAVFileProvider: ExtendedFileProvider {
|
||||
return false
|
||||
}
|
||||
let supportedExt: [String] = ["jpg", "jpeg", "png", "gif"]
|
||||
return supportedExt.contains((path as NSString).pathExtension)
|
||||
return supportedExt.contains(path.pathExtension)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((ImageClass?, Error?) -> Void)) -> Progress? {
|
||||
guard self.baseURL?.host?.contains("dav.yandex.") ?? false else {
|
||||
dispatch_queue.async {
|
||||
completionHandler(nil, self.urlError(path, code: .resourceUnavailable))
|
||||
completionHandler(nil, URLError(.resourceUnavailable, url: self.url(of: path)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -444,7 +448,7 @@ extension WebDAVFileProvider: ExtendedFileProvider {
|
||||
@discardableResult
|
||||
open func propertiesOfFile(path: String, completionHandler: @escaping (([String : Any], [String], Error?) -> Void)) -> Progress? {
|
||||
dispatch_queue.async {
|
||||
completionHandler([:], [], self.urlError(path, code: .resourceUnavailable))
|
||||
completionHandler([:], [], URLError(.resourceUnavailable, url: self.url(of: path)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -463,11 +467,7 @@ struct DavResponse {
|
||||
init? (_ node: AEXMLElement, baseURL: URL?) {
|
||||
|
||||
func standardizePath(_ str: String) -> String {
|
||||
#if swift(>=4.0)
|
||||
let trimmedStr = str.hasPrefix("/") ? String(str[str.index(after: str.startIndex)...]) : str
|
||||
#else
|
||||
let trimmedStr = str.hasPrefix("/") ? str.substring(from: str.index(after: str.startIndex)) : str
|
||||
#endif
|
||||
return trimmedStr.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? str
|
||||
}
|
||||
|
||||
@@ -578,7 +578,7 @@ public final class WebDavFileObject: FileObject {
|
||||
}
|
||||
|
||||
/// MIME type of the file.
|
||||
open internal(set) var contentType: ContentMIMEType {
|
||||
public internal(set) var contentType: ContentMIMEType {
|
||||
get {
|
||||
return (allValues[.mimeTypeKey] as? String).flatMap(ContentMIMEType.init(rawValue:)) ?? .stream
|
||||
}
|
||||
@@ -588,7 +588,7 @@ public final class WebDavFileObject: FileObject {
|
||||
}
|
||||
|
||||
/// HTTP E-Tag, can be used to mark changed files.
|
||||
open internal(set) var entryTag: String? {
|
||||
public internal(set) var entryTag: String? {
|
||||
get {
|
||||
return allValues[.entryTagKey] as? String
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ class FilesProviderTests: XCTestCase, FileProviderDelegate {
|
||||
self.testRemoveFile(provider, filePath: self.testFolderName)
|
||||
}
|
||||
testBasic(provider)
|
||||
testArchiving(provider)
|
||||
testOperations(provider)
|
||||
}
|
||||
|
||||
@@ -41,6 +42,7 @@ class FilesProviderTests: XCTestCase, FileProviderDelegate {
|
||||
}
|
||||
let provider = WebDAVFileProvider(baseURL: url, credential: cred)!
|
||||
provider.delegate = self
|
||||
testArchiving(provider)
|
||||
addTeardownBlock {
|
||||
self.testRemoveFile(provider, filePath: self.testFolderName)
|
||||
}
|
||||
@@ -54,6 +56,7 @@ class FilesProviderTests: XCTestCase, FileProviderDelegate {
|
||||
let cred = URLCredential(user: "testuser", password: pass, persistence: .forSession)
|
||||
let provider = DropboxFileProvider(credential: cred)
|
||||
provider.delegate = self
|
||||
testArchiving(provider)
|
||||
addTeardownBlock {
|
||||
self.testRemoveFile(provider, filePath: self.testFolderName)
|
||||
}
|
||||
@@ -62,6 +65,7 @@ class FilesProviderTests: XCTestCase, FileProviderDelegate {
|
||||
}
|
||||
|
||||
func testFTPPassive() {
|
||||
/*
|
||||
guard let urlStr = ProcessInfo.processInfo.environment["ftp_url"] else { return }
|
||||
let url = URL(string: urlStr)!
|
||||
let cred: URLCredential?
|
||||
@@ -70,8 +74,14 @@ class FilesProviderTests: XCTestCase, FileProviderDelegate {
|
||||
} else {
|
||||
cred = nil
|
||||
}
|
||||
*/
|
||||
let url = URL(string: "ftp://ftp.edmapplication.com")!
|
||||
let cred = URLCredential(user: "abbas@edmapplication.com", password: "baTsivWZ4", persistence: .forSession)
|
||||
//let url = URL(string: "ftpes://ftp.adidas-group.com:21")!
|
||||
//let cred = URLCredential(user: "ecomwe-reversals-full", password: "rNeUj726Xqk2k", persistence: .forSession)
|
||||
let provider = FTPFileProvider(baseURL: url, mode: .extendedPassive, credential: cred)!
|
||||
provider.delegate = self
|
||||
testArchiving(provider)
|
||||
addTeardownBlock {
|
||||
self.testRemoveFile(provider, filePath: self.testFolderName)
|
||||
}
|
||||
@@ -85,10 +95,11 @@ class FilesProviderTests: XCTestCase, FileProviderDelegate {
|
||||
let cred = URLCredential(user: "testuser", password: pass, persistence: .forSession)
|
||||
let provider = OneDriveFileProvider(credential: cred)
|
||||
provider.delegate = self
|
||||
testBasic(provider)
|
||||
testArchiving(provider)
|
||||
addTeardownBlock {
|
||||
self.testRemoveFile(provider, filePath: self.testFolderName)
|
||||
}
|
||||
testBasic(provider)
|
||||
testOperations(provider)
|
||||
}
|
||||
|
||||
@@ -207,7 +218,12 @@ class FilesProviderTests: XCTestCase, FileProviderDelegate {
|
||||
XCTAssertNil(error, "\(desc) failed: \(error?.localizedDescription ?? "no error desc")")
|
||||
expectation.fulfill()
|
||||
}
|
||||
wait(for: [expectation], timeout: timeout)
|
||||
if provider is FTPFileProvider {
|
||||
// FTP will need to download and upload file again.
|
||||
wait(for: [expectation], timeout: timeout * 6)
|
||||
} else {
|
||||
wait(for: [expectation], timeout: timeout)
|
||||
}
|
||||
print("Test fulfilled: \(desc).")
|
||||
}
|
||||
|
||||
@@ -225,8 +241,9 @@ class FilesProviderTests: XCTestCase, FileProviderDelegate {
|
||||
|
||||
private func randomData(size: Int = 262144) -> Data {
|
||||
var keyData = Data(count: size)
|
||||
let count = keyData.count
|
||||
let result = keyData.withUnsafeMutableBytes {
|
||||
SecRandomCopyBytes(kSecRandomDefault, keyData.count, $0)
|
||||
SecRandomCopyBytes(kSecRandomDefault, count, $0)
|
||||
}
|
||||
if result == errSecSuccess {
|
||||
return keyData
|
||||
@@ -247,7 +264,6 @@ class FilesProviderTests: XCTestCase, FileProviderDelegate {
|
||||
|
||||
fileprivate func testUploadFile(_ provider: FileProvider, filePath: String) {
|
||||
// test Upload/Download
|
||||
let url = dummyFile()
|
||||
let desc = "Uploading file in \(provider.type)"
|
||||
print("Test started: \(desc).")
|
||||
let expectation = XCTestExpectation(description: desc)
|
||||
@@ -310,6 +326,41 @@ class FilesProviderTests: XCTestCase, FileProviderDelegate {
|
||||
print("Test fulfilled: \(desc).")
|
||||
}
|
||||
|
||||
fileprivate func testArchiving(_ provider: FileProvider) {
|
||||
let archivedData = NSKeyedArchiver.archivedData(withRootObject: provider)
|
||||
let unarchived = NSKeyedUnarchiver.unarchiveObject(with: archivedData) as? FileProvider
|
||||
XCTAssertNotNil(unarchived)
|
||||
XCTAssertEqual(unarchived?.baseURL, provider.baseURL, "archived provider is not same with original")
|
||||
XCTAssertEqual(unarchived?.credential, provider.credential, "archived provider is not same with original")
|
||||
if let provider = provider as? FileProviderBasicRemote, let unarchived_r = unarchived as? FileProviderBasicRemote {
|
||||
XCTAssertEqual(unarchived_r.useCache, provider.useCache, "archived provider is not same with original")
|
||||
XCTAssertEqual(unarchived_r.validatingCache, provider.validatingCache, "archived provider is not same with original")
|
||||
}
|
||||
if let provider = provider as? OneDriveFileProvider, let unarchived_o = unarchived as? OneDriveFileProvider {
|
||||
XCTAssertEqual(unarchived_o.route.rawValue, provider.route.rawValue, "archived provider is not same with original")
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func testSymlink(_ provider: FileProvider & FileProviderSymbolicLink, filePath: String) {
|
||||
let desc = "Symlink in \(provider.type)"
|
||||
print("Test started: \(desc).")
|
||||
let expectation = XCTestExpectation(description: desc)
|
||||
provider.create(symbolicLink: filePath + " Link", withDestinationPath: filePath) { (error) in
|
||||
XCTAssertNil(error, "\(desc) failed: \(error?.localizedDescription ?? "no error desc")")
|
||||
provider.destination(ofSymbolicLink: filePath + " Link", completionHandler: { (fileObject, error) in
|
||||
provider.removeItem(path: filePath + " Link", completionHandler: nil)
|
||||
XCTAssertNil(error, "\(desc) failed: \(error?.localizedDescription ?? "no error desc")")
|
||||
XCTAssertNotNil(fileObject, "file '\(filePath)' didn't exist")
|
||||
guard fileObject != nil else { return }
|
||||
XCTAssertEqual(fileObject!.path, filePath, "file path is different from '\(filePath)'")
|
||||
XCTAssertEqual(fileObject!.type, URLFileResourceType.regular, "file '\(filePath)' is not a regular file")
|
||||
expectation.fulfill()
|
||||
})
|
||||
}
|
||||
wait(for: [expectation], timeout: timeout)
|
||||
print("Test fulfilled: \(desc).")
|
||||
}
|
||||
|
||||
fileprivate func testBasic(_ provider: FileProvider) {
|
||||
let filepath = "/test/file.txt"
|
||||
let fileurl = provider.url(of: filepath)
|
||||
@@ -336,6 +387,9 @@ class FilesProviderTests: XCTestCase, FileProviderDelegate {
|
||||
testContentsFile(provider, filePath: textFilePath)
|
||||
testRenameFile(provider, filePath: textFilePath, to: renamedFilePath)
|
||||
testCopyFile(provider, filePath: renamedFilePath, to: textFilePath)
|
||||
if let provider = provider as? FileProvider & FileProviderSymbolicLink {
|
||||
testSymlink(provider, filePath: textFilePath)
|
||||
}
|
||||
testRemoveFile(provider, filePath: textFilePath)
|
||||
|
||||
// TODO: Test search
|
||||
|
||||
Reference in New Issue
Block a user