Compare commits
115 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c7201a8be7 | |||
| d2a03369fd | |||
| d08098008a | |||
| 3c5303214e | |||
| ad8f333d16 | |||
| 5e8f653c52 | |||
| e4cd7ddf22 | |||
| 1efb0e3fe5 | |||
| 1292856646 | |||
| e39f9c29ec | |||
| f43199c22a | |||
| 3d44de0b40 | |||
| 21b5214481 | |||
| f0b4925db2 | |||
| 0da957d25f | |||
| 0de558c160 | |||
| cad68da076 | |||
| 39da09edd4 | |||
| 5129aee1b5 | |||
| 478f0819b5 | |||
| b2a7800f7c | |||
| 96c102d156 | |||
| 8aedd8e72a | |||
| 5b55debe8a | |||
| 0fa062a946 | |||
| 879f86c1f9 | |||
| 80d5f02bbd | |||
| b8a7721b2f | |||
| 44b4784cd3 | |||
| 7d9e2247f2 | |||
| 0ace562442 | |||
| 63d831ef90 | |||
| d6b91348a3 | |||
| fd9d4c1ab4 | |||
| 41e266c2a9 | |||
| 6127f4a7d9 | |||
| faca943beb | |||
| 9345436fa2 | |||
| 6228d88d41 | |||
| 5b395915a9 | |||
| e4f12a502b | |||
| 8faad0ec65 | |||
| bd5569d213 | |||
| 14ed279879 | |||
| fd293f7bdb | |||
| 3d9625e243 | |||
| 0d017ebfc4 | |||
| fd36df67f3 | |||
| 5be8c4ef5e | |||
| 0374fd7688 | |||
| 5ddfa43555 | |||
| 21850bb548 | |||
| b166e111e0 | |||
| 1dd7561215 | |||
| f94719deb0 | |||
| b13df0a977 | |||
| 24af7aa4c2 | |||
| 06039ad993 | |||
| d8fec3e346 | |||
| 5c93bc8731 | |||
| 61ba245189 | |||
| 02e6cd37dd | |||
| 55608fb8d0 | |||
| 3e3582f6fa | |||
| dd7a9d20b6 | |||
| d4a9b4a34f | |||
| 34c663e62c | |||
| 1415dda987 | |||
| cd465c1288 | |||
| a605b0cd85 | |||
| bf7043de29 | |||
| ff5e13931f | |||
| 75af738d2e | |||
| f54a1253e4 | |||
| 1394a92662 | |||
| dab171c755 | |||
| ea5de2e2aa | |||
| 2253cca086 | |||
| e15a900ade | |||
| 5c2c56c44c | |||
| ff4bbdf0de | |||
| 163a218ac2 | |||
| 81401ee36f | |||
| 759ba3c7bf | |||
| f5c8f6308b | |||
| 4dbb0adb18 | |||
| bf62d585fd | |||
| f21f658874 | |||
| e6eba3d198 | |||
| 6959a14dc1 | |||
| 29a9e0fb82 | |||
| c7b4e1f124 | |||
| 99a433a0fc | |||
| 4023fbc62e | |||
| 1045901d7c | |||
| 528d5eebc3 | |||
| 079f8f4b77 | |||
| e12f386a9d | |||
| 38e217bc19 | |||
| 0b41abd4ef | |||
| aa781adeb2 | |||
| d61e51ba1c | |||
| cdff7db32e | |||
| 9533a0e3c9 | |||
| 194673b3b6 | |||
| 194c8a41aa | |||
| c290377433 | |||
| 330a22c45d | |||
| e899804e28 | |||
| 489a9a16d0 | |||
| 93359d5173 | |||
| 12ff3a410d | |||
| a7de09362b | |||
| b1fe37cf2a | |||
| bd59eacee2 |
+29
-20
@@ -12,14 +12,14 @@ env:
|
||||
- MACOS_SDK=macosx
|
||||
- TVOS_SDK=appletvsimulator
|
||||
matrix:
|
||||
- DESTINATION="OS=10.2,name=iPad Air 2" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD_LINT="YES"
|
||||
- DESTINATION="OS=9.0,name=iPhone 6" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD_LINT="NO"
|
||||
- DESTINATION="OS=8.1,name=iPhone 4S" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD_LINT="NO"
|
||||
- DESTINATION="OS=10.2,name=iPad Air 2" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="NO" CARTHAGEDEPLOY="YES"
|
||||
- DESTINATION="OS=9.0,name=iPhone 6" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="YES" CARTHAGEDEPLOY="NO"
|
||||
- DESTINATION="OS=8.1,name=iPhone 4S" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="NO" CARTHAGEDEPLOY="NO"
|
||||
|
||||
- DESTINATION="OS=10.1,name=Apple TV 1080p" SCHEME="$TVOS_FRAMEWORK_SCHEME" SDK="$TVOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD_LINT="NO"
|
||||
- DESTINATION="OS=10.0,name=Apple TV 1080p" SCHEME="$TVOS_FRAMEWORK_SCHEME" SDK="$TVOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD_LINT="NO"
|
||||
- DESTINATION="OS=10.1,name=Apple TV 1080p" SCHEME="$TVOS_FRAMEWORK_SCHEME" SDK="$TVOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="NO" CARTHAGEDEPLOY="NO"
|
||||
# - DESTINATION="OS=10.0,name=Apple TV 1080p" SCHEME="$TVOS_FRAMEWORK_SCHEME" SDK="$TVOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="NO" CARTHAGEDEPLOY="NO"
|
||||
|
||||
- DESTINATION="arch=x86_64" SCHEME="$MACOS_FRAMEWORK_SCHEME" SDK="$MACOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD_LINT="NO"
|
||||
- DESTINATION="arch=x86_64" SCHEME="$MACOS_FRAMEWORK_SCHEME" SDK="$MACOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="NO" CARTHAGEDEPLOY="NO"
|
||||
before_install:
|
||||
- gem install xcpretty --no-rdoc --no-ri --no-document --quiet
|
||||
- gem install cocoapods --no-rdoc --no-ri --no-document --quiet
|
||||
@@ -29,11 +29,6 @@ script:
|
||||
- set -o pipefail
|
||||
- xcodebuild -version
|
||||
|
||||
# Run `pod lib lint` if specified
|
||||
- if [ $POD_LINT == "YES" ]; then
|
||||
pod lib lint;
|
||||
fi
|
||||
|
||||
# Build Example in Debug if specified
|
||||
- if [ $BUILD_EXAMPLE == "YES" ]; then
|
||||
xcodebuild -project "$WORKSPACE" -scheme "$EXAMPLE_SCHEME" -sdk "$SDK" -destination "$DESTINATION" -configuration Debug ONLY_ACTIVE_ARCH=NO build | xcpretty;
|
||||
@@ -53,20 +48,34 @@ script:
|
||||
xcodebuild -project "$PROJECT" -scheme "$SCHEME" -sdk "$SDK" -destination "$DESTINATION" -configuration Release ONLY_ACTIVE_ARCH=NO build | xcpretty;
|
||||
fi
|
||||
|
||||
# after_success:
|
||||
# Run `pod lib lint` if specified
|
||||
- if [ $POD == "YES" ]; then
|
||||
pod lib lint --quick;
|
||||
fi
|
||||
|
||||
after_success:
|
||||
# Run `pod trunk push` if specified
|
||||
- if [ $POD == "YES" ] && [ -n "$TRAVIS_TAG" ]; then
|
||||
pod trunk push --allow-warnings;
|
||||
fi
|
||||
|
||||
# - bash <(curl -s https://codecov.io/bash)
|
||||
before_deploy:
|
||||
- brew update
|
||||
- brew outdated carthage || brew upgrade carthage
|
||||
- carthage version
|
||||
- carthage build --no-skip-current --verbose
|
||||
- carthage archive $PROJECTNAME
|
||||
- if [ $CARTHAGEDEPLOY == "YES" ] && [ -n "$TRAVIS_TAG" ]; then
|
||||
brew update;
|
||||
brew outdated carthage || brew upgrade carthage;
|
||||
carthage version;
|
||||
carthage build --no-skip-current --verbose;
|
||||
carthage archive $PROJECTNAME;
|
||||
fi
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key: $GITHUBTOKEN
|
||||
api_key: "$GITHUBTOKEN"
|
||||
file: $FRAMEWORK_NAME.zip
|
||||
skip_cleanup: true
|
||||
on:
|
||||
repo: amosavian/$PROJECTNAME
|
||||
tags: true
|
||||
# repo: amosavian/$PROJECTNAME
|
||||
repo: amosavian/FileProvider
|
||||
tags: true
|
||||
condition: "$CARTHAGEDEPLOY = YES"
|
||||
@@ -1,5 +1,5 @@
|
||||
#
|
||||
# Be sure to run `pod spec lint FileProvider.podspec' to ensure this is a
|
||||
# Be sure to run `pod spec lint FilesProvider.podspec' to ensure this is a
|
||||
# valid spec and to remove all comments including this before submitting the spec.
|
||||
#
|
||||
# To learn more about Podspec attributes see http://docs.cocoapods.org/specification.html
|
||||
@@ -15,9 +15,9 @@ Pod::Spec.new do |s|
|
||||
# summary should be tweet-length, and the description more in depth.
|
||||
#
|
||||
|
||||
s.name = "FileProvider"
|
||||
s.version = "0.12.5"
|
||||
s.summary = "FileManager replacement for Local and Remote (WebDAV/Dropbox/OneDrive/SMB2) files on iOS and macOS."
|
||||
s.name = "FilesProvider"
|
||||
s.version = "0.20.1"
|
||||
s.summary = "FileManager replacement for Local and Remote (WebDAV/FTP/Dropbox/OneDrive/SMB2) files on iOS and macOS."
|
||||
|
||||
# This description is used to generate tags and improve search results.
|
||||
# * Think: What does it do? Why did you write it? What is the focus?
|
||||
@@ -35,6 +35,15 @@
|
||||
792572411DF23BDA006A1526 /* LocalHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792572401DF23BDA006A1526 /* LocalHelper.swift */; };
|
||||
792572421DF23BDA006A1526 /* LocalHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792572401DF23BDA006A1526 /* LocalHelper.swift */; };
|
||||
792572431DF23BDA006A1526 /* LocalHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792572401DF23BDA006A1526 /* LocalHelper.swift */; };
|
||||
7936BC121E880F5700A6C81C /* FTPFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7936BC111E880F5700A6C81C /* FTPFileProvider.swift */; };
|
||||
7936BC131E880F5700A6C81C /* FTPFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7936BC111E880F5700A6C81C /* FTPFileProvider.swift */; };
|
||||
7936BC141E880F5700A6C81C /* FTPFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7936BC111E880F5700A6C81C /* FTPFileProvider.swift */; };
|
||||
793CCE2A1F4B8C5C00BC8288 /* FoundationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793CCE291F4B8C5C00BC8288 /* FoundationExtensions.swift */; };
|
||||
793CCE2B1F4B8C5C00BC8288 /* FoundationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793CCE291F4B8C5C00BC8288 /* FoundationExtensions.swift */; };
|
||||
793CCE2C1F4B8C5C00BC8288 /* FoundationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793CCE291F4B8C5C00BC8288 /* FoundationExtensions.swift */; };
|
||||
793CCE2E1F4B8C7B00BC8288 /* HashMAC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793CCE2D1F4B8C7B00BC8288 /* HashMAC.swift */; };
|
||||
793CCE2F1F4B8C7B00BC8288 /* HashMAC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793CCE2D1F4B8C7B00BC8288 /* HashMAC.swift */; };
|
||||
793CCE301F4B8C7B00BC8288 /* HashMAC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793CCE2D1F4B8C7B00BC8288 /* HashMAC.swift */; };
|
||||
79480FF61E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79480FF51E3ABDD0007E7275 /* CloudFileProvider.swift */; };
|
||||
79480FF71E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79480FF51E3ABDD0007E7275 /* CloudFileProvider.swift */; };
|
||||
79480FF81E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79480FF51E3ABDD0007E7275 /* CloudFileProvider.swift */; };
|
||||
@@ -47,6 +56,10 @@
|
||||
794C220E1D591A4B00EC49B8 /* SMB2QueryTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794C220D1D591A4B00EC49B8 /* SMB2QueryTypes.swift */; };
|
||||
794C220F1D591A4B00EC49B8 /* SMB2QueryTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794C220D1D591A4B00EC49B8 /* SMB2QueryTypes.swift */; };
|
||||
794C22101D591A4B00EC49B8 /* SMB2QueryTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794C220D1D591A4B00EC49B8 /* SMB2QueryTypes.swift */; };
|
||||
7958155A1F478ED9003344DD /* HTTPFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 795815591F478ED9003344DD /* HTTPFileProvider.swift */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
@@ -109,12 +122,14 @@
|
||||
79BD63BE1E2CC3C20035128C /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63BD1E2CC3C20035128C /* ImageIO.framework */; };
|
||||
79BD63C01E2CC3CD0035128C /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63BF1E2CC3CD0035128C /* CoreGraphics.framework */; };
|
||||
79BD63C21E2CC3D30035128C /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63C11E2CC3D30035128C /* AVFoundation.framework */; };
|
||||
79BD63C51E2D17880035128C /* OneDriveFileProvide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C31E2D17880035128C /* OneDriveFileProvide.swift */; };
|
||||
79BD63C61E2D17880035128C /* OneDriveFileProvide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C31E2D17880035128C /* OneDriveFileProvide.swift */; };
|
||||
79BD63C71E2D17880035128C /* OneDriveFileProvide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C31E2D17880035128C /* OneDriveFileProvide.swift */; };
|
||||
79BD63C51E2D17880035128C /* OneDriveFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C31E2D17880035128C /* OneDriveFileProvider.swift */; };
|
||||
79BD63C61E2D17880035128C /* OneDriveFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C31E2D17880035128C /* OneDriveFileProvider.swift */; };
|
||||
79BD63C71E2D17880035128C /* OneDriveFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C31E2D17880035128C /* OneDriveFileProvider.swift */; };
|
||||
79BD63C81E2D17880035128C /* OneDriveHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C41E2D17880035128C /* OneDriveHelper.swift */; };
|
||||
79BD63C91E2D17880035128C /* OneDriveHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C41E2D17880035128C /* OneDriveHelper.swift */; };
|
||||
79BD63CA1E2D17880035128C /* OneDriveHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD63C41E2D17880035128C /* OneDriveHelper.swift */; };
|
||||
79F4678B1E8B80F200C91A85 /* FTPHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 798654321E8874BC002FA550 /* FTPHelper.swift */; };
|
||||
79F4678C1E8B80F200C91A85 /* FTPHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 798654321E8874BC002FA550 /* FTPHelper.swift */; };
|
||||
79F5745B1DFDB10B00179ABF /* FileObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79F5745A1DFDB10A00179ABF /* FileObject.swift */; };
|
||||
79F5745C1DFDB10B00179ABF /* FileObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79F5745A1DFDB10A00179ABF /* FileObject.swift */; };
|
||||
79F5745D1DFDB10B00179ABF /* FileObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79F5745A1DFDB10A00179ABF /* FileObject.swift */; };
|
||||
@@ -130,13 +145,18 @@
|
||||
7924B1921D89DAE000589DB7 /* Parser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = "<group>"; };
|
||||
7924B1A81D89F79200589DB7 /* FPSStreamTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FPSStreamTask.swift; sourceTree = "<group>"; };
|
||||
792572401DF23BDA006A1526 /* LocalHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalHelper.swift; sourceTree = "<group>"; };
|
||||
7936BC111E880F5700A6C81C /* FTPFileProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTPFileProvider.swift; sourceTree = "<group>"; };
|
||||
793CCE291F4B8C5C00BC8288 /* FoundationExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationExtensions.swift; sourceTree = "<group>"; };
|
||||
793CCE2D1F4B8C7B00BC8288 /* HashMAC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HashMAC.swift; sourceTree = "<group>"; };
|
||||
79480FF51E3ABDD0007E7275 /* CloudFileProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudFileProvider.swift; sourceTree = "<group>"; };
|
||||
794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DropboxHelper.swift; sourceTree = "<group>"; };
|
||||
794C22091D5893F800EC49B8 /* SMB2Notification.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMB2Notification.swift; sourceTree = "<group>"; };
|
||||
794C220D1D591A4B00EC49B8 /* SMB2QueryTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMB2QueryTypes.swift; sourceTree = "<group>"; };
|
||||
799396671D48B7F600086753 /* FileProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FileProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
799396751D48B80D00086753 /* FileProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FileProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
799396821D48B82700086753 /* FileProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FileProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
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>"; };
|
||||
799396671D48B7F600086753 /* FilesProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FilesProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
799396751D48B80D00086753 /* FilesProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FilesProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
799396821D48B82700086753 /* FilesProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FilesProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
7993968B1D48B8C700086753 /* Info-iOS.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Info-iOS.plist"; sourceTree = "<group>"; };
|
||||
7993968C1D48B8C700086753 /* Info-MacOS.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Info-MacOS.plist"; sourceTree = "<group>"; };
|
||||
7993968D1D48B8C700086753 /* Info-tvOS.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Info-tvOS.plist"; sourceTree = "<group>"; };
|
||||
@@ -170,7 +190,7 @@
|
||||
79BD63BD1E2CC3C20035128C /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/System/Library/Frameworks/ImageIO.framework; sourceTree = DEVELOPER_DIR; };
|
||||
79BD63BF1E2CC3CD0035128C /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/System/Library/Frameworks/CoreGraphics.framework; sourceTree = DEVELOPER_DIR; };
|
||||
79BD63C11E2CC3D30035128C /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/System/Library/Frameworks/AVFoundation.framework; sourceTree = DEVELOPER_DIR; };
|
||||
79BD63C31E2D17880035128C /* OneDriveFileProvide.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OneDriveFileProvide.swift; sourceTree = "<group>"; };
|
||||
79BD63C31E2D17880035128C /* OneDriveFileProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OneDriveFileProvider.swift; sourceTree = "<group>"; };
|
||||
79BD63C41E2D17880035128C /* OneDriveHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OneDriveHelper.swift; sourceTree = "<group>"; };
|
||||
79F5745A1DFDB10A00179ABF /* FileObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileObject.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
@@ -243,6 +263,15 @@
|
||||
path = AEXML;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
793CCE281F4B8C3600BC8288 /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
793CCE291F4B8C5C00BC8288 /* FoundationExtensions.swift */,
|
||||
793CCE2D1F4B8C7B00BC8288 /* HashMAC.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7993965B1D48B7BF00086753 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -257,9 +286,9 @@
|
||||
799396681D48B7F600086753 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
799396671D48B7F600086753 /* FileProvider.framework */,
|
||||
799396751D48B80D00086753 /* FileProvider.framework */,
|
||||
799396821D48B82700086753 /* FileProvider.framework */,
|
||||
799396671D48B7F600086753 /* FilesProvider.framework */,
|
||||
799396751D48B80D00086753 /* FilesProvider.framework */,
|
||||
799396821D48B82700086753 /* FilesProvider.framework */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -278,6 +307,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7924B18B1D89DAE000589DB7 /* AEXML */,
|
||||
793CCE281F4B8C3600BC8288 /* Extensions */,
|
||||
799396991D48C02300086753 /* SMBTypes */,
|
||||
799396941D48C02300086753 /* FileProvider.h */,
|
||||
799396951D48C02300086753 /* FileProvider.swift */,
|
||||
@@ -286,15 +316,18 @@
|
||||
792572401DF23BDA006A1526 /* LocalHelper.swift */,
|
||||
79480FF51E3ABDD0007E7275 /* CloudFileProvider.swift */,
|
||||
79BD638B1E2CC2300035128C /* ExtendedLocalFileProvider.swift */,
|
||||
7924B1A81D89F79200589DB7 /* FPSStreamTask.swift */,
|
||||
7902C0851D61B56D00564440 /* RemoteSession.swift */,
|
||||
795815591F478ED9003344DD /* HTTPFileProvider.swift */,
|
||||
799396931D48C02300086753 /* DropboxFileProvider.swift */,
|
||||
794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */,
|
||||
79BD63C31E2D17880035128C /* OneDriveFileProvide.swift */,
|
||||
79BD63C31E2D17880035128C /* OneDriveFileProvider.swift */,
|
||||
79BD63C41E2D17880035128C /* OneDriveHelper.swift */,
|
||||
7924B1A81D89F79200589DB7 /* FPSStreamTask.swift */,
|
||||
799396A61D48C02300086753 /* WebDAVFileProvider.swift */,
|
||||
7936BC111E880F5700A6C81C /* FTPFileProvider.swift */,
|
||||
798654321E8874BC002FA550 /* FTPHelper.swift */,
|
||||
799396971D48C02300086753 /* SMBClient.swift */,
|
||||
799396981D48C02300086753 /* SMBFileProvider.swift */,
|
||||
799396A61D48C02300086753 /* WebDAVFileProvider.swift */,
|
||||
);
|
||||
path = Sources;
|
||||
sourceTree = "<group>";
|
||||
@@ -353,9 +386,9 @@
|
||||
/* End PBXHeadersBuildPhase section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
799396661D48B7F600086753 /* FileProvider iOS */ = {
|
||||
799396661D48B7F600086753 /* FilesProvider iOS */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 7993966F1D48B7F600086753 /* Build configuration list for PBXNativeTarget "FileProvider iOS" */;
|
||||
buildConfigurationList = 7993966F1D48B7F600086753 /* Build configuration list for PBXNativeTarget "FilesProvider iOS" */;
|
||||
buildPhases = (
|
||||
799396621D48B7F600086753 /* Sources */,
|
||||
799396631D48B7F600086753 /* Frameworks */,
|
||||
@@ -366,14 +399,14 @@
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = "FileProvider iOS";
|
||||
name = "FilesProvider iOS";
|
||||
productName = "FileProvider iOS";
|
||||
productReference = 799396671D48B7F600086753 /* FileProvider.framework */;
|
||||
productReference = 799396671D48B7F600086753 /* FilesProvider.framework */;
|
||||
productType = "com.apple.product-type.framework";
|
||||
};
|
||||
799396741D48B80D00086753 /* FileProvider OSX */ = {
|
||||
799396741D48B80D00086753 /* FilesProvider OSX */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 7993967A1D48B80D00086753 /* Build configuration list for PBXNativeTarget "FileProvider OSX" */;
|
||||
buildConfigurationList = 7993967A1D48B80D00086753 /* Build configuration list for PBXNativeTarget "FilesProvider OSX" */;
|
||||
buildPhases = (
|
||||
799396701D48B80D00086753 /* Sources */,
|
||||
799396711D48B80D00086753 /* Frameworks */,
|
||||
@@ -384,14 +417,14 @@
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = "FileProvider OSX";
|
||||
name = "FilesProvider OSX";
|
||||
productName = "FileProvider OSX";
|
||||
productReference = 799396751D48B80D00086753 /* FileProvider.framework */;
|
||||
productReference = 799396751D48B80D00086753 /* FilesProvider.framework */;
|
||||
productType = "com.apple.product-type.framework";
|
||||
};
|
||||
799396811D48B82700086753 /* FileProvider tvOS */ = {
|
||||
799396811D48B82700086753 /* FilesProvider tvOS */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 799396871D48B82700086753 /* Build configuration list for PBXNativeTarget "FileProvider tvOS" */;
|
||||
buildConfigurationList = 799396871D48B82700086753 /* Build configuration list for PBXNativeTarget "FilesProvider tvOS" */;
|
||||
buildPhases = (
|
||||
7993967D1D48B82700086753 /* Sources */,
|
||||
7993967E1D48B82700086753 /* Frameworks */,
|
||||
@@ -402,9 +435,9 @@
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = "FileProvider tvOS";
|
||||
name = "FilesProvider tvOS";
|
||||
productName = "FileProvider tvOS";
|
||||
productReference = 799396821D48B82700086753 /* FileProvider.framework */;
|
||||
productReference = 799396821D48B82700086753 /* FilesProvider.framework */;
|
||||
productType = "com.apple.product-type.framework";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
@@ -427,7 +460,7 @@
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 7993965F1D48B7BF00086753 /* Build configuration list for PBXProject "FileProvider" */;
|
||||
buildConfigurationList = 7993965F1D48B7BF00086753 /* Build configuration list for PBXProject "FilesProvider" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = English;
|
||||
hasScannedForEncodings = 0;
|
||||
@@ -439,9 +472,9 @@
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
799396661D48B7F600086753 /* FileProvider iOS */,
|
||||
799396741D48B80D00086753 /* FileProvider OSX */,
|
||||
799396811D48B82700086753 /* FileProvider tvOS */,
|
||||
799396661D48B7F600086753 /* FilesProvider iOS */,
|
||||
799396741D48B80D00086753 /* FilesProvider OSX */,
|
||||
799396811D48B82700086753 /* FilesProvider tvOS */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -479,12 +512,15 @@
|
||||
799396D41D48C02300086753 /* SMB2Tree.swift in Sources */,
|
||||
7924B1A21D89DAE000589DB7 /* Options.swift in Sources */,
|
||||
792572411DF23BDA006A1526 /* LocalHelper.swift in Sources */,
|
||||
7936BC121E880F5700A6C81C /* FTPFileProvider.swift in Sources */,
|
||||
79480FF61E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */,
|
||||
79F5745B1DFDB10B00179ABF /* FileObject.swift in Sources */,
|
||||
79BD63C51E2D17880035128C /* OneDriveFileProvide.swift in Sources */,
|
||||
79BD63C51E2D17880035128C /* OneDriveFileProvider.swift in Sources */,
|
||||
7924B1991D89DAE000589DB7 /* Element.swift in Sources */,
|
||||
799396C81D48C02300086753 /* SMB2IOCtl.swift in Sources */,
|
||||
799396D71D48C02300086753 /* SMB2Types.swift in Sources */,
|
||||
7958155A1F478ED9003344DD /* HTTPFileProvider.swift in Sources */,
|
||||
798654331E8874BC002FA550 /* FTPHelper.swift in Sources */,
|
||||
7924B1B21D89FCDA00589DB7 /* FPSStreamTask.swift in Sources */,
|
||||
799396C51D48C02300086753 /* SMB2FileOperation.swift in Sources */,
|
||||
79BD63C81E2D17880035128C /* OneDriveHelper.swift in Sources */,
|
||||
@@ -501,10 +537,12 @@
|
||||
799396E01D48C02300086753 /* WebDAVFileProvider.swift in Sources */,
|
||||
799396DA1D48C02300086753 /* SMBErrorType.swift in Sources */,
|
||||
799396C21D48C02300086753 /* SMB2FileHandle.swift in Sources */,
|
||||
793CCE2E1F4B8C7B00BC8288 /* HashMAC.swift in Sources */,
|
||||
799396CB1D48C02300086753 /* SMB2Query.swift in Sources */,
|
||||
799396AA1D48C02300086753 /* DropboxFileProvider.swift in Sources */,
|
||||
79BD638C1E2CC2300035128C /* ExtendedLocalFileProvider.swift in Sources */,
|
||||
7924B1B31D89FD6400589DB7 /* SMBClient.swift in Sources */,
|
||||
793CCE2A1F4B8C5C00BC8288 /* FoundationExtensions.swift in Sources */,
|
||||
7924B1A51D89DAE000589DB7 /* Parser.swift in Sources */,
|
||||
799396B01D48C02300086753 /* FileProvider.swift in Sources */,
|
||||
7924B19C1D89DAE000589DB7 /* Error.swift in Sources */,
|
||||
@@ -519,12 +557,15 @@
|
||||
799396D51D48C02300086753 /* SMB2Tree.swift in Sources */,
|
||||
7924B1A31D89DAE000589DB7 /* Options.swift in Sources */,
|
||||
792572421DF23BDA006A1526 /* LocalHelper.swift in Sources */,
|
||||
7936BC131E880F5700A6C81C /* FTPFileProvider.swift in Sources */,
|
||||
79480FF71E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */,
|
||||
79F5745C1DFDB10B00179ABF /* FileObject.swift in Sources */,
|
||||
79BD63C61E2D17880035128C /* OneDriveFileProvide.swift in Sources */,
|
||||
79BD63C61E2D17880035128C /* OneDriveFileProvider.swift in Sources */,
|
||||
7924B1B01D89F7DE00589DB7 /* FPSStreamTask.swift in Sources */,
|
||||
7924B19A1D89DAE000589DB7 /* Element.swift in Sources */,
|
||||
799396C91D48C02300086753 /* SMB2IOCtl.swift in Sources */,
|
||||
7958155B1F478ED9003344DD /* HTTPFileProvider.swift in Sources */,
|
||||
79F4678B1E8B80F200C91A85 /* FTPHelper.swift in Sources */,
|
||||
799396D81D48C02300086753 /* SMB2Types.swift in Sources */,
|
||||
799396C61D48C02300086753 /* SMB2FileOperation.swift in Sources */,
|
||||
79BD63C91E2D17880035128C /* OneDriveHelper.swift in Sources */,
|
||||
@@ -541,10 +582,12 @@
|
||||
799396E11D48C02300086753 /* WebDAVFileProvider.swift in Sources */,
|
||||
799396DB1D48C02300086753 /* SMBErrorType.swift in Sources */,
|
||||
799396C31D48C02300086753 /* SMB2FileHandle.swift in Sources */,
|
||||
793CCE2F1F4B8C7B00BC8288 /* HashMAC.swift in Sources */,
|
||||
799396CC1D48C02300086753 /* SMB2Query.swift in Sources */,
|
||||
7924B1AD1D89F7D800589DB7 /* SMBClient.swift in Sources */,
|
||||
79BD638D1E2CC2300035128C /* ExtendedLocalFileProvider.swift in Sources */,
|
||||
799396AB1D48C02300086753 /* DropboxFileProvider.swift in Sources */,
|
||||
793CCE2B1F4B8C5C00BC8288 /* FoundationExtensions.swift in Sources */,
|
||||
7924B1A61D89DAE000589DB7 /* Parser.swift in Sources */,
|
||||
799396B11D48C02300086753 /* FileProvider.swift in Sources */,
|
||||
7924B19D1D89DAE000589DB7 /* Error.swift in Sources */,
|
||||
@@ -559,12 +602,15 @@
|
||||
799396D61D48C02300086753 /* SMB2Tree.swift in Sources */,
|
||||
7924B1A41D89DAE000589DB7 /* Options.swift in Sources */,
|
||||
792572431DF23BDA006A1526 /* LocalHelper.swift in Sources */,
|
||||
7936BC141E880F5700A6C81C /* FTPFileProvider.swift in Sources */,
|
||||
79480FF81E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */,
|
||||
79F5745D1DFDB10B00179ABF /* FileObject.swift in Sources */,
|
||||
79BD63C71E2D17880035128C /* OneDriveFileProvide.swift in Sources */,
|
||||
79BD63C71E2D17880035128C /* OneDriveFileProvider.swift in Sources */,
|
||||
7924B1B11D89F7DF00589DB7 /* FPSStreamTask.swift in Sources */,
|
||||
7924B19B1D89DAE000589DB7 /* Element.swift in Sources */,
|
||||
799396CA1D48C02300086753 /* SMB2IOCtl.swift in Sources */,
|
||||
7958155C1F478ED9003344DD /* HTTPFileProvider.swift in Sources */,
|
||||
79F4678C1E8B80F200C91A85 /* FTPHelper.swift in Sources */,
|
||||
799396D91D48C02300086753 /* SMB2Types.swift in Sources */,
|
||||
799396C71D48C02300086753 /* SMB2FileOperation.swift in Sources */,
|
||||
79BD63CA1E2D17880035128C /* OneDriveHelper.swift in Sources */,
|
||||
@@ -581,10 +627,12 @@
|
||||
799396E21D48C02300086753 /* WebDAVFileProvider.swift in Sources */,
|
||||
799396DC1D48C02300086753 /* SMBErrorType.swift in Sources */,
|
||||
799396C41D48C02300086753 /* SMB2FileHandle.swift in Sources */,
|
||||
793CCE301F4B8C7B00BC8288 /* HashMAC.swift in Sources */,
|
||||
799396CD1D48C02300086753 /* SMB2Query.swift in Sources */,
|
||||
7924B1AE1D89F7D900589DB7 /* SMBClient.swift in Sources */,
|
||||
79BD638E1E2CC2300035128C /* ExtendedLocalFileProvider.swift in Sources */,
|
||||
799396AC1D48C02300086753 /* DropboxFileProvider.swift in Sources */,
|
||||
793CCE2C1F4B8C5C00BC8288 /* FoundationExtensions.swift in Sources */,
|
||||
7924B1A71D89DAE000589DB7 /* Parser.swift in Sources */,
|
||||
799396B21D48C02300086753 /* FileProvider.swift in Sources */,
|
||||
7924B19E1D89DAE000589DB7 /* Error.swift in Sources */,
|
||||
@@ -597,7 +645,7 @@
|
||||
799396601D48B7BF00086753 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_VERSION_STRING = 0.12.5;
|
||||
BUNDLE_VERSION_STRING = 0.20.1;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
@@ -607,6 +655,7 @@
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -620,6 +669,7 @@
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_NAME = FilesProvider;
|
||||
SWIFT_VERSION = 3.0;
|
||||
};
|
||||
name = Debug;
|
||||
@@ -627,7 +677,7 @@
|
||||
799396611D48B7BF00086753 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_VERSION_STRING = 0.12.5;
|
||||
BUNDLE_VERSION_STRING = 0.20.1;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
@@ -647,6 +697,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
PRODUCT_NAME = FilesProvider;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
SWIFT_VERSION = 3.0;
|
||||
};
|
||||
@@ -657,7 +708,6 @@
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
BUNDLE_VERSION_STRING = 0.8.2;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
@@ -694,8 +744,7 @@
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FileProvider-iOS";
|
||||
PRODUCT_NAME = FileProvider;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FilesProvider-iOS";
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -710,7 +759,6 @@
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
BUNDLE_VERSION_STRING = 0.8.2;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
@@ -742,8 +790,7 @@
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FileProvider-iOS";
|
||||
PRODUCT_NAME = FileProvider;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FilesProvider-iOS";
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
@@ -799,8 +846,7 @@
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks";
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.10;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FileProvider-OSX";
|
||||
PRODUCT_NAME = FileProvider;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FilesProvider-OSX";
|
||||
SDKROOT = macosx;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -850,8 +896,7 @@
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks";
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.10;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FileProvider-OSX";
|
||||
PRODUCT_NAME = FileProvider;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FilesProvider-OSX";
|
||||
SDKROOT = macosx;
|
||||
SKIP_INSTALL = YES;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
@@ -902,8 +947,7 @@
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FileProvider-tvOS";
|
||||
PRODUCT_NAME = FileProvider;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FilesProvider-tvOS";
|
||||
SDKROOT = appletvos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -952,8 +996,7 @@
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FileProvider-tvOS";
|
||||
PRODUCT_NAME = FileProvider;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FilesProvider-tvOS";
|
||||
SDKROOT = appletvos;
|
||||
SKIP_INSTALL = YES;
|
||||
TARGETED_DEVICE_FAMILY = 3;
|
||||
@@ -967,7 +1010,7 @@
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
7993965F1D48B7BF00086753 /* Build configuration list for PBXProject "FileProvider" */ = {
|
||||
7993965F1D48B7BF00086753 /* Build configuration list for PBXProject "FilesProvider" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
799396601D48B7BF00086753 /* Debug */,
|
||||
@@ -976,7 +1019,7 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
7993966F1D48B7F600086753 /* Build configuration list for PBXNativeTarget "FileProvider iOS" */ = {
|
||||
7993966F1D48B7F600086753 /* Build configuration list for PBXNativeTarget "FilesProvider iOS" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
7993966D1D48B7F600086753 /* Debug */,
|
||||
@@ -985,7 +1028,7 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
7993967A1D48B80D00086753 /* Build configuration list for PBXNativeTarget "FileProvider OSX" */ = {
|
||||
7993967A1D48B80D00086753 /* Build configuration list for PBXNativeTarget "FilesProvider OSX" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
7993967B1D48B80D00086753 /* Debug */,
|
||||
@@ -994,7 +1037,7 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
799396871D48B82700086753 /* Build configuration list for PBXNativeTarget "FileProvider tvOS" */ = {
|
||||
799396871D48B82700086753 /* Build configuration list for PBXNativeTarget "FilesProvider tvOS" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
799396881D48B82700086753 /* Debug */,
|
||||
+12
-10
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0810"
|
||||
LastUpgradeVersion = "0820"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -15,9 +15,9 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "799396741D48B80D00086753"
|
||||
BuildableName = "FileProvider.framework"
|
||||
BlueprintName = "FileProvider OSX"
|
||||
ReferencedContainer = "container:FileProvider.xcodeproj">
|
||||
BuildableName = "FilesProvider.framework"
|
||||
BlueprintName = "FilesProvider OSX"
|
||||
ReferencedContainer = "container:FilesProvider.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
@@ -26,6 +26,7 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
@@ -36,6 +37,7 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
@@ -46,9 +48,9 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "799396741D48B80D00086753"
|
||||
BuildableName = "FileProvider.framework"
|
||||
BlueprintName = "FileProvider OSX"
|
||||
ReferencedContainer = "container:FileProvider.xcodeproj">
|
||||
BuildableName = "FilesProvider.framework"
|
||||
BlueprintName = "FilesProvider OSX"
|
||||
ReferencedContainer = "container:FilesProvider.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
@@ -64,9 +66,9 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "799396741D48B80D00086753"
|
||||
BuildableName = "FileProvider.framework"
|
||||
BlueprintName = "FileProvider OSX"
|
||||
ReferencedContainer = "container:FileProvider.xcodeproj">
|
||||
BuildableName = "FilesProvider.framework"
|
||||
BlueprintName = "FilesProvider OSX"
|
||||
ReferencedContainer = "container:FilesProvider.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
+12
-10
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0810"
|
||||
LastUpgradeVersion = "0820"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -15,9 +15,9 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "799396661D48B7F600086753"
|
||||
BuildableName = "FileProvider.framework"
|
||||
BlueprintName = "FileProvider iOS"
|
||||
ReferencedContainer = "container:FileProvider.xcodeproj">
|
||||
BuildableName = "FilesProvider.framework"
|
||||
BlueprintName = "FilesProvider iOS"
|
||||
ReferencedContainer = "container:FilesProvider.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
@@ -26,6 +26,7 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
@@ -36,6 +37,7 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
@@ -46,9 +48,9 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "799396661D48B7F600086753"
|
||||
BuildableName = "FileProvider.framework"
|
||||
BlueprintName = "FileProvider iOS"
|
||||
ReferencedContainer = "container:FileProvider.xcodeproj">
|
||||
BuildableName = "FilesProvider.framework"
|
||||
BlueprintName = "FilesProvider iOS"
|
||||
ReferencedContainer = "container:FilesProvider.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
@@ -64,9 +66,9 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "799396661D48B7F600086753"
|
||||
BuildableName = "FileProvider.framework"
|
||||
BlueprintName = "FileProvider iOS"
|
||||
ReferencedContainer = "container:FileProvider.xcodeproj">
|
||||
BuildableName = "FilesProvider.framework"
|
||||
BlueprintName = "FilesProvider iOS"
|
||||
ReferencedContainer = "container:FilesProvider.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
+11
-9
@@ -15,9 +15,9 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "799396811D48B82700086753"
|
||||
BuildableName = "FileProvider.framework"
|
||||
BlueprintName = "FileProvider tvOS"
|
||||
ReferencedContainer = "container:FileProvider.xcodeproj">
|
||||
BuildableName = "FilesProvider.framework"
|
||||
BlueprintName = "FilesProvider tvOS"
|
||||
ReferencedContainer = "container:FilesProvider.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
@@ -26,6 +26,7 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
@@ -36,6 +37,7 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
@@ -46,9 +48,9 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "799396811D48B82700086753"
|
||||
BuildableName = "FileProvider.framework"
|
||||
BlueprintName = "FileProvider tvOS"
|
||||
ReferencedContainer = "container:FileProvider.xcodeproj">
|
||||
BuildableName = "FilesProvider.framework"
|
||||
BlueprintName = "FilesProvider tvOS"
|
||||
ReferencedContainer = "container:FilesProvider.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
@@ -64,9 +66,9 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "799396811D48B82700086753"
|
||||
BuildableName = "FileProvider.framework"
|
||||
BlueprintName = "FileProvider tvOS"
|
||||
ReferencedContainer = "container:FileProvider.xcodeproj">
|
||||
BuildableName = "FilesProvider.framework"
|
||||
BlueprintName = "FilesProvider tvOS"
|
||||
ReferencedContainer = "container:FilesProvider.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "FileProvider"
|
||||
)
|
||||
name: "FilesProvider"
|
||||
)
|
||||
|
||||
@@ -1,63 +1,71 @@
|
||||
# FileProvider
|
||||

|
||||
|
||||
>This Swift library provide a swifty way to deal with local and remote files and directories in a unified way.
|
||||
|
||||
<center>
|
||||
|
||||
[![Swift Version][swift-image]][swift-url]
|
||||
[![Platform][platform-image]](#)
|
||||
[![License][license-image]][license-url]
|
||||
|
||||
[![Release versin][release-image]][release-url]
|
||||
[][cocoapods]
|
||||
[![Carthage compatible][carthage-image]](https://github.com/Carthage/Carthage)
|
||||
|
||||
[![Build Status][travis-image]][travis-url]
|
||||
[![Codebeat Badge][codebeat-image]][codebeat-url]
|
||||
[![Cocoapods Docs][docs-image]][docs-url]
|
||||
|
||||
[![Release version][release-image]][release-url]
|
||||
[][cocoapods]
|
||||
[![Carthage compatible][carthage-image]](https://github.com/Carthage/Carthage)
|
||||
[![Cocoapods Downloads][cocoapods-downloads]][cocoapods]
|
||||
[![Cocoapods Apps][cocoapods-apps]][cocoapods]
|
||||
|
||||
Old Cocoapods repo stats:
|
||||
[![Cocoapods Downloads][cocoapods-downloads-old]][cocoapods-old]
|
||||
[![Cocoapods Apps][cocoapods-apps-old]][cocoapods-old]
|
||||
|
||||
</center>
|
||||
|
||||
<!---
|
||||
[![Cocoapods Doc][docs-image]][docs-url]
|
||||
[](https://codecov.io/gh/amosavian/FileProvider)
|
||||
--->
|
||||
|
||||
This library provides implementaion of WebDav, Dropbox, OneDrive and SMB2 (incomplete) and local files.
|
||||
This library provides implementaion of WebDav, FTP, Dropbox, OneDrive and SMB2 (incomplete) and local files.
|
||||
|
||||
All functions are async calls and it wont block your main thread.
|
||||
|
||||
Local and WebDAV providers are fully tested and can be used in production environment.
|
||||
All functions do async calls and it wont block your main thread.
|
||||
|
||||
## Features
|
||||
|
||||
- [x] **LocalFileProvider** a wrapper around `FileManager` with some additions like searching and reading a portion of file.
|
||||
- [x] **CloudFileProvider** A wrapper around app's ubiquitous container to iCloud Drive in iOS 8+ API.
|
||||
- [x] **WebDAVFileProvider** WebDAV protocol is defacto file transmission standard, replaced FTP.
|
||||
- [x] **LocalFileProvider** a wrapper around `FileManager` with some additions like builtin coordinating, searching and reading a portion of file.
|
||||
- [x] **CloudFileProvider** A wrapper around app's ubiquitous container API of iCloud Drive.
|
||||
- [x] **WebDAVFileProvider** WebDAV protocol is defacto file transmission standard, supported by some cloud services like `ownCloud`, `Box.com` and `Yandex.disk`.
|
||||
- [x] **FTPFileProvider** While deprecated in 1990s due to serious security concerns, it's still in use on some Web hosts.
|
||||
- [x] **DropboxFileProvider** A wrapper around Dropbox Web API.
|
||||
* For now it has limitation in uploading files up to 150MB.
|
||||
- [x] **OneDriveFileProvider** A wrapper around OneDrive REST API, works with `onedrive.com` and compatible (business) servers.
|
||||
* For now it has limitation in uploading files up to 100MB.
|
||||
- [ ] **GoogleFileProvider** A wrapper around Goodle Drive REST API.
|
||||
- [ ] **AmazonS3FileProvider** Amazon storage backend. Used by many sites.
|
||||
- [ ] **GoogleFileProvider** A wrapper around Goodle Drive REST API.
|
||||
- [ ] **SMBFileProvider** SMB2/3 introduced in 2006, which is a file and printer sharing protocol originated from Microsoft Windows and now is replacing AFP protocol on macOS.
|
||||
* Data types and some basic functions are implemented but *main interface is not implemented yet!*.
|
||||
* SMB1/CIFS is deprecated and very tricky to be implemented.
|
||||
- [ ] **FTPFileProvider** while deprecated in 1990s, it's still in use on some Web hosts.
|
||||
* SMBv1/CIFS is insecure, deprecated and kinda tricky to be implemented due to strict memory allignment in Swift.
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Swift 3**
|
||||
- **Swift 3.0 or higher**
|
||||
- iOS 8.0 , OSX 10.10
|
||||
- XCode 8.0
|
||||
|
||||
Legacy version is available in swift-2 branch
|
||||
Legacy version is available in swift-2 branch.
|
||||
|
||||
## Installation
|
||||
|
||||
### Important: this library has been renamed to avoid conflict in iOS 11, macOS 10.13 and Xcode 9.0. Please read issue [#53](https://github.com/amosavian/FileProvider/issues/53) to find more.
|
||||
|
||||
|
||||
### Cocoapods / Carthage / Swift Package Manager
|
||||
|
||||
Add this line to your pods file:
|
||||
|
||||
```ruby
|
||||
pod "FileProvider"
|
||||
pod "FilesProvider"
|
||||
```
|
||||
|
||||
Or add this to Cartfile:
|
||||
@@ -69,7 +77,7 @@ github "amosavian/FileProvider"
|
||||
Or to use in Swift Package Manager add this line in `Dependencies`:
|
||||
|
||||
```swift
|
||||
.Package(url: "https://github.com/amosavian/FileProvider.git", majorVersion: 0, minorVersion: 12)
|
||||
.Package(url: "https://github.com/amosavian/FileProvider.git", majorVersion: 0)
|
||||
```
|
||||
|
||||
### Manually
|
||||
@@ -95,7 +103,7 @@ Then you can do either:
|
||||
|
||||
* Copy Source folder to your project and Voila!
|
||||
|
||||
* Drop FileProvider.xcodeproj to you Xcode workspace and add the framework to your Embeded Binaries in target.
|
||||
* Drop `FilesProvider.xcodeproj` to you Xcode workspace and add the framework to your Embeded Binaries in target.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -106,10 +114,12 @@ Each provider has a specific class which conforms to FileProvider protocol and s
|
||||
For LocalFileProvider if you want to deal with `Documents` folder
|
||||
|
||||
``` swift
|
||||
import FilesProvider
|
||||
|
||||
let documentsProvider = LocalFileProvider()
|
||||
|
||||
// Equals with:
|
||||
let documentsProvider = LocalFileProvider(directory: .documentDirectory, domainMask: = .userDomainMask)
|
||||
let documentsProvider = LocalFileProvider(for: .documentDirectory, in: .userDomainMask)
|
||||
|
||||
// Equals with:
|
||||
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
@@ -119,15 +129,19 @@ let documentsProvider = LocalFileProvider(baseURL: documentsURL)
|
||||
Also for using group shared container:
|
||||
|
||||
```swift
|
||||
import FilesProvider
|
||||
|
||||
let documentsProvider = LocalFileProvider(sharedContainerId: "group.yourcompany.appContainer")
|
||||
// Replace your group identifier with string above
|
||||
// Replace your group identifier
|
||||
```
|
||||
|
||||
You can't change the base url later. and all paths are related to this base url by default.
|
||||
|
||||
To initialize an iCloud Container provider use below code, This will automatically manager creating Documents folder in container:
|
||||
To initialize an iCloud Container provider look at [here](https://medium.com/ios-os-x-development/icloud-drive-documents-1a46b5706fe1) to see how to update project settings then use below code, This will automatically manager creating Documents folder in container:
|
||||
|
||||
```swift
|
||||
import FilesProvider
|
||||
|
||||
let documentsProvider = CloudFileProvider(containerId: nil)
|
||||
```
|
||||
|
||||
@@ -135,11 +149,13 @@ let documentsProvider = CloudFileProvider(containerId: nil)
|
||||
For remote file providers authentication may be necessary:
|
||||
|
||||
``` swift
|
||||
import FilesProvider
|
||||
|
||||
let credential = URLCredential(user: "user", password: "pass", persistence: .permanent)
|
||||
let webdavProvider = WebDAVFileProvider(baseURL: URL(string: "http://www.example.com/dav")!, credential: credential)
|
||||
```
|
||||
|
||||
* In case you want to connect non-secure servers for WebDAV (http) in iOS 9+ / macOS 10.11+ you should disable App Transport Security (ATS) according to [this guide.](https://gist.github.com/mlynch/284699d676fe9ed0abfa)
|
||||
* In case you want to connect non-secure servers for WebDAV (http) or FTP in iOS 9+ / macOS 10.11+ you should disable App Transport Security (ATS) according to [this guide.](https://gist.github.com/mlynch/284699d676fe9ed0abfa)
|
||||
|
||||
* For Dropbox & OneDrive, user is clientID and password is Token which both must be retrieved via [OAuth2 API of Dropbox](https://www.dropbox.com/developers/reference/oauth-guide). There are libraries like [p2/OAuth2](https://github.com/p2/OAuth2) or [OAuthSwift](https://github.com/OAuthSwift/OAuthSwift) which can facilate the procedure to retrieve token. The latter is easier to use and prefered.
|
||||
|
||||
@@ -151,7 +167,7 @@ You can use `url(of:)` method if provider to get direct access url (local or rem
|
||||
|
||||
For updating User interface please consider using delegate method instead of completion handlers. Delegate methods are guaranteed to run in main thread to avoid bugs.
|
||||
|
||||
It's simply three method which indicated whether the operation failed, succeed and how much of operation has been done (suitable for uploading and downloading operations).
|
||||
There's simply three method which indicated whether the operation failed, succeed and how much of operation has been done (suitable for uploading and downloading operations).
|
||||
|
||||
Your class should conforms `FileProviderDelegate` class:
|
||||
|
||||
@@ -160,7 +176,7 @@ override func viewDidLoad() {
|
||||
documentsProvider.delegate = self as FileProviderDelegate
|
||||
}
|
||||
|
||||
func fileproviderSucceed(_ fileProvider: FileProviderOperations, operation: FileOperation) {
|
||||
func fileproviderSucceed(_ fileProvider: FileProviderOperations, operation: FileOperationType) {
|
||||
switch operation {
|
||||
case .copy(source: let source, destination: let dest):
|
||||
print("\(source) copied to \(dest).")
|
||||
@@ -171,7 +187,7 @@ func fileproviderSucceed(_ fileProvider: FileProviderOperations, operation: File
|
||||
}
|
||||
}
|
||||
|
||||
func fileproviderFailed(_ fileProvider: FileProviderOperations, operation: FileOperation) {
|
||||
func fileproviderFailed(_ fileProvider: FileProviderOperations, operation: FileOperationType) {
|
||||
switch operation {
|
||||
case .copy(source: let source, destination: let dest):
|
||||
print("copy of \(source) failed.")
|
||||
@@ -182,7 +198,7 @@ func fileproviderFailed(_ fileProvider: FileProviderOperations, operation: FileO
|
||||
}
|
||||
}
|
||||
|
||||
func fileproviderProgress(_ fileProvider: FileProviderOperations, operation: FileOperation, progress: Float) {
|
||||
func fileproviderProgress(_ fileProvider: FileProviderOperations, operation: FileOperationType, progress: Float) {
|
||||
switch operation {
|
||||
case .copy(source: let source, destination: let dest):
|
||||
print("Copy\(source) to \(dest): \(progress * 100) completed.")
|
||||
@@ -204,7 +220,7 @@ You can also implement `FileOperationDelegate` protocol to control behaviour of
|
||||
|
||||
`fileProvider(shouldProceedAfterError:, operation:)` will be called if an error occured during file operations. Return `true` if you want to continue operation on next files or `false` if you want stop operation further. Default value is false if you don't implement delegate.
|
||||
|
||||
**Note: these methods will be called for files in a directory and its subfolders recursively.**
|
||||
**Note: In `LocalFileProvider`, these methods will be called for files in a directory and its subfolders recursively.**
|
||||
|
||||
### Directory contents and file attributes
|
||||
|
||||
@@ -230,10 +246,10 @@ To get list of files in a directory:
|
||||
documentsProvider.contentsOfDirectory(path: "/", completionHandler: {
|
||||
contents, error in
|
||||
for file in contents {
|
||||
print("Name: \(attributes.name)")
|
||||
print("Size: \(attributes.size)")
|
||||
print("Creation Date: \(attributes.creationDate)")
|
||||
print("Modification Date: \(attributes.modifiedDate)")
|
||||
print("Name: \(file.name)")
|
||||
print("Size: \(file.size)")
|
||||
print("Creation Date: \(file.creationDate)")
|
||||
print("Modification Date: \(file.modifiedDate)")
|
||||
}
|
||||
})
|
||||
```
|
||||
@@ -250,29 +266,21 @@ func storageProperties(completionHandler: { total, used in
|
||||
|
||||
* if this function is unavailable on provider or an error has been occurred, total space will be reported `-1` and used space `0`
|
||||
|
||||
### Change current directory
|
||||
|
||||
```swift
|
||||
documentsProvider.currentPath = "/New Folder"
|
||||
// now path is ~/Documents/New Folder
|
||||
```
|
||||
|
||||
You can then pass "" (empty string) to `contentsOfDirectory` method to list files in current directory.
|
||||
|
||||
### Creating File and Folders
|
||||
|
||||
Creating new directory:
|
||||
|
||||
```swift
|
||||
documentsProvider.create(folder: "new folder", at: "/", completionHandler: nil)
|
||||
documentsProvider.create(folder: "new folder", at: "/", completionHandler: { error in
|
||||
if let error = error {
|
||||
// Error handling here
|
||||
} else {
|
||||
// The operation succeed
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Creating new file from data:
|
||||
|
||||
```swift
|
||||
let data = "hello world!".data(encoding: .utf8)
|
||||
documentsProvider.create(file: "newFile.txt", at: "/", contents: data, completionHandler: nil)
|
||||
```
|
||||
To create a file, use `writeContents(path:, content:, atomically:, completionHandler:)` method.
|
||||
|
||||
### Copy and Move/Rename Files
|
||||
|
||||
@@ -296,11 +304,9 @@ documentsProvider.moveItem(path: "new folder/old.txt", to: "new.txt", overwrite:
|
||||
documentsProvider.removeItem(path: "new.txt", completionHandler: nil)
|
||||
```
|
||||
|
||||
***Caution:*** This method will delete directories with all it's contents recursively.
|
||||
|
||||
### Fetching Contents of File
|
||||
|
||||
There is two method for this purpose, one of them loads entire file into NSData and another can load a portion of file.
|
||||
There is two method for this purpose, one of them loads entire file into `Data` and another can load a portion of file.
|
||||
|
||||
```swift
|
||||
documentsProvider.contents(path: "old.txt", completionHandler: {
|
||||
@@ -329,6 +335,33 @@ let data = "What's up Newyork!".data(encoding: .utf8)
|
||||
documentsProvider.writeContents(path: "old.txt", content: data, atomically: true, completionHandler: nil)
|
||||
```
|
||||
|
||||
### Copying Files to and From Local Storage
|
||||
|
||||
There are two methods to download and upload files between provider's and local storage. These methods use `URLSessionDownloadTask` and `URLSessionUploadTask` classes and allows to use background session and provide progress via delegate.
|
||||
|
||||
To upload a file:
|
||||
|
||||
```swift
|
||||
let fileURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("image.jpg")
|
||||
documentsProvider.copyItem(localFile: fileURL, to: "/upload/image.jpg", overwrite: true, completionHandler: nil)
|
||||
```
|
||||
|
||||
To download a file:
|
||||
|
||||
```swift
|
||||
let fileURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("image.jpg")
|
||||
documentsProvider.copyItem(path: "/download/image.jpg", toLocalURL: fileURL, overwrite: true, completionHandler: nil)
|
||||
```
|
||||
|
||||
* It's safe only to assume these methods **won't** handle directories to upload/download recursively. If you need, you can list directories, create directories on target and copy files using these methods.
|
||||
* FTP provider allows developer to either use apple implemented `URLSessionDownloadTask` or custom implemented method based on stream task via `useAppleImplementation` property. FTP protocol is not supported by background session.
|
||||
|
||||
### Operation Progress
|
||||
|
||||
Creating/Copying/Deleting/Searching functions return a `(NS)Progress`. It provides operation type, progress and a `.cancel()` method which allows you to cancel operation in midst. You can check `cancellable` property to check either you can cancel operation via this object or not.
|
||||
|
||||
- **Note:** Progress reporting is not supported by native `(NS)FileManager` so `LocalFileProvider`.
|
||||
|
||||
### Undo Operations
|
||||
|
||||
Providers conform to `FileProviderUndoable` can perform undo for **some** operations like moving/renaming, copying and creating (file or folder). **Now, only `LocalFileProvider` supports this feature.** To implement:
|
||||
@@ -372,11 +405,9 @@ class ViewController: UIViewController
|
||||
}
|
||||
```
|
||||
|
||||
### Operation Handle
|
||||
### File Coordination
|
||||
|
||||
Creating/Copying/Deleting functions return a `OperationHandle` for remote operations. It provides operation type, progress and a `.cancel()` method which allows you to cancel operation in midst.
|
||||
|
||||
It's not supported by native `(NS)FileManager` so `LocalFileProvider`, but this functionality will be added to future `PosixFileProvider` class.
|
||||
`LocalFileProvider` and its descendents has a `isCoordinating` property. By setting this, provider will use `NSFileCoordinating` class to do all file operations. It's mandatory for iCloud, while recommended when using shared container or anywhere that simultaneous operations on a file/folder is common.
|
||||
|
||||
### Monitoring File Changes
|
||||
|
||||
@@ -384,12 +415,12 @@ You can monitor updates in some file system (Local and SMB2), there is three met
|
||||
|
||||
```swift
|
||||
// to register a new notification handler
|
||||
documentsProvider.registerNotifcation(path: provider.currentPath) {
|
||||
documentsProvider.registerNotifcation(path: "/") {
|
||||
// calling functions to update UI
|
||||
}
|
||||
|
||||
// To discontinue monitoring folders:
|
||||
documentsProvider.unregisterNotifcation(path: provider.currentPath)
|
||||
documentsProvider.unregisterNotifcation(path: "/")
|
||||
```
|
||||
|
||||
* **Please note** in LocalFileProvider it will also monitor changes in subfolders. This behaviour can varies according to file system specification.
|
||||
@@ -405,9 +436,10 @@ To check either file thumbnail is supported or not and fetch thumbnail, use (and
|
||||
|
||||
```swift
|
||||
let path = "/newImage.jpg"
|
||||
let thumbSize = CGSize(width: 64, height: 64)
|
||||
let thumbSize = CGSize(width: 64, height: 64) // or nil which renders to default dimension of provider
|
||||
if documentsProvider.thumbnailOfFileSupported(path: path {
|
||||
documentsProvider.thumbnailOfFile(path: file.path, dimension: thumbSize, completionHandler: { (image, error) in
|
||||
// Interacting with UI must be placed in main thread
|
||||
DispatchQueue.main.async {
|
||||
self.previewImage.image = image
|
||||
}
|
||||
@@ -435,6 +467,13 @@ if documentsProvider..propertiesOfFile(path: file.path, completionHandler: { (pr
|
||||
|
||||
We would love for you to contribute to **FileProvider**, check the `LICENSE` file for more info.
|
||||
|
||||
Things you may consider to help us:
|
||||
|
||||
- [ ] Implement request/response stack for `SMBClient`
|
||||
- [ ] Implement Test-case (`XCTest`)
|
||||
- [ ] Add Sample project for iOS
|
||||
- [ ] Add Sample project for macOS
|
||||
|
||||
## Projects in use
|
||||
|
||||
* [EDM - Browse and Receive Files](https://itunes.apple.com/us/app/edm-browse-and-receive-files/id948397575?ls=1&mt=8)
|
||||
@@ -444,16 +483,19 @@ If you used this library in your project, you can open an issue to inform us.
|
||||
|
||||
## Meta
|
||||
|
||||
Amir-Abbas Mousavian – [@amosavian](https://twitter.com/amosavian)
|
||||
Amir-Abbas Mousavian – [@amosavian](https://twitter.com/amosavian)
|
||||
|
||||
Thanks to [Hootan Moradi](https://github.com/hoootan) for designing logo.
|
||||
|
||||
Distributed under the MIT license. See `LICENSE` for more information.
|
||||
|
||||
[https://github.com/amosavian/](https://github.com/amosavian/)
|
||||
|
||||
[cocoapods]: https://cocoapods.org/pods/FileProvider
|
||||
[swift-image]: https://img.shields.io/badge/swift-3.0-orange.svg
|
||||
[cocoapods]: https://cocoapods.org/pods/FilesProvider
|
||||
[cocoapods-old]: https://cocoapods.org/pods/FileProvider
|
||||
[swift-image]: https://img.shields.io/badge/swift-3.0,%203.1-orange.svg
|
||||
[swift-url]: https://swift.org/
|
||||
[platform-image]: https://img.shields.io/cocoapods/p/FileProvider.svg
|
||||
[platform-image]: https://img.shields.io/cocoapods/p/FilesProvider.svg
|
||||
[license-image]: https://img.shields.io/github/license/amosavian/FileProvider.svg
|
||||
[license-url]: LICENSE
|
||||
[codebeat-image]: https://codebeat.co/badges/7b359f48-78eb-4647-ab22-56262a827517
|
||||
@@ -463,7 +505,9 @@ Distributed under the MIT license. See `LICENSE` for more information.
|
||||
[release-url]: https://github.com/amosavian/FileProvider/releases
|
||||
[release-image]: https://img.shields.io/github/release/amosavian/FileProvider.svg
|
||||
[carthage-image]: https://img.shields.io/badge/Carthage-compatible-4BC51D.svg
|
||||
[cocoapods-downloads]: https://img.shields.io/cocoapods/dt/FileProvider.svg
|
||||
[cocoapods-apps]: https://img.shields.io/cocoapods/at/FileProvider.svg
|
||||
[docs-image]: https://img.shields.io/cocoapods/metrics/doc-percent/FileProvider.svg
|
||||
[docs-url]: http://cocoadocs.org/docsets/FileProvider/
|
||||
[cocoapods-downloads-old]: https://img.shields.io/cocoapods/dt/FileProvider.svg
|
||||
[cocoapods-apps-old]: https://img.shields.io/cocoapods/at/FileProvider.svg
|
||||
[cocoapods-downloads]: https://img.shields.io/cocoapods/dt/FilesProvider.svg
|
||||
[cocoapods-apps]: https://img.shields.io/cocoapods/at/FilesProvider.svg
|
||||
[docs-image]: https://img.shields.io/cocoapods/metrics/doc-percent/FilesProvider.svg
|
||||
[docs-url]: http://cocoadocs.org/docsets/FilesProvider/
|
||||
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2014-2017 Marko Tadić <tadija@me.com> http://tadija.net
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+478
-233
@@ -8,7 +8,15 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
open class CloudFileProvider: LocalFileProvider {
|
||||
/**
|
||||
Allows accessing to iCloud Drive stored files. Determine scope when initializing, to either access
|
||||
to public documents folder or files stored as data.
|
||||
|
||||
To setup a functional iCloud container, please
|
||||
[read this page](https://medium.com/ios-os-x-development/icloud-drive-documents-1a46b5706fe1).
|
||||
*/
|
||||
open class CloudFileProvider: LocalFileProvider, FileProviderSharing {
|
||||
/// An string to identify type of provider.
|
||||
open override class var type: String { return "iCloudDrive" }
|
||||
|
||||
/// Forces file operations to use `NSFileCoordinating`,
|
||||
@@ -18,6 +26,7 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
return true
|
||||
}
|
||||
set {
|
||||
assert(true, "CloudFileProvider.isCoorinating can't be set")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -28,6 +37,9 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
/// Scope of container, indicates user can manipulate data/files or not.
|
||||
open fileprivate(set) var scope: UbiquitousScope
|
||||
|
||||
/// Set this property to ignore initiations asserting to be on secondary thread
|
||||
static open var asserting: Bool = true
|
||||
|
||||
/**
|
||||
Initializes the provider for the iCloud container associated with the specified identifier and
|
||||
establishes access to that container.
|
||||
@@ -40,8 +52,8 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
- Parameter scope: Use `.documents` (default) to put documents that the user is allowed to access inside a Documents subdirectory. Otherwise use `.data` to store user-related data files that your app needs to share but that are not files you want the user to manipulate directly.
|
||||
*/
|
||||
public init? (containerId: String?, scope: UbiquitousScope = .documents) {
|
||||
assert(!Thread.isMainThread, "LocalFileProvider.init(containerId:) is not recommended to be executed on Main Thread.")
|
||||
guard FileManager.default.ubiquityIdentityToken == nil else {
|
||||
assert(!(CloudFileProvider.asserting && Thread.isMainThread), "CloudFileProvider.init(containerId:) is not recommended to be executed on Main Thread.")
|
||||
guard FileManager.default.ubiquityIdentityToken != nil else {
|
||||
return nil
|
||||
}
|
||||
guard let ubiquityURL = FileManager.default.url(forUbiquityContainerIdentifier: containerId) else {
|
||||
@@ -51,17 +63,22 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
self.scope = scope
|
||||
let baseURL: URL
|
||||
if scope == .documents {
|
||||
baseURL = ubiquityURL.standardized.appendingPathComponent("Documents/")
|
||||
baseURL = ubiquityURL.appendingPathComponent("Documents/")
|
||||
} else {
|
||||
baseURL = ubiquityURL.standardized
|
||||
baseURL = ubiquityURL
|
||||
}
|
||||
|
||||
super.init(baseURL: baseURL)
|
||||
self.isCoorinating = true
|
||||
|
||||
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: .concurrent)
|
||||
#if swift(>=3.1)
|
||||
let queueLabel = "FileProvider.\(Swift.type(of: self).type)"
|
||||
#else
|
||||
let queueLabel = "FileProvider.\(type(of: self).type)"
|
||||
#endif
|
||||
dispatch_queue = DispatchQueue(label: queueLabel, attributes: .concurrent)
|
||||
operation_queue = OperationQueue()
|
||||
operation_queue.name = "FileProvider.\(type(of: self).type).Operation"
|
||||
operation_queue.name = "\(queueLabel).Operation"
|
||||
|
||||
fileManager.url(forUbiquityContainerIdentifier: containerId)
|
||||
opFileManager.url(forUbiquityContainerIdentifier: containerId)
|
||||
@@ -69,16 +86,54 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
try? fileManager.createDirectory(at: baseURL, withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
// FIXME: create runloop for dispatch_queue, start query on it
|
||||
|
||||
public required convenience init?(coder aDecoder: NSCoder) {
|
||||
guard let containerId = aDecoder.decodeObject(forKey: "containerId") as? String,
|
||||
let scopeString = aDecoder.decodeObject(forKey: "scope") as? String,
|
||||
let scope = UbiquitousScope(rawValue: scopeString) else {
|
||||
return nil
|
||||
}
|
||||
self.init(containerId: containerId, scope: scope)
|
||||
self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? ""
|
||||
self.isCoorinating = aDecoder.decodeBool(forKey: "isCoorinating")
|
||||
}
|
||||
|
||||
open override func encode(with aCoder: NSCoder) {
|
||||
aCoder.encode(self.containerId, forKey: "containerId")
|
||||
aCoder.encode(self.scope.rawValue, forKey: "scope")
|
||||
aCoder.encode(self.currentPath, forKey: "currentPath")
|
||||
aCoder.encode(self.isCoorinating, forKey: "isCoorinating")
|
||||
}
|
||||
|
||||
open override func copy(with zone: NSZone? = nil) -> Any {
|
||||
let copy = CloudFileProvider(containerId: self.containerId, scope: self.scope)
|
||||
copy?.currentPath = self.currentPath
|
||||
copy?.delegate = self.delegate
|
||||
copy?.fileOperationDelegate = self.fileOperationDelegate
|
||||
return copy as Any
|
||||
}
|
||||
|
||||
/**
|
||||
Returns an Array of `FileObject`s identifying the the directory entries via asynchronous completion handler.
|
||||
|
||||
If the directory contains no entries or an error is occured, this method will return the empty array.
|
||||
|
||||
- Parameter path: path to target directory. If empty, root will be iterated.
|
||||
- Parameter completionHandler: a closure with result of directory entries or error.
|
||||
`contents`: An array of `FileObject` identifying the the directory entries.
|
||||
`error`: Error returned by system.
|
||||
*/
|
||||
open override func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
|
||||
// FIXME: create runloop for dispatch_queue, start query on it
|
||||
dispatch_queue.async {
|
||||
let pathURL = self.url(of: path)
|
||||
|
||||
let query = NSMetadataQuery()
|
||||
query.predicate = NSPredicate(format: "%K BEGINSWITH %@", NSMetadataItemPathKey, pathURL.path)
|
||||
query.valueListAttributes = [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]
|
||||
query.searchScopes = [self.scope.rawValue]
|
||||
var finishObserver: NSObjectProtocol?
|
||||
finishObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
|
||||
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
|
||||
defer {
|
||||
query.stop()
|
||||
NotificationCenter.default.removeObserver(finishObserver!)
|
||||
@@ -106,29 +161,46 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
}
|
||||
|
||||
query.stop()
|
||||
completionHandler(contents, nil)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(contents, nil)
|
||||
}
|
||||
|
||||
})
|
||||
DispatchQueue.main.async {
|
||||
if !query.start() {
|
||||
completionHandler([], self.throwError(path, code: CocoaError.fileReadNoPermission))
|
||||
self.dispatch_queue.async {
|
||||
completionHandler([], self.throwError(path, code: CocoaError.fileReadNoPermission))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Please don't rely this function to get iCloud drive total and remaining capacity
|
||||
/// - Important: iCloud Storage size and free space is unavailable, it returns local space
|
||||
open override func storageProperties(completionHandler: (@escaping (_ total: Int64, _ used: Int64) -> Void)) {
|
||||
super.storageProperties(completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns a `FileObject` containing the attributes of the item (file, directory, symlink, etc.) at the path in question via asynchronous completion handler.
|
||||
|
||||
If the directory contains no entries or an error is occured, this method will return the empty `FileObject`.
|
||||
|
||||
- Parameter path: path to target directory. If empty, attributes of root will be returned.
|
||||
- Parameter completionHandler: a closure with result of directory entries or error.
|
||||
`attributes`: A `FileObject` containing the attributes of the item.
|
||||
`error`: Error returned by system.
|
||||
*/
|
||||
open override func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
|
||||
dispatch_queue.async {
|
||||
let pathURL = self.url(of: path)
|
||||
let query = NSMetadataQuery()
|
||||
query.predicate = NSPredicate(format: "%K LIKE %@", NSMetadataItemPathKey, pathURL.path)
|
||||
query.valueListAttributes = [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]
|
||||
query.searchScopes = [self.scope.rawValue]
|
||||
var finishObserver: NSObjectProtocol?
|
||||
finishObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
|
||||
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
|
||||
defer {
|
||||
query.stop()
|
||||
NotificationCenter.default.removeObserver(finishObserver!)
|
||||
@@ -138,145 +210,105 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
|
||||
guard let result = (query.results as? [NSMetadataItem])?.first, let attribs = result.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
|
||||
let error = self.throwError(path, code: CocoaError.fileNoSuchFile)
|
||||
completionHandler(nil, error)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let file = self.mapFileObject(attributes: attribs) {
|
||||
completionHandler(file, nil)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(file, nil)
|
||||
}
|
||||
} else {
|
||||
let noFileError = self.throwError(path, code: CocoaError.fileNoSuchFile)
|
||||
completionHandler(nil, noFileError)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, noFileError)
|
||||
}
|
||||
}
|
||||
})
|
||||
DispatchQueue.main.async {
|
||||
if !query.start() {
|
||||
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadNoPermission))
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadNoPermission))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open override func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard let r = super.create(folder: folderName, at: atPath, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open override func create(file fileName: String, at atPath: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard let r = super.create(file: fileName, at: atPath, contents: data, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open override func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard let r = super.moveItem(path: path, to: toPath, overwrite: overwrite, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open override func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard let r = super.copyItem(path: path, to: toPath, overwrite: overwrite, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open override func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard let r = super.removeItem(path: path, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open override func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
// TODO: Make use of overwrite parameter
|
||||
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
|
||||
operation_queue.addOperation {
|
||||
let tempFolder: URL
|
||||
if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) {
|
||||
tempFolder = FileManager.default.temporaryDirectory
|
||||
/**
|
||||
Search files inside directory using query asynchronously.
|
||||
|
||||
- Note: For now only it's limited to file names. `query` parameter may take `NSPredicate` format in near future.
|
||||
|
||||
- Parameters:
|
||||
- path: location of directory to start search
|
||||
- recursive: Searching subdirectories of path
|
||||
- query: Simple string of file name to be search (for now).
|
||||
- foundItemHandler: Closure which is called when a file is found
|
||||
- completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured.
|
||||
*/
|
||||
open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? {
|
||||
|
||||
let mapDict: [String: String] = ["url": NSMetadataItemURLKey, "name": NSMetadataItemFSNameKey, "path": NSMetadataItemPathKey, "filesize": NSMetadataItemFSSizeKey, "modifiedDate": NSMetadataItemFSContentChangeDateKey, "creationDate": NSMetadataItemFSCreationDateKey, "contentType": NSMetadataItemContentTypeKey]
|
||||
|
||||
func updateQueryKeys(_ queryComponent: NSPredicate) -> NSPredicate {
|
||||
if let cQuery = queryComponent as? NSCompoundPredicate {
|
||||
let newSub = cQuery.subpredicates.map { updateQueryKeys($0 as! NSPredicate) }
|
||||
switch cQuery.compoundPredicateType {
|
||||
case .and: return NSCompoundPredicate(andPredicateWithSubpredicates: newSub)
|
||||
case .not: return NSCompoundPredicate(notPredicateWithSubpredicate: newSub.first!)
|
||||
case .or: return NSCompoundPredicate(orPredicateWithSubpredicates: newSub)
|
||||
}
|
||||
} else if let cQuery = queryComponent as? NSComparisonPredicate {
|
||||
var newLeft = cQuery.leftExpression
|
||||
var newRight = cQuery.rightExpression
|
||||
if newLeft.expressionType == .keyPath, let newKey = mapDict[newLeft.keyPath] {
|
||||
newLeft = NSExpression(forKeyPath: newKey)
|
||||
}
|
||||
if newRight.expressionType == .keyPath, let newKey = mapDict[newRight.keyPath] {
|
||||
newRight = NSExpression(forKeyPath: newKey)
|
||||
}
|
||||
if newLeft.expressionType == .keyPath, newLeft.keyPath == "type" {
|
||||
newRight = NSExpression(forConstantValue: newRight.constantValue as? String == "directory" ? "public.directory": "public.data")
|
||||
}
|
||||
if newRight.expressionType == .keyPath, newRight.keyPath == "type" {
|
||||
newLeft = NSExpression(forConstantValue: newLeft.constantValue as? String == "directory" ? "public.directory": "public.data")
|
||||
}
|
||||
return NSComparisonPredicate(leftExpression: newLeft, rightExpression: newRight, modifier: cQuery.comparisonPredicateModifier, type: cQuery.predicateOperatorType, options: cQuery.options)
|
||||
} else {
|
||||
tempFolder = URL(fileURLWithPath: NSTemporaryDirectory())
|
||||
}
|
||||
let tmpFile = tempFolder.appendingPathComponent(UUID().uuidString)
|
||||
|
||||
do {
|
||||
try self.opFileManager.copyItem(at: localFile, to: tmpFile)
|
||||
let toUrl = self.url(of: toPath)
|
||||
try self.opFileManager.setUbiquitous(true, itemAt: tmpFile, destinationURL: toUrl)
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
})
|
||||
} catch let e {
|
||||
if self.opFileManager.fileExists(atPath: tmpFile.path) {
|
||||
try? self.opFileManager.removeItem(at: tmpFile)
|
||||
}
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
})
|
||||
return queryComponent
|
||||
}
|
||||
}
|
||||
return CloudOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open override func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
|
||||
|
||||
do {
|
||||
try self.opFileManager.startDownloadingUbiquitousItem(at: self.url(of: path))
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
let progress = Progress(parent: nil, userInfo: nil)
|
||||
|
||||
guard let r = super.copyItem(path: path, toLocalURL: toLocalURL, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open override func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
guard let r = super.contents(path: path, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open override func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
guard let r = super.contents(path: path, offset: offset, length: length, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open override func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard let r = super.writeContents(path: path, contents: data, atomically: atomically, overwrite: overwrite, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
open override func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
|
||||
dispatch_queue.async {
|
||||
let pathURL = self.url(of: path)
|
||||
let query = NSMetadataQuery()
|
||||
query.predicate = NSPredicate(format: "(%K BEGINSWITH %@) && (%K LIKE %@)", NSMetadataItemPathKey, pathURL.path, NSMetadataItemFSNameKey, query)
|
||||
query.searchScopes = [self.scope.rawValue]
|
||||
progress.setUserInfoObject(pathURL, forKey: .fileURLKey)
|
||||
let mdquery = NSMetadataQuery()
|
||||
mdquery.predicate = NSPredicate(format: "(%K BEGINSWITH %@) && (\(updateQueryKeys(query).predicateFormat))", NSMetadataItemPathKey, pathURL.path)
|
||||
mdquery.searchScopes = [self.scope.rawValue]
|
||||
|
||||
var lastReportedCount = 0
|
||||
|
||||
progress.cancellationHandler = { [weak mdquery] in
|
||||
mdquery?.stop()
|
||||
}
|
||||
|
||||
if let foundItemHandler = foundItemHandler {
|
||||
var updateObserver: NSObjectProtocol?
|
||||
|
||||
updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryGatheringProgress, object: query, queue: nil, using: { (notification) in
|
||||
// FIXME: Remove this section as it won't work as expected on iCloud
|
||||
updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryGatheringProgress, object: mdquery, queue: nil, using: { (notification) in
|
||||
|
||||
query.disableUpdates()
|
||||
mdquery.disableUpdates()
|
||||
|
||||
guard query.resultCount > lastReportedCount else { return }
|
||||
guard mdquery.resultCount > lastReportedCount else { return }
|
||||
|
||||
for index in lastReportedCount..<query.resultCount {
|
||||
guard let attribs = (query.result(at: index) as? NSMetadataItem)?.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
|
||||
for index in lastReportedCount..<mdquery.resultCount {
|
||||
guard let attribs = (mdquery.result(at: index) as? NSMetadataItem)?.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -288,24 +320,25 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
foundItemHandler(file)
|
||||
}
|
||||
}
|
||||
lastReportedCount = query.resultCount
|
||||
lastReportedCount = mdquery.resultCount
|
||||
progress.totalUnitCount = Int64(lastReportedCount)
|
||||
|
||||
query.enableUpdates()
|
||||
mdquery.enableUpdates()
|
||||
})
|
||||
}
|
||||
|
||||
var finishObserver: NSObjectProtocol?
|
||||
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
|
||||
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: mdquery, queue: nil, using: { (notification) in
|
||||
defer {
|
||||
query.stop()
|
||||
mdquery.stop()
|
||||
NotificationCenter.default.removeObserver(finishObserver!)
|
||||
}
|
||||
|
||||
guard let results = query.results as? [NSMetadataItem] else {
|
||||
guard let results = mdquery.results as? [NSMetadataItem] else {
|
||||
return
|
||||
}
|
||||
|
||||
query.disableUpdates()
|
||||
mdquery.disableUpdates()
|
||||
|
||||
var contents = [FileObject]()
|
||||
for result in results {
|
||||
@@ -321,25 +354,224 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
contents.append(file)
|
||||
}
|
||||
}
|
||||
|
||||
completionHandler(contents, nil)
|
||||
progress.completedUnitCount = Int64(contents.count)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(contents, nil)
|
||||
}
|
||||
})
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if !query.start() {
|
||||
completionHandler([], self.throwError(path, code: CocoaError.fileReadNoPermission))
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
if !mdquery.start() {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler([], self.throwError(path, code: CocoaError.fileReadNoPermission))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return progress
|
||||
}
|
||||
|
||||
open override func isReachable(completionHandler: @escaping (Bool) -> Void) {
|
||||
dispatch_queue.async {
|
||||
completionHandler(self.fileManager.ubiquityIdentityToken != nil)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Removes the file or directory at the specified path.
|
||||
|
||||
- Important: Due to a bug (race condition?) in Apple API, it takes about 3-5 seconds to update containing folder
|
||||
list and triggering notification registered for directory while completion handler will run almost immediately.
|
||||
It's your responsibility to workaourd this bug/feature and mark file as deleted in your software.
|
||||
|
||||
- Parameters:
|
||||
- path: file or directory path.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: A `Progress` object to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
open override func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
return super.removeItem(path: path, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
/**
|
||||
Uploads a file from local file url to designated path asynchronously.
|
||||
Method will fail if source is not a local url with `file://` scheme.
|
||||
|
||||
- Parameters:
|
||||
- localFile: a file url to file.
|
||||
- to: destination path of file, including file/directory name.
|
||||
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: A `Progress` object to get progress or cancel progress.
|
||||
*/
|
||||
@discardableResult
|
||||
open override func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
// TODO: Make use of overwrite parameter
|
||||
let operation = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
|
||||
let progress = Progress(parent: nil, userInfo: nil)
|
||||
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
||||
progress.kind = .file
|
||||
progress.isCancellable = false
|
||||
progress.setUserInfoObject(localFile, forKey: .fileURLKey)
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
||||
monitorFile(path: toPath, operation: operation, progress: progress)
|
||||
operation_queue.addOperation {
|
||||
let tempFolder: URL
|
||||
if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) {
|
||||
tempFolder = FileManager.default.temporaryDirectory
|
||||
} else {
|
||||
tempFolder = URL(fileURLWithPath: NSTemporaryDirectory())
|
||||
}
|
||||
let tmpFile = tempFolder.appendingPathComponent(UUID().uuidString)
|
||||
|
||||
do {
|
||||
try self.opFileManager.copyItem(at: localFile, to: tmpFile)
|
||||
let toUrl = self.url(of: toPath)
|
||||
try self.opFileManager.setUbiquitous(true, itemAt: tmpFile, destinationURL: toUrl)
|
||||
completionHandler?(nil)
|
||||
self.delegateNotify(operation)
|
||||
} catch let e {
|
||||
if self.opFileManager.fileExists(atPath: tmpFile.path) {
|
||||
try? self.opFileManager.removeItem(at: tmpFile)
|
||||
}
|
||||
completionHandler?(e)
|
||||
self.delegateNotify(operation, error: e)
|
||||
}
|
||||
}
|
||||
return progress
|
||||
}
|
||||
|
||||
/**
|
||||
Download a file from `path` to designated local file url asynchronously.
|
||||
Method will fail if destination is not a local url with `file://` scheme.
|
||||
|
||||
- Parameters:
|
||||
- path: original file or directory path.
|
||||
- toLocalURL: destination local url of file, including file/directory name.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: A `Progress` object to get progress or cancel progress.
|
||||
*/
|
||||
@discardableResult
|
||||
open override func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let operation = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
|
||||
let progress = Progress(parent: nil, userInfo: nil)
|
||||
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
||||
progress.kind = .file
|
||||
progress.isCancellable = false
|
||||
progress.setUserInfoObject(self.url(of: path), forKey: .fileURLKey)
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
||||
monitorFile(path: path, operation: operation, progress: progress)
|
||||
do {
|
||||
try self.opFileManager.startDownloadingUbiquitousItem(at: self.url(of: path))
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
self.delegateNotify(operation, error: e)
|
||||
return nil
|
||||
}
|
||||
let _ = super.copyItem(path: path, toLocalURL: toLocalURL, completionHandler: completionHandler)
|
||||
return progress
|
||||
}
|
||||
|
||||
/**
|
||||
Retreives a `Data` object with the contents of the file asynchronously vis contents argument of completion handler.
|
||||
If path specifies a directory, or if some other error occurs, data will be nil.
|
||||
|
||||
- Parameters:
|
||||
- path: Path of file.
|
||||
- completionHandler: a closure with result of file contents or error.
|
||||
`contents`: contents of file in a `Data` object.
|
||||
`error`: Error returned by system.
|
||||
- Returns: A `Progress` object to get progress or cancel progress.
|
||||
*/
|
||||
@discardableResult
|
||||
open override func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
|
||||
let operation = FileOperationType.fetch(path: path)
|
||||
let progress = Progress(parent: nil, userInfo: nil)
|
||||
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
||||
progress.kind = .file
|
||||
progress.setUserInfoObject(self.url(of: path), forKey: .fileURLKey)
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
||||
monitorFile(path: path, operation: operation, progress: progress)
|
||||
_ = super.contents(path: path, completionHandler: completionHandler)
|
||||
return progress
|
||||
}
|
||||
|
||||
/**
|
||||
Retreives a `Data` object with a portion contents of the file asynchronously vis contents argument of completion handler.
|
||||
If path specifies a directory, or if some other error occurs, data will be nil.
|
||||
|
||||
- Parameters:
|
||||
- path: Path of file.
|
||||
- offset: First byte index which should be read. **Starts from 0.**
|
||||
- length: Bytes count of data. Pass `-1` to read until the end of file.
|
||||
- completionHandler: a closure with result of file contents or error.
|
||||
`contents`: contents of file in a `Data` object.
|
||||
`error`: Error returned by system.
|
||||
- Returns: A `Progress` object to get progress or cancel progress.
|
||||
*/
|
||||
@discardableResult
|
||||
open override func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
|
||||
let operation = FileOperationType.fetch(path: path)
|
||||
let progress = Progress(parent: nil, userInfo: nil)
|
||||
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
||||
progress.kind = .file
|
||||
progress.setUserInfoObject(self.url(of: path), forKey: .fileURLKey)
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
||||
monitorFile(path: path, operation: operation, progress: progress)
|
||||
_ = super.contents(path: path, offset: offset, length: length, completionHandler: completionHandler)
|
||||
return progress
|
||||
}
|
||||
|
||||
/**
|
||||
Write the contents of the `Data` to a location asynchronously.
|
||||
|
||||
- Parameters:
|
||||
- path: Path of target file.
|
||||
- contents: Data to be written into file.
|
||||
- overwrite: Destination file should be overwritten if file is already exists. Default is `false`.
|
||||
- atomically: data will be written to a temporary file before writing to final location. Default is `false`.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: A `Progress` object to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
open override func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let operation = FileOperationType.fetch(path: path)
|
||||
let progress = Progress(parent: nil, userInfo: nil)
|
||||
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
||||
progress.kind = .file
|
||||
progress.setUserInfoObject(self.url(of: path), forKey: .fileURLKey)
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
||||
monitorFile(path: path, operation: operation, progress: progress)
|
||||
_ = super.writeContents(path: path, contents: data, atomically: atomically, overwrite: overwrite, completionHandler: completionHandler)
|
||||
return progress
|
||||
}
|
||||
|
||||
fileprivate var monitors = [String: (NSMetadataQuery, NSObjectProtocol)]()
|
||||
|
||||
/**
|
||||
Starts monitoring a path and its subpaths, including files and folders, for any change,
|
||||
including copy, move/rename, content changes, etc.
|
||||
To avoid thread congestion, `evetHandler` will be triggered with 0.2 seconds interval,
|
||||
and has a 0.25 second delay, to ensure it's called after updates.
|
||||
|
||||
- Note: this functionality is available only in `LocalFileProvider` and `CloudFileProvider`.
|
||||
- Note: `eventHandler` is not called on main thread, for updating UI. dispatch routine to main thread.
|
||||
- Important: `eventHandler` may be called if file is changed in recursive subpaths of registered path.
|
||||
This may cause negative impact on performance if a root path is being monitored.
|
||||
|
||||
- Parameters:
|
||||
- path: path of directory.
|
||||
- eventHandler: Closure executed after change, on a secondary thread.
|
||||
*/
|
||||
open override func registerNotifcation(path: String, eventHandler: @escaping (() -> Void)) {
|
||||
self.unregisterNotifcation(path: path)
|
||||
let pathURL = self.url(of: path)
|
||||
let query = NSMetadataQuery()
|
||||
query.predicate = NSPredicate(format: "(%K BEGINSWITH %@)", NSMetadataItemPathKey, pathURL.path)
|
||||
query.valueListAttributes = []
|
||||
query.searchScopes = [self.scope.rawValue]
|
||||
|
||||
let updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidUpdate, object: query, queue: nil, using: { (notification) in
|
||||
@@ -358,6 +590,9 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/// Stops monitoring the path.
|
||||
///
|
||||
/// - Parameter path: path of directory.
|
||||
open override func unregisterNotifcation(path: String) {
|
||||
guard let (query, observer) = monitors[path] else {
|
||||
return
|
||||
@@ -368,27 +603,22 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
monitors.removeValue(forKey: path)
|
||||
}
|
||||
|
||||
/// Investigate either the path is registered for change notification or not.
|
||||
///
|
||||
/// - Parameter path: path of directory.
|
||||
/// - Returns: Directory is being monitored or not.
|
||||
open override func isRegisteredForNotification(path: String) -> Bool {
|
||||
return monitors[path] != nil
|
||||
}
|
||||
|
||||
open override func copy(with zone: NSZone? = nil) -> Any {
|
||||
let copy = CloudFileProvider(containerId: self.containerId)
|
||||
copy?.currentPath = self.currentPath
|
||||
copy?.delegate = self.delegate
|
||||
copy?.fileOperationDelegate = self.fileOperationDelegate
|
||||
copy?.isPathRelative = self.isPathRelative
|
||||
return copy as Any
|
||||
}
|
||||
|
||||
fileprivate func mapFileObject(attributes attribs: [String: Any]) -> FileObject? {
|
||||
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardized, let name = attribs[NSMetadataItemFSNameKey] as? String else {
|
||||
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardizedFileURL, let name = attribs[NSMetadataItemFSNameKey] as? String else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let path = self.relativePathOf(url: url)
|
||||
let rpath = path.hasPrefix("/") ? path.substring(from: path.index(after: path.startIndex)) : path
|
||||
let relativeUrl = URL(string: rpath, relativeTo: self.baseURL)
|
||||
let relativeUrl = URL(string: rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? rpath, relativeTo: self.baseURL)
|
||||
let file = FileObject(url: relativeUrl ?? url, name: name, path: path)
|
||||
|
||||
file.size = (attribs[NSMetadataItemFSSizeKey] as? NSNumber)?.int64Value ?? -1
|
||||
@@ -401,8 +631,69 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
return file
|
||||
}
|
||||
|
||||
/// Removes local copy of file, but spares cloud copy/
|
||||
/// - Parameter path: Path of file or directory to be remoed from local
|
||||
fileprivate func monitorFile(path: String, operation: FileOperationType, progress: Progress?) {
|
||||
dispatch_queue.async {
|
||||
let pathURL = self.url(of: path)
|
||||
let size = pathURL.fileSize
|
||||
progress?.totalUnitCount = size > 0 ? size : 0
|
||||
let query = NSMetadataQuery()
|
||||
query.predicate = NSPredicate(format: "%K LIKE %@", NSMetadataItemPathKey, pathURL.path)
|
||||
query.valueListAttributes = [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataUbiquitousItemPercentDownloadedKey, NSMetadataUbiquitousItemPercentUploadedKey, NSMetadataItemFSSizeKey]
|
||||
query.searchScopes = [self.scope.rawValue]
|
||||
var updateObserver: NSObjectProtocol?
|
||||
updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidUpdate, object: query, queue: nil, using: { (notification) in
|
||||
query.disableUpdates()
|
||||
|
||||
guard let item = (query.results as? [NSMetadataItem])?.first else {
|
||||
return
|
||||
}
|
||||
|
||||
if progress?.totalUnitCount == 0, let size = item.value(forAttribute: NSMetadataItemFSSizeKey) as? Int64 {
|
||||
progress?.totalUnitCount = size
|
||||
}
|
||||
let downloaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentDownloadedKey) as? Double ?? 0
|
||||
let uploaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentUploadedKey) as? Double ?? 0
|
||||
if (downloaded == 0 || downloaded == 100) && (uploaded > 0 && uploaded < 100) {
|
||||
progress?.completedUnitCount = Int64(uploaded / 100 * Double(progress?.totalUnitCount ?? 0))
|
||||
self.delegateNotify(operation, progress: uploaded / 100)
|
||||
} else if (uploaded == 0 || uploaded == 100) && (downloaded > 0 && downloaded < 100) {
|
||||
progress?.completedUnitCount = Int64(downloaded / 100 * Double(progress?.totalUnitCount ?? 0))
|
||||
self.delegateNotify(operation, progress: downloaded / 100)
|
||||
} else if uploaded == 100 || downloaded == 100 {
|
||||
progress?.completedUnitCount = progress?.totalUnitCount ?? 0
|
||||
query.stop()
|
||||
NotificationCenter.default.removeObserver(updateObserver!)
|
||||
self.delegateNotify(operation)
|
||||
}
|
||||
|
||||
query.enableUpdates()
|
||||
})
|
||||
|
||||
DispatchQueue.main.async {
|
||||
progress?.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
query.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 let e {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, nil, nil, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@@ -415,27 +706,34 @@ open class CloudFileProvider: LocalFileProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a pulic url with expiration date, can be shared with other people.
|
||||
open func temporaryLink(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)
|
||||
completionHandler(url, nil, expiration as Date?, nil)
|
||||
} catch let e {
|
||||
completionHandler(nil, nil, nil, e)
|
||||
}
|
||||
}
|
||||
/// Returns current version of file on this device and all versions of files in user devices.
|
||||
/// - Parameter path: Path of file or directory.
|
||||
/// - Parameter completionHandler: Retrieve current version on this device and all versions available. `currentVersion` will be nil if file doesn't exist. If an error parameter was provided, a presentable `Error` will be returned.
|
||||
func versionsOfItem(path: String, completionHandler: @escaping ((_ currentVersion: NSFileVersion?, _ versions: [NSFileVersion], _ error: Error?) -> Void)) {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
/// Resolves conflicts by selecting a version.
|
||||
/// - Parameter path: Path of file or directory.
|
||||
/// - Parameter version: Version than will be choose as main version. `nil` value indicates current version on this device will be selected.
|
||||
/// - Parameter completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
func selectVersionOfItem(path: String, version: NSFileVersion? = nil, completionHandler: SimpleCompletionHandler) {
|
||||
NotImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
/// Scope of iCloud, wrapper for NSMetadataQueryUbiquitous...Scope constants
|
||||
public enum UbiquitousScope: RawRepresentable {
|
||||
/// Search all files not in the Documents directories of the app’s iCloud container directories.
|
||||
/// Use this scope to store user-related data files that your app needs to share
|
||||
/// but that are not files you want the user to manipulate directly.
|
||||
///
|
||||
/// Raw value is equivalent to `NSMetadataQueryUbiquitousDataScope`
|
||||
case data
|
||||
/// Search all files in the Documents directories of the app’s iCloud container directories.
|
||||
/// Put documents that the user is allowed to access inside a Documents subdirectory.
|
||||
///
|
||||
/// Raw value is equivalent to `NSMetadataQueryUbiquitousDocumentsScope`
|
||||
case documents
|
||||
|
||||
public typealias RawValue = String
|
||||
@@ -461,89 +759,36 @@ public enum UbiquitousScope: RawRepresentable {
|
||||
}
|
||||
}
|
||||
|
||||
open class CloudOperationHandle: OperationHandle {
|
||||
public let baseURL: URL?
|
||||
public let operationType: FileOperationType
|
||||
/*
|
||||
func getMetadataItem(url: URL) -> NSMetadataItem? {
|
||||
let query = NSMetadataQuery()
|
||||
query.predicate = NSPredicate(format: "(%K LIKE %@)", NSMetadataItemPathKey, url.path)
|
||||
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope, NSMetadataQueryUbiquitousDataScope]
|
||||
|
||||
init (operationType: FileOperationType, baseURL: URL?) {
|
||||
self.baseURL = baseURL
|
||||
self.operationType = operationType
|
||||
}
|
||||
var item: NSMetadataItem?
|
||||
|
||||
private var sourceURL: URL? {
|
||||
guard let source = operationType.source, let baseURL = baseURL else { return nil }
|
||||
return source.hasPrefix("file://") ? URL(fileURLWithPath: source) : baseURL.appendingPathComponent(source)
|
||||
}
|
||||
|
||||
private var destURL: URL? {
|
||||
guard let dest = operationType.destination, let baseURL = baseURL else { return nil }
|
||||
return dest.hasPrefix("file://") ? URL(fileURLWithPath: dest) : baseURL.appendingPathComponent(dest)
|
||||
}
|
||||
|
||||
open var bytesSoFar: Int64 {
|
||||
assert(!Thread.isMainThread, "Don't run \(#function) method on main thread")
|
||||
|
||||
guard let url = destURL ?? sourceURL, let item = CloudOperationHandle.getMetadataItem(url: url) else { return 0 }
|
||||
let downloaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentDownloadedKey) as? Double ?? 0
|
||||
let uploaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentUploadedKey) as? Double ?? 0
|
||||
guard let size = item.value(forAttribute: NSMetadataItemFSSizeKey) as? Int64 else { return -1 }
|
||||
if (downloaded == 0 || downloaded == 100) && (uploaded > 0 && uploaded < 100) {
|
||||
return Int64(uploaded * (Double(size) / 100))
|
||||
} else if (uploaded == 0 || uploaded == 100) && (downloaded > 0 && downloaded < 100) {
|
||||
return Int64(downloaded * (Double(size) / 100))
|
||||
} else if uploaded == 100 || downloaded == 100 {
|
||||
return size
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
var finishObserver: NSObjectProtocol?
|
||||
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
|
||||
defer {
|
||||
query.stop()
|
||||
group.leave()
|
||||
NotificationCenter.default.removeObserver(finishObserver!)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
open var totalBytes: Int64 {
|
||||
assert(!Thread.isMainThread, "Don't run \(#function) method on main thread")
|
||||
guard let url = destURL ?? sourceURL, let item = CloudOperationHandle.getMetadataItem(url: url) else { return -1 }
|
||||
return item.value(forAttribute: NSMetadataItemFSSizeKey) as? Int64 ?? -1
|
||||
}
|
||||
|
||||
open var inProgress: Bool {
|
||||
guard let url = destURL ?? sourceURL, let item = CloudOperationHandle.getMetadataItem(url: url) else { return false }
|
||||
let downloadStatus = item.value(forAttribute: NSMetadataUbiquitousItemDownloadingStatusKey) as? String ?? NSMetadataUbiquitousItemDownloadingStatusNotDownloaded
|
||||
let isUploading = item.value(forAttribute: NSMetadataUbiquitousItemIsUploadingKey) as? Bool ?? false
|
||||
return downloadStatus == NSMetadataUbiquitousItemDownloadingStatusCurrent || isUploading
|
||||
}
|
||||
|
||||
/// Not usable in local provider
|
||||
open func cancel() -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
fileprivate static func getMetadataItem(url: URL) -> NSMetadataItem? {
|
||||
let query = NSMetadataQuery()
|
||||
query.predicate = NSPredicate(format: "(%K LIKE %@)", NSMetadataItemPathKey, url.path)
|
||||
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope, NSMetadataQueryUbiquitousDataScope]
|
||||
|
||||
var item: NSMetadataItem?
|
||||
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
var finishObserver: NSObjectProtocol?
|
||||
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
|
||||
defer {
|
||||
query.stop()
|
||||
group.leave()
|
||||
NotificationCenter.default.removeObserver(finishObserver!)
|
||||
}
|
||||
|
||||
if query.resultCount > 0 {
|
||||
item = query.result(at: 0) as? NSMetadataItem
|
||||
}
|
||||
|
||||
query.disableUpdates()
|
||||
|
||||
})
|
||||
|
||||
DispatchQueue.main.async {
|
||||
query.start()
|
||||
if query.resultCount > 0 {
|
||||
item = query.result(at: 0) as? NSMetadataItem
|
||||
}
|
||||
_ = group.wait(timeout: DispatchTime.now() + 30)
|
||||
return item
|
||||
|
||||
query.disableUpdates()
|
||||
|
||||
})
|
||||
|
||||
DispatchQueue.main.async {
|
||||
query.start()
|
||||
}
|
||||
_ = group.wait(timeout: .now() + 30)
|
||||
return item
|
||||
}
|
||||
*/
|
||||
|
||||
+184
-279
@@ -10,43 +10,20 @@
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
|
||||
open class DropboxFileProvider: FileProviderBasicRemote {
|
||||
open class var type: String { return "DropBox" }
|
||||
open let isPathRelative: Bool
|
||||
open let baseURL: URL?
|
||||
open var currentPath: String
|
||||
/**
|
||||
Allows accessing to Dropbox stored files. This provider doesn't cache or save files internally, however you can
|
||||
set `useCache` and `cache` properties to use Foundation `NSURLCache` system.
|
||||
|
||||
- Note: Uploading files and data are limited to 150MB, for now.
|
||||
*/
|
||||
open class DropboxFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
override open class var type: String { return "Dropbox" }
|
||||
|
||||
/// Dropbox RPC API URL, which is equal with [https://api.dropboxapi.com/2/](https://api.dropboxapi.com/2/)
|
||||
open let apiURL: URL
|
||||
/// Dropbox contents download/upload API URL, which is equal with [https://content.dropboxapi.com/2/](https://content.dropboxapi.com/2/)
|
||||
open let contentURL: URL
|
||||
|
||||
open var dispatch_queue: DispatchQueue
|
||||
open var operation_queue: OperationQueue {
|
||||
willSet {
|
||||
assert(_session == nil, "It's not effective to change dispatch_queue property after session is initialized.")
|
||||
}
|
||||
}
|
||||
|
||||
open weak var delegate: FileProviderDelegate?
|
||||
open let credential: URLCredential?
|
||||
open private(set) var cache: URLCache?
|
||||
public var useCache: Bool
|
||||
public var validatingCache: Bool
|
||||
|
||||
fileprivate var _session: URLSession?
|
||||
fileprivate var sessionDelegate: SessionDelegate?
|
||||
public var session: URLSession {
|
||||
if _session == nil {
|
||||
self.sessionDelegate = SessionDelegate(fileProvider: self, credential: credential)
|
||||
let config = URLSessionConfiguration.default
|
||||
config.urlCache = cache
|
||||
config.requestCachePolicy = .returnCacheDataElseLoad
|
||||
_session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDelegate?, delegateQueue: self.operation_queue)
|
||||
}
|
||||
return _session!
|
||||
}
|
||||
|
||||
/**
|
||||
Initializer for Dropbox provider with given client ID and Token.
|
||||
These parameters must be retrieved via [OAuth2 API of Dropbox](https://www.dropbox.com/developers/reference/oauth-guide).
|
||||
@@ -55,55 +32,53 @@ open class DropboxFileProvider: FileProviderBasicRemote {
|
||||
The latter is easier to use and prefered. Also you can use [auth0/Lock](https://github.com/auth0/Lock.iOS-OSX) which provides graphical user interface.
|
||||
|
||||
- Parameter credential: a `URLCredential` object with Client ID set as `user` and Token set as `password`.
|
||||
- Parameter cache: A URLCache to cache downloaded files and contents. If set to nil, URLCache.shared object will be used.
|
||||
- Parameter cache: A URLCache to cache downloaded files and contents.
|
||||
*/
|
||||
public init(credential: URLCredential?, cache: URLCache? = nil) {
|
||||
self.baseURL = nil
|
||||
self.isPathRelative = true
|
||||
self.currentPath = ""
|
||||
self.useCache = false
|
||||
self.validatingCache = true
|
||||
self.cache = cache
|
||||
self.credential = credential
|
||||
|
||||
self.apiURL = URL(string: "https://api.dropboxapi.com/2/")!
|
||||
self.contentURL = URL(string: "https://content.dropboxapi.com/2/")!
|
||||
|
||||
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: .concurrent)
|
||||
operation_queue = OperationQueue()
|
||||
operation_queue.name = "FileProvider.\(type(of: self).type).Operation"
|
||||
|
||||
super.init(baseURL: nil, credential: credential, cache: cache)
|
||||
}
|
||||
|
||||
deinit {
|
||||
if fileProviderCancelTasksOnInvalidating {
|
||||
_session?.invalidateAndCancel()
|
||||
} else {
|
||||
_session?.finishTasksAndInvalidate()
|
||||
}
|
||||
public required convenience init?(coder aDecoder: NSCoder) {
|
||||
self.init(credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential)
|
||||
self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? ""
|
||||
self.useCache = aDecoder.decodeBool(forKey: "useCache")
|
||||
self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache")
|
||||
}
|
||||
|
||||
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
|
||||
list(path) { (contents, cursor, error) in
|
||||
override open func copy(with zone: NSZone? = nil) -> Any {
|
||||
let copy = DropboxFileProvider(credential: self.credential, cache: self.cache)
|
||||
copy.currentPath = self.currentPath
|
||||
copy.delegate = self.delegate
|
||||
copy.fileOperationDelegate = self.fileOperationDelegate
|
||||
copy.useCache = self.useCache
|
||||
copy.validatingCache = self.validatingCache
|
||||
return copy
|
||||
}
|
||||
|
||||
open override func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
|
||||
let progress = Progress(parent: nil, userInfo: nil)
|
||||
list(path, progress: progress) { (contents, cursor, error) in
|
||||
completionHandler(contents, error)
|
||||
}
|
||||
}
|
||||
|
||||
open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
|
||||
open override func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
|
||||
let url = URL(string: "files/get_metadata", relativeTo: apiURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.set(httpAuthentication: credential, with: .oAuth2)
|
||||
request.set(httpContentType: .json)
|
||||
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString]
|
||||
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
|
||||
request.httpBody = Data(jsonDictionary: requestDictionary)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderDropboxError?
|
||||
var fileObject: DropboxFileObject?
|
||||
if let response = response as? HTTPURLResponse {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
|
||||
serverError = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
|
||||
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr), let file = DropboxFileObject(json: json) {
|
||||
if let json = data?.deserializeJSON(), let file = DropboxFileObject(json: json) {
|
||||
fileObject = file
|
||||
}
|
||||
}
|
||||
@@ -112,15 +87,15 @@ open class DropboxFileProvider: FileProviderBasicRemote {
|
||||
task.resume()
|
||||
}
|
||||
|
||||
open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) {
|
||||
open override func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) {
|
||||
let url = URL(string: "users/get_space_usage", relativeTo: apiURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.set(httpAuthentication: credential, with: .oAuth2)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var totalSize: Int64 = -1
|
||||
var usedSize: Int64 = 0
|
||||
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr) {
|
||||
if let json = data?.deserializeJSON() {
|
||||
totalSize = ((json["allocation"] as? NSDictionary)?["allocated"] as? NSNumber)?.int64Value ?? -1
|
||||
usedSize = (json["used"] as? NSNumber)?.int64Value ?? 0
|
||||
}
|
||||
@@ -129,170 +104,128 @@ open class DropboxFileProvider: FileProviderBasicRemote {
|
||||
task.resume()
|
||||
}
|
||||
|
||||
open weak var fileOperationDelegate: FileOperationDelegate?
|
||||
}
|
||||
|
||||
extension DropboxFileProvider: FileProviderOperations {
|
||||
public func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let path = (atPath as NSString).appendingPathComponent(folderName) + "/"
|
||||
return doOperation(.create(path: path), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func create(file fileName: String, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let filePath = (path as NSString).appendingPathComponent(fileName)
|
||||
return self.writeContents(path: filePath, contents: data ?? Data(), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
return doOperation(.copy(source: path, destination: toPath), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
return doOperation(.remove(path: path), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
fileprivate func doOperation(_ operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
|
||||
return nil
|
||||
open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? {
|
||||
let progress = Progress(parent: nil, userInfo: nil)
|
||||
var foundFiles = [DropboxFileObject]()
|
||||
if let queryStr = query.findValue(forKey: "name", operator: .beginsWith) as? String {
|
||||
// Dropbox only support searching for file names begin with query in non-enterprise accounts.
|
||||
// We will use it if there is a `name BEGINSWITH[c] "query"` in predicate, then filter to form final result.
|
||||
search(path, query: queryStr, progress: progress, foundItem: { (file) in
|
||||
if query.evaluate(with: file.mapPredicate()) {
|
||||
foundFiles.append(file)
|
||||
foundItemHandler?(file)
|
||||
}
|
||||
}, completionHandler: { (error) in
|
||||
completionHandler(foundFiles, error)
|
||||
})
|
||||
} else {
|
||||
// Dropbox doesn't support searching attributes natively. The workaround is to fallback to listing all files
|
||||
// and filter it locally. It may have a network burden in case there is many files in Dropbox, so please use it concisely.
|
||||
list(path, recursive: true, progress: progress, progressHandler: { (files, _, error) in
|
||||
for file in files where query.evaluate(with: file.mapPredicate()) {
|
||||
foundItemHandler?(file)
|
||||
}
|
||||
}, completionHandler: { (files, _, error) in
|
||||
let predicatedFiles = files.filter { query.evaluate(with: $0.mapPredicate()) }
|
||||
completionHandler(predicatedFiles, error)
|
||||
})
|
||||
}
|
||||
return progress
|
||||
}
|
||||
|
||||
override func request(for operation: FileOperationType, overwrite: Bool = false, attributes: [URLResourceKey : Any] = [:]) -> URLRequest {
|
||||
// content operations
|
||||
var request: URLRequest
|
||||
switch operation {
|
||||
case .copy(source: let source, destination: let dest) where dest.lowercased().hasPrefix("file://"):
|
||||
let url = URL(string: "files/download", relativeTo: contentURL)!
|
||||
request = URLRequest(url: url)
|
||||
request.set(httpAuthentication: credential, with: .oAuth2)
|
||||
request.set(dropboxArgKey: ["path": correctPath(source)! as NSString])
|
||||
case .fetch(let path):
|
||||
let url = URL(string: "files/download", relativeTo: contentURL)!
|
||||
request = URLRequest(url: url)
|
||||
request.set(httpAuthentication: credential, with: .oAuth2)
|
||||
request.set(dropboxArgKey: ["path": correctPath(path)! as NSString])
|
||||
case .copy(source: let source, destination: let dest) where source.lowercased().hasPrefix("file://"):
|
||||
var requestDictionary = [String: AnyObject]()
|
||||
let url: URL = URL(string: "files/upload", relativeTo: contentURL)!
|
||||
requestDictionary["path"] = correctPath(dest) as NSString?
|
||||
requestDictionary["mode"] = (overwrite ? "overwrite" : "add") as NSString
|
||||
requestDictionary["client_modified"] = (attributes[.contentModificationDateKey] as? Date)?.format(with: .rfc3339) as NSString?
|
||||
request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.set(httpAuthentication: credential, with: .oAuth2)
|
||||
request.set(httpContentType: .stream)
|
||||
request.set(dropboxArgKey: requestDictionary)
|
||||
case .modify(let path):
|
||||
var requestDictionary = [String: AnyObject]()
|
||||
let url: URL = URL(string: "files/upload", relativeTo: contentURL)!
|
||||
requestDictionary["path"] = correctPath(path) as NSString?
|
||||
requestDictionary["mode"] = (overwrite ? "overwrite" : "add") as NSString
|
||||
requestDictionary["client_modified"] = (attributes[.contentModificationDateKey] as? Date)?.format(with: .rfc3339) as NSString?
|
||||
request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.set(httpAuthentication: credential, with: .oAuth2)
|
||||
request.set(httpContentType: .stream)
|
||||
request.set(dropboxArgKey: requestDictionary)
|
||||
default:
|
||||
return self.apiRequest(for: operation, overwrite: overwrite)
|
||||
}
|
||||
return request
|
||||
}
|
||||
|
||||
func apiRequest(for operation: FileOperationType, overwrite: Bool = false) -> URLRequest {
|
||||
let url: String
|
||||
guard let sourcePath = operation.source else { return nil }
|
||||
let sourcePath = operation.source
|
||||
let destPath = operation.destination
|
||||
var requestDictionary = [String: AnyObject]()
|
||||
switch operation {
|
||||
case .create:
|
||||
url = "files/create_folder"
|
||||
url = "files/create_folder_v2"
|
||||
|
||||
case .copy:
|
||||
url = "files/copy"
|
||||
url = "files/copy_v2"
|
||||
requestDictionary["allow_shared_folder"] = NSNumber(value: true)
|
||||
case .move:
|
||||
url = "files/move"
|
||||
url = "files/move_v2"
|
||||
requestDictionary["allow_shared_folder"] = NSNumber(value: true)
|
||||
case .remove:
|
||||
url = "files/delete"
|
||||
url = "files/delete_v2"
|
||||
default: // modify, link, fetch
|
||||
return nil
|
||||
fatalError("Unimplemented operation \(operation.description) in \(#file)")
|
||||
}
|
||||
var request = URLRequest(url: URL(string: url, relativeTo: apiURL)!)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
var requestDictionary = [String: AnyObject]()
|
||||
request.set(httpAuthentication: credential, with: .oAuth2)
|
||||
request.set(httpContentType: .json)
|
||||
if let dest = correctPath(destPath) as NSString? {
|
||||
requestDictionary["from_path"] = correctPath(sourcePath) as NSString?
|
||||
requestDictionary["to_path"] = dest
|
||||
} else {
|
||||
requestDictionary["path"] = correctPath(sourcePath) as NSString?
|
||||
}
|
||||
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderDropboxError?
|
||||
if let response = response as? HTTPURLResponse, response.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) {
|
||||
serverError = FileProviderDropboxError(code: code, path: sourcePath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
}
|
||||
completionHandler?(serverError ?? error)
|
||||
self.delegateNotify(operation, error: serverError ?? error)
|
||||
})
|
||||
task.taskDescription = operation.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(operationType: operation, tasks: [task])
|
||||
request.httpBody = Data(jsonDictionary: requestDictionary)
|
||||
return request
|
||||
}
|
||||
|
||||
public func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
return upload_simple(toPath, localFile: localFile, overwrite: overwrite, operation: opType, completionHandler: completionHandler)
|
||||
override func serverError(with code: FileProviderHTTPErrorCode, path: String?, data: Data?) -> FileProviderHTTPError {
|
||||
return FileProviderDropboxError(code: code, path: path ?? "", errorDescription: data.flatMap({ String(data: $0, encoding: .utf8) }))
|
||||
}
|
||||
|
||||
public func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.copy(source: path, destination: destURL.absoluteString)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
let url = URL(string: "files/download", relativeTo: contentURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
let requestDictionary: [String: AnyObject] = ["path": path as NSString]
|
||||
let requestJson = dictionaryToJSON(requestDictionary) ?? ""
|
||||
request.setValue(requestJson, forHTTPHeaderField: "Dropbox-API-Arg")
|
||||
let task = session.downloadTask(with: request, completionHandler: { (cacheURL, response, error) in
|
||||
guard let cacheURL = cacheURL, let httpResponse = response as? HTTPURLResponse , httpResponse.statusCode < 300 else {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: (response as? HTTPURLResponse)?.statusCode ?? -1)
|
||||
let errorData : Data? = nil //Data(contentsOf:cacheURL) // TODO: Figure out how to get error response data for the error description
|
||||
let serverError : FileProviderDropboxError? = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: errorData ?? Data(), encoding: .utf8)) : nil
|
||||
completionHandler?(serverError ?? error)
|
||||
return
|
||||
}
|
||||
do {
|
||||
try FileManager.default.moveItem(at: cacheURL, to: destURL)
|
||||
completionHandler?(nil)
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
}
|
||||
})
|
||||
task.taskDescription = opType.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(operationType: opType, tasks: [task])
|
||||
}
|
||||
}
|
||||
|
||||
extension DropboxFileProvider: FileProviderReadWrite {
|
||||
public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
if length == 0 || offset < 0 {
|
||||
dispatch_queue.async {
|
||||
completionHandler(Data(), nil)
|
||||
}
|
||||
override func upload_simple(_ targetPath: String, request: URLRequest, data: Data?, localFile: URL?, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let size = data?.count ?? Int((try? localFile?.resourceValues(forKeys: [.fileSizeKey]))??.fileSize ?? -1)
|
||||
if size > 150 * 1024 * 1024 {
|
||||
let error = FileProviderDropboxError(code: .payloadTooLarge, path: targetPath, errorDescription: nil)
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
return nil
|
||||
}
|
||||
|
||||
let opType = FileOperationType.fetch(path: path)
|
||||
let url = URL(string: "files/download", relativeTo: contentURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
if length > 0 {
|
||||
request.setValue("bytes=\(offset)-\(offset + length - 1)", forHTTPHeaderField: "Range")
|
||||
} else if offset > 0 && length < 0 {
|
||||
request.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
|
||||
}
|
||||
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString]
|
||||
request.setValue(dictionaryToJSON(requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderDropboxError?
|
||||
if let httpResponse = response as? HTTPURLResponse , httpResponse.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: httpResponse.statusCode) {
|
||||
serverError = FileProviderDropboxError(code: code, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
}
|
||||
let filedata = serverError ?? error == nil ? data : nil
|
||||
completionHandler(filedata, serverError ?? error)
|
||||
})
|
||||
task.taskDescription = opType.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(operationType: opType, tasks: [task])
|
||||
return super.upload_simple(targetPath, request: request, data: data, localFile: localFile, operation: operation, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.modify(path: path)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
// FIXME: remove 150MB restriction
|
||||
return upload_simple(path, data: data, overwrite: overwrite, operation: opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
|
||||
var foundFiles = [DropboxFileObject]()
|
||||
search(path, query: query, foundItem: { (file) in
|
||||
foundFiles.append(file)
|
||||
foundItemHandler?(file)
|
||||
}, completionHandler: { (error) in
|
||||
completionHandler(foundFiles, error)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
fileprivate func registerNotifcation(path: String, eventHandler: (() -> Void)) {
|
||||
/* There is two ways to monitor folders changing in Dropbox. Either using webooks
|
||||
* which means you have to implement a server to translate it to push notifications
|
||||
@@ -305,48 +238,17 @@ extension DropboxFileProvider: FileProviderReadWrite {
|
||||
fileprivate func unregisterNotifcation(path: String) {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
// TODO: Implement /get_account & /get_current_account
|
||||
}
|
||||
|
||||
extension DropboxFileProvider {
|
||||
/// *DEPRECATED:* Use `publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?))` function instead.
|
||||
@available(*, deprecated, renamed: "publicLink(to:completionHandler:)", message: "Use publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?)) function instead.")
|
||||
open func temporaryLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: DropboxFileObject?, _ error: Error?) -> Void)) {
|
||||
self.publicLink(to: path) { (url, file, _, error) in
|
||||
completionHandler(url, file, error)
|
||||
}
|
||||
}
|
||||
|
||||
/// *DEPRECATED:* Use `publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?))` function instead.
|
||||
@available(*, deprecated, renamed: "publicLink(to:completionHandler:)", message: "Use publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?)) function instead.")
|
||||
open func temporaryLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: DropboxFileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
|
||||
self.publicLink(to: path) { (url, file, expiration, error) in
|
||||
completionHandler(url, file, expiration, error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Genrates a public url to a file to be shared with other users and can be downloaded without authentication.
|
||||
|
||||
- Important: URL will be available for a limitied time (4 hours according to Dropbox documentation).
|
||||
|
||||
- Parameters:
|
||||
- to: path of file, including file/directory name.
|
||||
- completionHandler: a block with result of directory entries or error.
|
||||
`link`: a url returned by Dropbox to share.
|
||||
`attribute`: a `FileObject` containing the attributes of the item.
|
||||
`expiration`: a `Date` object, determines when the public url will expires.
|
||||
`error`: Error returned by Dropbox.
|
||||
*/
|
||||
open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: DropboxFileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
|
||||
// TODO: Implement /get_account & /get_current_account
|
||||
|
||||
open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
|
||||
let url = URL(string: "files/get_temporary_link", relativeTo: apiURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.set(httpAuthentication: credential, with: .oAuth2)
|
||||
request.set(httpContentType: .json)
|
||||
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString]
|
||||
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
|
||||
request.httpBody = Data(jsonDictionary: requestDictionary)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderDropboxError?
|
||||
var link: URL?
|
||||
@@ -354,7 +256,7 @@ extension DropboxFileProvider {
|
||||
if let response = response as? HTTPURLResponse {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
|
||||
serverError = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
|
||||
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr) {
|
||||
if let json = data?.deserializeJSON() {
|
||||
if let linkStr = json["link"] as? String {
|
||||
link = URL(string: linkStr)
|
||||
}
|
||||
@@ -376,10 +278,10 @@ extension DropboxFileProvider {
|
||||
- Parameters:
|
||||
- remoteURL: a valid remote url to file.
|
||||
- to: Destination path of file, including file/directory name.
|
||||
- completionHandler: a block with result of directory entries or error.
|
||||
`jobId`: Job ID returned by Dropbox to monitor the copy/download progress.
|
||||
`attribute`: A `FileObject` containing the attributes of the item.
|
||||
`error`: Error returned by Dropbox.
|
||||
- completionHandler: a closure with result of directory entries or error.
|
||||
- `jobId`: Job ID returned by Dropbox to monitor the copy/download progress.
|
||||
- `attribute`: A `FileObject` containing the attributes of the item.
|
||||
- `error`: Error returned by Dropbox.
|
||||
*/
|
||||
open func copyItem(remoteURL: URL, to toPath: String, completionHandler: @escaping ((_ jobId: String?, _ attribute: DropboxFileObject?, _ error: Error?) -> Void)) {
|
||||
if remoteURL.isFileURL {
|
||||
@@ -389,10 +291,10 @@ extension DropboxFileProvider {
|
||||
let url = URL(string: "files/save_url", relativeTo: apiURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.set(httpAuthentication: credential, with: .oAuth2)
|
||||
request.set(httpContentType: .json)
|
||||
let requestDictionary: [String: AnyObject] = ["path": correctPath(toPath)! as NSString, "url" : remoteURL.absoluteString as NSString]
|
||||
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
|
||||
request.httpBody = Data(jsonDictionary: requestDictionary)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderDropboxError?
|
||||
var jobId: String?
|
||||
@@ -400,7 +302,7 @@ extension DropboxFileProvider {
|
||||
if let response = response as? HTTPURLResponse {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
|
||||
serverError = code != nil ? FileProviderDropboxError(code: code!, path: toPath, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
|
||||
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr) {
|
||||
if let json = data?.deserializeJSON() {
|
||||
jobId = json["async_job_id"] as? String
|
||||
if let attribDic = json["metadata"] as? [String: AnyObject] {
|
||||
fileObject = DropboxFileObject(json: attribDic)
|
||||
@@ -424,10 +326,10 @@ extension DropboxFileProvider {
|
||||
let url = URL(string: "files/copy_reference/save", relativeTo: apiURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.set(httpAuthentication: credential, with: .oAuth2)
|
||||
request.set(httpContentType: .json)
|
||||
let requestDictionary: [String: AnyObject] = ["path": correctPath(toPath)! as NSString, "copy_reference" : reference as NSString]
|
||||
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
|
||||
request.httpBody = Data(jsonDictionary: requestDictionary)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderDropboxError?
|
||||
if let response = response as? HTTPURLResponse {
|
||||
@@ -441,7 +343,7 @@ extension DropboxFileProvider {
|
||||
}
|
||||
|
||||
extension DropboxFileProvider: ExtendedFileProvider {
|
||||
public func thumbnailOfFileSupported(path: String) -> Bool {
|
||||
open func thumbnailOfFileSupported(path: String) -> Bool {
|
||||
switch (path as NSString).pathExtension.lowercased() {
|
||||
case "jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff":
|
||||
return true
|
||||
@@ -456,7 +358,7 @@ extension DropboxFileProvider: ExtendedFileProvider {
|
||||
}
|
||||
}
|
||||
|
||||
public func propertiesOfFileSupported(path: String) -> Bool {
|
||||
open func propertiesOfFileSupported(path: String) -> Bool {
|
||||
let fileExt = (path as NSString).pathExtension.lowercased()
|
||||
switch fileExt {
|
||||
case "jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff":
|
||||
@@ -471,42 +373,57 @@ extension DropboxFileProvider: ExtendedFileProvider {
|
||||
}
|
||||
|
||||
/// Default value for dimension is 64x64, according to Dropbox documentation
|
||||
public func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) {
|
||||
open func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) {
|
||||
let url: URL
|
||||
let thumbAPI: Bool
|
||||
switch (path as NSString).pathExtension.lowercased() {
|
||||
case "jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff":
|
||||
url = URL(string: "files/get_thumbnail", relativeTo: contentURL)!
|
||||
thumbAPI = true
|
||||
case "doc", "docx", "docm", "xls", "xlsx", "xlsm":
|
||||
fallthrough
|
||||
case "ppt", "pps", "ppsx", "ppsm", "pptx", "pptm":
|
||||
fallthrough
|
||||
case "rtf":
|
||||
url = URL(string: "files/get_preview", relativeTo: contentURL)!
|
||||
thumbAPI = false
|
||||
default:
|
||||
return
|
||||
}
|
||||
var request = URLRequest(url: url)
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.set(httpAuthentication: credential, with: .oAuth2)
|
||||
var requestDictionary: [String: AnyObject] = ["path": path as NSString]
|
||||
requestDictionary["format"] = "jpeg" as NSString
|
||||
if let dimension = dimension {
|
||||
requestDictionary["size"] = "w\(Int(dimension.width))h\(Int(dimension.height))" as NSString
|
||||
if thumbAPI {
|
||||
requestDictionary["format"] = "jpeg" as NSString
|
||||
let size: String
|
||||
switch dimension?.height ?? 64 {
|
||||
case 0...32: size = "w32h32"
|
||||
case 33...64: size = "w64h64"
|
||||
case 65...128: size = "w128h128"
|
||||
case 129...480: size = "w640h480"
|
||||
default: size = "w1024h768"
|
||||
}
|
||||
requestDictionary["size"] = size as NSString
|
||||
}
|
||||
request.setValue(dictionaryToJSON(requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
|
||||
request.set(dropboxArgKey: requestDictionary)
|
||||
let task = self.session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var image: ImageClass? = nil
|
||||
if let r = response as? HTTPURLResponse, let result = r.allHeaderFields["Dropbox-API-Result"] as? String, let jsonResult = jsonToDictionary(result) {
|
||||
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.throwError(path, code: URLError.cannotDecodeRawData as FoundationErrorEnum))
|
||||
completionHandler(nil, self.throwError(path, code: URLError.cannotDecodeRawData))
|
||||
}
|
||||
}
|
||||
if let data = data {
|
||||
if DropboxFileProvider.dataIsPDF(data) {
|
||||
image = DropboxFileProvider.convertToImage(pdfData: data)
|
||||
if data.isPDF, let pageImage = DropboxFileProvider.convertToImage(pdfData: data) {
|
||||
image = pageImage
|
||||
} else if let contentType = (response as? HTTPURLResponse)?.allHeaderFields["Content-Type"] as? String, contentType.contains("text/html") {
|
||||
// TODO: Implement converting html returned type of get_preview to image
|
||||
} else {
|
||||
image = ImageClass(data: data)
|
||||
} else if let fetchedimage = ImageClass(data: data){
|
||||
if let dimension = dimension {
|
||||
image = DropboxFileProvider.scaleDown(image: fetchedimage, toSize: dimension)
|
||||
} else {
|
||||
image = fetchedimage
|
||||
}
|
||||
}
|
||||
}
|
||||
completionHandler(image, error)
|
||||
@@ -514,14 +431,14 @@ extension DropboxFileProvider: ExtendedFileProvider {
|
||||
task.resume()
|
||||
}
|
||||
|
||||
public func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) {
|
||||
open func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) {
|
||||
let url = URL(string: "files/get_metadata", relativeTo: apiURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.set(httpAuthentication: credential, with: .oAuth2)
|
||||
request.set(httpContentType: .json)
|
||||
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString, "include_media_info": NSNumber(value: true)]
|
||||
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
|
||||
request.httpBody = Data(jsonDictionary: requestDictionary)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderDropboxError?
|
||||
var dic = [String: Any]()
|
||||
@@ -529,7 +446,7 @@ extension DropboxFileProvider: ExtendedFileProvider {
|
||||
if let response = response as? HTTPURLResponse {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
|
||||
serverError = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
|
||||
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr), let properties = json["media_info"] as? [String: Any] {
|
||||
if let json = data?.deserializeJSON(), let properties = (json["media_info"] as? [String: Any])?["metadata"] as? [String: Any] {
|
||||
(dic, keys) = self.mapMediaInfo(properties)
|
||||
}
|
||||
}
|
||||
@@ -538,15 +455,3 @@ extension DropboxFileProvider: ExtendedFileProvider {
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
||||
extension DropboxFileProvider: FileProvider {
|
||||
open func copy(with zone: NSZone? = nil) -> Any {
|
||||
let copy = DropboxFileProvider(credential: self.credential, cache: self.cache)
|
||||
copy.currentPath = self.currentPath
|
||||
copy.delegate = self.delegate
|
||||
copy.fileOperationDelegate = self.fileOperationDelegate
|
||||
copy.useCache = self.useCache
|
||||
copy.validatingCache = self.validatingCache
|
||||
return copy
|
||||
}
|
||||
}
|
||||
|
||||
+67
-116
@@ -8,44 +8,50 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Error returned by Dropbox server when trying to access or do operations on a file or folder.
|
||||
public struct FileProviderDropboxError: FileProviderHTTPError {
|
||||
public let code: FileProviderHTTPErrorCode
|
||||
public let path: String
|
||||
public let errorDescription: String?
|
||||
}
|
||||
|
||||
/// Containts path, url and attributes of a Dropbox file or resource.
|
||||
public final class DropboxFileObject: FileObject {
|
||||
internal init(name: String, path: String) {
|
||||
super.init(url: URL(string: path) ?? URL(string: "/")!, name: name, path: path)
|
||||
}
|
||||
|
||||
internal convenience init? (jsonStr: String) {
|
||||
guard let json = jsonToDictionary(jsonStr) else { return nil }
|
||||
guard let json = jsonStr.deserializeJSON() else { return nil }
|
||||
self.init(json: json)
|
||||
}
|
||||
|
||||
internal convenience init? (json: [String: AnyObject]) {
|
||||
internal init? (json: [String: AnyObject]) {
|
||||
var json = json
|
||||
if json["name"] == nil, let metadata = json["metadata"] as? [String: AnyObject] {
|
||||
json = metadata
|
||||
}
|
||||
guard let name = json["name"] as? String else { return nil }
|
||||
guard let path = json["path_display"] as? String else { return nil }
|
||||
self.init(name: name, path: path)
|
||||
super.init(url: nil, name: name, path: path)
|
||||
self.size = (json["size"] as? NSNumber)?.int64Value ?? -1
|
||||
self.serverTime = resolve(dateString: json["server_modified"] as? String ?? "")
|
||||
self.modifiedDate = resolve(dateString: json["client_modified"] as? String ?? "")
|
||||
self.serverTime = Date(rfcString: json["server_modified"] as? String ?? "")
|
||||
self.modifiedDate = Date(rfcString: json["client_modified"] as? String ?? "")
|
||||
self.type = (json[".tag"] as? String) == "folder" ? .directory : .regular
|
||||
self.isReadOnly = (json["sharing_info"]?["read_only"] as? NSNumber)?.boolValue ?? false
|
||||
self.id = json["id"] as? String
|
||||
self.rev = json["rev"] as? String
|
||||
}
|
||||
|
||||
/// The time contents of file has been modified on server, returns nil if not set
|
||||
open internal(set) var serverTime: Date? {
|
||||
get {
|
||||
return allValues[.serverDate] as? Date
|
||||
return allValues[.serverDateKey] as? Date
|
||||
}
|
||||
set {
|
||||
allValues[.serverDate] = newValue
|
||||
allValues[.serverDateKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// The document identifier is a value assigned by the Dropbox to a file.
|
||||
/// This value is used to identify the document regardless of where it is moved on a volume.
|
||||
/// The identifier persists across system restarts.
|
||||
open internal(set) var id: String? {
|
||||
get {
|
||||
return allValues[.documentIdentifierKey] as? String
|
||||
@@ -55,6 +61,8 @@ 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? {
|
||||
get {
|
||||
return allValues[.generationIdentifierKey] as? String
|
||||
@@ -65,9 +73,13 @@ public final class DropboxFileObject: FileObject {
|
||||
}
|
||||
}
|
||||
|
||||
// codebeat:disable[ARITY]
|
||||
// codebeat:disable[ARITY]
|
||||
internal extension DropboxFileProvider {
|
||||
func list(_ path: String, cursor: String? = nil, prevContents: [DropboxFileObject] = [], recursive: Bool = false, completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) {
|
||||
|
||||
|
||||
func list(_ path: String, cursor: String? = nil, prevContents: [DropboxFileObject] = [], recursive: Bool = false, session: URLSession? = nil, progress: Progress, progressHandler: ((_ contents: [FileObject], _ nextCursor: String?, _ error: Error?) -> Void)? = nil, completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) {
|
||||
if progress.isCancelled { return }
|
||||
|
||||
var requestDictionary = [String: AnyObject]()
|
||||
let url: URL
|
||||
if let cursor = cursor {
|
||||
@@ -80,129 +92,74 @@ internal extension DropboxFileProvider {
|
||||
}
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
request.set(httpAuthentication: credential, with: .oAuth2)
|
||||
request.set(httpContentType: .json)
|
||||
request.httpBody = Data(jsonDictionary: requestDictionary)
|
||||
let task = (session ?? self.session).dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var responseError: FileProviderDropboxError?
|
||||
var files = prevContents
|
||||
var files = [DropboxFileObject]()
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderDropboxError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
}
|
||||
if let data = data, let jsonStr = String(data: data, encoding: .utf8) {
|
||||
let json = jsonToDictionary(jsonStr)
|
||||
if let entries = json?["entries"] as? [AnyObject] , entries.count > 0 {
|
||||
if let json = data?.deserializeJSON() {
|
||||
if let entries = json["entries"] as? [AnyObject] , entries.count > 0 {
|
||||
files.reserveCapacity(entries.count)
|
||||
for entry in entries {
|
||||
if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry) {
|
||||
files.append(file)
|
||||
progress.totalUnitCount = Int64(files.count)
|
||||
}
|
||||
}
|
||||
let ncursor = json?["cursor"] as? String
|
||||
let hasmore = (json?["has_more"] as? NSNumber)?.boolValue ?? false
|
||||
if hasmore {
|
||||
self.list(path, cursor: ncursor, prevContents: files, completionHandler: completionHandler)
|
||||
let ncursor = json["cursor"] as? String
|
||||
let hasmore = (json["has_more"] as? NSNumber)?.boolValue ?? false
|
||||
if hasmore && !progress.isCancelled {
|
||||
progressHandler?(files, ncursor, responseError ?? error)
|
||||
self.list(path, cursor: ncursor, prevContents: prevContents + files, progress: progress, completionHandler: completionHandler)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
completionHandler(files, nil, responseError ?? error)
|
||||
progressHandler?(files, nil, responseError ?? error)
|
||||
completionHandler(prevContents + files, nil, responseError ?? error)
|
||||
})
|
||||
progress.cancellationHandler = { [weak task] in
|
||||
task?.cancel()
|
||||
}
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
task.taskDescription = FileOperationType.fetch(path: path).json
|
||||
task.resume()
|
||||
}
|
||||
|
||||
func upload_simple(_ targetPath: String, data: Data, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
if data.count > 150 * 1024 * 1024 {
|
||||
let error = FileProviderDropboxError(code: .payloadTooLarge, path: targetPath, errorDescription: nil)
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(.create(path: targetPath), error: error)
|
||||
return nil
|
||||
}
|
||||
var requestDictionary = [String: AnyObject]()
|
||||
let url: URL
|
||||
url = URL(string: "files/upload", relativeTo: contentURL)!
|
||||
requestDictionary["path"] = correctPath(targetPath) as NSString?
|
||||
requestDictionary["mode"] = (overwrite ? "overwrite" : "add") as NSString
|
||||
requestDictionary["client_modified"] = rfc3339utc(of: modifiedDate) as NSString
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue(dictionaryToJSON(requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
|
||||
request.httpBody = data
|
||||
let task = session.uploadTask(with: request, from: data, completionHandler: { (data, response, error) in
|
||||
var responseError: FileProviderDropboxError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderDropboxError(code: rCode, path: targetPath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
}
|
||||
completionHandler?(responseError ?? error)
|
||||
self.delegateNotify(.create(path: targetPath), error: responseError ?? error)
|
||||
})
|
||||
task.taskDescription = operation.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(operationType: operation, tasks: [task])
|
||||
}
|
||||
|
||||
func upload_simple(_ targetPath: String, localFile: URL, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let size = (try? localFile.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? -1
|
||||
if size > 150 * 1024 * 1024 {
|
||||
let error = FileProviderDropboxError(code: .payloadTooLarge, path: targetPath, errorDescription: nil)
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(.create(path: targetPath), error: error)
|
||||
return nil
|
||||
}
|
||||
var requestDictionary = [String: AnyObject]()
|
||||
let url: URL
|
||||
url = URL(string: "files/upload", relativeTo: contentURL)!
|
||||
requestDictionary["path"] = correctPath(targetPath) as NSString?
|
||||
requestDictionary["mode"] = (overwrite ? "overwrite" : "add") as NSString
|
||||
requestDictionary["client_modified"] = rfc3339utc(of: modifiedDate) as NSString
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue(dictionaryToJSON(requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
|
||||
let task = session.uploadTask(with: request, fromFile: localFile, completionHandler: { (data, response, error) in
|
||||
var responseError: FileProviderDropboxError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderDropboxError(code: rCode, path: targetPath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
}
|
||||
completionHandler?(responseError ?? error)
|
||||
self.delegateNotify(.create(path: targetPath), error: responseError ?? error)
|
||||
})
|
||||
task.taskDescription = operation.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(operationType: operation, tasks: [task])
|
||||
}
|
||||
|
||||
func search(_ startPath: String = "", query: String, start: Int = 0, maxResultPerPage: Int = 25, maxResults: Int = -1, foundItem:@escaping ((_ file: DropboxFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) {
|
||||
func search(_ startPath: String = "", query: String, start: Int = 0, maxResultPerPage: Int = 25, maxResults: Int = -1, progress: Progress, foundItem:@escaping ((_ file: DropboxFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) {
|
||||
if progress.isCancelled { return }
|
||||
|
||||
let url = URL(string: "files/search", relativeTo: apiURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.set(httpAuthentication: credential, with: .oAuth2)
|
||||
request.set(httpContentType: .json)
|
||||
var requestDictionary: [String: AnyObject] = ["path": startPath as NSString]
|
||||
requestDictionary["query"] = query as NSString
|
||||
requestDictionary["start"] = start as NSNumber
|
||||
requestDictionary["max_results"] = maxResultPerPage as NSNumber
|
||||
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
|
||||
request.httpBody = Data(jsonDictionary: requestDictionary)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var responseError: FileProviderDropboxError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderDropboxError(code: rCode, path: startPath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
}
|
||||
if let data = data, let jsonStr = String(data: data, encoding: .utf8) {
|
||||
let json = jsonToDictionary(jsonStr)
|
||||
if let entries = json?["matches"] as? [AnyObject] , entries.count > 0 {
|
||||
if let json = data?.deserializeJSON() {
|
||||
if let entries = json["matches"] as? [AnyObject] , entries.count > 0 {
|
||||
for entry in entries {
|
||||
if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry) {
|
||||
foundItem(file)
|
||||
progress.completedUnitCount += 1
|
||||
}
|
||||
}
|
||||
let rstart = json?["start"] as? Int
|
||||
let hasmore = (json?["more"] as? NSNumber)?.boolValue ?? false
|
||||
if hasmore, let rstart = rstart {
|
||||
self.search(startPath, query: query, start: rstart + entries.count, maxResultPerPage: maxResultPerPage, foundItem: foundItem, completionHandler: completionHandler)
|
||||
let rstart = json["start"] as? Int
|
||||
let hasmore = (json["more"] as? NSNumber)?.boolValue ?? false
|
||||
if hasmore && !progress.isCancelled, let rstart = rstart {
|
||||
self.search(startPath, query: query, start: rstart + entries.count, maxResultPerPage: maxResultPerPage, progress: progress, foundItem: foundItem, completionHandler: completionHandler)
|
||||
} else {
|
||||
completionHandler(responseError ?? error)
|
||||
}
|
||||
@@ -210,7 +167,11 @@ internal extension DropboxFileProvider {
|
||||
}
|
||||
}
|
||||
completionHandler(responseError ?? error)
|
||||
})
|
||||
})
|
||||
progress.cancellationHandler = { [weak task] in
|
||||
task?.cancel()
|
||||
}
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
@@ -232,29 +193,19 @@ internal extension DropboxFileProvider {
|
||||
DropboxFileProvider.decimalFormatter.numberStyle = .decimal
|
||||
DropboxFileProvider.decimalFormatter.maximumFractionDigits = 5
|
||||
keys.append("Location")
|
||||
let latStr = DropboxFileProvider.decimalFormatter.string(from: NSNumber(value: latitude))
|
||||
let longStr = DropboxFileProvider.decimalFormatter.string(from: NSNumber(value: longitude))
|
||||
let latStr = DropboxFileProvider.decimalFormatter.string(from: NSNumber(value: latitude))!
|
||||
let longStr = DropboxFileProvider.decimalFormatter.string(from: NSNumber(value: longitude))!
|
||||
dic["Location"] = "\(latStr), \(longStr)"
|
||||
}
|
||||
if let timeTakenStr = json["time_taken"] as? String, let timeTaken = resolve(dateString: timeTakenStr) {
|
||||
if let timeTakenStr = json["time_taken"] as? String, let timeTaken = Date(rfcString: timeTakenStr) {
|
||||
keys.append("Date taken")
|
||||
DropboxFileProvider.dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
dic["Date taken"] = DropboxFileProvider.dateFormatter.string(from: timeTaken)
|
||||
}
|
||||
if let duration = json["duration"] as? UInt64 {
|
||||
keys.append("Duration")
|
||||
dic["Duration"] = DropboxFileProvider.formatshort(interval: TimeInterval(duration))
|
||||
dic["Duration"] = TimeInterval(duration).formatshort
|
||||
}
|
||||
return (dic, keys)
|
||||
}
|
||||
|
||||
func delegateNotify(_ operation: FileOperationType, error: Error?) {
|
||||
DispatchQueue.main.async(execute: {
|
||||
if error == nil {
|
||||
self.delegate?.fileproviderSucceed(self, operation: operation)
|
||||
} else {
|
||||
self.delegate?.fileproviderFailed(self, operation: operation)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,15 +10,9 @@ import Foundation
|
||||
import ImageIO
|
||||
import CoreGraphics
|
||||
import AVFoundation
|
||||
#if os(iOS) || os(tvOS)
|
||||
import UIKit
|
||||
#elseif os(macOS)
|
||||
import Cocoa
|
||||
#endif
|
||||
|
||||
extension LocalFileProvider: ExtendedFileProvider {
|
||||
|
||||
public func thumbnailOfFileSupported(path: String) -> Bool {
|
||||
open func thumbnailOfFileSupported(path: String) -> Bool {
|
||||
switch (path as NSString).pathExtension.lowercased() {
|
||||
case LocalFileInformationGenerator.imageThumbnailExtensions:
|
||||
return true
|
||||
@@ -37,7 +31,7 @@ extension LocalFileProvider: ExtendedFileProvider {
|
||||
}
|
||||
}
|
||||
|
||||
public func propertiesOfFileSupported(path: String) -> Bool {
|
||||
open func propertiesOfFileSupported(path: String) -> Bool {
|
||||
let fileExt = (path as NSString).pathExtension.lowercased()
|
||||
switch fileExt {
|
||||
case LocalFileInformationGenerator.imagePropertiesExtensions:
|
||||
@@ -60,7 +54,7 @@ extension LocalFileProvider: ExtendedFileProvider {
|
||||
}
|
||||
}
|
||||
|
||||
public func thumbnailOfFile(path: String, dimension: CGSize? = nil, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) {
|
||||
open func thumbnailOfFile(path: String, dimension: CGSize? = nil, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) {
|
||||
let dimension = dimension ?? CGSize(width: 64, height: 64)
|
||||
(dispatch_queue).async {
|
||||
var thumbnailImage: ImageClass? = nil
|
||||
@@ -92,7 +86,7 @@ extension LocalFileProvider: ExtendedFileProvider {
|
||||
}
|
||||
}
|
||||
|
||||
public func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String: Any], _ keys: [String], _ error: Error?) -> Void)) {
|
||||
open func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String: Any], _ keys: [String], _ error: Error?) -> Void)) {
|
||||
(dispatch_queue).async {
|
||||
let fileExt = (path as NSString).pathExtension.lowercased()
|
||||
var getter: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))?
|
||||
@@ -126,31 +120,90 @@ extension LocalFileProvider: ExtendedFileProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds supported file types and thumbnail/properties generator for specefied type of file
|
||||
public struct LocalFileInformationGenerator {
|
||||
/// Image extensions supportes for thumbnail.
|
||||
///
|
||||
/// Default: `["jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff", "ico"]`
|
||||
static public var imageThumbnailExtensions: [String] = ["jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff", "ico"]
|
||||
static public var audioThumbnailExtensions: [String] = ["mp3", "aac", "m4a"]
|
||||
static public var videoThumbnailExtensions: [String] = ["mov", "mp4", "m4v", "mpg", "mpeg"]
|
||||
static public var pdfThumbnailExtensions: [String] = ["pdf"]
|
||||
static public var officeThumbnailExtensions: [String] = []
|
||||
static public var customThumbnailExtensions: [String] = []
|
||||
|
||||
/// Audio and music extensions supportes for thumbnail.
|
||||
///
|
||||
/// Default: `["mp3", "aac", "m4a"]`
|
||||
static public var audioThumbnailExtensions: [String] = ["mp3", "aac", "m4a"]
|
||||
|
||||
/// Video extensions supportes for thumbnail.
|
||||
///
|
||||
/// Default: `["mov", "mp4", "m4v", "mpg", "mpeg"]`
|
||||
static public var videoThumbnailExtensions: [String] = ["mov", "mp4", "m4v", "mpg", "mpeg"]
|
||||
|
||||
/// Portable document file extensions supportes for thumbnail.
|
||||
///
|
||||
/// Default: `["pdf"]`
|
||||
static public var pdfThumbnailExtensions: [String] = ["pdf"]
|
||||
|
||||
/// Office document extensions supportes for thumbnail.
|
||||
///
|
||||
/// Default: `empty`
|
||||
static public var officeThumbnailExtensions: [String] = []
|
||||
|
||||
/// Custom document extensions supportes for thumbnail.
|
||||
///
|
||||
/// Default: `empty`
|
||||
static public var customThumbnailExtensions: [String] = []
|
||||
|
||||
|
||||
/// Image extensions supportes for properties.
|
||||
///
|
||||
/// Default: `["jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff"]`
|
||||
static public var imagePropertiesExtensions: [String] = ["jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff"]
|
||||
|
||||
/// Audio and music extensions supportes for properties.
|
||||
///
|
||||
/// Default: `["mp3", "aac", "m4a", "caf"]`
|
||||
static public var audioPropertiesExtensions: [String] = ["mp3", "aac", "m4a", "caf"]
|
||||
|
||||
/// Video extensions supportes for properties.
|
||||
///
|
||||
/// Default: `["mp4", "mpg", "3gp", "mov", "avi"]`
|
||||
static public var videoPropertiesExtensions: [String] = ["mp4", "mpg", "3gp", "mov", "avi"]
|
||||
|
||||
/// Portable document file extensions supportes for properties.
|
||||
///
|
||||
/// Default: `["pdf"]`
|
||||
static public var pdfPropertiesExtensions: [String] = ["pdf"]
|
||||
|
||||
/// Archive extensions (like zip) supportes for properties.
|
||||
///
|
||||
/// Default: `empty`
|
||||
static public var archivePropertiesExtensions: [String] = []
|
||||
|
||||
/// Office document extensions supportes for properties.
|
||||
///
|
||||
/// Default: `empty`
|
||||
static public var officePropertiesExtensions: [String] = []
|
||||
|
||||
/// Custom document extensions supportes for properties.
|
||||
///
|
||||
/// Default: `empty`
|
||||
static public var customPropertiesExtensions: [String] = []
|
||||
|
||||
/// Thumbnail generator closure for image files.
|
||||
static public var imageThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in
|
||||
return ImageClass(contentsOfFile: fileURL.path)
|
||||
}
|
||||
|
||||
/// Thumbnail generator closure for audio and music files.
|
||||
static public var audioThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in
|
||||
let playerItem = AVPlayerItem(url: fileURL)
|
||||
let metadataList = playerItem.asset.commonMetadata
|
||||
#if swift(>=4.0)
|
||||
let commonKeyArtwork = AVMetadataKey.commonKeyArtwork
|
||||
#else
|
||||
let commonKeyArtwork = AVMetadataCommonKeyArtwork
|
||||
#endif
|
||||
for item in metadataList {
|
||||
if item.commonKey == AVMetadataCommonKeyArtwork {
|
||||
if item.commonKey == commonKeyArtwork {
|
||||
if let data = item.dataValue {
|
||||
return ImageClass(data: data)
|
||||
}
|
||||
@@ -159,6 +212,7 @@ public struct LocalFileInformationGenerator {
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Thumbnail generator closure for video files.
|
||||
static public var videoThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in
|
||||
let asset = AVAsset(url: fileURL)
|
||||
let assetImgGenerate = AVAssetImageGenerator(asset: asset)
|
||||
@@ -174,25 +228,30 @@ public struct LocalFileInformationGenerator {
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Thumbnail generator closure for portable document files files.
|
||||
static public var pdfThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in
|
||||
guard let data = try? Data(contentsOf: fileURL) else { return nil }
|
||||
return LocalFileProvider.convertToImage(pdfData: data)
|
||||
return LocalFileProvider.convertToImage(pdfURL: fileURL)
|
||||
}
|
||||
|
||||
/// Thumbnail generator closure for office document files.
|
||||
/// - Note: No default implementation is avaiable
|
||||
static public var officeThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Thumbnail generator closure for custom type of files.
|
||||
/// - Note: No default implementation is avaiable
|
||||
static public var customThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Properties generator closure for image files.
|
||||
static public var imageProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
|
||||
func add(key: String, value: Any?) {
|
||||
if let value = value {
|
||||
if let value = value, !((value as? String)?.isEmpty ?? false) {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
}
|
||||
@@ -227,31 +286,29 @@ public struct LocalFileInformationGenerator {
|
||||
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)
|
||||
if let cr = tiffDict[kCGImagePropertyTIFFCopyright as String] as? String , !cr.isEmpty {
|
||||
add(key: "Copyright", value: cr)
|
||||
|
||||
}
|
||||
if let date = tiffDict[kCGImagePropertyTIFFDateTime as String] as? String , !date.isEmpty {
|
||||
add(key: "Date taken", value: date)
|
||||
}
|
||||
add(key: "Copyright", value: tiffDict[kCGImagePropertyTIFFCopyright as String] as? String)
|
||||
add(key: "Date taken", value: tiffDict[kCGImagePropertyTIFFDateTime as String] as? String)
|
||||
|
||||
if let latitude = tiffDict[kCGImagePropertyGPSLatitude as String] as? NSNumber, let longitude = tiffDict[kCGImagePropertyGPSLongitude as String] as? NSNumber {
|
||||
add(key: "Location", value: "\(latitude), \(longitude)")
|
||||
}
|
||||
add(key: "Altitude", value: tiffDict[kCGImagePropertyGPSAltitude as String] as? NSNumber)
|
||||
add(key: "Area", value: tiffDict[kCGImagePropertyGPSAreaInformation as String])
|
||||
|
||||
add(key: "Color space", value: imageDict[kCGImagePropertyColorModel as String])
|
||||
add(key: "Focal length", value: exifDict[kCGImagePropertyExifFocalLength as String])
|
||||
add(key: "F number", value: exifDict[kCGImagePropertyExifFNumber as String])
|
||||
add(key: "Exposure program", value: exifDict[kCGImagePropertyExifExposureProgram as String])
|
||||
|
||||
if let exp = exifDict[kCGImagePropertyExifExposureTime as String] as? NSNumber {
|
||||
let expfrac = simplify(Int64(exp.doubleValue * 10_000_000_000_000), 10_000_000_000_000)
|
||||
let expfrac = simplify(Int64(exp.doubleValue * 1_163_962_800_000), 1_163_962_800_000)
|
||||
add(key: "Exposure time", value: "\(expfrac.newTop)/\(expfrac.newBottom)")
|
||||
}
|
||||
if let iso = exifDict[kCGImagePropertyExifISOSpeedRatings as String] as? NSArray , iso.count > 0 {
|
||||
add(key: "ISO speed", value: iso[0])
|
||||
}
|
||||
add(key: "ISO speed", value: (exifDict[kCGImagePropertyExifISOSpeedRatings as String] as? [NSNumber])?.first)
|
||||
return (dic, keys)
|
||||
}
|
||||
|
||||
/// Properties generator closure for audio and music files.
|
||||
static var audioProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
@@ -267,32 +324,39 @@ public struct LocalFileInformationGenerator {
|
||||
guard let key = key else {
|
||||
return nil
|
||||
}
|
||||
guard let regex = try? NSRegularExpression(pattern: "([a-z])([A-Z])" , options: NSRegularExpression.Options()) else {
|
||||
guard let regex = try? NSRegularExpression(pattern: "([a-z])([A-Z])" , options: []) else {
|
||||
return nil
|
||||
}
|
||||
let newKey = regex.stringByReplacingMatches(in: key, options: NSRegularExpression.MatchingOptions(), range: NSMakeRange(0, (key as NSString).length) , withTemplate: "$1 $2")
|
||||
let newKey = regex.stringByReplacingMatches(in: key, options: [], range: NSRange(location: 0, length: (key as NSString).length) , withTemplate: "$1 $2")
|
||||
return newKey.capitalized
|
||||
}
|
||||
|
||||
if FileManager.default.fileExists(atPath: fileURL.path) {
|
||||
let playerItem = AVPlayerItem(url: fileURL)
|
||||
let metadataList = playerItem.asset.commonMetadata
|
||||
for item in metadataList {
|
||||
if let description = makeDescription(item.commonKey) {
|
||||
if let value = item.stringValue {
|
||||
keys.append(description)
|
||||
dic[description] = value
|
||||
}
|
||||
guard FileManager.default.fileExists(atPath: fileURL.path) else {
|
||||
return (dic, keys)
|
||||
}
|
||||
let playerItem = AVPlayerItem(url: fileURL)
|
||||
let metadataList = playerItem.asset.commonMetadata
|
||||
for item in metadataList {
|
||||
#if swift(>=4.0)
|
||||
let commonKey = item.commonKey?.rawValue
|
||||
#else
|
||||
let commonKey = item.commonKey
|
||||
#endif
|
||||
if let description = makeDescription(commonKey) {
|
||||
if let value = item.stringValue {
|
||||
keys.append(description)
|
||||
dic[description] = value
|
||||
}
|
||||
}
|
||||
if let ap = try? AVAudioPlayer(contentsOf: fileURL) {
|
||||
add(key: "Duration", value: LocalFileProvider.formatshort(interval: ap.duration))
|
||||
add(key: "Bitrate", value: ap.settings[AVSampleRateKey] as? Int)
|
||||
}
|
||||
}
|
||||
if let ap = try? AVAudioPlayer(contentsOf: fileURL) {
|
||||
add(key: "Duration", value: ap.duration.formatshort)
|
||||
add(key: "Bitrate", value: ap.settings[AVSampleRateKey] as? Int)
|
||||
}
|
||||
return (dic, keys)
|
||||
}
|
||||
|
||||
/// Properties generator closure for video files.
|
||||
static public var videoProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
@@ -313,21 +377,29 @@ public struct LocalFileInformationGenerator {
|
||||
}
|
||||
}
|
||||
let asset = AVURLAsset(url: fileURL, options: nil)
|
||||
#if swift(>=4.0)
|
||||
let videoTracks = asset.tracks(withMediaType: AVMediaType.video)
|
||||
#else
|
||||
let videoTracks = asset.tracks(withMediaType: AVMediaTypeVideo)
|
||||
if videoTracks.count > 0 {
|
||||
#endif
|
||||
if let videoTrack = videoTracks.first {
|
||||
var bitrate: Float = 0
|
||||
let width = Int(videoTracks[0].naturalSize.width)
|
||||
let height = Int(videoTracks[0].naturalSize.height)
|
||||
let width = Int(videoTrack.naturalSize.width)
|
||||
let height = Int(videoTrack.naturalSize.height)
|
||||
add(key: "Dimensions", value: "\(width)x\(height)")
|
||||
var duration: Int64 = 0
|
||||
for track in videoTracks {
|
||||
duration += track.timeRange.duration.timescale > 0 ? track.timeRange.duration.value / Int64(track.timeRange.duration.timescale) : 0
|
||||
bitrate += track.estimatedDataRate
|
||||
}
|
||||
add(key: "Duration", value: LocalFileProvider.formatshort(interval: TimeInterval(duration)))
|
||||
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 {
|
||||
@@ -337,32 +409,53 @@ public struct LocalFileInformationGenerator {
|
||||
return (dic, keys)
|
||||
}
|
||||
|
||||
/// Properties generator closure for protable documents files.
|
||||
static public var pdfProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
|
||||
func add(key: String, value: Any?) {
|
||||
if let value = value {
|
||||
if let value = value, !((value as? String)?.isEmpty ?? false) {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
func getKey(_ key: String, from dict: CGPDFDictionaryRef) -> String? {
|
||||
var cfValue: CGPDFStringRef? = nil
|
||||
if (CGPDFDictionaryGetString(dict, key, &cfValue)), let value = CGPDFStringCopyTextString(cfValue!) {
|
||||
var cfStrValue: CGPDFStringRef?
|
||||
if (CGPDFDictionaryGetString(dict, key, &cfStrValue)), let value = cfStrValue.flatMap({ CGPDFStringCopyTextString($0) }) {
|
||||
return value as String
|
||||
}
|
||||
var cfArrayValue: CGPDFArrayRef?
|
||||
if (CGPDFDictionaryGetArray(dict, key, &cfArrayValue)), let cfArray = cfArrayValue {
|
||||
var array = [String]()
|
||||
for i in 0..<CGPDFArrayGetCount(cfArray) {
|
||||
var cfItemValue: CGPDFStringRef?
|
||||
if CGPDFArrayGetString(cfArray, i, &cfItemValue), let item = cfItemValue.flatMap({ CGPDFStringCopyTextString($0) }) {
|
||||
array.append(item as String)
|
||||
}
|
||||
}
|
||||
return array.joined(separator: ", ")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertDate(_ date: String) -> Date? {
|
||||
var dateStr = date
|
||||
func convertDate(_ date: String?) -> Date? {
|
||||
guard let date = date else { return nil }
|
||||
var dateStr = date.replacingOccurrences(of: "'", with: "")
|
||||
if dateStr.hasPrefix("D:") {
|
||||
dateStr = date.substring(from: date.characters.index(date.startIndex, offsetBy: 2))
|
||||
dateStr.characters.removeFirst(2)
|
||||
}
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyyMMddHHmmssTZD"
|
||||
dateFormatter.dateFormat = "yyyyMMddHHmmssTZ"
|
||||
if let result = dateFormatter.date(from: dateStr) {
|
||||
return result
|
||||
}
|
||||
dateFormatter.dateFormat = "yyyyMMddHHmmssZZZZZ"
|
||||
if let result = dateFormatter.date(from: dateStr) {
|
||||
return result
|
||||
}
|
||||
dateFormatter.dateFormat = "yyyyMMddHHmmssZ"
|
||||
if let result = dateFormatter.date(from: dateStr) {
|
||||
return result
|
||||
}
|
||||
@@ -373,48 +466,45 @@ public struct LocalFileInformationGenerator {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let data = try? Data(contentsOf: fileURL), let provider = CGDataProvider(data: data as CFData), let reference = CGPDFDocument(provider), let dict = reference.info {
|
||||
if let title = getKey("Title", from: dict), !title.isEmpty {
|
||||
add(key: "Title", value: title)
|
||||
}
|
||||
if let author = getKey("Author", from: dict), !author.isEmpty {
|
||||
add(key: "Author", value: author)
|
||||
}
|
||||
if let subject = getKey("Subject", from: dict), !subject.isEmpty {
|
||||
add(key: "Subject", value: subject)
|
||||
}
|
||||
var majorVersion: Int32 = 0
|
||||
var minorVersion: Int32 = 0
|
||||
reference.getVersion(majorVersion: &majorVersion, minorVersion: &minorVersion)
|
||||
if majorVersion > 0 {
|
||||
add(key: "Version", value: String(majorVersion) + "." + String(minorVersion))
|
||||
}
|
||||
add(key: "Pages", value: reference.numberOfPages)
|
||||
|
||||
if reference.numberOfPages > 0, let pageRef = reference.page(at: 1) {
|
||||
let size = pageRef.getBoxRect(CGPDFBox.mediaBox).size
|
||||
add(key: "Resolution", value: "\(Int(size.width))x\(Int(size.height))")
|
||||
}
|
||||
if let creator = getKey("Creator", from: dict), !creator.isEmpty {
|
||||
add(key: "Content creator", value: creator)
|
||||
}
|
||||
if let creationDateString = getKey("CreationDate", from: dict) {
|
||||
add(key: "Creation date", value: convertDate(creationDateString))
|
||||
}
|
||||
if let modifiedDateString = getKey("ModDate", from: dict) {
|
||||
add(key: "Modified date", value: convertDate(modifiedDateString))
|
||||
}
|
||||
add(key: "Security", value: reference.isEncrypted ? "Present" : "None")
|
||||
add(key: "Allows printing", value: reference.allowsPrinting ? "Yes" : "No")
|
||||
add(key: "Allows copying", value: reference.allowsCopying ? "Yes" : "No")
|
||||
guard let provider = CGDataProvider(url: fileURL as CFURL), let reference = CGPDFDocument(provider), let dict = reference.info else {
|
||||
return (dic, keys)
|
||||
}
|
||||
add(key: "Title", value: getKey("Title", from: dict))
|
||||
add(key: "Author", value: getKey("Author", from: dict))
|
||||
add(key: "Subject", value: getKey("Subject", from: dict))
|
||||
add(key: "Producer", value: getKey("Producer", from: dict))
|
||||
add(key: "Keywords", value: getKey("Keywords", from: dict))
|
||||
var majorVersion: Int32 = 0
|
||||
var minorVersion: Int32 = 0
|
||||
reference.getVersion(majorVersion: &majorVersion, minorVersion: &minorVersion)
|
||||
if majorVersion > 0 {
|
||||
add(key: "Version", value: String(majorVersion) + "." + String(minorVersion))
|
||||
}
|
||||
add(key: "Pages", value: reference.numberOfPages)
|
||||
|
||||
if reference.numberOfPages > 0, let pageRef = reference.page(at: 1) {
|
||||
let size = pageRef.getBoxRect(CGPDFBox.mediaBox).size
|
||||
add(key: "Resolution", value: "\(Int(size.width))x\(Int(size.height))")
|
||||
}
|
||||
add(key: "Content creator", value: getKey("Creator", from: dict))
|
||||
add(key: "Creation date", value: convertDate(getKey("CreationDate", from: dict)))
|
||||
add(key: "Modified date", value: convertDate(getKey("ModDate", from: dict)))
|
||||
add(key: "Security", value: reference.isEncrypted)
|
||||
add(key: "Allows printing", value: reference.allowsPrinting)
|
||||
add(key: "Allows copying", value: reference.allowsCopying)
|
||||
return (dic, keys)
|
||||
}
|
||||
|
||||
/// Properties generator closure for video files.
|
||||
/// - Note: No default implementation is avaiable
|
||||
static public var archiveProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = nil
|
||||
|
||||
/// Properties generator closure for office doument files.
|
||||
/// - Note: No default implementation is avaiable
|
||||
static public var officeProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = nil
|
||||
|
||||
/// Properties generator closure for custom type of files.
|
||||
/// - Note: No default implementation is avaiable
|
||||
static public var customProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = nil
|
||||
}
|
||||
|
||||
|
||||
+478
@@ -0,0 +1,478 @@
|
||||
//
|
||||
// FileProviderExtensions.swift
|
||||
// FileProvider
|
||||
//
|
||||
// Created by Amir Abbas on 12/27/1395 AP.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Array where Element: FileObject {
|
||||
/// Returns a sorted array of `FileObject`s by criterias set in attributes.
|
||||
public func sort(by type: FileObjectSorting.SortType, ascending: Bool = true, isDirectoriesFirst: Bool = false) -> [Element] {
|
||||
let sorting = FileObjectSorting(type: type, ascending: ascending, isDirectoriesFirst: isDirectoriesFirst)
|
||||
return sorting.sort(self) as! [Element]
|
||||
}
|
||||
|
||||
/// Sorts array of `FileObject`s by criterias set in attributes.
|
||||
public mutating func sorted(by type: FileObjectSorting.SortType, ascending: Bool = true, isDirectoriesFirst: Bool = false) {
|
||||
self = self.sort(by: type, ascending: ascending, isDirectoriesFirst: isDirectoriesFirst)
|
||||
}
|
||||
}
|
||||
|
||||
public extension Sequence where Iterator.Element == UInt8 {
|
||||
func hexString() -> String {
|
||||
return self.map{String(format: "%02X", $0)}.joined()
|
||||
}
|
||||
}
|
||||
|
||||
public extension URLFileResourceType {
|
||||
/// **FileProvider** returns corresponding `URLFileResourceType` of a `FileAttributeType` value
|
||||
public init(fileTypeValue: FileAttributeType) {
|
||||
switch fileTypeValue {
|
||||
case FileAttributeType.typeCharacterSpecial: self = .characterSpecial
|
||||
case FileAttributeType.typeDirectory: self = .directory
|
||||
case FileAttributeType.typeBlockSpecial: self = .blockSpecial
|
||||
case FileAttributeType.typeRegular: self = .regular
|
||||
case FileAttributeType.typeSymbolicLink: self = .symbolicLink
|
||||
case FileAttributeType.typeSocket: self = .socket
|
||||
case FileAttributeType.typeUnknown: self = .unknown
|
||||
default: self = .unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension URLResourceKey {
|
||||
/// **FileProvider** returns url of file object.
|
||||
public static let fileURLKey = URLResourceKey(rawValue: "NSURLFileURLKey")
|
||||
/// **FileProvider** returns modification date of file in server
|
||||
public static let serverDateKey = URLResourceKey(rawValue: "NSURLServerDateKey")
|
||||
/// **FileProvider** returns HTTP ETag string of remote resource
|
||||
public static let entryTagKey = URLResourceKey(rawValue: "NSURLEntryTagKey")
|
||||
/// **FileProvider** returns MIME type of file, if returned by server
|
||||
public static let mimeTypeKey = URLResourceKey(rawValue: "NSURLMIMETypeIdentifierKey")
|
||||
/// **FileProvider** returns either file is encrypted or not
|
||||
public static let isEncryptedKey = URLResourceKey(rawValue: "NSURLIsEncryptedKey")
|
||||
}
|
||||
|
||||
public extension ProgressUserInfoKey {
|
||||
/// **FileProvider** returns associated `FileProviderOperationType`
|
||||
public static let fileProvderOperationTypeKey = ProgressUserInfoKey("FilesProviderOperationTypeKey")
|
||||
/// **FileProvider** returns start date/time of operation
|
||||
public static let startingTimeKey = ProgressUserInfoKey("NSProgressStartingTimeKey")
|
||||
}
|
||||
|
||||
internal extension URL {
|
||||
var uw_scheme: String {
|
||||
return self.scheme ?? ""
|
||||
}
|
||||
|
||||
var fileIsDirectory: Bool {
|
||||
return (try? self.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory ?? false
|
||||
}
|
||||
|
||||
var fileSize: Int64 {
|
||||
return Int64((try? self.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? -1)
|
||||
}
|
||||
|
||||
var fileExists: Bool {
|
||||
return self.isFileURL && FileManager.default.fileExists(atPath: self.path)
|
||||
}
|
||||
}
|
||||
|
||||
public extension URLRequest {
|
||||
/// Defines HTTP Authentication method required to access
|
||||
public enum AuthenticationType {
|
||||
/// Basic method for authentication
|
||||
case basic
|
||||
/// Digest method for authentication
|
||||
case digest
|
||||
/// OAuth 1.0 method for authentication (OAuth)
|
||||
case oAuth1
|
||||
/// OAuth 2.0 method for authentication (Bearer)
|
||||
case oAuth2
|
||||
}
|
||||
}
|
||||
|
||||
struct Quality<T> {
|
||||
let value: T
|
||||
let quality: Float
|
||||
|
||||
var stringifed: String {
|
||||
var representaion = String(describing: value)
|
||||
let quality = min(1, max(self.quality, 0))
|
||||
if let value = value as? Locale {
|
||||
representaion = "\(value.identifier.replacingOccurrences(of: "_", with: "-"))"
|
||||
}
|
||||
if let value = value as? String.Encoding {
|
||||
let cfEncoding = CFStringConvertNSStringEncodingToEncoding(value.rawValue)
|
||||
representaion = CFStringConvertEncodingToIANACharSetName(cfEncoding) as String? ?? "*"
|
||||
}
|
||||
let qualityDesc = String(format: "%.1f", quality)
|
||||
return "\(representaion); q=\(qualityDesc)"
|
||||
}
|
||||
}
|
||||
|
||||
internal extension URLRequest {
|
||||
mutating func set(httpAuthentication credential: URLCredential?, with type: AuthenticationType) {
|
||||
func base64(_ str: String) -> String {
|
||||
let plainData = str.data(using: .utf8)
|
||||
let base64String = plainData!.base64EncodedString(options: [])
|
||||
return base64String
|
||||
}
|
||||
|
||||
guard let credential = credential else { return }
|
||||
switch type {
|
||||
case .basic:
|
||||
let user = credential.user?.replacingOccurrences(of: ":", with: "") ?? ""
|
||||
let pass = credential.password ?? ""
|
||||
let authStr = "\(user):\(pass)"
|
||||
if let base64Auth = authStr.data(using: .utf8)?.base64EncodedString() {
|
||||
self.setValue("Basic \(base64Auth)", forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
case .digest:
|
||||
// handled by RemoteSessionDelegate
|
||||
break
|
||||
case .oAuth1:
|
||||
if let oauth = credential.password {
|
||||
self.setValue("OAuth \(oauth)", forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
case .oAuth2:
|
||||
if let bearer = credential.password {
|
||||
self.setValue("Bearer \(bearer)", forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutating func set(httpAcceptCharset acceptCharset: String.Encoding) {
|
||||
let cfEncoding = CFStringConvertNSStringEncodingToEncoding(acceptCharset.rawValue)
|
||||
if let charsetString = CFStringConvertEncodingToIANACharSetName(cfEncoding) as String? {
|
||||
self.addValue(charsetString, forHTTPHeaderField: "Accept-Charset")
|
||||
}
|
||||
}
|
||||
|
||||
mutating func set(httpAcceptCharset acceptCharset: Quality<String.Encoding>) {
|
||||
self.addValue(acceptCharset.stringifed, forHTTPHeaderField: "Accept-Charset")
|
||||
}
|
||||
|
||||
mutating func set(httpAcceptCharsets acceptCharsets: [String.Encoding]) {
|
||||
self.setValue(nil, forHTTPHeaderField: "Accept-Charset")
|
||||
for charset in acceptCharsets {
|
||||
let cfEncoding = CFStringConvertNSStringEncodingToEncoding(charset.rawValue)
|
||||
if let charsetString = CFStringConvertEncodingToIANACharSetName(cfEncoding) as String? {
|
||||
self.addValue(charsetString, forHTTPHeaderField: "Accept-Charset")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutating func set(httpAcceptCharsets acceptCharsets: [Quality<String.Encoding>]) {
|
||||
self.setValue(nil, forHTTPHeaderField: "Accept-Charset")
|
||||
for charset in acceptCharsets.sorted(by: { $0.quality > $1.quality }) {
|
||||
self.addValue(charset.stringifed, forHTTPHeaderField: "Accept-Charset")
|
||||
}
|
||||
}
|
||||
|
||||
enum Encoding: String {
|
||||
case all = "*"
|
||||
case identity
|
||||
case gzip
|
||||
case deflate
|
||||
}
|
||||
|
||||
mutating func set(httpAcceptEncoding acceptEncoding: Encoding) {
|
||||
self.addValue(acceptEncoding.rawValue, forHTTPHeaderField: "Accept-Encoding")
|
||||
}
|
||||
|
||||
mutating func set(httpAcceptEncoding acceptEncoding: Quality<Encoding>) {
|
||||
self.addValue(acceptEncoding.stringifed, forHTTPHeaderField: "Accept-Encoding")
|
||||
}
|
||||
|
||||
mutating func set(httpAcceptEncodings acceptEncodings: [Encoding]) {
|
||||
self.setValue(nil, forHTTPHeaderField: "Accept-Encoding")
|
||||
for encoding in acceptEncodings {
|
||||
self.addValue(encoding.rawValue, forHTTPHeaderField: "Accept-Encoding")
|
||||
}
|
||||
}
|
||||
|
||||
mutating func set(httpAcceptEncodings acceptEncodings: [Quality<Encoding>]) {
|
||||
self.setValue(nil, forHTTPHeaderField: "Accept-Encoding")
|
||||
for encoding in acceptEncodings.sorted(by: { $0.quality > $1.quality }) {
|
||||
self.addValue(encoding.stringifed, forHTTPHeaderField: "Accept-Encoding")
|
||||
}
|
||||
}
|
||||
|
||||
mutating func set(httpAcceptLanguage acceptLanguage: Locale) {
|
||||
let langCode = acceptLanguage.identifier.replacingOccurrences(of: "_", with: "-")
|
||||
self.addValue(langCode, forHTTPHeaderField: "Accept-Language")
|
||||
}
|
||||
|
||||
mutating func set(httpAcceptLanguage acceptLanguage: Quality<Locale>) {
|
||||
self.addValue(acceptLanguage.stringifed, forHTTPHeaderField: "Accept-Language")
|
||||
}
|
||||
|
||||
mutating func set(httpAcceptLanguages acceptLanguages: [Locale]) {
|
||||
self.setValue(nil, forHTTPHeaderField: "Accept-Language")
|
||||
for lang in acceptLanguages {
|
||||
let langCode = lang.identifier.replacingOccurrences(of: "_", with: "-")
|
||||
self.addValue(langCode, forHTTPHeaderField: "Accept-Language")
|
||||
}
|
||||
}
|
||||
|
||||
mutating func set(httpAcceptLanguages acceptLanguages: [Quality<Locale>]) {
|
||||
self.setValue(nil, forHTTPHeaderField: "Accept-Language")
|
||||
for lang in acceptLanguages.sorted(by: { $0.quality > $1.quality} ) {
|
||||
self.addValue(lang.stringifed, forHTTPHeaderField: "Accept-Language")
|
||||
}
|
||||
}
|
||||
|
||||
mutating func set(httpRangeWithOffset offset: Int64, length: Int) {
|
||||
if length > 0 {
|
||||
self.setValue("bytes=\(offset)-\(offset + Int64(length) - 1)", forHTTPHeaderField: "Range")
|
||||
} else if offset > 0 && length < 0 {
|
||||
self.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
|
||||
}
|
||||
}
|
||||
|
||||
mutating func set(httpRange range: Range<Int>) {
|
||||
let range = max(0, range.lowerBound)..<range.upperBound
|
||||
if range.upperBound < Int.max && range.count > 0 {
|
||||
self.setValue("bytes=\(range.lowerBound)-\(range.upperBound - 1)", forHTTPHeaderField: "Range")
|
||||
} else if range.lowerBound > 0 {
|
||||
self.setValue("bytes=\(range.lowerBound)-", forHTTPHeaderField: "Range")
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentMIMEType: RawRepresentable {
|
||||
public var rawValue: String
|
||||
public typealias RawValue = String
|
||||
|
||||
public init(rawValue: String) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
static let javascript = ContentMIMEType(rawValue: "application/javascript")
|
||||
static let json = ContentMIMEType(rawValue: "application/json")
|
||||
static let pdf = ContentMIMEType(rawValue: "application/pdf")
|
||||
static let stream = ContentMIMEType(rawValue: "application/octet-stream")
|
||||
static let zip = ContentMIMEType(rawValue: "application/zip")
|
||||
|
||||
// Texts
|
||||
static let css = ContentMIMEType(rawValue: "text/css")
|
||||
static let html = ContentMIMEType(rawValue: "text/html")
|
||||
static let plainText = ContentMIMEType(rawValue: "text/plain")
|
||||
static let xml = ContentMIMEType(rawValue: "text/xml")
|
||||
|
||||
// Images
|
||||
static let gif = ContentMIMEType(rawValue: "image/gif")
|
||||
static let jpeg = ContentMIMEType(rawValue: "image/jpeg")
|
||||
static let png = ContentMIMEType(rawValue: "image/png")
|
||||
}
|
||||
|
||||
mutating func set(httpContentType contentType: ContentMIMEType, charset: String.Encoding? = nil) {
|
||||
var parameter = ""
|
||||
if let charset = charset {
|
||||
let cfEncoding = CFStringConvertNSStringEncodingToEncoding(charset.rawValue)
|
||||
if let charsetString = CFStringConvertEncodingToIANACharSetName(cfEncoding) as String? {
|
||||
parameter = ";charset=" + charsetString
|
||||
}
|
||||
}
|
||||
|
||||
self.setValue(contentType.rawValue + parameter, forHTTPHeaderField: "Content-Type")
|
||||
}
|
||||
|
||||
mutating func set(dropboxArgKey requestDictionary: [String: AnyObject]) {
|
||||
guard let jsonData = try? JSONSerialization.data(withJSONObject: requestDictionary, options: []) else {
|
||||
return
|
||||
}
|
||||
guard var jsonString = String(data: jsonData, encoding: .utf8) else { return }
|
||||
jsonString = jsonString.asciiEscaped().replacingOccurrences(of: "\\/", with: "/")
|
||||
|
||||
self.setValue(jsonString, forHTTPHeaderField: "Dropbox-API-Arg")
|
||||
}
|
||||
}
|
||||
|
||||
internal extension CharacterSet {
|
||||
static let filePathAllowed = CharacterSet.urlPathAllowed.subtracting(CharacterSet(charactersIn: ":"))
|
||||
}
|
||||
|
||||
internal extension Data {
|
||||
internal var isPDF: Bool {
|
||||
return self.count > 4 && self.scanString(length: 4, using: .ascii) == "%PDF"
|
||||
}
|
||||
|
||||
init? (jsonDictionary dictionary: [String: AnyObject]) {
|
||||
guard let data = try? JSONSerialization.data(withJSONObject: dictionary, options: []) else {
|
||||
return nil
|
||||
}
|
||||
self = data
|
||||
}
|
||||
|
||||
func deserializeJSON() -> [String: AnyObject]? {
|
||||
if let dic = try? JSONSerialization.jsonObject(with: self, options: []) as? [String: AnyObject] {
|
||||
return dic
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
init<T>(value: T) {
|
||||
var value = value
|
||||
self = Data(buffer: UnsafeBufferPointer(start: &value, count: 1))
|
||||
}
|
||||
|
||||
func scanValue<T>() -> T? {
|
||||
guard MemoryLayout<T>.size <= self.count else { return nil }
|
||||
return self.withUnsafeBytes { $0.pointee }
|
||||
}
|
||||
|
||||
func scanValue<T>(start: Int) -> T? {
|
||||
let length = MemoryLayout<T>.size
|
||||
guard self.count >= start + length else { return nil }
|
||||
return self.subdata(in: start..<start+length).withUnsafeBytes { $0.pointee }
|
||||
}
|
||||
|
||||
func scanString(start: Int = 0, length: Int, using encoding: String.Encoding = .utf8) -> String? {
|
||||
guard self.count >= start + length else { return nil }
|
||||
return String(data: self.subdata(in: start..<start+length), encoding: encoding)
|
||||
}
|
||||
|
||||
static func mapMemory<T, U>(from: T) -> U? {
|
||||
guard MemoryLayout<T>.size >= MemoryLayout<U>.size else { return nil }
|
||||
let data = Data(value: from)
|
||||
return data.scanValue()
|
||||
}
|
||||
}
|
||||
|
||||
internal extension String {
|
||||
init? (jsonDictionary: [String: AnyObject]) {
|
||||
guard let data = Data(jsonDictionary: jsonDictionary) else {
|
||||
return nil
|
||||
}
|
||||
self.init(data: data, encoding: .utf8)
|
||||
}
|
||||
|
||||
func deserializeJSON(using encoding: String.Encoding = .utf8) -> [String: AnyObject]? {
|
||||
guard let data = self.data(using: encoding) else {
|
||||
return nil
|
||||
}
|
||||
return data.deserializeJSON()
|
||||
}
|
||||
|
||||
func asciiEscaped() -> String {
|
||||
var res = ""
|
||||
for char in self.unicodeScalars {
|
||||
let substring = String(char)
|
||||
if substring.canBeConverted(to: .ascii) {
|
||||
res.append(substring)
|
||||
} else {
|
||||
res = res.appendingFormat("\\u%04x", char.value)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
internal extension TimeInterval {
|
||||
internal var formatshort: String {
|
||||
var result = "0:00"
|
||||
if self < TimeInterval(Int32.max) {
|
||||
result = ""
|
||||
var time = DateComponents()
|
||||
time.hour = Int(self / 3600)
|
||||
time.minute = Int((self.truncatingRemainder(dividingBy: 3600)) / 60)
|
||||
time.second = Int(self.truncatingRemainder(dividingBy: 60))
|
||||
let formatter = NumberFormatter()
|
||||
formatter.paddingCharacter = "0"
|
||||
formatter.minimumIntegerDigits = 2
|
||||
formatter.maximumFractionDigits = 0
|
||||
let formatterFirst = NumberFormatter()
|
||||
formatterFirst.maximumFractionDigits = 0
|
||||
if time.hour! > 0 {
|
||||
result = "\(formatterFirst.string(from: NSNumber(value: time.hour!))!):\(formatter.string(from: NSNumber(value: time.minute!))!):\(formatter.string(from: NSNumber(value: time.second!))!)"
|
||||
} else {
|
||||
result = "\(formatterFirst.string(from: NSNumber(value: time.minute!))!):\(formatter.string(from: NSNumber(value: time.second!))!)"
|
||||
}
|
||||
}
|
||||
result = result.trimmingCharacters(in: CharacterSet(charactersIn: ": "))
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
public extension Date {
|
||||
/// Date formats used commonly in internet messaging defined by various RFCs.
|
||||
public enum RFCStandards: String {
|
||||
/// Date format defined by usenet, commonly used in old implementations.
|
||||
case rfc850 = "EEEE',' dd'-'MMM'-'yy HH':'mm':'ss z"
|
||||
/// Date format defined by RFC 1132 for http.
|
||||
case rfc1123 = "EEE',' dd' 'MMM' 'yyyy HH':'mm':'ss z"
|
||||
/// Date format defined by ISO 8601, also defined in RFC 3339. Used by Dropbox.
|
||||
case iso8601 = "yyyy'-'MM'-'dd'T'HH':'mm':'ssZ"
|
||||
/// Date string returned by asctime() function.
|
||||
case asctime = "EEE MMM d HH':'mm':'ss yyyy"
|
||||
|
||||
/// Equivalent to and defined by RFC 1123.
|
||||
public static let http = RFCStandards.rfc1123
|
||||
/// Equivalent to and defined by ISO 8610.
|
||||
public static let rfc3339 = RFCStandards.iso8601
|
||||
/// Equivalent to and defined by RFC 850.
|
||||
public static let usenet = RFCStandards.rfc850
|
||||
|
||||
// Sorted by commonness
|
||||
fileprivate static let allValues: [RFCStandards] = [.rfc1123, .rfc850, .iso8601, .asctime]
|
||||
}
|
||||
|
||||
/// Checks date string against various RFC standards and returns `Date`.
|
||||
public init?(rfcString: String) {
|
||||
let dateFor: DateFormatter = DateFormatter()
|
||||
dateFor.locale = Locale(identifier: "en_US")
|
||||
|
||||
for standard in RFCStandards.allValues {
|
||||
dateFor.dateFormat = standard.rawValue
|
||||
if let date = dateFor.date(from: rfcString) {
|
||||
self = date
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Formats date according to RFCs standard.
|
||||
public func format(with standard: RFCStandards, locale: Locale? = nil, timeZone: TimeZone? = nil) -> String {
|
||||
let fm = DateFormatter()
|
||||
fm.dateFormat = standard.rawValue
|
||||
fm.timeZone = timeZone ?? TimeZone(identifier: "UTC")
|
||||
fm.locale = locale ?? Locale(identifier: "en_US_POSIX")
|
||||
return fm.string(from: self)
|
||||
}
|
||||
}
|
||||
|
||||
internal extension NSPredicate {
|
||||
func findValue(forKey key: String?, operator op: NSComparisonPredicate.Operator? = nil) -> Any? {
|
||||
let val = findAllValues(forKey: key).lazy.filter { (op == nil || $0.operator == op!) && !$0.not }
|
||||
return val.first?.value
|
||||
}
|
||||
|
||||
func findAllValues(forKey key: String?) -> [(value: Any, operator: NSComparisonPredicate.Operator, not: Bool)] {
|
||||
if let cQuery = self as? NSCompoundPredicate {
|
||||
let find = cQuery.subpredicates.flatMap { ($0 as! NSPredicate).findAllValues(forKey: key) }
|
||||
if cQuery.compoundPredicateType == .not {
|
||||
return find.map { return ($0.value, $0.operator, !$0.not) }
|
||||
}
|
||||
return find
|
||||
} else if let cQuery = self as? NSComparisonPredicate {
|
||||
if cQuery.leftExpression.expressionType == .keyPath, key == nil || cQuery.leftExpression.keyPath == key!, let const = cQuery.rightExpression.constantValue {
|
||||
return [(value: const, operator: cQuery.predicateOperatorType, false)]
|
||||
}
|
||||
if cQuery.rightExpression.expressionType == .keyPath, key == nil || cQuery.rightExpression.keyPath == key!, let const = cQuery.leftExpression.constantValue {
|
||||
return [(value: const, operator: cQuery.predicateOperatorType, false)]
|
||||
}
|
||||
return []
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension URLError.Code: FoundationErrorEnum {}
|
||||
extension CocoaError.Code: FoundationErrorEnum {}
|
||||
@@ -0,0 +1,462 @@
|
||||
// Originally based on CryptoSwift by Marcin Krzyżanowski <marcin.krzyzanowski@gmail.com>
|
||||
// Copyright (C) 2014 Marcin Krzyżanowski <marcin.krzyzanowski@gmail.com>
|
||||
// This software is provided 'as-is', without any express or implied warranty.
|
||||
//
|
||||
// In no event will the authors be held liable for any damages arising from the use of this software.
|
||||
//
|
||||
// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
|
||||
//
|
||||
// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required.
|
||||
// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
|
||||
// - This notice may not be removed or altered from any source or binary distribution.
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol SHA2Variant {
|
||||
static var size: Int { get }
|
||||
static var h: [UInt64] { get }
|
||||
static var k: [UInt64] { get }
|
||||
|
||||
static func resultingArray<T>(_ hh:[T]) -> ArraySlice<T>
|
||||
static func calculate(_ message: [UInt8]) -> [UInt8]
|
||||
}
|
||||
|
||||
protocol SHA2Variant32: SHA2Variant { }
|
||||
protocol SHA2Variant64: SHA2Variant { }
|
||||
|
||||
extension SHA2Variant32 {
|
||||
static func calculate(_ message: [UInt8]) -> [UInt8] {
|
||||
var tmpMessage = message
|
||||
|
||||
let len = 64
|
||||
|
||||
// Step 1. Append Padding Bits
|
||||
tmpMessage.append(0x80) // append one bit (UInt8 with one bit) to message
|
||||
|
||||
// append "0" bit until message length in bits ≡ 448 (mod 512)
|
||||
var msgLength = tmpMessage.count
|
||||
var counter = 0
|
||||
|
||||
while msgLength % len != (len - 8) {
|
||||
counter += 1
|
||||
msgLength += 1
|
||||
}
|
||||
|
||||
tmpMessage += [UInt8](repeating: 0, count: counter)
|
||||
|
||||
// hash values
|
||||
var hh = [UInt32]()
|
||||
Self.h.forEach {(h) -> () in
|
||||
hh.append(UInt32(h))
|
||||
}
|
||||
|
||||
// append message length, in a 64-bit big-endian integer. So now the message length is a multiple of 512 bits.
|
||||
tmpMessage += arrayOfBytes(message.count * 8, length: 64 / 8)
|
||||
|
||||
// Process the message in successive 512-bit chunks:
|
||||
let chunkSizeBytes = 512 / 8 // 64
|
||||
for chunk in BytesSequence(chunkSize: chunkSizeBytes, data: tmpMessage) {
|
||||
// break chunk into sixteen 32-bit words M[j], 0 ≤ j ≤ 15, big-endian
|
||||
// Extend the sixteen 32-bit words into sixty-four 32-bit words:
|
||||
var M:[UInt32] = [UInt32](repeating: 0, count: Self.k.count)
|
||||
for x in 0..<M.count {
|
||||
switch (x) {
|
||||
case 0...15:
|
||||
let start = chunk.startIndex + (x * MemoryLayout.size(ofValue: M[x]))
|
||||
let end = start + MemoryLayout.size(ofValue: M[x])
|
||||
let le = toUInt32Array(chunk[start..<end])[0]
|
||||
M[x] = le.bigEndian
|
||||
break
|
||||
default:
|
||||
let s0 = rotateRight(M[x-15], n: 7) ^ rotateRight(M[x-15], n: 18) ^ (M[x-15] >> 3) //FIXME: n
|
||||
let s1 = rotateRight(M[x-2], n: 17) ^ rotateRight(M[x-2], n: 19) ^ (M[x-2] >> 10)
|
||||
M[x] = M[x-16] &+ s0 &+ M[x-7] &+ s1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var A = hh[0]
|
||||
var B = hh[1]
|
||||
var C = hh[2]
|
||||
var D = hh[3]
|
||||
var E = hh[4]
|
||||
var F = hh[5]
|
||||
var G = hh[6]
|
||||
var H = hh[7]
|
||||
|
||||
// Main loop
|
||||
for j in 0..<Self.k.count {
|
||||
let s0 = rotateRight(A,n: 2) ^ rotateRight(A,n: 13) ^ rotateRight(A,n: 22)
|
||||
let maj = (A & B) ^ (A & C) ^ (B & C)
|
||||
let t2 = s0 &+ maj
|
||||
let s1 = rotateRight(E,n: 6) ^ rotateRight(E,n: 11) ^ rotateRight(E,n: 25)
|
||||
let ch = (E & F) ^ ((~E) & G)
|
||||
let t1 = H &+ s1 &+ ch &+ UInt32(Self.k[j]) &+ M[j]
|
||||
|
||||
H = G
|
||||
G = F
|
||||
F = E
|
||||
E = D &+ t1
|
||||
D = C
|
||||
C = B
|
||||
B = A
|
||||
A = t1 &+ t2
|
||||
}
|
||||
|
||||
hh[0] = (hh[0] &+ A)
|
||||
hh[1] = (hh[1] &+ B)
|
||||
hh[2] = (hh[2] &+ C)
|
||||
hh[3] = (hh[3] &+ D)
|
||||
hh[4] = (hh[4] &+ E)
|
||||
hh[5] = (hh[5] &+ F)
|
||||
hh[6] = (hh[6] &+ G)
|
||||
hh[7] = (hh[7] &+ H)
|
||||
}
|
||||
|
||||
// Produce the final hash value (big-endian) as a 160 bit number:
|
||||
var result = [UInt8]()
|
||||
result.reserveCapacity(hh.count / 4)
|
||||
Self.resultingArray(hh).forEach {
|
||||
let item = $0.bigEndian
|
||||
result += [UInt8(item & 0xff), UInt8((item >> 8) & 0xff), UInt8((item >> 16) & 0xff), UInt8((item >> 24) & 0xff)]
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
extension SHA2Variant64 {
|
||||
static func calculate(_ message: [UInt8]) -> [UInt8] {
|
||||
var tmpMessage = message
|
||||
|
||||
let len = 128
|
||||
|
||||
// Step 1. Append Padding Bits
|
||||
tmpMessage.append(0x80) // append one bit (UInt8 with one bit) to message
|
||||
|
||||
// append "0" bit until message length in bits ≡ 448 (mod 512)
|
||||
var msgLength = tmpMessage.count
|
||||
var counter = 0
|
||||
|
||||
while msgLength % len != (len - 8) {
|
||||
counter += 1
|
||||
msgLength += 1
|
||||
}
|
||||
|
||||
tmpMessage += [UInt8](repeating: 0, count: counter)
|
||||
|
||||
// hash values
|
||||
var hh = [UInt64]()
|
||||
Self.h.forEach {(h) -> () in
|
||||
hh.append(h)
|
||||
}
|
||||
|
||||
|
||||
// append message length, in a 64-bit big-endian integer. So now the message length is a multiple of 512 bits.
|
||||
tmpMessage += arrayOfBytes(message.count * 8, length: 64 / 8)
|
||||
|
||||
// Process the message in successive 1024-bit chunks:
|
||||
let chunkSizeBytes = 1024 / 8 // 128
|
||||
for chunk in BytesSequence(chunkSize: chunkSizeBytes, data: tmpMessage) {
|
||||
// break chunk into sixteen 64-bit words M[j], 0 ≤ j ≤ 15, big-endian
|
||||
// Extend the sixteen 64-bit words into eighty 64-bit words:
|
||||
var M = [UInt64](repeating: 0, count: Self.k.count)
|
||||
for x in 0..<M.count {
|
||||
switch (x) {
|
||||
case 0...15:
|
||||
let start = chunk.startIndex + (x * MemoryLayout.size(ofValue: M[x]))
|
||||
let end = start + MemoryLayout.size(ofValue: M[x])
|
||||
let le = toUInt64Array(chunk[start..<end])[0]
|
||||
M[x] = le.bigEndian
|
||||
break
|
||||
default:
|
||||
let s0 = rotateRight(M[x-15], n: 1) ^ rotateRight(M[x-15], n: 8) ^ (M[x-15] >> 7)
|
||||
let s1 = rotateRight(M[x-2], n: 19) ^ rotateRight(M[x-2], n: 61) ^ (M[x-2] >> 6)
|
||||
M[x] = M[x-16] &+ s0 &+ M[x-7] &+ s1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var A = hh[0]
|
||||
var B = hh[1]
|
||||
var C = hh[2]
|
||||
var D = hh[3]
|
||||
var E = hh[4]
|
||||
var F = hh[5]
|
||||
var G = hh[6]
|
||||
var H = hh[7]
|
||||
|
||||
// Main loop
|
||||
for j in 0..<Self.k.count {
|
||||
let s0 = rotateRight(A,n: 28) ^ rotateRight(A,n: 34) ^ rotateRight(A,n: 39) //FIXME: n:
|
||||
let maj = (A & B) ^ (A & C) ^ (B & C)
|
||||
let t2 = s0 &+ maj
|
||||
let s1 = rotateRight(E,n: 14) ^ rotateRight(E,n: 18) ^ rotateRight(E,n: 41)
|
||||
let ch = (E & F) ^ ((~E) & G)
|
||||
let t1 = H &+ s1 &+ ch &+ Self.k[j] &+ UInt64(M[j])
|
||||
|
||||
H = G
|
||||
G = F
|
||||
F = E
|
||||
E = D &+ t1
|
||||
D = C
|
||||
C = B
|
||||
B = A
|
||||
A = t1 &+ t2
|
||||
}
|
||||
|
||||
hh[0] = (hh[0] &+ A)
|
||||
hh[1] = (hh[1] &+ B)
|
||||
hh[2] = (hh[2] &+ C)
|
||||
hh[3] = (hh[3] &+ D)
|
||||
hh[4] = (hh[4] &+ E)
|
||||
hh[5] = (hh[5] &+ F)
|
||||
hh[6] = (hh[6] &+ G)
|
||||
hh[7] = (hh[7] &+ H)
|
||||
}
|
||||
|
||||
// Produce the final hash value (big-endian)
|
||||
var result = [UInt8]()
|
||||
result.reserveCapacity(hh.count / 4)
|
||||
Self.resultingArray(hh).forEach {
|
||||
let item = $0.bigEndian
|
||||
var partialResult = [UInt8]()
|
||||
partialResult.reserveCapacity(8)
|
||||
for i in 0..<8 {
|
||||
let shift = UInt64(8 * i)
|
||||
partialResult.append(UInt8((item >> shift) & 0xff))
|
||||
}
|
||||
result += partialResult
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
final class SHA256 : SHA2Variant32 {
|
||||
static let size = 64
|
||||
static let h: [UInt64] = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]
|
||||
static let k: [UInt64] = [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2]
|
||||
|
||||
static func resultingArray<T>(_ hh: [T]) -> ArraySlice<T> {
|
||||
return ArraySlice(hh)
|
||||
}
|
||||
}
|
||||
|
||||
final class SHA384 : SHA2Variant64 {
|
||||
static let size = 128
|
||||
static let h: [UInt64] = [0xcbbb9d5dc1059ed8, 0x629a292a367cd507, 0x9159015a3070dd17, 0x152fecd8f70e5939, 0x67332667ffc00b31, 0x8eb44a8768581511, 0xdb0c2e0d64f98fa7, 0x47b5481dbefa4fa4]
|
||||
static let k: [UInt64] = [0x428a2f98d728ae22, 0x7137449123ef65cd, 0xb5c0fbcfec4d3b2f, 0xe9b5dba58189dbbc, 0x3956c25bf348b538,
|
||||
0x59f111f1b605d019, 0x923f82a4af194f9b, 0xab1c5ed5da6d8118, 0xd807aa98a3030242, 0x12835b0145706fbe,
|
||||
0x243185be4ee4b28c, 0x550c7dc3d5ffb4e2, 0x72be5d74f27b896f, 0x80deb1fe3b1696b1, 0x9bdc06a725c71235,
|
||||
0xc19bf174cf692694, 0xe49b69c19ef14ad2, 0xefbe4786384f25e3, 0x0fc19dc68b8cd5b5, 0x240ca1cc77ac9c65,
|
||||
0x2de92c6f592b0275, 0x4a7484aa6ea6e483, 0x5cb0a9dcbd41fbd4, 0x76f988da831153b5, 0x983e5152ee66dfab,
|
||||
0xa831c66d2db43210, 0xb00327c898fb213f, 0xbf597fc7beef0ee4, 0xc6e00bf33da88fc2, 0xd5a79147930aa725,
|
||||
0x06ca6351e003826f, 0x142929670a0e6e70, 0x27b70a8546d22ffc, 0x2e1b21385c26c926, 0x4d2c6dfc5ac42aed,
|
||||
0x53380d139d95b3df, 0x650a73548baf63de, 0x766a0abb3c77b2a8, 0x81c2c92e47edaee6, 0x92722c851482353b,
|
||||
0xa2bfe8a14cf10364, 0xa81a664bbc423001, 0xc24b8b70d0f89791, 0xc76c51a30654be30, 0xd192e819d6ef5218,
|
||||
0xd69906245565a910, 0xf40e35855771202a, 0x106aa07032bbd1b8, 0x19a4c116b8d2d0c8, 0x1e376c085141ab53,
|
||||
0x2748774cdf8eeb99, 0x34b0bcb5e19b48a8, 0x391c0cb3c5c95a63, 0x4ed8aa4ae3418acb, 0x5b9cca4f7763e373,
|
||||
0x682e6ff3d6b2b8a3, 0x748f82ee5defb2fc, 0x78a5636f43172f60, 0x84c87814a1f0ab72, 0x8cc702081a6439ec,
|
||||
0x90befffa23631e28, 0xa4506cebde82bde9, 0xbef9a3f7b2c67915, 0xc67178f2e372532b, 0xca273eceea26619c,
|
||||
0xd186b8c721c0c207, 0xeada7dd6cde0eb1e, 0xf57d4f7fee6ed178, 0x06f067aa72176fba, 0x0a637dc5a2c898a6,
|
||||
0x113f9804bef90dae, 0x1b710b35131c471b, 0x28db77f523047d84, 0x32caab7b40c72493, 0x3c9ebe0a15c9bebc,
|
||||
0x431d67c49c100d4c, 0x4cc5d4becb3e42b6, 0x597f299cfc657e2a, 0x5fcb6fab3ad6faec, 0x6c44198c4a475817]
|
||||
|
||||
public static func resultingArray<T>(_ hh: [T]) -> ArraySlice<T> {
|
||||
return hh[0..<6]
|
||||
}
|
||||
}
|
||||
|
||||
final class SHA512 : SHA2Variant64 {
|
||||
static let size = 128
|
||||
static let h: [UInt64] = [0x6a09e667f3bcc908, 0xbb67ae8584caa73b, 0x3c6ef372fe94f82b, 0xa54ff53a5f1d36f1, 0x510e527fade682d1, 0x9b05688c2b3e6c1f, 0x1f83d9abfb41bd6b, 0x5be0cd19137e2179]
|
||||
static let k: [UInt64] = [0x428a2f98d728ae22, 0x7137449123ef65cd, 0xb5c0fbcfec4d3b2f, 0xe9b5dba58189dbbc, 0x3956c25bf348b538,
|
||||
0x59f111f1b605d019, 0x923f82a4af194f9b, 0xab1c5ed5da6d8118, 0xd807aa98a3030242, 0x12835b0145706fbe,
|
||||
0x243185be4ee4b28c, 0x550c7dc3d5ffb4e2, 0x72be5d74f27b896f, 0x80deb1fe3b1696b1, 0x9bdc06a725c71235,
|
||||
0xc19bf174cf692694, 0xe49b69c19ef14ad2, 0xefbe4786384f25e3, 0x0fc19dc68b8cd5b5, 0x240ca1cc77ac9c65,
|
||||
0x2de92c6f592b0275, 0x4a7484aa6ea6e483, 0x5cb0a9dcbd41fbd4, 0x76f988da831153b5, 0x983e5152ee66dfab,
|
||||
0xa831c66d2db43210, 0xb00327c898fb213f, 0xbf597fc7beef0ee4, 0xc6e00bf33da88fc2, 0xd5a79147930aa725,
|
||||
0x06ca6351e003826f, 0x142929670a0e6e70, 0x27b70a8546d22ffc, 0x2e1b21385c26c926, 0x4d2c6dfc5ac42aed,
|
||||
0x53380d139d95b3df, 0x650a73548baf63de, 0x766a0abb3c77b2a8, 0x81c2c92e47edaee6, 0x92722c851482353b,
|
||||
0xa2bfe8a14cf10364, 0xa81a664bbc423001, 0xc24b8b70d0f89791, 0xc76c51a30654be30, 0xd192e819d6ef5218,
|
||||
0xd69906245565a910, 0xf40e35855771202a, 0x106aa07032bbd1b8, 0x19a4c116b8d2d0c8, 0x1e376c085141ab53,
|
||||
0x2748774cdf8eeb99, 0x34b0bcb5e19b48a8, 0x391c0cb3c5c95a63, 0x4ed8aa4ae3418acb, 0x5b9cca4f7763e373,
|
||||
0x682e6ff3d6b2b8a3, 0x748f82ee5defb2fc, 0x78a5636f43172f60, 0x84c87814a1f0ab72, 0x8cc702081a6439ec,
|
||||
0x90befffa23631e28, 0xa4506cebde82bde9, 0xbef9a3f7b2c67915, 0xc67178f2e372532b, 0xca273eceea26619c,
|
||||
0xd186b8c721c0c207, 0xeada7dd6cde0eb1e, 0xf57d4f7fee6ed178, 0x06f067aa72176fba, 0x0a637dc5a2c898a6,
|
||||
0x113f9804bef90dae, 0x1b710b35131c471b, 0x28db77f523047d84, 0x32caab7b40c72493, 0x3c9ebe0a15c9bebc,
|
||||
0x431d67c49c100d4c, 0x4cc5d4becb3e42b6, 0x597f299cfc657e2a, 0x5fcb6fab3ad6faec, 0x6c44198c4a475817]
|
||||
|
||||
static func resultingArray<T>(_ hh: [T]) -> ArraySlice<T> {
|
||||
return ArraySlice(hh)
|
||||
}
|
||||
}
|
||||
|
||||
final class SHA2<Variant: SHA2Variant> {
|
||||
static var size: Int {
|
||||
return Variant.size
|
||||
}
|
||||
|
||||
static func calculate(_ message: [UInt8]) -> [UInt8] {
|
||||
return Variant.calculate(message)
|
||||
}
|
||||
}
|
||||
|
||||
final class HMAC<Variant: SHA2Variant> {
|
||||
public static func authenticate(message:[UInt8], withKey key: [UInt8]) -> [UInt8] {
|
||||
var key = key
|
||||
|
||||
if (key.count > Variant.size) {
|
||||
key = Variant.calculate(key)
|
||||
}
|
||||
|
||||
if (key.count < Variant.size) { // keys shorter than blocksize are zero-padded
|
||||
key = key + [UInt8](repeating: 0, count: Variant.size - key.count)
|
||||
}
|
||||
|
||||
var opad = [UInt8](repeating: 0x5c, count: Variant.size)
|
||||
for (idx, _) in key.enumerated() {
|
||||
opad[idx] = key[idx] ^ opad[idx]
|
||||
}
|
||||
var ipad = [UInt8](repeating: 0x36, count: Variant.size)
|
||||
for (idx, _) in key.enumerated() {
|
||||
ipad[idx] = key[idx] ^ ipad[idx]
|
||||
}
|
||||
|
||||
let ipadAndMessageHash = Variant.calculate(ipad + message)
|
||||
let finalHash = Variant.calculate(opad + ipadAndMessageHash);
|
||||
|
||||
return finalHash
|
||||
}
|
||||
|
||||
static func authenticate(message: String, withKey key: [UInt8]) -> [UInt8] {
|
||||
return authenticate(message: [UInt8](message.utf8), withKey: key)
|
||||
}
|
||||
|
||||
static func authenticate(message: Data, withKey key: Data) -> Data {
|
||||
return Data(bytes: authenticate(message: Array(message), withKey: Array(key)))
|
||||
}
|
||||
|
||||
static func authenticate(message: String, withKey key: Data) -> Data {
|
||||
return Data(bytes: authenticate(message: [UInt8](message.utf8), withKey: Array(key)))
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct BytesSequence: Sequence {
|
||||
let chunkSize: Int
|
||||
let data: [UInt8]
|
||||
|
||||
init(chunkSize: Int, data: [UInt8]) {
|
||||
self.chunkSize = chunkSize
|
||||
self.data = data
|
||||
}
|
||||
|
||||
func makeIterator() -> AnyIterator<ArraySlice<UInt8>> {
|
||||
var offset:Int = 0
|
||||
|
||||
return AnyIterator {
|
||||
let end = Swift.min(self.chunkSize, self.data.count - offset)
|
||||
let result = self.data[offset..<offset + end]
|
||||
offset += result.count
|
||||
return result.count > 0 ? result : nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func rotateRight(_ x:UInt16, n:UInt16) -> UInt16 {
|
||||
return (x >> n) | (x << (16 - n))
|
||||
}
|
||||
|
||||
fileprivate func rotateRight(_ x:UInt32, n:UInt32) -> UInt32 {
|
||||
return (x >> n) | (x << (32 - n))
|
||||
}
|
||||
|
||||
fileprivate func rotateRight(_ x:UInt64, n:UInt64) -> UInt64 {
|
||||
return ((x >> n) | (x << (64 - n)))
|
||||
}
|
||||
|
||||
fileprivate func toUInt32Array(_ slice: ArraySlice<UInt8>) -> Array<UInt32> {
|
||||
var result = Array<UInt32>()
|
||||
result.reserveCapacity(16)
|
||||
|
||||
for idx in stride(from: slice.startIndex, to: slice.endIndex, by: MemoryLayout<UInt32>.size) {
|
||||
let val1:UInt32 = (UInt32(slice[idx.advanced(by: 3)]) << 24)
|
||||
let val2:UInt32 = (UInt32(slice[idx.advanced(by: 2)]) << 16)
|
||||
let val3:UInt32 = (UInt32(slice[idx.advanced(by: 1)]) << 8)
|
||||
let val4:UInt32 = UInt32(slice[idx])
|
||||
let val:UInt32 = val1 | val2 | val3 | val4
|
||||
result.append(val)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fileprivate func toUInt64Array(_ slice: ArraySlice<UInt8>) -> Array<UInt64> {
|
||||
var result = Array<UInt64>()
|
||||
result.reserveCapacity(32)
|
||||
for idx in stride(from: slice.startIndex, to: slice.endIndex, by: MemoryLayout<UInt64>.size) {
|
||||
var val:UInt64 = 0
|
||||
val |= UInt64(slice[idx.advanced(by: 7)]) << 56
|
||||
val |= UInt64(slice[idx.advanced(by: 6)]) << 48
|
||||
val |= UInt64(slice[idx.advanced(by: 5)]) << 40
|
||||
val |= UInt64(slice[idx.advanced(by: 4)]) << 32
|
||||
val |= UInt64(slice[idx.advanced(by: 3)]) << 24
|
||||
val |= UInt64(slice[idx.advanced(by: 2)]) << 16
|
||||
val |= UInt64(slice[idx.advanced(by: 1)]) << 8
|
||||
val |= UInt64(slice[idx.advanced(by: 0)]) << 0
|
||||
result.append(val)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fileprivate func arrayOfBytes<T>(_ value:T, length:Int? = nil) -> [UInt8] {
|
||||
let totalBytes = length ?? MemoryLayout<T>.size
|
||||
|
||||
let valuePointer = UnsafeMutablePointer<T>.allocate(capacity: 1)
|
||||
|
||||
valuePointer.pointee = value
|
||||
|
||||
let bytesPointer = UnsafeMutableRawPointer(valuePointer).assumingMemoryBound(to: UInt8.self)
|
||||
var bytes = [UInt8](repeating: 0, count: totalBytes)
|
||||
for j in 0..<min(MemoryLayout<T>.size,totalBytes) {
|
||||
bytes[totalBytes - 1 - j] = (bytesPointer + j).pointee
|
||||
}
|
||||
|
||||
valuePointer.deinitialize()
|
||||
valuePointer.deallocate(capacity: 1)
|
||||
|
||||
return bytes
|
||||
}
|
||||
|
||||
public extension String {
|
||||
public func fp_sha256() -> [UInt8] {
|
||||
return SHA2<SHA256>.calculate([UInt8](self.utf8))
|
||||
}
|
||||
|
||||
public func fp_sha384() -> [UInt8] {
|
||||
return SHA2<SHA384>.calculate([UInt8](self.utf8))
|
||||
}
|
||||
|
||||
public func fp_sha512() -> [UInt8] {
|
||||
return SHA2<SHA512>.calculate([UInt8](self.utf8))
|
||||
}
|
||||
}
|
||||
|
||||
public extension Data {
|
||||
public func fp_sha256() -> [UInt8] {
|
||||
return SHA2<SHA256>.calculate(Array(self))
|
||||
}
|
||||
|
||||
public func fp_sha384() -> [UInt8] {
|
||||
return SHA2<SHA384>.calculate(Array(self))
|
||||
}
|
||||
|
||||
public func fp_sha512() -> [UInt8] {
|
||||
return SHA2<SHA512>.calculate(Array(self))
|
||||
}
|
||||
}
|
||||
+555
-322
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,894 @@
|
||||
//
|
||||
// FTPFileProvider.swift
|
||||
// FileProvider
|
||||
//
|
||||
// Created by Amir Abbas Mousavian.
|
||||
// Copyright © 2017 Mousavian. Distributed under MIT license.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
Allows accessing to FTP files and directories. This provider doesn't cache or save files internally.
|
||||
It's a complete reimplementation and doesn't use CFNetwork deprecated API.
|
||||
*/
|
||||
open class FTPFileProvider: FileProviderBasicRemote, FileProviderOperations, FileProviderReadWrite {
|
||||
open class var type: String { return "FTP" }
|
||||
open let baseURL: URL?
|
||||
open var currentPath: String
|
||||
|
||||
open var dispatch_queue: DispatchQueue
|
||||
open var operation_queue: OperationQueue {
|
||||
willSet {
|
||||
assert(_session == nil, "It's not effective to change dispatch_queue property after session is initialized.")
|
||||
}
|
||||
}
|
||||
|
||||
open weak var delegate: FileProviderDelegate?
|
||||
open var credential: URLCredential? {
|
||||
didSet {
|
||||
sessionDelegate?.credential = self.credential
|
||||
}
|
||||
}
|
||||
open private(set) var cache: URLCache?
|
||||
public var useCache: Bool
|
||||
public var validatingCache: Bool
|
||||
|
||||
/// Determine either FTP session is in passive or active mode.
|
||||
public let passiveMode: Bool
|
||||
|
||||
/// Force to use URLSessionDownloadTask/URLSessionDataTask when possible
|
||||
public var useAppleImplementation = true
|
||||
|
||||
fileprivate var _session: URLSession?
|
||||
internal var sessionDelegate: SessionDelegate?
|
||||
public var session: URLSession {
|
||||
get {
|
||||
if _session == nil {
|
||||
self.sessionDelegate = SessionDelegate(fileProvider: self)
|
||||
let config = URLSessionConfiguration.default
|
||||
config.urlCache = cache
|
||||
config.requestCachePolicy = .returnCacheDataElseLoad
|
||||
_session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDelegate?, delegateQueue: self.operation_queue)
|
||||
_session?.sessionDescription = UUID().uuidString
|
||||
initEmptySessionHandler(_session!.sessionDescription!)
|
||||
}
|
||||
return _session!
|
||||
}
|
||||
|
||||
set {
|
||||
assert(newValue.delegate is SessionDelegate, "session instances should have a SessionDelegate instance as delegate.")
|
||||
_session = newValue
|
||||
if session.sessionDescription?.isEmpty ?? true {
|
||||
_session?.sessionDescription = UUID().uuidString
|
||||
}
|
||||
self.sessionDelegate = newValue.delegate as? SessionDelegate
|
||||
initEmptySessionHandler(_session!.sessionDescription!)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Initializer for FTP provider with given username and password.
|
||||
|
||||
- Note: `passive` value should be set according to server settings and firewall presence.
|
||||
|
||||
- Parameter baseURL: a url with `ftp://hostaddress/` format.
|
||||
- Parameter passive: FTP server data connection, `true` means passive connection (data connection created by client)
|
||||
and `false` means active connection (data connection created by server). Default is `true` (passive mode).
|
||||
- Parameter credential: a `URLCredential` object contains user and password.
|
||||
- Parameter cache: A URLCache to cache downloaded files and contents. (unimplemented for FTP and should be nil)
|
||||
*/
|
||||
public init? (baseURL: URL, passive: Bool = true, credential: URLCredential? = nil, cache: URLCache? = nil) {
|
||||
guard (baseURL.scheme ?? "ftp").lowercased().hasPrefix("ftp") else { return nil }
|
||||
guard baseURL.host != nil else { return nil }
|
||||
var urlComponents = URLComponents(url: baseURL, resolvingAgainstBaseURL: true)!
|
||||
let defaultPort: Int = baseURL.scheme == "ftps" ? 990 : 21
|
||||
urlComponents.port = urlComponents.port ?? defaultPort
|
||||
urlComponents.scheme = urlComponents.scheme ?? "ftp"
|
||||
|
||||
self.baseURL = (urlComponents.url!.path.hasSuffix("/") ? urlComponents.url! : urlComponents.url!.appendingPathComponent("")).absoluteURL
|
||||
self.passiveMode = passive
|
||||
self.currentPath = ""
|
||||
self.useCache = false
|
||||
self.validatingCache = true
|
||||
self.cache = cache
|
||||
self.credential = credential
|
||||
|
||||
#if swift(>=3.1)
|
||||
let queueLabel = "FileProvider.\(Swift.type(of: self).type)"
|
||||
#else
|
||||
let queueLabel = "FileProvider.\(type(of: self).type)"
|
||||
#endif
|
||||
dispatch_queue = DispatchQueue(label: queueLabel, attributes: .concurrent)
|
||||
operation_queue = OperationQueue()
|
||||
operation_queue.name = "\(queueLabel).Operation"
|
||||
}
|
||||
|
||||
public required convenience init?(coder aDecoder: NSCoder) {
|
||||
guard let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL else { return nil }
|
||||
self.init(baseURL: baseURL, passive: aDecoder.decodeBool(forKey: "passiveMode"), credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential)
|
||||
self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? ""
|
||||
self.useCache = aDecoder.decodeBool(forKey: "useCache")
|
||||
self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache")
|
||||
self.useAppleImplementation = aDecoder.decodeBool(forKey: "useAppleImplementation")
|
||||
}
|
||||
|
||||
public func encode(with aCoder: NSCoder) {
|
||||
aCoder.encode(self.baseURL, forKey: "baseURL")
|
||||
aCoder.encode(self.credential, forKey: "credential")
|
||||
aCoder.encode(self.currentPath, forKey: "currentPath")
|
||||
aCoder.encode(self.useCache, forKey: "useCache")
|
||||
aCoder.encode(self.validatingCache, forKey: "validatingCache")
|
||||
aCoder.encode(self.useAppleImplementation, forKey: "useAppleImplementation")
|
||||
aCoder.encode(self.passiveMode, forKey: "passiveMode")
|
||||
}
|
||||
|
||||
public static var supportsSecureCoding: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
open func copy(with zone: NSZone? = nil) -> Any {
|
||||
let copy = FTPFileProvider(baseURL: self.baseURL!, credential: self.credential, cache: self.cache)!
|
||||
copy.currentPath = self.currentPath
|
||||
copy.delegate = self.delegate
|
||||
copy.fileOperationDelegate = self.fileOperationDelegate
|
||||
copy.useCache = self.useCache
|
||||
copy.validatingCache = self.validatingCache
|
||||
copy.useAppleImplementation = self.useAppleImplementation
|
||||
return copy
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let sessionuuid = _session?.sessionDescription {
|
||||
removeSessionHandler(for: sessionuuid)
|
||||
}
|
||||
|
||||
if fileProviderCancelTasksOnInvalidating {
|
||||
_session?.invalidateAndCancel()
|
||||
} else {
|
||||
_session?.finishTasksAndInvalidate()
|
||||
}
|
||||
}
|
||||
|
||||
internal var serverSupportsRFC3659: Bool = true
|
||||
|
||||
open func contentsOfDirectory(path: String, completionHandler: @escaping (([FileObject], Error?) -> Void)) {
|
||||
self.contentsOfDirectory(path: path, rfc3659enabled: serverSupportsRFC3659, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns an Array of `FileObject`s identifying the the directory entries via asynchronous completion handler.
|
||||
|
||||
If the directory contains no entries or an error is occured, this method will return the empty array.
|
||||
|
||||
- Parameter path: path to target directory. If empty, root will be iterated.
|
||||
- Parameter rfc3659enabled: uses MLST command instead of old LIST to get files attributes, default is `true`.
|
||||
- Parameter completionHandler: a closure with result of directory entries or error.
|
||||
`contents`: An array of `FileObject` identifying the the directory entries.
|
||||
`error`: Error returned by system.
|
||||
*/
|
||||
open func contentsOfDirectory(path apath: String, rfc3659enabled: Bool , completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
|
||||
let path = ftpPath(apath)
|
||||
|
||||
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
|
||||
self.ftpLogin(task) { (error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler([], error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
self.ftpList(task, of: self.ftpPath(path), useMLST: rfc3659enabled, completionHandler: { (contents, error) in
|
||||
defer {
|
||||
self.ftpQuit(task)
|
||||
}
|
||||
if let error = error {
|
||||
if ((error as NSError).domain == URLError.errorDomain && (error as NSError).code == URLError.unsupportedURL.rawValue) {
|
||||
self.contentsOfDirectory(path: path, rfc3659enabled: false, completionHandler: completionHandler)
|
||||
return
|
||||
}
|
||||
|
||||
self.dispatch_queue.async {
|
||||
completionHandler([], error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let files: [FileObject] = contents.flatMap {
|
||||
rfc3659enabled ? self.parseMLST($0, in: path) : self.parseUnixList($0, in: path)
|
||||
}
|
||||
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(files, nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
open func attributesOfItem(path: String, completionHandler: @escaping ((FileObject?, Error?) -> Void)) {
|
||||
self.attributesOfItem(path: path, rfc3659enabled: serverSupportsRFC3659, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns a `FileObject` containing the attributes of the item (file, directory, symlink, etc.) at the path in question via asynchronous completion handler.
|
||||
|
||||
If the directory contains no entries or an error is occured, this method will return the empty `FileObject`.
|
||||
|
||||
- Parameter path: path to target directory. If empty, attributes of root will be returned.
|
||||
- Parameter rfc3659enabled: uses MLST command instead of old LIST to get files attributes, default is true.
|
||||
- Parameter completionHandler: a closure with result of directory entries or error.
|
||||
`attributes`: A `FileObject` containing the attributes of the item.
|
||||
`error`: Error returned by system.
|
||||
*/
|
||||
open func attributesOfItem(path apath: String, rfc3659enabled: Bool, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
|
||||
let path = ftpPath(apath)
|
||||
|
||||
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
|
||||
self.ftpLogin(task) { (error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let command = rfc3659enabled ? "MLST \(path)" : "LIST \(path)"
|
||||
self.execute(command: command, on: task, completionHandler: { (response, error) in
|
||||
defer {
|
||||
self.ftpQuit(task)
|
||||
}
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
guard let response = response, response.hasPrefix("250") || (response.hasPrefix("50") && rfc3659enabled) else {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, self.throwError(path, code: URLError.badServerResponse))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if response.hasPrefix("500") {
|
||||
self.serverSupportsRFC3659 = false
|
||||
self.attributesOfItem(path: path, rfc3659enabled: false, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
let lines = response.components(separatedBy: "\n").flatMap { $0.isEmpty ? nil : $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
guard lines.count > 2 else {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, self.throwError(path, code: URLError.badServerResponse))
|
||||
}
|
||||
return
|
||||
}
|
||||
let file = rfc3659enabled ? self.parseMLST(lines[1], in: path) : self.parseUnixList(lines[1], in: path)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(file, nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) {
|
||||
dispatch_queue.async {
|
||||
completionHandler(-1, 0)
|
||||
}
|
||||
}
|
||||
|
||||
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? {
|
||||
let progress = Progress(parent: nil, userInfo: nil)
|
||||
if recursive {
|
||||
return self.recursiveList(path: path, useMLST: true, foundItemsHandler: { items in
|
||||
if let foundItemHandler = foundItemHandler {
|
||||
for item in items where query.evaluate(with: item.mapPredicate()) {
|
||||
foundItemHandler(item)
|
||||
}
|
||||
progress.totalUnitCount = Int64(items.count)
|
||||
}
|
||||
}, completionHandler: {files, error in
|
||||
if let error = error {
|
||||
completionHandler([], error)
|
||||
return
|
||||
}
|
||||
|
||||
let foundFiles = files.filter { query.evaluate(with: $0.mapPredicate()) }
|
||||
completionHandler(foundFiles, nil)
|
||||
})
|
||||
} else {
|
||||
self.contentsOfDirectory(path: path, completionHandler: { (items, error) in
|
||||
if let error = error {
|
||||
completionHandler([], error)
|
||||
return
|
||||
}
|
||||
|
||||
var result = [FileObject]()
|
||||
for item in items where query.evaluate(with: item.mapPredicate()) {
|
||||
foundItemHandler?(item)
|
||||
result.append(item)
|
||||
}
|
||||
completionHandler(result, nil)
|
||||
})
|
||||
}
|
||||
|
||||
return progress
|
||||
}
|
||||
|
||||
open func url(of path: String?) -> URL {
|
||||
let path = (path ?? self.currentPath).trimmingCharacters(in: CharacterSet(charactersIn: "/ ")).addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? (path ?? self.currentPath)
|
||||
|
||||
var baseUrlComponent = URLComponents(url: self.baseURL!, resolvingAgainstBaseURL: true)
|
||||
baseUrlComponent?.user = credential?.user
|
||||
baseUrlComponent?.password = credential?.password
|
||||
return URL(string: path, relativeTo: baseUrlComponent?.url ?? baseURL) ?? baseUrlComponent?.url ?? baseURL!
|
||||
}
|
||||
|
||||
open func relativePathOf(url: URL) -> String {
|
||||
// check if url derieved from current base url
|
||||
let relativePath = url.relativePath
|
||||
if !relativePath.isEmpty, url.baseURL == self.baseURL {
|
||||
return (relativePath.removingPercentEncoding ?? relativePath).replacingOccurrences(of: "/", with: "", options: .anchored)
|
||||
}
|
||||
|
||||
if !relativePath.isEmpty, self.baseURL == self.url(of: "/") {
|
||||
return (relativePath.removingPercentEncoding ?? relativePath).replacingOccurrences(of: "/", with: "", options: .anchored)
|
||||
}
|
||||
|
||||
return relativePath.replacingOccurrences(of: "/", with: "", options: .anchored)
|
||||
}
|
||||
|
||||
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
|
||||
self.attributesOfItem(path: "/") { (file, error) in
|
||||
completionHandler(file != nil)
|
||||
}
|
||||
}
|
||||
|
||||
open weak var fileOperationDelegate: FileOperationDelegate?
|
||||
|
||||
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let path = (atPath as NSString).appendingPathComponent(folderName) + "/"
|
||||
return doOperation(.create(path: path), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
return doOperation(.copy(source: path, destination: toPath), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
return doOperation(.remove(path: path), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
// check file is not a folder
|
||||
guard (try? localFile.resourceValues(forKeys: [.fileResourceTypeKey]))?.fileResourceType ?? .unknown == .regular else {
|
||||
dispatch_queue.async {
|
||||
completionHandler?(self.throwError(localFile.path, code: URLError.fileIsDirectory))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
let operation = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let progress = Progress(totalUnitCount: 0)
|
||||
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
||||
progress.kind = .file
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
||||
|
||||
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
|
||||
self.ftpLogin(task) { (error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
self.ftpStore(task, filePath: self.ftpPath(toPath), fromData: nil, fromFile: localFile, onTask: { task in
|
||||
weak var weakTask = task
|
||||
progress.cancellationHandler = {
|
||||
weakTask?.cancel()
|
||||
}
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
}, onProgress: { bytesSent, totalSent, expectedBytes in
|
||||
progress.completedUnitCount = totalSent
|
||||
self.delegateNotify(operation, progress: progress.fractionCompleted)
|
||||
}, completionHandler: { (error) in
|
||||
if error != nil {
|
||||
progress.cancel()
|
||||
}
|
||||
self.ftpQuit(task)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return progress
|
||||
}
|
||||
|
||||
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let operation = FileOperationType.copy(source: path, destination: destURL.absoluteString)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
var progress = Progress(totalUnitCount: 0)
|
||||
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
||||
progress.kind = .file
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
||||
|
||||
if self.useAppleImplementation {
|
||||
self.attributesOfItem(path: path, completionHandler: { (file, error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if file?.isDirectory ?? false {
|
||||
self.dispatch_queue.async {
|
||||
let error = self.throwError(path, code: URLError.fileIsDirectory)
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
progress.totalUnitCount = file?.size ?? 0
|
||||
|
||||
let task = self.session.downloadTask(with: self.url(of: path))
|
||||
completionHandlersForTasks[self.session.sessionDescription!]?[task.taskIdentifier] = completionHandler
|
||||
downloadCompletionHandlersForTasks[self.session.sessionDescription!]?[task.taskIdentifier] = { tempURL in
|
||||
do {
|
||||
try FileManager.default.moveItem(at: tempURL, to: destURL)
|
||||
completionHandler?(nil)
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
}
|
||||
}
|
||||
task.taskDescription = operation.json
|
||||
task.addObserver(self.sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: &progress)
|
||||
task.addObserver(self.sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: &progress)
|
||||
progress.cancellationHandler = { [weak task] in
|
||||
task?.cancel()
|
||||
}
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
task.resume()
|
||||
})
|
||||
} else {
|
||||
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
|
||||
self.ftpLogin(task) { (error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
self.ftpRetrieveFile(task, filePath: self.ftpPath(path), onTask: { task in
|
||||
weak var weakTask = task
|
||||
progress.cancellationHandler = {
|
||||
weakTask?.cancel()
|
||||
}
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
}, onProgress: { recevied, totalReceived, totalSize in
|
||||
progress.totalUnitCount = totalSize
|
||||
progress.completedUnitCount = totalReceived
|
||||
self.delegateNotify(operation, progress: progress.fractionCompleted)
|
||||
}) { (tmpurl, error) in
|
||||
if let error = error {
|
||||
progress.cancel()
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let tmpurl = tmpurl {
|
||||
try? FileManager.default.moveItem(at: tmpurl, to: destURL)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(nil)
|
||||
self.delegateNotify(operation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return progress
|
||||
}
|
||||
|
||||
open func contents(path: String, completionHandler: @escaping ((Data?, Error?) -> Void)) -> Progress? {
|
||||
let operation = FileOperationType.fetch(path: path)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if self.useAppleImplementation {
|
||||
var progress = Progress(totalUnitCount: 0)
|
||||
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
||||
progress.kind = .file
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
||||
|
||||
let task = session.downloadTask(with: url(of: path))
|
||||
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
|
||||
if error != nil {
|
||||
progress.cancel()
|
||||
}
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { tempURL in
|
||||
do {
|
||||
let data = try Data(contentsOf: tempURL)
|
||||
completionHandler(data, nil)
|
||||
} catch let e {
|
||||
completionHandler(nil, e)
|
||||
}
|
||||
}
|
||||
task.taskDescription = operation.json
|
||||
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: &progress)
|
||||
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: &progress)
|
||||
progress.cancellationHandler = { [weak task] in
|
||||
task?.cancel()
|
||||
}
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
task.resume()
|
||||
return progress
|
||||
} else {
|
||||
return self.contents(path: path, offset: 0, length: -1, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
|
||||
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
|
||||
let operation = FileOperationType.fetch(path: path)
|
||||
if length == 0 || offset < 0 {
|
||||
dispatch_queue.async {
|
||||
completionHandler(Data(), nil)
|
||||
self.delegateNotify(operation)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
let progress = Progress(totalUnitCount: 0)
|
||||
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
||||
progress.kind = .file
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
||||
|
||||
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
|
||||
self.ftpLogin(task) { (error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
self.ftpRetrieveData(task, filePath: self.ftpPath(path), from: offset, length: length, onTask: { task in
|
||||
weak var weakTask = task
|
||||
progress.cancellationHandler = {
|
||||
weakTask?.cancel()
|
||||
}
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
}, onProgress: { recevied, totalReceived, totalSize in
|
||||
progress.totalUnitCount = totalSize
|
||||
progress.completedUnitCount = totalReceived
|
||||
self.delegateNotify(operation, progress: progress.fractionCompleted)
|
||||
}) { (data, error) in
|
||||
if let error = error {
|
||||
progress.cancel()
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let data = data {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(data, nil)
|
||||
self.delegateNotify(operation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return progress
|
||||
}
|
||||
|
||||
open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let operation = FileOperationType.modify(path: path)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let progress = Progress(totalUnitCount: Int64(data?.count ?? 0))
|
||||
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
||||
progress.kind = .file
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
||||
|
||||
let task = session.fpstreamTask(withHostName: baseURL!.host!, port: baseURL!.port!)
|
||||
self.ftpLogin(task) { (error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let storeHandler = {
|
||||
self.ftpStore(task, filePath: self.ftpPath(path), fromData: data ?? Data(), fromFile: nil, onTask: { task in
|
||||
weak var weakTask = task
|
||||
progress.cancellationHandler = {
|
||||
weakTask?.cancel()
|
||||
}
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
}, onProgress: { bytesSent, totalSent, expectedBytes in
|
||||
progress.completedUnitCount = totalSent
|
||||
self.delegateNotify(operation, progress: progress.fractionCompleted)
|
||||
}, completionHandler: { (error) in
|
||||
if error != nil {
|
||||
progress.cancel()
|
||||
}
|
||||
self.ftpQuit(task)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if overwrite {
|
||||
storeHandler()
|
||||
} else {
|
||||
self.attributesOfItem(path: path, completionHandler: { (file, erroe) in
|
||||
if file == nil {
|
||||
storeHandler()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return progress
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a symbolic link at the specified path that points to an item at the given path.
|
||||
This method does not traverse symbolic links contained in destination path, making it possible
|
||||
to create symbolic links to locations that do not yet exist.
|
||||
Also, if the final path component is a symbolic link, that link is not followed.
|
||||
|
||||
- Note: Many servers does't support this functionality.
|
||||
|
||||
- Parameters:
|
||||
- symbolicLink: The file path at which to create the new symbolic link. The last component of the path issued as the name of the link.
|
||||
- withDestinationPath: The path that contains the item to be pointed to by the link. In other words, this is the destination of the link.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
*/
|
||||
open func create(symbolicLink path: String, withDestinationPath destPath: String, completionHandler: SimpleCompletionHandler) {
|
||||
let operation = FileOperationType.link(link: path, target: destPath)
|
||||
_=self.doOperation(operation, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
|
||||
extension FTPFileProvider {
|
||||
fileprivate func doOperation(_ operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
let sourcePath = operation.source
|
||||
let destPath = operation.destination
|
||||
|
||||
let command: String
|
||||
switch operation {
|
||||
case .create:
|
||||
command = "MKD \(ftpPath(sourcePath))"
|
||||
case .copy:
|
||||
command = "SITE CPFR \(ftpPath(sourcePath))\r\nSITE CPTO \(ftpPath(destPath!))"
|
||||
case .move:
|
||||
command = "RNFR \(ftpPath(sourcePath))\r\nRNTO \(ftpPath(destPath!))"
|
||||
case .remove:
|
||||
command = "DELE \(ftpPath(sourcePath))"
|
||||
case .link:
|
||||
command = "SITE SYMLINK \(ftpPath(sourcePath)) \(ftpPath(destPath!))"
|
||||
default: // modify, fetch
|
||||
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!)
|
||||
self.ftpLogin(task) { (error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
self.execute(command: command, on: task, completionHandler: { (response, error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
guard let response = response else {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: self.throwError(sourcePath, code: URLError.badServerResponse))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let codes: [Int] = response.components(separatedBy: .newlines).flatMap({ $0.isEmpty ? nil : $0})
|
||||
.flatMap {
|
||||
let code = $0.components(separatedBy: .whitespaces).flatMap({ $0.isEmpty ? nil : $0}).first
|
||||
return code != nil ? Int(code!) : nil
|
||||
}
|
||||
|
||||
if codes.filter({ (450..<560).contains($0) }).count > 0 {
|
||||
let errorCode: URLError.Code
|
||||
switch operation {
|
||||
case .create:
|
||||
errorCode = URLError.cannotCreateFile
|
||||
case .modify:
|
||||
errorCode = URLError.cannotWriteToFile
|
||||
case .copy:
|
||||
self.fallbackCopy(operation, progress: progress, completionHandler: completionHandler)
|
||||
return
|
||||
case .move:
|
||||
errorCode = URLError.cannotMoveFile
|
||||
case .remove:
|
||||
self.fallbackRemove(operation, progress: progress, on: task, completionHandler: completionHandler)
|
||||
return
|
||||
case .link:
|
||||
errorCode = URLError.cannotWriteToFile
|
||||
default:
|
||||
errorCode = URLError.cannotOpenFile
|
||||
}
|
||||
let error = self.throwError(sourcePath, code: errorCode)
|
||||
progress.cancel()
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
}
|
||||
self.delegateNotify(operation, error: error)
|
||||
return
|
||||
}
|
||||
|
||||
progress.completedUnitCount = progress.totalUnitCount
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(nil)
|
||||
}
|
||||
self.delegateNotify(operation)
|
||||
})
|
||||
}
|
||||
|
||||
progress.cancellationHandler = { [weak task] in
|
||||
task?.cancel()
|
||||
}
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
return progress
|
||||
}
|
||||
|
||||
private func fallbackCopy(_ operation: FileOperationType, progress: Progress, completionHandler: SimpleCompletionHandler) {
|
||||
let sourcePath = operation.source
|
||||
guard let destPath = operation.destination else { return }
|
||||
|
||||
let localURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString).appendingPathExtension("tmp")
|
||||
|
||||
progress.becomeCurrent(withPendingUnitCount: 1)
|
||||
_ = self.copyItem(path: sourcePath, toLocalURL: localURL) { (error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
progress.becomeCurrent(withPendingUnitCount: 1)
|
||||
_ = self.copyItem(localFile: localURL, to: destPath) { error in
|
||||
completionHandler?(nil)
|
||||
self.delegateNotify(operation)
|
||||
}
|
||||
progress.resignCurrent()
|
||||
}
|
||||
progress.resignCurrent()
|
||||
return
|
||||
}
|
||||
|
||||
private func fallbackRemove(_ operation: FileOperationType, progress: Progress, on task: FileProviderStreamTask, completionHandler: SimpleCompletionHandler) {
|
||||
let sourcePath = operation.source
|
||||
|
||||
self.execute(command: "SITE RMDIR \(ftpPath(sourcePath))", on: task) { (response, error) in
|
||||
if let error = error {
|
||||
progress.cancel()
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
}
|
||||
self.delegateNotify(operation, error: error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let response = response else {
|
||||
progress.cancel()
|
||||
let error = self.throwError(sourcePath, code: URLError.badServerResponse)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
}
|
||||
self.delegateNotify(operation, error: error)
|
||||
return
|
||||
}
|
||||
|
||||
if response.hasPrefix("50") {
|
||||
self.fallbackRecursiveRemove(operation, progress: progress, on: task, completionHandler: completionHandler)
|
||||
return
|
||||
}
|
||||
|
||||
var error: Error?
|
||||
if !response.hasPrefix("2") {
|
||||
error = self.throwError(sourcePath, code: URLError.cannotRemoveFile)
|
||||
}
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
}
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
}
|
||||
|
||||
private func fallbackRecursiveRemove(_ operation: FileOperationType, progress: Progress, on task: FileProviderStreamTask, completionHandler: SimpleCompletionHandler) {
|
||||
let sourcePath = operation.source
|
||||
|
||||
_ = self.recursiveList(path: sourcePath, useMLST: true, completionHandler: { (contents, error) in
|
||||
if let error = error {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
progress.becomeCurrent(withPendingUnitCount: 1)
|
||||
let recursiveProgress = Progress(parent: progress, userInfo: nil)
|
||||
recursiveProgress.totalUnitCount = Int64(contents.count)
|
||||
let sortedContents = contents.sorted(by: {
|
||||
$0.path.localizedStandardCompare($1.path) == .orderedDescending
|
||||
})
|
||||
progress.resignCurrent()
|
||||
var command = ""
|
||||
for file in sortedContents {
|
||||
command += (file.isDirectory ? "RMD \(self.ftpPath(file.path))" : "DELE \(self.ftpPath(file.path))") + "\r\n"
|
||||
}
|
||||
command += "RMD \(self.ftpPath(sourcePath))"
|
||||
|
||||
self.execute(command: command, on: task, completionHandler: { (response, error) in
|
||||
recursiveProgress.completedUnitCount += 1
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
// TODO: Digest response
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
extension FTPFileProvider: FileProvider { }
|
||||
@@ -0,0 +1,955 @@
|
||||
//
|
||||
// FTPHelper.swift
|
||||
// FileProvider
|
||||
//
|
||||
// Created by Amir Abbas Mousavian.
|
||||
// Copyright © 2017 Mousavian. Distributed under MIT license.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
internal extension FTPFileProvider {
|
||||
func readDataUntilEOF(of task: FileProviderStreamTask, minLength: Int, receivedData: Data? = nil, timeout: TimeInterval, completionHandler: @escaping (_ data: Data?, _ errror:Error?) -> Void) {
|
||||
task.readData(ofMinLength: minLength, maxLength: 65535, timeout: timeout) { (data, eof, error) in
|
||||
if let error = error {
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
var receivedData = receivedData
|
||||
if let data = data {
|
||||
if receivedData != nil {
|
||||
receivedData!.append(data)
|
||||
} else {
|
||||
receivedData = data
|
||||
}
|
||||
}
|
||||
|
||||
if eof {
|
||||
completionHandler(receivedData, nil)
|
||||
} else {
|
||||
self.readDataUntilEOF(of: task, minLength: 0, receivedData: receivedData, timeout: timeout, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func execute(command: String, on task: FileProviderStreamTask, minLength: Int = 4, afterSend: ((_ error: Error?) -> Void)? = nil, completionHandler: @escaping (_ response: String?, _ error: Error?) -> Void) {
|
||||
let timeout = session.configuration.timeoutIntervalForRequest
|
||||
let terminalcommand = command + "\r\n"
|
||||
task.write(terminalcommand.data(using: .utf8)!, timeout: timeout) { (error) in
|
||||
if let error = error {
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
afterSend?(error)
|
||||
|
||||
if task.state == .suspended {
|
||||
task.resume()
|
||||
}
|
||||
|
||||
task.readData(ofMinLength: minLength, maxLength: 1024, timeout: timeout) { (data, eof, error) in
|
||||
if let error = error {
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
if let data = data, let response = String(data: data, encoding: .utf8) {
|
||||
completionHandler(response.trimmingCharacters(in: .whitespacesAndNewlines), nil)
|
||||
} else {
|
||||
completionHandler(nil, self.throwError("", code: URLError.cannotParseResponse))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ftpLogin(_ task: FileProviderStreamTask, completionHandler: @escaping (_ error: Error?) -> Void) {
|
||||
let timeout = session.configuration.timeoutIntervalForRequest
|
||||
if task.state == .suspended {
|
||||
task.resume()
|
||||
}
|
||||
|
||||
var isSecure = false
|
||||
// Implicit FTP Connection
|
||||
if self.baseURL?.port == 990 || self.baseURL?.scheme == "ftps" {
|
||||
task.startSecureConnection()
|
||||
isSecure = true
|
||||
}
|
||||
|
||||
let credential = self.credential
|
||||
|
||||
task.readData(ofMinLength: 4, maxLength: 2048, timeout: timeout) { (data, eof, error) in
|
||||
if let error = error {
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data, let response = String(data: data, encoding: .utf8) else {
|
||||
completionHandler(self.throwError("", code: URLError.cannotParseResponse))
|
||||
return
|
||||
}
|
||||
|
||||
guard response.hasPrefix("22") else {
|
||||
let error = FileProviderFTPError(message: response)
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
|
||||
let loginHandle: () -> Void = {
|
||||
self.execute(command: "USER \(credential?.user ?? "anonymous")", on: task) { (response, error) in
|
||||
if let error = error {
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let response = response else {
|
||||
completionHandler(self.throwError("", code: URLError.badServerResponse))
|
||||
return
|
||||
}
|
||||
|
||||
// successfully logged in
|
||||
if response.hasPrefix("23") {
|
||||
completionHandler(nil)
|
||||
return
|
||||
}
|
||||
|
||||
// needs password
|
||||
if FileProviderFTPError(message: response).code == 331 {
|
||||
self.execute(command: "PASS \(credential?.password ?? "fileprovider@")", on: task) { (response, error) in
|
||||
if response?.hasPrefix("23") ?? false {
|
||||
completionHandler(nil)
|
||||
} else {
|
||||
completionHandler(self.throwError("", code: URLError.userAuthenticationRequired))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let error = FileProviderFTPError(message: response)
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !isSecure && self.baseURL?.scheme == "ftpes" {
|
||||
// Explicit FTP Connection, by upgrading connection to FTP/SSL
|
||||
self.execute(command: "AUTH TLS", on: task, minLength: 0, completionHandler: { (response, error) in
|
||||
if let error = error {
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
|
||||
if let response = response, response.hasPrefix("23") {
|
||||
task.startSecureConnection()
|
||||
isSecure = true
|
||||
self.execute(command: "PBSZ 0\r\nPROT P", on: task, completionHandler: { (response, error) in
|
||||
if let error = error {
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
|
||||
loginHandle()
|
||||
})
|
||||
}
|
||||
})
|
||||
} else if isSecure {
|
||||
self.execute(command: "PBSZ 0\r\nPROT P", on: task, completionHandler: { (response, error) in
|
||||
if let error = error {
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
|
||||
loginHandle()
|
||||
})
|
||||
} else {
|
||||
loginHandle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ftpCwd(_ task: FileProviderStreamTask, to path: String, completionHandler: @escaping (_ error: Error?) -> Void) {
|
||||
self.execute(command: "CWD \(path)", on: task) { (response, error) in
|
||||
if let error = error {
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let response = response else {
|
||||
completionHandler(self.throwError(path, code: URLError.badServerResponse))
|
||||
return
|
||||
}
|
||||
|
||||
// successfully logged in
|
||||
if response.hasPrefix("25") {
|
||||
completionHandler(nil)
|
||||
}
|
||||
// not logged in
|
||||
else if response.hasPrefix("55") {
|
||||
let error = FileProviderFTPError(message: response)
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ftpPassive(_ task: FileProviderStreamTask, completionHandler: @escaping (_ dataTask: FileProviderStreamTask?, _ error: Error?) -> Void) {
|
||||
func trimmedNumber(_ s : String) -> String {
|
||||
let characterSet = Set("+*#0123456789".characters)
|
||||
return String(s.characters.lazy.filter(characterSet.contains))
|
||||
}
|
||||
|
||||
self.execute(command: "PASV", on: task) { (response, error) in
|
||||
if let error = error {
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let response = response, let destString = response.components(separatedBy: " ").flatMap({ $0 }).last.flatMap({ String($0) }) else {
|
||||
completionHandler(nil, self.throwError("", code: URLError.badServerResponse))
|
||||
return
|
||||
}
|
||||
|
||||
let destArray = destString.components(separatedBy: ",").flatMap({ UInt32(trimmedNumber($0)) })
|
||||
guard destArray.count == 6 else {
|
||||
completionHandler(nil, self.throwError("", code: URLError.badServerResponse))
|
||||
return
|
||||
}
|
||||
|
||||
// first 4 elements are ip, 2 next are port, as byte
|
||||
var host = destArray.prefix(4).flatMap({ String($0) }).joined(separator: ".")
|
||||
let port = Int(destArray[4] << 8 + destArray[5])
|
||||
// IPv6 workaround
|
||||
if host == "127.555.555.555" {
|
||||
host = self.baseURL!.host!
|
||||
}
|
||||
|
||||
let passiveTask = self.session.fpstreamTask(withHostName: host, port: port)
|
||||
passiveTask.resume()
|
||||
if self.baseURL?.scheme == "ftps" || self.baseURL?.scheme == "ftpes" || self.baseURL?.port == 990 {
|
||||
passiveTask.startSecureConnection()
|
||||
}
|
||||
completionHandler(passiveTask, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func ftpActive(_ task: FileProviderStreamTask, completionHandler: @escaping (_ dataTask: FileProviderStreamTask?, _ error: Error?) -> Void) {
|
||||
let service = NetService(domain: "", type: "_tcp.", name: "", port: 0)
|
||||
service.publish(options: .listenForConnections)
|
||||
let startTime = Date()
|
||||
while service.port < 1 && startTime.timeIntervalSinceNow > -self.session.configuration.timeoutIntervalForRequest {
|
||||
usleep(100_000)
|
||||
}
|
||||
let activeTask = self.session.fpstreamTask(withNetService: service)
|
||||
activeTask.resume()
|
||||
if self.baseURL?.scheme == "ftps" || self.baseURL?.port == 990 {
|
||||
activeTask.startSecureConnection()
|
||||
}
|
||||
|
||||
self.execute(command: "PORT \(service.port)", on: task) { (response, error) in
|
||||
if let error = error {
|
||||
activeTask.cancel()
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let response = response else {
|
||||
activeTask.cancel()
|
||||
completionHandler(nil, self.throwError("", code: URLError.badServerResponse))
|
||||
return
|
||||
}
|
||||
|
||||
guard !response.hasPrefix("5") else {
|
||||
activeTask.cancel()
|
||||
completionHandler(nil, self.throwError("", code: URLError.cannotConnectToHost))
|
||||
return
|
||||
}
|
||||
|
||||
completionHandler(activeTask, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func ftpDataConnect(_ task: FileProviderStreamTask, completionHandler: @escaping (_ dataTask: FileProviderStreamTask?, _ error: Error?) -> Void) {
|
||||
if self.passiveMode {
|
||||
self.ftpPassive(task, completionHandler: completionHandler)
|
||||
} else {
|
||||
dispatch_queue.async {
|
||||
self.ftpActive(task, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ftpRest(_ task: FileProviderStreamTask, startPosition: Int64, completionHandler: @escaping (_ error: Error?) -> Void) {
|
||||
self.execute(command: "REST \(startPosition)", on: task) { (response, error) in
|
||||
if let error = error {
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
|
||||
// Successful
|
||||
guard let response = response else {
|
||||
completionHandler(self.throwError("", code: URLError.badServerResponse))
|
||||
return
|
||||
}
|
||||
|
||||
if response.hasPrefix("35") {
|
||||
completionHandler(nil)
|
||||
} else {
|
||||
let error = FileProviderFTPError(message: response, path: "")
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ftpList(_ task: FileProviderStreamTask, of path: String, useMLST: Bool, completionHandler: @escaping (_ contents: [String], _ error: Error?) -> Void) {
|
||||
self.ftpDataConnect(task) { (dataTask, error) in
|
||||
if let error = error {
|
||||
completionHandler([], error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let dataTask = dataTask else {
|
||||
completionHandler([], self.throwError(path, code: URLError.badServerResponse))
|
||||
return
|
||||
}
|
||||
|
||||
var success = false
|
||||
let command = useMLST ? "MLSD \(path)" : "LIST \(path)"
|
||||
self.execute(command: command, on: task, minLength: 20, afterSend: { error in
|
||||
// starting passive task
|
||||
let timeout = self.session.configuration.timeoutIntervalForRequest
|
||||
|
||||
DispatchQueue.global().async {
|
||||
var finalData = Data()
|
||||
var eof = false
|
||||
var error: Error?
|
||||
while !eof {
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
dataTask.readData(ofMinLength: 1, maxLength: 65535, timeout: timeout, completionHandler: { (data, seof, serror) in
|
||||
if let data = data {
|
||||
finalData.append(data)
|
||||
}
|
||||
eof = seof
|
||||
error = serror
|
||||
group.leave()
|
||||
})
|
||||
let waitResult = group.wait(timeout: .now() + timeout)
|
||||
|
||||
if let error = error {
|
||||
if !((error as NSError).domain == URLError.errorDomain && (error as NSError).code == URLError.cancelled.rawValue) {
|
||||
completionHandler([], error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if waitResult == .timedOut {
|
||||
completionHandler([], self.throwError(path, code: URLError.timedOut))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
guard let response = String(data: finalData, encoding: .utf8) else {
|
||||
completionHandler([], self.throwError(path, code: URLError.badServerResponse))
|
||||
return
|
||||
}
|
||||
|
||||
let contents: [String] = response.components(separatedBy: "\n").flatMap({ $0.trimmingCharacters(in: .whitespacesAndNewlines) })
|
||||
success = true
|
||||
completionHandler(contents, nil)
|
||||
return
|
||||
}
|
||||
}) { (response, error) in
|
||||
if let error = error {
|
||||
completionHandler([], error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let response = response else {
|
||||
completionHandler([], self.throwError(path, code: URLError.cannotParseResponse))
|
||||
return
|
||||
}
|
||||
|
||||
if response.hasPrefix("500") && useMLST {
|
||||
dataTask.cancel()
|
||||
self.serverSupportsRFC3659 = false
|
||||
completionHandler([], self.throwError(path, code: URLError.unsupportedURL))
|
||||
return
|
||||
}
|
||||
|
||||
if !success && !(response.hasPrefix("25") || response.hasPrefix("15")) {
|
||||
let error = FileProviderFTPError(message: response, path: path)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler([], error)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func recursiveList(path: String, useMLST: Bool, foundItemsHandler: ((_ contents: [FileObject]) -> Void)? = nil, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) -> Progress? {
|
||||
let progress = Progress(totalUnitCount: 0)
|
||||
let queue = DispatchQueue(label: "\(self.type).recursiveList")
|
||||
queue.async {
|
||||
let group = DispatchGroup()
|
||||
var result = [FileObject]()
|
||||
var success = true
|
||||
group.enter()
|
||||
self.contentsOfDirectory(path: path, completionHandler: { (files, error) in
|
||||
success = success && (error == nil)
|
||||
if let error = error {
|
||||
completionHandler([], error)
|
||||
group.leave()
|
||||
return
|
||||
}
|
||||
|
||||
result.append(contentsOf: files)
|
||||
progress.completedUnitCount = Int64(files.count)
|
||||
foundItemsHandler?(files)
|
||||
|
||||
let directories: [FileObject] = files.filter { $0.isDirectory }
|
||||
progress.becomeCurrent(withPendingUnitCount: Int64(directories.count))
|
||||
for dir in directories {
|
||||
group.enter()
|
||||
_=self.recursiveList(path: dir.path, useMLST: useMLST, foundItemsHandler: foundItemsHandler, completionHandler: { (contents, error) in
|
||||
success = success && (error == nil)
|
||||
if let error = error {
|
||||
completionHandler([], error)
|
||||
group.leave()
|
||||
return
|
||||
}
|
||||
|
||||
foundItemsHandler?(files)
|
||||
result.append(contentsOf: contents)
|
||||
|
||||
group.leave()
|
||||
})
|
||||
}
|
||||
progress.resignCurrent()
|
||||
group.leave()
|
||||
})
|
||||
group.wait()
|
||||
|
||||
if success {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(result, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
return progress
|
||||
}
|
||||
|
||||
func ftpRetrieveData(_ task: FileProviderStreamTask, filePath: String, from position: Int64 = 0, length: Int = -1, onTask: ((_ task: FileProviderStreamTask) -> Void)?, onProgress: ((_ bytesReceived: Int64, _ totalReceived: Int64, _ expectedBytes: Int64) -> Void)?, completionHandler: @escaping (_ data: Data?, _ error: Error?) -> Void) {
|
||||
|
||||
// Check cache
|
||||
if useCache, let url = URL(string: filePath.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? filePath, relativeTo: self.baseURL!)?.absoluteURL, let cachedResponse = self.cache?.cachedResponse(for: URLRequest(url: url)), cachedResponse.data.count > 0 {
|
||||
dispatch_queue.async {
|
||||
completionHandler(cachedResponse.data, nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
self.attributesOfItem(path: filePath) { (file, error) in
|
||||
let totalSize = file?.size ?? -1
|
||||
// Retreive data from server
|
||||
self.ftpDataConnect(task) { (dataTask, error) in
|
||||
if let error = error {
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let dataTask = dataTask else {
|
||||
completionHandler(nil, self.throwError(filePath, code: URLError.badServerResponse))
|
||||
return
|
||||
}
|
||||
|
||||
// Send retreive command
|
||||
let len = 19 /* TYPE response */ + 65 + String(position).characters.count /* REST Response */ + 53 + filePath.characters.count + String(totalSize).characters.count /* RETR open response */ + 26 /* RETR Transfer complete message. */
|
||||
self.execute(command: "TYPE I" + "\r\n" + "REST \(position)" + "\r\n" + "RETR \(filePath)", on: task, minLength: len, afterSend: { error in
|
||||
// starting passive task
|
||||
onTask?(dataTask)
|
||||
|
||||
let timeout = self.session.configuration.timeoutIntervalForRequest
|
||||
DispatchQueue.global().async {
|
||||
var finalData = Data()
|
||||
var eof = false
|
||||
var error: Error?
|
||||
while !eof {
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
dataTask.readData(ofMinLength: 0, maxLength: 65535, timeout: timeout, completionHandler: { (data, seof, serror) in
|
||||
if let data = data {
|
||||
finalData.append(data)
|
||||
onProgress?(Int64(data.count), Int64(finalData.count), totalSize)
|
||||
}
|
||||
eof = seof || (length > 0 && finalData.count >= length)
|
||||
if length > 0 && finalData.count > length {
|
||||
finalData.count = length
|
||||
}
|
||||
error = serror
|
||||
group.leave()
|
||||
})
|
||||
let waitResult = group.wait(timeout: .now() + timeout)
|
||||
|
||||
if let error = error {
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
if waitResult == .timedOut {
|
||||
completionHandler(nil, self.throwError(filePath, code: URLError.timedOut))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if let url = URL(string: filePath.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? filePath, relativeTo: self.baseURL!)?.absoluteURL {
|
||||
let urlresponse = URLResponse(url: url, mimeType: nil, expectedContentLength: finalData.count, textEncodingName: nil)
|
||||
let cachedResponse = CachedURLResponse(response: urlresponse, data: finalData)
|
||||
let request = URLRequest(url: url)
|
||||
self.cache?.storeCachedResponse(cachedResponse, for: request)
|
||||
}
|
||||
|
||||
completionHandler(finalData, nil)
|
||||
return
|
||||
}
|
||||
}) { (response, error) in
|
||||
if let error = error {
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let response = response else {
|
||||
completionHandler(nil, self.throwError(filePath, code: URLError.cannotParseResponse))
|
||||
return
|
||||
}
|
||||
|
||||
if !(response.hasPrefix("1") || !response.hasPrefix("2")) {
|
||||
let error = FileProviderFTPError(message: response)
|
||||
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ftpRetrieveFile(_ task: FileProviderStreamTask, filePath: String, from position: Int64 = 0, length: Int = -1, onTask: ((_ task: FileProviderStreamTask) -> Void)?, onProgress: ((_ bytesReceived: Int64, _ totalReceived: Int64, _ expectedBytes: Int64) -> Void)?, completionHandler: @escaping (_ file: URL?, _ error: Error?) -> Void) {
|
||||
let tempURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString).appendingPathExtension("tmp")
|
||||
|
||||
// Check cache
|
||||
if useCache, let url = URL(string: filePath.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? filePath, relativeTo: self.baseURL!)?.absoluteURL, let cachedResponse = self.cache?.cachedResponse(for: URLRequest(url: url)), cachedResponse.data.count > 0 {
|
||||
dispatch_queue.async {
|
||||
do {
|
||||
try cachedResponse.data.write(to: tempURL)
|
||||
completionHandler(tempURL, nil)
|
||||
} catch {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
try? FileManager.default.removeItem(at: tempURL)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
self.attributesOfItem(path: filePath) { (file, error) in
|
||||
let totalSize = file?.size ?? -1
|
||||
// Retreive data from server
|
||||
self.ftpDataConnect(task) { (dataTask, error) in
|
||||
if let error = error {
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let dataTask = dataTask else {
|
||||
completionHandler(nil, self.throwError(filePath, code: URLError.badServerResponse))
|
||||
return
|
||||
}
|
||||
|
||||
// Send retreive command
|
||||
let len = 19 /* TYPE response */ + 65 + String(position).characters.count /* REST Response */ + 53 + filePath.characters.count + String(totalSize).characters.count /* RETR open response */ + 26 /* RETR Transfer complete message. */
|
||||
self.execute(command: "TYPE I" + "\r\n" + "REST \(position)" + "\r\n" + "RETR \(filePath)", on: task, minLength: len, afterSend: { error in
|
||||
// starting passive task
|
||||
onTask?(dataTask)
|
||||
|
||||
let timeout = self.session.configuration.timeoutIntervalForRequest
|
||||
DispatchQueue.global().async {
|
||||
var finalData = Data()
|
||||
var eof = false
|
||||
var error: Error?
|
||||
while !eof {
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
dataTask.readData(ofMinLength: 0, maxLength: 65535, timeout: timeout, completionHandler: { (data, seof, serror) in
|
||||
if let data = data {
|
||||
finalData.append(data)
|
||||
onProgress?(Int64(data.count), Int64(finalData.count), totalSize)
|
||||
}
|
||||
eof = seof || (length > 0 && finalData.count >= length)
|
||||
if length > 0 && finalData.count > length {
|
||||
finalData.count = length
|
||||
}
|
||||
error = serror
|
||||
group.leave()
|
||||
})
|
||||
let waitResult = group.wait(timeout: .now() + timeout)
|
||||
|
||||
if let error = error {
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
if waitResult == .timedOut {
|
||||
error = self.throwError("", code: URLError.timedOut)
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if let url = URL(string: filePath.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? filePath, relativeTo: self.baseURL!)?.absoluteURL {
|
||||
let urlresponse = URLResponse(url: url, mimeType: nil, expectedContentLength: finalData.count, textEncodingName: nil)
|
||||
let cachedResponse = CachedURLResponse(response: urlresponse, data: finalData)
|
||||
let request = URLRequest(url: url)
|
||||
self.cache?.storeCachedResponse(cachedResponse, for: request)
|
||||
}
|
||||
|
||||
self.dispatch_queue.async {
|
||||
do {
|
||||
try finalData.write(to: tempURL)
|
||||
completionHandler(tempURL, nil)
|
||||
} catch {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
try? FileManager.default.removeItem(at: tempURL)
|
||||
}
|
||||
return
|
||||
}
|
||||
}) { (response, error) in
|
||||
if let error = error {
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let response = response else {
|
||||
completionHandler(nil, self.throwError(filePath, code: URLError.cannotParseResponse))
|
||||
return
|
||||
}
|
||||
|
||||
if !(response.hasPrefix("1") || response.hasPrefix("2")) {
|
||||
let error = FileProviderFTPError(message: response)
|
||||
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ftpStore(_ task: FileProviderStreamTask, filePath: String, fromData: Data?, fromFile: URL?, onTask: ((_ task: FileProviderStreamTask) -> Void)?, onProgress: ((_ bytesSent: Int64, _ totalSent: Int64, _ expectedBytes: Int64) -> Void)?, completionHandler: @escaping (_ error: Error?) -> Void) {
|
||||
let timeout = self.session.configuration.timeoutIntervalForRequest
|
||||
operation_queue.addOperation {
|
||||
guard let size: Int64 = (fromData != nil ? Int64(fromData!.count) : nil) ?? fromFile?.fileSize else { return }
|
||||
|
||||
var error: Error?
|
||||
let chunkSize: Int
|
||||
switch size {
|
||||
case 0..<262_144: chunkSize = 32_768 // 0KB To 256KB, page size is 32KB
|
||||
case 262_144..<1_048_576: chunkSize = 65_536 // 256KB To 1MB, page size is 64KB
|
||||
case 1_048_576..<10_485_760: chunkSize = 131_072 // 1MB To 10MB, page size is 128KB
|
||||
case 10_048_576..<33_554_432: chunkSize = 262_144 // 1MB To 10MB, page size is 256KB
|
||||
default: chunkSize = 524_288 // Larger than 32MB, page size is 512KB
|
||||
}
|
||||
|
||||
var fileHandle: FileHandle?
|
||||
if let file = fromFile {
|
||||
fileHandle = FileHandle(forReadingAtPath: file.path)
|
||||
}
|
||||
defer {
|
||||
fileHandle?.closeFile()
|
||||
}
|
||||
|
||||
var eof = false
|
||||
var sent: Int64 = 0
|
||||
|
||||
while !eof {
|
||||
let subdata: Data
|
||||
if let data = fromData {
|
||||
let endIndex = min(data.count, Int(sent) + chunkSize)
|
||||
eof = endIndex == data.count
|
||||
subdata = data.subdata(in: Int(sent)..<endIndex)
|
||||
}else if let fileHandle = fileHandle {
|
||||
subdata = fileHandle.readData(ofLength: chunkSize)
|
||||
eof = Int64(fileHandle.offsetInFile) == size
|
||||
} else {
|
||||
return
|
||||
}
|
||||
if subdata.count == 0 { continue }
|
||||
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
self.ftpStore(task, data: subdata, to: filePath, from: sent, onTask: onTask, completionHandler: { (serror) in
|
||||
error = serror
|
||||
sent += Int64(subdata.count)
|
||||
group.leave()
|
||||
onProgress?(Int64(subdata.count), sent, size)
|
||||
})
|
||||
let waitResult = group.wait(timeout: .now() + timeout)
|
||||
|
||||
if waitResult == .timedOut {
|
||||
error = self.throwError(filePath, code: URLError.timedOut)
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
if let error = error {
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
}
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func ftpStore(_ task: FileProviderStreamTask, data: Data, to filePath: String, from position: Int64, onTask: ((_ task: FileProviderStreamTask) -> Void)?, completionHandler: @escaping (_ error: Error?) -> Void) {
|
||||
self.ftpDataConnect(task) { (dataTask, error) in
|
||||
if let error = error {
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let dataTask = dataTask else {
|
||||
completionHandler(self.throwError(filePath, code: URLError.badServerResponse))
|
||||
return
|
||||
}
|
||||
|
||||
// Send retreive command
|
||||
var success = false
|
||||
let len = 19 /* TYPE response */ + 65 + String(position).characters.count /* REST Response */ + 44 + filePath.characters.count /* STOR open response */ + 10 /* RETR Transfer complete message. */
|
||||
self.execute(command: "TYPE I" + "\r\n" + "REST \(position)" + "\r\n" + "STOR \(filePath)", on: task, minLength: len, afterSend: { error in
|
||||
// starting passive task
|
||||
let timeout = self.session.configuration.timeoutIntervalForRequest
|
||||
if self.baseURL?.scheme == "ftps" || self.baseURL?.port == 990 {
|
||||
task.startSecureConnection()
|
||||
}
|
||||
onTask?(dataTask)
|
||||
|
||||
if data.count == 0 { return }
|
||||
|
||||
dataTask.write(data, timeout: timeout, completionHandler: { (error) in
|
||||
if let error = error {
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
success = true
|
||||
|
||||
dataTask.closeRead()
|
||||
dataTask.closeWrite()
|
||||
})
|
||||
}) { (response, error) in
|
||||
guard success else { return }
|
||||
|
||||
if let error = error {
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let response = response else {
|
||||
completionHandler(self.throwError(filePath, code: URLError.cannotParseResponse))
|
||||
return
|
||||
}
|
||||
|
||||
if !(response.hasPrefix("1") || response.hasPrefix("2")) {
|
||||
let error = FileProviderFTPError(message: response)
|
||||
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ftpQuit(_ task: FileProviderStreamTask) {
|
||||
self.execute(command: "QUIT", on: task) { (_, _) in
|
||||
//task.closeRead()
|
||||
//task.closeWrite()
|
||||
}
|
||||
}
|
||||
|
||||
func ftpPath(_ apath: String) -> String {
|
||||
var path = apath.isEmpty ? self.currentPath : apath
|
||||
|
||||
// path of base url should be concreted into file path!
|
||||
path = baseURL!.appendingPathComponent(path).path
|
||||
|
||||
// Fixing slashes
|
||||
if !path.hasPrefix("/") {
|
||||
path = "/" + path
|
||||
}
|
||||
if path.hasSuffix("/"){
|
||||
path.characters.removeLast()
|
||||
}
|
||||
|
||||
if path.isEmpty {
|
||||
path = "/"
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
func parseUnixList(_ text: String, in path: String) -> FileObject? {
|
||||
let gregorian = Calendar(identifier: .gregorian)
|
||||
let nearDateFormatter = DateFormatter()
|
||||
nearDateFormatter.calendar = gregorian
|
||||
nearDateFormatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
nearDateFormatter.dateFormat = "MMM dd hh:ss yyyy"
|
||||
let farDateFormatter = DateFormatter()
|
||||
farDateFormatter.calendar = gregorian
|
||||
farDateFormatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
farDateFormatter.dateFormat = "MMM dd yyyy"
|
||||
let thisYear = gregorian.component(.year, from: Date())
|
||||
|
||||
let components = text.components(separatedBy: " ").flatMap { $0.isEmpty ? nil : $0 }
|
||||
guard components.count >= 9 else { return nil }
|
||||
let posixPermission = components[0]
|
||||
let linksCount = Int(components[1]) ?? 0
|
||||
//let owner = components[2]
|
||||
//let groupOwner = components[3]
|
||||
let size = Int64(components[4]) ?? -1
|
||||
let date = components[5..<8].joined(separator: " ")
|
||||
let name = components[8..<components.count].joined(separator: " ")
|
||||
|
||||
guard name != "." && name != ".." else { return nil }
|
||||
var path = (path as NSString).appendingPathComponent(name)
|
||||
if path.hasPrefix("/") {
|
||||
path.characters.removeFirst()
|
||||
}
|
||||
|
||||
let file = FileObject(url: url(of: path), name: name, path: path)
|
||||
switch String(posixPermission.characters.first!) {
|
||||
case "d": file.type = .directory
|
||||
case "l": file.type = .symbolicLink
|
||||
default: file.type = .regular
|
||||
}
|
||||
file.isReadOnly = !posixPermission.contains("w")
|
||||
file.size = file.isDirectory ? -1 : size
|
||||
file.allValues[.linkCountKey] = linksCount
|
||||
|
||||
if let parsedDate = nearDateFormatter.date(from: date + " " + String(thisYear)) {
|
||||
if parsedDate > Date() {
|
||||
file.modifiedDate = gregorian.date(byAdding: .year, value: -1, to: parsedDate)
|
||||
} else {
|
||||
file.modifiedDate = parsedDate
|
||||
}
|
||||
} else if let parsedDate = farDateFormatter.date(from: date) {
|
||||
file.modifiedDate = parsedDate
|
||||
}
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
func parseMLST(_ text: String, in path: String) -> FileObject? {
|
||||
var components = text.components(separatedBy: ";").flatMap { $0.isEmpty ? nil : $0 }
|
||||
guard components.count > 1 else { return nil }
|
||||
|
||||
let nameOrPath = components.removeLast().trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
var correctedPath: String
|
||||
let name: String
|
||||
if nameOrPath.hasPrefix("/") {
|
||||
correctedPath = nameOrPath.replacingOccurrences(of: baseURL!.path, with: "", options: .anchored)
|
||||
name = (nameOrPath as NSString).lastPathComponent
|
||||
} else {
|
||||
name = nameOrPath
|
||||
correctedPath = (path as NSString).appendingPathComponent(nameOrPath)
|
||||
}
|
||||
if correctedPath.hasPrefix("/") {
|
||||
correctedPath.characters.removeFirst()
|
||||
}
|
||||
|
||||
var attributes = [String: String]()
|
||||
for component in components {
|
||||
let keyValue = component.components(separatedBy: "=") .flatMap { $0.isEmpty ? nil : $0 }
|
||||
guard keyValue.count >= 2, !keyValue[0].isEmpty else { continue }
|
||||
attributes[keyValue[0].lowercased()] = keyValue.dropFirst().joined(separator: "=")
|
||||
}
|
||||
|
||||
let file = FileObject(url: url(of: correctedPath), name: name, path: correctedPath)
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.calendar = Calendar(identifier: .gregorian)
|
||||
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
dateFormatter.dateFormat = "yyyyMMddhhmmss"
|
||||
for (key, attribute) in attributes {
|
||||
switch key {
|
||||
case "type":
|
||||
switch attribute.lowercased() {
|
||||
case "file": file.type = .regular
|
||||
case "dir": file.type = .directory
|
||||
case "link": file.type = .symbolicLink
|
||||
case "os.unix=block": file.type = .blockSpecial
|
||||
case "cdir", "pdir": return nil // . and .. files are redundant in listing
|
||||
default: file.type = .unknown
|
||||
}
|
||||
|
||||
case "unique":
|
||||
file.allValues[.fileResourceIdentifierKey] = attribute
|
||||
|
||||
case "modify":
|
||||
file.modifiedDate = dateFormatter.date(from: attribute)
|
||||
|
||||
case "create":
|
||||
file.creationDate = dateFormatter.date(from: attribute)
|
||||
|
||||
case "perm":
|
||||
file.allValues[.isReadableKey] = attribute.contains("r") || attribute.contains("l")
|
||||
file.allValues[.isWritableKey] = attribute.contains("w") || attribute.contains("a")
|
||||
|
||||
case "size":
|
||||
file.size = Int64(attribute) ?? -1
|
||||
|
||||
case "media-type":
|
||||
file.allValues[.mimeTypeKey] = attribute
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return file
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains error code and description returned by FTP/S provider.
|
||||
public struct FileProviderFTPError: Error {
|
||||
/// HTTP status code returned for error by server.
|
||||
public let code: Int
|
||||
/// Path of file/folder casued that error
|
||||
public let path: String
|
||||
/// Contents returned by server as error description
|
||||
public let errorDescription: String?
|
||||
|
||||
init(code: Int, path: String, errorDescription: String?) {
|
||||
self.code = code
|
||||
self.path = path
|
||||
self.errorDescription = errorDescription
|
||||
}
|
||||
|
||||
init(message response: String, path: String = "") {
|
||||
let message = response.components(separatedBy: .newlines).last ?? "No Response"
|
||||
let spaceIndex = message.characters.index(of: "-") ?? message.characters.index(of: " ") ?? message.startIndex
|
||||
self.code = Int(message.substring(to: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
|
||||
self.path = path
|
||||
if code > 0 {
|
||||
self.errorDescription = message.substring(from: spaceIndex).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
} else {
|
||||
self.errorDescription = message
|
||||
}
|
||||
}
|
||||
}
|
||||
+82
-108
@@ -8,36 +8,37 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Containts path and attributes of a file or resource.
|
||||
open class FileObject {
|
||||
/// Containts path, url and attributes of a file or resource.
|
||||
open class FileObject: Equatable {
|
||||
/// A `Dictionary` contains file information, using `URLResourceKey` keys.
|
||||
open internal(set) var allValues: [URLResourceKey: Any]
|
||||
|
||||
internal init(allValues: [URLResourceKey: Any]) {
|
||||
public init(allValues: [URLResourceKey: Any]) {
|
||||
self.allValues = allValues
|
||||
}
|
||||
|
||||
internal init(url: URL, name: String, path: String) {
|
||||
internal init(url: URL?, name: String, path: String) {
|
||||
self.allValues = [URLResourceKey: Any]()
|
||||
self.url = url
|
||||
if let url = url {
|
||||
self.url = url
|
||||
}
|
||||
self.name = name
|
||||
self.path = path
|
||||
}
|
||||
|
||||
/// url to access the resource, not supported by Dropbox provider
|
||||
@available(*, deprecated, renamed: "url", message: "Use url.absoluteURL instead.")
|
||||
open var absoluteURL: URL? {
|
||||
return url?.absoluteURL
|
||||
}
|
||||
|
||||
/// URL to access the resource, can be a relative URL against base URL.
|
||||
/// not supported by Dropbox provider.
|
||||
open internal(set) var url: URL? {
|
||||
open internal(set) var url: URL {
|
||||
get {
|
||||
return allValues[.fileURL] as? URL
|
||||
if let url = allValues[.fileURLKey] as? URL {
|
||||
return url
|
||||
} else {
|
||||
let path = self.path.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? self.path
|
||||
return URL(string: path) ?? URL(string: "/")!
|
||||
}
|
||||
}
|
||||
set {
|
||||
allValues[.fileURL] = newValue
|
||||
allValues[.fileURLKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,12 +102,6 @@ open class FileObject {
|
||||
}
|
||||
}
|
||||
|
||||
/// **OBSOLETED:** Use `type` property instead.
|
||||
@available(*, obsoleted: 1.0, renamed: "type", message: "Use type property instead.")
|
||||
open var fileType: URLFileResourceType? {
|
||||
return self.type
|
||||
}
|
||||
|
||||
/// File is hidden either because begining with dot or filesystem flags
|
||||
/// Setting this value on a file begining with dot has no effect
|
||||
open internal(set) var isHidden: Bool {
|
||||
@@ -142,37 +137,74 @@ open class FileObject {
|
||||
open var isSymLink: Bool {
|
||||
return self.type == .symbolicLink
|
||||
}
|
||||
}
|
||||
|
||||
internal func resolve(dateString: String) -> Date? {
|
||||
let dateFor: DateFormatter = DateFormatter()
|
||||
dateFor.locale = Locale(identifier: "en_US")
|
||||
dateFor.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ssZ"
|
||||
if let rfc3339 = dateFor.date(from: dateString) {
|
||||
return rfc3339
|
||||
}
|
||||
dateFor.dateFormat = "EEE',' dd' 'MMM' 'yyyy HH':'mm':'ss z"
|
||||
if let rfc1123 = dateFor.date(from: dateString) {
|
||||
return rfc1123
|
||||
}
|
||||
dateFor.dateFormat = "EEEE',' dd'-'MMM'-'yy HH':'mm':'ss z"
|
||||
if let rfc850 = dateFor.date(from: dateString) {
|
||||
return rfc850
|
||||
}
|
||||
dateFor.dateFormat = "EEE MMM d HH':'mm':'ss yyyy"
|
||||
if let asctime = dateFor.date(from: dateString) {
|
||||
return asctime
|
||||
|
||||
/// Check `FileObject` equality
|
||||
public static func ==(lhs: FileObject, rhs: FileObject) -> Bool {
|
||||
if rhs === lhs {
|
||||
return true
|
||||
}
|
||||
#if swift(>=3.1)
|
||||
if Swift.type(of: lhs) != Swift.type(of: rhs) {
|
||||
return false
|
||||
}
|
||||
#else
|
||||
if type(of: lhs) != type(of: rhs) {
|
||||
return false
|
||||
}
|
||||
#endif
|
||||
|
||||
if let rurl = rhs.allValues[.fileURLKey] as? URL, let lurl = lhs.allValues[.fileURLKey] as? URL {
|
||||
return rurl == lurl
|
||||
}
|
||||
return rhs.path == lhs.path && rhs.size == lhs.size && rhs.modifiedDate == lhs.modifiedDate
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
internal func rfc3339utc(of date:Date) -> String {
|
||||
let fm = DateFormatter()
|
||||
fm.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
|
||||
fm.timeZone = TimeZone(identifier:"UTC")
|
||||
fm.locale = Locale(identifier:"en_US_POSIX")
|
||||
return fm.string(from:date)
|
||||
internal func mapPredicate() -> [String: Any] {
|
||||
let mapDict: [URLResourceKey: String] = [.fileURLKey: "url", .nameKey: "name", .pathKey: "path", .fileSizeKey: "filesize", .creationDateKey: "creationDate",
|
||||
.contentModificationDateKey: "modifiedDate", .isHiddenKey: "isHidden", .isWritableKey: "isWritable", .serverDateKey: "serverDate", .entryTagKey: "entryTag", .mimeTypeKey: "mimeType"]
|
||||
let typeDict: [URLFileResourceType: String] = [.directory: "directory", .regular: "regular", .symbolicLink: "symbolicLink", .unknown: "unknown"]
|
||||
var result = [String: Any]()
|
||||
for (key, value) in allValues {
|
||||
if let convertkey = mapDict[key] {
|
||||
result[convertkey] = value
|
||||
}
|
||||
}
|
||||
result["eTag"] = result["entryTag"]
|
||||
result["isReadOnly"] = self.isReadOnly
|
||||
result["isDirectory"] = self.isDirectory
|
||||
result["isRegularFile"] = self.isRegularFile
|
||||
result["isSymLink"] = self.isSymLink
|
||||
result["type"] = typeDict[self.type ?? .unknown] ?? "unknown"
|
||||
return result
|
||||
}
|
||||
|
||||
/// Converts macOS spotlight query for searching files to a query that can be used for `searchFiles()` method
|
||||
static public func convertPredicate(fromSpotlight query: NSPredicate) -> NSPredicate {
|
||||
let mapDict: [String: URLResourceKey] = [NSMetadataItemURLKey: .fileURLKey, NSMetadataItemFSNameKey: .nameKey, NSMetadataItemPathKey: .pathKey,
|
||||
NSMetadataItemFSSizeKey: .fileSizeKey, NSMetadataItemFSCreationDateKey: .creationDateKey,
|
||||
NSMetadataItemFSContentChangeDateKey: .contentModificationDateKey, "kMDItemFSInvisible": .isHiddenKey, "kMDItemFSIsWriteable": .isWritableKey, "kMDItemKind": .mimeTypeKey]
|
||||
|
||||
if let cQuery = query as? NSCompoundPredicate {
|
||||
let newSub = cQuery.subpredicates.map { convertPredicate(fromSpotlight: $0 as! NSPredicate) }
|
||||
switch cQuery.compoundPredicateType {
|
||||
case .and: return NSCompoundPredicate(andPredicateWithSubpredicates: newSub)
|
||||
case .not: return NSCompoundPredicate(notPredicateWithSubpredicate: newSub[0])
|
||||
case .or: return NSCompoundPredicate(orPredicateWithSubpredicates: newSub)
|
||||
}
|
||||
} else if let cQuery = query as? NSComparisonPredicate {
|
||||
var newLeft = cQuery.leftExpression
|
||||
var newRight = cQuery.rightExpression
|
||||
if newLeft.expressionType == .keyPath, let newKey = mapDict[newLeft.keyPath] {
|
||||
newLeft = NSExpression(forKeyPath: newKey.rawValue)
|
||||
}
|
||||
if newRight.expressionType == .keyPath, let newKey = mapDict[newRight.keyPath] {
|
||||
newRight = NSExpression(forKeyPath: newKey.rawValue)
|
||||
}
|
||||
return NSComparisonPredicate(leftExpression: newLeft, rightExpression: newRight, modifier: cQuery.comparisonPredicateModifier, type: cQuery.predicateOperatorType, options: cQuery.options)
|
||||
} else {
|
||||
return query
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sorting FileObject array by given criteria, **not thread-safe**
|
||||
@@ -231,7 +263,7 @@ public struct FileObjectSorting {
|
||||
self.isDirectoriesFirst = isDirectoriesFirst
|
||||
}
|
||||
|
||||
/// Sorts array of `FileObject`s by criterias set in properties
|
||||
/// Sorts array of `FileObject`s by criterias set in attributes.
|
||||
public func sort(_ files: [FileObject]) -> [FileObject] {
|
||||
return files.sorted {
|
||||
if isDirectoriesFirst {
|
||||
@@ -267,61 +299,3 @@ public struct FileObjectSorting {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element: FileObject {
|
||||
/// Returns a sorted array of `FileObject`s by criterias set in properties.
|
||||
public func sort(by type: FileObjectSorting.SortType, ascending: Bool = true, isDirectoriesFirst: Bool = false) -> [Element] {
|
||||
let sorting = FileObjectSorting(type: type, ascending: ascending, isDirectoriesFirst: isDirectoriesFirst)
|
||||
return sorting.sort(self) as! [Element]
|
||||
}
|
||||
|
||||
/// Sorts array of `FileObject`s by criterias set in properties
|
||||
public mutating func sorted(by type: FileObjectSorting.SortType, ascending: Bool = true, isDirectoriesFirst: Bool = false) {
|
||||
self = self.sort(by: type, ascending: ascending, isDirectoriesFirst: isDirectoriesFirst)
|
||||
}
|
||||
}
|
||||
|
||||
extension URLFileResourceType {
|
||||
public init(fileTypeValue: FileAttributeType) {
|
||||
switch fileTypeValue {
|
||||
case FileAttributeType.typeCharacterSpecial: self = .characterSpecial
|
||||
case FileAttributeType.typeDirectory: self = .directory
|
||||
case FileAttributeType.typeBlockSpecial: self = .blockSpecial
|
||||
case FileAttributeType.typeRegular: self = .regular
|
||||
case FileAttributeType.typeSymbolicLink: self = .symbolicLink
|
||||
case FileAttributeType.typeSocket: self = .socket
|
||||
case FileAttributeType.typeUnknown: self = .unknown
|
||||
default: self = .unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal extension URLResourceKey {
|
||||
static let fileURL = URLResourceKey(rawValue: "NSURLFileURLKey")
|
||||
static let serverDate = URLResourceKey(rawValue: "NSURLServerDateKey")
|
||||
static let entryTag = URLResourceKey(rawValue: "NSURLEntryTagKey")
|
||||
static let mimeType = URLResourceKey(rawValue: "NSURLMIMETypeIdentifierKey")
|
||||
}
|
||||
|
||||
internal extension URL {
|
||||
var uw_scheme: String {
|
||||
return self.scheme ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
internal func jsonToDictionary(_ jsonString: String) -> [String: AnyObject]? {
|
||||
guard let data = jsonString.data(using: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
if let dic = try? JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions()) as? [String: AnyObject] {
|
||||
return dic
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
internal func dictionaryToJSON(_ dictionary: [String: AnyObject]) -> String? {
|
||||
if let data = try? JSONSerialization.data(withJSONObject: dictionary, options: JSONSerialization.WritingOptions()) {
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+379
-271
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,392 @@
|
||||
//
|
||||
// HTTPFileProvider.swift
|
||||
// FilesProvider
|
||||
//
|
||||
// Created by Amir Abbas Mousavian.
|
||||
// Copyright © 2017 Mousavian. Distributed under MIT license.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
The abstract base class for all REST/Web based providers such as WebDAV, Dropbox, OneDrive, Google Drive, etc. and encapsulates basic
|
||||
functionalitis such as downloading/uploading.
|
||||
|
||||
No instance of this class should (and can) be created. Use derivated classes instead. It leads to a crash with `fatalError()`.
|
||||
*/
|
||||
open class HTTPFileProvider: FileProviderBasicRemote, FileProviderOperations, FileProviderReadWrite {
|
||||
open class var type: String { fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.") }
|
||||
open let baseURL: URL?
|
||||
open var currentPath: String
|
||||
|
||||
open var dispatch_queue: DispatchQueue
|
||||
open var operation_queue: OperationQueue {
|
||||
willSet {
|
||||
assert(_session == nil, "It's not effective to change dispatch_queue property after session is initialized.")
|
||||
}
|
||||
}
|
||||
|
||||
open weak var delegate: FileProviderDelegate?
|
||||
open var credential: URLCredential? {
|
||||
didSet {
|
||||
sessionDelegate?.credential = self.credential
|
||||
}
|
||||
}
|
||||
open private(set) var cache: URLCache?
|
||||
public var useCache: Bool
|
||||
public var validatingCache: Bool
|
||||
|
||||
fileprivate var _session: URLSession?
|
||||
internal fileprivate(set) var sessionDelegate: SessionDelegate?
|
||||
public var session: URLSession {
|
||||
get {
|
||||
if _session == nil {
|
||||
self.sessionDelegate = SessionDelegate(fileProvider: self)
|
||||
let config = URLSessionConfiguration.default
|
||||
config.urlCache = cache
|
||||
config.requestCachePolicy = .returnCacheDataElseLoad
|
||||
_session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDelegate?, delegateQueue: self.operation_queue)
|
||||
_session!.sessionDescription = UUID().uuidString
|
||||
initEmptySessionHandler(_session!.sessionDescription!)
|
||||
}
|
||||
return _session!
|
||||
}
|
||||
|
||||
set {
|
||||
assert(newValue.delegate is SessionDelegate, "session instances should have a SessionDelegate instance as delegate.")
|
||||
_session = newValue
|
||||
if session.sessionDescription?.isEmpty ?? true {
|
||||
_session?.sessionDescription = UUID().uuidString
|
||||
}
|
||||
self.sessionDelegate = newValue.delegate as? SessionDelegate
|
||||
initEmptySessionHandler(_session!.sessionDescription!)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var _longpollSession: URLSession?
|
||||
/// This session has extended timeout up to 10 minutes, suitable for monitoring.
|
||||
internal var longpollSession: URLSession {
|
||||
if _longpollSession == nil {
|
||||
let config = URLSessionConfiguration.default
|
||||
config.timeoutIntervalForRequest = 600
|
||||
_longpollSession = URLSession(configuration: config, delegate: nil, delegateQueue: nil)
|
||||
}
|
||||
return _longpollSession!
|
||||
}
|
||||
|
||||
/**
|
||||
This is parent initializer for subclasses. Using this method on `HTTPFileProvider` will fail as `type` is not implemented.
|
||||
|
||||
- Parameters:
|
||||
- baseURL: Location of WebDAV server.
|
||||
- credential: An `URLCredential` object with `user` and `password`.
|
||||
- cache: A URLCache to cache downloaded files and contents.
|
||||
*/
|
||||
public init(baseURL: URL?, credential: URLCredential?, cache: URLCache?) {
|
||||
self.baseURL = baseURL
|
||||
self.currentPath = ""
|
||||
self.useCache = false
|
||||
self.validatingCache = true
|
||||
self.cache = cache
|
||||
self.credential = credential
|
||||
|
||||
#if swift(>=3.1)
|
||||
let queueLabel = "FileProvider.\(Swift.type(of: self).type)"
|
||||
#else
|
||||
let queueLabel = "FileProvider.\(type(of: self).type)"
|
||||
#endif
|
||||
dispatch_queue = DispatchQueue(label: queueLabel, attributes: .concurrent)
|
||||
operation_queue = OperationQueue()
|
||||
operation_queue.name = "\(queueLabel).Operation"
|
||||
}
|
||||
|
||||
public required convenience init?(coder aDecoder: NSCoder) {
|
||||
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
|
||||
}
|
||||
|
||||
public func encode(with aCoder: NSCoder) {
|
||||
aCoder.encode(self.baseURL, forKey: "baseURL")
|
||||
aCoder.encode(self.credential, forKey: "credential")
|
||||
aCoder.encode(self.currentPath, forKey: "currentPath")
|
||||
aCoder.encode(self.useCache, forKey: "useCache")
|
||||
aCoder.encode(self.validatingCache, forKey: "validatingCache")
|
||||
}
|
||||
|
||||
public static var supportsSecureCoding: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
open func copy(with zone: NSZone? = nil) -> Any {
|
||||
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let sessionuuid = _session?.sessionDescription {
|
||||
removeSessionHandler(for: sessionuuid)
|
||||
}
|
||||
|
||||
if fileProviderCancelTasksOnInvalidating {
|
||||
_session?.invalidateAndCancel()
|
||||
} else {
|
||||
_session?.finishTasksAndInvalidate()
|
||||
}
|
||||
}
|
||||
|
||||
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
|
||||
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
|
||||
}
|
||||
|
||||
open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
|
||||
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
|
||||
}
|
||||
|
||||
open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) {
|
||||
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
|
||||
}
|
||||
|
||||
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? {
|
||||
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
|
||||
}
|
||||
|
||||
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
|
||||
self.storageProperties { total, _ in
|
||||
completionHandler(total > 0)
|
||||
}
|
||||
}
|
||||
|
||||
open weak var fileOperationDelegate: FileOperationDelegate?
|
||||
|
||||
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let path = (atPath as NSString).appendingPathComponent(folderName) + "/"
|
||||
return doOperation(.create(path: path), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
return doOperation(.copy(source: path, destination: toPath), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
return doOperation(.remove(path: path), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
// check file is not a folder
|
||||
guard (try? localFile.resourceValues(forKeys: [.fileResourceTypeKey]))?.fileResourceType ?? .unknown == .regular else {
|
||||
dispatch_queue.async {
|
||||
completionHandler?(self.throwError(localFile.path, code: URLError.fileIsDirectory))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
let operation = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
let request = self.request(for: operation, overwrite: overwrite)
|
||||
return upload_simple(toPath, request: request, localFile: localFile, operation: operation, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let operation = FileOperationType.copy(source: path, destination: destURL.absoluteString)
|
||||
let request = self.request(for: operation)
|
||||
return self.download_simple(path: path, request: request, operation: operation, completionHandler: { [weak self] (tempURL, error) in
|
||||
if let error = error {
|
||||
completionHandler?(error)
|
||||
self?.delegateNotify(operation, error: error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let tempURL = tempURL else {
|
||||
completionHandler?(error)
|
||||
self?.delegateNotify(operation, error: error)
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
try FileManager.default.moveItem(at: tempURL, to: destURL)
|
||||
completionHandler?(nil)
|
||||
self?.delegateNotify(operation)
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
self?.delegateNotify(operation, error: e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
|
||||
if length == 0 || offset < 0 {
|
||||
dispatch_queue.async {
|
||||
completionHandler(Data(), nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
let operation = FileOperationType.fetch(path: path)
|
||||
var request = self.request(for: operation)
|
||||
request.set(httpRangeWithOffset: offset, length: length)
|
||||
return self.download_simple(path: path, request: request, operation: operation, completionHandler: { (tempURL, error) in
|
||||
if let error = error {
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let tempURL = tempURL else {
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: tempURL)
|
||||
completionHandler(data, nil)
|
||||
} catch let e {
|
||||
completionHandler(nil, e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let operation = FileOperationType.modify(path: path)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
let request = self.request(for: operation, overwrite: overwrite, attributes: [.contentModificationDateKey: Date()])
|
||||
return upload_simple(path, request: request, data: data ?? Data(), operation: operation, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
internal func request(for operation: FileOperationType, overwrite: Bool = false, attributes: [URLResourceKey: Any] = [:]) -> URLRequest {
|
||||
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
|
||||
}
|
||||
|
||||
internal func serverError(with code: FileProviderHTTPErrorCode, path: String?, data: Data?) -> FileProviderHTTPError {
|
||||
fatalError("HTTPFileProvider is an abstract class. Please implement \(#function) in subclass.")
|
||||
}
|
||||
|
||||
internal func multiStatusHandler(source: String, data: Data, completionHandler: SimpleCompletionHandler) -> Void {
|
||||
// WebDAV will override this function
|
||||
}
|
||||
|
||||
fileprivate func doOperation(_ operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let progress = Progress(totalUnitCount: 1)
|
||||
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
||||
progress.kind = .file
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
||||
|
||||
let request = self.request(for: operation)
|
||||
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderHTTPError?
|
||||
if let response = response as? HTTPURLResponse, response.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) {
|
||||
serverError = self.serverError(with: code, path: operation.source, data: data)
|
||||
}
|
||||
|
||||
if let response = response as? HTTPURLResponse, FileProviderHTTPErrorCode(rawValue: response.statusCode) == .multiStatus, let data = data {
|
||||
self.multiStatusHandler(source: operation.source, data: data, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
if serverError == nil && error == nil {
|
||||
progress.completedUnitCount = 1
|
||||
} else {
|
||||
progress.cancel()
|
||||
}
|
||||
completionHandler?(serverError ?? error)
|
||||
self.delegateNotify(operation, error: serverError ?? error)
|
||||
})
|
||||
task.taskDescription = operation.json
|
||||
progress.cancellationHandler = { [weak task] in
|
||||
task?.cancel()
|
||||
}
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
task.resume()
|
||||
return progress
|
||||
}
|
||||
|
||||
internal func upload_simple(_ targetPath: String, request: URLRequest, data: Data? = nil, localFile: URL? = nil, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let size = data?.count ?? Int((try? localFile?.resourceValues(forKeys: [.fileSizeKey]))??.fileSize ?? -1)
|
||||
|
||||
var progress = Progress(parent: nil, userInfo: nil)
|
||||
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
||||
progress.kind = .file
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
||||
progress.totalUnitCount = Int64(size)
|
||||
|
||||
let task: URLSessionUploadTask
|
||||
if let data = data {
|
||||
task = session.uploadTask(with: request, from: data)
|
||||
} else if let localFile = localFile {
|
||||
task = session.uploadTask(with: request, fromFile: localFile)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { [weak self] error in
|
||||
var responseError: FileProviderHTTPError?
|
||||
if let code = (task.response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
// We can't fetch server result from delegate!
|
||||
responseError = self?.serverError(with: rCode, path: targetPath, data: nil)
|
||||
}
|
||||
if !(responseError == nil && error == nil) {
|
||||
progress.cancel()
|
||||
}
|
||||
completionHandler?(responseError ?? error)
|
||||
self?.delegateNotify(operation, error: responseError ?? error)
|
||||
}
|
||||
task.taskDescription = operation.json
|
||||
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent), options: .new, context: &progress)
|
||||
progress.cancellationHandler = { [weak task] in
|
||||
task?.cancel()
|
||||
}
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
task.resume()
|
||||
return progress
|
||||
}
|
||||
|
||||
internal func download_simple(path: String, request: URLRequest, operation: FileOperationType, completionHandler: @escaping ((_ tempURL: URL?, _ error: Error?) -> Void)) -> Progress? {
|
||||
var progress = Progress(parent: nil, userInfo: nil)
|
||||
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
||||
progress.kind = .file
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
||||
|
||||
let task = session.downloadTask(with: request)
|
||||
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
|
||||
if error != nil {
|
||||
progress.cancel()
|
||||
}
|
||||
completionHandler(nil, error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { tempURL in
|
||||
guard let httpResponse = task.response as? HTTPURLResponse , httpResponse.statusCode < 300 else {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: (task.response as? HTTPURLResponse)?.statusCode ?? -1)
|
||||
let errorData : Data? = nil //Data(contentsOf:cacheURL) // TODO: Figure out how to get error response data for the error description
|
||||
let serverError : FileProviderHTTPError? = code != nil ? self.serverError(with: code!, path: path, data: errorData) : nil
|
||||
if serverError != nil {
|
||||
progress.cancel()
|
||||
}
|
||||
completionHandler(nil, serverError)
|
||||
self.delegateNotify(operation)
|
||||
return
|
||||
}
|
||||
|
||||
completionHandler(tempURL, nil)
|
||||
}
|
||||
task.taskDescription = operation.json
|
||||
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived), options: .new, context: &progress)
|
||||
task.addObserver(sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive), options: .new, context: &progress)
|
||||
progress.cancellationHandler = { [weak task] in
|
||||
task?.cancel()
|
||||
}
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
task.resume()
|
||||
return progress
|
||||
}
|
||||
}
|
||||
|
||||
extension HTTPFileProvider: FileProvider { }
|
||||
+290
-164
@@ -8,17 +8,24 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
This provider class allows interacting with local files placed in user disk. It also allows an
|
||||
easy way to use `NSFileCoordintaing` to coordinate read and write when neccessary.
|
||||
|
||||
it uses `FileManager` foundation class with some additions like searching and reading a portion of file.
|
||||
*/
|
||||
open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndoable {
|
||||
open class var type: String { return "Local" }
|
||||
open var isPathRelative: Bool
|
||||
open fileprivate(set) var baseURL: URL?
|
||||
open var currentPath: String
|
||||
open var dispatch_queue: DispatchQueue
|
||||
open var operation_queue: OperationQueue
|
||||
open weak var delegate: FileProviderDelegate?
|
||||
open internal(set) var credential: URLCredential?
|
||||
|
||||
open var credential: URLCredential?
|
||||
|
||||
/// Underlying `FileManager` object for listing and metadata fetching.
|
||||
open private(set) var fileManager = FileManager()
|
||||
/// Underlying `FileManager` object for operationa like copying, moving, etc.
|
||||
open private(set) var opFileManager = FileManager()
|
||||
fileprivate var fileProviderManagerDelegate: LocalFileProviderManagerDelegate? = nil
|
||||
|
||||
@@ -40,10 +47,10 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
|
||||
default values are `directory: .documentDirectory, domainMask: .userDomainMask`.
|
||||
|
||||
- Parameters:
|
||||
- directory: The search path directory. The supported values are described in `FileManager.SearchPathDirectory`.
|
||||
- domainMask: The file system domain to search. The value for this parameter is one or more of the constants described in `FileManager.SearchPathDomainMask`.
|
||||
*/
|
||||
public convenience init (directory: FileManager.SearchPathDirectory = .documentDirectory, domainMask: FileManager.SearchPathDomainMask = .userDomainMask) {
|
||||
- for: The search path directory. The supported values are described in `FileManager.SearchPathDirectory`.
|
||||
- in: Base locations for directory to search. The value for this parameter is one or more of the constants described in `FileManager.SearchPathDomainMask`.
|
||||
*/
|
||||
public convenience init (for directory: FileManager.SearchPathDirectory = .documentDirectory, in domainMask: FileManager.SearchPathDomainMask = .userDomainMask) {
|
||||
self.init(baseURL: FileManager.default.urls(for: directory, in: domainMask).first!)
|
||||
}
|
||||
|
||||
@@ -55,7 +62,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
|
||||
default values are `directory: .documentDirectory`.
|
||||
|
||||
- Parameters:
|
||||
- sharedContainerId: Same with `App Group` identifier defined in project settings.
|
||||
- sharedContainerId: Same with `App Group` identifier defined in project settings.
|
||||
- directory: The search path directory. The supported values are described in `FileManager.SearchPathDirectory`.
|
||||
*/
|
||||
public convenience init? (sharedContainerId: String, directory: FileManager.SearchPathDirectory = .documentDirectory) {
|
||||
@@ -63,7 +70,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
|
||||
return nil
|
||||
}
|
||||
|
||||
var finalBaseURL = baseURL
|
||||
var finalBaseURL = baseURL.absoluteURL
|
||||
|
||||
switch directory {
|
||||
case .documentDirectory:
|
||||
@@ -73,12 +80,13 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
|
||||
case .cachesDirectory:
|
||||
finalBaseURL = baseURL.appendingPathComponent("Library/Caches")
|
||||
case .applicationSupportDirectory:
|
||||
finalBaseURL = baseURL.appendingPathComponent("Library/Application%20support")
|
||||
finalBaseURL = baseURL.appendingPathComponent("Library/Application support")
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
self.init(baseURL: finalBaseURL)
|
||||
self.isCoorinating = true
|
||||
|
||||
try? fileManager.createDirectory(at: finalBaseURL, withIntermediateDirectories: true)
|
||||
}
|
||||
@@ -90,25 +98,51 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
|
||||
guard baseURL.isFileURL else {
|
||||
fatalError("Cannot initialize a Local provider from remote URL.")
|
||||
}
|
||||
self.baseURL = baseURL
|
||||
self.isPathRelative = true
|
||||
self.baseURL = (baseURL.absoluteString.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")).absoluteURL
|
||||
self.currentPath = ""
|
||||
self.credential = nil
|
||||
self.isCoorinating = false
|
||||
|
||||
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: .concurrent)
|
||||
#if swift(>=3.1)
|
||||
let queueLabel = "FileProvider.\(Swift.type(of: self).type)"
|
||||
#else
|
||||
let queueLabel = "FileProvider.\(type(of: self).type)"
|
||||
#endif
|
||||
dispatch_queue = DispatchQueue(label: queueLabel, attributes: .concurrent)
|
||||
operation_queue = OperationQueue()
|
||||
operation_queue.name = "FileProvider.\(type(of: self).type).Operation"
|
||||
operation_queue.name = "\(queueLabel).Operation"
|
||||
|
||||
fileProviderManagerDelegate = LocalFileProviderManagerDelegate(provider: self)
|
||||
opFileManager.delegate = fileProviderManagerDelegate
|
||||
|
||||
}
|
||||
|
||||
/// **DEPRECATED:** No longer is in use and overriding this method has no effect anymore.
|
||||
@available(*, deprecated, message: "Overriding this method has no effect anymore.")
|
||||
open class func defaultBaseURL() -> URL {
|
||||
return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
public required convenience init?(coder aDecoder: NSCoder) {
|
||||
guard let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL else {
|
||||
return nil
|
||||
}
|
||||
self.init(baseURL: baseURL)
|
||||
self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? ""
|
||||
self.isCoorinating = aDecoder.decodeBool(forKey: "isCoorinating")
|
||||
}
|
||||
|
||||
open func encode(with aCoder: NSCoder) {
|
||||
aCoder.encode(self.baseURL, forKey: "currentPath")
|
||||
aCoder.encode(self.currentPath, forKey: "currentPath")
|
||||
aCoder.encode(self.isCoorinating, forKey: "isCoorinating")
|
||||
}
|
||||
|
||||
public static var supportsSecureCoding: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public func copy(with zone: NSZone? = nil) -> Any {
|
||||
let copy = LocalFileProvider(baseURL: self.baseURL!)
|
||||
copy.currentPath = self.currentPath
|
||||
copy.undoManager = self.undoManager
|
||||
copy.isCoorinating = self.isCoorinating
|
||||
copy.delegate = self.delegate
|
||||
copy.fileOperationDelegate = self.fileOperationDelegate
|
||||
return copy
|
||||
}
|
||||
|
||||
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
|
||||
@@ -126,6 +160,12 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
|
||||
}
|
||||
}
|
||||
|
||||
open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
|
||||
dispatch_queue.async {
|
||||
completionHandler(LocalFileObject(fileWithPath: path, relativeTo: self.baseURL), nil)
|
||||
}
|
||||
}
|
||||
|
||||
open func storageProperties(completionHandler: (@escaping (_ total: Int64, _ used: Int64) -> Void)) {
|
||||
let values = try? baseURL?.resourceValues(forKeys: [.volumeTotalCapacityKey, .volumeAvailableCapacityKey])
|
||||
let totalSize = Int64(values??.volumeTotalCapacity ?? -1)
|
||||
@@ -133,73 +173,114 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
|
||||
completionHandler(totalSize, totalSize - freeSize)
|
||||
}
|
||||
|
||||
open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
|
||||
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? {
|
||||
let progress = Progress(parent: nil, userInfo: nil)
|
||||
progress.setUserInfoObject(self.url(of: path), forKey: .fileURLKey)
|
||||
|
||||
dispatch_queue.async {
|
||||
completionHandler(LocalFileObject(fileWithPath: path, relativeTo: self.baseURL), nil)
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
let iterator = self.fileManager.enumerator(at: self.url(of: path), includingPropertiesForKeys: nil, options: recursive ? [] : [.skipsSubdirectoryDescendants, .skipsPackageDescendants]) { (url, e) -> Bool in
|
||||
completionHandler([], e)
|
||||
return true
|
||||
}
|
||||
var result = [LocalFileObject]()
|
||||
while let fileURL = iterator?.nextObject() as? URL {
|
||||
if progress.isCancelled {
|
||||
break
|
||||
}
|
||||
let path = self.relativePathOf(url: fileURL)
|
||||
if let fileObject = LocalFileObject(fileWithPath: path, relativeTo: self.baseURL), query.evaluate(with: fileObject.mapPredicate()) {
|
||||
result.append(fileObject)
|
||||
progress.completedUnitCount = Int64(result.count)
|
||||
foundItemHandler?(fileObject)
|
||||
}
|
||||
}
|
||||
completionHandler(result, nil)
|
||||
}
|
||||
|
||||
return progress
|
||||
}
|
||||
|
||||
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
|
||||
dispatch_queue.async {
|
||||
completionHandler(self.fileManager.isReadableFile(atPath: self.baseURL!.path))
|
||||
}
|
||||
}
|
||||
|
||||
open weak var fileOperationDelegate : FileOperationDelegate?
|
||||
|
||||
@discardableResult
|
||||
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(folderName) + "/")
|
||||
return self.doOperation(opType, completionHandler: completionHandler)
|
||||
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let operation = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(folderName) + "/")
|
||||
return self.doOperation(operation, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func create(file fileName: String, at atPath: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let fileName = fileName.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||||
let path = (atPath as NSString).appendingPathComponent(fileName)
|
||||
let opType = FileOperationType.create(path: path)
|
||||
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let operation = FileOperationType.move(source: path, destination: toPath)
|
||||
|
||||
return self.doOperation(opType, data: data, atomically: true, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.move(source: path, destination: toPath)
|
||||
|
||||
if !overwrite && self.fileManager.fileExists(atPath: self.url(of: toPath).path) {
|
||||
completionHandler?(self.throwError(toPath, code: CocoaError.fileWriteFileExists as FoundationErrorEnum))
|
||||
let fileExists = ((try? self.url(of: toPath).checkResourceIsReachable()) ?? false) ||
|
||||
((try? self.url(of: toPath).checkPromisedItemIsReachable()) ?? false)
|
||||
if !overwrite && fileExists {
|
||||
let e = self.throwError(toPath, code: CocoaError.fileWriteFileExists)
|
||||
dispatch_queue.async {
|
||||
completionHandler?(e)
|
||||
}
|
||||
self.delegateNotify(operation, error: e)
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.doOperation(opType, completionHandler: completionHandler)
|
||||
return self.doOperation(operation, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.copy(source: path, destination: toPath)
|
||||
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let operation = FileOperationType.copy(source: path, destination: toPath)
|
||||
|
||||
if !overwrite && self.fileManager.fileExists(atPath: self.url(of: toPath).path) {
|
||||
completionHandler?(self.throwError(toPath, code: CocoaError.fileWriteFileExists as FoundationErrorEnum))
|
||||
let fileExists = ((try? self.url(of: toPath).checkResourceIsReachable()) ?? false) ||
|
||||
((try? self.url(of: toPath).checkPromisedItemIsReachable()) ?? false)
|
||||
if !overwrite && fileExists {
|
||||
let e = self.throwError(toPath, code: CocoaError.fileWriteFileExists)
|
||||
dispatch_queue.async {
|
||||
completionHandler?(e)
|
||||
}
|
||||
self.delegateNotify(operation, error: e)
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.doOperation(opType, completionHandler: completionHandler)
|
||||
return self.doOperation(operation, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.remove(path: path)
|
||||
return self.doOperation(opType, completionHandler: completionHandler)
|
||||
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let operation = FileOperationType.remove(path: path)
|
||||
return self.doOperation(operation, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
// TODO: Make use of overwrite parameter
|
||||
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
|
||||
return self.doOperation(opType, completionHandler: completionHandler)
|
||||
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let operation = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
|
||||
|
||||
let fileExists = ((try? self.url(of: toPath).checkResourceIsReachable()) ?? false) ||
|
||||
((try? self.url(of: toPath).checkPromisedItemIsReachable()) ?? false)
|
||||
if !overwrite && fileExists {
|
||||
let e = self.throwError(toPath, code: CocoaError.fileWriteFileExists)
|
||||
dispatch_queue.async {
|
||||
completionHandler?(e)
|
||||
}
|
||||
self.delegateNotify(operation, error: e)
|
||||
return nil
|
||||
}
|
||||
return self.doOperation(operation, forUploading: true, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
|
||||
return self.doOperation(opType, completionHandler: completionHandler)
|
||||
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let operation = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
|
||||
return self.doOperation(operation, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
dynamic func doSimpleOperation(_ box: UndoBox) {
|
||||
@objc dynamic func doSimpleOperation(_ box: UndoBox) {
|
||||
guard let _ = self.undoManager else { return }
|
||||
_ = self.doOperation(box.undoOperation) { (_) in
|
||||
return
|
||||
@@ -207,45 +288,71 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
fileprivate func doOperation(_ opType: FileOperationType, data: Data? = nil, atomically: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard let sourcePath = opType.source else { return nil }
|
||||
let destPath = opType.destination
|
||||
let source: URL = sourcePath.hasPrefix("file://") ? URL(string: sourcePath)! : self.url(of: sourcePath)
|
||||
let dest: URL?
|
||||
fileprivate func doOperation(_ operation: FileOperationType, data: Data? = nil, atomically: Bool = false, forUploading: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let progress = Progress(parent: nil, userInfo: nil)
|
||||
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
||||
progress.kind = .file
|
||||
progress.isCancellable = false
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.receiving, forKey: .fileOperationKindKey)
|
||||
func urlofpath(path: String) -> URL {
|
||||
if path.hasPrefix("file://") {
|
||||
let removedSchemePath = path.replacingOccurrences(of: "file://", with: "", options: .anchored)
|
||||
let pDecodedPath = removedSchemePath.removingPercentEncoding ?? removedSchemePath
|
||||
return URL(fileURLWithPath: pDecodedPath)
|
||||
} else {
|
||||
return self.url(of: path)
|
||||
}
|
||||
}
|
||||
|
||||
let sourcePath = operation.source
|
||||
let destPath = operation.destination
|
||||
let source: URL = urlofpath(path: sourcePath)
|
||||
progress.setUserInfoObject(source, forKey: .fileURLKey)
|
||||
|
||||
let dest: URL?
|
||||
if let destPath = destPath {
|
||||
dest = destPath.hasPrefix("file://") ? URL(string: destPath)! : self.url(of: destPath)
|
||||
dest = urlofpath(path: destPath)
|
||||
} else {
|
||||
dest = nil
|
||||
}
|
||||
|
||||
if let undoManager = self.undoManager, let undoOp = self.undoOperation(for: opType) {
|
||||
let undoBox = UndoBox(provider: self, operation: opType, undoOperation: undoOp)
|
||||
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(opType.actionDescription)
|
||||
undoManager.setActionName(operation.actionDescription)
|
||||
undoManager.endUndoGrouping()
|
||||
}
|
||||
|
||||
var successfulSecurityScopedResourceAccess = false
|
||||
|
||||
let operationHandler: (URL, URL?) -> Void = { source, dest in
|
||||
let successfulSecurityScopedResourceAccess = source.startAccessingSecurityScopedResource()
|
||||
do {
|
||||
switch opType {
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
switch operation {
|
||||
case .create:
|
||||
if sourcePath.hasSuffix("/") {
|
||||
progress.totalUnitCount = 1
|
||||
try self.opFileManager.createDirectory(at: source, withIntermediateDirectories: true, attributes: [:])
|
||||
} else {
|
||||
try data?.write(to: source, options: Data.WritingOptions.atomic)
|
||||
progress.totalUnitCount = Int64(data?.count ?? 0)
|
||||
try data?.write(to: source, options: .atomic)
|
||||
}
|
||||
case .modify:
|
||||
progress.totalUnitCount = Int64(data?.count ?? 0)
|
||||
try data?.write(to: source, options: atomically ? [.atomic] : [])
|
||||
case .copy:
|
||||
guard let dest = dest else { return }
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.copying, forKey: .fileOperationKindKey)
|
||||
progress.totalUnitCount = abs(source.fileSize)
|
||||
try self.opFileManager.copyItem(at: source, to: dest)
|
||||
case .move:
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.copying, forKey: .fileOperationKindKey)
|
||||
guard let dest = dest else { return }
|
||||
progress.totalUnitCount = abs(source.fileSize)
|
||||
try self.opFileManager.moveItem(at: source, to: dest)
|
||||
case.remove:
|
||||
progress.totalUnitCount = abs(source.fileSize)
|
||||
try self.opFileManager.removeItem(at: source)
|
||||
default:
|
||||
return
|
||||
@@ -253,134 +360,178 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
|
||||
if successfulSecurityScopedResourceAccess {
|
||||
source.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
|
||||
progress.completedUnitCount = progress.totalUnitCount
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(nil)
|
||||
}
|
||||
self.delegateNotify(operation)
|
||||
} catch let e {
|
||||
if successfulSecurityScopedResourceAccess {
|
||||
source.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
progress.cancel()
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(e)
|
||||
}
|
||||
self.delegateNotify(operation, error: e)
|
||||
}
|
||||
}
|
||||
|
||||
if isCoorinating {
|
||||
successfulSecurityScopedResourceAccess = source.startAccessingSecurityScopedResource()
|
||||
var intents = [NSFileAccessIntent]()
|
||||
|
||||
switch opType {
|
||||
case .create, .remove, .modify:
|
||||
switch operation {
|
||||
case .create, .modify:
|
||||
intents.append(NSFileAccessIntent.writingIntent(with: source, options: .forReplacing))
|
||||
case .copy:
|
||||
guard let dest = dest else { return nil }
|
||||
intents.append(NSFileAccessIntent.readingIntent(with: source, options: .withoutChanges))
|
||||
intents.append(NSFileAccessIntent.readingIntent(with: source, options: forUploading ? .forUploading : .withoutChanges))
|
||||
intents.append(NSFileAccessIntent.writingIntent(with: dest, options: .forReplacing))
|
||||
case .move:
|
||||
guard let dest = dest else { return nil }
|
||||
intents.append(NSFileAccessIntent.writingIntent(with: source, options: .forDeleting))
|
||||
intents.append(NSFileAccessIntent.writingIntent(with: source, options: .forMoving))
|
||||
intents.append(NSFileAccessIntent.writingIntent(with: dest, options: .forReplacing))
|
||||
case .remove:
|
||||
intents.append(NSFileAccessIntent.writingIntent(with: source, options: .forDeleting))
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
self.coordinated(intents: intents, completionHandler: operationHandler, errorHandler: { error in
|
||||
completionHandler?(error)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
self.coordinated(intents: intents, operationHandler: operationHandler, errorHandler: { error in
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
}
|
||||
self.delegateNotify(operation, error: error)
|
||||
})
|
||||
} else {
|
||||
operation_queue.addOperation {
|
||||
operationHandler(source, dest)
|
||||
}
|
||||
}
|
||||
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
return progress
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
let opType = FileOperationType.fetch(path: path)
|
||||
open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
|
||||
let operation = FileOperationType.fetch(path: path)
|
||||
let url = self.url(of: path)
|
||||
|
||||
let progress = Progress(parent: nil, userInfo: nil)
|
||||
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
||||
progress.kind = .file
|
||||
progress.isCancellable = false
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.receiving, forKey: .fileOperationKindKey)
|
||||
progress.setUserInfoObject(url, forKey: .fileURLKey)
|
||||
progress.totalUnitCount = url.fileSize
|
||||
|
||||
let operationHandler: (URL) -> Void = { url in
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
do {
|
||||
let data = try Data(contentsOf: url)
|
||||
completionHandler(data, nil)
|
||||
progress.completedUnitCount = progress.totalUnitCount
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(data, nil)
|
||||
}
|
||||
self.delegateNotify(operation)
|
||||
} catch let e {
|
||||
completionHandler(nil, e)
|
||||
progress.cancel()
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, e)
|
||||
}
|
||||
self.delegateNotify(operation, error: e)
|
||||
}
|
||||
}
|
||||
|
||||
if isCoorinating {
|
||||
let intent = NSFileAccessIntent.readingIntent(with: url, options: .withoutChanges)
|
||||
coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
|
||||
completionHandler(nil, error)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
coordinated(intents: [intent], operationHandler: operationHandler, errorHandler: { error in
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
self.delegateNotify(operation, error: error)
|
||||
})
|
||||
} else {
|
||||
dispatch_queue.async {
|
||||
operationHandler(url)
|
||||
}
|
||||
}
|
||||
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
|
||||
return progress
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
|
||||
if length == 0 || offset < 0 {
|
||||
dispatch_queue.async {
|
||||
completionHandler(Data(), nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
if offset == 0 && length < 0 {
|
||||
return self.contents(path: path, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
let opType = FileOperationType.fetch(path: path)
|
||||
|
||||
let operation = FileOperationType.fetch(path: path)
|
||||
let url = self.url(of: path)
|
||||
|
||||
let progress = Progress(parent: nil, userInfo: nil)
|
||||
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
||||
progress.kind = .file
|
||||
progress.isCancellable = false
|
||||
progress.setUserInfoObject(url, forKey: .fileURLKey)
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.receiving, forKey: .fileOperationKindKey)
|
||||
|
||||
let operationHandler: (URL) -> Void = { url in
|
||||
guard let handle = FileHandle(forReadingAtPath: url.path) else {
|
||||
completionHandler(nil, self.throwError(path, code: CocoaError.fileNoSuchFile as FoundationErrorEnum))
|
||||
self.dispatch_queue.async {
|
||||
let e = self.throwError(path, code: CocoaError.fileNoSuchFile)
|
||||
completionHandler(nil, e)
|
||||
self.delegateNotify(operation, error: e)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
defer {
|
||||
handle.closeFile()
|
||||
}
|
||||
|
||||
|
||||
let size = LocalFileObject(fileWithURL: url)?.size ?? -1
|
||||
progress.totalUnitCount = size
|
||||
guard size > offset else {
|
||||
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadTooLarge as FoundationErrorEnum))
|
||||
progress.cancel()
|
||||
self.dispatch_queue.async {
|
||||
let e = self.throwError(path, code: CocoaError.fileReadTooLarge)
|
||||
completionHandler(nil, e)
|
||||
self.delegateNotify(operation, error: e)
|
||||
}
|
||||
return
|
||||
}
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
handle.seek(toFileOffset: UInt64(offset))
|
||||
guard Int64(handle.offsetInFile) == offset else {
|
||||
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadTooLarge as FoundationErrorEnum))
|
||||
progress.cancel()
|
||||
self.dispatch_queue.async {
|
||||
let e = self.throwError(path, code: CocoaError.fileReadTooLarge)
|
||||
completionHandler(nil, e)
|
||||
self.delegateNotify(operation, error: e)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let data = handle.readData(ofLength: length)
|
||||
|
||||
completionHandler(data, nil)
|
||||
progress.completedUnitCount = progress.totalUnitCount
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(data, nil)
|
||||
self.delegateNotify(operation)
|
||||
}
|
||||
}
|
||||
|
||||
if isCoorinating {
|
||||
let intent = NSFileAccessIntent.readingIntent(with: url, options: .withoutChanges)
|
||||
coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
|
||||
coordinated(intents: [intent], operationHandler: operationHandler, errorHandler: { error in
|
||||
completionHandler(nil, error)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
self.delegateNotify(operation, error: error)
|
||||
})
|
||||
} else {
|
||||
dispatch_queue.async {
|
||||
@@ -388,33 +539,24 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
|
||||
}
|
||||
}
|
||||
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
return progress
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.modify(path: path)
|
||||
return self.doOperation(opType, data: data, atomically: atomically, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
open func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
|
||||
dispatch_queue.async {
|
||||
let iterator = self.fileManager.enumerator(at: self.url(of: path), includingPropertiesForKeys: nil, options: recursive ? [] : [.skipsSubdirectoryDescendants, .skipsPackageDescendants]) { (url, e) -> Bool in
|
||||
completionHandler([], e)
|
||||
return true
|
||||
open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let fileExists = ((try? self.url(of: path).checkResourceIsReachable()) ?? false) ||
|
||||
((try? self.url(of: path).checkPromisedItemIsReachable()) ?? false)
|
||||
if !overwrite && fileExists {
|
||||
let e = self.throwError(path, code: CocoaError.fileWriteFileExists)
|
||||
dispatch_queue.async {
|
||||
completionHandler?(e)
|
||||
}
|
||||
var result = [LocalFileObject]()
|
||||
while let fileURL = iterator?.nextObject() as? URL {
|
||||
if fileURL.lastPathComponent.lowercased().contains(query.lowercased()) {
|
||||
let path = self.relativePathOf(url: fileURL)
|
||||
if let fileObject = LocalFileObject(fileWithPath: path, relativeTo: self.baseURL) {
|
||||
result.append(fileObject)
|
||||
foundItemHandler?(fileObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
completionHandler(result, nil)
|
||||
self.delegateNotify(.modify(path: path), error: e)
|
||||
return nil
|
||||
}
|
||||
|
||||
let operation: FileOperationType = fileExists ? .modify(path: path) : .create(path: path)
|
||||
return self.doOperation(operation, data: data ?? Data(), atomically: atomically, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
fileprivate var monitors = [LocalFolderMonitor]()
|
||||
@@ -422,7 +564,7 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
|
||||
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) ?? false
|
||||
let isdir = (try? dirurl.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory ?? false
|
||||
if !isdir {
|
||||
return
|
||||
}
|
||||
@@ -445,46 +587,30 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndo
|
||||
}
|
||||
|
||||
open func isRegisteredForNotification(path: String) -> Bool {
|
||||
return monitors.map( { self.relativePathOf(url: $0.url) } ).contains(path)
|
||||
return monitors.map( { self.relativePathOf(url: $0.url) } ).contains(path.trimmingCharacters(in: CharacterSet(charactersIn: "/")))
|
||||
}
|
||||
|
||||
open func copy(with zone: NSZone? = nil) -> Any {
|
||||
let copy = LocalFileProvider(baseURL: self.baseURL!)
|
||||
copy.currentPath = self.currentPath
|
||||
copy.delegate = self.delegate
|
||||
copy.fileOperationDelegate = self.fileOperationDelegate
|
||||
copy.isPathRelative = self.isPathRelative
|
||||
copy.undoManager = self.undoManager
|
||||
copy.isCoorinating = self.isCoorinating
|
||||
return copy
|
||||
}
|
||||
}
|
||||
|
||||
public extension LocalFileProvider {
|
||||
/**
|
||||
Creates a symbolic link at the specified path that points to an item at the given path.
|
||||
This method does not traverse symbolic links contained in destURL, making it possible
|
||||
to create symbolic links to locations that do not yet exist.
|
||||
Also, if the final path component in url is a symbolic link, that link is not followed.
|
||||
This method does not traverse symbolic links contained in destination path, making it possible
|
||||
to create symbolic links to locations that do not yet exist.
|
||||
Also, if the final path component is a symbolic link, that link is not followed.
|
||||
|
||||
- Parameters:
|
||||
- path: The file path at which to create the new symbolic link. The last component of the path issued as the name of the link.
|
||||
- destPath: The path that contains the item to be pointed to by the link. In other words, this is the destination of the link.
|
||||
- symbolicLink: The file path at which to create the new symbolic link. The last component of the path issued as the name of the link.
|
||||
- withDestinationPath: The path that contains the item to be pointed to by the link. In other words, this is the destination of the link.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
*/
|
||||
public func create(symbolicLink path: String, withDestinationPath destPath: String, completionHandler: SimpleCompletionHandler) {
|
||||
open func create(symbolicLink path: String, withDestinationPath destPath: String, completionHandler: SimpleCompletionHandler) {
|
||||
operation_queue.addOperation {
|
||||
let operation = FileOperationType.link(link: path, target: destPath)
|
||||
do {
|
||||
try self.opFileManager.createSymbolicLink(at: self.url(of: path), withDestinationURL: self.url(of: destPath))
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderSucceed(self, operation: .link(link: path, target: destPath))
|
||||
}
|
||||
self.delegateNotify(operation)
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: .link(link: path, target: destPath))
|
||||
}
|
||||
self.delegateNotify(operation, error: e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -494,7 +620,7 @@ public extension LocalFileProvider {
|
||||
/// - Parameters:
|
||||
/// - path: The path of a file or directory.
|
||||
/// - completionHandler: Returns destination url of given symbolic link, or an `Error` object if it fails.
|
||||
public func destination(ofSymbolicLink path: String, completionHandler: @escaping (_ url: URL?, _ error: Error?) -> Void) {
|
||||
open func destination(ofSymbolicLink path: String, completionHandler: @escaping (_ url: URL?, _ error: Error?) -> Void) {
|
||||
dispatch_queue.async {
|
||||
do {
|
||||
let destPath = try self.opFileManager.destinationOfSymbolicLink(atPath: self.url(of: path).path)
|
||||
@@ -508,30 +634,30 @@ public extension LocalFileProvider {
|
||||
}
|
||||
|
||||
internal extension LocalFileProvider {
|
||||
func coordinated(intents: [NSFileAccessIntent], completionHandler: @escaping (_ url: URL) -> Void, errorHandler: ((_ error: Error) -> Void)? = nil) {
|
||||
func coordinated(intents: [NSFileAccessIntent], operationHandler: @escaping (_ url: URL) -> Void, errorHandler: ((_ error: Error) -> Void)? = nil) {
|
||||
let coordinator = NSFileCoordinator(filePresenter: nil)
|
||||
coordinator.coordinate(with: intents, queue: operation_queue) { (error) in
|
||||
if let error = error {
|
||||
errorHandler?(error)
|
||||
return
|
||||
}
|
||||
completionHandler(intents[0].url)
|
||||
operationHandler(intents.first!.url)
|
||||
}
|
||||
}
|
||||
|
||||
func coordinated(intents: [NSFileAccessIntent], moving: Bool = false, completionHandler: @escaping (_ sourceUrl: URL, _ destURL: URL?) -> Void, errorHandler: ((_ error: Error) -> Void)? = nil) {
|
||||
func coordinated(intents: [NSFileAccessIntent], moving: Bool = false, operationHandler: @escaping (_ sourceUrl: URL, _ destURL: URL?) -> Void, errorHandler: ((_ error: Error) -> Void)? = nil) {
|
||||
let coordinator = NSFileCoordinator(filePresenter: nil)
|
||||
coordinator.coordinate(with: intents, queue: operation_queue) { (error) in
|
||||
if let error = error {
|
||||
errorHandler?(error)
|
||||
return
|
||||
}
|
||||
let newSource: URL = intents[0].url
|
||||
let newDest: URL? = intents.count > 1 ? intents[1].url : nil
|
||||
guard let newSource: URL = intents.first?.url else { return }
|
||||
let newDest: URL? = intents.dropFirst().first?.url
|
||||
if moving, let newDest = newDest {
|
||||
coordinator.item(at: newSource, willMoveTo: newDest)
|
||||
}
|
||||
completionHandler(newSource, newDest)
|
||||
operationHandler(newSource, newDest)
|
||||
if moving, let newDest = newDest {
|
||||
coordinator.item(at: newSource, didMoveTo: newDest)
|
||||
}
|
||||
|
||||
+25
-127
@@ -8,26 +8,26 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Containts path, url and attributes of a local file or resource.
|
||||
public final class LocalFileObject: FileObject {
|
||||
internal override init(url: URL, name: String, path: String) {
|
||||
internal override init(url: URL?, name: String, path: String) {
|
||||
super.init(url: url, name: name, path: path)
|
||||
}
|
||||
|
||||
/// 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?.absoluteString ?? "", with: "", options: .anchored)
|
||||
if path.hasPrefix("/") {
|
||||
rpath.remove(at: rpath.startIndex)
|
||||
var rpath = path.replacingOccurrences(of: relativeURL?.path ?? "", with: "", options: .anchored)
|
||||
if relativeURL != nil && rpath.hasPrefix("/") {
|
||||
_=rpath.characters.removeFirst()
|
||||
}
|
||||
if rpath.isEmpty {
|
||||
fileURL = relativeURL
|
||||
if #available(iOS 9.0, macOS 10.11, tvOS 9.0, *) {
|
||||
fileURL = URL(fileURLWithPath: rpath, relativeTo: relativeURL)
|
||||
} else {
|
||||
if #available(iOS 9.0, macOS 10.11, tvOS 9.0, *) {
|
||||
fileURL = URL(fileURLWithPath: rpath, relativeTo: relativeURL)
|
||||
} else {
|
||||
fileURL = URL(string: rpath, relativeTo: relativeURL)
|
||||
}
|
||||
rpath = rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? rpath
|
||||
fileURL = URL(string: rpath, relativeTo: relativeURL) ?? relativeURL
|
||||
}
|
||||
|
||||
if let fileURL = fileURL {
|
||||
self.init(fileWithURL: fileURL)
|
||||
} else {
|
||||
@@ -35,9 +35,10 @@ public final class LocalFileObject: FileObject {
|
||||
}
|
||||
}
|
||||
|
||||
/// Initiates a `LocalFileObject` with attributes of file in url.
|
||||
public convenience init?(fileWithURL fileURL: URL) {
|
||||
do {
|
||||
let values = try fileURL.resourceValues(forKeys: [.nameKey, .fileSizeKey, .fileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .fileResourceTypeKey, .isHiddenKey, .isWritableKey, .typeIdentifierKey, .generationIdentifierKey, .documentIdentifierKey])
|
||||
let values = try fileURL.resourceValues(forKeys: [.nameKey, .fileSizeKey, .totalFileSizeKey, .fileAllocatedSizeKey, .totalFileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .fileResourceTypeKey, .isHiddenKey, .isWritableKey, .typeIdentifierKey, .generationIdentifierKey, .documentIdentifierKey])
|
||||
let path = fileURL.relativePath.hasPrefix("/") ? fileURL.relativePath : "/" + fileURL.relativePath
|
||||
|
||||
self.init(url: fileURL, name: values.name ?? fileURL.lastPathComponent, path: path)
|
||||
@@ -49,6 +50,7 @@ public final class LocalFileObject: FileObject {
|
||||
}
|
||||
}
|
||||
|
||||
/// The total size allocated on disk for the file
|
||||
open internal(set) var allocatedSize: Int64 {
|
||||
get {
|
||||
return allValues[.fileAllocatedSizeKey] as? Int64 ?? 0
|
||||
@@ -58,6 +60,9 @@ 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? {
|
||||
get {
|
||||
return allValues[.documentIdentifierKey] as? Int
|
||||
@@ -67,6 +72,8 @@ 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? {
|
||||
get {
|
||||
let data = allValues[.generationIdentifierKey] as? Data
|
||||
@@ -87,18 +94,21 @@ internal final class LocalFolderMonitor {
|
||||
init(url: URL, handler: @escaping ()->Void) {
|
||||
self.url = url
|
||||
descriptor = open((url as NSURL).fileSystemRepresentation, O_EVTONLY)
|
||||
source = DispatchSource.makeFileSystemObjectSource(fileDescriptor: descriptor, eventMask: DispatchSource.FileSystemEvent.write, queue: qq)
|
||||
source = DispatchSource.makeFileSystemObjectSource(fileDescriptor: descriptor, eventMask: .write, queue: qq)
|
||||
// Folder monitoring is recursive and deep. Monitoring a root folder may be very costly
|
||||
// We have a 0.2 second delay to ensure we wont call handler 1000s times when there is
|
||||
// a huge file operation. This ensures app will work smoothly while this 250 milisec won't
|
||||
// affect user experince much
|
||||
let main_handler: ()->Void = {
|
||||
let main_handler: ()->Void = { [weak self] in
|
||||
guard let `self` = self else { return }
|
||||
if Date().timeIntervalSinceReferenceDate < self.monitoredTime + 0.2 {
|
||||
return
|
||||
}
|
||||
self.monitoredTime = Date().timeIntervalSinceReferenceDate
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.25, execute: {
|
||||
self.source.suspend()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: {
|
||||
handler()
|
||||
self.source.resume()
|
||||
})
|
||||
}
|
||||
source.setEventHandler(handler: main_handler)
|
||||
@@ -207,104 +217,6 @@ internal class LocalFileProviderManagerDelegate: NSObject, FileManagerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
open class LocalOperationHandle: OperationHandle {
|
||||
public let baseURL: URL
|
||||
public let operationType: FileOperationType
|
||||
|
||||
init (operationType: FileOperationType, baseURL: URL?) {
|
||||
self.baseURL = baseURL ?? URL(fileURLWithPath: "/")
|
||||
self.operationType = operationType
|
||||
}
|
||||
|
||||
private var sourceURL: URL? {
|
||||
guard let source = operationType.source else { return nil }
|
||||
return source.hasPrefix("file://") ? URL(fileURLWithPath: source) : baseURL.appendingPathComponent(source)
|
||||
}
|
||||
|
||||
private var destURL: URL? {
|
||||
guard let dest = operationType.destination else { return nil }
|
||||
return dest.hasPrefix("file://") ? URL(fileURLWithPath: dest) : baseURL.appendingPathComponent(dest)
|
||||
}
|
||||
|
||||
/// Caution: may put pressure on CPU, may have latency
|
||||
open var bytesSoFar: Int64 {
|
||||
assert(!Thread.isMainThread, "Don't run \(#function) method on main thread")
|
||||
switch operationType {
|
||||
case .modify:
|
||||
guard let url = sourceURL, url.isFileURL else { return 0 }
|
||||
if url.fileIsDirectory {
|
||||
return iterateDirectory(url, deep: true).totalsize
|
||||
} else {
|
||||
return url.fileSize
|
||||
}
|
||||
case .copy, .move:
|
||||
guard let url = destURL, url.isFileURL else { return 0 }
|
||||
if url.fileIsDirectory {
|
||||
return iterateDirectory(url, deep: true).totalsize
|
||||
} else {
|
||||
return url.fileSize
|
||||
}
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Caution: may put pressure on CPU, may have latency
|
||||
open var totalBytes: Int64 {
|
||||
assert(!Thread.isMainThread, "Don't run \(#function) method on main thread")
|
||||
switch operationType {
|
||||
case .copy, .move:
|
||||
guard let url = sourceURL, url.isFileURL else { return 0 }
|
||||
if url.fileIsDirectory {
|
||||
return iterateDirectory(url, deep: true).totalsize
|
||||
} else {
|
||||
return url.fileSize
|
||||
}
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/// Not usable in local provider
|
||||
open var inProgress: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
/// Not usable in local provider
|
||||
open func cancel() -> Bool{
|
||||
return false
|
||||
}
|
||||
|
||||
func iterateDirectory(_ pathURL: URL, deep: Bool) -> (folders: Int, files: Int, totalsize: Int64) {
|
||||
var folders = 0
|
||||
var files = 0
|
||||
var totalsize: Int64 = 0
|
||||
let keys: [URLResourceKey] = [.isDirectoryKey, .fileSizeKey]
|
||||
let enumOpt: FileManager.DirectoryEnumerationOptions = !deep ? [.skipsSubdirectoryDescendants, .skipsPackageDescendants] : []
|
||||
|
||||
let fp = FileManager()
|
||||
let filesList = fp.enumerator(at: pathURL, includingPropertiesForKeys: keys, options: enumOpt, errorHandler: nil)
|
||||
while let fileURL = filesList?.nextObject() as? URL {
|
||||
do {
|
||||
let values = try fileURL.resourceValues(forKeys: [.isDirectoryKey, .fileSizeKey])
|
||||
let isdir = values.isDirectory ?? false
|
||||
let size = Int64(values.fileSize ?? 0)
|
||||
if isdir {
|
||||
folders += 1
|
||||
} else {
|
||||
files += 1
|
||||
}
|
||||
totalsize += size
|
||||
} catch _ {
|
||||
}
|
||||
}
|
||||
|
||||
return (folders, files, totalsize)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class UndoBox: NSObject {
|
||||
weak var provider: FileProvideUndoable?
|
||||
let operation: FileOperationType
|
||||
@@ -316,17 +228,3 @@ class UndoBox: NSObject {
|
||||
self.undoOperation = undoOperation
|
||||
}
|
||||
}
|
||||
|
||||
internal extension URL {
|
||||
var fileIsDirectory: Bool {
|
||||
return (try? self.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory ?? false
|
||||
}
|
||||
|
||||
var fileSize: Int64 {
|
||||
return Int64((try? self.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? -1)
|
||||
}
|
||||
|
||||
var fileExists: Bool {
|
||||
return self.isFileURL && FileManager.default.fileExists(atPath: self.path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,418 +0,0 @@
|
||||
|
||||
//
|
||||
// OneDriveFileProvider.swift
|
||||
// FileProvider
|
||||
//
|
||||
// Created by Amir Abbas Mousavian.
|
||||
// Copyright © 2017 Mousavian. Distributed under MIT license.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
|
||||
open class OneDriveFileProvider: FileProviderBasicRemote {
|
||||
open class var type: String { return "OneDrive" }
|
||||
open let isPathRelative: Bool
|
||||
open let baseURL: URL?
|
||||
/// OneDrive server url, equals with unwrapped `baseURL`
|
||||
open var serverURL: URL { return baseURL! }
|
||||
/// Drive name for user, default is `root`. Changing its value will effect on new operations.
|
||||
open var drive: String
|
||||
/// Generated storage url from server url and drive name
|
||||
open var driveURL: URL {
|
||||
return URL(string: "/drive/\(drive):/", relativeTo: baseURL)!
|
||||
}
|
||||
open var currentPath: String
|
||||
|
||||
open var dispatch_queue: DispatchQueue
|
||||
open var operation_queue: OperationQueue {
|
||||
willSet {
|
||||
assert(_session == nil, "It's not effective to change dispatch_queue property after session is initialized.")
|
||||
}
|
||||
}
|
||||
|
||||
open weak var delegate: FileProviderDelegate?
|
||||
open let credential: URLCredential?
|
||||
open private(set) var cache: URLCache?
|
||||
public var useCache: Bool
|
||||
public var validatingCache: Bool
|
||||
|
||||
fileprivate var _session: URLSession?
|
||||
fileprivate var sessionDelegate: SessionDelegate?
|
||||
public var session: URLSession {
|
||||
if _session == nil {
|
||||
self.sessionDelegate = SessionDelegate(fileProvider: self, credential: credential)
|
||||
let queue = OperationQueue()
|
||||
//queue.underlyingQueue = dispatch_queue
|
||||
let config = URLSessionConfiguration.default
|
||||
config.urlCache = cache
|
||||
config.requestCachePolicy = .returnCacheDataElseLoad
|
||||
_session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDelegate?, delegateQueue: queue)
|
||||
}
|
||||
return _session!
|
||||
}
|
||||
|
||||
/**
|
||||
Initializer for Onedrive provider with given client ID and Token.
|
||||
These parameters must be retrieved via [Authentication for the OneDrive API](https://dev.onedrive.com/auth/readme.htm).
|
||||
|
||||
There are libraries like [p2/OAuth2](https://github.com/p2/OAuth2) or [OAuthSwift](https://github.com/OAuthSwift/OAuthSwift) which can facilate the procedure to retrieve token.
|
||||
The latter is easier to use and prefered. Also you can use [auth0/Lock](https://github.com/auth0/Lock.iOS-OSX) which provides graphical user interface.
|
||||
|
||||
- Parameters:
|
||||
- credential: a `URLCredential` object with Client ID set as `user` and Token set as `password`.
|
||||
- serverURL: server url, Set it if you are trying to connect OneDrive Business server, otherwise leave it
|
||||
`nil` to connect to OneDrive Personal uses.
|
||||
- drive: drive name for user on server, default value is `root`.
|
||||
- cache: A URLCache to cache downloaded files and contents. If set to nil, URLCache.shared object will be used.
|
||||
*/
|
||||
public init(credential: URLCredential?, serverURL: URL? = nil, drive: String = "root", cache: URLCache? = nil) {
|
||||
self.baseURL = (serverURL ?? URL(string: "https://api.onedrive.com/")!).appendingPathComponent("")
|
||||
self.drive = drive
|
||||
self.isPathRelative = true
|
||||
self.currentPath = ""
|
||||
self.useCache = false
|
||||
self.validatingCache = true
|
||||
self.cache = cache
|
||||
self.credential = credential
|
||||
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: .concurrent)
|
||||
operation_queue = OperationQueue()
|
||||
operation_queue.name = "FileProvider.\(type(of: self).type).Operation"
|
||||
}
|
||||
|
||||
deinit {
|
||||
if fileProviderCancelTasksOnInvalidating {
|
||||
_session?.invalidateAndCancel()
|
||||
} else {
|
||||
_session?.finishTasksAndInvalidate()
|
||||
}
|
||||
}
|
||||
|
||||
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
|
||||
list(path) { (contents, cursor, error) in
|
||||
completionHandler(contents, error)
|
||||
}
|
||||
}
|
||||
|
||||
open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
|
||||
let url = URL(string: escaped(path: path), relativeTo: driveURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderOneDriveError?
|
||||
var fileObject: OneDriveFileObject?
|
||||
if let response = response as? HTTPURLResponse {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
|
||||
serverError = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
|
||||
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr), let file = OneDriveFileObject(baseURL: self.baseURL, drive: self.drive, json: json) {
|
||||
fileObject = file
|
||||
}
|
||||
}
|
||||
completionHandler(fileObject, serverError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
|
||||
open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) {
|
||||
let url = URL(string: "/drive/root", relativeTo: baseURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var totalSize: Int64 = -1
|
||||
var usedSize: Int64 = 0
|
||||
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr) {
|
||||
totalSize = (json["total"] as? NSNumber)?.int64Value ?? -1
|
||||
usedSize = (json["used"] as? NSNumber)?.int64Value ?? 0
|
||||
}
|
||||
completionHandler(totalSize, usedSize)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
|
||||
open weak var fileOperationDelegate: FileOperationDelegate?
|
||||
}
|
||||
|
||||
extension OneDriveFileProvider: FileProviderOperations {
|
||||
|
||||
|
||||
public func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let path = (atPath as NSString).appendingPathComponent(folderName) + "/"
|
||||
return doOperation(.create(path: path), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func create(file fileName: String, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let filePath = (path as NSString).appendingPathComponent(fileName)
|
||||
return self.writeContents(path: filePath, contents: data ?? Data(), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
return doOperation(.copy(source: path, destination: toPath), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
return doOperation(.remove(path: path), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
fileprivate func doOperation(_ operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
guard let sourcePath = operation.source else { return nil }
|
||||
let destPath = operation.destination
|
||||
var request = URLRequest(url: URL(string: sourcePath, relativeTo: driveURL)!)
|
||||
switch operation {
|
||||
case .create:
|
||||
request.httpMethod = "CREATE"
|
||||
case .copy:
|
||||
request.httpMethod = "POST"
|
||||
case .move:
|
||||
request.httpMethod = "PATCH"
|
||||
case .remove:
|
||||
request.httpMethod = "DELETE"
|
||||
default: // modify, link, fetch
|
||||
return nil
|
||||
}
|
||||
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
var requestDictionary = [String: AnyObject]()
|
||||
if let dest = correctPath(destPath) as NSString? {
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
requestDictionary["parentReference"] = ("/drive/\(drive):" + dest.deletingLastPathComponent) as NSString
|
||||
requestDictionary["name"] = dest.lastPathComponent as NSString
|
||||
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
|
||||
}
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderOneDriveError?
|
||||
if let response = response as? HTTPURLResponse, response.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) {
|
||||
serverError = FileProviderOneDriveError(code: code, path: sourcePath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
}
|
||||
completionHandler?(serverError ?? error)
|
||||
self.delegateNotify(operation, error: serverError ?? error)
|
||||
})
|
||||
task.taskDescription = operation.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(operationType: operation, tasks: [task])
|
||||
}
|
||||
|
||||
public func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
return upload_simple(toPath, localFile: localFile, overwrite: overwrite, operation: opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.copy(source: path, destination: destURL.absoluteString)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
let url = URL(string: escaped(path: path) + ":/content", relativeTo: driveURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
let task = session.downloadTask(with: request, completionHandler: { (cacheURL, response, error) in
|
||||
guard let cacheURL = cacheURL, let httpResponse = response as? HTTPURLResponse , httpResponse.statusCode < 300 else {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: (response as? HTTPURLResponse)?.statusCode ?? -1)
|
||||
let errorData : Data? = nil //Data(contentsOf:cacheURL) // TODO: Figure out how to get error response data for the error description
|
||||
let serverError : FileProviderOneDriveError? = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: errorData ?? Data(), encoding: .utf8)) : nil
|
||||
completionHandler?(serverError ?? error)
|
||||
return
|
||||
}
|
||||
do {
|
||||
try FileManager.default.moveItem(at: cacheURL, to: destURL)
|
||||
completionHandler?(nil)
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
}
|
||||
})
|
||||
task.taskDescription = opType.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(operationType: opType, tasks: [task])
|
||||
}
|
||||
}
|
||||
|
||||
extension OneDriveFileProvider: FileProviderReadWrite {
|
||||
public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
if length == 0 || offset < 0 {
|
||||
dispatch_queue.async {
|
||||
completionHandler(Data(), nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
let opType = FileOperationType.fetch(path: path)
|
||||
let url = URL(string: escaped(path: path) + ":/content", relativeTo: driveURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
if length > 0 {
|
||||
request.setValue("bytes=\(offset)-\(offset + length - 1)", forHTTPHeaderField: "Range")
|
||||
} else if offset > 0 && length < 0 {
|
||||
request.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
|
||||
}
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderOneDriveError?
|
||||
if let httpResponse = response as? HTTPURLResponse , httpResponse.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: httpResponse.statusCode) {
|
||||
serverError = FileProviderOneDriveError(code: code, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
}
|
||||
let filedata = serverError ?? error == nil ? data : nil
|
||||
completionHandler(filedata, serverError ?? error)
|
||||
})
|
||||
task.taskDescription = opType.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(operationType: opType, tasks: [task])
|
||||
}
|
||||
|
||||
public func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.modify(path: path)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
// FIXME: remove 150MB restriction
|
||||
return upload_simple(path, data: data, overwrite: overwrite, operation: opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
|
||||
var foundFiles = [OneDriveFileObject]()
|
||||
search(path, query: query, foundItem: { (file) in
|
||||
foundFiles.append(file)
|
||||
foundItemHandler?(file)
|
||||
}, completionHandler: { (error) in
|
||||
completionHandler(foundFiles, error)
|
||||
})
|
||||
}
|
||||
|
||||
fileprivate func registerNotifcation(path: String, eventHandler: (() -> Void)) {
|
||||
/* There is two ways to monitor folders changing in OneDrive. Either using webooks
|
||||
* which means you have to implement a server to translate it to push notifications
|
||||
* or using apiv2 list_folder/longpoll method. The second one is implemeted here.
|
||||
* Tough webhooks are much more efficient, longpoll is much simpler to implement!
|
||||
* You can implemnt your own webhook service and replace this method accordingly.
|
||||
*/
|
||||
NotImplemented()
|
||||
}
|
||||
fileprivate func unregisterNotifcation(path: String) {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
Genrates a public url to a file to be shared with other users and can be downloaded without authentication.
|
||||
|
||||
- Parameters:
|
||||
- to: path of file, including file/directory name.
|
||||
- completionHandler: a block with result of directory entries or error.
|
||||
`link`: a url returned by OneDrive to share.
|
||||
`attribute`: `nil` for OneDrive.
|
||||
`expiration`: `nil` for OneDrive, as it doesn't expires.
|
||||
`error`: Error returned by OneDrive.
|
||||
*/
|
||||
open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: OneDriveFileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
|
||||
let url = URL(string: escaped(path: path) + ":/action.createLink", relativeTo: driveURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
let requestDictionary: [String: AnyObject] = ["type": "view" as NSString]
|
||||
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderOneDriveError?
|
||||
var link: URL?
|
||||
if let response = response as? HTTPURLResponse {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
|
||||
serverError = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
|
||||
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr) {
|
||||
if let linkDic = json["link"] as? NSDictionary, let linkStr = linkDic["webUrl"] as? String {
|
||||
link = URL(string: linkStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
completionHandler(link, nil, nil, serverError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension OneDriveFileProvider: ExtendedFileProvider {
|
||||
public func thumbnailOfFileSupported(path: String) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public func propertiesOfFileSupported(path: String) -> Bool {
|
||||
let fileExt = (path as NSString).pathExtension.lowercased()
|
||||
switch fileExt {
|
||||
case "jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff":
|
||||
return true
|
||||
case "mp3", "aac", "m4a", "wma":
|
||||
return true
|
||||
case "mp4", "mpg", "3gp", "mov", "avi", "wmv":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) {
|
||||
let url: URL
|
||||
if let dimension = dimension {
|
||||
url = URL(string: escaped(path: path) + ":/thumbnails/0/=c\(dimension.width)x\(dimension.height)/content", relativeTo: driveURL)!
|
||||
} else {
|
||||
url = URL(string: escaped(path: path) + ":/thumbnails/0/small/content", relativeTo: driveURL)!
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
let task = self.session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var image: ImageClass? = nil
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
let responseError = FileProviderOneDriveError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
completionHandler(nil, responseError)
|
||||
return
|
||||
}
|
||||
if let data = data {
|
||||
image = ImageClass(data: data)
|
||||
}
|
||||
completionHandler(image, error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
|
||||
public func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) {
|
||||
let url = URL(string: escaped(path: path), relativeTo: driveURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderOneDriveError?
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
if let response = response as? HTTPURLResponse {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
|
||||
serverError = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
|
||||
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr) {
|
||||
(dic, keys) = self.mapMediaInfo(json)
|
||||
}
|
||||
}
|
||||
completionHandler(dic, keys, serverError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
||||
extension OneDriveFileProvider: FileProvider {
|
||||
open func copy(with zone: NSZone? = nil) -> Any {
|
||||
let copy = OneDriveFileProvider(credential: self.credential, serverURL: self.baseURL, drive: self.drive, cache: self.cache)
|
||||
copy.currentPath = self.currentPath
|
||||
copy.delegate = self.delegate
|
||||
copy.fileOperationDelegate = self.fileOperationDelegate
|
||||
copy.useCache = self.useCache
|
||||
copy.validatingCache = self.validatingCache
|
||||
return copy
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
|
||||
//
|
||||
// OneDriveFileProvider.swift
|
||||
// FileProvider
|
||||
//
|
||||
// Created by Amir Abbas Mousavian.
|
||||
// Copyright © 2017 Mousavian. Distributed under MIT license.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
|
||||
/**
|
||||
Allows accessing to OneDrive stored files, either hosted on Microsoft servers or business coprporate one.
|
||||
This provider doesn't cache or save files internally, however you can set `useCache` and `cache` properties
|
||||
to use Foundation `NSURLCache` system.
|
||||
|
||||
- Note: Uploading files and data are limited to 100MB, for now.
|
||||
*/
|
||||
open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
override open class var type: String { return "OneDrive" }
|
||||
/// Drive name for user, default is `root`. Changing its value will effect on new operations.
|
||||
open var drive: String
|
||||
|
||||
/**
|
||||
Initializer for Onedrive provider with given client ID and Token.
|
||||
These parameters must be retrieved via [Authentication for the OneDrive API](https://dev.onedrive.com/auth/readme.htm).
|
||||
|
||||
There are libraries like [p2/OAuth2](https://github.com/p2/OAuth2) or [OAuthSwift](https://github.com/OAuthSwift/OAuthSwift) which can facilate the procedure to retrieve token.
|
||||
The latter is easier to use and prefered. Also you can use [auth0/Lock](https://github.com/auth0/Lock.iOS-OSX) which provides graphical user interface.
|
||||
|
||||
- Parameters:
|
||||
- credential: a `URLCredential` object with Client ID set as `user` and Token set as `password`.
|
||||
- serverURL: server url, Set it if you are trying to connect OneDrive Business server, otherwise leave it
|
||||
`nil` to connect to OneDrive Personal uses.
|
||||
- drive: drive name for user on server, default value is `root`.
|
||||
- cache: A URLCache to cache downloaded files and contents.
|
||||
*/
|
||||
public init(credential: URLCredential?, serverURL: URL? = nil, drive: String = "root", cache: URLCache? = nil) {
|
||||
let baseURL = serverURL?.absoluteURL ?? URL(string: "https://api.onedrive.com/")!
|
||||
let refinedBaseURL = baseURL.absoluteString.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")
|
||||
self.drive = drive
|
||||
super.init(baseURL: refinedBaseURL, credential: credential, cache: cache)
|
||||
}
|
||||
|
||||
public required convenience init?(coder aDecoder: NSCoder) {
|
||||
self.init(credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential,
|
||||
serverURL: aDecoder.decodeObject(forKey: "baseURL") as? URL,
|
||||
drive: aDecoder.decodeObject(forKey: "drive") as? String ?? "root")
|
||||
self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? ""
|
||||
self.useCache = aDecoder.decodeBool(forKey: "useCache")
|
||||
self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache")
|
||||
}
|
||||
|
||||
open override func encode(with aCoder: NSCoder) {
|
||||
super.encode(with: aCoder)
|
||||
aCoder.encode(self.drive, forKey: "drive")
|
||||
}
|
||||
|
||||
open override func copy(with zone: NSZone? = nil) -> Any {
|
||||
let copy = OneDriveFileProvider(credential: self.credential, serverURL: self.baseURL, drive: self.drive, cache: self.cache)
|
||||
copy.currentPath = self.currentPath
|
||||
copy.delegate = self.delegate
|
||||
copy.fileOperationDelegate = self.fileOperationDelegate
|
||||
copy.useCache = self.useCache
|
||||
copy.validatingCache = self.validatingCache
|
||||
return copy
|
||||
}
|
||||
|
||||
open override func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
|
||||
list(path) { (contents, cursor, error) in
|
||||
completionHandler(contents, error)
|
||||
}
|
||||
}
|
||||
|
||||
open override func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
|
||||
var request = URLRequest(url: url(of: path))
|
||||
request.httpMethod = "GET"
|
||||
request.set(httpAuthentication: credential, with: .oAuth2)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderOneDriveError?
|
||||
var fileObject: OneDriveFileObject?
|
||||
if let response = response as? HTTPURLResponse {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
|
||||
serverError = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
|
||||
if let json = data?.deserializeJSON(), let file = OneDriveFileObject(baseURL: self.baseURL, drive: self.drive, json: json) {
|
||||
fileObject = file
|
||||
}
|
||||
}
|
||||
completionHandler(fileObject, serverError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
|
||||
open override func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) {
|
||||
var request = URLRequest(url: url(of: ""))
|
||||
request.httpMethod = "GET"
|
||||
request.set(httpAuthentication: credential, with: .oAuth2)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var totalSize: Int64 = -1
|
||||
var usedSize: Int64 = 0
|
||||
if let json = data?.deserializeJSON() {
|
||||
totalSize = (json["total"] as? NSNumber)?.int64Value ?? -1
|
||||
usedSize = (json["used"] as? NSNumber)?.int64Value ?? 0
|
||||
}
|
||||
completionHandler(totalSize, usedSize)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
|
||||
open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? {
|
||||
var foundFiles = [OneDriveFileObject]()
|
||||
var queryStr: String?
|
||||
queryStr = query.findValue(forKey: "name") as? String ?? query.findAllValues(forKey: nil).flatMap { $0.value as? String }.first
|
||||
guard let finalQueryStr = queryStr else { return nil }
|
||||
let progress = Progress(parent: nil, userInfo: nil)
|
||||
progress.setUserInfoObject(url(of: path), forKey: .fileURLKey)
|
||||
search(path, query: finalQueryStr, recursive: recursive, progress: progress, foundItem: { (file) in
|
||||
if query.evaluate(with: file.mapPredicate()) {
|
||||
foundFiles.append(file)
|
||||
foundItemHandler?(file)
|
||||
}
|
||||
}, completionHandler: { (error) in
|
||||
completionHandler(foundFiles, error)
|
||||
})
|
||||
return progress
|
||||
}
|
||||
|
||||
open func url(of path: String, modifier: String? = nil) -> URL {
|
||||
var rpath: String = path
|
||||
|
||||
let driveURL = baseURL!.appendingPathComponent("drive/\(drive):/")
|
||||
|
||||
if rpath.hasPrefix("/") {
|
||||
_=rpath.characters.removeFirst()
|
||||
}
|
||||
if rpath.isEmpty {
|
||||
if let modifier = modifier {
|
||||
return driveURL.appendingPathComponent(modifier)
|
||||
}
|
||||
return driveURL
|
||||
}
|
||||
|
||||
rpath = rpath.trimmingCharacters(in: pathTrimSet)
|
||||
if let modifier = modifier {
|
||||
rpath = rpath + ":/" + modifier
|
||||
}
|
||||
|
||||
return driveURL.appendingPathComponent(rpath)
|
||||
}
|
||||
|
||||
open override func isReachable(completionHandler: @escaping (Bool) -> Void) {
|
||||
var request = URLRequest(url: url(of: ""))
|
||||
request.httpMethod = "HEAD"
|
||||
request.set(httpAuthentication: credential, with: .oAuth2)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
let status = (response as? HTTPURLResponse)?.statusCode ?? 400
|
||||
completionHandler(status == 200)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
|
||||
override func request(for operation: FileOperationType, overwrite: Bool = false, attributes: [URLResourceKey : Any] = [:]) -> URLRequest {
|
||||
let method: String
|
||||
let url: URL
|
||||
switch operation {
|
||||
case .fetch(path: let path):
|
||||
method = "GET"
|
||||
url = self.url(of: path, modifier: "content")
|
||||
case .modify(path: let path):
|
||||
method = "PUT"
|
||||
let queryStr = overwrite ? "" : "?@name.conflictBehavior=fail"
|
||||
url = self.url(of: path, modifier: "content\(queryStr)")
|
||||
case .create(path: let path):
|
||||
method = "CREATE"
|
||||
url = self.url(of: path)
|
||||
case .copy(let source, let dest) where !source.hasPrefix("file://") && !dest.hasPrefix("file://"):
|
||||
method = "POST"
|
||||
url = self.url(of: source)
|
||||
case .copy(let source, let dest) where source.hasPrefix("file://"):
|
||||
method = "PUT"
|
||||
let queryStr = overwrite ? "" : "?@name.conflictBehavior=fail"
|
||||
url = self.url(of: dest, modifier: "content\(queryStr)")
|
||||
case .copy(let source, let dest) where dest.hasPrefix("file://"):
|
||||
method = "GET"
|
||||
url = self.url(of: source, modifier: "content")
|
||||
case .move(source: let source, destination: _):
|
||||
method = "PATCH"
|
||||
url = self.url(of: source)
|
||||
case .remove(path: let path):
|
||||
method = "DELETE"
|
||||
url = self.url(of: path)
|
||||
default: // link
|
||||
fatalError("Unimplemented operation \(operation.description) in \(#file)")
|
||||
}
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = method
|
||||
request.set(httpAuthentication: credential, with: .oAuth2)
|
||||
|
||||
switch operation {
|
||||
case .copy(let source, let dest) where !source.hasPrefix("file://") && !dest.hasPrefix("file://"),
|
||||
.move(source: let source, destination: let dest):
|
||||
request.set(httpContentType: .json)
|
||||
let cdest = (correctPath(dest) as NSString?)!
|
||||
var requestDictionary = [String: AnyObject]()
|
||||
requestDictionary["parentReference"] = ("/drive/\(drive):" + cdest.deletingLastPathComponent) as NSString
|
||||
requestDictionary["name"] = cdest.lastPathComponent as NSString
|
||||
request.httpBody = Data(jsonDictionary: requestDictionary)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
return request
|
||||
}
|
||||
|
||||
override func serverError(with code: FileProviderHTTPErrorCode, path: String?, data: Data?) -> FileProviderHTTPError {
|
||||
return FileProviderOneDriveError(code: code, path: path ?? "", errorDescription: data.flatMap({ String(data: $0, encoding: .utf8) }))
|
||||
}
|
||||
|
||||
override func upload_simple(_ targetPath: String, request: URLRequest, data: Data?, localFile: URL?, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let size = data?.count ?? Int((try? localFile?.resourceValues(forKeys: [.fileSizeKey]))??.fileSize ?? -1)
|
||||
if size > 100 * 1024 * 1024 {
|
||||
let error = FileProviderOneDriveError(code: .payloadTooLarge, path: targetPath, errorDescription: nil)
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
return nil
|
||||
}
|
||||
|
||||
return super.upload_simple(targetPath, request: request, data: data, localFile: localFile, operation: operation, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
fileprivate func registerNotifcation(path: String, eventHandler: (() -> Void)) {
|
||||
/* There is two ways to monitor folders changing in OneDrive. Either using webooks
|
||||
* which means you have to implement a server to translate it to push notifications
|
||||
* or using apiv2 list_folder/longpoll method. The second one is implemeted here.
|
||||
* Tough webhooks are much more efficient, longpoll is much simpler to implement!
|
||||
* You can implemnt your own webhook service and replace this method accordingly.
|
||||
*/
|
||||
NotImplemented()
|
||||
}
|
||||
fileprivate func unregisterNotifcation(path: String) {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
|
||||
var request = URLRequest(url: self.url(of: path, modifier: "action.createLink"))
|
||||
request.httpMethod = "POST"
|
||||
let requestDictionary: [String: AnyObject] = ["type": "view" as NSString]
|
||||
request.httpBody = Data(jsonDictionary: requestDictionary)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderOneDriveError?
|
||||
var link: URL?
|
||||
if let response = response as? HTTPURLResponse {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
|
||||
serverError = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
|
||||
if let json = data?.deserializeJSON() {
|
||||
if let linkDic = json["link"] as? NSDictionary, let linkStr = linkDic["webUrl"] as? String {
|
||||
link = URL(string: linkStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
completionHandler(link, nil, nil, serverError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension OneDriveFileProvider: ExtendedFileProvider {
|
||||
open func thumbnailOfFileSupported(path: String) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
open func propertiesOfFileSupported(path: String) -> Bool {
|
||||
let fileExt = (path as NSString).pathExtension.lowercased()
|
||||
switch fileExt {
|
||||
case "jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff":
|
||||
return true
|
||||
case "mp3", "aac", "m4a", "wma":
|
||||
return true
|
||||
case "mp4", "mpg", "3gp", "mov", "avi", "wmv":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
open func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) {
|
||||
let url: URL
|
||||
if let dimension = dimension {
|
||||
url = self.url(of: path, modifier: "thumbnails/0/=c\(dimension.width)x\(dimension.height)/content")
|
||||
} else {
|
||||
url = self.url(of: path, modifier: "thumbnails/0/small/content")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.set(httpAuthentication: credential, with: .oAuth2)
|
||||
let task = self.session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var image: ImageClass? = nil
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
let responseError = FileProviderOneDriveError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
completionHandler(nil, responseError)
|
||||
return
|
||||
}
|
||||
if let data = data {
|
||||
image = ImageClass(data: data)
|
||||
}
|
||||
completionHandler(image, error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
|
||||
open func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) {
|
||||
var request = URLRequest(url: url(of: path))
|
||||
request.httpMethod = "GET"
|
||||
request.set(httpAuthentication: credential, with: .oAuth2)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderOneDriveError?
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
if let response = response as? HTTPURLResponse {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
|
||||
serverError = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
|
||||
if let json = data?.deserializeJSON() {
|
||||
(dic, keys) = self.mapMediaInfo(json)
|
||||
}
|
||||
}
|
||||
completionHandler(dic, keys, serverError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
+52
-110
@@ -8,40 +8,48 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Error returned by OneDrive server when trying to access or do operations on a file or folder.
|
||||
public struct FileProviderOneDriveError: FileProviderHTTPError {
|
||||
public let code: FileProviderHTTPErrorCode
|
||||
public let path: String
|
||||
public let errorDescription: String?
|
||||
}
|
||||
|
||||
/// Containts path, url and attributes of a OneDrive file or resource.
|
||||
public final class OneDriveFileObject: FileObject {
|
||||
internal init(baseURL: URL?, name: String, path: String) {
|
||||
var rpath = path
|
||||
if path.hasPrefix("/") {
|
||||
rpath.remove(at: rpath.startIndex)
|
||||
var rpath = (URL(string:path)?.appendingPathComponent(name).absoluteString)!
|
||||
if rpath.hasPrefix("/") {
|
||||
_=rpath.characters.removeFirst()
|
||||
}
|
||||
let url = URL(string: rpath, relativeTo: baseURL) ?? URL(string: path)!
|
||||
super.init(url: url, name: name, path: path)
|
||||
let url = URL(string: rpath, relativeTo: baseURL) ?? URL(string: rpath)!
|
||||
|
||||
super.init(url: url, name: name, path: rpath.removingPercentEncoding ?? path)
|
||||
}
|
||||
|
||||
internal convenience init? (baseURL: URL?, drive: String, jsonStr: String) {
|
||||
guard let json = jsonToDictionary(jsonStr) else { return nil }
|
||||
guard let json = jsonStr.deserializeJSON() else { return nil }
|
||||
self.init(baseURL: baseURL, drive: drive, json: json)
|
||||
}
|
||||
|
||||
internal convenience init? (baseURL: URL?, drive: String, json: [String: AnyObject]) {
|
||||
guard let name = json["name"] as? String else { return nil }
|
||||
guard let path = (json["parentReference"] as? NSDictionary)?["path"] as? String else { return nil }
|
||||
let lPath = path.replacingOccurrences(of: "/drive/\(drive):", with: "/", options: .anchored, range: nil)
|
||||
var lPath = path.replacingOccurrences(of: "/drive/\(drive):", with: "/", options: .anchored, range: nil)
|
||||
lPath = lPath.replacingOccurrences(of: "/:", with: "", options: .anchored)
|
||||
lPath = lPath.replacingOccurrences(of: "//", with: "", options: .anchored)
|
||||
self.init(baseURL: baseURL, name: name, path: lPath)
|
||||
self.size = (json["size"] as? NSNumber)?.int64Value ?? -1
|
||||
self.modifiedDate = resolve(dateString: json["lastModifiedDateTime"] as? String ?? "")
|
||||
self.creationDate = resolve(dateString: json["createdDateTime"] as? String ?? "")
|
||||
self.type = (json["folder"] as? String) != nil ? .directory : .regular
|
||||
self.modifiedDate = Date(rfcString: json["lastModifiedDateTime"] as? String ?? "")
|
||||
self.creationDate = Date(rfcString: json["createdDateTime"] as? String ?? "")
|
||||
self.type = json["folder"] != nil ? .directory : .regular
|
||||
self.id = json["id"] as? String
|
||||
self.entryTag = json["eTag"] as? String
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// The identifier persists across system restarts.
|
||||
open internal(set) var id: String? {
|
||||
get {
|
||||
return allValues[.documentIdentifierKey] as? String
|
||||
@@ -51,52 +59,48 @@ public final class OneDriveFileObject: FileObject {
|
||||
}
|
||||
}
|
||||
|
||||
/// MIME type of file contents returned by OneDrive server.
|
||||
open internal(set) var contentType: String {
|
||||
get {
|
||||
return allValues[.mimeType] as? String ?? ""
|
||||
return allValues[.mimeTypeKey] as? String ?? ""
|
||||
}
|
||||
set {
|
||||
allValues[.mimeType] = newValue
|
||||
allValues[.mimeTypeKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP E-Tag, can be used to mark changed files.
|
||||
open internal(set) var entryTag: String? {
|
||||
get {
|
||||
return allValues[.entryTag] as? String
|
||||
return allValues[.entryTagKey] as? String
|
||||
}
|
||||
set {
|
||||
allValues[.entryTag] = newValue
|
||||
allValues[.entryTagKey] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// codebeat:disable[ARITY]
|
||||
internal extension OneDriveFileProvider {
|
||||
func list(_ path: String, cursor: String? = nil, prevContents: [OneDriveFileObject] = [], completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) {
|
||||
let url: URL
|
||||
if let cursor = cursor {
|
||||
url = URL(string: cursor)!
|
||||
} else {
|
||||
url = URL(string: escaped(path: path), relativeTo: driveURL)!
|
||||
}
|
||||
func list(_ path: String, cursor: URL? = nil, prevContents: [OneDriveFileObject] = [], completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) {
|
||||
let url = cursor ?? self.url(of: path, modifier: "children")
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.set(httpAuthentication: credential, with: .oAuth2)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var responseError: FileProviderOneDriveError?
|
||||
var files = prevContents
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderOneDriveError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
}
|
||||
if let data = data, let jsonStr = String(data: data, encoding: .utf8) {
|
||||
let json = jsonToDictionary(jsonStr)
|
||||
if let entries = json?["value"] as? [AnyObject] , entries.count > 0 {
|
||||
if let json = data?.deserializeJSON() {
|
||||
if let entries = json["value"] as? [AnyObject] , entries.count > 0 {
|
||||
for entry in entries {
|
||||
if let entry = entry as? [String: AnyObject], let file = OneDriveFileObject(baseURL: self.baseURL, drive: self.drive, json: entry) {
|
||||
files.append(file)
|
||||
}
|
||||
}
|
||||
let ncursor = json?["@odata.nextLink"] as? String
|
||||
let ncursor: URL? = (json["@odata.nextLink"] as? String).flatMap { URL(string: $0) }
|
||||
let hasmore = ncursor != nil
|
||||
if hasmore {
|
||||
self.list(path, cursor: ncursor, prevContents: files, completionHandler: completionHandler)
|
||||
@@ -110,90 +114,34 @@ internal extension OneDriveFileProvider {
|
||||
task.resume()
|
||||
}
|
||||
|
||||
func upload_simple(_ targetPath: String, data: Data, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
if data.count > 100 * 1024 * 1024 {
|
||||
let error = FileProviderOneDriveError(code: .payloadTooLarge, path: targetPath, errorDescription: nil)
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(.create(path: targetPath), error: error)
|
||||
return nil
|
||||
func search(_ startPath: String = "", query: String, recursive: Bool, next: URL? = nil, progress: Progress, foundItem: @escaping ((_ file: OneDriveFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) {
|
||||
if progress.isCancelled {
|
||||
return
|
||||
}
|
||||
let queryStr = overwrite ? "" : "?@name.conflictBehavior=fail"
|
||||
let url = URL(string: escaped(path: targetPath) + ":/content" + queryStr, relativeTo: driveURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PUT"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
|
||||
request.httpBody = data
|
||||
let task = session.uploadTask(with: request, from: data, completionHandler: { (data, response, error) in
|
||||
var responseError: FileProviderOneDriveError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderOneDriveError(code: rCode, path: targetPath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
}
|
||||
completionHandler?(responseError ?? error)
|
||||
self.delegateNotify(.create(path: targetPath), error: responseError ?? error)
|
||||
})
|
||||
task.taskDescription = operation.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(operationType: operation, tasks: [task])
|
||||
}
|
||||
|
||||
func upload_simple(_ targetPath: String, localFile: URL, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let size = (try? localFile.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? -1
|
||||
if size > 100 * 1024 * 1024 {
|
||||
let error = FileProviderOneDriveError(code: .payloadTooLarge, path: targetPath, errorDescription: nil)
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(.create(path: targetPath), error: error)
|
||||
return nil
|
||||
}
|
||||
let queryStr = overwrite ? "" : "?@name.conflictBehavior=fail"
|
||||
let url = URL(string: escaped(path: targetPath) + ":/content" + queryStr, relativeTo: driveURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PUT"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
|
||||
let task = session.uploadTask(with: request, fromFile: localFile, completionHandler: { (data, response, error) in
|
||||
var responseError: FileProviderOneDriveError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderOneDriveError(code: rCode, path: targetPath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
}
|
||||
completionHandler?(responseError ?? error)
|
||||
self.delegateNotify(.create(path: targetPath), error: responseError ?? error)
|
||||
})
|
||||
task.taskDescription = operation.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(operationType: operation, tasks: [task])
|
||||
}
|
||||
|
||||
func search(_ startPath: String = "", query: String, next: String? = nil, foundItem:@escaping ((_ file: OneDriveFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) {
|
||||
|
||||
let url: URL
|
||||
if let next = next {
|
||||
url = URL(string: next)!
|
||||
} else if self.escaped(path: startPath) == "" {
|
||||
url = URL(string: "/drive/\(drive)/view.search?q=\(query)", relativeTo: baseURL)!
|
||||
} else {
|
||||
url = URL(string: "\(escaped(path: startPath))/view.search?q=\(query)", relativeTo: driveURL)!
|
||||
}
|
||||
let q = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
|
||||
let expanded = recursive ? "&expand=children" : ""
|
||||
url = next ?? self.url(of: startPath, modifier: "view.search?q=\(q)\(expanded)")
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.set(httpAuthentication: credential, with: .oAuth2)
|
||||
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var responseError: FileProviderOneDriveError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderOneDriveError(code: rCode, path: startPath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
}
|
||||
if let data = data, let jsonStr = String(data: data, encoding: .utf8) {
|
||||
let json = jsonToDictionary(jsonStr)
|
||||
if let entries = json?["value"] as? [AnyObject] , entries.count > 0 {
|
||||
if let json = data?.deserializeJSON() {
|
||||
if let entries = json["value"] as? [AnyObject] , entries.count > 0 {
|
||||
for entry in entries {
|
||||
if let entry = entry as? [String: AnyObject], let file = OneDriveFileObject(baseURL: self.baseURL, drive: self.drive, json: entry) {
|
||||
foundItem(file)
|
||||
}
|
||||
}
|
||||
let next = json?["@odata.nextLink"] as? String
|
||||
let hasmore = next != nil
|
||||
if hasmore, let next = next {
|
||||
self.search(startPath, query: query, next: next, foundItem: foundItem, completionHandler: completionHandler)
|
||||
let next: URL? = (json["@odata.nextLink"] as? String).flatMap { URL(string: $0) }
|
||||
if !progress.isCancelled, let next = next {
|
||||
self.search(startPath, query: query, recursive: recursive, next: next, progress: progress, foundItem: foundItem, completionHandler: completionHandler)
|
||||
} else {
|
||||
completionHandler(responseError ?? error)
|
||||
}
|
||||
@@ -201,7 +149,11 @@ internal extension OneDriveFileProvider {
|
||||
}
|
||||
}
|
||||
completionHandler(responseError ?? error)
|
||||
})
|
||||
})
|
||||
progress.cancellationHandler = { [weak task] in
|
||||
task?.cancel()
|
||||
}
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
@@ -244,14 +196,14 @@ internal extension OneDriveFileProvider {
|
||||
if let location = json["location"] as? [String: Any], let latitude = location["latitude"] as? Double, let longitude = location["longitude"] as? Double {
|
||||
OneDriveFileProvider.decimalFormatter.numberStyle = .decimal
|
||||
OneDriveFileProvider.decimalFormatter.maximumFractionDigits = 5
|
||||
let latStr = OneDriveFileProvider.decimalFormatter.string(from: NSNumber(value: latitude))
|
||||
let longStr = OneDriveFileProvider.decimalFormatter.string(from: NSNumber(value: longitude))
|
||||
let latStr = OneDriveFileProvider.decimalFormatter.string(from: NSNumber(value: latitude))!
|
||||
let longStr = OneDriveFileProvider.decimalFormatter.string(from: NSNumber(value: longitude))!
|
||||
add(key: "Location", value: "\(latStr), \(longStr)")
|
||||
}
|
||||
if let parent = json["image"] as? [String: Any] ?? json["video"] as? [String: Any], let duration = parent["duration"] as? UInt64 {
|
||||
add(key: "Duration", value: OneDriveFileProvider.formatshort(interval: TimeInterval(duration) / 1000))
|
||||
add(key: "Duration", value: (TimeInterval(duration) / 1000).formatshort)
|
||||
}
|
||||
if let timeTakenStr = json["takenDateTime"] as? String, let timeTaken = resolve(dateString: timeTakenStr) {
|
||||
if let timeTakenStr = json["takenDateTime"] as? String, let timeTaken = Date(rfcString: timeTakenStr) {
|
||||
OneDriveFileProvider.dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
add(key: "Date taken", value: OneDriveFileProvider.dateFormatter.string(from: timeTaken))
|
||||
}
|
||||
@@ -279,14 +231,4 @@ internal extension OneDriveFileProvider {
|
||||
|
||||
return (dic, keys)
|
||||
}
|
||||
|
||||
func delegateNotify(_ operation: FileOperationType, error: Error?) {
|
||||
DispatchQueue.main.async(execute: {
|
||||
if error == nil {
|
||||
self.delegate?.fileproviderSucceed(self, operation: operation)
|
||||
} else {
|
||||
self.delegate?.fileproviderFailed(self, operation: operation)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Regular → Executable
+231
-103
@@ -8,211 +8,338 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
open class RemoteOperationHandle: OperationHandle {
|
||||
|
||||
internal var tasks: [Weak<URLSessionTask>]
|
||||
|
||||
open private(set) var operationType: FileOperationType
|
||||
|
||||
init(operationType: FileOperationType, tasks: [URLSessionTask]) {
|
||||
self.operationType = operationType
|
||||
self.tasks = tasks.map { Weak<URLSessionTask>($0) }
|
||||
}
|
||||
|
||||
internal func add(task: URLSessionTask) {
|
||||
tasks.append(Weak<URLSessionTask>(task))
|
||||
}
|
||||
|
||||
private func reape() {
|
||||
self.tasks = tasks.filter { $0.value != nil }
|
||||
}
|
||||
|
||||
open var bytesSoFar: Int64 {
|
||||
return tasks.reduce(0) {
|
||||
if let task = $1.value as? URLSessionUploadTask {
|
||||
return $0 + task.countOfBytesSent
|
||||
} else {
|
||||
return $0 + ($1.value?.countOfBytesReceived ?? 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open var totalBytes: Int64 {
|
||||
return tasks.reduce(0) {
|
||||
if let task = $1.value as? URLSessionUploadTask {
|
||||
return $0 + task.countOfBytesExpectedToSend
|
||||
} else {
|
||||
return $0 + ($1.value?.countOfBytesExpectedToSend ?? 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open func cancel() -> Bool {
|
||||
var canceled = false
|
||||
for taskbox in tasks {
|
||||
taskbox.value?.cancel()
|
||||
canceled = true
|
||||
}
|
||||
return canceled
|
||||
}
|
||||
|
||||
open var inProgress: Bool {
|
||||
return tasks.reduce(false) { $0 || $1.value?.state ?? .canceling == .running }
|
||||
}
|
||||
}
|
||||
|
||||
/// A protocol defines properties for errors returned by HTTP/S based providers.
|
||||
/// Including Dropbox, OneDrive and WebDAV.
|
||||
public protocol FileProviderHTTPError: Error, CustomStringConvertible {
|
||||
/// HTTP status codes as an enum.
|
||||
typealias Code = FileProviderHTTPErrorCode
|
||||
/// HTTP status code returned for error by server.
|
||||
var code: FileProviderHTTPErrorCode { get }
|
||||
/// Path of file/folder casued that error
|
||||
var path: String { get }
|
||||
/// Contents returned by server as error description
|
||||
var errorDescription: String? { get }
|
||||
|
||||
var description: String { get }
|
||||
}
|
||||
|
||||
extension FileProviderHTTPError {
|
||||
public var description: String {
|
||||
return code.description
|
||||
}
|
||||
|
||||
public var localizedDescription: String {
|
||||
return description
|
||||
}
|
||||
}
|
||||
|
||||
class SessionDelegate: NSObject, URLSessionDataDelegate, URLSessionDownloadDelegate {
|
||||
internal var completionHandlersForTasks = [String: [Int: SimpleCompletionHandler]]()
|
||||
internal var downloadCompletionHandlersForTasks = [String: [Int: (URL) -> Void]]()
|
||||
internal var dataCompletionHandlersForTasks = [String: [Int: (Data) -> Void]]()
|
||||
|
||||
internal func initEmptySessionHandler(_ uuid: String) {
|
||||
completionHandlersForTasks[uuid] = [:]
|
||||
downloadCompletionHandlersForTasks[uuid] = [:]
|
||||
dataCompletionHandlersForTasks[uuid] = [:]
|
||||
}
|
||||
|
||||
internal func removeSessionHandler(for uuid: String) {
|
||||
_ = completionHandlersForTasks.removeValue(forKey: uuid)
|
||||
_ = downloadCompletionHandlersForTasks.removeValue(forKey: uuid)
|
||||
_ = dataCompletionHandlersForTasks.removeValue(forKey: uuid)
|
||||
}
|
||||
|
||||
/// All objects set to `FileProviderRemote.session` must be an instance of this class
|
||||
final public class SessionDelegate: NSObject, URLSessionDataDelegate, URLSessionDownloadDelegate, URLSessionStreamDelegate {
|
||||
|
||||
weak var fileProvider: FileProvider?
|
||||
weak var fileProvider: (FileProviderBasicRemote & FileProviderOperations)?
|
||||
var credential: URLCredential?
|
||||
|
||||
var finishDownloadHandler: ((_ session: Foundation.URLSession, _ downloadTask: URLSessionDownloadTask, _ didFinishDownloadingToURL: URL) -> Void)?
|
||||
var didSendDataHandler: ((_ session: Foundation.URLSession, _ task: URLSessionTask, _ bytesSent: Int64, _ totalBytesSent: Int64, _ totalBytesExpectedToSend: Int64) -> Void)?
|
||||
var didReceivedData: ((_ session: Foundation.URLSession, _ downloadTask: URLSessionDownloadTask, _ bytesWritten: Int64, _ totalBytesWritten: Int64, _ totalBytesExpectedToWrite: Int64) -> Void)?
|
||||
/// Forwardng URLSessionDownloadTaskDelegate call
|
||||
public var finishDownloadHandler: ((_ session: URLSession, _ downloadTask: URLSessionDownloadTask, _ didFinishDownloadingToURL: URL) -> Void)?
|
||||
/// Forwardng URLSessionTaskDelegate call
|
||||
public var didSendDataHandler: ((_ session: URLSession, _ task: URLSessionTask, _ bytesSent: Int64, _ totalBytesSent: Int64, _ totalBytesExpectedToSend: Int64) -> Void)?
|
||||
/// Forwardng URLSessionDownloadTaskDelegate call
|
||||
public var didReceivedData: ((_ session: URLSession, _ downloadTask: URLSessionDownloadTask, _ bytesWritten: Int64, _ totalBytesWritten: Int64, _ totalBytesExpectedToWrite: Int64) -> Void)?
|
||||
/// Forwardng URLSessionStreamTaskDelegate call
|
||||
public var didBecomeStream :((_ session: URLSession, _ taskId: Int, _ didBecome: InputStream, _ outputStream: OutputStream) -> Void)?
|
||||
|
||||
init(fileProvider: FileProvider, credential: URLCredential?) {
|
||||
public init(fileProvider: FileProviderBasicRemote & FileProviderOperations) {
|
||||
self.fileProvider = fileProvider
|
||||
self.credential = credential
|
||||
self.credential = fileProvider.credential
|
||||
}
|
||||
|
||||
open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
if let progress = context?.load(as: Progress.self), let newVal = change?[.newKey] as? Int64 {
|
||||
switch keyPath ?? "" {
|
||||
case #keyPath(URLSessionTask.countOfBytesReceived):
|
||||
progress.completedUnitCount = newVal
|
||||
if let startTime = progress.userInfo[ProgressUserInfoKey.startingTimeKey] as? Date, let task = object as? URLSessionTask {
|
||||
let elapsed = Date().timeIntervalSince(startTime)
|
||||
let throughput = Double(newVal) / elapsed
|
||||
progress.setUserInfoObject(NSNumber(value: throughput), forKey: .throughputKey)
|
||||
if task.countOfBytesExpectedToReceive > 0 {
|
||||
let remain = task.countOfBytesExpectedToReceive - task.countOfBytesReceived
|
||||
let estimatedTimeRemaining = Double(remain) / elapsed
|
||||
progress.setUserInfoObject(NSNumber(value: estimatedTimeRemaining), forKey: .estimatedTimeRemainingKey)
|
||||
}
|
||||
}
|
||||
case #keyPath(URLSessionTask.countOfBytesSent):
|
||||
progress.completedUnitCount = newVal
|
||||
if let startTime = progress.userInfo[ProgressUserInfoKey.startingTimeKey] as? Date, let task = object as? URLSessionTask {
|
||||
let elapsed = Date().timeIntervalSince(startTime)
|
||||
let throughput = Double(newVal) / elapsed
|
||||
progress.setUserInfoObject(NSNumber(value: throughput), forKey: .throughputKey)
|
||||
if task.countOfBytesExpectedToSend > 0 {
|
||||
let remain = task.countOfBytesExpectedToSend - task.countOfBytesSent
|
||||
let estimatedTimeRemaining = Double(remain) / elapsed
|
||||
progress.setUserInfoObject(NSNumber(value: estimatedTimeRemaining), forKey: .estimatedTimeRemainingKey)
|
||||
}
|
||||
}
|
||||
case #keyPath(URLSessionTask.countOfBytesExpectedToReceive), #keyPath(URLSessionTask.countOfBytesExpectedToSend):
|
||||
progress.totalUnitCount = newVal
|
||||
default:
|
||||
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// codebeat:disable[ARITY]
|
||||
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
|
||||
self.finishDownloadHandler?(session, downloadTask, location)
|
||||
return
|
||||
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
||||
if task is URLSessionDownloadTask {
|
||||
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived))
|
||||
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive))
|
||||
}
|
||||
if task is URLSessionUploadTask {
|
||||
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent))
|
||||
//task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToSend))
|
||||
}
|
||||
if !(error == nil && task is URLSessionDownloadTask) {
|
||||
let completionHandler = completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] ?? nil
|
||||
completionHandler?(error)
|
||||
_ = completionHandlersForTasks[session.sessionDescription!]?.removeValue(forKey: task.taskIdentifier)
|
||||
}
|
||||
|
||||
guard let json = task.taskDescription?.deserializeJSON(),
|
||||
let op = FileOperationType(json: json), let fileProvider = fileProvider else {
|
||||
return
|
||||
}
|
||||
|
||||
if !(task is URLSessionDownloadTask), case FileOperationType.fetch = op {
|
||||
return
|
||||
}
|
||||
if #available(iOS 9.0, macOS 10.11, *) {
|
||||
if task is URLSessionStreamTask {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if let error = error {
|
||||
fileProvider.delegate?.fileproviderFailed(fileProvider, operation: op, error: error)
|
||||
} else {
|
||||
fileProvider.delegate?.fileproviderSucceed(fileProvider, operation: op)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
|
||||
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
|
||||
let completionHandler = dataCompletionHandlersForTasks[session.sessionDescription!]?[dataTask.taskIdentifier] ?? nil
|
||||
completionHandler?(data)
|
||||
_ = dataCompletionHandlersForTasks[session.sessionDescription!]?.removeValue(forKey: dataTask.taskIdentifier)
|
||||
}
|
||||
|
||||
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
|
||||
self.finishDownloadHandler?(session, downloadTask, location)
|
||||
|
||||
let dcompletionHandler = downloadCompletionHandlersForTasks[session.sessionDescription!]?[downloadTask.taskIdentifier]
|
||||
dcompletionHandler?(location)
|
||||
_ = downloadCompletionHandlersForTasks[session.sessionDescription!]?.removeValue(forKey: downloadTask.taskIdentifier)
|
||||
_ = completionHandlersForTasks[session.sessionDescription!]?.removeValue(forKey: downloadTask.taskIdentifier)
|
||||
}
|
||||
|
||||
public func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
|
||||
self.didSendDataHandler?(session, task, bytesSent, totalBytesSent, totalBytesExpectedToSend)
|
||||
|
||||
guard let desc = task.taskDescription, let json = jsonToDictionary(desc) else {
|
||||
guard let json = task.taskDescription?.deserializeJSON(),
|
||||
let op = FileOperationType(json: json), let fileProvider = fileProvider else {
|
||||
return
|
||||
}
|
||||
guard let type = json["type"] as? String, let source = json["source"] as? String else {
|
||||
return
|
||||
}
|
||||
let dest = json["dest"] as? String
|
||||
let op : FileOperationType
|
||||
switch type {
|
||||
case "Create":
|
||||
op = .create(path: source)
|
||||
case "Copy":
|
||||
guard let dest = dest else { return }
|
||||
op = .copy(source: source, destination: dest)
|
||||
case "Move":
|
||||
guard let dest = dest else { return }
|
||||
op = .move(source: source, destination: dest)
|
||||
case "Modify":
|
||||
op = .modify(path: source)
|
||||
case "Remove":
|
||||
op = .remove(path: source)
|
||||
case "Link":
|
||||
guard let dest = dest else { return }
|
||||
op = .link(link: source, target: dest)
|
||||
|
||||
switch op {
|
||||
case .create(path: let path):
|
||||
if path.hasSuffix("/") { return }
|
||||
break
|
||||
case .modify:
|
||||
break
|
||||
case .copy(source: let source, destination: _) where source.hasPrefix("file://"):
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
let progress = Float(totalBytesSent) / Float(totalBytesExpectedToSend)
|
||||
|
||||
fileProvider?.delegate?.fileproviderProgress(fileProvider!, operation: op, progress: progress)
|
||||
fileProvider.delegateNotify(op, progress: Double(totalBytesSent) / Double(totalBytesExpectedToSend))
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
|
||||
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
|
||||
self.didReceivedData?(session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
|
||||
|
||||
guard let desc = downloadTask.taskDescription, let json = jsonToDictionary(desc), let source = json["source"] as? String, let dest = json["dest"] as? String else {
|
||||
if totalBytesExpectedToWrite == NSURLSessionTransferSizeUnknown { return }
|
||||
|
||||
guard let json = downloadTask.taskDescription?.deserializeJSON(),
|
||||
let op = FileOperationType(json: json), let fileProvider = fileProvider else {
|
||||
return
|
||||
}
|
||||
|
||||
fileProvider?.delegate?.fileproviderProgress(fileProvider!, operation: .copy(source: source, destination: dest), progress: Float(totalBytesWritten) / Float(totalBytesExpectedToWrite))
|
||||
fileProvider.delegateNotify(op, progress: Double(totalBytesWritten) / Double(totalBytesExpectedToWrite))
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
||||
let deposition: Foundation.URLSession.AuthChallengeDisposition = credential != nil ? .useCredential : .performDefaultHandling
|
||||
completionHandler(deposition, credential)
|
||||
public func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
||||
authenticate(didReceive: challenge, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
||||
let deposition: Foundation.URLSession.AuthChallengeDisposition = credential != nil ? .useCredential : .performDefaultHandling
|
||||
completionHandler(deposition, credential)
|
||||
public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
||||
authenticate(didReceive: challenge, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
func authenticate(didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
||||
switch (challenge.previousFailureCount, credential != nil) {
|
||||
case (0...1, true):
|
||||
completionHandler(.useCredential, credential)
|
||||
case (0, false):
|
||||
completionHandler(.useCredential, challenge.proposedCredential)
|
||||
default:
|
||||
completionHandler(.performDefaultHandling, nil)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 9.0, macOS 10.11, *)
|
||||
public func urlSession(_ session: URLSession, streamTask: URLSessionStreamTask, didBecome inputStream: InputStream, outputStream: OutputStream) {
|
||||
self.didBecomeStream?(session, streamTask.taskIdentifier, inputStream, outputStream)
|
||||
}
|
||||
}
|
||||
|
||||
public enum FileProviderHTTPErrorCode: Int {
|
||||
/// HTTP status codes as an enum.
|
||||
public enum FileProviderHTTPErrorCode: Int, CustomStringConvertible {
|
||||
/// `Continue` informational status with HTTP code 100
|
||||
case `continue` = 100
|
||||
/// `Switching Protocols` informational status with HTTP code 101
|
||||
case switchingProtocols = 101
|
||||
/// `Processing` informational status with HTTP code 102
|
||||
case processing = 102
|
||||
/// `OK` success status with HTTP code 200
|
||||
case ok = 200
|
||||
/// `Created` success status with HTTP code 201
|
||||
case created = 201
|
||||
/// `Accepted` success status with HTTP code 202
|
||||
case accepted = 202
|
||||
/// `Non Authoritative Information` success status with HTTP code 203
|
||||
case nonAuthoritativeInformation = 203
|
||||
/// `No Content` success status with HTTP code 204
|
||||
case noContent = 204
|
||||
/// `ResetcContent` success status with HTTP code 205
|
||||
case resetContent = 205
|
||||
/// `Partial Content` success status with HTTP code 206
|
||||
case partialContent = 206
|
||||
/// `Multi Status` success status with HTTP code 207
|
||||
case multiStatus = 207
|
||||
/// `Already Reported` success status with HTTP code 208
|
||||
case alreadyReported = 208
|
||||
/// `IM Used` success status with HTTP code 226
|
||||
case imUsed = 226
|
||||
/// `Multiple Choices` redirection status with HTTP code 300
|
||||
case multipleChoices = 300
|
||||
/// `Moved Permanently` redirection status with HTTP code 301
|
||||
case movedPermanently = 301
|
||||
/// `Found` redirection status with HTTP code 302
|
||||
case found = 302
|
||||
/// `See Other` redirection status with HTTP code 303
|
||||
case seeOther = 303
|
||||
/// `Not Modified` redirection status with HTTP code 304
|
||||
case notModified = 304
|
||||
/// `Use Proxy` redirection status with HTTP code 305
|
||||
case useProxy = 305
|
||||
/// `Switch Proxy` redirection status with HTTP code 306
|
||||
case switchProxy = 306
|
||||
/// `Temporary Redirect` redirection status with HTTP code 307
|
||||
case temporaryRedirect = 307
|
||||
/// `Permanent Redirect` redirection status with HTTP code 308
|
||||
case permanentRedirect = 308
|
||||
/// `Bad Request` client error status with HTTP code 400
|
||||
case badRequest = 400
|
||||
/// `Unauthorized` client error status with HTTP code 401
|
||||
case unauthorized = 401
|
||||
/// `Payment Required` client error status with HTTP code 402
|
||||
case paymentRequired = 402
|
||||
/// `Forbidden` client error status with HTTP code 403
|
||||
case forbidden = 403
|
||||
/// `Not Found` client error status with HTTP code 404
|
||||
case notFound = 404
|
||||
/// `Method Not Allowed` client error status with HTTP code 405
|
||||
case methodNotAllowed = 405
|
||||
/// `Not Acceptable` client error status with HTTP code 406
|
||||
case notAcceptable = 406
|
||||
/// `Proxy Authentication Required` client error status with HTTP code 407
|
||||
case proxyAuthenticationRequired = 407
|
||||
/// `Request Timeout` client error status with HTTP code 408
|
||||
case requestTimeout = 408
|
||||
/// `Conflict` client error status with HTTP code 409
|
||||
case conflict = 409
|
||||
/// `Gone` client error status with HTTP code 410
|
||||
case gone = 410
|
||||
/// `Length Required` client error status with HTTP code 411
|
||||
case lengthRequired = 411
|
||||
/// `Precondition Failed` client error status with HTTP code 412
|
||||
case preconditionFailed = 412
|
||||
/// `Payload Too Large` client error status with HTTP code 413
|
||||
case payloadTooLarge = 413
|
||||
/// `URI Too Long` client error status with HTTP code 414
|
||||
case uriTooLong = 414
|
||||
/// `Unsupported Media Type` status with HTTP code 415
|
||||
case unsupportedMediaType = 415
|
||||
/// `Range Not Satisfiable` client error status with HTTP code 416
|
||||
case rangeNotSatisfiable = 416
|
||||
/// `Expectation Failed` client error status with HTTP code 417
|
||||
case expectationFailed = 417
|
||||
/// `Misdirected Request` client error status with HTTP code 421
|
||||
case misdirectedRequest = 421
|
||||
/// `Unprocessable Entity` client error status with HTTP code 422
|
||||
case unprocessableEntity = 422
|
||||
/// `Locked` client error status with HTTP code 423
|
||||
case locked = 423
|
||||
/// `Failed Dependency` client error status with HTTP code 424
|
||||
case failedDependency = 424
|
||||
/// `Unordered Collection` client error status with HTTP code 425
|
||||
case unorderedCollection = 425
|
||||
/// `Upgrade Required` client error status with HTTP code 426
|
||||
case upgradeRequired = 426
|
||||
/// `Precondition Required` client error status with HTTP code 428
|
||||
case preconditionRequired = 428
|
||||
/// `Too Many Requests` client error status with HTTP code 429
|
||||
case tooManyRequests = 429
|
||||
/// `Request Header Fields Too Large` client error status with HTTP code 431
|
||||
case requestHeaderFieldsTooLarge = 431
|
||||
/// `Unavailable For Legal Reasons` client error status with HTTP code 451
|
||||
case unavailableForLegalReasons = 451
|
||||
/// `Internal Server Error` server error status with HTTP code 500
|
||||
case internalServerError = 500
|
||||
/// `Bad Gateway` server error status with HTTP code 502
|
||||
case badGateway = 502
|
||||
/// `Service Unavailable` server error status with HTTP code 503
|
||||
case serviceUnavailable = 503
|
||||
/// `Gateway Timeout` server error status with HTTP code 504
|
||||
case gatewayTimeout = 504
|
||||
/// `HTTP Version Not Supported` server error status with HTTP code 505
|
||||
case httpVersionNotSupported = 505
|
||||
case variantlsoNegotiates = 506
|
||||
/// `Variant Also Negotiates` server error status with HTTP code 506
|
||||
case variantAlsoNegotiates = 506
|
||||
/// `Insufficient Storage` server error status with HTTP code 507
|
||||
case insufficientStorage = 507
|
||||
/// `Loop Detected` server error status with HTTP code 508
|
||||
case loopDetected = 508
|
||||
/// `Bandwidth Limit Exceeded` server error status with HTTP code 509
|
||||
case bandwidthLimitExceeded = 509
|
||||
/// `Not Extended` server error status with HTTP code 510
|
||||
case notExtended = 510
|
||||
/// `Network Authentication Required` server error status with HTTP code 511
|
||||
case networkAuthenticationRequired = 511
|
||||
|
||||
fileprivate static let status1xx: [Int: String] = [100: "Continue", 101: "Switching Protocols", 102: "Processing"]
|
||||
@@ -233,6 +360,7 @@ public enum FileProviderHTTPErrorCode: Int {
|
||||
}
|
||||
}
|
||||
|
||||
/// Description of status based on first digit which indicated fail or success.
|
||||
public var typeDescription: String {
|
||||
switch self.rawValue {
|
||||
case 100...199: return "Informational"
|
||||
@@ -240,7 +368,7 @@ public enum FileProviderHTTPErrorCode: Int {
|
||||
case 300...399: return "Redirection"
|
||||
case 400...499: return "Client Error"
|
||||
case 500...599: return "Server Error"
|
||||
default: return "Server Error"
|
||||
default: return "Unknown Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-15
@@ -11,11 +11,11 @@ import Foundation
|
||||
// This client implementation is for little-endian platform, namely x86, x64 & arm
|
||||
// For big-endian platforms like PowerPC, there must be a huge overhaul
|
||||
|
||||
protocol SMBProtocolClientDelegate: class {
|
||||
func receivedResponse(client: SMB2ProtocolClient, response: SMBResponse, for: SMBRequest)
|
||||
protocol FileProviderSMBTaskDelegate: class {
|
||||
func receivedResponse(client: FileProviderSMBTask, response: SMBResponse, for: SMBRequest)
|
||||
}
|
||||
|
||||
class SMB2ProtocolClient: FPSStreamTask {
|
||||
class FileProviderSMBTask: FileProviderStreamTask {
|
||||
var timeout: TimeInterval = 30
|
||||
|
||||
private(set) var lastMessageID: UInt64 = 0
|
||||
@@ -31,14 +31,14 @@ class SMB2ProtocolClient: FPSStreamTask {
|
||||
private(set) var requestStack = [Int: SMBRequest]()
|
||||
private(set) var responseStack = [Int: SMBResponse]()
|
||||
|
||||
weak var delegate: SMBProtocolClientDelegate?
|
||||
weak var delegate: FileProviderSMBTaskDelegate?
|
||||
|
||||
func sendNegotiate(completionHandler: SimpleCompletionHandler) -> UInt64 {
|
||||
let mId = messageId()
|
||||
let smbHeader = SMB2.Header(command: .NEGOTIATE, creditRequestResponse: UInt16(126), messageId: mId, treeId: UInt32(0), sessionId: UInt64(0))
|
||||
let msg = SMB2.NegotiateRequest()
|
||||
let data = createSMB2Message(header: smbHeader, message: msg)
|
||||
self.writeData(data, timeout: 0, completionHandler: { (e) in
|
||||
self.write(data, timeout: timeout, completionHandler: { (e) in
|
||||
completionHandler?(e)
|
||||
})
|
||||
return mId
|
||||
@@ -50,9 +50,9 @@ class SMB2ProtocolClient: FPSStreamTask {
|
||||
let smbHeader = SMB2.Header(command: SMB2.Command.SESSION_SETUP, creditRequestResponse: credit, messageId: mId, treeId: UInt32(0), sessionId: sessionId)
|
||||
let msg = SMB2.SessionSetupRequest(singing: [])
|
||||
let data = createSMB2Message(header: smbHeader, message: msg)
|
||||
self.writeData(data, timeout: 0, completionHandler: { (e) in
|
||||
self.write(data, timeout: timeout, completionHandler: { (e) in
|
||||
if self.sessionId == 0 {
|
||||
self.readData(OfMinLength: 64, maxLength: 65536, timeout: self.timeout, completionHandler: { (data, eof, e2) in
|
||||
self.readData(ofMinLength: 64, maxLength: 65536, timeout: self.timeout, completionHandler: { (data, eof, e2) in
|
||||
// TODO: set session id
|
||||
completionHandler?(e2 ?? e)
|
||||
})
|
||||
@@ -68,15 +68,12 @@ class SMB2ProtocolClient: FPSStreamTask {
|
||||
}
|
||||
let mId = messageId()
|
||||
let smbHeader = SMB2.Header(command: .TREE_CONNECT, creditRequestResponse: 123, messageId: mId, treeId: 0, sessionId: sessionId)
|
||||
var share = ""
|
||||
let cmp = url.pathComponents
|
||||
if cmp.count > 0 {
|
||||
share = cmp[0]
|
||||
}
|
||||
let share = cmp.first ?? ""
|
||||
let tcHeader = SMB2.TreeConnectRequest.Header(flags: [])
|
||||
let msg = SMB2.TreeConnectRequest(header: tcHeader, host: host, share: share)
|
||||
let data = createSMB2Message(header: smbHeader, message: msg!)
|
||||
self.writeData(data, timeout: 0, completionHandler: { (e) in
|
||||
self.write(data, timeout: timeout, completionHandler: { (e) in
|
||||
completionHandler?(e)
|
||||
|
||||
})
|
||||
@@ -88,7 +85,7 @@ class SMB2ProtocolClient: FPSStreamTask {
|
||||
let smbHeader = SMB2.Header(command: .TREE_DISCONNECT, creditRequestResponse: 111, messageId: mId, treeId: treeId, sessionId: sessionId)
|
||||
let msg = SMB2.TreeDisconnect()
|
||||
let data = createSMB2Message(header: smbHeader, message: msg)
|
||||
self.writeData(data, timeout: 0, completionHandler: { (e) in
|
||||
self.write(data, timeout: timeout, completionHandler: { (e) in
|
||||
completionHandler?(e)
|
||||
})
|
||||
return mId
|
||||
@@ -99,7 +96,7 @@ class SMB2ProtocolClient: FPSStreamTask {
|
||||
let smbHeader = SMB2.Header(command: .LOGOFF, creditRequestResponse: 0, messageId: mId, treeId: 0, sessionId: sessionId)
|
||||
let msg = SMB2.LogOff()
|
||||
let data = createSMB2Message(header: smbHeader, message: msg)
|
||||
self.writeData(data, timeout: 0, completionHandler: { (e) in
|
||||
self.write(data, timeout: timeout, completionHandler: { (e) in
|
||||
completionHandler?(e)
|
||||
})
|
||||
return mId
|
||||
@@ -111,7 +108,7 @@ class SMB2ProtocolClient: FPSStreamTask {
|
||||
}
|
||||
|
||||
// MARK: create and analyse messages
|
||||
extension SMB2ProtocolClient {
|
||||
extension FileProviderSMBTask {
|
||||
func determineSMBVersion(_ data: Data) -> Float {
|
||||
let smbverChar: Int8 = Int8(bitPattern: data.first ?? 0)
|
||||
let version = 0 - smbverChar
|
||||
|
||||
@@ -10,28 +10,52 @@ import Foundation
|
||||
|
||||
class SMBFileProvider: FileProvider, FileProviderMonitor {
|
||||
open class var type: String { return "SMB" }
|
||||
open var isPathRelative: Bool = true
|
||||
open var baseURL: URL?
|
||||
open var currentPath: String = ""
|
||||
open var dispatch_queue: DispatchQueue
|
||||
open var operation_queue: OperationQueue
|
||||
open weak var delegate: FileProviderDelegate?
|
||||
open let credential: URLCredential?
|
||||
open var credential: URLCredential?
|
||||
|
||||
public typealias FileObjectClass = FileObject
|
||||
|
||||
public init? (baseURL: URL, credential: URLCredential, afterInitialized: SimpleCompletionHandler) {
|
||||
public init? (baseURL: URL, credential: URLCredential?) {
|
||||
guard baseURL.uw_scheme.lowercased() == "smb" else {
|
||||
return nil
|
||||
}
|
||||
self.baseURL = baseURL.appendingPathComponent("")
|
||||
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: .concurrent)
|
||||
|
||||
#if swift(>=3.1)
|
||||
let queueLabel = "FileProvider.\(Swift.type(of: self).type)"
|
||||
#else
|
||||
let queueLabel = "FileProvider.\(type(of: self).type)"
|
||||
#endif
|
||||
dispatch_queue = DispatchQueue(label: queueLabel, attributes: .concurrent)
|
||||
operation_queue = OperationQueue()
|
||||
operation_queue.name = "FileProvider.\(type(of: self).type).Operation"
|
||||
operation_queue.name = "\(queueLabel).Operation"
|
||||
|
||||
self.credential = credential
|
||||
}
|
||||
|
||||
|
||||
public required convenience init?(coder aDecoder: NSCoder) {
|
||||
guard let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL else {
|
||||
return nil
|
||||
}
|
||||
self.init(baseURL: baseURL,
|
||||
credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential)
|
||||
self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? ""
|
||||
}
|
||||
|
||||
open func encode(with aCoder: NSCoder) {
|
||||
aCoder.encode(self.baseURL, forKey: "baseURL")
|
||||
aCoder.encode(self.credential, forKey: "credential")
|
||||
aCoder.encode(self.currentPath, forKey: "currentPath")
|
||||
}
|
||||
|
||||
public static var supportsSecureCoding: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObjectClass], _ error: Error?) -> Void)) {
|
||||
NotImplemented()
|
||||
}
|
||||
@@ -44,62 +68,62 @@ class SMBFileProvider: FileProvider, FileProviderMonitor {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
func isReachable(completionHandler: @escaping (Bool) -> Void) {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
open weak var fileOperationDelegate: FileOperationDelegate?
|
||||
|
||||
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
NotImplemented()
|
||||
return nil
|
||||
}
|
||||
|
||||
open func create(file fileName: String, at atPath: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
NotImplemented()
|
||||
return nil
|
||||
}
|
||||
|
||||
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
NotImplemented()
|
||||
return nil
|
||||
}
|
||||
|
||||
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
NotImplemented()
|
||||
return nil
|
||||
}
|
||||
|
||||
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
NotImplemented()
|
||||
return nil
|
||||
}
|
||||
|
||||
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
NotImplemented()
|
||||
return nil
|
||||
}
|
||||
|
||||
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
|
||||
NotImplemented()
|
||||
return nil
|
||||
}
|
||||
|
||||
open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> Progress? {
|
||||
NotImplemented()
|
||||
return nil
|
||||
}
|
||||
|
||||
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
NotImplemented()
|
||||
return nil
|
||||
}
|
||||
|
||||
open func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler:((FileObjectClass) -> Void)?, completionHandler: @escaping ((_ files: [FileObjectClass], _ error: Error?) -> Void)) -> Progress? {
|
||||
NotImplemented()
|
||||
return nil
|
||||
}
|
||||
|
||||
open func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler:((FileObjectClass) -> Void)?, completionHandler: @escaping ((_ files: [FileObjectClass], _ error: Error?) -> Void)) {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
open func registerNotifcation(path: String, eventHandler: @escaping (() -> Void)) {
|
||||
NotImplemented()
|
||||
}
|
||||
@@ -113,7 +137,7 @@ class SMBFileProvider: FileProvider, FileProviderMonitor {
|
||||
}
|
||||
|
||||
open func copy(with zone: NSZone? = nil) -> Any {
|
||||
let copy = SMBFileProvider(baseURL: self.baseURL!, credential: self.credential!, afterInitialized: { _ in })!
|
||||
let copy = SMBFileProvider(baseURL: self.baseURL!, credential: self.credential!)!
|
||||
copy.currentPath = self.currentPath
|
||||
copy.delegate = self.delegate
|
||||
copy.fileOperationDelegate = self.fileOperationDelegate
|
||||
@@ -122,7 +146,7 @@ class SMBFileProvider: FileProvider, FileProviderMonitor {
|
||||
}
|
||||
|
||||
// MARK: basic CIFS interactivity
|
||||
public enum SMBFileProviderError: Int, Error, CustomStringConvertible {
|
||||
enum SMBFileProviderError: Int, Error, CustomStringConvertible {
|
||||
case badHeader
|
||||
case incompatibleHeader
|
||||
case incorrectParamsLength
|
||||
|
||||
@@ -66,32 +66,3 @@ struct SMBTime {
|
||||
return Date(timeIntervalSince1970: Double(self.time) / 10000000 - 11644473600)
|
||||
}
|
||||
}
|
||||
|
||||
extension Data {
|
||||
init<T>(value: T) {
|
||||
var value = value
|
||||
self = Data(buffer: UnsafeBufferPointer(start: &value, count: 1))
|
||||
}
|
||||
|
||||
func scanValue<T>() -> T? {
|
||||
guard MemoryLayout<T>.size <= self.count else { return nil }
|
||||
return self.withUnsafeBytes { $0.pointee }
|
||||
}
|
||||
|
||||
func scanValue<T>(start: Int) -> T? {
|
||||
let length = MemoryLayout<T>.size
|
||||
guard self.count >= start + length else { return nil }
|
||||
return self.subdata(in: start..<start+length).withUnsafeBytes { $0.pointee }
|
||||
}
|
||||
|
||||
func scanString(start: Int = 0, length: Int, encoding: String.Encoding) -> 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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@ extension SMB2 {
|
||||
let flags: WriteRequest.Flags
|
||||
}
|
||||
|
||||
// codebeat:disable[ARITY]
|
||||
init(fileId: FileId, offset: UInt64, remainingBytes: UInt32 = 0, data: Data, channel: Channel = .NONE, channelInfo: ChannelInfo? = nil, flags: WriteRequest.Flags = []) {
|
||||
var channelInfoOffset: UInt16 = 0
|
||||
var channelInfoLength: UInt16 = 0
|
||||
@@ -118,6 +119,7 @@ extension SMB2 {
|
||||
self.channelInfo = channelInfo
|
||||
self.fileData = data
|
||||
}
|
||||
// codebeat:enable[ARITY]
|
||||
|
||||
func data() -> Data {
|
||||
var result = Data(value: self.header)
|
||||
|
||||
@@ -243,7 +243,7 @@ extension SMB2 {
|
||||
if data.count < offset + 48 {
|
||||
return nil
|
||||
}
|
||||
let datestring = data.scanString(start: offset, length: 48, encoding: .utf16)
|
||||
let datestring = data.scanString(start: offset, length: 48, using: .utf16)
|
||||
if let datestring = datestring, let date = dateFormatter.date(from: datestring) {
|
||||
snapshots.append(SMBTime(date: date))
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ extension SMB2 {
|
||||
}
|
||||
|
||||
let fileNameLen = Int(data.scanValue(start: offset + 8) as UInt32? ?? 0)
|
||||
let fileName = data.scanString(start: offset + 12, length: fileNameLen, encoding: .utf16) ?? ""
|
||||
let fileName = data.scanString(start: offset + 12, length: fileNameLen, using: .utf16) ?? ""
|
||||
result.append((action: action, fileName: fileName))
|
||||
|
||||
offset += Int(nextOffset)
|
||||
|
||||
@@ -85,7 +85,7 @@ extension SMB2 {
|
||||
return []
|
||||
}
|
||||
let headersize = MemoryLayout.size(ofValue: header)
|
||||
let fileName = buffer.scanString(start: headersize, length: Int(header.fileNameLength), encoding: .utf16) ?? ""
|
||||
let fileName = buffer.scanString(start: headersize, length: Int(header.fileNameLength), using: .utf16) ?? ""
|
||||
result.append((header: header, fileName: fileName))
|
||||
if header.nextEntryOffset == 0 {
|
||||
break
|
||||
@@ -216,12 +216,12 @@ extension SMB2 {
|
||||
var asAllInformation: (header: FileAllInformationHeader, name: String) {
|
||||
let header: FileAllInformationHeader = buffer.scanValue()!
|
||||
let headersize = MemoryLayout<FileAllInformationHeader>.size
|
||||
let name = buffer.scanString(start: headersize, length: Int(header.nameLength), encoding: .utf16) ?? ""
|
||||
let name = buffer.scanString(start: headersize, length: Int(header.nameLength), using: .utf16) ?? ""
|
||||
return (header, name)
|
||||
}
|
||||
|
||||
var asAlternateNameInformation: String {
|
||||
return buffer.scanString(start: 0, length: buffer.count, encoding: .utf16) ?? ""
|
||||
return buffer.scanString(start: 0, length: buffer.count, using: .utf16) ?? ""
|
||||
}
|
||||
|
||||
var asAttributeTagInformation: FileAttributeTagInformation {
|
||||
@@ -280,14 +280,14 @@ extension SMB2 {
|
||||
var asStreamInformation: (header: FileStreamInformationHeader, name: String) {
|
||||
let header: FileStreamInformationHeader = buffer.scanValue()!
|
||||
let headersize = MemoryLayout<FileStreamInformationHeader>.size
|
||||
let name = buffer.scanString(start: headersize, length: Int(header.streamNameLength), encoding: .utf16) ?? ""
|
||||
let name = buffer.scanString(start: headersize, length: Int(header.streamNameLength), using: .utf16) ?? ""
|
||||
return (header, name)
|
||||
}
|
||||
|
||||
var asFsVolumeInformation: (header: FileFsVolumeInformationHeader, name: String) {
|
||||
let header: FileFsVolumeInformationHeader = buffer.scanValue()!
|
||||
let headersize = MemoryLayout<FileFsVolumeInformationHeader>.size
|
||||
let name = buffer.scanString(start: headersize, length: Int(header.labelLength), encoding: .utf16) ?? ""
|
||||
let name = buffer.scanString(start: headersize, length: Int(header.labelLength), using: .utf16) ?? ""
|
||||
return (header, name)
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@ extension SMB2 {
|
||||
var asFsAttributeInformation: (header: FileFsAttributeInformationHeader, name: String) {
|
||||
let header: FileFsAttributeInformationHeader = buffer.scanValue()!
|
||||
let headersize = MemoryLayout<FileFsAttributeInformationHeader>.size
|
||||
let name = buffer.scanString(start: headersize, length: Int(header.nameLength), encoding: .utf16) ?? ""
|
||||
let name = buffer.scanString(start: headersize, length: Int(header.nameLength), using: .utf16) ?? ""
|
||||
return (header, name)
|
||||
}
|
||||
|
||||
|
||||
+322
-364
@@ -7,43 +7,22 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
|
||||
/// Because this class uses NSURLSession, it's necessary to disable App Transport Security
|
||||
/// in case of using this class with unencrypted HTTP connection.
|
||||
|
||||
open class WebDAVFileProvider: FileProviderBasicRemote {
|
||||
open class var type: String { return "WebDAV" }
|
||||
open let isPathRelative: Bool
|
||||
open let baseURL: URL?
|
||||
open var currentPath: String
|
||||
|
||||
open var dispatch_queue: DispatchQueue
|
||||
open var operation_queue: OperationQueue {
|
||||
willSet {
|
||||
assert(_session == nil, "It's not effective to change dispatch_queue property after session is initialized.")
|
||||
}
|
||||
}
|
||||
|
||||
public weak var delegate: FileProviderDelegate?
|
||||
open let credential: URLCredential?
|
||||
open private(set) var cache: URLCache?
|
||||
public var useCache: Bool
|
||||
public var validatingCache: Bool
|
||||
|
||||
fileprivate var _session: URLSession?
|
||||
fileprivate var sessionDelegate: SessionDelegate?
|
||||
public var session: URLSession {
|
||||
if _session == nil {
|
||||
self.sessionDelegate = SessionDelegate(fileProvider: self, credential: credential)
|
||||
let queue = OperationQueue()
|
||||
//queue.underlyingQueue = dispatch_queue
|
||||
let config = URLSessionConfiguration.default
|
||||
config.urlCache = cache
|
||||
config.requestCachePolicy = .returnCacheDataElseLoad
|
||||
_session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDownloadDelegate?, delegateQueue: queue)
|
||||
}
|
||||
return _session!
|
||||
}
|
||||
/**
|
||||
Allows accessing to WebDAV server files. This provider doesn't cache or save files internally, however you can
|
||||
set `useCache` and `cache` properties to use Foundation `NSURLCache` system.
|
||||
|
||||
WebDAV system supported by many cloud services including [Box.com](https://www.box.com/home)
|
||||
and [Yandex disk](https://disk.yandex.com) and [ownCloud](https://owncloud.org).
|
||||
|
||||
- Important: Because this class uses `URLSession`, it's necessary to disable App Transport Security
|
||||
in case of using this class with unencrypted HTTP connection.
|
||||
[Read this to know how](http://iosdevtips.co/post/121756573323/ios-9-xcode-7-http-connect-server-error).
|
||||
*/
|
||||
open class WebDAVFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
override open class var type: String { return "WebDAV" }
|
||||
public var credentialType: URLRequest.AuthenticationType = .digest
|
||||
|
||||
/**
|
||||
Initializes WebDAV provider.
|
||||
@@ -51,42 +30,63 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
|
||||
- Parameters:
|
||||
- baseURL: Location of WebDAV server.
|
||||
- credential: An `URLCredential` object with `user` and `password`.
|
||||
- cache: A URLCache to cache downloaded files and contents. If set to nil, URLCache.shared object will be used.
|
||||
- cache: A URLCache to cache downloaded files and contents.
|
||||
*/
|
||||
public init? (baseURL: URL, credential: URLCredential?, cache: URLCache? = nil) {
|
||||
if !["http", "https"].contains(baseURL.uw_scheme.lowercased()) {
|
||||
return nil
|
||||
}
|
||||
self.baseURL = baseURL.appendingPathComponent("")
|
||||
self.isPathRelative = true
|
||||
self.currentPath = ""
|
||||
self.useCache = false
|
||||
self.validatingCache = true
|
||||
self.cache = cache
|
||||
self.credential = credential
|
||||
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: .concurrent)
|
||||
operation_queue = OperationQueue()
|
||||
operation_queue.name = "FileProvider.\(type(of: self).type).Operation"
|
||||
let refinedBaseURL = (baseURL.absoluteString.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")).absoluteURL
|
||||
super.init(baseURL: refinedBaseURL, credential: credential, cache: cache)
|
||||
}
|
||||
|
||||
deinit {
|
||||
if fileProviderCancelTasksOnInvalidating {
|
||||
_session?.invalidateAndCancel()
|
||||
} else {
|
||||
_session?.finishTasksAndInvalidate()
|
||||
public required convenience init?(coder aDecoder: NSCoder) {
|
||||
guard let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL else {
|
||||
return nil
|
||||
}
|
||||
self.init(baseURL: baseURL,
|
||||
credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential)
|
||||
self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? ""
|
||||
self.useCache = aDecoder.decodeBool(forKey: "useCache")
|
||||
self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache")
|
||||
}
|
||||
|
||||
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
|
||||
let opType = FileOperationType.fetch(path: path)
|
||||
override open func copy(with zone: NSZone? = nil) -> Any {
|
||||
let copy = WebDAVFileProvider(baseURL: self.baseURL!, credential: self.credential, cache: self.cache)!
|
||||
copy.currentPath = self.currentPath
|
||||
copy.delegate = self.delegate
|
||||
copy.fileOperationDelegate = self.fileOperationDelegate
|
||||
copy.useCache = self.useCache
|
||||
copy.validatingCache = self.validatingCache
|
||||
return copy
|
||||
}
|
||||
|
||||
override open func contentsOfDirectory(path: String, completionHandler: @escaping (([FileObject], Error?) -> Void)) {
|
||||
self.contentsOfDirectory(path: path, including: [], completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns an Array of `FileObject`s identifying the the directory entries via asynchronous completion handler.
|
||||
|
||||
If the directory contains no entries or an error is occured, this method will return the empty array.
|
||||
|
||||
- Parameter path: path to target directory. If empty, root will be iterated.
|
||||
- Parameter including: An array which determines which file properties should be considered to fetch.
|
||||
- Parameter completionHandler: a closure with result of directory entries or error.
|
||||
- `contents`: An array of `FileObject` identifying the the directory entries.
|
||||
- `error`: Error returned by system.
|
||||
*/
|
||||
open func contentsOfDirectory(path: String, including: [URLResourceKey], completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
|
||||
let operation = FileOperationType.fetch(path: path)
|
||||
let url = self.url(of: path).appendingPathComponent("")
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PROPFIND"
|
||||
request.setValue("1", forHTTPHeaderField: "Depth")
|
||||
request.setValue("text/xml; charset=\"utf-8\"", forHTTPHeaderField: "Content-Type")
|
||||
request.httpBody = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n<D:allprop/></D:propfind>".data(using: .utf8)
|
||||
request.set(httpAuthentication: credential, with: credentialType)
|
||||
request.set(httpContentType: .xml, charset: .utf8)
|
||||
request.httpBody = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n\(WebDavFileObject.propString(including))\n</D:propfind>".data(using: .utf8)
|
||||
request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length")
|
||||
runDataTask(with: request, operationHandle: RemoteOperationHandle(operationType: opType, tasks: []), completionHandler: { (data, response, error) in
|
||||
runDataTask(with: request, operation: operation, completionHandler: { (data, response, error) in
|
||||
var responseError: FileProviderWebDavError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
|
||||
@@ -105,13 +105,29 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
|
||||
})
|
||||
}
|
||||
|
||||
open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
|
||||
override open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
|
||||
self.attributesOfItem(path: path, including: [], completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns a `FileObject` containing the attributes of the item (file, directory, symlink, etc.) at the path in question via asynchronous completion handler.
|
||||
|
||||
If the directory contains no entries or an error is occured, this method will return the empty `FileObject`.
|
||||
|
||||
- Parameter path: path to target directory. If empty, attributes of root will be returned.
|
||||
- Parameter including: An array which determines which file properties should be considered to fetch.
|
||||
- Parameter completionHandler: a closure with result of directory entries or error.
|
||||
- `attributes`: A `FileObject` containing the attributes of the item.
|
||||
- `error`: Error returned by system.
|
||||
*/
|
||||
open func attributesOfItem(path: String, including: [URLResourceKey], completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
|
||||
let url = self.url(of: path)
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PROPFIND"
|
||||
request.setValue("1", forHTTPHeaderField: "Depth")
|
||||
request.setValue("text/xml; charset=\"utf-8\"", forHTTPHeaderField: "Content-Type")
|
||||
request.httpBody = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n<D:allprop/></D:propfind>".data(using: .utf8)
|
||||
request.setValue("0", forHTTPHeaderField: "Depth")
|
||||
request.set(httpAuthentication: credential, with: credentialType)
|
||||
request.set(httpContentType: .xml, charset: .utf8)
|
||||
request.httpBody = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n\(WebDavFileObject.propString(including))\n</D:propfind>".data(using: .utf8)
|
||||
request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length")
|
||||
runDataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var responseError: FileProviderWebDavError?
|
||||
@@ -129,7 +145,7 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
|
||||
})
|
||||
}
|
||||
|
||||
open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) {
|
||||
override open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) {
|
||||
// Not all WebDAV clients implements RFC2518 which allows geting storage quota.
|
||||
// In this case you won't get error. totalSize is NSURLSessionTransferSizeUnknown
|
||||
// and used space is zero.
|
||||
@@ -139,7 +155,8 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
|
||||
var request = URLRequest(url: baseURL)
|
||||
request.httpMethod = "PROPFIND"
|
||||
request.setValue("0", forHTTPHeaderField: "Depth")
|
||||
request.setValue("text/xml; charset=\"utf-8\"", forHTTPHeaderField: "Content-Type")
|
||||
request.set(httpAuthentication: credential, with: credentialType)
|
||||
request.set(httpContentType: .xml, charset: .utf8)
|
||||
request.httpBody = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n<D:prop><D:quota-available-bytes/><D:quota-used-bytes/></D:prop>\n</D:propfind>".data(using: .utf8)
|
||||
request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length")
|
||||
runDataTask(with: request, completionHandler: { (data, response, error) in
|
||||
@@ -156,252 +173,18 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
|
||||
})
|
||||
}
|
||||
|
||||
open weak var fileOperationDelegate: FileOperationDelegate?
|
||||
}
|
||||
|
||||
extension WebDAVFileProvider: FileProviderOperations {
|
||||
@discardableResult
|
||||
public func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(folderName) + "/")
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
let url = self.url(of: atPath).appendingPathComponent(folderName, isDirectory: true)
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "MKCOL"
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var responseError: FileProviderWebDavError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderWebDavError(code: rCode, path: url.relativePath, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
|
||||
}
|
||||
completionHandler?(responseError ?? error)
|
||||
self.delegateNotify(opType, error: responseError ?? error)
|
||||
})
|
||||
task.taskDescription = opType.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(operationType: opType, tasks: [task])
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func create(file fileName: String, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.create(path: (path as NSString).appendingPathComponent(fileName))
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
let url = self.url(of: path).appendingPathComponent(fileName)
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PUT"
|
||||
let task = session.uploadTask(with: request, from: data, completionHandler: { (data, response, error) in
|
||||
var responseError: FileProviderWebDavError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
|
||||
}
|
||||
completionHandler?(responseError ?? error)
|
||||
self.delegateNotify(opType, error: responseError ?? error)
|
||||
})
|
||||
task.taskDescription = opType.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(operationType: opType, tasks: [task])
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.move(source: path, destination: toPath)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
return self.doOperation(operation: opType, overwrite: overwrite, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.copy(source: path, destination: toPath)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
return self.doOperation(operation: opType, overwrite: overwrite, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.remove(path: path)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
return self.doOperation(operation: opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
func doOperation(operation opType: FileOperationType, overwrite: Bool? = nil, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let source = opType.source!
|
||||
let sourceURL = self.url(of: source)
|
||||
var request = URLRequest(url: sourceURL)
|
||||
if let dest = opType.destination {
|
||||
request.setValue(url(of:dest).absoluteString, forHTTPHeaderField: "Destination")
|
||||
}
|
||||
switch opType {
|
||||
case .copy:
|
||||
request.httpMethod = "COPY"
|
||||
case .move:
|
||||
request.httpMethod = "MOVE"
|
||||
case .remove:
|
||||
request.httpMethod = "DELETE"
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
if let overwrite = overwrite, !overwrite {
|
||||
request.setValue("F", forHTTPHeaderField: "Overwrite")
|
||||
}
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var responseError: FileProviderWebDavError?
|
||||
if let response = response as? HTTPURLResponse, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) {
|
||||
if response.statusCode >= 300 {
|
||||
responseError = FileProviderWebDavError(code: code, path: source, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: sourceURL)
|
||||
}
|
||||
if code == .multiStatus, let data = data {
|
||||
let xresponses = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
|
||||
for xresponse in xresponses where (xresponse.status ?? 0) >= 300 {
|
||||
let error = FileProviderWebDavError(code: code, path: source, errorDescription: String(data: data, encoding: .utf8), url: sourceURL)
|
||||
completionHandler?(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (response as? HTTPURLResponse)?.statusCode ?? 0 != FileProviderHTTPErrorCode.multiStatus.rawValue {
|
||||
completionHandler?(responseError ?? error)
|
||||
}
|
||||
|
||||
self.delegateNotify(opType, error: responseError ?? error)
|
||||
})
|
||||
task.taskDescription = opType.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(operationType: opType, tasks: [task])
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
// TODO: Make use of overwrite parameter
|
||||
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
let url = self.url(of:toPath)
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PUT"
|
||||
let task = session.uploadTask(with: request, fromFile: localFile, completionHandler: { (data, response, error) in
|
||||
var responseError: FileProviderWebDavError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderWebDavError(code: rCode, path: toPath, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
|
||||
}
|
||||
completionHandler?(responseError ?? error)
|
||||
self.delegateNotify(opType, error: responseError ?? error)
|
||||
})
|
||||
task.taskDescription = opType.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(operationType: opType, tasks: [task])
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
let url = self.url(of:path)
|
||||
let request = URLRequest(url: url)
|
||||
let task = session.downloadTask(with: request, completionHandler: { (sourceFileURL, response, error) in
|
||||
var responseError: FileProviderWebDavError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: nil, url: url)
|
||||
}
|
||||
if let sourceFileURL = sourceFileURL {
|
||||
do {
|
||||
try FileManager.default.copyItem(at: sourceFileURL, to: toLocalURL)
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
return
|
||||
}
|
||||
}
|
||||
completionHandler?(responseError ?? error)
|
||||
self.delegateNotify(opType, error: responseError ?? error)
|
||||
})
|
||||
task.taskDescription = opType.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(operationType: opType, tasks: [task])
|
||||
}
|
||||
}
|
||||
|
||||
extension WebDAVFileProvider: FileProviderReadWrite {
|
||||
@discardableResult
|
||||
public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
if length == 0 || offset < 0 {
|
||||
dispatch_queue.async {
|
||||
completionHandler(Data(), nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
let opType = FileOperationType.fetch(path: path)
|
||||
let url = self.url(of: path)
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
if length > 0 {
|
||||
request.setValue("bytes=\(offset)-\(offset + length - 1)", forHTTPHeaderField: "Range")
|
||||
} else if offset > 0 && length < 0 {
|
||||
request.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
|
||||
}
|
||||
let handle = RemoteOperationHandle(operationType: opType, tasks: [])
|
||||
runDataTask(with: request, operationHandle: handle, completionHandler: { (data, response, error) in
|
||||
var responseError: FileProviderWebDavError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
|
||||
}
|
||||
completionHandler(data, responseError ?? error)
|
||||
})
|
||||
return handle
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.modify(path: path)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
// FIXME: lock destination before writing process
|
||||
let url = atomically ? self.url(of: path).appendingPathExtension("tmp") : self.url(of: path)
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PUT"
|
||||
if !overwrite {
|
||||
request.setValue("F", forHTTPHeaderField: "Overwrite")
|
||||
}
|
||||
let task = session.uploadTask(with: request, from: data, completionHandler: { (data, response, error) in
|
||||
var responseError: FileProviderWebDavError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: self.url(of: path))
|
||||
}
|
||||
defer {
|
||||
self.delegateNotify(opType, error: responseError ?? error)
|
||||
}
|
||||
if let error = error {
|
||||
completionHandler?(error)
|
||||
return
|
||||
}
|
||||
if atomically {
|
||||
self.moveItem(path: (path as NSString).appendingPathExtension("tmp")!, to: path, completionHandler: completionHandler)
|
||||
}
|
||||
})
|
||||
task.taskDescription = opType.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(operationType: opType, tasks: [task])
|
||||
}
|
||||
|
||||
public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
|
||||
override open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) -> Progress? {
|
||||
let url = self.url(of: path)
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PROPFIND"
|
||||
//request.setValue("1", forHTTPHeaderField: "Depth")
|
||||
request.setValue("text/xml; charset=\"utf-8\"", forHTTPHeaderField: "Content-Type")
|
||||
// Depth infinity is disabled on some servers. Implement workaround?!
|
||||
request.setValue(recursive ? "infinity" : "1", forHTTPHeaderField: "Depth")
|
||||
request.set(httpAuthentication: credential, with: credentialType)
|
||||
request.set(httpContentType: .xml, charset: .utf8)
|
||||
request.httpBody = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n<D:allprop/></D:propfind>".data(using: .utf8)
|
||||
runDataTask(with: request, completionHandler: { (data, response, error) in
|
||||
let progress = Progress(parent: nil, userInfo: nil)
|
||||
progress.setUserInfoObject(url, forKey: .fileURLKey)
|
||||
let task = session.dataTask(with: request) { (data, response, error) in
|
||||
// FIXME: paginating results
|
||||
var responseError: FileProviderWebDavError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
@@ -411,21 +194,140 @@ extension WebDAVFileProvider: FileProviderReadWrite {
|
||||
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
|
||||
var fileObjects = [WebDavFileObject]()
|
||||
for attr in xresponse {
|
||||
let path = attr.href.path
|
||||
if !((path as NSString).lastPathComponent.contains(query)) {
|
||||
let fileObject = WebDavFileObject(attr)
|
||||
if !query.evaluate(with: fileObject.mapPredicate()) {
|
||||
continue
|
||||
}
|
||||
let fileObject = WebDavFileObject(attr)
|
||||
|
||||
fileObjects.append(fileObject)
|
||||
progress.completedUnitCount = Int64(fileObjects.count)
|
||||
foundItemHandler?(fileObject)
|
||||
}
|
||||
completionHandler(fileObjects, responseError ?? error)
|
||||
return
|
||||
}
|
||||
completionHandler([], responseError ?? error)
|
||||
}
|
||||
progress.cancellationHandler = { [weak task] in
|
||||
task?.cancel()
|
||||
}
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
task.resume()
|
||||
return progress
|
||||
}
|
||||
|
||||
override open func isReachable(completionHandler: @escaping (Bool) -> Void) {
|
||||
var request = URLRequest(url: baseURL!)
|
||||
request.httpMethod = "PROPFIND"
|
||||
request.setValue("0", forHTTPHeaderField: "Depth")
|
||||
request.set(httpAuthentication: credential, with: credentialType)
|
||||
request.set(httpContentType: .xml, charset: .utf8)
|
||||
request.httpBody = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n<D:prop><D:quota-available-bytes/><D:quota-used-bytes/></D:prop>\n</D:propfind>".data(using: .utf8)
|
||||
request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length")
|
||||
runDataTask(with: request, completionHandler: { (data, response, error) in
|
||||
let status = (response as? HTTPURLResponse)?.statusCode ?? 400
|
||||
completionHandler(status < 300)
|
||||
})
|
||||
}
|
||||
|
||||
open func publicLink(to path: String, completionHandler: @escaping ((URL?, FileObject?, Date?, Error?) -> Void)) {
|
||||
guard self.baseURL?.host?.contains("dav.yandex.") ?? false else {
|
||||
dispatch_queue.async {
|
||||
completionHandler(nil, nil, nil, self.throwError(path, code: URLError.resourceUnavailable))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let url = self.url(of: path)
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PROPPATCH"
|
||||
request.set(httpAuthentication: credential, with: credentialType)
|
||||
request.set(httpContentType: .xml, charset: .utf8)
|
||||
let body = "<propertyupdate xmlns=\"DAV:\">\n<set><prop>\n<public_url xmlns=\"urn:yandex:disk:meta\">true</public_url>\n</prop></set>\n</propertyupdate>"
|
||||
request.httpBody = body.data(using: .utf8)
|
||||
request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length")
|
||||
runDataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var responseError: FileProviderWebDavError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode, code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
|
||||
}
|
||||
if let data = data {
|
||||
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
|
||||
if let urlStr = xresponse.first?.prop["public_url"], let url = URL(string: urlStr) {
|
||||
completionHandler(url, nil, nil, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
completionHandler(nil, nil, nil, responseError ?? error)
|
||||
})
|
||||
}
|
||||
|
||||
override func request(for operation: FileOperationType, overwrite: Bool = true, attributes: [URLResourceKey: Any] = [:]) -> URLRequest {
|
||||
let method: String
|
||||
let url: URL
|
||||
let sourceURL = self.url(of: operation.source)
|
||||
|
||||
switch operation {
|
||||
case .fetch:
|
||||
method = "GET"
|
||||
url = sourceURL
|
||||
case .create:
|
||||
if sourceURL.absoluteString.hasSuffix("/") {
|
||||
method = "MKCOL"
|
||||
url = sourceURL
|
||||
} else {
|
||||
fallthrough
|
||||
}
|
||||
case .modify:
|
||||
method = "PUT"
|
||||
url = sourceURL
|
||||
break
|
||||
case .copy(let source, let dest):
|
||||
if source.hasPrefix("file://") {
|
||||
method = "PUT"
|
||||
url = self.url(of: dest)
|
||||
} else if dest.hasPrefix("file://") {
|
||||
method = "GET"
|
||||
url = sourceURL
|
||||
} else {
|
||||
method = "COPY"
|
||||
url = sourceURL
|
||||
}
|
||||
case .move:
|
||||
method = "MOVE"
|
||||
url = sourceURL
|
||||
case .remove:
|
||||
method = "DELETE"
|
||||
url = sourceURL
|
||||
default:
|
||||
fatalError("Unimplemented operation \(operation.description) in \(#file)")
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = method
|
||||
request.set(httpAuthentication: credential, with: credentialType)
|
||||
request.setValue(overwrite ? "T" : "F", forHTTPHeaderField: "Overwrite")
|
||||
if let dest = operation.destination, !dest.hasPrefix("file://") {
|
||||
request.setValue(self.url(of:dest).absoluteString, forHTTPHeaderField: "Destination")
|
||||
}
|
||||
|
||||
return request
|
||||
}
|
||||
|
||||
override func serverError(with code: FileProviderHTTPErrorCode, path: String?, data: Data?) -> FileProviderHTTPError {
|
||||
return FileProviderWebDavError(code: code, path: path ?? "", errorDescription: data.flatMap({ String(data: $0, encoding: .utf8) }), url: self.url(of: path ?? ""))
|
||||
}
|
||||
|
||||
override func multiStatusHandler(source: String, data: Data, completionHandler: SimpleCompletionHandler) {
|
||||
let xresponses = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
|
||||
for xresponse in xresponses where (xresponse.status ?? 0) >= 300 {
|
||||
let code = xresponse.status.flatMap { FileProviderHTTPErrorCode(rawValue: $0) } ?? .internalServerError
|
||||
let error = FileProviderWebDavError(code: code, path: source, errorDescription: String(data: data, encoding: .utf8), url: self.url(of: source))
|
||||
completionHandler?(error)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
fileprivate func registerNotifcation(path: String, eventHandler: (() -> Void)) {
|
||||
/* There is no unified api for monitoring WebDAV server content change/update
|
||||
* Microsoft Exchange uses SUBSCRIBE method, Apple uses push notification system.
|
||||
@@ -437,36 +339,58 @@ extension WebDAVFileProvider: FileProviderReadWrite {
|
||||
}
|
||||
fileprivate func unregisterNotifcation(path: String) {
|
||||
NotImplemented()
|
||||
}
|
||||
}*/
|
||||
// TODO: implements methods for lock mechanism
|
||||
}
|
||||
|
||||
extension WebDAVFileProvider: FileProvider {
|
||||
open func copy(with zone: NSZone? = nil) -> Any {
|
||||
let copy = WebDAVFileProvider(baseURL: self.baseURL!, credential: self.credential, cache: self.cache)!
|
||||
copy.currentPath = self.currentPath
|
||||
copy.delegate = self.delegate
|
||||
copy.fileOperationDelegate = self.fileOperationDelegate
|
||||
copy.useCache = self.useCache
|
||||
copy.validatingCache = self.validatingCache
|
||||
return copy
|
||||
extension WebDAVFileProvider: ExtendedFileProvider {
|
||||
open func thumbnailOfFileSupported(path: String) -> Bool {
|
||||
guard self.baseURL?.host?.contains("dav.yandex.") ?? false else {
|
||||
return false
|
||||
}
|
||||
let supportedExt: [String] = ["jpg", "jpeg", "png", "gif"]
|
||||
return supportedExt.contains((path as NSString).pathExtension)
|
||||
}
|
||||
|
||||
open func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((ImageClass?, Error?) -> Void)) {
|
||||
guard self.baseURL?.host?.contains("dav.yandex.") ?? false else {
|
||||
dispatch_queue.async {
|
||||
completionHandler(nil, self.throwError(path, code: URLError.resourceUnavailable))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let dimension = dimension ?? CGSize(width: 64, height: 64)
|
||||
let url = URL(string: self.url(of: path).absoluteString + "?preview&size=\(dimension.width)x\(dimension.height)")!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.set(httpAuthentication: credential, with: credentialType)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var responseError: FileProviderWebDavError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderWebDavError(code: rCode, path: url.relativePath, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
|
||||
completionHandler(nil, responseError ?? error)
|
||||
return
|
||||
}
|
||||
|
||||
completionHandler(data.flatMap({ ImageClass(data: $0) }), nil)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
|
||||
open func propertiesOfFileSupported(path: String) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
open func propertiesOfFile(path: String, completionHandler: @escaping (([String : Any], [String], Error?) -> Void)) {
|
||||
dispatch_queue.async {
|
||||
completionHandler([:], [], self.throwError(path, code: URLError.resourceUnavailable))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: WEBDAV XML response implementation
|
||||
|
||||
internal extension WebDAVFileProvider {
|
||||
fileprivate func delegateNotify(_ operation: FileOperationType, error: Error?) {
|
||||
DispatchQueue.main.async(execute: {
|
||||
if error == nil {
|
||||
self.delegate?.fileproviderSucceed(self, operation: operation)
|
||||
} else {
|
||||
self.delegate?.fileproviderFailed(self, operation: operation)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct DavResponse {
|
||||
let href: URL
|
||||
let hrefString: String
|
||||
@@ -475,12 +399,9 @@ struct DavResponse {
|
||||
|
||||
init? (_ node: AEXMLElement, baseURL: URL?) {
|
||||
|
||||
func removeSlash(_ str: String) -> String {
|
||||
if str.hasPrefix("/") {
|
||||
return str.substring(from: str.index(after: str.startIndex))
|
||||
} else {
|
||||
return str
|
||||
}
|
||||
func standardizePath(_ str: String) -> String {
|
||||
let trimmedStr = str.hasPrefix("/") ? str.substring(from: str.index(after: str.startIndex)) : str
|
||||
return trimmedStr.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? str
|
||||
}
|
||||
|
||||
// find node names with namespace
|
||||
@@ -502,9 +423,14 @@ struct DavResponse {
|
||||
guard let hrefString = node[hreftag].value else { return nil }
|
||||
|
||||
// trying to figure out relative path out of href
|
||||
let hrefAbsolute = URL(string: hrefString, relativeTo: baseURL)?.absoluteString ?? hrefString
|
||||
let relativePath = hrefAbsolute.replacingOccurrences(of: baseURL?.absoluteString ?? "", with: "", options: .anchored, range: nil)
|
||||
let hrefURL = URL(string: removeSlash(relativePath), relativeTo: baseURL) ?? baseURL
|
||||
let hrefAbsolute = URL(string: hrefString, relativeTo: baseURL)?.absoluteURL
|
||||
let relativePath: String
|
||||
if hrefAbsolute?.host?.replacingOccurrences(of: "www.", with: "", options: .anchored) == baseURL?.host?.replacingOccurrences(of: "www.", with: "", options: .anchored) {
|
||||
relativePath = hrefAbsolute?.path.replacingOccurrences(of: baseURL?.absoluteURL.path ?? "", with: "", options: .anchored, range: nil) ?? hrefString
|
||||
} else {
|
||||
relativePath = hrefAbsolute?.absoluteString.replacingOccurrences(of: baseURL?.absoluteString ?? "", with: "", options: .anchored, range: nil) ?? hrefString
|
||||
}
|
||||
let hrefURL = URL(string: standardizePath(relativePath), relativeTo: baseURL) ?? baseURL
|
||||
|
||||
guard let href = hrefURL?.standardized else { return nil }
|
||||
|
||||
@@ -542,69 +468,101 @@ struct DavResponse {
|
||||
}
|
||||
|
||||
static func parse(xmlResponse: Data, baseURL: URL?) -> [DavResponse] {
|
||||
guard let xml = try? AEXMLDocument(xml: xmlResponse) else { return [] }
|
||||
var result = [DavResponse]()
|
||||
do {
|
||||
let xml = try AEXMLDocument(xml: xmlResponse)
|
||||
var rootnode = xml.root
|
||||
var responsetag = "response"
|
||||
for node in rootnode.all ?? [] where node.name.lowercased().hasSuffix("multistatus") {
|
||||
rootnode = node
|
||||
var rootnode = xml.root
|
||||
var responsetag = "response"
|
||||
for node in rootnode.all ?? [] where node.name.lowercased().hasSuffix("multistatus") {
|
||||
rootnode = node
|
||||
}
|
||||
for node in rootnode.children where node.name.lowercased().hasSuffix("response") {
|
||||
responsetag = node.name
|
||||
break
|
||||
}
|
||||
for responseNode in rootnode[responsetag].all ?? [] {
|
||||
if let davResponse = DavResponse(responseNode, baseURL: baseURL) {
|
||||
result.append(davResponse)
|
||||
}
|
||||
for node in rootnode.children where node.name.lowercased().hasSuffix("response") {
|
||||
responsetag = node.name
|
||||
break
|
||||
}
|
||||
for responseNode in rootnode[responsetag].all ?? [] {
|
||||
if let davResponse = DavResponse(responseNode, baseURL: baseURL) {
|
||||
result.append(davResponse)
|
||||
}
|
||||
}
|
||||
} catch _ {
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/// Containts path, url and attributes of a WebDAV file or resource.
|
||||
public final class WebDavFileObject: FileObject {
|
||||
internal init(_ davResponse: DavResponse) {
|
||||
let href = davResponse.href
|
||||
let name = davResponse.prop["displayname"] ?? (davResponse.hrefString.removingPercentEncoding! as NSString).lastPathComponent
|
||||
let name = davResponse.prop["displayname"] ?? davResponse.href.lastPathComponent
|
||||
let relativePath = href.relativePath
|
||||
let path = relativePath.hasPrefix("/") ? relativePath : ("/" + relativePath)
|
||||
super.init(url: href, name: name, path: path)
|
||||
self.size = Int64(davResponse.prop["getcontentlength"] ?? "-1") ?? NSURLSessionTransferSizeUnknown
|
||||
self.creationDate = resolve(dateString: davResponse.prop["creationdate"] ?? "")
|
||||
self.modifiedDate = resolve(dateString: davResponse.prop["getlastmodified"] ?? "")
|
||||
self.creationDate = Date(rfcString: davResponse.prop["creationdate"] ?? "")
|
||||
self.modifiedDate = Date(rfcString: davResponse.prop["getlastmodified"] ?? "")
|
||||
self.contentType = davResponse.prop["getcontenttype"] ?? "octet/stream"
|
||||
self.isHidden = (Int(davResponse.prop["ishidden"] ?? "0") ?? 0) > 0
|
||||
self.type = self.contentType == "httpd/unix-directory" ? .directory : .regular
|
||||
self.entryTag = davResponse.prop["getetag"]
|
||||
}
|
||||
|
||||
/// MIME type of the file
|
||||
/// MIME type of the file.
|
||||
open internal(set) var contentType: String {
|
||||
get {
|
||||
return allValues[.mimeType] as? String ?? ""
|
||||
return allValues[.mimeTypeKey] as? String ?? ""
|
||||
}
|
||||
set {
|
||||
allValues[.mimeType] = newValue
|
||||
allValues[.mimeTypeKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP E-Tag, can be used to mark changed files
|
||||
/// HTTP E-Tag, can be used to mark changed files.
|
||||
open internal(set) var entryTag: String? {
|
||||
get {
|
||||
return allValues[.entryTag] as? String
|
||||
return allValues[.entryTagKey] as? String
|
||||
}
|
||||
set {
|
||||
allValues[.entryTag] = newValue
|
||||
allValues[.entryTagKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
internal class func resourceKeyToDAVProp(_ key: URLResourceKey) -> String? {
|
||||
switch key {
|
||||
case URLResourceKey.fileSizeKey:
|
||||
return "getcontentlength"
|
||||
case URLResourceKey.creationDateKey:
|
||||
return "creationdate"
|
||||
case URLResourceKey.contentModificationDateKey:
|
||||
return "getlastmodified"
|
||||
case URLResourceKey.fileResourceTypeKey, URLResourceKey.mimeTypeKey:
|
||||
return "getcontenttype"
|
||||
case URLResourceKey.isHiddenKey:
|
||||
return "ishidden"
|
||||
case URLResourceKey.entryTagKey:
|
||||
return "getetag"
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
internal class func propString(_ keys: [URLResourceKey]) -> String {
|
||||
var propKeys = ""
|
||||
for item in keys {
|
||||
if let prop = WebDavFileObject.resourceKeyToDAVProp(item) {
|
||||
propKeys += "<D:prop><D:\(prop)/></D:prop>"
|
||||
}
|
||||
}
|
||||
if propKeys.isEmpty {
|
||||
propKeys = "<D:allprop/>"
|
||||
}
|
||||
return propKeys
|
||||
}
|
||||
}
|
||||
|
||||
/// Error returned by WebDAV server when trying to access or do operations on a file or folder.
|
||||
public struct FileProviderWebDavError: FileProviderHTTPError {
|
||||
public let code: FileProviderHTTPErrorCode
|
||||
public let path: String
|
||||
public let errorDescription: String?
|
||||
/// URL of resource caused error.
|
||||
public let url: URL
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
Reference in New Issue
Block a user