Compare commits
156 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 435ed37d93 | |||
| 9e4a803345 | |||
| 0e53036855 | |||
| 87317c1d42 | |||
| 4c35e446a7 | |||
| 47516c63ad | |||
| 6cd5b7d8c5 | |||
| 598444c90f | |||
| 370866442a | |||
| 9cc9a6a96a | |||
| 2969af28b8 | |||
| d41e86b30a | |||
| aaa1def9e7 | |||
| 024a3637b4 | |||
| b20fc3efe5 | |||
| 682dd40072 | |||
| 3f35e600cd | |||
| 9cd2fa2e3a | |||
| 7be6985454 | |||
| 04ac3e22e7 | |||
| c8de7fdb69 | |||
| 42b879d4e2 | |||
| 5800c9a2ec | |||
| 7c9b2bf09a | |||
| ba08cdda1d | |||
| b380685932 | |||
| 5a5beb6891 | |||
| 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 | |||
| 42579be371 | |||
| edd4914ca7 | |||
| bd3e1bd74f | |||
| 371c481482 | |||
| dea1d01bf3 | |||
| ae4cd1dff3 | |||
| 72520973e9 | |||
| 63016285af | |||
| 3245c9df03 | |||
| ee1fa89747 | |||
| 4a6b25deac | |||
| 2423cbd0f6 | |||
| 480099ca8a | |||
| 755bf6bf84 |
+67
-18
@@ -1,32 +1,81 @@
|
||||
language: objective-c
|
||||
|
||||
osx_image: xcode8.2
|
||||
|
||||
xcode_project: FileProvider.xcodeproj
|
||||
xcode_project: $PROJECTNAME.xcodeproj
|
||||
env:
|
||||
global:
|
||||
- PROJECTNAME="FileProvider"
|
||||
- FRAMEWORK_NAME="FileProvider.framework"
|
||||
- PROJECT="$PROJECTNAME.xcodeproj"
|
||||
- FRAMEWORK_NAME="$PROJECTNAME.framework"
|
||||
- IOS_FRAMEWORK_SCHEME="$PROJECTNAME iOS"
|
||||
- MACOS_FRAMEWORK_SCHEME="$PROJECTNAME OSX"
|
||||
- TVOS_FRAMEWORK_SCHEME="$PROJECTNAME tvOS"
|
||||
- IOS_SDK=iphonesimulator
|
||||
- MACOS_SDK=macosx
|
||||
- TVOS_SDK=appletvsimulator
|
||||
matrix:
|
||||
- SHCEME="FileProvider OSX" SDK="macosx" ACTION="build"
|
||||
- SHCEME="FileProvider iOS" SDK="iphonesimulator" ACTION="build"
|
||||
- SHCEME="FileProvider tvOS" SDK="appletvsimulator" ACTION="build"
|
||||
- DESTINATION="OS=10.2,name=iPad Air 2" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="NO" CARTHAGEDEPLOY="YES"
|
||||
- DESTINATION="OS=9.0,name=iPhone 6" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="YES" CARTHAGEDEPLOY="NO"
|
||||
- DESTINATION="OS=8.1,name=iPhone 4S" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="NO" CARTHAGEDEPLOY="NO"
|
||||
|
||||
- DESTINATION="OS=10.1,name=Apple TV 1080p" SCHEME="$TVOS_FRAMEWORK_SCHEME" SDK="$TVOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="NO" CARTHAGEDEPLOY="NO"
|
||||
# - 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="NO" CARTHAGEDEPLOY="NO"
|
||||
before_install:
|
||||
- gem install xcpretty --no-rdoc --no-ri --no-document --quiet
|
||||
- brew update
|
||||
- brew outdated carthage || brew upgrade carthage
|
||||
- gem install cocoapods --no-rdoc --no-ri --no-document --quiet
|
||||
# - gem install xcpretty-travis-formatter
|
||||
|
||||
script:
|
||||
- set pipefail
|
||||
- set -o pipefail
|
||||
- xcodebuild -version
|
||||
- xcodebuild -project $PROJECT.xcodeproj -scheme "$SCHEME" -sdk $SDK $ACTION ONLY_ACTIVE_ARCH=NO | xcpretty
|
||||
# - pod lib lint --quick
|
||||
# after_success:
|
||||
|
||||
# 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;
|
||||
fi
|
||||
|
||||
# Build Framework in Debug and Run Tests if specified
|
||||
- if [ $RUN_TESTS == "YES" ]; then
|
||||
xcodebuild -project "$PROJECT" -scheme "$SCHEME" -sdk "$SDK" -destination "$DESTINATION" -configuration Debug ONLY_ACTIVE_ARCH=NO ENABLE_TESTABILITY=YES test | xcpretty;
|
||||
else
|
||||
xcodebuild -project "$PROJECT" -scheme "$SCHEME" -sdk "$SDK" -destination "$DESTINATION" -configuration Debug ONLY_ACTIVE_ARCH=NO build | xcpretty;
|
||||
fi
|
||||
|
||||
# Build Framework in Release and Run Tests if specified
|
||||
- if [ $RUN_TESTS == "YES" ]; then
|
||||
xcodebuild -project "$PROJECT" -scheme "$SCHEME" -sdk "$SDK" -destination "$DESTINATION" -configuration Release ONLY_ACTIVE_ARCH=NO ENABLE_TESTABILITY=YES test | xcpretty;
|
||||
else
|
||||
xcodebuild -project "$PROJECT" -scheme "$SCHEME" -sdk "$SDK" -destination "$DESTINATION" -configuration Release ONLY_ACTIVE_ARCH=NO build | xcpretty;
|
||||
fi
|
||||
|
||||
# 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:
|
||||
- carthage build --no-skip-current
|
||||
- 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:
|
||||
file: $PROJECTNAME.framework.zip
|
||||
provider: releases
|
||||
api_key: "$GITHUBTOKEN"
|
||||
file: $FRAMEWORK_NAME.zip
|
||||
skip_cleanup: true
|
||||
on:
|
||||
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.2"
|
||||
s.summary = "FileManager replacement for Local and Remote (WebDAV/Dropbox/OneDrive/SMB2) files on iOS and macOS."
|
||||
s.name = "FilesProvider"
|
||||
s.version = "0.21.0"
|
||||
s.summary = "FileManager replacement for Local and Remote (WebDAV/FTP/Dropbox/OneDrive/SMB2) files on iOS and macOS."
|
||||
|
||||
# This description is used to generate tags and improve search results.
|
||||
# * Think: What does it do? Why did you write it? What is the focus?
|
||||
@@ -58,7 +58,7 @@ Pod::Spec.new do |s|
|
||||
s.author = { "Amir Abbas Mousavian" => "a.mosavian@gmail.com" }
|
||||
# Or just: s.author = "Amir Abbas Mousavian"
|
||||
# s.authors = { "Amir Abbas Mousavian" => "a.mosavian@gmail.com" }
|
||||
# s.social_media_url = "https://twitter.com/amosavian"
|
||||
s.social_media_url = "https://twitter.com/amosavian"
|
||||
|
||||
# ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
|
||||
#
|
||||
@@ -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>"; };
|
||||
@@ -162,18 +182,15 @@
|
||||
79BD63A71E2CC2940035128C /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.2.sdk/System/Library/Frameworks/CoreGraphics.framework; sourceTree = DEVELOPER_DIR; };
|
||||
79BD63A91E2CC2BB0035128C /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.2.sdk/System/Library/Frameworks/AVFoundation.framework; sourceTree = DEVELOPER_DIR; };
|
||||
79BD63AB1E2CC2C20035128C /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.2.sdk/System/Library/Frameworks/ImageIO.framework; sourceTree = DEVELOPER_DIR; };
|
||||
79BD63AD1E2CC2EB0035128C /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.2.sdk/System/Library/Frameworks/MediaPlayer.framework; sourceTree = DEVELOPER_DIR; };
|
||||
79BD63AF1E2CC3300035128C /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.tbd; path = usr/lib/libxml2.tbd; sourceTree = SDKROOT; };
|
||||
79BD63B11E2CC3350035128C /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = System/Library/Frameworks/ImageIO.framework; sourceTree = SDKROOT; };
|
||||
79BD63B31E2CC33D0035128C /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = System/Library/Frameworks/MediaPlayer.framework; sourceTree = SDKROOT; };
|
||||
79BD63B51E2CC3860035128C /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; };
|
||||
79BD63B71E2CC38D0035128C /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; };
|
||||
79BD63B91E2CC39B0035128C /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.tbd; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/usr/lib/libxml2.tbd; sourceTree = DEVELOPER_DIR; };
|
||||
79BD63BB1E2CC3B90035128C /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/System/Library/Frameworks/MediaPlayer.framework; sourceTree = DEVELOPER_DIR; };
|
||||
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 */
|
||||
@@ -221,14 +238,11 @@
|
||||
79BD63C11E2CC3D30035128C /* AVFoundation.framework */,
|
||||
79BD63BF1E2CC3CD0035128C /* CoreGraphics.framework */,
|
||||
79BD63BD1E2CC3C20035128C /* ImageIO.framework */,
|
||||
79BD63BB1E2CC3B90035128C /* MediaPlayer.framework */,
|
||||
79BD63B91E2CC39B0035128C /* libxml2.tbd */,
|
||||
79BD63B71E2CC38D0035128C /* AVFoundation.framework */,
|
||||
79BD63B51E2CC3860035128C /* CoreFoundation.framework */,
|
||||
79BD63B31E2CC33D0035128C /* MediaPlayer.framework */,
|
||||
79BD63B11E2CC3350035128C /* ImageIO.framework */,
|
||||
79BD63AF1E2CC3300035128C /* libxml2.tbd */,
|
||||
79BD63AD1E2CC2EB0035128C /* MediaPlayer.framework */,
|
||||
79BD63AB1E2CC2C20035128C /* ImageIO.framework */,
|
||||
79BD63A91E2CC2BB0035128C /* AVFoundation.framework */,
|
||||
79BD63A71E2CC2940035128C /* CoreGraphics.framework */,
|
||||
@@ -249,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 = (
|
||||
@@ -263,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>";
|
||||
@@ -284,6 +307,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7924B18B1D89DAE000589DB7 /* AEXML */,
|
||||
793CCE281F4B8C3600BC8288 /* Extensions */,
|
||||
799396991D48C02300086753 /* SMBTypes */,
|
||||
799396941D48C02300086753 /* FileProvider.h */,
|
||||
799396951D48C02300086753 /* FileProvider.swift */,
|
||||
@@ -292,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>";
|
||||
@@ -359,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 */,
|
||||
@@ -372,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 */,
|
||||
@@ -390,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 */,
|
||||
@@ -408,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 */
|
||||
@@ -433,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;
|
||||
@@ -445,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 */
|
||||
@@ -485,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 */,
|
||||
@@ -507,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 */,
|
||||
@@ -525,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 */,
|
||||
@@ -547,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 */,
|
||||
@@ -565,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 */,
|
||||
@@ -587,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 */,
|
||||
@@ -603,7 +645,7 @@
|
||||
799396601D48B7BF00086753 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_VERSION_STRING = 0.12.2;
|
||||
BUNDLE_VERSION_STRING = 0.21.0;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
@@ -613,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;
|
||||
@@ -626,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;
|
||||
@@ -633,7 +677,7 @@
|
||||
799396611D48B7BF00086753 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_VERSION_STRING = 0.12.2;
|
||||
BUNDLE_VERSION_STRING = 0.21.0;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
@@ -653,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;
|
||||
};
|
||||
@@ -663,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;
|
||||
@@ -700,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";
|
||||
@@ -716,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;
|
||||
@@ -748,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";
|
||||
@@ -805,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";
|
||||
@@ -856,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";
|
||||
@@ -908,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";
|
||||
@@ -958,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;
|
||||
@@ -973,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 */,
|
||||
@@ -982,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 */,
|
||||
@@ -991,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 */,
|
||||
@@ -1000,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,23 +149,25 @@ 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. Also you can use [auth0/Lock](https://github.com/auth0/Lock.iOS-OSX) which provides graphical user interface.
|
||||
* 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.
|
||||
|
||||
For interaction with UI, set delegate variable of `FileProvider` object
|
||||
|
||||
You can use `absoluteURL()` method if provider to get direct access url (local or remote files) for some file systems which allows to do so (Dropbox doesn't support and returns path simply wrapped in URL)
|
||||
You can use `url(of:)` method if provider to get direct access url (local or remote files) for some file systems which allows to do so (Dropbox doesn't support and returns path simply wrapped in URL)
|
||||
|
||||
### Delegates
|
||||
|
||||
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,25 +335,92 @@ let data = "What's up Newyork!".data(encoding: .utf8)
|
||||
documentsProvider.writeContents(path: "old.txt", content: data, atomically: true, completionHandler: nil)
|
||||
```
|
||||
|
||||
### Operation Handle
|
||||
### Copying Files to and From Local Storage
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
It's not supported by native `(NS)FileManager` so `LocalFileProvider`, but this functionality will be added to future `PosixFileProvider` class.
|
||||
To upload a file:
|
||||
|
||||
### Monitoring FIle Changes
|
||||
```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:
|
||||
|
||||
```swift
|
||||
// To setup a new UndoManager:
|
||||
documentsProvider.setupUndoManager()
|
||||
// or if you have an UndoManager object already:
|
||||
documentsProvider.undoManager = self.undoManager
|
||||
|
||||
// e.g.: To undo last operation manually:
|
||||
documentsProvider.undoManager?.undo()
|
||||
```
|
||||
|
||||
You can also bind `UndoManager` object with view controller to use shake gesture and builtin undo support in iOS/macOS, add these code to your ViewController class like this sample code:
|
||||
|
||||
```swift
|
||||
class ViewController: UIViewController
|
||||
override var canBecomeFirstResponder: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override var undoManager: UndoManager? {
|
||||
return (provider as? FileProvideUndoable)?.undoManager
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
// Your code here
|
||||
UIApplication.shared.applicationSupportsShakeToEdit = true
|
||||
self.becomeFirstResponder()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
// Your code here
|
||||
UIApplication.shared.applicationSupportsShakeToEdit = false
|
||||
self.resignFirstResponder()
|
||||
}
|
||||
// The rest of your implementation
|
||||
}
|
||||
```
|
||||
|
||||
### File Coordination
|
||||
|
||||
`LocalFileProvider` and its descendents has a `isCoordinating` property. By setting this, provider will use `NSFileCoordinating` class to do all file operations. It's mandatory for iCloud, while recommended when using shared container or anywhere that simultaneous operations on a file/folder is common.
|
||||
|
||||
### Monitoring File Changes
|
||||
|
||||
You can monitor updates in some file system (Local and SMB2), there is three methods in supporting provider you can use to register a handler, to unregister and to check whether it's being monitored or not. It's useful to find out when new files added or removed from directory and update user interface. The handler will be dispatched to main threads to avoid UI bugs with a 0.25 sec delay.
|
||||
|
||||
```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.
|
||||
@@ -363,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
|
||||
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
|
||||
}
|
||||
@@ -377,7 +451,7 @@ if documentsProvider.thumbnailOfFileSupported(path: path {
|
||||
|
||||
##### Meta-informations
|
||||
|
||||
To get meta-information like image/video taken date, dimension, etc., use (and modify) these example code:
|
||||
To get meta-information like image/video taken date, location, dimension, etc., use (and modify) these example code:
|
||||
|
||||
```swift
|
||||
if documentsProvider..propertiesOfFile(path: file.path, completionHandler: { (propertiesDictionary, keys, error) in
|
||||
@@ -393,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)
|
||||
@@ -402,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
|
||||
@@ -421,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.
|
||||
+530
-302
File diff suppressed because it is too large
Load Diff
+255
-300
@@ -10,100 +10,94 @@
|
||||
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: You can pass file id or rev instead of file path, e.g `"id:1234abcd"` or `"rev:1234abcd"`, to point to a file or folder by ID.
|
||||
|
||||
- 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).
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
- 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.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
|
||||
completionHandler(contents, error)
|
||||
}
|
||||
override open func copy(with zone: NSZone? = nil) -> Any {
|
||||
let copy = DropboxFileProvider(credential: self.credential, cache: self.cache)
|
||||
copy.delegate = self.delegate
|
||||
copy.fileOperationDelegate = self.fileOperationDelegate
|
||||
copy.useCache = self.useCache
|
||||
copy.validatingCache = self.validatingCache
|
||||
return copy
|
||||
}
|
||||
|
||||
open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
|
||||
/**
|
||||
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.
|
||||
|
||||
- Parameters:
|
||||
- path: path to target directory. If empty, root will be iterated.
|
||||
- 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) {
|
||||
let query = NSPredicate(format: "TRUEPREDICATE")
|
||||
_ = searchFiles(path: path, recursive: false, query: query, foundItemHandler: nil, 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`.
|
||||
|
||||
- Parameters:
|
||||
- path: path to target directory. If empty, attributes of root will be returned.
|
||||
- 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) {
|
||||
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 serverError: FileProviderHTTPError?
|
||||
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) {
|
||||
serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) }
|
||||
if let json = data?.deserializeJSON(), let file = DropboxFileObject(json: json) {
|
||||
fileObject = file
|
||||
}
|
||||
}
|
||||
@@ -112,187 +106,176 @@ open class DropboxFileProvider: FileProviderBasicRemote {
|
||||
task.resume()
|
||||
}
|
||||
|
||||
open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) {
|
||||
/// Returns volume/provider information asynchronously.
|
||||
/// - Parameter volumeInfo: Information of filesystem/Provider returned by system/server.
|
||||
open override func storageProperties(completionHandler: @escaping (_ volumeInfo: VolumeObject?) -> 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) {
|
||||
totalSize = ((json["allocation"] as? NSDictionary)?["allocated"] as? NSNumber)?.int64Value ?? -1
|
||||
usedSize = (json["used"] as? NSNumber)?.int64Value ?? 0
|
||||
guard let json = data?.deserializeJSON() else {
|
||||
completionHandler(nil)
|
||||
return
|
||||
}
|
||||
completionHandler(totalSize, usedSize)
|
||||
|
||||
let volume = VolumeObject(allValues: [:])
|
||||
volume.totalCapacity = ((json["allocation"] as? NSDictionary)?["allocated"] as? NSNumber)?.int64Value ?? -1
|
||||
volume.usage = (json["used"] as? NSNumber)?.int64Value ?? 0
|
||||
completionHandler(volume)
|
||||
})
|
||||
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
|
||||
/**
|
||||
Search files inside directory using query asynchronously.
|
||||
|
||||
Sample predicates:
|
||||
```
|
||||
NSPredicate(format: "(name CONTAINS[c] 'hello') && (filesize >= 10000)")
|
||||
NSPredicate(format: "(modifiedDate >= %@)", Date())
|
||||
NSPredicate(format: "(path BEGINSWITH %@)", "folder/child folder")
|
||||
```
|
||||
|
||||
- Note: Don't pass Spotlight predicates to this method directly, use `FileProvider.convertSpotlightPredicateTo()` method to get usable predicate.
|
||||
|
||||
- Important: A file name criteria should be provided for Dropbox.
|
||||
|
||||
- Parameters:
|
||||
- path: location of directory to start search
|
||||
- recursive: Searching subdirectories of path
|
||||
- query: An `NSPredicate` object with keys like `FileObject` members, except `size` which becomes `filesize`.
|
||||
- foundItemHandler: Closure which is called when a file is found
|
||||
- completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured.
|
||||
- files: all files meat the `query` criteria.
|
||||
- error: `Error` returned by server if occured.
|
||||
- Returns: An `Progress` to get progress or cancel progress. Use `completedUnitCount` to iterate count of found items.
|
||||
*/
|
||||
open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? {
|
||||
let queryStr: String?
|
||||
if query.predicateFormat == "TRUEPREDICATE" {
|
||||
queryStr = nil
|
||||
} else {
|
||||
queryStr = query.findValue(forKey: "name", operator: .beginsWith) as? String
|
||||
}
|
||||
let requestHandler = self.listRequest(path: path, queryStr: queryStr, recursive: recursive)
|
||||
let queryIsTruePredicate = query.predicateFormat == "TRUEPREDICATE"
|
||||
return paginated(path, requestHandler: requestHandler,
|
||||
pageHandler: { [weak self] (data, progress) -> (files: [FileObject], error: Error?, newToken: String?) in
|
||||
guard let json = data?.deserializeJSON(), let entries = (json["entries"] ?? json["matches"]) as? [AnyObject] else {
|
||||
let err = self?.urlError(path, code: .badServerResponse)
|
||||
return ([], err, nil)
|
||||
}
|
||||
|
||||
var files = [FileObject]()
|
||||
for entry in entries {
|
||||
if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry), queryIsTruePredicate || query.evaluate(with: file.mapPredicate()) {
|
||||
files.append(file)
|
||||
progress.completedUnitCount += 1
|
||||
foundItemHandler?(file)
|
||||
}
|
||||
}
|
||||
let ncursor: String?
|
||||
if let hasmore = (json["has_more"] as? NSNumber)?.boolValue, hasmore {
|
||||
ncursor = json["cursor"] as? String
|
||||
} else if let hasmore = (json["more"] as? NSNumber)?.boolValue, hasmore {
|
||||
ncursor = (json["start"] as? Int).flatMap(String.init)
|
||||
} else {
|
||||
ncursor = nil
|
||||
}
|
||||
return (files, nil, ncursor)
|
||||
}, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
override func request(for operation: FileOperationType, overwrite: Bool = false, attributes: [URLResourceKey : Any] = [:]) -> URLRequest {
|
||||
|
||||
func uploadRequest(to path: String) -> URLRequest {
|
||||
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?
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.set(httpAuthentication: credential, with: .oAuth2)
|
||||
request.set(httpContentType: .stream)
|
||||
request.set(dropboxArgKey: requestDictionary)
|
||||
return request
|
||||
}
|
||||
|
||||
func downloadRequest(from path: String) -> URLRequest {
|
||||
let url = URL(string: "files/download", relativeTo: contentURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request = URLRequest(url: url)
|
||||
request.set(httpAuthentication: credential, with: .oAuth2)
|
||||
request.set(dropboxArgKey: ["path": correctPath(path)! as NSString])
|
||||
return request
|
||||
}
|
||||
|
||||
// content operations
|
||||
switch operation {
|
||||
case .copy(source: let source, destination: let dest) where dest.lowercased().hasPrefix("file://"):
|
||||
return downloadRequest(from: source)
|
||||
case .fetch(let path):
|
||||
return downloadRequest(from: path)
|
||||
case .copy(source: let source, destination: let dest) where source.lowercased().hasPrefix("file://"):
|
||||
return uploadRequest(to: dest)
|
||||
case .modify(let path):
|
||||
return uploadRequest(to: path)
|
||||
default:
|
||||
return self.apiRequest(for: operation, overwrite: overwrite)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
override func serverError(with code: FileProviderHTTPErrorCode, path: String?, data: Data?) -> FileProviderHTTPError {
|
||||
let errorDesc: String?
|
||||
if let response = data?.deserializeJSON() {
|
||||
errorDesc = (response["user_message"] as? String) ?? (response["error"]?["tag"] as? String)
|
||||
} else {
|
||||
errorDesc = data.flatMap({ String(data: $0, encoding: .utf8) })
|
||||
}
|
||||
return upload_simple(toPath, localFile: localFile, overwrite: overwrite, operation: opType, completionHandler: completionHandler)
|
||||
return FileProviderDropboxError(code: code, path: path ?? "", errorDescription: errorDesc)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
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])
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
override var maxUploadSimpleSupported: Int64 {
|
||||
return 157_286_400 // 150MB
|
||||
}
|
||||
|
||||
/*
|
||||
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,56 +288,25 @@ 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 serverError: FileProviderHTTPError?
|
||||
var link: URL?
|
||||
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) {
|
||||
serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) }
|
||||
if let json = data?.deserializeJSON() {
|
||||
if let linkStr = json["link"] as? String {
|
||||
link = URL(string: linkStr)
|
||||
}
|
||||
@@ -376,31 +328,31 @@ 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 {
|
||||
completionHandler(nil, nil, self.throwError(remoteURL.path, code: URLError.badURL))
|
||||
completionHandler(nil, nil, self.urlError(remoteURL.path, code: .badURL))
|
||||
return
|
||||
}
|
||||
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 serverError: FileProviderHTTPError?
|
||||
var jobId: String?
|
||||
var fileObject: DropboxFileObject?
|
||||
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) {
|
||||
serverError = code.flatMap { self.serverError(with: $0, path: toPath, data: data) }
|
||||
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,15 +376,15 @@ 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?
|
||||
var serverError: FileProviderHTTPError?
|
||||
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
|
||||
serverError = code.flatMap { self.serverError(with: $0, path: toPath, data: data) }
|
||||
}
|
||||
completionHandler?(serverError ?? error)
|
||||
})
|
||||
@@ -441,7 +393,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 +408,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 +423,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.urlError(path, code: .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,22 +481,22 @@ 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 serverError: FileProviderHTTPError?
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
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] {
|
||||
serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) }
|
||||
if let json = data?.deserializeJSON(), let properties = (json["media_info"] as? [String: Any])?["metadata"] as? [String: Any] {
|
||||
(dic, keys) = self.mapMediaInfo(properties)
|
||||
}
|
||||
}
|
||||
@@ -538,15 +505,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
|
||||
}
|
||||
}
|
||||
|
||||
+71
-177
@@ -8,217 +8,121 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct FileProviderDropboxError: Error, CustomStringConvertible {
|
||||
/// 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?
|
||||
|
||||
public var description: String {
|
||||
return code.description
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 = (json["server_modified"] as? String).flatMap(Date.init(rfcString:))
|
||||
self.modifiedDate = (json["client_modified"] as? String).flatMap(Date.init(rfcString:))
|
||||
self.type = (json[".tag"] as? String) == "folder" ? .directory : .regular
|
||||
self.isReadOnly = (json["sharing_info"]?["read_only"] as? NSNumber)?.boolValue ?? false
|
||||
self.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["NSURLServerDateKey"] as? Date
|
||||
return allValues[.serverDateKey] as? Date
|
||||
}
|
||||
set {
|
||||
allValues["NSURLServerDateKey"] = 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.
|
||||
open internal(set) var id: String? {
|
||||
get {
|
||||
return allValues["NSURLDocumentIdentifyKey"] as? String
|
||||
return allValues[.fileResourceIdentifierKey] as? String
|
||||
}
|
||||
set {
|
||||
allValues["NSURLDocumentIdentifyKey"] = newValue
|
||||
allValues[.fileResourceIdentifierKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// 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[URLResourceKey.generationIdentifierKey.rawValue] as? String
|
||||
return allValues[.generationIdentifierKey] as? String
|
||||
}
|
||||
set {
|
||||
allValues[URLResourceKey.generationIdentifierKey.rawValue] = newValue
|
||||
allValues[.generationIdentifierKey] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
var requestDictionary = [String: AnyObject]()
|
||||
let url: URL
|
||||
if let cursor = cursor {
|
||||
url = URL(string: "files/list_folder/continue", relativeTo: apiURL)!
|
||||
requestDictionary["cursor"] = cursor as NSString?
|
||||
internal func correctPath(_ path: String?) -> String? {
|
||||
guard let path = path else { return nil }
|
||||
if path.hasPrefix("id:") || path.hasPrefix("rev:") {
|
||||
return path
|
||||
}
|
||||
var p = path.hasPrefix("/") ? path : "/" + path
|
||||
if p.hasSuffix("/") {
|
||||
p.remove(at: p.index(before:p.endIndex))
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
internal func listRequest(path: String, queryStr: String? = nil, recursive: Bool = false) -> ((_ token: String?) -> URLRequest?) {
|
||||
if let queryStr = queryStr {
|
||||
return { [weak self] (token) -> URLRequest? in
|
||||
guard let `self` = self else { return nil }
|
||||
let url = URL(string: "files/search", relativeTo: self.apiURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.set(httpAuthentication: self.credential, with: .oAuth2)
|
||||
request.set(httpContentType: .json)
|
||||
var requestDictionary: [String: AnyObject] = ["path": self.correctPath(path) as NSString!]
|
||||
requestDictionary["query"] = queryStr as NSString
|
||||
requestDictionary["start"] = NSNumber(value: (token.flatMap( { Int($0) } ) ?? 0))
|
||||
request.httpBody = Data(jsonDictionary: requestDictionary)
|
||||
return request
|
||||
}
|
||||
} else {
|
||||
url = URL(string: "files/list_folder", relativeTo: apiURL)!
|
||||
requestDictionary["path"] = correctPath(path) as NSString?
|
||||
requestDictionary["recursive"] = recursive as NSNumber?
|
||||
}
|
||||
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
|
||||
var responseError: FileProviderDropboxError?
|
||||
var files = prevContents
|
||||
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 {
|
||||
for entry in entries {
|
||||
if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry) {
|
||||
files.append(file)
|
||||
}
|
||||
}
|
||||
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)
|
||||
return
|
||||
}
|
||||
return { [weak self] (token) -> URLRequest? in
|
||||
guard let `self` = self else { return nil }
|
||||
var requestDictionary = [String: AnyObject]()
|
||||
let url: URL
|
||||
if let token = token {
|
||||
url = URL(string: "files/list_folder/continue", relativeTo: self.apiURL)!
|
||||
requestDictionary["cursor"] = token as NSString?
|
||||
} else {
|
||||
url = URL(string: "files/list_folder", relativeTo: self.apiURL)!
|
||||
requestDictionary["path"] = self.correctPath(path) as NSString?
|
||||
requestDictionary["recursive"] = NSNumber(value: recursive)
|
||||
}
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.set(httpAuthentication: self.credential, with: .oAuth2)
|
||||
request.set(httpContentType: .json)
|
||||
request.httpBody = Data(jsonDictionary: requestDictionary)
|
||||
return request
|
||||
}
|
||||
completionHandler(files, nil, responseError ?? error)
|
||||
})
|
||||
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)) {
|
||||
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")
|
||||
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)
|
||||
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 {
|
||||
for entry in entries {
|
||||
if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry) {
|
||||
foundItem(file)
|
||||
}
|
||||
}
|
||||
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)
|
||||
} else {
|
||||
completionHandler(responseError ?? error)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
completionHandler(responseError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
// codebeat:enable[ARITY]
|
||||
|
||||
internal extension DropboxFileProvider {
|
||||
static let dateFormatter = DateFormatter()
|
||||
@@ -236,29 +140,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,49 +10,43 @@ 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:
|
||||
case LocalFileInformationGenerator.imageThumbnailExtensions.contains:
|
||||
return true
|
||||
case LocalFileInformationGenerator.audioThumbnailExtensions:
|
||||
case LocalFileInformationGenerator.audioThumbnailExtensions.contains:
|
||||
return true
|
||||
case LocalFileInformationGenerator.videoThumbnailExtensions:
|
||||
case LocalFileInformationGenerator.videoThumbnailExtensions.contains:
|
||||
return true
|
||||
case LocalFileInformationGenerator.pdfThumbnailExtensions:
|
||||
case LocalFileInformationGenerator.pdfThumbnailExtensions.contains:
|
||||
return true
|
||||
case LocalFileInformationGenerator.officeThumbnailExtensions:
|
||||
case LocalFileInformationGenerator.officeThumbnailExtensions.contains:
|
||||
return true
|
||||
case LocalFileInformationGenerator.customThumbnailExtensions:
|
||||
case LocalFileInformationGenerator.customThumbnailExtensions.contains:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public func propertiesOfFileSupported(path: String) -> Bool {
|
||||
open func propertiesOfFileSupported(path: String) -> Bool {
|
||||
let fileExt = (path as NSString).pathExtension.lowercased()
|
||||
switch fileExt {
|
||||
case LocalFileInformationGenerator.imagePropertiesExtensions:
|
||||
case LocalFileInformationGenerator.imagePropertiesExtensions.contains:
|
||||
return LocalFileInformationGenerator.imageProperties != nil
|
||||
case LocalFileInformationGenerator.audioPropertiesExtensions:
|
||||
case LocalFileInformationGenerator.audioPropertiesExtensions.contains:
|
||||
return LocalFileInformationGenerator.audioProperties != nil
|
||||
case LocalFileInformationGenerator.videoPropertiesExtensions:
|
||||
case LocalFileInformationGenerator.videoPropertiesExtensions.contains:
|
||||
return LocalFileInformationGenerator.videoProperties != nil
|
||||
case LocalFileInformationGenerator.pdfPropertiesExtensions:
|
||||
case LocalFileInformationGenerator.pdfPropertiesExtensions.contains:
|
||||
return LocalFileInformationGenerator.pdfProperties != nil
|
||||
case LocalFileInformationGenerator.archivePropertiesExtensions:
|
||||
case LocalFileInformationGenerator.archivePropertiesExtensions.contains:
|
||||
return LocalFileInformationGenerator.archiveProperties != nil
|
||||
case LocalFileInformationGenerator.officePropertiesExtensions:
|
||||
case LocalFileInformationGenerator.officePropertiesExtensions.contains:
|
||||
return LocalFileInformationGenerator.officeProperties != nil
|
||||
case LocalFileInformationGenerator.customPropertiesExtensions:
|
||||
case LocalFileInformationGenerator.customPropertiesExtensions.contains:
|
||||
return LocalFileInformationGenerator.customProperties != nil
|
||||
|
||||
default:
|
||||
@@ -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
|
||||
@@ -68,17 +62,17 @@ extension LocalFileProvider: ExtendedFileProvider {
|
||||
let fileURL = self.url(of: path)
|
||||
// Create Thumbnail and cache
|
||||
switch fileURL.pathExtension.lowercased() {
|
||||
case LocalFileInformationGenerator.videoThumbnailExtensions:
|
||||
case LocalFileInformationGenerator.videoThumbnailExtensions.contains:
|
||||
thumbnailImage = LocalFileInformationGenerator.videoThumbnail(fileURL)
|
||||
case LocalFileInformationGenerator.audioThumbnailExtensions:
|
||||
case LocalFileInformationGenerator.audioThumbnailExtensions.contains:
|
||||
thumbnailImage = LocalFileInformationGenerator.audioThumbnail(fileURL)
|
||||
case LocalFileInformationGenerator.imageThumbnailExtensions:
|
||||
case LocalFileInformationGenerator.imageThumbnailExtensions.contains:
|
||||
thumbnailImage = LocalFileInformationGenerator.imageThumbnail(fileURL)
|
||||
case LocalFileInformationGenerator.pdfThumbnailExtensions:
|
||||
case LocalFileInformationGenerator.pdfThumbnailExtensions.contains:
|
||||
thumbnailImage = LocalFileInformationGenerator.pdfThumbnail(fileURL)
|
||||
case LocalFileInformationGenerator.officeThumbnailExtensions:
|
||||
case LocalFileInformationGenerator.officeThumbnailExtensions.contains:
|
||||
thumbnailImage = LocalFileInformationGenerator.officeThumbnail(fileURL)
|
||||
case LocalFileInformationGenerator.customThumbnailExtensions:
|
||||
case LocalFileInformationGenerator.customThumbnailExtensions.contains:
|
||||
thumbnailImage = LocalFileInformationGenerator.customThumbnail(fileURL)
|
||||
default:
|
||||
completionHandler(nil, nil)
|
||||
@@ -92,24 +86,24 @@ 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]))?
|
||||
switch fileExt {
|
||||
case LocalFileInformationGenerator.imagePropertiesExtensions:
|
||||
case LocalFileInformationGenerator.imagePropertiesExtensions.contains:
|
||||
getter = LocalFileInformationGenerator.imageProperties
|
||||
case LocalFileInformationGenerator.audioPropertiesExtensions:
|
||||
case LocalFileInformationGenerator.audioPropertiesExtensions.contains:
|
||||
getter = LocalFileInformationGenerator.audioProperties
|
||||
case LocalFileInformationGenerator.videoPropertiesExtensions:
|
||||
case LocalFileInformationGenerator.videoPropertiesExtensions.contains:
|
||||
getter = LocalFileInformationGenerator.videoProperties
|
||||
case LocalFileInformationGenerator.pdfPropertiesExtensions:
|
||||
case LocalFileInformationGenerator.pdfPropertiesExtensions.contains:
|
||||
getter = LocalFileInformationGenerator.pdfProperties
|
||||
case LocalFileInformationGenerator.archivePropertiesExtensions:
|
||||
case LocalFileInformationGenerator.archivePropertiesExtensions.contains:
|
||||
getter = LocalFileInformationGenerator.archiveProperties
|
||||
case LocalFileInformationGenerator.officePropertiesExtensions:
|
||||
case LocalFileInformationGenerator.officePropertiesExtensions.contains:
|
||||
getter = LocalFileInformationGenerator.officeProperties
|
||||
case LocalFileInformationGenerator.customPropertiesExtensions:
|
||||
case LocalFileInformationGenerator.customPropertiesExtensions.contains:
|
||||
getter = LocalFileInformationGenerator.customProperties
|
||||
default:
|
||||
break
|
||||
@@ -123,35 +117,93 @@ extension LocalFileProvider: ExtendedFileProvider {
|
||||
|
||||
completionHandler(dic, keys, nil)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds supported file types and thumbnail/properties generator for specefied type of file
|
||||
public struct LocalFileInformationGenerator {
|
||||
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] = []
|
||||
/// Image extensions supportes for thumbnail.
|
||||
///
|
||||
/// Default: `["jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff", "ico"]`
|
||||
static public var imageThumbnailExtensions: [String] = ["heic", "jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff", "ico"]
|
||||
|
||||
static public var imagePropertiesExtensions: [String] = ["jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff"]
|
||||
static public var audioPropertiesExtensions: [String] = ["mp3", "aac", "m4a", "caf"]
|
||||
static public var videoPropertiesExtensions: [String] = ["mp4", "mpg", "3gp", "mov", "avi"]
|
||||
/// Audio and music extensions supportes for thumbnail.
|
||||
///
|
||||
/// Default: `["mp1", "mp2", "mp3", "mpa", "mpga", "m1a", "m2a", "m4a", "m4b", "m4p", "m4r", "aac", "snd", "caf", "aa", "aax", "adts", "aif", "aifc", "aiff", "au", "flac", "amr", "wav", "wave", "bwf", "ac3", "eac3", "ec3", "cdda"]`
|
||||
static public var audioThumbnailExtensions: [String] = ["mp1", "mp2", "mp3", "mpa", "mpga", "m1a", "m2a", "m4a", "m4b", "m4p", "m4r", "aac", "snd", "caf", "aa", "aax", "adts", "aif", "aifc", "aiff", "au", "flac", "amr", "wav", "wave", "bwf", "ac3", "eac3", "ec3", "cdda"]
|
||||
|
||||
/// Video extensions supportes for thumbnail.
|
||||
///
|
||||
/// Default: `["mov", "mp4", "mpg4", "m4v", "mqv", "mpg", "mpeg", "avi", "vfw", "3g2", "3gp", "3gp2", "3gpp", "qt"]`
|
||||
static public var videoThumbnailExtensions: [String] = ["mov", "mp4", "mpg4", "m4v", "mqv", "mpg", "mpeg", "avi", "vfw", "3g2", "3gp", "3gp2", "3gpp", "qt"]
|
||||
|
||||
/// 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] = ["heic", "jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff"]
|
||||
|
||||
/// Audio and music extensions supportes for properties.
|
||||
///
|
||||
/// Default: `["mp1", "mp2", "mp3", "mpa", "mpga", "m1a", "m2a", "m4a", "m4b", "m4p", "m4r", "aac", "snd", "caf", "aa", "aax", "adts", "aif", "aifc", "aiff", "au", "flac", "amr", "wav", "wave", "bwf", "ac3", "eac3", "ec3", "cdda"]`
|
||||
static public var audioPropertiesExtensions: [String] = ["mp1", "mp2", "mp3", "mpa", "mpga", "m1a", "m2a", "m4a", "m4b", "m4p", "m4r", "aac", "snd", "caf", "aa", "aax", "adts", "aif", "aifc", "aiff", "au", "flac", "amr", "wav", "wave", "bwf", "ac3", "eac3", "ec3", "cdda"]
|
||||
|
||||
/// Video extensions supportes for properties.
|
||||
///
|
||||
/// Default: `["mov", "mp4", "mpg4", "m4v", "mqv", "mpg", "mpeg", "avi", "vfw", "3g2", "3gp", "3gp2", "3gpp", "qt"]`
|
||||
static public var videoPropertiesExtensions: [String] = ["mov", "mp4", "mpg4", "m4v", "mqv", "mpg", "mpeg", "avi", "vfw", "3g2", "3gp", "3gp2", "3gpp", "qt"]
|
||||
|
||||
/// 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)
|
||||
}
|
||||
@@ -160,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)
|
||||
@@ -167,7 +220,7 @@ public struct LocalFileInformationGenerator {
|
||||
let time = CMTimeMake(asset.duration.value / 3, asset.duration.timescale)
|
||||
if let cgImage = try? assetImgGenerate.copyCGImage(at: time, actualTime: nil) {
|
||||
#if os(macOS)
|
||||
return ImageClass(cgImage: cgImage, size: NSSize.zero)
|
||||
return ImageClass(cgImage: cgImage, size: .zero)
|
||||
#else
|
||||
return ImageClass(cgImage: cgImage)
|
||||
#endif
|
||||
@@ -175,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
|
||||
}
|
||||
@@ -228,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]()
|
||||
@@ -268,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]()
|
||||
@@ -314,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 {
|
||||
@@ -338,32 +409,50 @@ 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
|
||||
if dateStr.hasPrefix("D:") {
|
||||
dateStr = date.substring(from: date.characters.index(date.startIndex, offsetBy: 2))
|
||||
}
|
||||
func convertDate(_ date: String?) -> Date? {
|
||||
guard let date = date else { return nil }
|
||||
let dateStr = date.replacingOccurrences(of: "'", with: "").replacingOccurrences(of: "D:", with: "", options: .anchored)
|
||||
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
|
||||
}
|
||||
@@ -374,51 +463,44 @@ 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
|
||||
}
|
||||
|
||||
fileprivate func ~=<T : Equatable>(array: [T], value: T) -> Bool {
|
||||
return array.contains(value)
|
||||
}
|
||||
|
||||
+502
@@ -0,0 +1,502 @@
|
||||
//
|
||||
// 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")
|
||||
/// **FileProvider** count of items in directory
|
||||
public static let childrensCount = URLResourceKey(rawValue: "MFPURLChildrensCount")
|
||||
}
|
||||
|
||||
public extension ProgressUserInfoKey {
|
||||
/// **FileProvider** returns associated `FileProviderOperationType`
|
||||
public static let fileProvderOperationTypeKey = ProgressUserInfoKey("MFPOperationTypeKey")
|
||||
/// **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 = String(describing: value)
|
||||
let quality: Float = 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
|
||||
}
|
||||
}
|
||||
|
||||
#if swift(>=4.0)
|
||||
#else
|
||||
extension String {
|
||||
var count: Int {
|
||||
return self.characters.count
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
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 []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ~=<T>(pattern: (T) -> Bool, value: T) -> Bool {
|
||||
return pattern(value)
|
||||
}
|
||||
|
||||
func hasPrefix(_ prefix: String) -> (_ value: String) -> Bool {
|
||||
return { (value: String) -> Bool in
|
||||
value.hasPrefix(prefix)
|
||||
}
|
||||
}
|
||||
|
||||
func hasSuffix(_ suffix: String) -> (_ value: String) -> Bool {
|
||||
return { (value: String) -> Bool in
|
||||
value.hasSuffix(suffix)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
// 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 var size: Int {
|
||||
return 64
|
||||
}
|
||||
|
||||
static var k: [UInt64] {
|
||||
return [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]
|
||||
}
|
||||
|
||||
// codebeat:disable[ABC]
|
||||
static func calculate(_ message: [UInt8]) -> [UInt8] {
|
||||
var tmpMessage = message
|
||||
|
||||
let len = Self.size
|
||||
|
||||
// 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.append(contentsOf: [UInt8](repeating: 0, count: counter))
|
||||
|
||||
// hash values
|
||||
var hh: [UInt32] = Self.h.map { UInt32($0) }
|
||||
let k = Self.k
|
||||
|
||||
// append message length, in a 64-bit big-endian integer. So now the message length is a multiple of 512 bits.
|
||||
tmpMessage.append(contentsOf: 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: k.count)
|
||||
for x in 0..<M.count {
|
||||
switch (x) {
|
||||
case 0...15:
|
||||
let start: Int = chunk.startIndex + (x * MemoryLayout.size(ofValue: M[x]))
|
||||
let end: Int = 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)
|
||||
let s2 = M[x-16]
|
||||
let s3 = M[x-7]
|
||||
M[x] = s2 &+ s0 &+ s3 &+ s1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var A = hh[0], B = hh[1], C = hh[2], D = hh[3], E = hh[4], F = hh[5], G = hh[6], H = hh[7]
|
||||
|
||||
// Main loop
|
||||
for j in 0..<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(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
|
||||
#if swift(>=4.0)
|
||||
result.append(UInt8(truncatingIfNeeded: item))
|
||||
result.append(UInt8(truncatingIfNeeded: item >> 8))
|
||||
result.append(UInt8(truncatingIfNeeded: item >> 16))
|
||||
result.append(UInt8(truncatingIfNeeded: item >> 24))
|
||||
#else
|
||||
result.append(UInt8(item))
|
||||
result.append(UInt8((item >> 8) & 0xFF))
|
||||
result.append(UInt8((item >> 16) & 0xFF))
|
||||
result.append(UInt8((item >> 24) & 0xFF))
|
||||
#endif
|
||||
|
||||
}
|
||||
return result
|
||||
}
|
||||
// codebeat:enable[ABC]
|
||||
}
|
||||
|
||||
extension SHA2Variant64 {
|
||||
static var size: Int {
|
||||
return 128
|
||||
}
|
||||
|
||||
static var k: [UInt64] {
|
||||
return [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]
|
||||
}
|
||||
|
||||
// codebeat:disable[ABC]
|
||||
static func calculate(_ message: [UInt8]) -> [UInt8] {
|
||||
var tmpMessage = message
|
||||
|
||||
let len = Self.size
|
||||
|
||||
// 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
|
||||
let k = Self.k
|
||||
|
||||
// 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: 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)
|
||||
let s2 = M[x-16]
|
||||
let s3 = M[x-7]
|
||||
M[x] = s2 &+ s0 &+ s3 &+ s1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var A = hh[0], B = hh[1], C = hh[2], D = hh[3], E = hh[4], F = hh[5], G = hh[6], H = hh[7]
|
||||
|
||||
// Main loop
|
||||
for j in 0..<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 &+ 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
|
||||
}
|
||||
// codebeat:enable[ABC]
|
||||
}
|
||||
|
||||
final class SHA256 : SHA2Variant32 {
|
||||
static let h: [UInt64] = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]
|
||||
|
||||
static func resultingArray<T>(_ hh: [T]) -> ArraySlice<T> {
|
||||
return ArraySlice(hh)
|
||||
}
|
||||
}
|
||||
|
||||
final class SHA384 : SHA2Variant64 {
|
||||
static let h: [UInt64] = [0xcbbb9d5dc1059ed8, 0x629a292a367cd507, 0x9159015a3070dd17, 0x152fecd8f70e5939, 0x67332667ffc00b31, 0x8eb44a8768581511, 0xdb0c2e0d64f98fa7, 0x47b5481dbefa4fa4]
|
||||
|
||||
public static func resultingArray<T>(_ hh: [T]) -> ArraySlice<T> {
|
||||
return hh[0..<6]
|
||||
}
|
||||
}
|
||||
|
||||
final class SHA512 : SHA2Variant64 {
|
||||
static let h: [UInt64] = [0x6a09e667f3bcc908, 0xbb67ae8584caa73b, 0x3c6ef372fe94f82b, 0xa54ff53a5f1d36f1, 0x510e527fade682d1, 0x9b05688c2b3e6c1f, 0x1f83d9abfb41bd6b, 0x5be0cd19137e2179]
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
+576
-342
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,867 @@
|
||||
//
|
||||
// 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?
|
||||
/// **OBSOLETED** Current active path used in `contentsOfDirectory(path:completionHandler:)` method.
|
||||
@available(*, obsoleted: 0.22, message: "This property is redundant with almost no use internally.")
|
||||
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
|
||||
_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"
|
||||
urlComponents.path = urlComponents.path.hasSuffix("/") ? urlComponents.path : urlComponents.path + "/"
|
||||
|
||||
self.baseURL = urlComponents.url!.absoluteURL
|
||||
self.passiveMode = passive
|
||||
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.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.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.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.
|
||||
- Parameter contents: An array of `FileObject` identifying the the directory entries.
|
||||
- Parameter 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)
|
||||
}
|
||||
do {
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
guard let response = response, response.hasPrefix("250") || (response.hasPrefix("50") && rfc3659enabled) else {
|
||||
throw self.urlError(path, code: .badServerResponse)
|
||||
}
|
||||
|
||||
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 {
|
||||
throw self.urlError(path, code: .badServerResponse)
|
||||
}
|
||||
let file: FileObject? = rfc3659enabled ? self.parseMLST(lines[1], in: path) : self.parseUnixList(lines[1], in: path)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(file, nil)
|
||||
}
|
||||
} catch {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
open func storageProperties(completionHandler: @escaping (_ volume: VolumeObject?) -> Void) {
|
||||
dispatch_queue.async {
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
|
||||
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? {
|
||||
let progress = Progress(totalUnitCount: -1)
|
||||
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?.trimmingCharacters(in: CharacterSet(charactersIn: "/ ")).addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? (path ?? "")
|
||||
|
||||
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.urlError(localFile.path, code: .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
|
||||
do {
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
if file?.isDirectory ?? false {
|
||||
throw self.urlError(path, code: .fileIsDirectory)
|
||||
}
|
||||
} catch {
|
||||
self.dispatch_queue.async {
|
||||
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
|
||||
var error: NSError?
|
||||
NSFileCoordinator().coordinate(writingItemAt: tempURL, options: .forMoving, writingItemAt: destURL, options: .forReplacing, error: &error, byAccessor: { (tempURL, destURL) in
|
||||
do {
|
||||
try FileManager.default.moveItem(at: tempURL, to: destURL)
|
||||
completionHandler?(nil)
|
||||
} catch {
|
||||
completionHandler?(error)
|
||||
}
|
||||
})
|
||||
if let error = error {
|
||||
completionHandler?(error)
|
||||
}
|
||||
}
|
||||
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 {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
}
|
||||
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: return nil // modify, fetch
|
||||
}
|
||||
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 {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
return
|
||||
}
|
||||
|
||||
self.execute(command: command, on: task, completionHandler: { (response, error) in
|
||||
if let error = error {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let response = response else {
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: self.urlError(sourcePath, code: .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 = .cannotCreateFile
|
||||
case .modify: errorCode = .cannotWriteToFile
|
||||
case .copy:
|
||||
self.fallbackCopy(operation, progress: progress, completionHandler: completionHandler)
|
||||
return
|
||||
case .move: errorCode = .cannotMoveFile
|
||||
case .remove:
|
||||
self.fallbackRemove(operation, progress: progress, on: task, completionHandler: completionHandler)
|
||||
return
|
||||
case .link: errorCode = .cannotWriteToFile
|
||||
default: errorCode = .cannotOpenFile
|
||||
}
|
||||
let error = self.urlError(sourcePath, code: errorCode)
|
||||
progress.cancel()
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
return
|
||||
}
|
||||
|
||||
progress.completedUnitCount = progress.totalUnitCount
|
||||
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
|
||||
do {
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
guard let response = response else {
|
||||
throw self.urlError(sourcePath, code: .badServerResponse)
|
||||
}
|
||||
|
||||
if response.hasPrefix("50") {
|
||||
self.fallbackRecursiveRemove(operation, progress: progress, on: task, completionHandler: completionHandler)
|
||||
return
|
||||
}
|
||||
|
||||
if !response.hasPrefix("2") {
|
||||
throw self.urlError(sourcePath, code: .cannotRemoveFile)
|
||||
}
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(nil)
|
||||
}
|
||||
self.delegateNotify(operation)
|
||||
} catch {
|
||||
progress.cancel()
|
||||
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(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,957 @@
|
||||
//
|
||||
// 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.urlError("", code: .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
|
||||
do {
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
guard let data = data, let response = String(data: data, encoding: .utf8) else {
|
||||
throw self.urlError("", code: .cannotParseResponse)
|
||||
}
|
||||
|
||||
guard response.hasPrefix("22") else {
|
||||
throw FileProviderFTPError(message: response)
|
||||
}
|
||||
} catch {
|
||||
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.urlError("", code: .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.urlError("", code: .userAuthenticationRequired))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let error = FileProviderFTPError(message: response)
|
||||
completionHandler(error)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
do {
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
guard let response = response else {
|
||||
throw self.urlError(path, code: .badServerResponse)
|
||||
}
|
||||
|
||||
// successfully logged in
|
||||
if response.hasPrefix("25") {
|
||||
completionHandler(nil)
|
||||
}
|
||||
// not logged in
|
||||
else if response.hasPrefix("55") {
|
||||
throw FileProviderFTPError(message: response)
|
||||
}
|
||||
} catch {
|
||||
completionHandler(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ftpPassive(_ task: FileProviderStreamTask, completionHandler: @escaping (_ dataTask: FileProviderStreamTask?, _ error: Error?) -> Void) {
|
||||
func trimmedNumber(_ s : String) -> String {
|
||||
return s.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
|
||||
}
|
||||
|
||||
self.execute(command: "PASV", on: task) { (response, error) in
|
||||
do {
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
guard let response = response, let destString = response.components(separatedBy: " ").flatMap({ $0 }).last.flatMap(String.init) else {
|
||||
throw self.urlError("", code: .badServerResponse)
|
||||
}
|
||||
|
||||
let destArray = destString.components(separatedBy: ",").flatMap({ UInt32(trimmedNumber($0)) })
|
||||
guard destArray.count == 6 else {
|
||||
throw self.urlError("", code: .badServerResponse)
|
||||
}
|
||||
|
||||
// first 4 elements are ip, 2 next are port, as byte
|
||||
var host = destArray.prefix(4).flatMap(String.init).joined(separator: ".")
|
||||
let portHi = Int(destArray[4]) << 8
|
||||
let portLo = Int(destArray[5])
|
||||
let port = portHi + portLo
|
||||
// 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)
|
||||
} catch {
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
do {
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
guard let response = response else {
|
||||
throw self.urlError("", code: .badServerResponse)
|
||||
}
|
||||
|
||||
guard !response.hasPrefix("5") else {
|
||||
throw self.urlError("", code: .cannotConnectToHost)
|
||||
}
|
||||
|
||||
completionHandler(activeTask, nil)
|
||||
} catch {
|
||||
activeTask.cancel()
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
do {
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
// Successful
|
||||
guard let response = response else {
|
||||
throw self.urlError("", code: .badServerResponse)
|
||||
}
|
||||
|
||||
if response.hasPrefix("35") {
|
||||
completionHandler(nil)
|
||||
} else {
|
||||
throw FileProviderFTPError(message: response, path: "")
|
||||
}
|
||||
} catch {
|
||||
completionHandler(error)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
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.urlError(path, code: .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?
|
||||
|
||||
do {
|
||||
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) {
|
||||
throw error
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if waitResult == .timedOut {
|
||||
throw self.urlError(path, code: .timedOut)
|
||||
}
|
||||
}
|
||||
|
||||
guard let response = String(data: finalData, encoding: .utf8) else {
|
||||
throw self.urlError(path, code: .badServerResponse)
|
||||
}
|
||||
|
||||
let contents: [String] = response.components(separatedBy: "\n").flatMap({ $0.trimmingCharacters(in: .whitespacesAndNewlines) })
|
||||
success = true
|
||||
completionHandler(contents, nil)
|
||||
} catch {
|
||||
completionHandler([], error)
|
||||
}
|
||||
}
|
||||
}) { (response, error) in
|
||||
do {
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
guard let response = response else {
|
||||
throw self.urlError(path, code: .cannotParseResponse)
|
||||
}
|
||||
|
||||
if response.hasPrefix("500") && useMLST {
|
||||
dataTask.cancel()
|
||||
self.serverSupportsRFC3659 = false
|
||||
throw self.urlError(path, code: .unsupportedURL)
|
||||
}
|
||||
|
||||
if !success && !(response.hasPrefix("25") || response.hasPrefix("15")) {
|
||||
throw FileProviderFTPError(message: response, path: path)
|
||||
}
|
||||
} catch {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler([], error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func recursiveList(path: String, useMLST: Bool, foundItemsHandler: ((_ contents: [FileObject]) -> Void)? = nil, completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) -> Progress? {
|
||||
let progress = Progress(totalUnitCount: -1)
|
||||
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.urlError(filePath, code: .badServerResponse))
|
||||
return
|
||||
}
|
||||
|
||||
// Send retreive command
|
||||
let len = 19 /* TYPE response */ + 65 + String(position).count /* REST Response */ + 53 + filePath.count + String(totalSize).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.urlError(filePath, code: .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
|
||||
do {
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
guard let response = response else {
|
||||
throw self.urlError(filePath, code: .cannotParseResponse)
|
||||
}
|
||||
|
||||
if !(response.hasPrefix("1") || !response.hasPrefix("2")) {
|
||||
throw FileProviderFTPError(message: response)
|
||||
}
|
||||
} catch {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.urlError(filePath, code: .badServerResponse))
|
||||
return
|
||||
}
|
||||
|
||||
// Send retreive command
|
||||
let len = 19 /* TYPE response */ + 65 + String(position).count /* REST Response */ + 53 + filePath.count + String(totalSize).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.urlError("", code: .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)
|
||||
// Removing temporary file after coordinating
|
||||
NSFileCoordinator().coordinate(writingItemAt: tempURL, options: .forDeleting, error: nil, byAccessor: { (tempURL) in
|
||||
try? FileManager.default.removeItem(at: tempURL)
|
||||
})
|
||||
} catch {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}) { (response, error) in
|
||||
do {
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
guard let response = response else {
|
||||
throw self.urlError(filePath, code: .cannotParseResponse)
|
||||
}
|
||||
|
||||
if !(response.hasPrefix("1") || response.hasPrefix("2")) {
|
||||
throw FileProviderFTPError(message: response)
|
||||
}
|
||||
} catch {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, chunk size is 32KB
|
||||
case 262_144..<1_048_576: chunkSize = 65_536 // 256KB To 1MB, chunk size is 64KB
|
||||
case 1_048_576..<10_485_760: chunkSize = 131_072 // 1MB To 10MB, chunk size is 128KB
|
||||
case 10_048_576..<33_554_432: chunkSize = 262_144 // 10MB To 32MB, chunk size is 256KB
|
||||
default: chunkSize = 524_288 // Larger than 32MB, chunk 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 let error = error {
|
||||
completionHandler(error)
|
||||
return
|
||||
}
|
||||
|
||||
if waitResult == .timedOut {
|
||||
error = self.urlError(filePath, code: .timedOut)
|
||||
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.urlError(filePath, code: .badServerResponse))
|
||||
return
|
||||
}
|
||||
|
||||
// Send retreive command
|
||||
var success = false
|
||||
let len = 19 /* TYPE response */ + 65 + String(position).count /* REST Response */ + 44 + filePath.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
|
||||
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 }
|
||||
|
||||
do {
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
guard let response = response else {
|
||||
throw self.urlError(filePath, code: .cannotParseResponse)
|
||||
}
|
||||
|
||||
if !(response.hasPrefix("1") || response.hasPrefix("2")) {
|
||||
throw FileProviderFTPError(message: response)
|
||||
}
|
||||
|
||||
completionHandler(nil)
|
||||
} catch {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ftpQuit(_ task: FileProviderStreamTask) {
|
||||
self.execute(command: "QUIT", on: task) { (_, _) in
|
||||
//task.closeRead()
|
||||
//task.closeWrite()
|
||||
}
|
||||
}
|
||||
|
||||
func ftpPath(_ apath: String) -> String {
|
||||
// path of base url should be concreted into file path! And remove final slash
|
||||
var path = baseURL!.appendingPathComponent(apath).path.replacingOccurrences(of: "/", with: "", options: [.anchored, .backwards])
|
||||
|
||||
// Fixing slashes
|
||||
if !path.hasPrefix("/") {
|
||||
path = "/" + 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 }
|
||||
let path = (path as NSString).appendingPathComponent(name).replacingOccurrences(of: "/", with: "", options: .anchored)
|
||||
|
||||
let file = FileObject(url: url(of: path), name: name, path: path)
|
||||
#if swift(>=4.0)
|
||||
let typeChar = posixPermission.first ?? Character(" ")
|
||||
#else
|
||||
let typeChar = posixPermission.characters.first ?? Character(" ")
|
||||
#endif
|
||||
switch String(typeChar) {
|
||||
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)
|
||||
}
|
||||
correctedPath = correctedPath.replacingOccurrences(of: "/", with: "", options: .anchored)
|
||||
|
||||
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"
|
||||
#if swift(>=4.0)
|
||||
let startIndex = (message.index(of: "-") ?? message.index(of: " ")) ?? message.startIndex
|
||||
self.code = Int(message[..<startIndex].trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
|
||||
#else
|
||||
let startIndex = (message.characters.index(of: "-") ?? message.characters.index(of: " ")) ?? message.startIndex
|
||||
self.code = Int(message.substring(to: startIndex).trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
|
||||
#endif
|
||||
self.path = path
|
||||
if code > 0 {
|
||||
#if swift(>=4.0)
|
||||
self.errorDescription = message[startIndex...].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
#else
|
||||
self.errorDescription = message.substring(from: startIndex).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
#endif
|
||||
} else {
|
||||
self.errorDescription = message
|
||||
}
|
||||
}
|
||||
}
|
||||
+200
-118
@@ -8,123 +8,128 @@
|
||||
|
||||
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: [String: Any]
|
||||
open internal(set) var allValues: [URLResourceKey: Any]
|
||||
|
||||
internal init(allValues: [String: Any]) {
|
||||
public init(allValues: [URLResourceKey: Any]) {
|
||||
self.allValues = allValues
|
||||
}
|
||||
|
||||
internal init(url: URL, name: String, path: String) {
|
||||
self.allValues = [String: Any]()
|
||||
self.url = url
|
||||
internal init(url: URL?, name: String, path: String) {
|
||||
self.allValues = [URLResourceKey: Any]()
|
||||
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["NSURLFileURLKey"] 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["NSURLFileURLKey"] = newValue
|
||||
allValues[.fileURLKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Name of the file, usually equals with the last path component
|
||||
open internal(set) var name: String {
|
||||
get {
|
||||
return allValues[URLResourceKey.nameKey.rawValue] as! String
|
||||
return allValues[.nameKey] as! String
|
||||
}
|
||||
set {
|
||||
allValues[URLResourceKey.nameKey.rawValue] = newValue
|
||||
allValues[.nameKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Relative path of file object
|
||||
open internal(set) var path: String {
|
||||
get {
|
||||
return allValues[URLResourceKey.pathKey.rawValue] as! String
|
||||
return allValues[.pathKey] as! String
|
||||
}
|
||||
set {
|
||||
allValues[URLResourceKey.pathKey.rawValue] = newValue
|
||||
allValues[.pathKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Size of file on disk, return -1 for directories.
|
||||
open internal(set) var size: Int64 {
|
||||
get {
|
||||
return allValues[URLResourceKey.fileSizeKey.rawValue] as? Int64 ?? -1
|
||||
return allValues[.fileSizeKey] as? Int64 ?? -1
|
||||
}
|
||||
set {
|
||||
allValues[URLResourceKey.fileSizeKey.rawValue] = newValue
|
||||
allValues[.fileSizeKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Count of children items of a driectory.
|
||||
open internal(set) var childrensCount: Int? {
|
||||
get {
|
||||
return allValues[.childrensCount] as? Int
|
||||
}
|
||||
set {
|
||||
allValues[.childrensCount] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// The time contents of file has been created, returns nil if not set
|
||||
open internal(set) var creationDate: Date? {
|
||||
get {
|
||||
return allValues[URLResourceKey.creationDateKey.rawValue] as? Date
|
||||
return allValues[.creationDateKey] as? Date
|
||||
}
|
||||
set {
|
||||
allValues[URLResourceKey.creationDateKey.rawValue] = newValue
|
||||
allValues[.creationDateKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// The time contents of file has been modified, returns nil if not set
|
||||
open internal(set) var modifiedDate: Date? {
|
||||
get {
|
||||
return allValues[URLResourceKey.contentModificationDateKey.rawValue] as? Date
|
||||
return allValues[.contentModificationDateKey] as? Date
|
||||
}
|
||||
set {
|
||||
allValues[URLResourceKey.contentModificationDateKey.rawValue] = newValue
|
||||
allValues[.contentModificationDateKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// return resource type of file, usually directory, regular or symLink
|
||||
open internal(set) var type: URLFileResourceType? {
|
||||
open internal(set) var type: URLFileResourceType {
|
||||
get {
|
||||
return allValues[URLResourceKey.fileResourceTypeKey.rawValue] as? URLFileResourceType
|
||||
return allValues[.fileResourceTypeKey] as? URLFileResourceType ?? .unknown
|
||||
}
|
||||
set {
|
||||
allValues[URLResourceKey.fileResourceTypeKey.rawValue] = newValue
|
||||
allValues[.fileResourceTypeKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// **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 {
|
||||
get {
|
||||
return allValues[URLResourceKey.isHiddenKey.rawValue] as? Bool ?? false
|
||||
return allValues[.isHiddenKey] as? Bool ?? false
|
||||
}
|
||||
set {
|
||||
allValues[URLResourceKey.isHiddenKey.rawValue] = newValue
|
||||
allValues[.isHiddenKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// File can not be written
|
||||
open internal(set) var isReadOnly: Bool {
|
||||
get {
|
||||
return !(allValues[URLResourceKey.isWritableKey.rawValue] as? Bool ?? true)
|
||||
return !(allValues[.isWritableKey] as? Bool ?? true)
|
||||
}
|
||||
set {
|
||||
allValues[URLResourceKey.isWritableKey.rawValue] = !newValue
|
||||
allValues[.isWritableKey] = !newValue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,39 +147,167 @@ 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 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"
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
/// Containts attributes of a provider.
|
||||
open class VolumeObject {
|
||||
/// A `Dictionary` contains volume information, using `URLResourceKey` keys.
|
||||
open internal(set) var allValues: [URLResourceKey: Any]
|
||||
|
||||
public init(allValues: [URLResourceKey: Any]) {
|
||||
self.allValues = allValues
|
||||
}
|
||||
|
||||
/// The root directory of the resource’s volume, returned as an `URL` object.
|
||||
open internal(set) var url: URL? {
|
||||
get {
|
||||
return allValues[.volumeURLKey] as? URL
|
||||
}
|
||||
set {
|
||||
allValues[.volumeURLKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// The name of the volume.
|
||||
open internal(set) var name: String? {
|
||||
get {
|
||||
return allValues[.volumeNameKey] as? String
|
||||
}
|
||||
set {
|
||||
allValues[.volumeNameKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// the volume’s capacity in bytes, return -1 if is undetermined.
|
||||
open internal(set) var totalCapacity: Int64 {
|
||||
get {
|
||||
return allValues[.volumeTotalCapacityKey] as? Int64 ?? -1
|
||||
}
|
||||
set {
|
||||
allValues[.volumeTotalCapacityKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// The volume’s available capacity in bytes.
|
||||
open internal(set) var availableCapacity: Int64 {
|
||||
get {
|
||||
return allValues[.volumeAvailableCapacityKey] as? Int64 ?? 0
|
||||
}
|
||||
set {
|
||||
allValues[.volumeAvailableCapacityKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
open internal(set) var usage: Int64 {
|
||||
get {
|
||||
return totalCapacity >= 0 ? totalCapacity - availableCapacity : -availableCapacity
|
||||
}
|
||||
set {
|
||||
availableCapacity = totalCapacity >= 0 ? totalCapacity - newValue : -newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// the volume’s creation date, returned as an `Date` object, or NULL if it cannot be determined
|
||||
open internal(set) var creationDate: Date? {
|
||||
get {
|
||||
return allValues[.volumeCreationDateKey] as? Date
|
||||
}
|
||||
set {
|
||||
allValues[.volumeCreationDateKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Determining whether the volume is read-only
|
||||
open internal(set) var isReadOnly: Bool {
|
||||
get {
|
||||
return allValues[.volumeIsReadOnlyKey] as? Bool ?? false
|
||||
}
|
||||
set {
|
||||
allValues[.volumeIsReadOnlyKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 10.0, macOS 10.12, tvOS 10.0, *)
|
||||
open internal(set) var isEncrypted: Bool {
|
||||
get {
|
||||
return allValues[.volumeIsEncryptedKey] as? Bool ?? false
|
||||
}
|
||||
set {
|
||||
allValues[.volumeIsEncryptedKey] = !newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Sorting FileObject array by given criteria, **not thread-safe**
|
||||
public struct FileObjectSorting {
|
||||
|
||||
@@ -231,7 +364,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,54 +400,3 @@ public struct FileObjectSorting {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element: FileObject {
|
||||
/// Returns a sorted array of `FileObject`s by criterias set in properties.
|
||||
public func sorted(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.sorted(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 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
|
||||
}
|
||||
|
||||
+456
-315
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,550 @@
|
||||
//
|
||||
// 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 derived 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?
|
||||
|
||||
/// **OBSOLETED** Current active path used in `contentsOfDirectory(path:completionHandler:)` method.
|
||||
@available(*, obsoleted: 0.22, message: "This property is redundant with almost no use internally.")
|
||||
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?) {
|
||||
// Make base url absolute and path as directory
|
||||
let urlStr = baseURL?.absoluteString
|
||||
self.baseURL = urlStr.flatMap { $0.hasSuffix("/") ? URL(string: $0) : URL(string: $0 + "/") }
|
||||
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.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 (_ volumeInfo: VolumeObject?) -> 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 { volume in
|
||||
completionHandler(volume != 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), overwrite: overwrite, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
return doOperation(.copy(source: path, destination: toPath), overwrite: overwrite, 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.urlError(localFile.path, code: .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)
|
||||
let cantLoadError = urlError(path, code: .cannotLoadFromNetwork)
|
||||
return self.download_simple(path: path, request: request, operation: operation, completionHandler: { [weak self] (tempURL, error) in
|
||||
do {
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
guard let tempURL = tempURL else {
|
||||
throw cantLoadError
|
||||
}
|
||||
|
||||
var coordError: NSError?
|
||||
NSFileCoordinator().coordinate(writingItemAt: tempURL, options: .forMoving, writingItemAt: destURL, options: .forReplacing, error: &coordError, byAccessor: { (tempURL, destURL) in
|
||||
do {
|
||||
try FileManager.default.moveItem(at: tempURL, to: destURL)
|
||||
|
||||
completionHandler?(nil)
|
||||
self?.delegateNotify(operation)
|
||||
} catch {
|
||||
completionHandler?(error)
|
||||
self?.delegateNotify(operation, error: error)
|
||||
}
|
||||
})
|
||||
|
||||
if let error = coordError {
|
||||
throw error
|
||||
}
|
||||
} catch {
|
||||
completionHandler?(error)
|
||||
self?.delegateNotify(operation, error: error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
Progressively fetch data of file and returns fetched data in `progressHandler`.
|
||||
If path specifies a directory, or if some other error occurs, data will be nil.
|
||||
|
||||
- Parameters:
|
||||
- path: Path of file.
|
||||
- progressHandler: a closure called every time a new `Data` is available.
|
||||
- position: start position of data fetched.
|
||||
- data: a portion of contents of file in a `Data` object.
|
||||
- completionHandler: a closure with result of file contents or error.
|
||||
- error: `Error` returned by system if occured.
|
||||
- Returns: An `Progress` to get progress or cancel progress.
|
||||
*/
|
||||
open func contents(path: String, progressHandler: @escaping (_ position: Int64, _ data: Data) -> Void, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let operation = FileOperationType.fetch(path: path)
|
||||
let request = self.request(for: operation)
|
||||
var position: Int64 = 0
|
||||
return download_progressive(path: path, request: request, operation: operation, progressHandler: { data in
|
||||
progressHandler(position, data)
|
||||
position += Int64(data.count)
|
||||
}, completionHandler: (completionHandler ?? { _ in return }))
|
||||
}
|
||||
|
||||
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)
|
||||
let cantLoadError = urlError(path, code: .cannotLoadFromNetwork)
|
||||
request.set(httpRangeWithOffset: offset, length: length)
|
||||
return self.download_simple(path: path, request: request, operation: operation, completionHandler: { (tempURL, error) in
|
||||
do {
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
guard let tempURL = tempURL else {
|
||||
throw cantLoadError
|
||||
}
|
||||
|
||||
let data = try Data(contentsOf: tempURL)
|
||||
completionHandler(data, nil)
|
||||
} catch {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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, overwrite: Bool = false, 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, overwrite: overwrite)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/// This method should be used in subclasses to fetch directory content from servers which support paginated results.
|
||||
/// Almost all HTTP based provider, except WebDAV, supports this method.
|
||||
///
|
||||
/// - Important: Please use `[weak self]` when implementing handlers to prevent retain cycles. In these cases,
|
||||
/// return `nil` as the result of handler as the operation will be aborted.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - path: path of directory which enqueued for listing, for informational use like errpr reporting.
|
||||
/// - requestHandler: Get token of next page and returns appropriate `URLRequest` to be sent to server.
|
||||
/// handler can return `nil` to cancel entire operation.
|
||||
/// - token: Token of the page which `URLRequest` is needed, token will be `nil` for initial page. .
|
||||
/// - pageHandler: Handler which is called after fetching results of a page to parse data. will return parse result as
|
||||
/// array of `FileObject` or error if data is nil or parsing is failed. Method will not continue to next page if
|
||||
/// `error` is returned, otherwise `nextToken` will be used for next page. `nil` value for `newToken` will indicate
|
||||
/// last page of directory contents.
|
||||
/// - data: Raw data returned from server. Handler should parse them and return files.
|
||||
/// - progress: `Progress` object that `completedUnits` will be increased when a new `FileObject` is parsed in method.
|
||||
/// - completionHandler: All file objects returned by `pageHandler` will be passed to this handler, or error if occured.
|
||||
/// This handler will be called when `pageHandler` returns `nil for `newToken`.
|
||||
/// - contents: all files parsed via `pageHandler` will be return aggregated.
|
||||
/// - error: `Error` returned by server. `nil` means success. If exists, it means `contents` are incomplete.
|
||||
internal func paginated(_ path: String, requestHandler: @escaping (_ token: String?) -> URLRequest?, pageHandler: @escaping (_ data: Data?, _ progress: Progress) -> (files: [FileObject], error: Error?, newToken: String?), completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) -> Progress {
|
||||
let progress = Progress(totalUnitCount: -1)
|
||||
self.paginated(path, startToken: nil, currentProgress: progress, previousResult: [], requestHandler: requestHandler, pageHandler: pageHandler, completionHandler: completionHandler)
|
||||
return progress
|
||||
}
|
||||
|
||||
// codebeat:disable[ARITY]
|
||||
private func paginated(_ path: String, startToken: String?, currentProgress progress: Progress, previousResult: [FileObject], requestHandler: @escaping (_ token: String?) -> URLRequest?, pageHandler: @escaping (_ data: Data?, _ progress: Progress) -> (files: [FileObject], error: Error?, newToken: String?), completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) {
|
||||
guard !progress.isCancelled, let request = requestHandler(startToken) else {
|
||||
return
|
||||
}
|
||||
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
if let error = error {
|
||||
completionHandler(previousResult, error)
|
||||
return
|
||||
}
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
let responseError = self.serverError(with: rCode, path: path, data: data)
|
||||
completionHandler(previousResult, responseError)
|
||||
return
|
||||
}
|
||||
|
||||
let (newFiles, err, newToken) = pageHandler(data, progress)
|
||||
if let error = err {
|
||||
completionHandler(previousResult, error)
|
||||
return
|
||||
}
|
||||
let files = previousResult + newFiles
|
||||
if let newToken = newToken, !progress.isCancelled {
|
||||
_ = self.paginated(path, startToken: newToken, currentProgress: progress, previousResult: files, requestHandler: requestHandler, pageHandler: pageHandler, completionHandler: completionHandler)
|
||||
} else {
|
||||
completionHandler(files, nil)
|
||||
}
|
||||
|
||||
})
|
||||
progress.cancellationHandler = { [weak task] in
|
||||
task?.cancel()
|
||||
}
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
task.resume()
|
||||
}
|
||||
// codebeat:enable[ARITY]
|
||||
|
||||
internal var maxUploadSimpleSupported: Int64 { return Int64.max }
|
||||
|
||||
internal func upload_simple(_ targetPath: String, request: URLRequest, data: Data? = nil, localFile: URL? = nil, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> Progress? {
|
||||
let size: Int64
|
||||
if let data = data {
|
||||
size = Int64(data.count)
|
||||
} else if let localFile = localFile {
|
||||
let fSize = (try? localFile.resourceValues(forKeys: [.fileSizeKey]))?.fileSize
|
||||
size = Int64(fSize ?? -1)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
if size > maxUploadSimpleSupported {
|
||||
let error = self.serverError(with: .payloadTooLarge, path: targetPath, data: nil)
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
return nil
|
||||
}
|
||||
|
||||
var progress = Progress(totalUnitCount: -1)
|
||||
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
||||
progress.kind = .file
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
||||
progress.totalUnitCount = size
|
||||
|
||||
let taskHandler = { (task: URLSessionTask) -> Void in
|
||||
completionHandlersForTasks[self.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(self.sessionDelegate!, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent), options: .new, context: &progress)
|
||||
progress.cancellationHandler = { [weak task] in
|
||||
task?.cancel()
|
||||
}
|
||||
progress.setUserInfoObject(Date(), forKey: .startingTimeKey)
|
||||
task.resume()
|
||||
}
|
||||
|
||||
if let data = data {
|
||||
let task = session.uploadTask(with: request, from: data)
|
||||
taskHandler(task)
|
||||
} else if let localFile = localFile {
|
||||
var error: NSError?
|
||||
NSFileCoordinator().coordinate(readingItemAt: localFile, options: .forUploading, error: &error, byAccessor: { (url) in
|
||||
let task = self.session.uploadTask(with: request, fromFile: localFile)
|
||||
taskHandler(task)
|
||||
})
|
||||
if let error = error {
|
||||
completionHandler?(error)
|
||||
}
|
||||
}
|
||||
|
||||
return progress
|
||||
}
|
||||
|
||||
internal func download_progressive(path: String, request: URLRequest, operation: FileOperationType, responseHandler: ((_ response: URLResponse) -> Void)? = nil, progressHandler: @escaping (_ data: Data) -> Void, completionHandler: @escaping (_ error: Error?) -> Void) -> Progress? {
|
||||
var progress = Progress(totalUnitCount: -1)
|
||||
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
||||
progress.kind = .file
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
||||
|
||||
let task = session.dataTask(with: request)
|
||||
responseCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { response in
|
||||
responseHandler?(response)
|
||||
}
|
||||
|
||||
dataCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { data in
|
||||
progressHandler(data)
|
||||
}
|
||||
|
||||
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
|
||||
if error != nil {
|
||||
progress.cancel()
|
||||
}
|
||||
completionHandler(error)
|
||||
self.delegateNotify(operation, error: error)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
internal func download_simple(path: String, request: URLRequest, operation: FileOperationType, completionHandler: @escaping ((_ tempURL: URL?, _ error: Error?) -> Void)) -> Progress? {
|
||||
var progress = Progress(totalUnitCount: -1)
|
||||
progress.setUserInfoObject(operation, forKey: .fileProvderOperationTypeKey)
|
||||
progress.kind = .file
|
||||
progress.setUserInfoObject(Progress.FileOperationKind.downloading, forKey: .fileOperationKindKey)
|
||||
|
||||
let task = session.downloadTask(with: request)
|
||||
completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in
|
||||
if error != nil {
|
||||
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? = try? Data(contentsOf: tempURL)
|
||||
let serverError = code.flatMap { self.serverError(with: $0, path: path, data: errorData) }
|
||||
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 { }
|
||||
+460
-459
File diff suppressed because it is too large
Load Diff
+54
-129
@@ -8,26 +8,23 @@
|
||||
|
||||
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)
|
||||
}
|
||||
if rpath.isEmpty {
|
||||
fileURL = relativeURL
|
||||
var rpath = path.replacingOccurrences(of: relativeURL?.path ?? "", with: "", options: .anchored).replacingOccurrences(of: "/", with: "", options: .anchored)
|
||||
if #available(iOS 9.0, macOS 10.11, *) {
|
||||
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,36 +32,61 @@ 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])
|
||||
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)
|
||||
for (key, value) in values.allValues {
|
||||
self.allValues[key.rawValue] = value
|
||||
self.allValues[key] = value
|
||||
}
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// The total size allocated on disk for the file
|
||||
open internal(set) var allocatedSize: Int64 {
|
||||
get {
|
||||
return allValues[URLResourceKey.fileAllocatedSizeKey.rawValue] as? Int64 ?? 0
|
||||
return allValues[.fileAllocatedSizeKey] as? Int64 ?? 0
|
||||
}
|
||||
set {
|
||||
allValues[URLResourceKey.fileAllocatedSizeKey.rawValue] = Int(exactly: newValue) ?? Int.max
|
||||
allValues[.fileAllocatedSizeKey] = Int(exactly: newValue) ?? Int.max
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
set {
|
||||
allValues[.documentIdentifierKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// 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[URLResourceKey.generationIdentifierKey.rawValue] as? Data
|
||||
let data = allValues[.generationIdentifierKey] as? Data
|
||||
return data?.map { String(format: "%02hhx", $0) }.joined()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Count of children items of a driectory. It costs disk access for local directories.
|
||||
open public(set) override var childrensCount: Int? {
|
||||
get {
|
||||
return try? FileManager.default.contentsOfDirectory(atPath: self.url.path).count
|
||||
}
|
||||
set {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal final class LocalFolderMonitor {
|
||||
@@ -79,18 +101,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)
|
||||
@@ -199,114 +224,14 @@ internal class LocalFileProviderManagerDelegate: NSObject, FileManagerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
open class LocalOperationHandle: OperationHandle {
|
||||
public let baseURL: URL
|
||||
public let operationType: FileOperationType
|
||||
class UndoBox: NSObject {
|
||||
weak var provider: FileProvideUndoable?
|
||||
let operation: FileOperationType
|
||||
let undoOperation: 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)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
init(provider: FileProvideUndoable, operation: FileOperationType, undoOperation: FileOperationType) {
|
||||
self.provider = provider
|
||||
self.operation = operation
|
||||
self.undoOperation = undoOperation
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,514 @@
|
||||
|
||||
//
|
||||
// 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: You can pass file id instead of file path, e.g `"id:1234abcd"`, to point to a file or folder by ID.
|
||||
|
||||
- Note: Uploading files and data are limited to 100MB, for now.
|
||||
*/
|
||||
open class OneDriveFileProvider: HTTPFileProvider, FileProviderSharing {
|
||||
override open class var type: String { return "OneDrive" }
|
||||
|
||||
/// Route to access file container on OneDrive. For default logined user use `.me` otherwise you can acesss
|
||||
/// container based on drive id, group id, site id or user id for another user's default container
|
||||
public enum Route: RawRepresentable {
|
||||
/// Access to default container for current user
|
||||
case me
|
||||
/// Access to a specific drive by id
|
||||
case drive(uuid: UUID)
|
||||
/// Access to a default drive of a group by their id
|
||||
case group(uuid: UUID)
|
||||
/// Access to a default drive of a site by their id
|
||||
case site(uuid: UUID)
|
||||
/// Access to a default drive of a user by their id
|
||||
case user(uuid: UUID)
|
||||
|
||||
public init?(rawValue: String) {
|
||||
let components = rawValue.components(separatedBy: ";")
|
||||
guard let type = components.first else {
|
||||
return nil
|
||||
}
|
||||
if type == "me" {
|
||||
self = .me
|
||||
}
|
||||
guard let uuid = components.last.flatMap({ UUID(uuidString: $0) }) else {
|
||||
return nil
|
||||
}
|
||||
switch type {
|
||||
case "drive":
|
||||
self = .drive(uuid: uuid)
|
||||
case "group":
|
||||
self = .group(uuid: uuid)
|
||||
case "site":
|
||||
self = .site(uuid: uuid)
|
||||
case "user":
|
||||
self = .user(uuid: uuid)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public var rawValue: String {
|
||||
switch self {
|
||||
case .me:
|
||||
return "me;"
|
||||
case .drive(uuid: let uuid):
|
||||
return "drive;" + uuid.uuidString
|
||||
case .group(uuid: let uuid):
|
||||
return "group;" + uuid.uuidString
|
||||
case .site(uuid: let uuid):
|
||||
return "site;" + uuid.uuidString
|
||||
case .user(uuid: let uuid):
|
||||
return "user;" + uuid.uuidString
|
||||
}
|
||||
}
|
||||
|
||||
/// Return path component in URL for selected drive
|
||||
var drivePath: String {
|
||||
switch self {
|
||||
case .me:
|
||||
return "me/drive"
|
||||
case .drive(uuid: let uuid):
|
||||
return "drives/" + uuid.uuidString
|
||||
case .group(uuid: let uuid):
|
||||
return "groups/" + uuid.uuidString + "/drive"
|
||||
case .site(uuid: let uuid):
|
||||
return "sites/" + uuid.uuidString + "/drive"
|
||||
case .user(uuid: let uuid):
|
||||
return "users/" + uuid.uuidString + "/drive"
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Route for container, default is `.me`.
|
||||
open let route: Route
|
||||
|
||||
/**
|
||||
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.
|
||||
|
||||
- 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 user.
|
||||
- drive: drive name for user on server, default value is `root`.
|
||||
- cache: A URLCache to cache downloaded files and contents.
|
||||
*/
|
||||
@available(*, deprecated, message: "use init(credential:, serverURL:, route:, cache:) instead.")
|
||||
public init(credential: URLCredential?, serverURL: URL? = nil, drive: String?, cache: URLCache? = nil) {
|
||||
let baseURL = serverURL?.absoluteURL ?? URL(string: "https://api.onedrive.com/")!
|
||||
let refinedBaseURL = baseURL.absoluteString.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")
|
||||
self.route = drive.flatMap({ UUID(uuidString: $0) }).flatMap({ Route.drive(uuid: $0) }) ?? .me
|
||||
super.init(baseURL: refinedBaseURL, credential: credential, cache: cache)
|
||||
}
|
||||
|
||||
/**
|
||||
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.
|
||||
- route: drive name for user on server, default value is `.me`.
|
||||
- cache: A URLCache to cache downloaded files and contents.
|
||||
*/
|
||||
public init(credential: URLCredential?, serverURL: URL? = nil, route: Route = .me, cache: URLCache? = nil) {
|
||||
let baseURL = serverURL?.absoluteURL ?? URL(string: "https://api.onedrive.com/")!
|
||||
let refinedBaseURL = baseURL.absoluteString.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")
|
||||
self.route = route
|
||||
super.init(baseURL: refinedBaseURL, credential: credential, cache: cache)
|
||||
}
|
||||
|
||||
public required convenience init?(coder aDecoder: NSCoder) {
|
||||
let route: Route
|
||||
if let driveId = aDecoder.decodeObject(forKey: "drive") as? String, let uuid = UUID(uuidString: driveId) {
|
||||
route = .drive(uuid: uuid)
|
||||
} else {
|
||||
route = (aDecoder.decodeObject(forKey: "route") as? String).flatMap({ Route(rawValue: $0) }) ?? .me
|
||||
}
|
||||
self.init(credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential,
|
||||
serverURL: aDecoder.decodeObject(forKey: "baseURL") as? URL,
|
||||
route: route)
|
||||
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.route.rawValue, forKey: "route")
|
||||
}
|
||||
|
||||
open override func copy(with zone: NSZone? = nil) -> Any {
|
||||
let copy = OneDriveFileProvider(credential: self.credential, serverURL: self.baseURL, route: self.route, cache: self.cache)
|
||||
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) {
|
||||
_ = paginated(path, requestHandler: { [weak self] (token) -> URLRequest? in
|
||||
guard let `self` = self else { return nil }
|
||||
let url = token.flatMap(URL.init(string:)) ?? self.url(of: path, modifier: "children")
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.set(httpAuthentication: self.credential, with: .oAuth2)
|
||||
return request
|
||||
}, pageHandler: { [weak self] (data, _) -> (files: [FileObject], error: Error?, newToken: String?) in
|
||||
guard let `self` = self else { return ([], nil, nil) }
|
||||
|
||||
guard let json = data?.deserializeJSON(), let entries = json["value"] as? [AnyObject] else {
|
||||
let err = self.urlError(path, code: .badServerResponse)
|
||||
return ([], err, nil)
|
||||
}
|
||||
|
||||
var files = [FileObject]()
|
||||
for entry in entries {
|
||||
if let entry = entry as? [String: AnyObject], let file = OneDriveFileObject(baseURL: self.baseURL, route: self.route, json: entry) {
|
||||
files.append(file)
|
||||
}
|
||||
}
|
||||
return (files, nil, json["@odata.nextLink"] as? String)
|
||||
}, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
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: FileProviderHTTPError?
|
||||
var fileObject: OneDriveFileObject?
|
||||
if let response = response as? HTTPURLResponse {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
|
||||
serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) }
|
||||
if let json = data?.deserializeJSON(), let file = OneDriveFileObject(baseURL: self.baseURL, route: self.route, json: json) {
|
||||
fileObject = file
|
||||
}
|
||||
}
|
||||
completionHandler(fileObject, serverError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
|
||||
open override func storageProperties(completionHandler: @escaping (_ volumeInfo: VolumeObject?) -> 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
|
||||
guard let json = data?.deserializeJSON() else {
|
||||
completionHandler(nil)
|
||||
return
|
||||
}
|
||||
|
||||
let volume = VolumeObject(allValues: [:])
|
||||
volume.url = request.url
|
||||
volume.name = json["name"] as? String
|
||||
volume.creationDate = (json["createdDateTime"] as? String).flatMap { Date(rfcString: $0) }
|
||||
volume.totalCapacity = (json["quota"]?["total"] as? NSNumber)?.int64Value ?? -1
|
||||
volume.availableCapacity = (json["quota"]?["remaining"] as? NSNumber)?.int64Value ?? 0
|
||||
completionHandler(volume)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
|
||||
open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? {
|
||||
let queryStr = query.findValue(forKey: "name") as? String ?? query.findAllValues(forKey: nil).flatMap { $0.value as? String }.first
|
||||
|
||||
return paginated(path, requestHandler: { [weak self] (token) -> URLRequest? in
|
||||
guard let `self` = self else { return nil }
|
||||
|
||||
let url: URL
|
||||
if let next = token.flatMap(URL.init(string:)) {
|
||||
url = next
|
||||
} else {
|
||||
let bURL = self.baseURL!.appendingPathComponent(self.route.drivePath).appendingPathComponent("root/search")
|
||||
var components = URLComponents(url: bURL, resolvingAgainstBaseURL: false)!
|
||||
let qItem = URLQueryItem(name: "q", value: (queryStr ?? "*"))
|
||||
components.queryItems = [qItem]
|
||||
if recursive {
|
||||
components.queryItems?.append(URLQueryItem(name: "expand", value: "children"))
|
||||
}
|
||||
url = components.url!
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
return request
|
||||
}, pageHandler: { [weak self] (data, progress) -> (files: [FileObject], error: Error?, newToken: String?) in
|
||||
guard let `self` = self else { return ([], nil, nil) }
|
||||
guard let json = data?.deserializeJSON(), let entries = json["value"] as? [AnyObject] else {
|
||||
let err = self.urlError(path, code: .badServerResponse)
|
||||
return ([], err, nil)
|
||||
}
|
||||
|
||||
var foundFiles = [FileObject]()
|
||||
for entry in entries {
|
||||
if let entry = entry as? [String: AnyObject], let file = OneDriveFileObject(baseURL: self.baseURL, route: self.route, json: entry), query.evaluate(with: file.mapPredicate()) {
|
||||
foundFiles.append(file)
|
||||
foundItemHandler?(file)
|
||||
}
|
||||
}
|
||||
|
||||
return (foundFiles, nil, json["@odata.nextLink"] as? String)
|
||||
}, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
open func url(of path: String, modifier: String? = nil) -> URL {
|
||||
var url: URL = baseURL!
|
||||
var rpath: String = path
|
||||
let isId = path.hasPrefix("id:")
|
||||
|
||||
url.appendPathComponent(route.drivePath)
|
||||
|
||||
if isId {
|
||||
url.appendPathComponent("root:")
|
||||
} else {
|
||||
url.appendPathComponent("items")
|
||||
}
|
||||
|
||||
rpath = rpath.trimmingCharacters(in: pathTrimSet)
|
||||
|
||||
switch (modifier == nil, rpath.isEmpty, isId) {
|
||||
case (true, false, _):
|
||||
url.appendPathComponent(rpath)
|
||||
case (true, true, _):
|
||||
break
|
||||
case (false, true, _):
|
||||
url.appendPathComponent(modifier!)
|
||||
case (false, false, true):
|
||||
url.appendPathComponent(rpath)
|
||||
url.appendPathComponent(modifier!)
|
||||
case (false, false, false):
|
||||
url.appendPathComponent(rpath + ":")
|
||||
url.appendPathComponent(modifier!)
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
func correctPath(_ path: String) -> String {
|
||||
if path.hasPrefix("id:") {
|
||||
return path
|
||||
}
|
||||
var p = path.hasPrefix("/") ? path : "/" + path
|
||||
if p.hasSuffix("/") {
|
||||
p.remove(at: p.index(before:p.endIndex))
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
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)
|
||||
// Remove gzip to fix availability of progress per (Oleg Marchik)[https://github.com/evilutioner] PR (#61)
|
||||
request.set(httpAcceptEncodings: [.deflate, .identity])
|
||||
|
||||
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 parentRefrence: [String: AnyObject] = [:]
|
||||
if cdest.hasPrefix("id:") {
|
||||
parentRefrence["id"] = cdest.components(separatedBy: "/").first as NSString?
|
||||
switch self.route {
|
||||
case .drive(uuid: let uuid):
|
||||
parentRefrence["driveId"] = uuid.uuidString as NSString
|
||||
default:
|
||||
break
|
||||
}
|
||||
} else {
|
||||
parentRefrence["path"] = cdest.deletingLastPathComponent as NSString
|
||||
}
|
||||
var requestDictionary = [String: AnyObject]()
|
||||
requestDictionary["parentReference"] = parentRefrence as NSDictionary
|
||||
requestDictionary["name"] = (cdest as NSString).lastPathComponent as NSString
|
||||
request.httpBody = Data(jsonDictionary: requestDictionary)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
return request
|
||||
}
|
||||
|
||||
override func serverError(with code: FileProviderHTTPErrorCode, path: String?, data: Data?) -> FileProviderHTTPError {
|
||||
let errorDesc: String?
|
||||
if let response = data?.deserializeJSON() {
|
||||
errorDesc = response["error"]?["message"] as? String
|
||||
} else {
|
||||
errorDesc = data.flatMap({ String(data: $0, encoding: .utf8) })
|
||||
}
|
||||
return FileProviderOneDriveError(code: code, path: path ?? "", errorDescription: errorDesc)
|
||||
}
|
||||
|
||||
override var maxUploadSimpleSupported: Int64 {
|
||||
return 104_857_600 // 100MB
|
||||
}
|
||||
|
||||
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: FileProviderHTTPError?
|
||||
var link: URL?
|
||||
if let response = response as? HTTPURLResponse {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
|
||||
serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) }
|
||||
if let json = data?.deserializeJSON() {
|
||||
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 = self.serverError(with: rCode, path: path, data: data)
|
||||
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: FileProviderHTTPError?
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
if let response = response as? HTTPURLResponse {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
|
||||
serverError = code.flatMap { self.serverError(with: $0, path: path, data: data) }
|
||||
if let json = data?.deserializeJSON() {
|
||||
(dic, keys) = self.mapMediaInfo(json)
|
||||
}
|
||||
}
|
||||
completionHandler(dic, keys, serverError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
+47
-177
@@ -8,209 +8,89 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct FileProviderOneDriveError: Error, CustomStringConvertible {
|
||||
/// 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?
|
||||
|
||||
public var description: String {
|
||||
return code.description
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
let url = URL(string: rpath, relativeTo: baseURL) ?? URL(string: path)!
|
||||
super.init(url: url, name: name, path: path)
|
||||
let rpath = (URL(string:path)?.appendingPathComponent(name).absoluteString)!.replacingOccurrences(of: "/", with: "", options: .anchored)
|
||||
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 }
|
||||
self.init(baseURL: baseURL, drive: drive, json: json)
|
||||
internal convenience init? (baseURL: URL?, route: OneDriveFileProvider.Route, jsonStr: String) {
|
||||
guard let json = jsonStr.deserializeJSON() else { return nil }
|
||||
self.init(baseURL: baseURL, route: route, json: json)
|
||||
}
|
||||
|
||||
internal convenience init? (baseURL: URL?, drive: String, json: [String: AnyObject]) {
|
||||
internal convenience init? (baseURL: URL?, route: OneDriveFileProvider.Route, 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)
|
||||
guard let path = json["parentReference"]?["path"] as? String else { return nil }
|
||||
var lPath = path.replacingOccurrences(of: route.drivePath, 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.childrensCount = json["folder"]?["childCount"] as? Int
|
||||
self.modifiedDate = (json["lastModifiedDateTime"] as? String).flatMap { Date(rfcString: $0) }
|
||||
self.creationDate = (json["createdDateTime"] as? String).flatMap { Date(rfcString: $0) }
|
||||
self.type = json["folder"] != nil ? .directory : .regular
|
||||
self.contentType = json["file"]?["mimeType"] as? String ?? "application/octet-stream"
|
||||
self.id = json["id"] as? String
|
||||
self.entryTag = json["eTag"] as? String
|
||||
let hashes = json["file"]?["hashes"] as? NSDictionary
|
||||
// checks for both sha1 or quickXor. First is available in personal drives, second in business one.
|
||||
self.hash = (hashes?["sha1Hash"] as? String) ?? (hashes?["quickXorHash"] as? String)
|
||||
}
|
||||
|
||||
/// The document identifier is a value assigned by the OneDrive to a file.
|
||||
/// This value is used to identify the document regardless of where it is moved on a volume.
|
||||
open internal(set) var id: String? {
|
||||
get {
|
||||
return allValues["NSURLDocumentIdentifyKey"] as? String
|
||||
return allValues[.fileResourceIdentifierKey] as? String
|
||||
}
|
||||
set {
|
||||
allValues["NSURLDocumentIdentifyKey"] = newValue
|
||||
allValues[.fileResourceIdentifierKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// MIME type of file contents returned by OneDrive server.
|
||||
open internal(set) var contentType: String {
|
||||
get {
|
||||
return allValues["NSURLContentTypeKey"] as? String ?? ""
|
||||
return allValues[.mimeTypeKey] as? String ?? "application/octet-stream"
|
||||
}
|
||||
set {
|
||||
allValues["NSURLContentTypeKey"] = newValue
|
||||
allValues[.mimeTypeKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP E-Tag, can be used to mark changed files.
|
||||
open internal(set) var entryTag: String? {
|
||||
get {
|
||||
return allValues["NSURLEntryTagKey"] as? String
|
||||
return allValues[.entryTagKey] as? String
|
||||
}
|
||||
set {
|
||||
allValues["NSURLEntryTagKey"] = newValue
|
||||
allValues[.entryTagKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculated hash from OneDrive server. Hex string SHA1 in personal or Base65 string [QuickXOR](https://dev.onedrive.com/snippets/quickxorhash.htm) in business drives.
|
||||
open internal(set) var hash: String? {
|
||||
get {
|
||||
return allValues[.documentIdentifierKey] as? String
|
||||
}
|
||||
set {
|
||||
allValues[.documentIdentifierKey] = 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)!
|
||||
}
|
||||
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 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 {
|
||||
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 hasmore = ncursor != nil
|
||||
if hasmore {
|
||||
self.list(path, cursor: ncursor, prevContents: files, completionHandler: completionHandler)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
completionHandler(files, nil, responseError ?? error)
|
||||
})
|
||||
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 > 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")
|
||||
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)!
|
||||
}
|
||||
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 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 {
|
||||
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)
|
||||
} else {
|
||||
completionHandler(responseError ?? error)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
completionHandler(responseError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
// codebeat:enable[ARITY]
|
||||
|
||||
internal extension OneDriveFileProvider {
|
||||
static let dateFormatter = DateFormatter()
|
||||
static let decimalFormatter = NumberFormatter()
|
||||
@@ -248,14 +128,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))
|
||||
}
|
||||
@@ -283,14 +163,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
+243
-99
@@ -8,197 +8,336 @@
|
||||
|
||||
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) }
|
||||
/// 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 }
|
||||
}
|
||||
|
||||
extension FileProviderHTTPError {
|
||||
public var description: String {
|
||||
return code.description
|
||||
}
|
||||
|
||||
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 }
|
||||
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 var responseCompletionHandlersForTasks = [String: [Int: (URLResponse) -> 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)?
|
||||
|
||||
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 URLSessionUploadTask {
|
||||
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesSent))
|
||||
//task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToSend))
|
||||
} else if task is URLSessionDownloadTask {
|
||||
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived))
|
||||
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive))
|
||||
}
|
||||
|
||||
_ = dataCompletionHandlersForTasks[session.sessionDescription!]?.removeValue(forKey: task.taskIdentifier)
|
||||
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
|
||||
}
|
||||
|
||||
switch op {
|
||||
case .fetch:
|
||||
if task is URLSessionDataTask {
|
||||
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesReceived))
|
||||
task.removeObserver(self, forKeyPath: #keyPath(URLSessionTask.countOfBytesExpectedToReceive))
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if !(task is URLSessionDownloadTask), case FileOperationType.fetch = op {
|
||||
return
|
||||
}
|
||||
if #available(iOS 9.0, macOS 10.11, *) {
|
||||
if task is URLSessionStreamTask {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fileProvider.delegateNotify(op, error: error)
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
|
||||
self.didSendDataHandler?(session, task, bytesSent, totalBytesSent, totalBytesExpectedToSend)
|
||||
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
|
||||
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, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
|
||||
let handler = responseCompletionHandlersForTasks[session.sessionDescription!]?[dataTask.taskIdentifier] ?? nil
|
||||
handler?(response)
|
||||
completionHandler(.allow)
|
||||
_ = responseCompletionHandlersForTasks[session.sessionDescription!]?.removeValue(forKey: dataTask.taskIdentifier)
|
||||
}
|
||||
|
||||
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
|
||||
if let completionHandler = dataCompletionHandlersForTasks[session.sessionDescription!]?[dataTask.taskIdentifier] {
|
||||
if let json = dataTask.taskDescription?.deserializeJSON(),
|
||||
let op = FileOperationType(json: json), let fileProvider = fileProvider {
|
||||
fileProvider.delegateNotify(op, progress: Double(dataTask.countOfBytesReceived) / Double(dataTask.countOfBytesExpectedToReceive))
|
||||
}
|
||||
completionHandler(data)
|
||||
}
|
||||
|
||||
guard let desc = task.taskDescription, let json = jsonToDictionary(desc) else {
|
||||
}
|
||||
|
||||
public func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
|
||||
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) {
|
||||
self.didReceivedData?(session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
|
||||
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
|
||||
if totalBytesExpectedToWrite == NSURLSessionTransferSizeUnknown { return }
|
||||
|
||||
guard let desc = downloadTask.taskDescription, let json = jsonToDictionary(desc), let source = json["source"] as? String, let dest = json["dest"] as? String else {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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"]
|
||||
@@ -219,6 +358,11 @@ public enum FileProviderHTTPErrorCode: Int {
|
||||
}
|
||||
}
|
||||
|
||||
public var localizedDescription: String {
|
||||
return HTTPURLResponse.localizedString(forStatusCode: self.rawValue)
|
||||
}
|
||||
|
||||
/// Description of status based on first digit which indicated fail or success.
|
||||
public var typeDescription: String {
|
||||
switch self.rawValue {
|
||||
case 100...199: return "Informational"
|
||||
@@ -226,7 +370,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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+14
-16
@@ -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
|
||||
@@ -141,7 +138,8 @@ extension SMB2ProtocolClient {
|
||||
let paramData = Data(bytesNoCopy: UnsafeMutablePointer<UInt8>(&rawParamWords), count: rawParamWords.count, deallocator: .free)
|
||||
paramWords = paramData.scanValue()!
|
||||
offset += paramWordsCount * 2
|
||||
let messageBytesCount = Int(UInt16(buffer[0]) + UInt16(buffer[1]) << 8)
|
||||
let messageBytesCountHi = Int(buffer[1]) << 8
|
||||
let messageBytesCount = Int(buffer[0]) + messageBytesCountHi
|
||||
offset += MemoryLayout<UInt16>.size
|
||||
guard data.count >= (offset + messageBytesCount) else {
|
||||
throw SMBFileProviderError.incorrectMessageLength
|
||||
|
||||
@@ -10,96 +10,120 @@ 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
|
||||
}
|
||||
|
||||
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObjectClass], _ error: Error?) -> Void)) {
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObjectClass?, _ error: Error?) -> Void)) {
|
||||
open func attributesOfItem(path: String, completionHandler: @escaping (_ attributes: FileObjectClass?, _ error: Error?) -> Void) {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) {
|
||||
open func storageProperties(completionHandler: @escaping (_ volume: VolumeObject?) -> Void) {
|
||||
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
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
// codebeat:disable[TOO_MANY_IVARS]
|
||||
// SMB/CIFS Types
|
||||
struct SMB1 {
|
||||
struct Header { // 32 bytes
|
||||
@@ -75,6 +76,7 @@ struct SMB1 {
|
||||
}
|
||||
}
|
||||
|
||||
// codebeat:disable[ARITY]
|
||||
init(command: Command, treeId: UInt16, pid: UInt32, userId: UInt16, multiplexId: UInt16, flags: Flags, flags2: Flags2 = [.LONG_NAMES, .ERR_STATUS, .UNICODE], ntStatus: UInt32 = 0, securityKey: UInt32 = 0, securityCID: UInt16 = 0, securitySequenceNumber: UInt16 = 0) {
|
||||
self.protocolID = Header.protocolConst
|
||||
self._command = command.rawValue
|
||||
@@ -91,6 +93,7 @@ struct SMB1 {
|
||||
self.userId = userId
|
||||
self.multiplexId = multiplexId
|
||||
}
|
||||
// codebeat:enable[ARITY]
|
||||
}
|
||||
|
||||
struct Flags: OptionSet {
|
||||
@@ -215,3 +218,4 @@ struct SMB1 {
|
||||
case INVALID = 0xFE
|
||||
}
|
||||
}
|
||||
// codebeat:enable[TOO_MANY_IVARS]
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,10 @@ extension SMB2 {
|
||||
var contextCount: UInt16
|
||||
fileprivate let reserved2: UInt16
|
||||
var clientStartTime: SMBTime {
|
||||
let time = Int64(contextOffset) + (Int64(contextCount) << 32) + (Int64(contextCount) << 48)
|
||||
let lo = Int64(contextOffset)
|
||||
let hi1 = Int64(contextCount) << 32
|
||||
let hi2 = Int64(contextCount) << 48
|
||||
let time: Int64 = lo + hi1 + hi2
|
||||
return SMBTime(time: time)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ extension SMB2 {
|
||||
return nil
|
||||
}
|
||||
self.header = header
|
||||
let path = "\\\\\(host)\\\(share)"
|
||||
let path = "\\\\" + host + "\\" + share
|
||||
self.buffer = path.data(using: .utf16)
|
||||
}
|
||||
|
||||
|
||||
+384
-415
@@ -7,43 +7,25 @@
|
||||
//
|
||||
|
||||
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
|
||||
/**
|
||||
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" }
|
||||
|
||||
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!
|
||||
}
|
||||
/// An enum which defines HTTP Authentication method, usually you should it default `.digest`.
|
||||
/// If the server uses OAuth authentication, credential must be set with token as `password`, like Dropbox.
|
||||
public var credentialType: URLRequest.AuthenticationType = .digest
|
||||
|
||||
/**
|
||||
Initializes WebDAV provider.
|
||||
@@ -51,72 +33,83 @@ 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(""))
|
||||
super.init(baseURL: refinedBaseURL.absoluteURL, 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.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)
|
||||
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.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length")
|
||||
runDataTask(with: request, operationHandle: RemoteOperationHandle(operationType: opType, tasks: []), 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, url: url)
|
||||
}
|
||||
var fileObjects = [WebDavFileObject]()
|
||||
if let data = data {
|
||||
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
|
||||
for attr in xresponse where attr.href != url {
|
||||
if attr.href.path == url.path {
|
||||
continue
|
||||
}
|
||||
fileObjects.append(WebDavFileObject(attr))
|
||||
}
|
||||
}
|
||||
completionHandler(fileObjects, responseError ?? error)
|
||||
})
|
||||
override open func copy(with zone: NSZone? = nil) -> Any {
|
||||
let copy = WebDAVFileProvider(baseURL: self.baseURL!, credential: self.credential, cache: self.cache)!
|
||||
copy.delegate = self.delegate
|
||||
copy.fileOperationDelegate = self.fileOperationDelegate
|
||||
copy.useCache = self.useCache
|
||||
copy.validatingCache = self.validatingCache
|
||||
return copy
|
||||
}
|
||||
|
||||
open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
|
||||
override open func contentsOfDirectory(path: String, completionHandler: @escaping (([FileObject], Error?) -> Void)) {
|
||||
let query = NSPredicate(format: "TRUEPREDICATE")
|
||||
_ = searchFiles(path: path, recursive: false, query: query, including: [], foundItemHandler: nil, 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.
|
||||
- Parameter contents: An array of `FileObject` identifying the the directory entries.
|
||||
- Parameter error: Error returned by system.
|
||||
*/
|
||||
open func contentsOfDirectory(path: String, including: [URLResourceKey], completionHandler: @escaping (_ contents: [FileObject], _ error: Error?) -> Void) {
|
||||
let query = NSPredicate(format: "TRUEPREDICATE")
|
||||
_ = searchFiles(path: path, recursive: false, query: query, including: including, foundItemHandler: nil, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
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.
|
||||
- Parameter attributes: A `FileObject` containing the attributes of the item.
|
||||
- Parameter 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(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length")
|
||||
request.setValue("0", forHTTPHeaderField: "Depth")
|
||||
request.set(httpAuthentication: credential, with: credentialType)
|
||||
request.set(httpContentType: .xml, charset: .utf8)
|
||||
request.httpBody = WebDavFileObject.xmlProp(including)
|
||||
runDataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var responseError: FileProviderWebDavError?
|
||||
var responseError: FileProviderHTTPError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode, code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderWebDavError(code: rCode, url: url)
|
||||
responseError = self.serverError(with: rCode, path: path, data: data)
|
||||
}
|
||||
if let data = data {
|
||||
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
|
||||
@@ -129,7 +122,7 @@ open class WebDAVFileProvider: FileProviderBasicRemote {
|
||||
})
|
||||
}
|
||||
|
||||
open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) {
|
||||
override open func storageProperties(completionHandler: @escaping (_ volumeInfo: VolumeObject?) -> 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,291 +132,200 @@ 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.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")
|
||||
request.set(httpAuthentication: credential, with: credentialType)
|
||||
request.set(httpContentType: .xml, charset: .utf8)
|
||||
request.httpBody = WebDavFileObject.xmlProp([.volumeTotalCapacityKey, .volumeAvailableCapacityKey, .creationDateKey])
|
||||
runDataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var totalSize: Int64 = -1
|
||||
var usedSize: Int64 = 0
|
||||
if let data = data {
|
||||
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
|
||||
if let attr = xresponse.first {
|
||||
totalSize = Int64(attr.prop["quota-available-bytes"] ?? "") ?? -1
|
||||
usedSize = Int64(attr.prop["quota-used-bytes"] ?? "") ?? 0
|
||||
}
|
||||
}
|
||||
completionHandler(totalSize, usedSize)
|
||||
})
|
||||
}
|
||||
|
||||
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, 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, 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 sourceURL = self.url(of:opType.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, 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 {
|
||||
completionHandler?(FileProviderWebDavError(code: code, url: sourceURL))
|
||||
}
|
||||
}
|
||||
}
|
||||
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, 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, 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, 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, url: self.url(of: path))
|
||||
}
|
||||
defer {
|
||||
self.delegateNotify(opType, error: responseError ?? error)
|
||||
}
|
||||
if let error = error {
|
||||
completionHandler?(error)
|
||||
guard let data = data, let attr = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL).first else {
|
||||
completionHandler(nil)
|
||||
return
|
||||
}
|
||||
if atomically {
|
||||
self.moveItem(path: (path as NSString).appendingPathExtension("tmp")!, to: path, completionHandler: completionHandler)
|
||||
|
||||
let volume = VolumeObject(allValues: [:])
|
||||
volume.creationDate = attr.prop["creationdate"].flatMap { Date(rfcString: $0) }
|
||||
volume.availableCapacity = attr.prop["quota-available-bytes"].flatMap({ Int64($0) }) ?? 0
|
||||
if let usage = attr.prop["quota-used-bytes"].flatMap({ Int64($0) }) {
|
||||
volume.totalCapacity = volume.availableCapacity + usage
|
||||
}
|
||||
})
|
||||
task.taskDescription = opType.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(operationType: opType, tasks: [task])
|
||||
completionHandler(volume)
|
||||
})
|
||||
}
|
||||
|
||||
public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
|
||||
open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ([FileObject], Error?) -> Void) -> Progress? {
|
||||
return searchFiles(path: path, recursive: recursive, query: query, including: [], foundItemHandler: foundItemHandler, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
/**
|
||||
Search files inside directory using query asynchronously.
|
||||
|
||||
- Note: Query string is limited to file name, to search based on other file attributes, use NSPredicate version.
|
||||
|
||||
- Parameters:
|
||||
- path: location of directory to start search
|
||||
- recursive: Searching subdirectories of path
|
||||
- query: Simple string that file name begins with to be search, case-insensitive.
|
||||
- including: An array which determines which file properties should be considered to fetch.
|
||||
- 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.
|
||||
- files: all files meat the `query` criteria.
|
||||
- error: `Error` returned by server if occured.
|
||||
*/
|
||||
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, including: [URLResourceKey], foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping (_ files: [FileObject], _ error: Error?) -> Void) -> Progress? {
|
||||
let url = self.url(of: path)
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PROPFIND"
|
||||
//request.setValue("1", forHTTPHeaderField: "Depth")
|
||||
request.setValue("text/xml; charset=\"utf-8\"", forHTTPHeaderField: "Content-Type")
|
||||
request.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
|
||||
// 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 = WebDavFileObject.xmlProp([])
|
||||
let progress = Progress(totalUnitCount: -1)
|
||||
progress.setUserInfoObject(url, forKey: .fileURLKey)
|
||||
|
||||
let queryIsTruePredicate = query.predicateFormat == "TRUEPREDICATE"
|
||||
let task = session.dataTask(with: request) { (data, response, error) in
|
||||
// FIXME: paginating results
|
||||
var responseError: FileProviderWebDavError?
|
||||
var responseError: FileProviderHTTPError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderWebDavError(code: rCode, url: url)
|
||||
responseError = self.serverError(with: rCode, path: path, data: data)
|
||||
}
|
||||
if let data = data {
|
||||
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)) {
|
||||
continue
|
||||
}
|
||||
let fileObject = WebDavFileObject(attr)
|
||||
fileObjects.append(fileObject)
|
||||
foundItemHandler?(fileObject)
|
||||
}
|
||||
completionHandler(fileObjects, responseError ?? error)
|
||||
guard let data = data else {
|
||||
completionHandler([], responseError ?? error)
|
||||
return
|
||||
}
|
||||
completionHandler([], responseError ?? error)
|
||||
|
||||
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
|
||||
var fileObjects = [WebDavFileObject]()
|
||||
for attr in xresponse where attr.href.path != url.path {
|
||||
let fileObject = WebDavFileObject(attr)
|
||||
if !queryIsTruePredicate && !query.evaluate(with: fileObject.mapPredicate()) {
|
||||
continue
|
||||
}
|
||||
|
||||
fileObjects.append(fileObject)
|
||||
progress.completedUnitCount = Int64(fileObjects.count)
|
||||
foundItemHandler?(fileObject)
|
||||
}
|
||||
completionHandler(fileObjects, 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 = WebDavFileObject.xmlProp([.volumeTotalCapacityKey, .volumeAvailableCapacityKey])
|
||||
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.urlError(path, code: .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)
|
||||
runDataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var responseError: FileProviderHTTPError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode, code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = self.serverError(with: rCode, path: path, data: data)
|
||||
}
|
||||
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 = self.serverError(with: code, path: source, data: data)
|
||||
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.
|
||||
@@ -435,36 +337,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.urlError(path, code: .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: FileProviderHTTPError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = self.serverError(with: rCode, path: url.relativePath, data: data)
|
||||
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.urlError(path, code: .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
|
||||
@@ -473,12 +397,13 @@ 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 {
|
||||
#if swift(>=4.0)
|
||||
let trimmedStr = str.hasPrefix("/") ? String(str[str.index(after: str.startIndex)...]) : str
|
||||
#else
|
||||
let trimmedStr = str.hasPrefix("/") ? str.substring(from: str.index(after: str.startIndex)) : str
|
||||
#endif
|
||||
return trimmedStr.addingPercentEncoding(withAllowedCharacters: .filePathAllowed) ?? str
|
||||
}
|
||||
|
||||
// find node names with namespace
|
||||
@@ -500,9 +425,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 }
|
||||
|
||||
@@ -540,71 +470,110 @@ 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.contentType = davResponse.prop["getcontenttype"] ?? "octet/stream"
|
||||
self.creationDate = davResponse.prop["creationdate"].flatMap { Date(rfcString: $0) }
|
||||
self.modifiedDate = davResponse.prop["getlastmodified"].flatMap { Date(rfcString: $0) }
|
||||
self.contentType = davResponse.prop["getcontenttype"] ?? "application/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["NSURLContentTypeKey"] as? String ?? ""
|
||||
return allValues[.mimeTypeKey] as? String ?? "application/octet-stream"
|
||||
}
|
||||
set {
|
||||
allValues["NSURLContentTypeKey"] = 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["NSURLEntryTagKey"] as? String
|
||||
return allValues[.entryTagKey] as? String
|
||||
}
|
||||
set {
|
||||
allValues["NSURLEntryTagKey"] = 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"
|
||||
case URLResourceKey.volumeTotalCapacityKey:
|
||||
// WebDAV doesn't have total capacity, but it's can be calculated via used capacity
|
||||
return "quota-used-bytes"
|
||||
case URLResourceKey.volumeAvailableCapacityKey:
|
||||
return "quota-available-bytes"
|
||||
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
|
||||
}
|
||||
|
||||
internal class func xmlProp(_ keys: [URLResourceKey]) -> Data {
|
||||
return "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n\(WebDavFileObject.propString(keys))\n</D:propfind>".data(using: .utf8)!
|
||||
}
|
||||
}
|
||||
|
||||
public struct FileProviderWebDavError: Error, CustomStringConvertible {
|
||||
/// 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
|
||||
|
||||
public var description: String {
|
||||
return code.description
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
Reference in New Issue
Block a user