Compare commits
92 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 525825ff5d | |||
| a35384dc31 | |||
| 0c558fdbb4 | |||
| e30861ad18 | |||
| 538e9bb42d | |||
| ef2d69c54c | |||
| 9f210ef356 | |||
| 5a7dfa1039 | |||
| be7a370a42 | |||
| b84e6ea7ab | |||
| 82b1d2c450 | |||
| c38ee1ccd3 | |||
| 1a44df3fd7 | |||
| eff3725680 | |||
| 9368f636d3 | |||
| 3f2fda638f | |||
| 6737f98978 | |||
| 3e55cc60f7 | |||
| d7ae709cf7 | |||
| 4f6e3c7bd5 | |||
| b121fe6182 | |||
| 29b7f720f5 | |||
| 067e14b76f | |||
| 09e39ada82 | |||
| 6889b302b7 | |||
| 96ce12f226 | |||
| 091fd14a88 | |||
| 10aac532f7 | |||
| 3aac0a88c4 | |||
| 24355a4c6c | |||
| d2cf657ab2 | |||
| f2d54c7f92 | |||
| 0e9a82a288 | |||
| ea7e089718 | |||
| 19ea14ebc1 | |||
| 47885c0a4e | |||
| ac44aa190f | |||
| 50f6a393a0 | |||
| 779b38f381 | |||
| a374841883 | |||
| 0123d8b117 | |||
| fdc4bb818d | |||
| a08a9fe7a0 | |||
| 792ac6b015 | |||
| 8bad5944bc | |||
| 6ef2ab11c4 | |||
| 7f27d46c70 | |||
| b1ec99b1b8 | |||
| c4b8065cd3 | |||
| 2d8454c711 | |||
| 3b35c066de | |||
| 7d8de9cefe | |||
| e84fef20ea | |||
| 4a9a3196a2 | |||
| f4e6d277ae | |||
| 1328a8e9e2 | |||
| 5bf620916f | |||
| 4f56e20441 | |||
| 9dda618b73 | |||
| da60c05188 | |||
| 4366855d54 | |||
| fe05fd83fe | |||
| 826d207e6b | |||
| 66fc1e1284 | |||
| a1d489f5a5 |
+68
-15
@@ -1,27 +1,80 @@
|
||||
language: objective-c
|
||||
osx_image: xcode8
|
||||
xcode_project: FileProvider.xcodeproj
|
||||
osx_image: xcode8.2
|
||||
xcode_project: $PROJECTNAME.xcodeproj
|
||||
env:
|
||||
global:
|
||||
- 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:
|
||||
- DESTINATION="OS=10.2,name=iPad Air 2" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="YES" CARTHAGEDEPLOY="NO"
|
||||
- DESTINATION="OS=9.0,name=iPhone 6" SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SDK" RUN_TESTS="NO" BUILD_EXAMPLE="NO" POD="NO" CARTHAGEDEPLOY="YES"
|
||||
- DESTINATION="OS=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 FileProvider.xcodeproj -scheme "FileProvider OSX" -sdk macosx | xcpretty
|
||||
- xcodebuild -project FileProvider.xcodeproj -scheme "FileProvider iOS" -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO | xcpretty
|
||||
- xcodebuild -project FileProvider.xcodeproj -scheme "FileProvider tvOS" -sdk appletvsimulator ONLY_ACTIVE_ARCH=NO | xcpretty
|
||||
- pod lib lint --quick
|
||||
|
||||
# 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;
|
||||
fi
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
# Run `pod trunk push` if specified
|
||||
- if [ $POD == "YES" ] && [ -n "$TRAVIS_TAG" ]; then
|
||||
pod trunk push;
|
||||
fi
|
||||
|
||||
# - bash <(curl -s https://codecov.io/bash)
|
||||
before_deploy:
|
||||
- carthage build --no-skip-current
|
||||
- carthage archive FileProvider
|
||||
- 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: FileProvider.framework.zip
|
||||
provider: releases
|
||||
api_key: "$GITHUBTOKEN"
|
||||
file: $FRAMEWORK_NAME.zip
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
repo: amosavian/$PROJECTNAME
|
||||
tags: true
|
||||
condition: "$CARTHAGEDEPLOY = YES"
|
||||
@@ -16,8 +16,8 @@ Pod::Spec.new do |s|
|
||||
#
|
||||
|
||||
s.name = "FileProvider"
|
||||
s.version = "0.6.0"
|
||||
s.summary = "FileManager replacement for Local and Remote (WebDAV/Dropbox/SMB2) files on iOS and MacOS."
|
||||
s.version = "0.14.1"
|
||||
s.summary = "FileManager replacement for Local and Remote (WebDAV/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?
|
||||
@@ -26,8 +26,8 @@ Pod::Spec.new do |s|
|
||||
# * Finally, don't worry about the indent, CocoaPods strips it!
|
||||
s.description = <<-DESC
|
||||
This Swift library provide a swifty way to deal with local and remote files
|
||||
and directories in same way. For now Local and WebDAV providers are ready to use
|
||||
and SMB2, Dropbox, FTP and AmazonS3 is planned for future.
|
||||
and directories in same way. For now Local, WebDAV and Dropbox providers are ready to use.
|
||||
SMB2, FTP and AmazonS3 is planned for future.
|
||||
DESC
|
||||
|
||||
s.homepage = "https://github.com/amosavian/FileProvider"
|
||||
|
||||
@@ -10,9 +10,7 @@
|
||||
7902C0861D61B56D00564440 /* RemoteSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7902C0851D61B56D00564440 /* RemoteSession.swift */; };
|
||||
7902C0871D61B67100564440 /* RemoteSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7902C0851D61B56D00564440 /* RemoteSession.swift */; };
|
||||
7902C0881D61B67100564440 /* RemoteSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7902C0851D61B56D00564440 /* RemoteSession.swift */; };
|
||||
7924B1931D89DAE000589DB7 /* AEXML.h in Headers */ = {isa = PBXBuildFile; fileRef = 7924B18C1D89DAE000589DB7 /* AEXML.h */; };
|
||||
7924B1941D89DAE000589DB7 /* AEXML.h in Headers */ = {isa = PBXBuildFile; fileRef = 7924B18C1D89DAE000589DB7 /* AEXML.h */; };
|
||||
7924B1951D89DAE000589DB7 /* AEXML.h in Headers */ = {isa = PBXBuildFile; fileRef = 7924B18C1D89DAE000589DB7 /* AEXML.h */; };
|
||||
791950F51DE58A5400B4426E /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 791950F41DE58A5400B4426E /* libxml2.tbd */; };
|
||||
7924B1961D89DAE000589DB7 /* Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7924B18D1D89DAE000589DB7 /* Document.swift */; };
|
||||
7924B1971D89DAE000589DB7 /* Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7924B18D1D89DAE000589DB7 /* Document.swift */; };
|
||||
7924B1981D89DAE000589DB7 /* Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7924B18D1D89DAE000589DB7 /* Document.swift */; };
|
||||
@@ -22,9 +20,6 @@
|
||||
7924B19C1D89DAE000589DB7 /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7924B18F1D89DAE000589DB7 /* Error.swift */; };
|
||||
7924B19D1D89DAE000589DB7 /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7924B18F1D89DAE000589DB7 /* Error.swift */; };
|
||||
7924B19E1D89DAE000589DB7 /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7924B18F1D89DAE000589DB7 /* Error.swift */; };
|
||||
7924B19F1D89DAE000589DB7 /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 7924B1901D89DAE000589DB7 /* Info.plist */; };
|
||||
7924B1A01D89DAE000589DB7 /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 7924B1901D89DAE000589DB7 /* Info.plist */; };
|
||||
7924B1A11D89DAE000589DB7 /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 7924B1901D89DAE000589DB7 /* Info.plist */; };
|
||||
7924B1A21D89DAE000589DB7 /* Options.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7924B1911D89DAE000589DB7 /* Options.swift */; };
|
||||
7924B1A31D89DAE000589DB7 /* Options.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7924B1911D89DAE000589DB7 /* Options.swift */; };
|
||||
7924B1A41D89DAE000589DB7 /* Options.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7924B1911D89DAE000589DB7 /* Options.swift */; };
|
||||
@@ -37,6 +32,12 @@
|
||||
7924B1B11D89F7DF00589DB7 /* FPSStreamTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7924B1A81D89F79200589DB7 /* FPSStreamTask.swift */; };
|
||||
7924B1B21D89FCDA00589DB7 /* FPSStreamTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7924B1A81D89F79200589DB7 /* FPSStreamTask.swift */; };
|
||||
7924B1B31D89FD6400589DB7 /* SMBClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396971D48C02300086753 /* SMBClient.swift */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
794C21FE1D58912A00EC49B8 /* DropboxHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */; };
|
||||
794C21FF1D58912A00EC49B8 /* DropboxHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */; };
|
||||
794C22001D58912A00EC49B8 /* DropboxHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */; };
|
||||
@@ -94,18 +95,42 @@
|
||||
799396E01D48C02300086753 /* WebDAVFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396A61D48C02300086753 /* WebDAVFileProvider.swift */; };
|
||||
799396E11D48C02300086753 /* WebDAVFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396A61D48C02300086753 /* WebDAVFileProvider.swift */; };
|
||||
799396E21D48C02300086753 /* WebDAVFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799396A61D48C02300086753 /* WebDAVFileProvider.swift */; };
|
||||
79BD638C1E2CC2300035128C /* ExtendedLocalFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD638B1E2CC2300035128C /* ExtendedLocalFileProvider.swift */; };
|
||||
79BD638D1E2CC2300035128C /* ExtendedLocalFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD638B1E2CC2300035128C /* ExtendedLocalFileProvider.swift */; };
|
||||
79BD638E1E2CC2300035128C /* ExtendedLocalFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BD638B1E2CC2300035128C /* ExtendedLocalFileProvider.swift */; };
|
||||
79BD63A81E2CC2940035128C /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63A71E2CC2940035128C /* CoreGraphics.framework */; };
|
||||
79BD63AA1E2CC2BB0035128C /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63A91E2CC2BB0035128C /* AVFoundation.framework */; };
|
||||
79BD63AC1E2CC2C20035128C /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63AB1E2CC2C20035128C /* ImageIO.framework */; };
|
||||
79BD63B01E2CC3300035128C /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63AF1E2CC3300035128C /* libxml2.tbd */; };
|
||||
79BD63B21E2CC3350035128C /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63B11E2CC3350035128C /* ImageIO.framework */; };
|
||||
79BD63B61E2CC3860035128C /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63B51E2CC3860035128C /* CoreFoundation.framework */; };
|
||||
79BD63B81E2CC38D0035128C /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63B71E2CC38D0035128C /* AVFoundation.framework */; };
|
||||
79BD63BA1E2CC39B0035128C /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63B91E2CC39B0035128C /* libxml2.tbd */; };
|
||||
79BD63BE1E2CC3C20035128C /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD63BD1E2CC3C20035128C /* ImageIO.framework */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
7902C0851D61B56D00564440 /* RemoteSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteSession.swift; sourceTree = "<group>"; };
|
||||
7924B18C1D89DAE000589DB7 /* AEXML.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AEXML.h; sourceTree = "<group>"; };
|
||||
791950F41DE58A5400B4426E /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.tbd; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.1.sdk/usr/lib/libxml2.tbd; sourceTree = DEVELOPER_DIR; };
|
||||
7924B18D1D89DAE000589DB7 /* Document.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Document.swift; sourceTree = "<group>"; };
|
||||
7924B18E1D89DAE000589DB7 /* Element.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Element.swift; sourceTree = "<group>"; };
|
||||
7924B18F1D89DAE000589DB7 /* Error.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = "<group>"; };
|
||||
7924B1901D89DAE000589DB7 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
7924B1911D89DAE000589DB7 /* Options.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Options.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -133,6 +158,21 @@
|
||||
799396A31D48C02300086753 /* SMB2Types.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMB2Types.swift; sourceTree = "<group>"; };
|
||||
799396A41D48C02300086753 /* SMBErrorType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMBErrorType.swift; sourceTree = "<group>"; };
|
||||
799396A61D48C02300086753 /* WebDAVFileProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebDAVFileProvider.swift; sourceTree = "<group>"; };
|
||||
79BD638B1E2CC2300035128C /* ExtendedLocalFileProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtendedLocalFileProvider.swift; sourceTree = "<group>"; };
|
||||
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; };
|
||||
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; };
|
||||
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; };
|
||||
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>"; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -140,6 +180,10 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
79BD63AA1E2CC2BB0035128C /* AVFoundation.framework in Frameworks */,
|
||||
79BD63AC1E2CC2C20035128C /* ImageIO.framework in Frameworks */,
|
||||
79BD63A81E2CC2940035128C /* CoreGraphics.framework in Frameworks */,
|
||||
791950F51DE58A5400B4426E /* libxml2.tbd in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -147,6 +191,10 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
79BD63B81E2CC38D0035128C /* AVFoundation.framework in Frameworks */,
|
||||
79BD63B61E2CC3860035128C /* CoreFoundation.framework in Frameworks */,
|
||||
79BD63B21E2CC3350035128C /* ImageIO.framework in Frameworks */,
|
||||
79BD63B01E2CC3300035128C /* libxml2.tbd in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -154,20 +202,41 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
79BD63C21E2CC3D30035128C /* AVFoundation.framework in Frameworks */,
|
||||
79BD63C01E2CC3CD0035128C /* CoreGraphics.framework in Frameworks */,
|
||||
79BD63BE1E2CC3C20035128C /* ImageIO.framework in Frameworks */,
|
||||
79BD63BA1E2CC39B0035128C /* libxml2.tbd in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
791950F31DE58A5300B4426E /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
79BD63C11E2CC3D30035128C /* AVFoundation.framework */,
|
||||
79BD63BF1E2CC3CD0035128C /* CoreGraphics.framework */,
|
||||
79BD63BD1E2CC3C20035128C /* ImageIO.framework */,
|
||||
79BD63B91E2CC39B0035128C /* libxml2.tbd */,
|
||||
79BD63B71E2CC38D0035128C /* AVFoundation.framework */,
|
||||
79BD63B51E2CC3860035128C /* CoreFoundation.framework */,
|
||||
79BD63B11E2CC3350035128C /* ImageIO.framework */,
|
||||
79BD63AF1E2CC3300035128C /* libxml2.tbd */,
|
||||
79BD63AB1E2CC2C20035128C /* ImageIO.framework */,
|
||||
79BD63A91E2CC2BB0035128C /* AVFoundation.framework */,
|
||||
79BD63A71E2CC2940035128C /* CoreGraphics.framework */,
|
||||
791950F41DE58A5400B4426E /* libxml2.tbd */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7924B18B1D89DAE000589DB7 /* AEXML */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7924B18C1D89DAE000589DB7 /* AEXML.h */,
|
||||
7924B18D1D89DAE000589DB7 /* Document.swift */,
|
||||
7924B18E1D89DAE000589DB7 /* Element.swift */,
|
||||
7924B18F1D89DAE000589DB7 /* Error.swift */,
|
||||
7924B1901D89DAE000589DB7 /* Info.plist */,
|
||||
7924B1911D89DAE000589DB7 /* Options.swift */,
|
||||
7924B1921D89DAE000589DB7 /* Parser.swift */,
|
||||
);
|
||||
@@ -177,9 +246,11 @@
|
||||
7993965B1D48B7BF00086753 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
79E34A101E2AC6C600E1293B /* Extra */,
|
||||
799396911D48C02300086753 /* Sources */,
|
||||
7993968A1D48B8C700086753 /* Pod */,
|
||||
799396681D48B7F600086753 /* Products */,
|
||||
791950F31DE58A5300B4426E /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -206,14 +277,20 @@
|
||||
799396911D48C02300086753 /* Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
799396991D48C02300086753 /* SMBTypes */,
|
||||
799396931D48C02300086753 /* DropboxFileProvider.swift */,
|
||||
7924B18B1D89DAE000589DB7 /* AEXML */,
|
||||
794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */,
|
||||
799396991D48C02300086753 /* SMBTypes */,
|
||||
799396941D48C02300086753 /* FileProvider.h */,
|
||||
799396951D48C02300086753 /* FileProvider.swift */,
|
||||
79F5745A1DFDB10A00179ABF /* FileObject.swift */,
|
||||
799396961D48C02300086753 /* LocalFileProvider.swift */,
|
||||
792572401DF23BDA006A1526 /* LocalHelper.swift */,
|
||||
79480FF51E3ABDD0007E7275 /* CloudFileProvider.swift */,
|
||||
79BD638B1E2CC2300035128C /* ExtendedLocalFileProvider.swift */,
|
||||
7902C0851D61B56D00564440 /* RemoteSession.swift */,
|
||||
799396931D48C02300086753 /* DropboxFileProvider.swift */,
|
||||
794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */,
|
||||
79BD63C31E2D17880035128C /* OneDriveFileProvide.swift */,
|
||||
79BD63C41E2D17880035128C /* OneDriveHelper.swift */,
|
||||
7924B1A81D89F79200589DB7 /* FPSStreamTask.swift */,
|
||||
799396971D48C02300086753 /* SMBClient.swift */,
|
||||
799396981D48C02300086753 /* SMBFileProvider.swift */,
|
||||
@@ -242,6 +319,13 @@
|
||||
path = SMBTypes;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
79E34A101E2AC6C600E1293B /* Extra */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
name = Extra;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXHeadersBuildPhase section */
|
||||
@@ -249,7 +333,6 @@
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
7924B1931D89DAE000589DB7 /* AEXML.h in Headers */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -257,7 +340,6 @@
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
7924B1941D89DAE000589DB7 /* AEXML.h in Headers */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -265,7 +347,6 @@
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
7924B1951D89DAE000589DB7 /* AEXML.h in Headers */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -332,7 +413,7 @@
|
||||
7993965C1D48B7BF00086753 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 0800;
|
||||
LastUpgradeCheck = 0810;
|
||||
TargetAttributes = {
|
||||
799396661D48B7F600086753 = {
|
||||
CreatedOnToolsVersion = 7.3.1;
|
||||
@@ -370,7 +451,6 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
7924B19F1D89DAE000589DB7 /* Info.plist in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -378,7 +458,6 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
7924B1A01D89DAE000589DB7 /* Info.plist in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -386,7 +465,6 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
7924B1A11D89DAE000589DB7 /* Info.plist in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -400,11 +478,16 @@
|
||||
799396B31D48C02300086753 /* LocalFileProvider.swift in Sources */,
|
||||
799396D41D48C02300086753 /* SMB2Tree.swift in Sources */,
|
||||
7924B1A21D89DAE000589DB7 /* Options.swift in Sources */,
|
||||
792572411DF23BDA006A1526 /* LocalHelper.swift in Sources */,
|
||||
79480FF61E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */,
|
||||
79F5745B1DFDB10B00179ABF /* FileObject.swift in Sources */,
|
||||
79BD63C51E2D17880035128C /* OneDriveFileProvide.swift in Sources */,
|
||||
7924B1991D89DAE000589DB7 /* Element.swift in Sources */,
|
||||
799396C81D48C02300086753 /* SMB2IOCtl.swift in Sources */,
|
||||
799396D71D48C02300086753 /* SMB2Types.swift in Sources */,
|
||||
7924B1B21D89FCDA00589DB7 /* FPSStreamTask.swift in Sources */,
|
||||
799396C51D48C02300086753 /* SMB2FileOperation.swift in Sources */,
|
||||
79BD63C81E2D17880035128C /* OneDriveHelper.swift in Sources */,
|
||||
799396BF1D48C02300086753 /* SMB2DataTypes.swift in Sources */,
|
||||
799396B91D48C02300086753 /* SMBFileProvider.swift in Sources */,
|
||||
794C220E1D591A4B00EC49B8 /* SMB2QueryTypes.swift in Sources */,
|
||||
@@ -420,6 +503,7 @@
|
||||
799396C21D48C02300086753 /* SMB2FileHandle.swift in Sources */,
|
||||
799396CB1D48C02300086753 /* SMB2Query.swift in Sources */,
|
||||
799396AA1D48C02300086753 /* DropboxFileProvider.swift in Sources */,
|
||||
79BD638C1E2CC2300035128C /* ExtendedLocalFileProvider.swift in Sources */,
|
||||
7924B1B31D89FD6400589DB7 /* SMBClient.swift in Sources */,
|
||||
7924B1A51D89DAE000589DB7 /* Parser.swift in Sources */,
|
||||
799396B01D48C02300086753 /* FileProvider.swift in Sources */,
|
||||
@@ -434,11 +518,16 @@
|
||||
799396B41D48C02300086753 /* LocalFileProvider.swift in Sources */,
|
||||
799396D51D48C02300086753 /* SMB2Tree.swift in Sources */,
|
||||
7924B1A31D89DAE000589DB7 /* Options.swift in Sources */,
|
||||
792572421DF23BDA006A1526 /* LocalHelper.swift in Sources */,
|
||||
79480FF71E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */,
|
||||
79F5745C1DFDB10B00179ABF /* FileObject.swift in Sources */,
|
||||
79BD63C61E2D17880035128C /* OneDriveFileProvide.swift in Sources */,
|
||||
7924B1B01D89F7DE00589DB7 /* FPSStreamTask.swift in Sources */,
|
||||
7924B19A1D89DAE000589DB7 /* Element.swift in Sources */,
|
||||
799396C91D48C02300086753 /* SMB2IOCtl.swift in Sources */,
|
||||
799396D81D48C02300086753 /* SMB2Types.swift in Sources */,
|
||||
799396C61D48C02300086753 /* SMB2FileOperation.swift in Sources */,
|
||||
79BD63C91E2D17880035128C /* OneDriveHelper.swift in Sources */,
|
||||
799396C01D48C02300086753 /* SMB2DataTypes.swift in Sources */,
|
||||
799396BA1D48C02300086753 /* SMBFileProvider.swift in Sources */,
|
||||
794C220F1D591A4B00EC49B8 /* SMB2QueryTypes.swift in Sources */,
|
||||
@@ -454,6 +543,7 @@
|
||||
799396C31D48C02300086753 /* SMB2FileHandle.swift in Sources */,
|
||||
799396CC1D48C02300086753 /* SMB2Query.swift in Sources */,
|
||||
7924B1AD1D89F7D800589DB7 /* SMBClient.swift in Sources */,
|
||||
79BD638D1E2CC2300035128C /* ExtendedLocalFileProvider.swift in Sources */,
|
||||
799396AB1D48C02300086753 /* DropboxFileProvider.swift in Sources */,
|
||||
7924B1A61D89DAE000589DB7 /* Parser.swift in Sources */,
|
||||
799396B11D48C02300086753 /* FileProvider.swift in Sources */,
|
||||
@@ -468,11 +558,16 @@
|
||||
799396B51D48C02300086753 /* LocalFileProvider.swift in Sources */,
|
||||
799396D61D48C02300086753 /* SMB2Tree.swift in Sources */,
|
||||
7924B1A41D89DAE000589DB7 /* Options.swift in Sources */,
|
||||
792572431DF23BDA006A1526 /* LocalHelper.swift in Sources */,
|
||||
79480FF81E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */,
|
||||
79F5745D1DFDB10B00179ABF /* FileObject.swift in Sources */,
|
||||
79BD63C71E2D17880035128C /* OneDriveFileProvide.swift in Sources */,
|
||||
7924B1B11D89F7DF00589DB7 /* FPSStreamTask.swift in Sources */,
|
||||
7924B19B1D89DAE000589DB7 /* Element.swift in Sources */,
|
||||
799396CA1D48C02300086753 /* SMB2IOCtl.swift in Sources */,
|
||||
799396D91D48C02300086753 /* SMB2Types.swift in Sources */,
|
||||
799396C71D48C02300086753 /* SMB2FileOperation.swift in Sources */,
|
||||
79BD63CA1E2D17880035128C /* OneDriveHelper.swift in Sources */,
|
||||
799396C11D48C02300086753 /* SMB2DataTypes.swift in Sources */,
|
||||
799396BB1D48C02300086753 /* SMBFileProvider.swift in Sources */,
|
||||
794C22101D591A4B00EC49B8 /* SMB2QueryTypes.swift in Sources */,
|
||||
@@ -488,6 +583,7 @@
|
||||
799396C41D48C02300086753 /* SMB2FileHandle.swift in Sources */,
|
||||
799396CD1D48C02300086753 /* SMB2Query.swift in Sources */,
|
||||
7924B1AE1D89F7D900589DB7 /* SMBClient.swift in Sources */,
|
||||
79BD638E1E2CC2300035128C /* ExtendedLocalFileProvider.swift in Sources */,
|
||||
799396AC1D48C02300086753 /* DropboxFileProvider.swift in Sources */,
|
||||
7924B1A71D89DAE000589DB7 /* Parser.swift in Sources */,
|
||||
799396B21D48C02300086753 /* FileProvider.swift in Sources */,
|
||||
@@ -501,6 +597,7 @@
|
||||
799396601D48B7BF00086753 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_VERSION_STRING = 0.14.1;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
@@ -510,9 +607,12 @@
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
@@ -527,6 +627,7 @@
|
||||
799396611D48B7BF00086753 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_VERSION_STRING = 0.14.1;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
@@ -536,6 +637,8 @@
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
@@ -554,9 +657,8 @@
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
BUNDLE_VERSION_STRING = 0.8.2;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
@@ -568,19 +670,15 @@
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEFINES_MODULE = YES;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
@@ -596,7 +694,6 @@
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FileProvider-iOS";
|
||||
PRODUCT_NAME = FileProvider;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -613,9 +710,8 @@
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
BUNDLE_VERSION_STRING = 0.8.2;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
@@ -627,16 +723,13 @@
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEFINES_MODULE = YES;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
@@ -680,20 +773,17 @@
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEFINES_MODULE = YES;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
FRAMEWORK_VERSION = A;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
@@ -709,7 +799,6 @@
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks";
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.10;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FileProvider-OSX";
|
||||
PRODUCT_NAME = FileProvider;
|
||||
SDKROOT = macosx;
|
||||
@@ -740,17 +829,15 @@
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEFINES_MODULE = YES;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
FRAMEWORK_VERSION = A;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
@@ -791,19 +878,16 @@
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEFINES_MODULE = YES;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
@@ -818,7 +902,6 @@
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mousavian.FileProvider-tvOS";
|
||||
PRODUCT_NAME = FileProvider;
|
||||
SDKROOT = appletvos;
|
||||
@@ -850,16 +933,14 @@
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEFINES_MODULE = YES;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0800"
|
||||
LastUpgradeVersion = "0810"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0800"
|
||||
LastUpgradeVersion = "0810"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0800"
|
||||
LastUpgradeVersion = "0810"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Amir Abbas Mousavian
|
||||
Copyright (c) 2016-17 Amir Abbas Mousavian
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.3.0</string>
|
||||
<string>${BUNDLE_VERSION_STRING}</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.3.0</string>
|
||||
<string>${BUNDLE_VERSION_STRING}</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.3.0</string>
|
||||
<string>${BUNDLE_VERSION_STRING}</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
|
||||
@@ -1,39 +1,48 @@
|
||||
# FileProvider
|
||||

|
||||
|
||||
>This Swift library provide a swifty way to deal with local and remote files and directories in a unified way.
|
||||
|
||||
[![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]
|
||||
[](https://cocoapods.org/pods/FileProvider)
|
||||
[![codebeat badge][codebeat-image]][codebeat-url]
|
||||
[![Codebeat Badge][codebeat-image]][codebeat-url]
|
||||
[![Cocoapods Docs][docs-image]][docs-url]
|
||||
[![Cocoapods Downloads][cocoapods-downloads]][cocoapods]
|
||||
[![Cocoapods Apps][cocoapods-apps]][cocoapods]
|
||||
|
||||
<!---
|
||||
|
||||
[](https://codecov.io/gh/amosavian/FileProvider)
|
||||
[](https://github.com/Carthage/Carthage)
|
||||
[](https://codecov.io/gh/amosavian/FileProvider)
|
||||
--->
|
||||
|
||||
This library provides implementaion of WebDav and SMB2 (incomplete) and local files.
|
||||
This library provides implementaion of WebDav, 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.
|
||||
|
||||
## 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] **DropboxFileProvider** A wrapper around Dropbox Web API. For now it has limitation in uploading files up to 150MB.
|
||||
- [ ] **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. I implemented data types and some basic functions but *main interface is not implemented yet!* SMB1/CIFS is depericated and very tricky to be implemented
|
||||
- [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.
|
||||
- [ ] **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.
|
||||
- [ ] **AmazonS3FileProvider**
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Swift 3**
|
||||
- **Swift 3.0 +**
|
||||
- iOS 8.0 , OSX 10.10
|
||||
- XCode 8.0
|
||||
|
||||
@@ -43,27 +52,48 @@ Legacy version is available in swift-2 branch
|
||||
|
||||
### Cocoapods / Carthage / Swift Package Manager
|
||||
|
||||
FileProvider supports both CocoaPods.
|
||||
|
||||
Add this line to your pods file:
|
||||
|
||||
pod "FileProvider"
|
||||
```ruby
|
||||
pod "FileProvider"
|
||||
```
|
||||
|
||||
Or add this to Cartfile:
|
||||
|
||||
```
|
||||
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)
|
||||
```
|
||||
|
||||
### Manually
|
||||
|
||||
### Git
|
||||
To have latest updates with ease, use this command on terminal to get a clone:
|
||||
|
||||
git clone https://github.com/amosavian/FileProvider FileProvider
|
||||
|
||||
```bash
|
||||
git clone https://github.com/amosavian/FileProvider
|
||||
```
|
||||
|
||||
You can update your library using this command in FileProvider folder:
|
||||
|
||||
git pull
|
||||
```bash
|
||||
git pull
|
||||
```
|
||||
|
||||
if you have a git based project, use this command in your projects directory to add this project as a submodule to your project:
|
||||
|
||||
git submodule add https://github.com/amosavian/FileProvider FileProvider
|
||||
```bash
|
||||
git submodule add https://github.com/amosavian/FileProvider
|
||||
```
|
||||
Then you can do either:
|
||||
|
||||
### Manually
|
||||
Copy Source folder to your project and Voila!
|
||||
* 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.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -73,28 +103,47 @@ Each provider has a specific class which conforms to FileProvider protocol and s
|
||||
|
||||
For LocalFileProvider if you want to deal with `Documents` folder
|
||||
|
||||
let documentsProvider = LocalFileProvider()
|
||||
``` swift
|
||||
let documentsProvider = LocalFileProvider()
|
||||
|
||||
is equal to:
|
||||
|
||||
let documentPath = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true);
|
||||
let documentsURL = URL(fileURLWithPath: documentPath);
|
||||
let documentsProvider = LocalFileProvider(baseURL: documentsURL)
|
||||
// Equals with:
|
||||
let documentsProvider = LocalFileProvider(directory: .documentDirectory, domainMask: = .userDomainMask)
|
||||
|
||||
// Equals with:
|
||||
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
let documentsProvider = LocalFileProvider(baseURL: documentsURL)
|
||||
```
|
||||
|
||||
Also for using group shared container:
|
||||
|
||||
```swift
|
||||
let documentsProvider = LocalFileProvider(sharedContainerId: "group.yourcompany.appContainer")
|
||||
// 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 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
|
||||
let documentsProvider = CloudFileProvider(containerId: nil)
|
||||
```
|
||||
|
||||
|
||||
For remote file providers authentication may be necessary:
|
||||
|
||||
let credential = URLCredential(user: "user", password: "pass", persistence: .permanent)
|
||||
let webdavProvider = WebDAVFileProvider(baseURL: URL(string: "http://www.example.com/dav")!, credential: credential)
|
||||
``` swift
|
||||
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)
|
||||
|
||||
* For Dropbox, 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.
|
||||
* 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
|
||||
|
||||
@@ -104,40 +153,42 @@ It's simply three method which indicated whether the operation failed, succeed a
|
||||
|
||||
Your class should conforms `FileProviderDelegate` class:
|
||||
|
||||
override func viewDidLoad() {
|
||||
documentsProvider.delegate = self as FileProviderDelegate
|
||||
}
|
||||
```swift
|
||||
override func viewDidLoad() {
|
||||
documentsProvider.delegate = self as FileProviderDelegate
|
||||
}
|
||||
|
||||
func fileproviderSucceed(_ fileProvider: FileProviderOperations, operation: FileOperation) {
|
||||
switch operation {
|
||||
case .copy(source: let source, destination: let dest):
|
||||
print("\(source) copied to \(dest).")
|
||||
case .remove(path: let path):
|
||||
print("\(path) has been deleted.")
|
||||
default:
|
||||
break
|
||||
}
|
||||
func fileproviderSucceed(_ fileProvider: FileProviderOperations, operation: FileOperationType) {
|
||||
switch operation {
|
||||
case .copy(source: let source, destination: let dest):
|
||||
print("\(source) copied to \(dest).")
|
||||
case .remove(path: let path):
|
||||
print("\(path) has been deleted.")
|
||||
default:
|
||||
print("\(operation.actionDescription) from \(operation.source!) to \(operation.destination) succeed")
|
||||
}
|
||||
|
||||
func fileproviderFailed(_ fileProvider: FileProviderOperations, operation: FileOperation) {
|
||||
switch operation {
|
||||
case .copy(source: let source, destination: let dest):
|
||||
print("copy of \(source) failed.")
|
||||
case .remove(path: let path):
|
||||
print("\(path) can't be deleted.")
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func fileproviderProgress(_ fileProvider: FileProviderOperations, operation: FileOperation, progress: Float) {
|
||||
switch operation {
|
||||
case .copy(source: let source, destination: let dest):
|
||||
print("Copy\(source) to \(dest): \(progress * 100) completed.")
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func fileproviderFailed(_ fileProvider: FileProviderOperations, operation: FileOperationType) {
|
||||
switch operation {
|
||||
case .copy(source: let source, destination: let dest):
|
||||
print("copy of \(source) failed.")
|
||||
case .remove:
|
||||
print("file can't be deleted.")
|
||||
default:
|
||||
print("\(operation.actionDescription) from \(operation.source!) to \(operation.destination) failed")
|
||||
}
|
||||
}
|
||||
|
||||
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.")
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** `fileproviderProgress()` delegate method is not called by `LocalFileProvider` currently.
|
||||
|
||||
@@ -159,42 +210,50 @@ There is a `FileObject` class which holds file attributes like size and creation
|
||||
|
||||
For a single file:
|
||||
|
||||
documentsProvider.attributesOfItem(path: "/file.txt", completionHandler: {
|
||||
(attributes: LocalFileObject?, error: ErrorType?) -> Void in
|
||||
if let attributes = attributes {
|
||||
print("File Size: \(attributes.size)")
|
||||
print("Creation Date: \(attributes.createdDate)")
|
||||
print("Modification Date: \(attributes.modifiedDate)")
|
||||
print("Is Read Only: \(attributes.isReadOnly)")
|
||||
}
|
||||
})
|
||||
```swift
|
||||
documentsProvider.attributesOfItem(path: "/file.txt", completionHandler: {
|
||||
attributes, error in
|
||||
if let attributes = attributes {
|
||||
print("File Size: \(attributes.size)")
|
||||
print("Creation Date: \(attributes.creationDate)")
|
||||
print("Modification Date: \(attributes.modifiedDate)")
|
||||
print("Is Read Only: \(attributes.isReadOnly)")
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
To get list of files in a directory:
|
||||
|
||||
documentsProvider.contentsOfDirectory(path: "/", completionHandler: {
|
||||
(contents: [LocalFileObject], error: ErrorType?) -> Void in
|
||||
for file in contents {
|
||||
print("Name: \(attributes.name)")
|
||||
print("Size: \(attributes.size)")
|
||||
print("Creation Date: \(attributes.createdDate)")
|
||||
print("Modification Date: \(attributes.modifiedDate)")
|
||||
}
|
||||
})
|
||||
```swift
|
||||
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)")
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
To get size of strage and used/free space:
|
||||
|
||||
func storageProperties(completionHandler: {(total: Int64, used: Int64) -> Void in
|
||||
print("Total Storage Space: \(total)")
|
||||
print("Used Space: \(used)")
|
||||
print("Free Space: \(total - used)")
|
||||
})
|
||||
```swift
|
||||
func storageProperties(completionHandler: { total, used in
|
||||
print("Total Storage Space: \(total)")
|
||||
print("Used Space: \(used)")
|
||||
print("Free Space: \(total - used)")
|
||||
})
|
||||
```
|
||||
|
||||
* 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
|
||||
|
||||
documentsProvider.currentPath = "/New Folder"
|
||||
// now path is ~/Documents/New Folder
|
||||
```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.
|
||||
|
||||
@@ -202,81 +261,190 @@ You can then pass "" (empty string) to `contentsOfDirectory` method to list file
|
||||
|
||||
Creating new directory:
|
||||
|
||||
documentsProvider.create(folder: "new folder", at: "/", completionHandler: nil)
|
||||
```swift
|
||||
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 stream:
|
||||
Creating new file from data:
|
||||
|
||||
let data = "hello world!".data(encoding: String.encoding.utf8)
|
||||
let file = FileObject(name: "old.txt", createdDate: Date(), modifiedDate: Date(), isHidden: false, isReadOnly: true)
|
||||
documentsProvider.create(file: file, at: "/", contents: data, completionHandler: nil)
|
||||
```swift
|
||||
let data = "hello world!".data(encoding: .utf8)
|
||||
documentsProvider.create(file: "newFile.txt", at: "/", contents: data, completionHandler: nil)
|
||||
```
|
||||
|
||||
### Copy and Move/Rename Files
|
||||
|
||||
Copy file old.txt to new.txt in current path:
|
||||
|
||||
documentsProvider.copyItem(path: "new folder/old.txt", to: "new.txt", overwrite: false, completionHandler: nil)
|
||||
```swift
|
||||
documentsProvider.copyItem(path: "new folder/old.txt", to: "new.txt", overwrite: false, completionHandler: nil)
|
||||
```
|
||||
|
||||
Move file old.txt to new.txt in current path:
|
||||
|
||||
documentsProvider.moveItem(path: "new folder/old.txt", to: "new.txt", overwrite: false, completionHandler: nil)
|
||||
```swift
|
||||
documentsProvider.moveItem(path: "new folder/old.txt", to: "new.txt", overwrite: false, completionHandler: nil)
|
||||
```
|
||||
|
||||
**Note:** To have a consistent behaviour, create intermediate directories first if necessary.
|
||||
**Note:** To have a consistent behavior, create intermediate directories first if necessary.
|
||||
|
||||
### Delete Files
|
||||
|
||||
documentsProvider.removeItem(path: "new.txt", completionHandler: nil)
|
||||
```swift
|
||||
documentsProvider.removeItem(path: "new.txt", completionHandler: nil)
|
||||
```
|
||||
|
||||
***Caution:*** This method will delete directories with all it's content recursively.
|
||||
***Caution:*** This method will delete directories with all it's contents recursively.
|
||||
|
||||
### Retrieve Content of File
|
||||
### 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.
|
||||
|
||||
documentsProvider.contents(path: "old.txt", completionHandler: {
|
||||
(contents: Data?, error: ErrorType?) -> Void
|
||||
if let contents = contents {
|
||||
print(String(data: contents, encoding: String.encoding.utf8)) // "hello world!"
|
||||
}
|
||||
})
|
||||
```swift
|
||||
documentsProvider.contents(path: "old.txt", completionHandler: {
|
||||
contents, error in
|
||||
if let contents = contents {
|
||||
print(String(data: contents, encoding: .utf8)) // "hello world!"
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
If you want to retrieve a portion of file you can use `contents` method with offset and length arguments. Please note first byte of file has offset: 0.
|
||||
|
||||
documentsProvider.contents(path: "old.txt", offset: 2, length: 5, completionHandler: {
|
||||
(contents: Data?, error: ErrorType?) -> Void
|
||||
if let contents = contents {
|
||||
print(String(data: contents, encoding: String.encoding.utf8)) // "llo w"
|
||||
}
|
||||
})
|
||||
```swift
|
||||
documentsProvider.contents(path: "old.txt", offset: 2, length: 5, completionHandler: {
|
||||
contents, error in
|
||||
if let contents = contents {
|
||||
print(String(data: contents, encoding: .utf8)) // "llo w"
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Write Data To Files
|
||||
|
||||
let data = "What's up Newyork!".data(encoding: String.encoding.utf8)
|
||||
documentsProvider.writeContents(path: "old.txt", content: data, atomically: true, completionHandler: nil)
|
||||
```swift
|
||||
let data = "What's up Newyork!".data(encoding: .utf8)
|
||||
documentsProvider.writeContents(path: "old.txt", content: data, atomically: true, completionHandler: nil)
|
||||
```
|
||||
|
||||
### 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
|
||||
}
|
||||
```
|
||||
|
||||
### Operation Handle
|
||||
|
||||
Creating/Copying/Deleting functions return a `OperationHandle` for remote operations. It provides progress and a `.cancel()` method which allows you to cancel operation in midst.
|
||||
Creating/Copying/Deleting functions return a `OperationHandle` for remote operations. It provides operation type, progress and a `.cancel()` method which allows you to cancel operation in midst.
|
||||
|
||||
It's not supported by native `(NS)FileManager` so `LocalFileProvider`, but this functionality will be added to future `PosixFileProvider` class.
|
||||
|
||||
### Monitoring FIle Changes
|
||||
### 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.
|
||||
|
||||
documentsProvider.registerNotifcation(path: provider.currentPath)
|
||||
{
|
||||
// calling functions to update UI
|
||||
}
|
||||
```swift
|
||||
// to register a new notification handler
|
||||
documentsProvider.registerNotifcation(path: provider.currentPath) {
|
||||
// calling functions to update UI
|
||||
}
|
||||
|
||||
// To discontinue monitoring folders:
|
||||
documentsProvider.unregisterNotifcation(path: provider.currentPath)
|
||||
// To discontinue monitoring folders:
|
||||
documentsProvider.unregisterNotifcation(path: provider.currentPath)
|
||||
```
|
||||
|
||||
* **Please note** in LocalFileProvider it will also monitor changes in subfolders. This behaviour can varies according to file system specification.
|
||||
|
||||
### Thumbnail and meta-information
|
||||
|
||||
Providers which conform `ExtendedFileProvider` are able to generate thumbnail or provide file meta-information for images, media and pdf files.
|
||||
|
||||
Local, OneDrive and Dropbox providers support this functionality.
|
||||
|
||||
##### Thumbnails
|
||||
To check either file thumbnail is supported or not and fetch thumbnail, use (and modify) these example code:
|
||||
|
||||
```swift
|
||||
let path = "/newImage.jpg"
|
||||
let thumbSize = CGSize(width: 64, height: 64)
|
||||
if documentsProvider.thumbnailOfFileSupported(path: path {
|
||||
documentsProvider.thumbnailOfFile(path: file.path, dimension: thumbSize, completionHandler: { (image, error) in
|
||||
DispatchQueue.main.async {
|
||||
self.previewImage.image = image
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* Please note it won't cache generated images. if you don't do it yourself, it may hit you app's performance.
|
||||
|
||||
##### Meta-informations
|
||||
|
||||
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
|
||||
for key in keys {
|
||||
print("\(key): \(propertiesDictionary[key])")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* **Bonus:** You can modify/extend Local provider generator by setting `LocalFileInformationGenerator` static variables and methods
|
||||
|
||||
## Contribute
|
||||
|
||||
We would love for you to contribute to **FileProvider**, check the `LICENSE` file for more info.
|
||||
|
||||
Things to do:
|
||||
|
||||
- [ ] 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)
|
||||
@@ -286,17 +454,28 @@ 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/)
|
||||
|
||||
[swift-image]:https://img.shields.io/badge/swift-3.0-orange.svg
|
||||
[cocoapods]: https://cocoapods.org/pods/FileProvider
|
||||
[swift-image]: https://img.shields.io/badge/swift-3.0-orange.svg
|
||||
[swift-url]: https://swift.org/
|
||||
[license-image]: https://img.shields.io/badge/License-MIT-blue.svg
|
||||
[platform-image]: https://img.shields.io/cocoapods/p/FileProvider.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
|
||||
[codebeat-url]: https://codebeat.co/projects/github-com-amosavian-fileprovider
|
||||
[travis-image]: https://img.shields.io/travis/amosavian/FileProvider/master.svg
|
||||
[travis-url]: https://travis-ci.org/amosavian/FileProvider
|
||||
[travis-url]: https://travis-ci.org/amosavian/FileProvider
|
||||
[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/
|
||||
@@ -1,29 +0,0 @@
|
||||
//
|
||||
// AEXML.h
|
||||
//
|
||||
// Copyright (c) 2014 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.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
FOUNDATION_EXPORT double AEXMLVersionNumber;
|
||||
FOUNDATION_EXPORT const unsigned char AEXMLVersionString[];
|
||||
|
||||
Executable → Regular
+1
-1
@@ -29,7 +29,7 @@ import Foundation
|
||||
|
||||
XML Parsing is also done with this object.
|
||||
*/
|
||||
open class AEXMLDocument: AEXMLElement {
|
||||
internal class AEXMLDocument: AEXMLElement {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
|
||||
Executable → Regular
+2
-2
@@ -30,7 +30,7 @@ import Foundation
|
||||
You can access its structure by using subscript like this: `element["foo"]["bar"]` which would
|
||||
return `<bar></bar>` element from `<element><foo><bar></bar></foo></element>` XML as an `AEXMLElement` object.
|
||||
*/
|
||||
open class AEXMLElement {
|
||||
internal class AEXMLElement {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
@@ -86,7 +86,7 @@ open class AEXMLElement {
|
||||
/// The first element with given name **(Empty element with error if not exists)**.
|
||||
open subscript(key: String) -> AEXMLElement {
|
||||
guard let
|
||||
first = children.filter({ $0.name == key }).first
|
||||
first = children.first(where: { $0.name == key })
|
||||
else {
|
||||
let errorElement = AEXMLElement(name: key)
|
||||
errorElement.error = AEXMLError.elementNotFound
|
||||
|
||||
Executable → Regular
+1
-1
@@ -25,7 +25,7 @@
|
||||
import Foundation
|
||||
|
||||
/// A type representing error value that can be thrown or inside `error` property of `AEXMLElement`.
|
||||
public enum AEXMLError: Error {
|
||||
internal enum AEXMLError: Error {
|
||||
/// This will be inside `error` property of `AEXMLElement` when subscript is used for not-existing element.
|
||||
case elementNotFound
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>3.0.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
Executable → Regular
+1
-1
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
|
||||
/// Options used in `AEXMLDocument`
|
||||
public struct AEXMLOptions {
|
||||
internal struct AEXMLOptions {
|
||||
|
||||
/// Values used in XML Document header
|
||||
public struct DocumentHeader {
|
||||
|
||||
Executable → Regular
@@ -0,0 +1,803 @@
|
||||
//
|
||||
// CloudFileProvider.swift
|
||||
// FileProvider
|
||||
//
|
||||
// Created by Amir Abbas Mousavian.
|
||||
// Copyright © 2017 Mousavian. Distributed under MIT license.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
Allows accessing to iCloud Drive stored files. Determine scope when initializing, to either access
|
||||
to public documents folder or files stored as data.
|
||||
|
||||
To setup a functional iCloud container, please
|
||||
[read this page](https://medium.com/ios-os-x-development/icloud-drive-documents-1a46b5706fe1).
|
||||
*/
|
||||
open class CloudFileProvider: LocalFileProvider {
|
||||
/// An string to identify type of provider.
|
||||
open override class var type: String { return "iCloudDrive" }
|
||||
|
||||
/// Forces file operations to use `NSFileCoordinating`,
|
||||
/// Actually this is readonly, and value is always true.
|
||||
override open var isCoorinating: Bool {
|
||||
get {
|
||||
return true
|
||||
}
|
||||
set {
|
||||
assert(true, "CloudFileProvider.isCoorinating can't be set")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/// The fully-qualified container identifier for an iCloud container directory.
|
||||
open fileprivate(set) var containerId: String?
|
||||
|
||||
/// Scope of container, indicates user can manipulate data/files or not.
|
||||
open fileprivate(set) var scope: UbiquitousScope
|
||||
|
||||
/// Set this property to ignore initiations asserting to be on secondary thread
|
||||
static open var asserting: Bool = true
|
||||
|
||||
/**
|
||||
Initializes the provider for the iCloud container associated with the specified identifier and
|
||||
establishes access to that container.
|
||||
|
||||
- Important: Do not call this method from your app’s main thread. Because this method might take a nontrivial amount of time to set up iCloud and return the requested URL, you should always call it from a secondary thread.
|
||||
|
||||
- Parameter containerId: The fully-qualified container identifier for an iCloud container directory. The string you specify must not contain wildcards and must be of the form `<TEAMID>.<CONTAINER>`, where `<TEAMID>` is your development team ID and `<CONTAINER>` is the bundle identifier of the container you want to access.\
|
||||
The container identifiers for your app must be declared in the `com.apple.developer.ubiquity-container-identifiers` array of the `.entitlements` property list file in your Xcode project.\
|
||||
If you specify nil for this parameter, this method uses the first container listed in the `com.apple.developer.ubiquity-container-identifiers` entitlement array.
|
||||
- Parameter scope: Use `.documents` (default) to put documents that the user is allowed to access inside a Documents subdirectory. Otherwise use `.data` to store user-related data files that your app needs to share but that are not files you want the user to manipulate directly.
|
||||
*/
|
||||
public init? (containerId: String?, scope: UbiquitousScope = .documents) {
|
||||
assert(!CloudFileProvider.asserting || !Thread.isMainThread, "LocalFileProvider.init(containerId:) is not recommended to be executed on Main Thread.")
|
||||
guard FileManager.default.ubiquityIdentityToken != nil else {
|
||||
return nil
|
||||
}
|
||||
guard let ubiquityURL = FileManager.default.url(forUbiquityContainerIdentifier: containerId) else {
|
||||
return nil
|
||||
}
|
||||
self.containerId = containerId
|
||||
self.scope = scope
|
||||
let baseURL: URL
|
||||
if scope == .documents {
|
||||
baseURL = ubiquityURL.appendingPathComponent("Documents/")
|
||||
} else {
|
||||
baseURL = ubiquityURL
|
||||
}
|
||||
|
||||
super.init(baseURL: baseURL)
|
||||
self.isCoorinating = true
|
||||
|
||||
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: .concurrent)
|
||||
operation_queue = OperationQueue()
|
||||
operation_queue.name = "FileProvider.\(type(of: self).type).Operation"
|
||||
|
||||
fileManager.url(forUbiquityContainerIdentifier: containerId)
|
||||
opFileManager.url(forUbiquityContainerIdentifier: containerId)
|
||||
|
||||
try? fileManager.createDirectory(at: baseURL, withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns an Array of `FileObject`s identifying the the directory entries via asynchronous completion handler.
|
||||
|
||||
If the directory contains no entries or an error is occured, this method will return the empty array.
|
||||
|
||||
- Parameter path: path to target directory. If empty, `currentPath` value will be used.
|
||||
- Parameter completionHandler: a closure with result of directory entries or error.
|
||||
`contents`: An array of `FileObject` identifying the the directory entries.
|
||||
`error`: Error returned by system.
|
||||
*/
|
||||
open override func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
|
||||
// FIXME: create runloop for dispatch_queue, start query on it
|
||||
dispatch_queue.async {
|
||||
let pathURL = self.url(of: path)
|
||||
|
||||
let query = NSMetadataQuery()
|
||||
query.predicate = NSPredicate(format: "%K BEGINSWITH %@", NSMetadataItemPathKey, pathURL.path)
|
||||
query.valueListAttributes = [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]
|
||||
query.searchScopes = [self.scope.rawValue]
|
||||
var finishObserver: NSObjectProtocol?
|
||||
finishObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
|
||||
defer {
|
||||
query.stop()
|
||||
NotificationCenter.default.removeObserver(finishObserver!)
|
||||
}
|
||||
|
||||
guard let results = query.results as? [NSMetadataItem] else {
|
||||
return
|
||||
}
|
||||
|
||||
query.disableUpdates()
|
||||
|
||||
var contents = [FileObject]()
|
||||
for result in results {
|
||||
guard let attribs = result.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
|
||||
continue
|
||||
}
|
||||
|
||||
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardized, url.deletingLastPathComponent().path.trimmingCharacters(in: pathTrimSet) == pathURL.path.trimmingCharacters(in: pathTrimSet) else {
|
||||
continue
|
||||
}
|
||||
|
||||
if let file = self.mapFileObject(attributes: attribs) {
|
||||
contents.append(file)
|
||||
}
|
||||
}
|
||||
|
||||
query.stop()
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(contents, nil)
|
||||
}
|
||||
|
||||
})
|
||||
DispatchQueue.main.async {
|
||||
if !query.start() {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler([], self.throwError(path, code: CocoaError.fileReadNoPermission))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Please don't rely this function to get iCloud drive total and remaining capacity
|
||||
/// - Important: iCloud Storage size and free space is unavailable, it returns local space
|
||||
open override func storageProperties(completionHandler: (@escaping (_ total: Int64, _ used: Int64) -> Void)) {
|
||||
super.storageProperties(completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns a `FileObject` containing the attributes of the item (file, directory, symlink, etc.) at the path in question via asynchronous completion handler.
|
||||
|
||||
If the directory contains no entries or an error is occured, this method will return the empty `FileObject`.
|
||||
|
||||
- Parameter path: path to target directory. If empty, `currentPath` value will be used.
|
||||
- Parameter completionHandler: a closure with result of directory entries or error.
|
||||
`attributes`: A `FileObject` containing the attributes of the item.
|
||||
`error`: Error returned by system.
|
||||
*/
|
||||
open override func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
|
||||
dispatch_queue.async {
|
||||
let pathURL = self.url(of: path)
|
||||
let query = NSMetadataQuery()
|
||||
query.predicate = NSPredicate(format: "%K LIKE %@", NSMetadataItemPathKey, pathURL.path)
|
||||
query.valueListAttributes = [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]
|
||||
query.searchScopes = [self.scope.rawValue]
|
||||
var finishObserver: NSObjectProtocol?
|
||||
finishObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
|
||||
defer {
|
||||
query.stop()
|
||||
NotificationCenter.default.removeObserver(finishObserver!)
|
||||
}
|
||||
|
||||
query.disableUpdates()
|
||||
|
||||
guard let result = (query.results as? [NSMetadataItem])?.first, let attribs = result.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
|
||||
let error = self.throwError(path, code: CocoaError.fileNoSuchFile)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let file = self.mapFileObject(attributes: attribs) {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(file, nil)
|
||||
}
|
||||
} else {
|
||||
let noFileError = self.throwError(path, code: CocoaError.fileNoSuchFile)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, noFileError)
|
||||
}
|
||||
}
|
||||
})
|
||||
DispatchQueue.main.async {
|
||||
if !query.start() {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadNoPermission))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Search files inside directory using query asynchronously.
|
||||
|
||||
- Note: For now only it's limited to file names. `query` parameter may take `NSPredicate` format in near future.
|
||||
|
||||
- Parameters:
|
||||
- path: location of directory to start search
|
||||
- recursive: Searching subdirectories of path
|
||||
- query: Simple string of file name to be search (for now).
|
||||
- foundItemHandler: Closure which is called when a file is found
|
||||
- completionHandler: Closure which will be called after finishing search. Returns an arry of `FileObject` or error if occured.
|
||||
*/
|
||||
open override func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
|
||||
|
||||
let mapDict: [String: String] = ["url": NSMetadataItemURLKey, "name": NSMetadataItemFSNameKey, "path": NSMetadataItemPathKey, "filesize": NSMetadataItemFSSizeKey, "modifiedDate": NSMetadataItemFSContentChangeDateKey, "creationDate": NSMetadataItemFSCreationDateKey, "contentType": NSMetadataItemContentTypeKey]
|
||||
|
||||
func updateQueryKeys(_ queryComponent: NSPredicate) -> NSPredicate {
|
||||
if let cQuery = queryComponent as? NSCompoundPredicate {
|
||||
let newSub = cQuery.subpredicates.map { updateQueryKeys($0 as! NSPredicate) }
|
||||
switch cQuery.compoundPredicateType {
|
||||
case .and: return NSCompoundPredicate(andPredicateWithSubpredicates: newSub)
|
||||
case .not: return NSCompoundPredicate(notPredicateWithSubpredicate: newSub.first!)
|
||||
case .or: return NSCompoundPredicate(orPredicateWithSubpredicates: newSub)
|
||||
}
|
||||
} else if let cQuery = queryComponent as? NSComparisonPredicate {
|
||||
var newLeft = cQuery.leftExpression
|
||||
var newRight = cQuery.rightExpression
|
||||
if newLeft.expressionType == .keyPath, let newKey = mapDict[newLeft.keyPath] {
|
||||
newLeft = NSExpression(forKeyPath: newKey)
|
||||
}
|
||||
if newRight.expressionType == .keyPath, let newKey = mapDict[newRight.keyPath] {
|
||||
newRight = NSExpression(forKeyPath: newKey)
|
||||
}
|
||||
if newLeft.expressionType == .keyPath, newLeft.keyPath == "type" {
|
||||
newRight = NSExpression(forConstantValue: newRight.constantValue as? String == "directory" ? "public.directory": "public.data")
|
||||
}
|
||||
if newRight.expressionType == .keyPath, newRight.keyPath == "type" {
|
||||
newLeft = NSExpression(forConstantValue: newLeft.constantValue as? String == "directory" ? "public.directory": "public.data")
|
||||
}
|
||||
return NSComparisonPredicate(leftExpression: newLeft, rightExpression: newRight, modifier: cQuery.comparisonPredicateModifier, type: cQuery.predicateOperatorType, options: cQuery.options)
|
||||
} else {
|
||||
return queryComponent
|
||||
}
|
||||
}
|
||||
|
||||
dispatch_queue.async {
|
||||
let pathURL = self.url(of: path)
|
||||
let mdquery = NSMetadataQuery()
|
||||
mdquery.predicate = NSPredicate(format: "(%K BEGINSWITH %@) && (\(updateQueryKeys(query).predicateFormat))", NSMetadataItemPathKey, pathURL.path)
|
||||
mdquery.searchScopes = [self.scope.rawValue]
|
||||
|
||||
var lastReportedCount = 0
|
||||
|
||||
if let foundItemHandler = foundItemHandler {
|
||||
var updateObserver: NSObjectProtocol?
|
||||
|
||||
// FIXME: Remove this section as it won't work as expected on iCloud
|
||||
updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryGatheringProgress, object: mdquery, queue: nil, using: { (notification) in
|
||||
|
||||
mdquery.disableUpdates()
|
||||
|
||||
guard mdquery.resultCount > lastReportedCount else { return }
|
||||
|
||||
for index in lastReportedCount..<mdquery.resultCount {
|
||||
guard let attribs = (mdquery.result(at: index) as? NSMetadataItem)?.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
|
||||
continue
|
||||
}
|
||||
|
||||
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardized, recursive || url.deletingLastPathComponent().path.trimmingCharacters(in: pathTrimSet) == pathURL.path.trimmingCharacters(in: pathTrimSet) else {
|
||||
continue
|
||||
}
|
||||
|
||||
if let file = self.mapFileObject(attributes: attribs) {
|
||||
foundItemHandler(file)
|
||||
}
|
||||
}
|
||||
lastReportedCount = mdquery.resultCount
|
||||
|
||||
mdquery.enableUpdates()
|
||||
})
|
||||
}
|
||||
|
||||
var finishObserver: NSObjectProtocol?
|
||||
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: mdquery, queue: nil, using: { (notification) in
|
||||
defer {
|
||||
mdquery.stop()
|
||||
NotificationCenter.default.removeObserver(finishObserver!)
|
||||
}
|
||||
|
||||
guard let results = mdquery.results as? [NSMetadataItem] else {
|
||||
return
|
||||
}
|
||||
|
||||
mdquery.disableUpdates()
|
||||
|
||||
var contents = [FileObject]()
|
||||
for result in results {
|
||||
guard let attribs = result.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
|
||||
continue
|
||||
}
|
||||
|
||||
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardized, recursive || url.deletingLastPathComponent().path.trimmingCharacters(in: pathTrimSet) == pathURL.path.trimmingCharacters(in: pathTrimSet) else {
|
||||
continue
|
||||
}
|
||||
|
||||
if let file = self.mapFileObject(attributes: attribs) {
|
||||
contents.append(file)
|
||||
}
|
||||
}
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(contents, nil)
|
||||
}
|
||||
})
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if !mdquery.start() {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler([], self.throwError(path, code: CocoaError.fileReadNoPermission))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open override func isReachable(completionHandler: @escaping (Bool) -> Void) {
|
||||
dispatch_queue.async {
|
||||
completionHandler(self.fileManager.ubiquityIdentityToken != nil)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a new directory at the specified path asynchronously.
|
||||
This will create any necessary intermediate directories.
|
||||
|
||||
- Parameters:
|
||||
- folder: Directory name.
|
||||
- at: Parent path of new directory.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
open override func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard let r = super.create(folder: folderName, at: atPath, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
/**
|
||||
Creates an new file with data passed to method asynchronously.
|
||||
Returns error via completionHandler if file is already exists.
|
||||
|
||||
- Parameters:
|
||||
- file: New file name with extension separated by period.
|
||||
- at: Parent path of new file.
|
||||
- data: Data of new files. Pass nil or `Data()` to create empty file.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
open override func create(file fileName: String, at atPath: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard let r = super.create(file: fileName, at: atPath, contents: data, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
/**
|
||||
Moves a file or directory from `path` to designated path asynchronously.
|
||||
When you want move a file, destination path should also consists of file name.
|
||||
Either a new name or the old one.
|
||||
|
||||
- Parameters:
|
||||
- path: original file or directory path.
|
||||
- to: destination path of file or directory, including file/directory name.
|
||||
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
open override func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard let r = super.moveItem(path: path, to: toPath, overwrite: overwrite, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
/**
|
||||
Copies a file or directory from `path` to designated path asynchronously.
|
||||
When want copy a file, destination path should also consists of file name.
|
||||
Either a new name or the old one.
|
||||
|
||||
- Parameters:
|
||||
- path: original file or directory path.
|
||||
- to: destination path of file or directory, including file/directory name.
|
||||
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
open override func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard let r = super.copyItem(path: path, to: toPath, overwrite: overwrite, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
/**
|
||||
Removes the file or directory at the specified path.
|
||||
|
||||
- Important: Due to a bug (race condition?) in Apple API, it takes about 3-5 seconds to update containing folder
|
||||
list and triggering notification registered for directory while completion handler will run almost immediately.
|
||||
It's your responsibility to workaourd this bug/feature and mark file as deleted in your software.
|
||||
|
||||
- Parameters:
|
||||
- path: file or directory path.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `CloudFileProvider`.
|
||||
|
||||
*/
|
||||
@discardableResult
|
||||
open override func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard let r = super.removeItem(path: path, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
/**
|
||||
Uploads a file from local file url to designated path asynchronously.
|
||||
Method will fail if source is not a local url with `file://` scheme.
|
||||
|
||||
- Parameters:
|
||||
- localFile: a file url to file.
|
||||
- to: destination path of file, including file/directory name.
|
||||
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress.
|
||||
*/
|
||||
@discardableResult
|
||||
open override func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
// TODO: Make use of overwrite parameter
|
||||
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
|
||||
operation_queue.addOperation {
|
||||
let tempFolder: URL
|
||||
if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) {
|
||||
tempFolder = FileManager.default.temporaryDirectory
|
||||
} else {
|
||||
tempFolder = URL(fileURLWithPath: NSTemporaryDirectory())
|
||||
}
|
||||
let tmpFile = tempFolder.appendingPathComponent(UUID().uuidString)
|
||||
|
||||
do {
|
||||
try self.opFileManager.copyItem(at: localFile, to: tmpFile)
|
||||
let toUrl = self.url(of: toPath)
|
||||
try self.opFileManager.setUbiquitous(true, itemAt: tmpFile, destinationURL: toUrl)
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
})
|
||||
} catch let e {
|
||||
if self.opFileManager.fileExists(atPath: tmpFile.path) {
|
||||
try? self.opFileManager.removeItem(at: tmpFile)
|
||||
}
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
})
|
||||
}
|
||||
}
|
||||
return CloudOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
/**
|
||||
Download a file from `path` to designated local file url asynchronously.
|
||||
Method will fail if destination is not a local url with `file://` scheme.
|
||||
|
||||
- Parameters:
|
||||
- path: original file or directory path.
|
||||
- toLocalURL: destination local url of file, including file/directory name.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress.
|
||||
*/
|
||||
@discardableResult
|
||||
open override func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
|
||||
|
||||
do {
|
||||
try self.opFileManager.startDownloadingUbiquitousItem(at: self.url(of: path))
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let r = super.copyItem(path: path, toLocalURL: toLocalURL, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
/**
|
||||
Retreives a `Data` object with the contents of the file asynchronously vis contents argument of completion handler.
|
||||
If path specifies a directory, or if some other error occurs, data will be nil.
|
||||
|
||||
- Parameters:
|
||||
- path: Path of file.
|
||||
- completionHandler: a closure with result of file contents or error.
|
||||
`contents`: contents of file in a `Data` object.
|
||||
`error`: Error returned by system.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress.
|
||||
*/
|
||||
@discardableResult
|
||||
open override func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
guard let r = super.contents(path: path, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
/**
|
||||
Retreives a `Data` object with a portion contents of the file asynchronously vis contents argument of completion handler.
|
||||
If path specifies a directory, or if some other error occurs, data will be nil.
|
||||
|
||||
- Parameters:
|
||||
- path: Path of file.
|
||||
- offset: First byte index which should be read. **Starts from 0.**
|
||||
- length: Bytes count of data. Pass `-1` to read until the end of file.
|
||||
- completionHandler: a closure with result of file contents or error.
|
||||
`contents`: contents of file in a `Data` object.
|
||||
`error`: Error returned by system.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress.
|
||||
*/
|
||||
@discardableResult
|
||||
open override func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
guard let r = super.contents(path: path, offset: offset, length: length, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
/**
|
||||
Write the contents of the `Data` to a location asynchronously.
|
||||
|
||||
- Parameters:
|
||||
- path: Path of target file.
|
||||
- contents: Data to be written into file.
|
||||
- overwrite: Destination file should be overwritten if file is already exists. Default is `false`.
|
||||
- atomically: data will be written to a temporary file before writing to final location. Default is `false`.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
open override func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard let r = super.writeContents(path: path, contents: data, atomically: atomically, overwrite: overwrite, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
fileprivate var monitors = [String: (NSMetadataQuery, NSObjectProtocol)]()
|
||||
|
||||
/**
|
||||
Starts monitoring a path and its subpaths, including files and folders, for any change,
|
||||
including copy, move/rename, content changes, etc.
|
||||
To avoid thread congestion, `evetHandler` will be triggered with 0.2 seconds interval,
|
||||
and has a 0.25 second delay, to ensure it's called after updates.
|
||||
|
||||
- Note: this functionality is available only in `LocalFileProvider` and `CloudFileProvider`.
|
||||
- Note: `eventHandler` is not called on main thread, for updating UI. dispatch routine to main thread.
|
||||
- Important: `eventHandler` may be called if file is changed in recursive subpaths of registered path.
|
||||
This may cause negative impact on performance if a root path is being monitored.
|
||||
|
||||
- Parameters:
|
||||
- path: path of directory.
|
||||
- eventHandler: Closure executed after change, on a secondary thread.
|
||||
*/
|
||||
open override func registerNotifcation(path: String, eventHandler: @escaping (() -> Void)) {
|
||||
self.unregisterNotifcation(path: path)
|
||||
let pathURL = self.url(of: path)
|
||||
let query = NSMetadataQuery()
|
||||
query.predicate = NSPredicate(format: "(%K BEGINSWITH %@)", NSMetadataItemPathKey, pathURL.path)
|
||||
query.valueListAttributes = []
|
||||
query.searchScopes = [self.scope.rawValue]
|
||||
|
||||
let updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidUpdate, object: query, queue: nil, using: { (notification) in
|
||||
|
||||
query.disableUpdates()
|
||||
|
||||
eventHandler()
|
||||
|
||||
query.enableUpdates()
|
||||
})
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if query.start() {
|
||||
self.monitors[path] = (query, updateObserver)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stops monitoring the path.
|
||||
///
|
||||
/// - Parameter path: path of directory.
|
||||
open override func unregisterNotifcation(path: String) {
|
||||
guard let (query, observer) = monitors[path] else {
|
||||
return
|
||||
}
|
||||
query.disableUpdates()
|
||||
query.stop()
|
||||
NotificationCenter.default.removeObserver(observer)
|
||||
monitors.removeValue(forKey: path)
|
||||
}
|
||||
|
||||
/// Investigate either the path is registered for change notification or not.
|
||||
///
|
||||
/// - Parameter path: path of directory.
|
||||
/// - Returns: Directory is being monitored or not.
|
||||
open override func isRegisteredForNotification(path: String) -> Bool {
|
||||
return monitors[path] != nil
|
||||
}
|
||||
|
||||
open override func copy(with zone: NSZone? = nil) -> Any {
|
||||
let copy = CloudFileProvider(containerId: self.containerId)
|
||||
copy?.currentPath = self.currentPath
|
||||
copy?.delegate = self.delegate
|
||||
copy?.fileOperationDelegate = self.fileOperationDelegate
|
||||
return copy as Any
|
||||
}
|
||||
|
||||
fileprivate func mapFileObject(attributes attribs: [String: Any]) -> FileObject? {
|
||||
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardizedFileURL, let name = attribs[NSMetadataItemFSNameKey] as? String else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let path = self.relativePathOf(url: url)
|
||||
let rpath = path.hasPrefix("/") ? path.substring(from: path.index(after: path.startIndex)) : path
|
||||
let relativeUrl = URL(string: rpath, relativeTo: self.baseURL)
|
||||
let file = FileObject(url: relativeUrl ?? url, name: name, path: path)
|
||||
|
||||
file.size = (attribs[NSMetadataItemFSSizeKey] as? NSNumber)?.int64Value ?? -1
|
||||
file.creationDate = attribs[NSMetadataItemFSCreationDateKey] as? Date
|
||||
file.modifiedDate = attribs[NSMetadataItemFSContentChangeDateKey] as? Date
|
||||
let isFolder = (attribs[NSMetadataItemContentTypeTreeKey] as? [String])?.contains("public.folder") ?? false
|
||||
let isSymbolic = (attribs[NSMetadataItemContentTypeTreeKey] as? [String])?.contains("public.symlink") ?? false
|
||||
file.type = isFolder ? .directory : (isSymbolic ? .symbolicLink : .regular)
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
/// Removes local copy of file, but spares cloud copy/
|
||||
/// - Parameter path: Path of file or directory to be remoed from local
|
||||
/// - Parameter completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
open func evictItem(path: String, completionHandler: SimpleCompletionHandler) {
|
||||
operation_queue.addOperation {
|
||||
do {
|
||||
try self.opFileManager.evictUbiquitousItem(at: self.url(of: path))
|
||||
completionHandler?(nil)
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Genrates a public url to a file to be shared with other users and can be downloaded without authentication.
|
||||
|
||||
- Important: URL will be available for a limitied time, determined in `expiration` argument.
|
||||
|
||||
- Parameters:
|
||||
- to: path of file, including file/directory name.
|
||||
- completionHandler: a closure with result of directory entries or error.
|
||||
`link`: a url returned by Dropbox to share.
|
||||
`attribute`: a `FileObject` containing the attributes of the item.
|
||||
`expiration`: a `Date` object, determines when the public url will expires.
|
||||
`error`: Error returned by Dropbox.
|
||||
*/
|
||||
open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
|
||||
operation_queue.addOperation {
|
||||
do {
|
||||
var expiration: NSDate?
|
||||
let url = try self.opFileManager.url(forPublishingUbiquitousItemAt: self.url(of: path), expiration: &expiration)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(url, nil, expiration as Date?, nil)
|
||||
}
|
||||
} catch let e {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, nil, nil, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum UbiquitousScope: RawRepresentable {
|
||||
/// Search all files not in the Documents directories of the app’s iCloud container directories.
|
||||
/// Use this scope to store user-related data files that your app needs to share
|
||||
/// but that are not files you want the user to manipulate directly.
|
||||
case data
|
||||
/// Search all files in the Documents directories of the app’s iCloud container directories.
|
||||
/// Put documents that the user is allowed to access inside a Documents subdirectory.
|
||||
case documents
|
||||
|
||||
public typealias RawValue = String
|
||||
|
||||
public init? (rawValue: String) {
|
||||
switch rawValue {
|
||||
case NSMetadataQueryUbiquitousDataScope:
|
||||
self = .data
|
||||
case NSMetadataQueryUbiquitousDocumentsScope:
|
||||
self = .documents
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public var rawValue: String {
|
||||
switch self {
|
||||
case .data:
|
||||
return NSMetadataQueryUbiquitousDataScope
|
||||
case .documents:
|
||||
return NSMetadataQueryUbiquitousDocumentsScope
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class CloudOperationHandle: OperationHandle {
|
||||
public let baseURL: URL?
|
||||
public let operationType: FileOperationType
|
||||
|
||||
init (operationType: FileOperationType, baseURL: URL?) {
|
||||
self.baseURL = baseURL
|
||||
self.operationType = operationType
|
||||
}
|
||||
|
||||
private var sourceURL: URL? {
|
||||
guard let source = operationType.source, let baseURL = baseURL else { return nil }
|
||||
return source.hasPrefix("file://") ? URL(fileURLWithPath: source) : baseURL.appendingPathComponent(source)
|
||||
}
|
||||
|
||||
private var destURL: URL? {
|
||||
guard let dest = operationType.destination, let baseURL = baseURL else { return nil }
|
||||
return dest.hasPrefix("file://") ? URL(fileURLWithPath: dest) : baseURL.appendingPathComponent(dest)
|
||||
}
|
||||
|
||||
open var bytesSoFar: Int64 {
|
||||
assert(!Thread.isMainThread, "Don't run \(#function) method on main thread")
|
||||
|
||||
guard let url = destURL ?? sourceURL, let item = CloudOperationHandle.getMetadataItem(url: url) else { return 0 }
|
||||
let downloaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentDownloadedKey) as? Double ?? 0
|
||||
let uploaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentUploadedKey) as? Double ?? 0
|
||||
guard let size = item.value(forAttribute: NSMetadataItemFSSizeKey) as? Int64 else { return -1 }
|
||||
if (downloaded == 0 || downloaded == 100) && (uploaded > 0 && uploaded < 100) {
|
||||
return Int64(uploaded * (Double(size) / 100))
|
||||
} else if (uploaded == 0 || uploaded == 100) && (downloaded > 0 && downloaded < 100) {
|
||||
return Int64(downloaded * (Double(size) / 100))
|
||||
} else if uploaded == 100 || downloaded == 100 {
|
||||
return size
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
open var totalBytes: Int64 {
|
||||
assert(!Thread.isMainThread, "Don't run \(#function) method on main thread")
|
||||
guard let url = destURL ?? sourceURL, let item = CloudOperationHandle.getMetadataItem(url: url) else { return -1 }
|
||||
return item.value(forAttribute: NSMetadataItemFSSizeKey) as? Int64 ?? -1
|
||||
}
|
||||
|
||||
open var inProgress: Bool {
|
||||
guard let url = destURL ?? sourceURL, let item = CloudOperationHandle.getMetadataItem(url: url) else { return false }
|
||||
let downloadStatus = item.value(forAttribute: NSMetadataUbiquitousItemDownloadingStatusKey) as? String ?? NSMetadataUbiquitousItemDownloadingStatusNotDownloaded
|
||||
let isUploading = item.value(forAttribute: NSMetadataUbiquitousItemIsUploadingKey) as? Bool ?? false
|
||||
return downloadStatus == NSMetadataUbiquitousItemDownloadingStatusCurrent || isUploading
|
||||
}
|
||||
|
||||
/// Not usable in local provider
|
||||
open func cancel() -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
fileprivate static func getMetadataItem(url: URL) -> NSMetadataItem? {
|
||||
let query = NSMetadataQuery()
|
||||
query.predicate = NSPredicate(format: "(%K LIKE %@)", NSMetadataItemPathKey, url.path)
|
||||
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope, NSMetadataQueryUbiquitousDataScope]
|
||||
|
||||
var item: NSMetadataItem?
|
||||
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
var finishObserver: NSObjectProtocol?
|
||||
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
|
||||
defer {
|
||||
query.stop()
|
||||
group.leave()
|
||||
NotificationCenter.default.removeObserver(finishObserver!)
|
||||
}
|
||||
|
||||
if query.resultCount > 0 {
|
||||
item = query.result(at: 0) as? NSMetadataItem
|
||||
}
|
||||
|
||||
query.disableUpdates()
|
||||
|
||||
})
|
||||
|
||||
DispatchQueue.main.async {
|
||||
query.start()
|
||||
}
|
||||
_ = group.wait(timeout: DispatchTime.now() + 30)
|
||||
return item
|
||||
}
|
||||
}
|
||||
+387
-146
@@ -10,43 +10,91 @@
|
||||
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 DropboxFileProvider: NSObject, FileProviderBasic {
|
||||
open static let type: String = "WebDAV"
|
||||
open let isPathRelative: Bool = true
|
||||
/**
|
||||
Allows accessing to Dropbox stored files. This provider doesn't cache or save files internally, however you can
|
||||
set `useCache` and `cache` properties to use Foundation `NSURLCache` system.
|
||||
|
||||
- Note: Uploading files and data are limited to 150MB, for now.
|
||||
*/
|
||||
open class DropboxFileProvider: FileProviderBasicRemote {
|
||||
open class var type: String { return "DropBox" }
|
||||
open let baseURL: URL?
|
||||
open var currentPath: String = ""
|
||||
open var dispatch_queue: DispatchQueue {
|
||||
open var currentPath: String
|
||||
|
||||
/// 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?
|
||||
internal var session: URLSession {
|
||||
public var session: URLSession {
|
||||
if _session == nil {
|
||||
self.sessionDelegate = SessionDelegate(fileProvider: self, credential: credential)
|
||||
let queue = OperationQueue()
|
||||
//queue.underlyingQueue = dispatch_queue
|
||||
_session = URLSession(configuration: URLSessionConfiguration.default, delegate: sessionDelegate as URLSessionDelegate?, delegateQueue: queue)
|
||||
let config = URLSessionConfiguration.default
|
||||
config.urlCache = cache
|
||||
config.requestCachePolicy = .returnCacheDataElseLoad
|
||||
_session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDelegate?, delegateQueue: self.operation_queue)
|
||||
}
|
||||
return _session!
|
||||
}
|
||||
|
||||
public init? (credential: URLCredential?) {
|
||||
fileprivate var _longpollSession: URLSession?
|
||||
internal var longpollSession: URLSession {
|
||||
if _longpollSession == nil {
|
||||
let config = URLSessionConfiguration.default
|
||||
config.timeoutIntervalForRequest = 600
|
||||
_longpollSession = URLSession(configuration: config, delegate: nil, delegateQueue: nil)
|
||||
}
|
||||
return _longpollSession!
|
||||
}
|
||||
|
||||
/**
|
||||
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.
|
||||
|
||||
- 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.
|
||||
*/
|
||||
public init(credential: URLCredential?, cache: URLCache? = nil) {
|
||||
self.baseURL = nil
|
||||
dispatch_queue = DispatchQueue(label: "FileProvider.\(DropboxFileProvider.type)", attributes: DispatchQueue.Attributes.concurrent)
|
||||
//let url = baseURL.uw_absoluteString
|
||||
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"
|
||||
|
||||
}
|
||||
|
||||
deinit {
|
||||
_session?.invalidateAndCancel()
|
||||
if fileProviderCancelTasksOnInvalidating {
|
||||
_session?.invalidateAndCancel()
|
||||
} else {
|
||||
_session?.finishTasksAndInvalidate()
|
||||
}
|
||||
}
|
||||
|
||||
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
|
||||
@@ -56,73 +104,101 @@ open class DropboxFileProvider: NSObject, FileProviderBasic {
|
||||
}
|
||||
|
||||
open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
|
||||
let url = URL(string: "https://api.dropboxapi.com/2/files/get_metadata")!
|
||||
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")
|
||||
let requestDictionary = ["path": correctPath(path)! as NSString]
|
||||
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString]
|
||||
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderDropboxError?
|
||||
var fileObject: DropboxFileObject?
|
||||
if let response = response as? HTTPURLResponse {
|
||||
defer {
|
||||
self.delegateNotify(.create(path: path), error: error)
|
||||
}
|
||||
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
|
||||
let dbError: FileProviderDropboxError? = 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 = self.mapToFileObject(json) {
|
||||
completionHandler(file, dbError)
|
||||
return
|
||||
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) {
|
||||
fileObject = file
|
||||
}
|
||||
completionHandler(nil, dbError)
|
||||
return
|
||||
}
|
||||
completionHandler(nil, error)
|
||||
completionHandler(fileObject, serverError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
|
||||
open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) {
|
||||
let url = URL(string: "https://api.dropboxapi.com/2/users/get_space_usage")!
|
||||
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")
|
||||
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) {
|
||||
let totalSize = ((json["allocation"] as? NSDictionary)?["allocated"] as? NSNumber)?.int64Value ?? -1
|
||||
let usedSize = (json["used"] as? NSNumber)?.int64Value ?? 0
|
||||
completionHandler(totalSize, usedSize)
|
||||
return
|
||||
totalSize = ((json["allocation"] as? NSDictionary)?["allocated"] as? NSNumber)?.int64Value ?? -1
|
||||
usedSize = (json["used"] as? NSNumber)?.int64Value ?? 0
|
||||
}
|
||||
completionHandler(-1, 0)
|
||||
completionHandler(totalSize, usedSize)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
|
||||
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
|
||||
var foundFiles = [DropboxFileObject]()
|
||||
if let queryStr = query.findValue(forKey: "name", operator: .beginsWith) as? String {
|
||||
// Dropbox only support searching for file names begin with query in non-enterprise accounts.
|
||||
// We will use it if there is a `name BEGINSWITH[c] "query"` in predicate, then filter to form final result.
|
||||
search(path, query: queryStr, foundItem: { (file) in
|
||||
if query.evaluate(with: file.mapPredicate()) {
|
||||
foundFiles.append(file)
|
||||
foundItemHandler?(file)
|
||||
}
|
||||
}, completionHandler: { (error) in
|
||||
completionHandler(foundFiles, error)
|
||||
})
|
||||
} else {
|
||||
// Dropbox doesn't support searching attributes natively. The workaround is to fallback to listing all files
|
||||
// and filter it locally. It may have a network burden in case there is many files in Dropbox, so please use it concisely.
|
||||
list(path, recursive: true, progressHandler: { (files, _, error) in
|
||||
for file in files where query.evaluate(with: file.mapPredicate()) {
|
||||
foundItemHandler?(file)
|
||||
}
|
||||
}, completionHandler: { (files, _, error) in
|
||||
let predicatedFiles = files.filter { query.evaluate(with: $0.mapPredicate()) }
|
||||
completionHandler(predicatedFiles, error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
|
||||
self.storageProperties { total, _ in
|
||||
completionHandler(total > 0)
|
||||
}
|
||||
}
|
||||
|
||||
open weak var fileOperationDelegate: FileOperationDelegate?
|
||||
}
|
||||
|
||||
extension DropboxFileProvider: FileProviderOperations {
|
||||
|
||||
|
||||
public func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
open 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 fileAttribs: FileObject, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
return self.writeContents(path: path, contents: data ?? Data(), completionHandler: completionHandler)
|
||||
open 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 = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
open 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 = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
open 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? {
|
||||
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
return doOperation(.remove(path: path), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@@ -131,83 +207,71 @@ extension DropboxFileProvider: FileProviderOperations {
|
||||
return nil
|
||||
}
|
||||
let url: String
|
||||
var path: String?, fromPath: String?, toPath: String?
|
||||
guard let sourcePath = operation.source else { return nil }
|
||||
let destPath = operation.destination
|
||||
switch operation {
|
||||
case .create(path: let p):
|
||||
url = "https://api.dropboxapi.com/2/files/create_folder"
|
||||
path = p
|
||||
case .copy(source: let fp, destination: let tp):
|
||||
url = "https://api.dropboxapi.com/2/files/copy"
|
||||
fromPath = fp
|
||||
toPath = tp
|
||||
case .move(source: let fp, destination: let tp):
|
||||
url = "https://api.dropboxapi.com/2/files/move"
|
||||
fromPath = fp
|
||||
toPath = tp
|
||||
case .modify(path: let p):
|
||||
return nil
|
||||
case .remove(path: let p):
|
||||
url = "https://api.dropboxapi.com/2/files/delete"
|
||||
path = p
|
||||
case .link(link: _, target: _):
|
||||
case .create:
|
||||
url = "files/create_folder"
|
||||
case .copy:
|
||||
url = "files/copy"
|
||||
case .move:
|
||||
url = "files/move"
|
||||
case .remove:
|
||||
url = "files/delete"
|
||||
default: // modify, link, fetch
|
||||
return nil
|
||||
}
|
||||
var request = URLRequest(url: URL(string: url)!)
|
||||
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]()
|
||||
requestDictionary["path"] = correctPath(path) as NSString?
|
||||
requestDictionary["from_path"] = correctPath(fromPath) as NSString?
|
||||
requestDictionary["to_path"] = correctPath(toPath) as NSString?
|
||||
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
|
||||
if let response = response as? HTTPURLResponse {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
|
||||
let dbError: FileProviderDropboxError? = code != nil ? FileProviderDropboxError(code: code!, path: path ?? fromPath ?? "", errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
|
||||
defer {
|
||||
self.delegateNotify(operation, error: error ?? dbError)
|
||||
}
|
||||
/*if let data = data, let jsonStr = String(data: data, encoding: NSUTF8StringEncoding) {
|
||||
let json = self.jsonToDictionary(jsonStr)
|
||||
}*/
|
||||
completionHandler?(dbError)
|
||||
return
|
||||
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?(error)
|
||||
})
|
||||
completionHandler?(serverError ?? error)
|
||||
self.delegateNotify(operation, error: serverError ?? error)
|
||||
})
|
||||
task.taskDescription = operation.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(tasks: [task])
|
||||
return RemoteOperationHandle(operationType: operation, tasks: [task])
|
||||
}
|
||||
|
||||
public func copyItem(localFile: URL, to toPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: .copy(source: localFile.absoluteString, destination: toPath)) ?? true == true else {
|
||||
open 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
|
||||
}
|
||||
guard let data = try? Data(contentsOf: localFile) else {
|
||||
let error = throwError(localFile.absoluteString, code: URLError.fileDoesNotExist as FoundationErrorEnum)
|
||||
completionHandler?(error)
|
||||
return nil
|
||||
}
|
||||
return upload_simple(toPath, data: data, overwrite: true, operation: .copy(source: localFile.absoluteString, destination: toPath), completionHandler: completionHandler)
|
||||
return upload_simple(toPath, localFile: localFile, overwrite: overwrite, operation: opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: .copy(source: path, destination: destURL.absoluteString)) ?? true == true else {
|
||||
open 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: "https://content.dropboxapi.com/2/files/download")!
|
||||
let url = URL(string: "files/download", relativeTo: contentURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
let requestDictionary = ["path": path as NSString]
|
||||
request.setValue(dictionaryToJSON(requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
|
||||
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 dbError: FileProviderDropboxError? = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: nil) : nil
|
||||
completionHandler?(dbError ?? error)
|
||||
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 {
|
||||
@@ -217,60 +281,56 @@ extension DropboxFileProvider: FileProviderOperations {
|
||||
completionHandler?(e)
|
||||
}
|
||||
})
|
||||
task.taskDescription = dictionaryToJSON(["type": "Copy" as NSString, "source": path as NSString, "dest": destURL.absoluteString as NSString])
|
||||
task.taskDescription = opType.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(tasks: [task])
|
||||
return RemoteOperationHandle(operationType: opType, tasks: [task])
|
||||
}
|
||||
}
|
||||
|
||||
extension DropboxFileProvider: FileProviderReadWrite {
|
||||
public func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
return self.contents(path: path, offset: 0, length: -1, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
let url = URL(string: "https://content.dropboxapi.com/2/files/download")!
|
||||
open 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)", forHTTPHeaderField: "Range")
|
||||
request.setValue("bytes=\(offset)-\(offset + length - 1)", forHTTPHeaderField: "Range")
|
||||
} else if offset > 0 && length < 0 {
|
||||
request.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
|
||||
}
|
||||
let requestDictionary = ["path": path]
|
||||
request.setValue(dictionaryToJSON(requestDictionary as [String : AnyObject]), forHTTPHeaderField: "Dropbox-API-Arg")
|
||||
let task = session.dataTask(with: request, completionHandler: { (datam, response, error) in
|
||||
guard let data = datam, let httpResponse = response as? HTTPURLResponse , httpResponse.statusCode < 300 else {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: (response as? HTTPURLResponse)?.statusCode ?? -1)
|
||||
let dbError: FileProviderDropboxError? = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: datam ?? Data(), encoding: .utf8)) : nil
|
||||
completionHandler(nil, dbError ?? error)
|
||||
return
|
||||
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))
|
||||
}
|
||||
completionHandler(data, error)
|
||||
let filedata = serverError ?? error == nil ? data : nil
|
||||
completionHandler(filedata, serverError ?? error)
|
||||
})
|
||||
task.taskDescription = opType.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(tasks: [task])
|
||||
return RemoteOperationHandle(operationType: opType, tasks: [task])
|
||||
}
|
||||
|
||||
public func writeContents(path: String, contents data: Data, atomically: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: .modify(path: path)) ?? true == true else {
|
||||
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: true, operation: .modify(path: path), 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)
|
||||
})
|
||||
return upload_simple(path, data: data, overwrite: overwrite, operation: opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
@@ -283,51 +343,195 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Implement /copy_reference, /get_temporary_link, /save_url, /get_account & /get_current_account
|
||||
/// *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 closure with result of directory entries or error.
|
||||
`link`: a url returned by Dropbox to share.
|
||||
`attribute`: a `FileObject` containing the attributes of the item.
|
||||
`expiration`: a `Date` object, determines when the public url will expires.
|
||||
`error`: Error returned by Dropbox.
|
||||
*/
|
||||
open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: DropboxFileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
|
||||
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")
|
||||
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString]
|
||||
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderDropboxError?
|
||||
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) {
|
||||
if let linkStr = json["link"] as? String {
|
||||
link = URL(string: linkStr)
|
||||
}
|
||||
if let attribDic = json["metadata"] as? [String: AnyObject] {
|
||||
fileObject = DropboxFileObject(json: attribDic)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let expiration: Date? = link != nil ? Date(timeIntervalSinceNow: 4 * 60 * 60) : nil
|
||||
completionHandler(link, fileObject, expiration, serverError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
|
||||
/**
|
||||
Downloads a file from remote url to designated path asynchronously.
|
||||
|
||||
- Parameters:
|
||||
- remoteURL: a valid remote url to file.
|
||||
- to: Destination path of file, including file/directory name.
|
||||
- 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))
|
||||
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")
|
||||
let requestDictionary: [String: AnyObject] = ["path": correctPath(toPath)! as NSString, "url" : remoteURL.absoluteString as NSString]
|
||||
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderDropboxError?
|
||||
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) {
|
||||
jobId = json["async_job_id"] as? String
|
||||
if let attribDic = json["metadata"] as? [String: AnyObject] {
|
||||
fileObject = DropboxFileObject(json: attribDic)
|
||||
}
|
||||
}
|
||||
}
|
||||
completionHandler(jobId, fileObject, serverError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
|
||||
/**
|
||||
Copys a file from another user Dropbox storage to designated path asynchronously.
|
||||
|
||||
- Parameters:
|
||||
- reference: a valid reference string from another user via `copy_reference/get` REST method.
|
||||
- to: Destination path of file, including file/directory name.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
*/
|
||||
open func copyItem(reference: String, to toPath: String, completionHandler: SimpleCompletionHandler) {
|
||||
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")
|
||||
let requestDictionary: [String: AnyObject] = ["path": correctPath(toPath)! as NSString, "copy_reference" : reference 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 {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
|
||||
serverError = code != nil ? FileProviderDropboxError(code: code!, path: toPath, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
|
||||
}
|
||||
completionHandler?(serverError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
/*case "doc", "docx", "docm", "xls", "xlsx", "xlsm":
|
||||
case "doc", "docx", "docm", "xls", "xlsx", "xlsm":
|
||||
return true
|
||||
case "ppt", "pps", "ppsx", "ppsm", "pptx", "pptm":
|
||||
return true
|
||||
case "rtf":
|
||||
return true*/
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public func propertiesOfFileSupported(path: String) -> Bool {
|
||||
return false
|
||||
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":
|
||||
return true*/
|
||||
case "mp4", "mpg", "3gp", "mov", "avi":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public func thumbnailOfFile(path: String, dimension: CGSize, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) {
|
||||
|
||||
/// Default value for dimension is 64x64, according to Dropbox documentation
|
||||
open func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) {
|
||||
let url: URL
|
||||
switch (path as NSString).pathExtension.lowercased() {
|
||||
case "jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff":
|
||||
url = URL(string: "https://content.dropboxapi.com/2/files/get_thumbnail")!
|
||||
/*case "doc", "docx", "docm", "xls", "xlsx", "xlsm":
|
||||
url = URL(string: "files/get_thumbnail", relativeTo: contentURL)!
|
||||
case "doc", "docx", "docm", "xls", "xlsx", "xlsm":
|
||||
fallthrough
|
||||
case "ppt", "pps", "ppsx", "ppsm", "pptx", "pptm":
|
||||
fallthrough
|
||||
case "rtf":
|
||||
url = NSURL(string: "https://content.dropboxapi.com/2/files/get_preview")!*/
|
||||
url = URL(string: "files/get_preview", relativeTo: contentURL)!
|
||||
default:
|
||||
return
|
||||
}
|
||||
var request = URLRequest(url: url)
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
var requestDictionary = ["path": path as NSString]
|
||||
var requestDictionary: [String: AnyObject] = ["path": path as NSString]
|
||||
requestDictionary["format"] = "jpeg" as NSString
|
||||
requestDictionary["size"] = "w\(Int(dimension.width))h\(Int(dimension.height))" as NSString
|
||||
if let dimension = dimension {
|
||||
requestDictionary["size"] = "w\(Int(dimension.width))h\(Int(dimension.height))" as NSString
|
||||
}
|
||||
request.setValue(dictionaryToJSON(requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
|
||||
self.session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
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 jsonResult["error"] != nil {
|
||||
@@ -335,15 +539,52 @@ extension DropboxFileProvider: ExtendedFileProvider {
|
||||
}
|
||||
}
|
||||
if let data = data {
|
||||
image = ImageClass(data: data)
|
||||
if DropboxFileProvider.dataIsPDF(data) {
|
||||
image = DropboxFileProvider.convertToImage(pdfData: data)
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
completionHandler(image, error)
|
||||
})
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
|
||||
public func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) {
|
||||
NotImplemented()
|
||||
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")
|
||||
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString, "include_media_info": NSNumber(value: true)]
|
||||
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderDropboxError?
|
||||
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] {
|
||||
(dic, keys) = self.mapMediaInfo(properties)
|
||||
}
|
||||
}
|
||||
completionHandler(dic, keys, serverError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
||||
extension DropboxFileProvider: FileProvider {}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
+145
-72
@@ -2,47 +2,87 @@
|
||||
// DropboxHelper.swift
|
||||
// FileProvider
|
||||
//
|
||||
// Created by Amir Abbas Mousavian on 5/18/95.
|
||||
//
|
||||
// Created by Amir Abbas Mousavian.
|
||||
// Copyright © 2016 Mousavian. Distributed under MIT license.
|
||||
//
|
||||
|
||||
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 {
|
||||
public let serverTime: Date?
|
||||
public let id: String?
|
||||
public let rev: String?
|
||||
|
||||
// codebeat:disable[ARITY]
|
||||
public init(name: String, path: String, size: Int64 = -1, serverTime: Date? = nil, modifiedDate: Date? = nil, fileType: FileType = .regular, isHidden: Bool = false, isReadOnly: Bool = false, id: String? = nil, rev: String? = nil) {
|
||||
self.serverTime = serverTime
|
||||
self.id = id
|
||||
self.rev = rev
|
||||
super.init(absoluteURL: URL(string: path), name: name, path: path, size: size, createdDate: nil, modifiedDate: modifiedDate, fileType: fileType, isHidden: isHidden, isReadOnly: isReadOnly)
|
||||
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 }
|
||||
self.init(json: json)
|
||||
}
|
||||
|
||||
internal convenience init? (json: [String: AnyObject]) {
|
||||
guard let name = json["name"] as? String else { return nil }
|
||||
guard let path = json["path_display"] as? String else { return nil }
|
||||
self.init(name: name, path: path)
|
||||
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.type = (json[".tag"] as? String) == "folder" ? .directory : .regular
|
||||
self.isReadOnly = (json["sharing_info"]?["read_only"] as? NSNumber)?.boolValue ?? false
|
||||
self.id = json["id"] as? String
|
||||
self.rev = json["rev"] as? String
|
||||
}
|
||||
|
||||
/// The time contents of file has been modified on server, returns nil if not set
|
||||
open internal(set) var serverTime: Date? {
|
||||
get {
|
||||
return allValues[.serverDate] as? Date
|
||||
}
|
||||
set {
|
||||
allValues[.serverDate] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// The document identifier is a value assigned by the Dropbox to a file.
|
||||
/// This value is used to identify the document regardless of where it is moved on a volume.
|
||||
/// The identifier persists across system restarts.
|
||||
open internal(set) var id: String? {
|
||||
get {
|
||||
return allValues[.documentIdentifierKey] as? String
|
||||
}
|
||||
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 internal(set) var rev: String? {
|
||||
get {
|
||||
return allValues[.generationIdentifierKey] as? String
|
||||
}
|
||||
set {
|
||||
allValues[.generationIdentifierKey] = newValue
|
||||
}
|
||||
}
|
||||
// codebeat:enable[ARITY]
|
||||
}
|
||||
|
||||
// codebeat:disable[ARITY]
|
||||
internal extension DropboxFileProvider {
|
||||
func list(_ path: String, cursor: String? = nil, prevContents: [DropboxFileObject] = [], recursive: Bool = false, completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) {
|
||||
func list(_ path: String, cursor: String? = nil, prevContents: [DropboxFileObject] = [], recursive: Bool = false, session: URLSession? = nil, progressHandler: ((_ contents: [FileObject], _ nextCursor: String?, _ error: Error?) -> Void)? = nil, completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) {
|
||||
var requestDictionary = [String: AnyObject]()
|
||||
let url: URL
|
||||
if let cursor = cursor {
|
||||
url = URL(string: "https://api.dropboxapi.com/2/files/list_folder/continue")!
|
||||
url = URL(string: "files/list_folder/continue", relativeTo: apiURL)!
|
||||
requestDictionary["cursor"] = cursor as NSString?
|
||||
} else {
|
||||
url = URL(string: "https://api.dropboxapi.com/2/files/list_folder")!
|
||||
url = URL(string: "files/list_folder", relativeTo: apiURL)!
|
||||
requestDictionary["path"] = correctPath(path) as NSString?
|
||||
requestDictionary["recursive"] = recursive as NSNumber?
|
||||
}
|
||||
@@ -51,45 +91,50 @@ internal extension DropboxFileProvider {
|
||||
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
|
||||
let task = (session ?? self.session).dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var responseError: FileProviderDropboxError?
|
||||
var files = [DropboxFileObject]()
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderDropboxError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
}
|
||||
if let data = data, let jsonStr = String(data: data, encoding: .utf8) {
|
||||
let json = jsonToDictionary(jsonStr)
|
||||
if let entries = json?["entries"] as? [AnyObject] , entries.count > 0 {
|
||||
var files = prevContents
|
||||
files.reserveCapacity(entries.count)
|
||||
for entry in entries {
|
||||
if let entry = entry as? [String: AnyObject], let file = self.mapToFileObject(entry) {
|
||||
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)
|
||||
} else {
|
||||
completionHandler(files, ncursor, responseError ?? error)
|
||||
progressHandler?(files, ncursor, responseError ?? error)
|
||||
self.list(path, cursor: ncursor, prevContents: prevContents + files, completionHandler: completionHandler)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
completionHandler([], nil, responseError ?? error)
|
||||
})
|
||||
progressHandler?(files, nil, responseError ?? error)
|
||||
completionHandler(prevContents + 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? {
|
||||
assert(data.count < 150*1024*1024, "Maximum size of allowed size to upload is 150MB")
|
||||
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: "https://content.dropboxapi.com/2/files/upload")!
|
||||
url = URL(string: "files/upload", relativeTo: contentURL)!
|
||||
requestDictionary["path"] = correctPath(targetPath) as NSString?
|
||||
requestDictionary["mode"] = (overwrite ? "overwrite" : "add") as NSString
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ssz"
|
||||
requestDictionary["client_modified"] = dateFormatter.string(from: modifiedDate) 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")
|
||||
@@ -101,33 +146,48 @@ internal extension DropboxFileProvider {
|
||||
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))
|
||||
}
|
||||
defer {
|
||||
self.delegateNotify(.create(path: targetPath), error: responseError ?? error)
|
||||
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)
|
||||
})
|
||||
var dic: [String: AnyObject] = ["type": operation.description as NSString]
|
||||
switch operation {
|
||||
case .create(path: let s):
|
||||
dic["source"] = s as NSString
|
||||
case .copy(source: let s, destination: let d):
|
||||
dic["source"] = s as NSString
|
||||
dic["dest"] = d as NSString
|
||||
case .modify(path: let s):
|
||||
dic["source"] = s as NSString
|
||||
case .move(source: let s, destination: let d):
|
||||
dic["source"] = s as NSString
|
||||
dic["dest"] = d as NSString
|
||||
default:
|
||||
break
|
||||
}
|
||||
task.taskDescription = dictionaryToJSON(dic)
|
||||
self.delegateNotify(.create(path: targetPath), error: responseError ?? error)
|
||||
})
|
||||
task.taskDescription = operation.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(tasks: [task])
|
||||
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: "https://api.dropboxapi.com/2/files/search")!
|
||||
let url = URL(string: "files/search", relativeTo: apiURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
@@ -146,7 +206,7 @@ internal extension DropboxFileProvider {
|
||||
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 = self.mapToFileObject(entry) {
|
||||
if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry) {
|
||||
foundItem(file)
|
||||
}
|
||||
}
|
||||
@@ -168,22 +228,35 @@ internal extension DropboxFileProvider {
|
||||
// codebeat:enable[ARITY]
|
||||
|
||||
internal extension DropboxFileProvider {
|
||||
func mapToFileObject(_ jsonStr: String) -> DropboxFileObject? {
|
||||
guard let json = jsonToDictionary(jsonStr) else { return nil }
|
||||
return self.mapToFileObject(json)
|
||||
}
|
||||
static let dateFormatter = DateFormatter()
|
||||
static let decimalFormatter = NumberFormatter()
|
||||
|
||||
func mapToFileObject(_ json: [String: AnyObject]) -> DropboxFileObject? {
|
||||
guard let name = json["name"] as? String else { return nil }
|
||||
guard let path = json["path_display"] as? String else { return nil }
|
||||
let size = (json["size"] as? NSNumber)?.int64Value ?? -1
|
||||
let serverTime = resolve(dateString: json["server_modified"] as? String ?? "")
|
||||
let modifiedDate = resolve(dateString: json["client_modified"] as? String ?? "")
|
||||
let isDirectory = (json[".tag"] as? String) == "folder"
|
||||
let isReadonly = (json["sharing_info"]?["read_only"] as? NSNumber)?.boolValue ?? false
|
||||
let id = json["id"] as? String
|
||||
let rev = json["id"] as? String
|
||||
return DropboxFileObject(name: name, path: path, size: size, serverTime: serverTime, modifiedDate: modifiedDate, fileType: isDirectory ? .directory : .regular, isReadOnly: isReadonly, id: id, rev: rev)
|
||||
func mapMediaInfo(_ json: [String: Any]) -> (dictionary: [String: Any], keys: [String]) {
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
if let dimensions = json["dimensions"] as? [String: Any], let height = dimensions["height"] as? UInt64, let width = dimensions["width"] as? UInt64 {
|
||||
keys.append("Dimensions")
|
||||
dic["Dimensions"] = "\(width)x\(height)"
|
||||
}
|
||||
if let location = json["location"] as? [String: Any], let latitude = location["latitude"] as? Double, let longitude = location["longitude"] as? Double {
|
||||
|
||||
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))
|
||||
dic["Location"] = "\(latStr), \(longStr)"
|
||||
}
|
||||
if let timeTakenStr = json["time_taken"] as? String, let timeTaken = resolve(dateString: 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))
|
||||
}
|
||||
return (dic, keys)
|
||||
}
|
||||
|
||||
func delegateNotify(_ operation: FileOperationType, error: Error?) {
|
||||
|
||||
@@ -0,0 +1,485 @@
|
||||
//
|
||||
// ExtendedLocalFileProvider.swift
|
||||
// FileProvider
|
||||
//
|
||||
// Created by Amir Abbas Mousavian.
|
||||
// Copyright © 2017 Mousavian. Distributed under MIT license.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ImageIO
|
||||
import CoreGraphics
|
||||
import AVFoundation
|
||||
|
||||
extension LocalFileProvider: ExtendedFileProvider {
|
||||
public func thumbnailOfFileSupported(path: String) -> Bool {
|
||||
switch (path as NSString).pathExtension.lowercased() {
|
||||
case LocalFileInformationGenerator.imageThumbnailExtensions:
|
||||
return true
|
||||
case LocalFileInformationGenerator.audioThumbnailExtensions:
|
||||
return true
|
||||
case LocalFileInformationGenerator.videoThumbnailExtensions:
|
||||
return true
|
||||
case LocalFileInformationGenerator.pdfThumbnailExtensions:
|
||||
return true
|
||||
case LocalFileInformationGenerator.officeThumbnailExtensions:
|
||||
return true
|
||||
case LocalFileInformationGenerator.customThumbnailExtensions:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public func propertiesOfFileSupported(path: String) -> Bool {
|
||||
let fileExt = (path as NSString).pathExtension.lowercased()
|
||||
switch fileExt {
|
||||
case LocalFileInformationGenerator.imagePropertiesExtensions:
|
||||
return LocalFileInformationGenerator.imageProperties != nil
|
||||
case LocalFileInformationGenerator.audioPropertiesExtensions:
|
||||
return LocalFileInformationGenerator.audioProperties != nil
|
||||
case LocalFileInformationGenerator.videoPropertiesExtensions:
|
||||
return LocalFileInformationGenerator.videoProperties != nil
|
||||
case LocalFileInformationGenerator.pdfPropertiesExtensions:
|
||||
return LocalFileInformationGenerator.pdfProperties != nil
|
||||
case LocalFileInformationGenerator.archivePropertiesExtensions:
|
||||
return LocalFileInformationGenerator.archiveProperties != nil
|
||||
case LocalFileInformationGenerator.officePropertiesExtensions:
|
||||
return LocalFileInformationGenerator.officeProperties != nil
|
||||
case LocalFileInformationGenerator.customPropertiesExtensions:
|
||||
return LocalFileInformationGenerator.customProperties != nil
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public 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
|
||||
// Check cache
|
||||
let fileURL = self.url(of: path)
|
||||
// Create Thumbnail and cache
|
||||
switch fileURL.pathExtension.lowercased() {
|
||||
case LocalFileInformationGenerator.videoThumbnailExtensions:
|
||||
thumbnailImage = LocalFileInformationGenerator.videoThumbnail(fileURL)
|
||||
case LocalFileInformationGenerator.audioThumbnailExtensions:
|
||||
thumbnailImage = LocalFileInformationGenerator.audioThumbnail(fileURL)
|
||||
case LocalFileInformationGenerator.imageThumbnailExtensions:
|
||||
thumbnailImage = LocalFileInformationGenerator.imageThumbnail(fileURL)
|
||||
case LocalFileInformationGenerator.pdfThumbnailExtensions:
|
||||
thumbnailImage = LocalFileInformationGenerator.pdfThumbnail(fileURL)
|
||||
case LocalFileInformationGenerator.officeThumbnailExtensions:
|
||||
thumbnailImage = LocalFileInformationGenerator.officeThumbnail(fileURL)
|
||||
case LocalFileInformationGenerator.customThumbnailExtensions:
|
||||
thumbnailImage = LocalFileInformationGenerator.customThumbnail(fileURL)
|
||||
default:
|
||||
completionHandler(nil, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if let image = thumbnailImage {
|
||||
let scaledImage = LocalFileProvider.scaleDown(image: image, toSize: dimension)
|
||||
completionHandler(scaledImage, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public 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:
|
||||
getter = LocalFileInformationGenerator.imageProperties
|
||||
case LocalFileInformationGenerator.audioPropertiesExtensions:
|
||||
getter = LocalFileInformationGenerator.audioProperties
|
||||
case LocalFileInformationGenerator.videoPropertiesExtensions:
|
||||
getter = LocalFileInformationGenerator.videoProperties
|
||||
case LocalFileInformationGenerator.pdfPropertiesExtensions:
|
||||
getter = LocalFileInformationGenerator.pdfProperties
|
||||
case LocalFileInformationGenerator.archivePropertiesExtensions:
|
||||
getter = LocalFileInformationGenerator.archiveProperties
|
||||
case LocalFileInformationGenerator.officePropertiesExtensions:
|
||||
getter = LocalFileInformationGenerator.officeProperties
|
||||
case LocalFileInformationGenerator.customPropertiesExtensions:
|
||||
getter = LocalFileInformationGenerator.customProperties
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
if let getterMethod = getter {
|
||||
(dic, keys) = getterMethod(self.url(of: path))
|
||||
}
|
||||
|
||||
completionHandler(dic, keys, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds supported file types and thumbnail/properties generator for specefied type of file
|
||||
public struct LocalFileInformationGenerator {
|
||||
/// Image extensions supportes for thumbnail.
|
||||
///
|
||||
/// Default: `["jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff", "ico"]`
|
||||
static public var imageThumbnailExtensions: [String] = ["jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff", "ico"]
|
||||
|
||||
/// Audio and music extensions supportes for thumbnail.
|
||||
///
|
||||
/// Default: `["mp3", "aac", "m4a"]`
|
||||
static public var audioThumbnailExtensions: [String] = ["mp3", "aac", "m4a"]
|
||||
|
||||
/// Video extensions supportes for thumbnail.
|
||||
///
|
||||
/// Default: `["mov", "mp4", "m4v", "mpg", "mpeg"]`
|
||||
static public var videoThumbnailExtensions: [String] = ["mov", "mp4", "m4v", "mpg", "mpeg"]
|
||||
|
||||
/// Portable document file extensions supportes for thumbnail.
|
||||
///
|
||||
/// Default: `["pdf"]`
|
||||
static public var pdfThumbnailExtensions: [String] = ["pdf"]
|
||||
|
||||
/// Office document extensions supportes for thumbnail.
|
||||
///
|
||||
/// Default: `empty`
|
||||
static public var officeThumbnailExtensions: [String] = []
|
||||
|
||||
/// Custom document extensions supportes for thumbnail.
|
||||
///
|
||||
/// Default: `empty`
|
||||
static public var customThumbnailExtensions: [String] = []
|
||||
|
||||
|
||||
/// Image extensions supportes for properties.
|
||||
///
|
||||
/// Default: `["jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff"]`
|
||||
static public var imagePropertiesExtensions: [String] = ["jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff"]
|
||||
|
||||
/// Audio and music extensions supportes for properties.
|
||||
///
|
||||
/// Default: `["mp3", "aac", "m4a", "caf"]`
|
||||
static public var audioPropertiesExtensions: [String] = ["mp3", "aac", "m4a", "caf"]
|
||||
|
||||
/// Video extensions supportes for properties.
|
||||
///
|
||||
/// Default: `["mp4", "mpg", "3gp", "mov", "avi"]`
|
||||
static public var videoPropertiesExtensions: [String] = ["mp4", "mpg", "3gp", "mov", "avi"]
|
||||
|
||||
/// Portable document file extensions supportes for properties.
|
||||
///
|
||||
/// Default: `["pdf"]`
|
||||
static public var pdfPropertiesExtensions: [String] = ["pdf"]
|
||||
|
||||
/// Archive extensions (like zip) supportes for properties.
|
||||
///
|
||||
/// Default: `empty`
|
||||
static public var archivePropertiesExtensions: [String] = []
|
||||
|
||||
/// Office document extensions supportes for properties.
|
||||
///
|
||||
/// Default: `empty`
|
||||
static public var officePropertiesExtensions: [String] = []
|
||||
|
||||
/// Custom document extensions supportes for properties.
|
||||
///
|
||||
/// Default: `empty`
|
||||
static public var customPropertiesExtensions: [String] = []
|
||||
|
||||
/// Thumbnail generator closure for image files.
|
||||
static public var imageThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in
|
||||
return ImageClass(contentsOfFile: fileURL.path)
|
||||
}
|
||||
|
||||
/// Thumbnail generator closure for audio and music files.
|
||||
static public var audioThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in
|
||||
let playerItem = AVPlayerItem(url: fileURL)
|
||||
let metadataList = playerItem.asset.commonMetadata
|
||||
for item in metadataList {
|
||||
if item.commonKey == AVMetadataCommonKeyArtwork {
|
||||
if let data = item.dataValue {
|
||||
return ImageClass(data: data)
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
assetImgGenerate.appliesPreferredTrackTransform = true
|
||||
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: .zero)
|
||||
#else
|
||||
return ImageClass(cgImage: cgImage)
|
||||
#endif
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
func simplify(_ top:Int64, _ bottom:Int64) -> (newTop:Int, newBottom:Int) {
|
||||
var x = top
|
||||
var y = bottom
|
||||
while (y != 0) {
|
||||
let buffer = y
|
||||
y = x % y
|
||||
x = buffer
|
||||
}
|
||||
let hcfVal = x
|
||||
let newTopVal = top/hcfVal
|
||||
let newBottomVal = bottom/hcfVal
|
||||
return(Int(newTopVal), Int(newBottomVal))
|
||||
}
|
||||
|
||||
guard let cgDataRef = CGImageSourceCreateWithURL(fileURL as CFURL, nil), let cfImageDict = CGImageSourceCopyPropertiesAtIndex(cgDataRef, 0, nil) else {
|
||||
return (dic, keys)
|
||||
}
|
||||
let imageDict = cfImageDict as NSDictionary
|
||||
let tiffDict = imageDict[kCGImagePropertyTIFFDictionary as String] as? NSDictionary ?? [:]
|
||||
let exifDict = imageDict[kCGImagePropertyExifDictionary as String] as? NSDictionary ?? [:]
|
||||
if let pixelWidth = imageDict.object(forKey: kCGImagePropertyPixelWidth) as? NSNumber, let pixelHeight = imageDict.object(forKey: kCGImagePropertyPixelHeight) as? NSNumber {
|
||||
add(key: "Dimensions", value: "\(pixelWidth)x\(pixelHeight)")
|
||||
}
|
||||
|
||||
add(key: "DPI", value: imageDict[kCGImagePropertyDPIWidth as String])
|
||||
add(key: "Device make", value: tiffDict[kCGImagePropertyTIFFMake as String])
|
||||
add(key: "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)
|
||||
}
|
||||
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: "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)
|
||||
add(key: "Exposure time", value: "\(expfrac.newTop)/\(expfrac.newBottom)")
|
||||
}
|
||||
add(key: "ISO speed", value: (exifDict[kCGImagePropertyExifISOSpeedRatings as String] as? NSArray)?.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]()
|
||||
|
||||
func add(key: String, value: Any?) {
|
||||
if let value = value {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
func makeDescription(_ key: String?) -> String? {
|
||||
guard let key = key else {
|
||||
return nil
|
||||
}
|
||||
guard let regex = try? NSRegularExpression(pattern: "([a-z])([A-Z])" , options: NSRegularExpression.Options()) else {
|
||||
return nil
|
||||
}
|
||||
let newKey = regex.stringByReplacingMatches(in: key, options: NSRegularExpression.MatchingOptions(), range: NSMakeRange(0, (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
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
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]()
|
||||
|
||||
func add(key: String, value: Any?) {
|
||||
if let value = value {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if let audioprops = LocalFileInformationGenerator.audioProperties?(fileURL) {
|
||||
dic = audioprops.prop
|
||||
keys = audioprops.keys
|
||||
dic.removeValue(forKey: "Duration")
|
||||
if let index = keys.index(of: "Duration") {
|
||||
keys.remove(at: index)
|
||||
}
|
||||
}
|
||||
let asset = AVURLAsset(url: fileURL, options: nil)
|
||||
let videoTracks = asset.tracks(withMediaType: AVMediaTypeVideo)
|
||||
if let videoTrack = videoTracks.first {
|
||||
var bitrate: Float = 0
|
||||
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: "Video Bitrate", value: "\(Int(ceil(bitrate / 1000))) kbps")
|
||||
}
|
||||
let audioTracks = asset.tracks(withMediaType: AVMediaTypeAudio)
|
||||
// dic["Audio channels"] = audioTracks.count
|
||||
var bitrate: Float = 0
|
||||
for track in audioTracks {
|
||||
bitrate += track.estimatedDataRate
|
||||
}
|
||||
add(key: "Audio Bitrate", value: "\(Int(ceil(bitrate / 1000))) kbps")
|
||||
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 {
|
||||
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!) {
|
||||
return value as String
|
||||
}
|
||||
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))
|
||||
}
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyyMMddHHmmssTZD"
|
||||
if let result = dateFormatter.date(from: dateStr) {
|
||||
return result
|
||||
}
|
||||
dateFormatter.dateFormat = "yyyyMMddHHmmss"
|
||||
if let result = dateFormatter.date(from: dateStr) {
|
||||
return result
|
||||
}
|
||||
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")
|
||||
}
|
||||
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)
|
||||
}
|
||||
+13
-28
@@ -13,8 +13,7 @@ private var lasttaskIdAssociated = 1_000_000_000
|
||||
|
||||
/// This class is a replica of NSURLSessionStreamTask with same api for iOS 7/8
|
||||
/// while it will fallback to NSURLSessionStreamTask in iOS 9.
|
||||
@objc
|
||||
open class FPSStreamTask: URLSessionTask, StreamDelegate {
|
||||
internal class FPSStreamTask: URLSessionTask, StreamDelegate {
|
||||
fileprivate var inputStream: InputStream?
|
||||
fileprivate var outputStream: OutputStream?
|
||||
|
||||
@@ -401,8 +400,8 @@ open class FPSStreamTask: URLSessionTask, StreamDelegate {
|
||||
if #available(iOS 9.0, OSX 10.11, *) {
|
||||
_underlyingTask!.startSecureConnection()
|
||||
} else {
|
||||
inputStream!.setProperty(StreamSocketSecurityLevel.negotiatedSSL.rawValue, forKey: Stream.PropertyKey.socketSecurityLevelKey)
|
||||
outputStream!.setProperty(StreamSocketSecurityLevel.negotiatedSSL.rawValue, forKey: Stream.PropertyKey.socketSecurityLevelKey)
|
||||
inputStream!.setProperty(StreamSocketSecurityLevel.negotiatedSSL.rawValue, forKey: .socketSecurityLevelKey)
|
||||
outputStream!.setProperty(StreamSocketSecurityLevel.negotiatedSSL.rawValue, forKey: .socketSecurityLevelKey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,8 +413,8 @@ open class FPSStreamTask: URLSessionTask, StreamDelegate {
|
||||
if #available(iOS 9.0, OSX 10.11, *) {
|
||||
_underlyingTask!.stopSecureConnection()
|
||||
} else {
|
||||
inputStream!.setProperty(StreamSocketSecurityLevel.none.rawValue, forKey: Stream.PropertyKey.socketSecurityLevelKey)
|
||||
outputStream!.setProperty(StreamSocketSecurityLevel.none.rawValue, forKey: Stream.PropertyKey.socketSecurityLevelKey)
|
||||
inputStream!.setProperty(StreamSocketSecurityLevel.none.rawValue, forKey: .socketSecurityLevelKey)
|
||||
outputStream!.setProperty(StreamSocketSecurityLevel.none.rawValue, forKey: .socketSecurityLevelKey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,23 +448,23 @@ open class FPSStreamTask: URLSessionTask, StreamDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
extension URLSession {
|
||||
internal extension URLSession {
|
||||
/* Creates a bidirectional stream task to a given host and port.
|
||||
*/
|
||||
public func fpstreamTaskWithHostName(_ hostname: String, port: Int) -> FPSStreamTask {
|
||||
func fpstreamTaskWithHostName(_ hostname: String, port: Int) -> FPSStreamTask {
|
||||
return FPSStreamTask(session: self, host: hostname, port: port)
|
||||
}
|
||||
|
||||
/* Creates a bidirectional stream task with an NSNetService to identify the endpoint.
|
||||
* The NSNetService will be resolved before any IO completes.
|
||||
*/
|
||||
public func fpstreamTaskWithNetService(_ service: NetService) -> FPSStreamTask {
|
||||
func fpstreamTaskWithNetService(_ service: NetService) -> FPSStreamTask {
|
||||
return fpstreamTaskWithNetService(service)
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public protocol FPSStreamDelegate : URLSessionTaskDelegate {
|
||||
internal protocol FPSStreamDelegate : URLSessionTaskDelegate {
|
||||
|
||||
|
||||
/* Indiciates that the read side of a connection has been closed. Any
|
||||
@@ -503,21 +502,7 @@ public protocol FPSStreamDelegate : URLSessionTaskDelegate {
|
||||
@objc optional func urlSession(_ session: URLSession, streamTask: FPSStreamTask, didBecome inputStream: InputStream, outputStream: OutputStream)
|
||||
}
|
||||
|
||||
private let ports = ["http": 80,
|
||||
"https": 443,
|
||||
"smb": 445,
|
||||
"ftp": 21,
|
||||
"sftp": 22,
|
||||
"sftp": 2121,
|
||||
"telnet": 23,
|
||||
"pop": 110,
|
||||
"smtp": 25,
|
||||
"imap": 143]
|
||||
private let securePorts = ["https": 443,
|
||||
"smb": 445,
|
||||
"sftp": 22,
|
||||
"sftp": 2121,
|
||||
"telnet": 992,
|
||||
"pop": 995,
|
||||
"smtp": 465,
|
||||
"imap": 993]
|
||||
private let ports: [String: Int] = ["http": 80, "https": 443, "smb": 445,"ftp": 21,"ftps": 22, "sftp": 2121,
|
||||
"telnet": 23, "pop": 110, "smtp": 25, "imap": 143]
|
||||
private let securePorts: [String: Int] = ["https": 443, "smb": 445, "ftps": 22, "sftp": 2121,
|
||||
"telnet": 992, "pop": 995, "smtp": 465, "imap": 993]
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
//
|
||||
// FileProvider.swift
|
||||
// FileProvider
|
||||
//
|
||||
// Created by Amir Abbas Mousavian.
|
||||
// Copyright © 2016 Mousavian. Distributed under MIT license.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Containts path, url and attributes of a file or resource.
|
||||
open class FileObject: Equatable {
|
||||
/// A `Dictionary` contains file information, using `URLResourceKey` keys.
|
||||
open internal(set) var allValues: [URLResourceKey: Any]
|
||||
|
||||
internal init(allValues: [URLResourceKey: Any]) {
|
||||
self.allValues = allValues
|
||||
}
|
||||
|
||||
internal init(url: URL, name: String, path: String) {
|
||||
self.allValues = [URLResourceKey: Any]()
|
||||
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? {
|
||||
get {
|
||||
return allValues[.fileURL] as? URL
|
||||
}
|
||||
set {
|
||||
allValues[.fileURL] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Name of the file, usually equals with the last path component
|
||||
open internal(set) var name: String {
|
||||
get {
|
||||
return allValues[.nameKey] as! String
|
||||
}
|
||||
set {
|
||||
allValues[.nameKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Relative path of file object
|
||||
open internal(set) var path: String {
|
||||
get {
|
||||
return allValues[.pathKey] as! String
|
||||
}
|
||||
set {
|
||||
allValues[.pathKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Size of file on disk, return -1 for directories.
|
||||
open internal(set) var size: Int64 {
|
||||
get {
|
||||
return allValues[.fileSizeKey] as? Int64 ?? -1
|
||||
}
|
||||
set {
|
||||
allValues[.fileSizeKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// The time contents of file has been created, returns nil if not set
|
||||
open internal(set) var creationDate: Date? {
|
||||
get {
|
||||
return allValues[.creationDateKey] as? Date
|
||||
}
|
||||
set {
|
||||
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[.contentModificationDateKey] as? Date
|
||||
}
|
||||
set {
|
||||
allValues[.contentModificationDateKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// return resource type of file, usually directory, regular or symLink
|
||||
open internal(set) var type: URLFileResourceType? {
|
||||
get {
|
||||
return allValues[.fileResourceTypeKey] as? URLFileResourceType
|
||||
}
|
||||
set {
|
||||
allValues[.fileResourceTypeKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// 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[.isHiddenKey] as? Bool ?? false
|
||||
}
|
||||
set {
|
||||
allValues[.isHiddenKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// File can not be written
|
||||
open internal(set) var isReadOnly: Bool {
|
||||
get {
|
||||
return !(allValues[.isWritableKey] as? Bool ?? true)
|
||||
}
|
||||
set {
|
||||
allValues[.isWritableKey] = !newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// File is a Directory
|
||||
open var isDirectory: Bool {
|
||||
return self.type == .directory
|
||||
}
|
||||
|
||||
/// File is a normal file
|
||||
open var isRegularFile: Bool {
|
||||
return self.type == .regular
|
||||
}
|
||||
|
||||
/// File is a Symbolic link
|
||||
open var isSymLink: Bool {
|
||||
return self.type == .symbolicLink
|
||||
}
|
||||
|
||||
/// Check `FileObject` equality
|
||||
public static func ==(lhs: FileObject, rhs: FileObject) -> Bool {
|
||||
if rhs === lhs {
|
||||
return true
|
||||
}
|
||||
if type(of: lhs) != type(of: rhs) {
|
||||
return false
|
||||
}
|
||||
if let rurl = rhs.url, let lurl = lhs.url {
|
||||
return rurl == lurl
|
||||
}
|
||||
return rhs.path == lhs.path && rhs.size == lhs.size && rhs.modifiedDate == lhs.modifiedDate
|
||||
}
|
||||
|
||||
internal func mapPredicate() -> [String: Any] {
|
||||
let mapDict: [URLResourceKey: String] = [.fileURL: "url", .nameKey: "name", .pathKey: "path", .fileSizeKey: "filesize", .creationDateKey: "creationDate",
|
||||
.contentModificationDateKey: "modifiedDate", .isHiddenKey: "isHidden", .isWritableKey: "isWritable", .serverDate: "serverDate", .entryTag: "entryTag", .mimeType: "mimeType"]
|
||||
let typeDict: [URLFileResourceType: String] = [.directory: "directory", .regular: "regular", .symbolicLink: "symbolicLink", .unknown: "unknown"]
|
||||
var result = [String: Any]()
|
||||
for (key, value) in allValues {
|
||||
if let convertkey = mapDict[key] {
|
||||
result[convertkey] = value
|
||||
}
|
||||
}
|
||||
result["eTag"] = result["entryTag"]
|
||||
result["isReadOnly"] = self.isReadOnly
|
||||
result["isDirectory"] = self.isDirectory
|
||||
result["isRegularFile"] = self.isRegularFile
|
||||
result["isSymLink"] = self.isSymLink
|
||||
result["type"] = typeDict[self.type ?? .unknown] ?? "unknown"
|
||||
return result
|
||||
}
|
||||
|
||||
static public func convertPredicate(fromSpotlight query: NSPredicate) -> NSPredicate {
|
||||
let mapDict: [String: URLResourceKey] = [NSMetadataItemURLKey: .fileURL, NSMetadataItemFSNameKey: .nameKey, NSMetadataItemPathKey: .pathKey,
|
||||
NSMetadataItemFSSizeKey: .fileSizeKey, NSMetadataItemFSCreationDateKey: .creationDateKey,
|
||||
NSMetadataItemFSContentChangeDateKey: .contentModificationDateKey, "kMDItemFSInvisible": .isHiddenKey, "kMDItemFSIsWriteable": .isWritableKey, "kMDItemKind": .mimeType]
|
||||
|
||||
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 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
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
internal func rfc3339utc(of date:Date) -> String {
|
||||
let fm = DateFormatter()
|
||||
fm.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
|
||||
fm.timeZone = TimeZone(identifier:"UTC")
|
||||
fm.locale = Locale(identifier:"en_US_POSIX")
|
||||
return fm.string(from:date)
|
||||
}
|
||||
|
||||
/// Sorting FileObject array by given criteria, **not thread-safe**
|
||||
public struct FileObjectSorting {
|
||||
|
||||
/// Determines sort kind by which item of File object
|
||||
public enum SortType {
|
||||
/// Sorting by default Finder (case-insensitive) behavior
|
||||
case name
|
||||
/// Sorting by case-sensitive form of file name
|
||||
case nameCaseSensitive
|
||||
/// Sorting by case-in sensitive form of file name
|
||||
case nameCaseInsensitive
|
||||
/// Sorting by file type
|
||||
case `extension`
|
||||
/// Sorting by file modified date
|
||||
case modifiedDate
|
||||
/// Sorting by file creation date
|
||||
case creationDate
|
||||
/// Sorting by file modified date
|
||||
case size
|
||||
|
||||
/// all sort types
|
||||
static var allItems: [SortType] {
|
||||
return [.name, .nameCaseSensitive, .nameCaseInsensitive, .extension,
|
||||
.modifiedDate,.creationDate, .size]
|
||||
}
|
||||
}
|
||||
|
||||
public let sortType: SortType
|
||||
/// puts A before Z, default is true
|
||||
public let ascending: Bool
|
||||
/// puts directories on top, regardless of other attributes, default is false
|
||||
public let isDirectoriesFirst: Bool
|
||||
|
||||
public static let nameAscending = FileObjectSorting(type: .name, ascending: true)
|
||||
public static let nameDesceding = FileObjectSorting(type: .name, ascending: false)
|
||||
public static let sizeAscending = FileObjectSorting(type: .size, ascending: true)
|
||||
public static let sizeDesceding = FileObjectSorting(type: .size, ascending: false)
|
||||
public static let extensionAscending = FileObjectSorting(type: .extension, ascending: true)
|
||||
public static let extensionDesceding = FileObjectSorting(type: .extension, ascending: false)
|
||||
public static let modifiedAscending = FileObjectSorting(type: .modifiedDate, ascending: true)
|
||||
public static let modifiedDesceding = FileObjectSorting(type: .modifiedDate, ascending: false)
|
||||
public static let createdAscending = FileObjectSorting(type: .creationDate, ascending: true)
|
||||
public static let createdDesceding = FileObjectSorting(type: .creationDate, ascending: false)
|
||||
|
||||
/// Initializes a `FileObjectSorting` allows to sort an `Array` of `FileObject`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - type: Determines to sort based on which file property.
|
||||
/// - ascending: `true` of resulting `Array` is ascending
|
||||
/// - isDirectoriesFirst: Puts directoris on the top of resulting `Array`.
|
||||
public init (type: SortType, ascending: Bool = true, isDirectoriesFirst: Bool = false) {
|
||||
self.sortType = type
|
||||
self.ascending = ascending
|
||||
self.isDirectoriesFirst = isDirectoriesFirst
|
||||
}
|
||||
|
||||
/// Sorts array of `FileObject`s by criterias set in attributes.
|
||||
public func sort(_ files: [FileObject]) -> [FileObject] {
|
||||
return files.sorted {
|
||||
if isDirectoriesFirst {
|
||||
if ($0.isDirectory) && !($1.isDirectory) {
|
||||
return true
|
||||
}
|
||||
if !($0.isDirectory) && ($1.isDirectory) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
switch sortType {
|
||||
case .name:
|
||||
return ($0.name).localizedStandardCompare($1.name) == (ascending ? .orderedAscending : .orderedDescending)
|
||||
case .nameCaseSensitive:
|
||||
return ($0.name).localizedCompare($1.name) == (ascending ? .orderedAscending : .orderedDescending)
|
||||
case .nameCaseInsensitive:
|
||||
return ($0.name).localizedCaseInsensitiveCompare($1.name) == (ascending ? .orderedAscending : .orderedDescending)
|
||||
case .extension:
|
||||
let kind1 = $0.isDirectory ? "folder" : ($0.path as NSString).pathExtension
|
||||
let kind2 = $1.isDirectory ? "folder" : ($1.path as NSString).pathExtension
|
||||
return kind1.localizedCaseInsensitiveCompare(kind2) == (ascending ? .orderedAscending : .orderedDescending)
|
||||
case .modifiedDate:
|
||||
let fileMod1 = $0.modifiedDate ?? Date.distantPast
|
||||
let fileMod2 = $1.modifiedDate ?? Date.distantPast
|
||||
return ascending ? fileMod1 < fileMod2 : fileMod1 > fileMod2
|
||||
case .creationDate:
|
||||
let fileCreation1 = $0.creationDate ?? Date.distantPast
|
||||
let fileCreation2 = $1.creationDate ?? Date.distantPast
|
||||
return ascending ? fileCreation1 < fileCreation2 : fileCreation1 > fileCreation2
|
||||
case .size:
|
||||
return ascending ? $0.size < $1.size : $0.size > $1.size
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
extension URLFileResourceType {
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal extension URLResourceKey {
|
||||
static let fileURL = URLResourceKey(rawValue: "NSURLFileURLKey")
|
||||
static let serverDate = URLResourceKey(rawValue: "NSURLServerDateKey")
|
||||
static let entryTag = URLResourceKey(rawValue: "NSURLEntryTagKey")
|
||||
static let mimeType = URLResourceKey(rawValue: "NSURLMIMETypeIdentifierKey")
|
||||
}
|
||||
|
||||
internal extension URL {
|
||||
var uw_scheme: String {
|
||||
return self.scheme ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
internal func jsonToDictionary(_ jsonString: String) -> [String: AnyObject]? {
|
||||
guard let data = jsonString.data(using: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
if let dic = try? JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions()) as? [String: AnyObject] {
|
||||
return dic
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
internal func dictionaryToJSON(_ dictionary: [String: AnyObject]) -> String? {
|
||||
if let data = try? JSONSerialization.data(withJSONObject: dictionary, options: JSONSerialization.WritingOptions()) {
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -5,6 +5,9 @@
|
||||
// Created by Amir Abbas Mousavian on 5/6/95.
|
||||
//
|
||||
//
|
||||
|
||||
#import <TargetConditionals.h>
|
||||
|
||||
#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR
|
||||
#import <UIKit/UIKit.h>
|
||||
//! Project version number for FileProvider iOS.
|
||||
|
||||
+859
-177
File diff suppressed because it is too large
Load Diff
+490
-416
@@ -8,320 +8,155 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public final class LocalFileObject: FileObject {
|
||||
public let allocatedSize: Int64
|
||||
// codebeat:disable[ARITY]
|
||||
public init(absoluteURL: URL, name: String, path: String, size: Int64 = -1, allocatedSize: Int64 = 0, createdDate: Date? = nil, modifiedDate: Date? = nil, fileType: FileType = .regular, isHidden: Bool = false, isReadOnly: Bool = false) {
|
||||
self.allocatedSize = allocatedSize
|
||||
super.init(absoluteURL: absoluteURL, name: name, path: path, size: size, createdDate: createdDate, modifiedDate: modifiedDate, fileType: fileType, isHidden: isHidden, isReadOnly: isReadOnly)
|
||||
}
|
||||
// codebeat:enable[ARITY]
|
||||
}
|
||||
|
||||
open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
open static let type = "Local"
|
||||
open var isPathRelative: Bool = true
|
||||
open var baseURL: URL? = LocalFileProvider.defaultBaseURL()
|
||||
open var currentPath: String = ""
|
||||
/**
|
||||
This provider class allows interacting with local files placed in user disk. It also allows an
|
||||
easy way to use `NSFileCoordintaing` to coordinate read and write when neccessary.
|
||||
|
||||
it uses `FileManager` foundation class with some additions like searching and reading a portion of file.
|
||||
*/
|
||||
open class LocalFileProvider: FileProvider, FileProviderMonitor, FileProvideUndoable {
|
||||
open class var type: String { return "Local" }
|
||||
open fileprivate(set) var baseURL: URL?
|
||||
open var currentPath: String
|
||||
open var dispatch_queue: DispatchQueue
|
||||
open var operation_queue: DispatchQueue
|
||||
open var operation_queue: OperationQueue
|
||||
open weak var delegate: FileProviderDelegate?
|
||||
open let credential: URLCredential? = nil
|
||||
|
||||
open let fileManager = FileManager()
|
||||
open let opFileManager = FileManager()
|
||||
open internal(set) var credential: URLCredential?
|
||||
|
||||
/// Underlying `FileManager` object for listing and metadata fetching.
|
||||
open private(set) var fileManager = FileManager()
|
||||
/// Underlying `FileManager` object for operationa like copying, moving, etc.
|
||||
open private(set) var opFileManager = FileManager()
|
||||
fileprivate var fileProviderManagerDelegate: LocalFileProviderManagerDelegate? = nil
|
||||
|
||||
public init () {
|
||||
dispatch_queue = DispatchQueue(label: "FileProvider.\(LocalFileProvider.type)", attributes: DispatchQueue.Attributes.concurrent)
|
||||
operation_queue = DispatchQueue(label: "FileProvider.\(LocalFileProvider.type).Operation", attributes: [])
|
||||
fileProviderManagerDelegate = LocalFileProviderManagerDelegate(provider: self)
|
||||
opFileManager.delegate = fileProviderManagerDelegate
|
||||
open var undoManager: UndoManager? = nil
|
||||
|
||||
/**
|
||||
Forces file operations to use `NSFileCoordinating`, should be set `true` if:
|
||||
- Files are on ubiquity (iCloud) container.
|
||||
- Multiple processes are accessing same file, recommended when accessing a shared/public
|
||||
user document in macOS and when using app extensions in iOS/tvOS (shared container).
|
||||
|
||||
By default it's `true` when using iCloud or shared container (App Group) initializers,
|
||||
otherwise it's `false` to accelerate operations.
|
||||
*/
|
||||
open var isCoorinating: Bool
|
||||
|
||||
/**
|
||||
Initializes provider for the specified common directory in the requested domains.
|
||||
default values are `directory: .documentDirectory, domainMask: .userDomainMask`.
|
||||
|
||||
- Parameters:
|
||||
- directory: The search path directory. The supported values are described in `FileManager.SearchPathDirectory`.
|
||||
- domainMask: The file system domain to search. The value for this parameter is one or more of the constants described in `FileManager.SearchPathDomainMask`.
|
||||
*/
|
||||
public convenience init (directory: FileManager.SearchPathDirectory = .documentDirectory, domainMask: FileManager.SearchPathDomainMask = .userDomainMask) {
|
||||
self.init(baseURL: FileManager.default.urls(for: directory, in: domainMask).first!)
|
||||
}
|
||||
|
||||
/**
|
||||
Failable initializer for the specified shared container directory, allows data and files to be shared among app
|
||||
and extensions regarding sandbox requirements. Container ID is same with app group specified in project `Capabilities`
|
||||
tab under `App Group` item. If you don't have enough privilage to access container or the app group imply does't exist,
|
||||
initialing will fail.
|
||||
default values are `directory: .documentDirectory`.
|
||||
|
||||
- Parameters:
|
||||
- sharedContainerId: Same with `App Group` identifier defined in project settings.
|
||||
- directory: The search path directory. The supported values are described in `FileManager.SearchPathDirectory`.
|
||||
*/
|
||||
public convenience init? (sharedContainerId: String, directory: FileManager.SearchPathDirectory = .documentDirectory) {
|
||||
guard let baseURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: sharedContainerId) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var finalBaseURL = baseURL
|
||||
|
||||
switch directory {
|
||||
case .documentDirectory:
|
||||
finalBaseURL = baseURL.appendingPathComponent("Documents")
|
||||
case .libraryDirectory:
|
||||
finalBaseURL = baseURL.appendingPathComponent("Library")
|
||||
case .cachesDirectory:
|
||||
finalBaseURL = baseURL.appendingPathComponent("Library/Caches")
|
||||
case .applicationSupportDirectory:
|
||||
finalBaseURL = baseURL.appendingPathComponent("Library/Application support")
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
self.init(baseURL: finalBaseURL)
|
||||
self.isCoorinating = true
|
||||
|
||||
try? fileManager.createDirectory(at: finalBaseURL, withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
/// Initializes provider for the specified local URL.
|
||||
///
|
||||
/// - Parameter baseURL: Local URL location for base directory.
|
||||
public init (baseURL: URL) {
|
||||
guard baseURL.isFileURL else {
|
||||
fatalError("Cannot initialize a Local provider from remote URL.")
|
||||
}
|
||||
self.baseURL = baseURL
|
||||
dispatch_queue = DispatchQueue(label: "FileProvider.\(LocalFileProvider.type)", attributes: DispatchQueue.Attributes.concurrent)
|
||||
operation_queue = DispatchQueue(label: "FileProvider.\(LocalFileProvider.type).Operation", attributes: [])
|
||||
self.currentPath = ""
|
||||
self.credential = nil
|
||||
self.isCoorinating = false
|
||||
|
||||
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: .concurrent)
|
||||
operation_queue = OperationQueue()
|
||||
operation_queue.name = "FileProvider.\(type(of: self).type).Operation"
|
||||
|
||||
fileProviderManagerDelegate = LocalFileProviderManagerDelegate(provider: self)
|
||||
opFileManager.delegate = fileProviderManagerDelegate
|
||||
|
||||
}
|
||||
|
||||
fileprivate static func defaultBaseURL() -> URL {
|
||||
let paths = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true);
|
||||
return URL(fileURLWithPath: paths[0])
|
||||
/// **DEPRECATED:** No longer is in use and overriding this method has no effect anymore.
|
||||
@available(*, deprecated, message: "Overriding this method has no effect anymore.")
|
||||
open class func defaultBaseURL() -> URL {
|
||||
return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
}
|
||||
|
||||
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
|
||||
dispatch_queue.async {
|
||||
do {
|
||||
let contents = try self.fileManager.contentsOfDirectory(at: self.absoluteURL(path), includingPropertiesForKeys: [URLResourceKey.nameKey, URLResourceKey.fileSizeKey, URLResourceKey.fileAllocatedSizeKey, URLResourceKey.creationDateKey, URLResourceKey.contentModificationDateKey, URLResourceKey.isHiddenKey, URLResourceKey.volumeIsReadOnlyKey], options: FileManager.DirectoryEnumerationOptions.skipsSubdirectoryDescendants)
|
||||
let filesAttributes = contents.map({ (fileURL) -> LocalFileObject in
|
||||
return self.attributesOfItem(url: fileURL)
|
||||
let contents = try self.fileManager.contentsOfDirectory(at: self.url(of: path), includingPropertiesForKeys: nil, options: .skipsSubdirectoryDescendants)
|
||||
let filesAttributes = contents.flatMap({ (fileURL) -> LocalFileObject? in
|
||||
let path = self.relativePathOf(url: fileURL)
|
||||
return LocalFileObject(fileWithPath: path, relativeTo: self.baseURL)
|
||||
})
|
||||
completionHandler(filesAttributes, nil)
|
||||
} catch let e as NSError {
|
||||
} catch let e {
|
||||
completionHandler([], e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal func attributesOfItem(url fileURL: URL) -> LocalFileObject {
|
||||
var namev, sizev, allocated, filetypev, creationDatev, modifiedDatev, hiddenv, readonlyv: AnyObject?
|
||||
_ = try? (fileURL as NSURL).getResourceValue(&namev, forKey: URLResourceKey.nameKey)
|
||||
_ = try? (fileURL as NSURL).getResourceValue(&sizev, forKey: URLResourceKey.fileSizeKey)
|
||||
_ = try? (fileURL as NSURL).getResourceValue(&allocated, forKey: URLResourceKey.fileAllocatedSizeKey)
|
||||
_ = try? (fileURL as NSURL).getResourceValue(&creationDatev, forKey: URLResourceKey.creationDateKey)
|
||||
_ = try? (fileURL as NSURL).getResourceValue(&modifiedDatev, forKey: URLResourceKey.contentModificationDateKey)
|
||||
_ = try? (fileURL as NSURL).getResourceValue(&filetypev, forKey: URLResourceKey.fileResourceTypeKey)
|
||||
_ = try? (fileURL as NSURL).getResourceValue(&hiddenv, forKey: URLResourceKey.isHiddenKey)
|
||||
_ = try? (fileURL as NSURL).getResourceValue(&readonlyv, forKey: URLResourceKey.volumeIsReadOnlyKey)
|
||||
let path: String
|
||||
if isPathRelative {
|
||||
path = self.relativePathOf(url: fileURL)
|
||||
} else {
|
||||
path = fileURL.path
|
||||
open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
|
||||
dispatch_queue.async {
|
||||
completionHandler(LocalFileObject(fileWithPath: path, relativeTo: self.baseURL), nil)
|
||||
}
|
||||
let filetype = URLFileResourceType(rawValue: filetypev as? String ?? "")
|
||||
let fileAttr = LocalFileObject(absoluteURL: fileURL, name: namev as! String, path: path, size: sizev?.int64Value ?? -1, allocatedSize: allocated?.int64Value ?? -1, createdDate: creationDatev as? Date, modifiedDate: modifiedDatev as? Date, fileType: FileType(urlResourceTypeValue: filetype), isHidden: hiddenv?.boolValue ?? false, isReadOnly: readonlyv?.boolValue ?? false)
|
||||
return fileAttr
|
||||
}
|
||||
|
||||
open func storageProperties(completionHandler: (@escaping (_ total: Int64, _ used: Int64) -> Void)) {
|
||||
let dict = (try? FileManager.default.attributesOfFileSystem(forPath: baseURL?.path ?? "/"))
|
||||
let totalSize = (dict?[FileAttributeKey.systemSize] as? NSNumber)?.int64Value ?? -1;
|
||||
let freeSize = (dict?[FileAttributeKey.systemFreeSize] as? NSNumber)?.int64Value ?? 0;
|
||||
let values = try? baseURL?.resourceValues(forKeys: [.volumeTotalCapacityKey, .volumeAvailableCapacityKey])
|
||||
let totalSize = Int64(values??.volumeTotalCapacity ?? -1)
|
||||
let freeSize = Int64(values??.volumeAvailableCapacity ?? 0)
|
||||
completionHandler(totalSize, totalSize - freeSize)
|
||||
}
|
||||
|
||||
open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
|
||||
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
|
||||
dispatch_queue.async {
|
||||
completionHandler(self.attributesOfItem(url: self.absoluteURL(path)), nil)
|
||||
}
|
||||
}
|
||||
|
||||
open weak var fileOperationDelegate : FileOperationDelegate?
|
||||
|
||||
@objc(createWithFolder:at:completionHandler:)
|
||||
@discardableResult
|
||||
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
operation_queue.async {
|
||||
do {
|
||||
try self.opFileManager.createDirectory(at: self.absoluteURL(atPath).appendingPathComponent(folderName), withIntermediateDirectories: true, attributes: [:])
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.delegate?.fileproviderSucceed(self, operation: .create(path: (atPath as NSString).appendingPathComponent(folderName) + "/"))
|
||||
})
|
||||
} catch let e as NSError {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.delegate?.fileproviderFailed(self, operation: .create(path: (atPath as NSString).appendingPathComponent(folderName) + "/"))
|
||||
})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func create(file fileAttribs: FileObject, at atPath: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
operation_queue.async {
|
||||
let fileURL = self.absoluteURL(atPath).appendingPathComponent(fileAttribs.name)
|
||||
var attributes = [String : Any]()
|
||||
if let createdDate = fileAttribs.createdDate {
|
||||
attributes[FileAttributeKey.creationDate.rawValue] = createdDate as NSDate
|
||||
}
|
||||
if let modDate = fileAttribs.modifiedDate {
|
||||
attributes[FileAttributeKey.modificationDate.rawValue] = modDate as NSDate
|
||||
}
|
||||
if fileAttribs.isReadOnly {
|
||||
attributes[FileAttributeKey.posixPermissions.rawValue] = NSNumber(value: 365 as Int16)
|
||||
}
|
||||
let success = self.opFileManager.createFile(atPath: fileURL.path, contents: data, attributes: attributes)
|
||||
if success {
|
||||
do {
|
||||
try (fileURL as NSURL).setResourceValue(fileAttribs.isHidden, forKey: URLResourceKey.isHiddenKey)
|
||||
} catch _ {}
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.delegate?.fileproviderSucceed(self, operation: .create(path: (atPath as NSString).appendingPathComponent(fileAttribs.name)))
|
||||
})
|
||||
} else {
|
||||
completionHandler?(self.throwError(atPath, code: URLError.cannotCreateFile as FoundationErrorEnum))
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.delegate?.fileproviderFailed(self, operation: .create(path: (atPath as NSString).appendingPathComponent(fileAttribs.name)))
|
||||
})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
// FIXME: progress
|
||||
operation_queue.async {
|
||||
if !overwrite && self.fileManager.fileExists(atPath: self.absoluteURL(toPath).path) {
|
||||
completionHandler?(self.throwError(toPath, code: URLError.cannotMoveFile as FoundationErrorEnum))
|
||||
return
|
||||
}
|
||||
do {
|
||||
try self.opFileManager.moveItem(at: self.absoluteURL(path), to: self.absoluteURL(toPath))
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.delegate?.fileproviderSucceed(self, operation: .move(source: path, destination: toPath))
|
||||
})
|
||||
} catch let e as NSError {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.delegate?.fileproviderFailed(self, operation: .move(source: path, destination: toPath))
|
||||
})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
// FIXME: progress, for files > 100mb, monitor file by another thread, for dirs check copied items count
|
||||
operation_queue.async {
|
||||
if !overwrite && self.fileManager.fileExists(atPath: self.absoluteURL(toPath).path) {
|
||||
completionHandler?(self.throwError(toPath, code: URLError.cannotWriteToFile as FoundationErrorEnum))
|
||||
return
|
||||
}
|
||||
do {
|
||||
try self.opFileManager.copyItem(at: self.absoluteURL(path), to: self.absoluteURL(toPath))
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.delegate?.fileproviderSucceed(self, operation: .copy(source: path, destination: toPath))
|
||||
})
|
||||
} catch let e as NSError {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.delegate?.fileproviderFailed(self, operation: .copy(source: path, destination: toPath))
|
||||
})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
operation_queue.async {
|
||||
do {
|
||||
try self.opFileManager.removeItem(at: self.absoluteURL(path))
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.delegate?.fileproviderSucceed(self, operation: .remove(path: path))
|
||||
})
|
||||
} catch let e as NSError {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.delegate?.fileproviderFailed(self, operation: .remove(path: path))
|
||||
})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func copyItem(localFile: URL, to toPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
operation_queue.async {
|
||||
do {
|
||||
try self.opFileManager.copyItem(at: localFile, to: self.absoluteURL(toPath))
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.delegate?.fileproviderSucceed(self, operation: .copy(source: localFile.absoluteString, destination: toPath))
|
||||
})
|
||||
} catch let e as NSError {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.delegate?.fileproviderFailed(self, operation: .copy(source: localFile.absoluteString, destination: toPath))
|
||||
})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
operation_queue.async {
|
||||
do {
|
||||
try self.opFileManager.copyItem(at: self.absoluteURL(path), to: toLocalURL)
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.delegate?.fileproviderSucceed(self, operation: .copy(source: path, destination: toLocalURL.absoluteString))
|
||||
})
|
||||
} catch let e as NSError {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.delegate?.fileproviderFailed(self, operation: .copy(source: path, destination: toLocalURL.absoluteString))
|
||||
})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
dispatch_queue.async {
|
||||
let data = self.fileManager.contents(atPath: self.absoluteURL(path).path)
|
||||
completionHandler(data, nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
// Unfortunatlely there is no method provided in NSFileManager to read a segment of file.
|
||||
// So we have to fallback to POSIX provided methods
|
||||
dispatch_queue.async {
|
||||
let aPath = self.absoluteURL(path).path
|
||||
guard !self.attributesOfItem(url: self.absoluteURL(path)).isDirectory && self.fileManager.fileExists(atPath: aPath) else {
|
||||
completionHandler(nil, self.throwError(path, code: URLError.cannotOpenFile as FoundationErrorEnum))
|
||||
return
|
||||
}
|
||||
let fd_from = open(aPath, O_RDONLY)
|
||||
if fd_from < 0 {
|
||||
completionHandler(nil, self.throwError(path, code: URLError.cannotOpenFile as FoundationErrorEnum))
|
||||
return
|
||||
}
|
||||
defer { precondition(close(fd_from) >= 0) }
|
||||
lseek(fd_from, offset, SEEK_SET)
|
||||
var buf = [UInt8](repeating: 0, count: length)
|
||||
let nread = read(fd_from, &buf, buf.count)
|
||||
if nread < 0 {
|
||||
completionHandler(nil, self.throwError(path, code: URLError.noPermissionsToReadFile as FoundationErrorEnum))
|
||||
} else if nread == 0 {
|
||||
completionHandler(nil, nil)
|
||||
} else {
|
||||
let data = Data(bytesNoCopy: UnsafeMutablePointer<UInt8>(&buf), count: nread, deallocator: .free)
|
||||
completionHandler(data, nil)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func writeContents(path: String, contents data: Data, atomically: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
operation_queue.async {
|
||||
try? data.write(to: self.absoluteURL(path), options: atomically ? [.atomic] : [])
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.delegate?.fileproviderSucceed(self, operation: .modify(path: path))
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
open func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
|
||||
dispatch_queue.async {
|
||||
let iterator = self.fileManager.enumerator(at: self.absoluteURL(path), includingPropertiesForKeys: nil, options: recursive ? FileManager.DirectoryEnumerationOptions() : .skipsSubdirectoryDescendants) { (url, e) -> Bool in
|
||||
let iterator = self.fileManager.enumerator(at: self.url(of: path), includingPropertiesForKeys: nil, options: recursive ? [] : [.skipsSubdirectoryDescendants, .skipsPackageDescendants]) { (url, e) -> Bool in
|
||||
completionHandler([], e)
|
||||
return true
|
||||
}
|
||||
var result = [LocalFileObject]()
|
||||
while let fileURL = iterator?.nextObject() as? URL {
|
||||
if fileURL.lastPathComponent.lowercased().contains(query.lowercased()) {
|
||||
let fileObject = self.attributesOfItem(url: fileURL)
|
||||
result.append(self.attributesOfItem(url: fileURL))
|
||||
let path = self.relativePathOf(url: fileURL)
|
||||
if let fileObject = LocalFileObject(fileWithPath: path, relativeTo: self.baseURL), query.evaluate(with: fileObject.mapPredicate()) {
|
||||
result.append(fileObject)
|
||||
foundItemHandler?(fileObject)
|
||||
}
|
||||
}
|
||||
@@ -329,20 +164,321 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
}
|
||||
}
|
||||
|
||||
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
|
||||
dispatch_queue.async {
|
||||
completionHandler(self.fileManager.isReadableFile(atPath: self.baseURL!.path))
|
||||
}
|
||||
}
|
||||
|
||||
open weak var fileOperationDelegate : FileOperationDelegate?
|
||||
|
||||
@discardableResult
|
||||
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(folderName) + "/")
|
||||
return self.doOperation(opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func create(file fileName: String, at atPath: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let fileName = fileName.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||||
let path = (atPath as NSString).appendingPathComponent(fileName)
|
||||
let opType = FileOperationType.create(path: path)
|
||||
|
||||
return self.doOperation(opType, data: data, atomically: true, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.move(source: path, destination: toPath)
|
||||
|
||||
if !overwrite && self.fileManager.fileExists(atPath: self.url(of: toPath).path) {
|
||||
completionHandler?(self.throwError(toPath, code: CocoaError.fileWriteFileExists as FoundationErrorEnum))
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.doOperation(opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.copy(source: path, destination: toPath)
|
||||
|
||||
if !overwrite && self.fileManager.fileExists(atPath: self.url(of: toPath).path) {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(self.throwError(toPath, code: CocoaError.fileWriteFileExists as FoundationErrorEnum))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.doOperation(opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.remove(path: path)
|
||||
return self.doOperation(opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
if !overwrite && self.fileManager.fileExists(atPath: self.url(of: toPath).path) {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(self.throwError(toPath, code: CocoaError.fileWriteFileExists as FoundationErrorEnum))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
|
||||
return self.doOperation(opType, forUploading: true, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
|
||||
return self.doOperation(opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
dynamic func doSimpleOperation(_ box: UndoBox) {
|
||||
guard let _ = self.undoManager else { return }
|
||||
_ = self.doOperation(box.undoOperation) { (_) in
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
fileprivate func doOperation(_ opType: FileOperationType, data: Data? = nil, atomically: Bool = false, forUploading: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
|
||||
func urlofpath(path: String) -> URL {
|
||||
if path.hasPrefix("file://") {
|
||||
let removedSchemePath = path.replacingOccurrences(of: "file://", with: "", options: .anchored)
|
||||
let pDecodedPath = removedSchemePath.removingPercentEncoding ?? removedSchemePath
|
||||
return URL(fileURLWithPath: pDecodedPath)
|
||||
} else {
|
||||
return self.url(of: path)
|
||||
}
|
||||
}
|
||||
|
||||
guard let sourcePath = opType.source else { return nil }
|
||||
let destPath = opType.destination
|
||||
let source: URL = urlofpath(path: sourcePath)
|
||||
|
||||
let dest: URL?
|
||||
if let destPath = destPath {
|
||||
dest = urlofpath(path: destPath)
|
||||
} else {
|
||||
dest = nil
|
||||
}
|
||||
|
||||
if let undoManager = self.undoManager, let undoOp = self.undoOperation(for: opType) {
|
||||
let undoBox = UndoBox(provider: self, operation: opType, undoOperation: undoOp)
|
||||
undoManager.beginUndoGrouping()
|
||||
undoManager.registerUndo(withTarget: self, selector: #selector(LocalFileProvider.doSimpleOperation(_:)), object: undoBox)
|
||||
undoManager.setActionName(opType.actionDescription)
|
||||
undoManager.endUndoGrouping()
|
||||
}
|
||||
|
||||
var successfulSecurityScopedResourceAccess = false
|
||||
|
||||
let operationHandler: (URL, URL?) -> Void = { source, dest in
|
||||
do {
|
||||
switch opType {
|
||||
case .create:
|
||||
if sourcePath.hasSuffix("/") {
|
||||
try self.opFileManager.createDirectory(at: source, withIntermediateDirectories: true, attributes: [:])
|
||||
} else {
|
||||
try data?.write(to: source, options: Data.WritingOptions.atomic)
|
||||
}
|
||||
case .modify:
|
||||
try data?.write(to: source, options: atomically ? [.atomic] : [])
|
||||
case .copy:
|
||||
guard let dest = dest else { return }
|
||||
try self.opFileManager.copyItem(at: source, to: dest)
|
||||
case .move:
|
||||
guard let dest = dest else { return }
|
||||
try self.opFileManager.moveItem(at: source, to: dest)
|
||||
case.remove:
|
||||
try self.opFileManager.removeItem(at: source)
|
||||
default:
|
||||
return
|
||||
}
|
||||
if successfulSecurityScopedResourceAccess {
|
||||
source.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(nil)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
}
|
||||
} catch let e {
|
||||
if successfulSecurityScopedResourceAccess {
|
||||
source.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(e)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isCoorinating {
|
||||
successfulSecurityScopedResourceAccess = source.startAccessingSecurityScopedResource()
|
||||
var intents = [NSFileAccessIntent]()
|
||||
switch opType {
|
||||
case .create, .modify:
|
||||
intents.append(NSFileAccessIntent.writingIntent(with: source, options: .forReplacing))
|
||||
case .copy:
|
||||
guard let dest = dest else { return nil }
|
||||
intents.append(NSFileAccessIntent.readingIntent(with: source, options: forUploading ? .forUploading : .withoutChanges))
|
||||
intents.append(NSFileAccessIntent.writingIntent(with: dest, options: .forReplacing))
|
||||
case .move:
|
||||
guard let dest = dest else { return nil }
|
||||
intents.append(NSFileAccessIntent.writingIntent(with: source, options: .forMoving))
|
||||
intents.append(NSFileAccessIntent.writingIntent(with: dest, options: .forReplacing))
|
||||
case .remove:
|
||||
intents.append(NSFileAccessIntent.writingIntent(with: source, options: .forDeleting))
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
self.coordinated(intents: intents, completionHandler: operationHandler, errorHandler: { error in
|
||||
self.dispatch_queue.async {
|
||||
completionHandler?(error)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
operation_queue.addOperation {
|
||||
operationHandler(source, dest)
|
||||
}
|
||||
}
|
||||
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
let opType = FileOperationType.fetch(path: path)
|
||||
let url = self.url(of: path)
|
||||
|
||||
let operationHandler: (URL) -> Void = { url in
|
||||
do {
|
||||
let data = try Data(contentsOf: url)
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(data, nil)
|
||||
}
|
||||
} catch let e {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isCoorinating {
|
||||
let intent = NSFileAccessIntent.readingIntent(with: url, options: .withoutChanges)
|
||||
coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
dispatch_queue.async {
|
||||
operationHandler(url)
|
||||
}
|
||||
}
|
||||
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open 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
|
||||
}
|
||||
|
||||
if offset == 0 && length < 0 {
|
||||
return self.contents(path: path, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
let opType = FileOperationType.fetch(path: path)
|
||||
let url = self.url(of: path)
|
||||
|
||||
let operationHandler: (URL) -> Void = { url in
|
||||
guard let handle = FileHandle(forReadingAtPath: url.path) else {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, self.throwError(path, code: CocoaError.fileNoSuchFile as FoundationErrorEnum))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
defer {
|
||||
handle.closeFile()
|
||||
}
|
||||
|
||||
let size = LocalFileObject(fileWithURL: url)?.size ?? -1
|
||||
guard size > offset else {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadTooLarge as FoundationErrorEnum))
|
||||
}
|
||||
return
|
||||
}
|
||||
handle.seek(toFileOffset: UInt64(offset))
|
||||
guard Int64(handle.offsetInFile) == offset else {
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadTooLarge as FoundationErrorEnum))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let data = handle.readData(ofLength: length)
|
||||
|
||||
self.dispatch_queue.async {
|
||||
completionHandler(data, nil)
|
||||
}
|
||||
}
|
||||
|
||||
if isCoorinating {
|
||||
let intent = NSFileAccessIntent.readingIntent(with: url, options: .withoutChanges)
|
||||
coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
|
||||
completionHandler(nil, error)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
dispatch_queue.async {
|
||||
operationHandler(url)
|
||||
}
|
||||
}
|
||||
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.modify(path: path)
|
||||
return self.doOperation(opType, data: data, atomically: atomically, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
fileprivate var monitors = [LocalFolderMonitor]()
|
||||
|
||||
open func registerNotifcation(path: String, eventHandler: @escaping (() -> Void)) {
|
||||
self.unregisterNotifcation(path: path)
|
||||
let absurl = self.absoluteURL(path)
|
||||
var isdirv: AnyObject?
|
||||
do {
|
||||
try (absurl as NSURL).getResourceValue(&isdirv, forKey: URLResourceKey.isDirectoryKey)
|
||||
} catch _ {
|
||||
}
|
||||
if !(isdirv?.boolValue ?? false) {
|
||||
let dirurl = self.url(of: path)
|
||||
let isdir = (try? dirurl.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false) ?? false
|
||||
if !isdir {
|
||||
return
|
||||
}
|
||||
let monitor = LocalFolderMonitor(url: absurl) {
|
||||
let monitor = LocalFolderMonitor(url: dirurl) {
|
||||
eventHandler()
|
||||
}
|
||||
monitor.start()
|
||||
@@ -361,157 +497,95 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
}
|
||||
|
||||
open func isRegisteredForNotification(path: String) -> Bool {
|
||||
return monitors.map( { self.relativePathOf(url: $0.url) } ).contains(path)
|
||||
return monitors.map( { self.relativePathOf(url: $0.url) } ).contains(path.trimmingCharacters(in: CharacterSet(charactersIn: "/")))
|
||||
}
|
||||
|
||||
open func copy(with zone: NSZone? = nil) -> Any {
|
||||
let copy = LocalFileProvider(baseURL: self.baseURL!)
|
||||
copy.currentPath = self.currentPath
|
||||
copy.undoManager = self.undoManager
|
||||
copy.isCoorinating = self.isCoorinating
|
||||
copy.delegate = self.delegate
|
||||
copy.fileOperationDelegate = self.fileOperationDelegate
|
||||
return copy
|
||||
}
|
||||
}
|
||||
|
||||
public extension LocalFileProvider {
|
||||
/**
|
||||
Creates a symbolic link at the specified path that points to an item at the given path.
|
||||
This method does not traverse symbolic links contained in destURL, making it possible
|
||||
to create symbolic links to locations that do not yet exist.
|
||||
Also, if the final path component in url is a symbolic link, that link is not followed.
|
||||
|
||||
- Parameters:
|
||||
- path: The file path at which to create the new symbolic link. The last component of the path issued as the name of the link.
|
||||
- destPath: The path that contains the item to be pointed to by the link. In other words, this is the destination of the link.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
*/
|
||||
public func create(symbolicLink path: String, withDestinationPath destPath: String, completionHandler: SimpleCompletionHandler) {
|
||||
operation_queue.async {
|
||||
operation_queue.addOperation {
|
||||
do {
|
||||
try self.opFileManager.createSymbolicLink(at: self.absoluteURL(path), withDestinationURL: self.absoluteURL(destPath))
|
||||
try self.opFileManager.createSymbolicLink(at: self.url(of: path), withDestinationURL: self.url(of: destPath))
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async(execute: {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderSucceed(self, operation: .link(link: path, target: destPath))
|
||||
})
|
||||
} catch let e as NSError {
|
||||
}
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async(execute: {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: .link(link: path, target: destPath))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the path of the item pointed to by a symbolic link.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - path: The path of a file or directory.
|
||||
/// - completionHandler: Returns destination url of given symbolic link, or an `Error` object if it fails.
|
||||
public func destination(ofSymbolicLink path: String, completionHandler: @escaping (_ url: URL?, _ error: Error?) -> Void) {
|
||||
dispatch_queue.async {
|
||||
do {
|
||||
let destPath = try self.opFileManager.destinationOfSymbolicLink(atPath: self.url(of: path).path)
|
||||
let destUrl = URL(fileURLWithPath: destPath)
|
||||
completionHandler(destUrl, nil)
|
||||
} catch let e{
|
||||
completionHandler(nil, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class LocalFileProviderManagerDelegate: NSObject, FileManagerDelegate {
|
||||
weak var provider: LocalFileProvider?
|
||||
|
||||
init(provider: LocalFileProvider) {
|
||||
self.provider = provider
|
||||
}
|
||||
|
||||
func fileManager(_ fileManager: FileManager, shouldCopyItemAt srcURL: URL, to dstURL: URL) -> Bool {
|
||||
guard let provider = self.provider, let delegate = provider.fileOperationDelegate else {
|
||||
return true
|
||||
}
|
||||
let srcPath = provider.relativePathOf(url: srcURL)
|
||||
let dstPath = provider.relativePathOf(url: dstURL)
|
||||
return delegate.fileProvider(provider, shouldDoOperation: .copy(source: srcPath, destination: dstPath))
|
||||
}
|
||||
|
||||
func fileManager(_ fileManager: FileManager, shouldMoveItemAt srcURL: URL, to dstURL: URL) -> Bool {
|
||||
guard let provider = self.provider, let delegate = provider.fileOperationDelegate else {
|
||||
return true
|
||||
}
|
||||
let srcPath = provider.relativePathOf(url: srcURL)
|
||||
let dstPath = provider.relativePathOf(url: dstURL)
|
||||
return delegate.fileProvider(provider, shouldDoOperation: .move(source: srcPath, destination: dstPath))
|
||||
}
|
||||
|
||||
func fileManager(_ fileManager: FileManager, shouldRemoveItemAt URL: URL) -> Bool {
|
||||
guard let provider = self.provider, let delegate = provider.fileOperationDelegate else {
|
||||
return true
|
||||
}
|
||||
let path = provider.relativePathOf(url: URL)
|
||||
return delegate.fileProvider(provider, shouldDoOperation: .remove(path: path))
|
||||
}
|
||||
|
||||
func fileManager(_ fileManager: FileManager, shouldLinkItemAt srcURL: URL, to dstURL: URL) -> Bool {
|
||||
guard let provider = self.provider, let delegate = provider.fileOperationDelegate else {
|
||||
return true
|
||||
}
|
||||
let srcPath = provider.relativePathOf(url: srcURL)
|
||||
let dstPath = provider.relativePathOf(url: dstURL)
|
||||
return delegate.fileProvider(provider, shouldDoOperation: .link(link: srcPath, target: dstPath))
|
||||
}
|
||||
|
||||
func fileManager(_ fileManager: FileManager, shouldProceedAfterError error: Error, copyingItemAt srcURL: URL, to dstURL: URL) -> Bool {
|
||||
guard let provider = self.provider, let delegate = provider.fileOperationDelegate else {
|
||||
return false
|
||||
}
|
||||
let srcPath = provider.relativePathOf(url: srcURL)
|
||||
let dstPath = provider.relativePathOf(url: dstURL)
|
||||
return delegate.fileProvider(provider, shouldProceedAfterError: error, operation: .copy(source: srcPath, destination: dstPath))
|
||||
}
|
||||
|
||||
func fileManager(_ fileManager: FileManager, shouldProceedAfterError error: Error, movingItemAt srcURL: URL, to dstURL: URL) -> Bool {
|
||||
guard let provider = self.provider, let delegate = provider.fileOperationDelegate else {
|
||||
return false
|
||||
}
|
||||
let srcPath = provider.relativePathOf(url: srcURL)
|
||||
let dstPath = provider.relativePathOf(url: dstURL)
|
||||
return delegate.fileProvider(provider, shouldProceedAfterError: error, operation: .move(source: srcPath, destination: dstPath))
|
||||
}
|
||||
|
||||
func fileManager(_ fileManager: FileManager, shouldProceedAfterError error: Error, removingItemAt URL: URL) -> Bool {
|
||||
guard let provider = self.provider, let delegate = provider.fileOperationDelegate else {
|
||||
return false
|
||||
}
|
||||
let path = provider.relativePathOf(url: URL)
|
||||
return delegate.fileProvider(provider, shouldProceedAfterError: error, operation: .remove(path: path))
|
||||
}
|
||||
|
||||
func fileManager(_ fileManager: FileManager, shouldProceedAfterError error: Error, linkingItemAt srcURL: URL, to dstURL: URL) -> Bool {
|
||||
guard let provider = self.provider, let delegate = provider.fileOperationDelegate else {
|
||||
return false
|
||||
}
|
||||
let srcPath = provider.relativePathOf(url: srcURL)
|
||||
let dstPath = provider.relativePathOf(url: dstURL)
|
||||
return delegate.fileProvider(provider, shouldProceedAfterError: error, operation: .link(link: srcPath, target: dstPath))
|
||||
}
|
||||
}
|
||||
|
||||
internal class LocalFolderMonitor {
|
||||
fileprivate let source: DispatchSourceFileSystemObject
|
||||
fileprivate let descriptor: CInt
|
||||
fileprivate let qq: DispatchQueue = DispatchQueue.global(qos: DispatchQoS.QoSClass.default)
|
||||
fileprivate var state: Bool = false
|
||||
fileprivate var monitoredTime: TimeInterval = Date().timeIntervalSinceReferenceDate
|
||||
var url: URL
|
||||
|
||||
/// Creates a folder monitor object with monitoring enabled.
|
||||
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)
|
||||
// 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 = {
|
||||
if Date().timeIntervalSinceReferenceDate < self.monitoredTime + 0.2 {
|
||||
internal extension LocalFileProvider {
|
||||
func coordinated(intents: [NSFileAccessIntent], completionHandler: @escaping (_ url: URL) -> Void, errorHandler: ((_ error: Error) -> Void)? = nil) {
|
||||
let coordinator = NSFileCoordinator(filePresenter: nil)
|
||||
coordinator.coordinate(with: intents, queue: operation_queue) { (error) in
|
||||
if let error = error {
|
||||
errorHandler?(error)
|
||||
return
|
||||
}
|
||||
self.monitoredTime = Date().timeIntervalSinceReferenceDate
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.25, execute: {
|
||||
handler()
|
||||
})
|
||||
}
|
||||
source.setEventHandler(handler: main_handler)
|
||||
source.setCancelHandler {
|
||||
close(self.descriptor)
|
||||
}
|
||||
start()
|
||||
}
|
||||
|
||||
/// Starts sending notifications if currently stopped
|
||||
func start() {
|
||||
if !state {
|
||||
state = true
|
||||
source.resume()
|
||||
completionHandler(intents.first!.url)
|
||||
}
|
||||
}
|
||||
|
||||
/// Stops sending notifications if currently enabled
|
||||
func stop() {
|
||||
if state {
|
||||
state = false
|
||||
source.suspend()
|
||||
func coordinated(intents: [NSFileAccessIntent], moving: Bool = false, completionHandler: @escaping (_ sourceUrl: URL, _ destURL: URL?) -> Void, errorHandler: ((_ error: Error) -> Void)? = nil) {
|
||||
let coordinator = NSFileCoordinator(filePresenter: nil)
|
||||
coordinator.coordinate(with: intents, queue: operation_queue) { (error) in
|
||||
if let error = error {
|
||||
errorHandler?(error)
|
||||
return
|
||||
}
|
||||
guard let newSource: URL = intents.first?.url else { return }
|
||||
let newDest: URL? = intents.dropFirst().first?.url
|
||||
if moving, let newDest = newDest {
|
||||
coordinator.item(at: newSource, willMoveTo: newDest)
|
||||
}
|
||||
completionHandler(newSource, newDest)
|
||||
if moving, let newDest = newDest {
|
||||
coordinator.item(at: newSource, didMoveTo: newDest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
source.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
//
|
||||
// LocalFileProvider.swift
|
||||
// FileProvider
|
||||
//
|
||||
// Created by Amir Abbas Mousavian.
|
||||
// Copyright © 2016 Mousavian. Distributed under MIT license.
|
||||
//
|
||||
|
||||
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) {
|
||||
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?.path ?? "", with: "", options: .anchored)
|
||||
if relativeURL != nil && rpath.hasPrefix("/") {
|
||||
rpath.remove(at: rpath.startIndex)
|
||||
}
|
||||
if rpath.isEmpty {
|
||||
fileURL = 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)
|
||||
}
|
||||
}
|
||||
if let fileURL = fileURL {
|
||||
self.init(fileWithURL: fileURL)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Initiates a `LocalFileObject` with attributes of file in url.
|
||||
public convenience init?(fileWithURL fileURL: URL) {
|
||||
do {
|
||||
let values = try fileURL.resourceValues(forKeys: [.nameKey, .fileSizeKey, .fileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .fileResourceTypeKey, .isHiddenKey, .isWritableKey, .typeIdentifierKey, .generationIdentifierKey, .documentIdentifierKey])
|
||||
let 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] = value
|
||||
}
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// The total size allocated on disk for the file
|
||||
open internal(set) var allocatedSize: Int64 {
|
||||
get {
|
||||
return allValues[.fileAllocatedSizeKey] as? Int64 ?? 0
|
||||
}
|
||||
set {
|
||||
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[.generationIdentifierKey] as? Data
|
||||
return data?.map { String(format: "%02hhx", $0) }.joined()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal final class LocalFolderMonitor {
|
||||
fileprivate let source: DispatchSourceFileSystemObject
|
||||
fileprivate let descriptor: CInt
|
||||
fileprivate let qq: DispatchQueue = DispatchQueue.global(qos: .default)
|
||||
fileprivate var state: Bool = false
|
||||
fileprivate var monitoredTime: TimeInterval = Date().timeIntervalSinceReferenceDate
|
||||
var url: URL
|
||||
|
||||
/// Creates a folder monitor object with monitoring enabled.
|
||||
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)
|
||||
// Folder monitoring is recursive and deep. Monitoring a root folder may be very costly
|
||||
// We have a 0.2 second delay to ensure we wont call handler 1000s times when there is
|
||||
// a huge file operation. This ensures app will work smoothly while this 250 milisec won't
|
||||
// affect user experince much
|
||||
let main_handler: ()->Void = { [weak self] in
|
||||
guard let `self` = self else { return }
|
||||
if Date().timeIntervalSinceReferenceDate < self.monitoredTime + 0.2 {
|
||||
return
|
||||
}
|
||||
self.monitoredTime = Date().timeIntervalSinceReferenceDate
|
||||
self.source.suspend()
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.25, execute: {
|
||||
handler()
|
||||
self.source.resume()
|
||||
})
|
||||
}
|
||||
source.setEventHandler(handler: main_handler)
|
||||
source.setCancelHandler {
|
||||
close(self.descriptor)
|
||||
}
|
||||
start()
|
||||
}
|
||||
|
||||
/// Starts sending notifications if currently stopped
|
||||
func start() {
|
||||
if !state {
|
||||
state = true
|
||||
source.resume()
|
||||
}
|
||||
}
|
||||
|
||||
/// Stops sending notifications if currently enabled
|
||||
func stop() {
|
||||
if state {
|
||||
state = false
|
||||
source.suspend()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
source.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
internal class LocalFileProviderManagerDelegate: NSObject, FileManagerDelegate {
|
||||
weak var provider: LocalFileProvider?
|
||||
|
||||
init(provider: LocalFileProvider) {
|
||||
self.provider = provider
|
||||
}
|
||||
|
||||
func fileManager(_ fileManager: FileManager, shouldCopyItemAt srcURL: URL, to dstURL: URL) -> Bool {
|
||||
guard let provider = self.provider, let delegate = provider.fileOperationDelegate else {
|
||||
return true
|
||||
}
|
||||
let srcPath = provider.relativePathOf(url: srcURL)
|
||||
let dstPath = provider.relativePathOf(url: dstURL)
|
||||
return delegate.fileProvider(provider, shouldDoOperation: .copy(source: srcPath, destination: dstPath))
|
||||
}
|
||||
|
||||
func fileManager(_ fileManager: FileManager, shouldMoveItemAt srcURL: URL, to dstURL: URL) -> Bool {
|
||||
guard let provider = self.provider, let delegate = provider.fileOperationDelegate else {
|
||||
return true
|
||||
}
|
||||
let srcPath = provider.relativePathOf(url: srcURL)
|
||||
let dstPath = provider.relativePathOf(url: dstURL)
|
||||
return delegate.fileProvider(provider, shouldDoOperation: .move(source: srcPath, destination: dstPath))
|
||||
}
|
||||
|
||||
func fileManager(_ fileManager: FileManager, shouldRemoveItemAt URL: URL) -> Bool {
|
||||
guard let provider = self.provider, let delegate = provider.fileOperationDelegate else {
|
||||
return true
|
||||
}
|
||||
let path = provider.relativePathOf(url: URL)
|
||||
return delegate.fileProvider(provider, shouldDoOperation: .remove(path: path))
|
||||
}
|
||||
|
||||
func fileManager(_ fileManager: FileManager, shouldLinkItemAt srcURL: URL, to dstURL: URL) -> Bool {
|
||||
guard let provider = self.provider, let delegate = provider.fileOperationDelegate else {
|
||||
return true
|
||||
}
|
||||
let srcPath = provider.relativePathOf(url: srcURL)
|
||||
let dstPath = provider.relativePathOf(url: dstURL)
|
||||
return delegate.fileProvider(provider, shouldDoOperation: .link(link: srcPath, target: dstPath))
|
||||
}
|
||||
|
||||
func fileManager(_ fileManager: FileManager, shouldProceedAfterError error: Error, copyingItemAt srcURL: URL, to dstURL: URL) -> Bool {
|
||||
guard let provider = self.provider, let delegate = provider.fileOperationDelegate else {
|
||||
return false
|
||||
}
|
||||
let srcPath = provider.relativePathOf(url: srcURL)
|
||||
let dstPath = provider.relativePathOf(url: dstURL)
|
||||
return delegate.fileProvider(provider, shouldProceedAfterError: error, operation: .copy(source: srcPath, destination: dstPath))
|
||||
}
|
||||
|
||||
func fileManager(_ fileManager: FileManager, shouldProceedAfterError error: Error, movingItemAt srcURL: URL, to dstURL: URL) -> Bool {
|
||||
guard let provider = self.provider, let delegate = provider.fileOperationDelegate else {
|
||||
return false
|
||||
}
|
||||
let srcPath = provider.relativePathOf(url: srcURL)
|
||||
let dstPath = provider.relativePathOf(url: dstURL)
|
||||
return delegate.fileProvider(provider, shouldProceedAfterError: error, operation: .move(source: srcPath, destination: dstPath))
|
||||
}
|
||||
|
||||
func fileManager(_ fileManager: FileManager, shouldProceedAfterError error: Error, removingItemAt URL: URL) -> Bool {
|
||||
guard let provider = self.provider, let delegate = provider.fileOperationDelegate else {
|
||||
return false
|
||||
}
|
||||
let path = provider.relativePathOf(url: URL)
|
||||
return delegate.fileProvider(provider, shouldProceedAfterError: error, operation: .remove(path: path))
|
||||
}
|
||||
|
||||
func fileManager(_ fileManager: FileManager, shouldProceedAfterError error: Error, linkingItemAt srcURL: URL, to dstURL: URL) -> Bool {
|
||||
guard let provider = self.provider, let delegate = provider.fileOperationDelegate else {
|
||||
return false
|
||||
}
|
||||
let srcPath = provider.relativePathOf(url: srcURL)
|
||||
let dstPath = provider.relativePathOf(url: dstURL)
|
||||
return delegate.fileProvider(provider, shouldProceedAfterError: error, operation: .link(link: srcPath, target: dstPath))
|
||||
}
|
||||
}
|
||||
|
||||
/// Local operation handling is limited. Please don't use as much as possible.
|
||||
open class LocalOperationHandle: OperationHandle {
|
||||
public let baseURL: URL
|
||||
public let operationType: FileOperationType
|
||||
|
||||
init (operationType: FileOperationType, baseURL: URL?) {
|
||||
self.baseURL = baseURL ?? URL(fileURLWithPath: "/")
|
||||
self.operationType = operationType
|
||||
}
|
||||
|
||||
private var sourceURL: URL? {
|
||||
guard let source = operationType.source else { return nil }
|
||||
return source.hasPrefix("file://") ? URL(fileURLWithPath: source) : baseURL.appendingPathComponent(source)
|
||||
}
|
||||
|
||||
private var destURL: URL? {
|
||||
guard let dest = operationType.destination else { return nil }
|
||||
return dest.hasPrefix("file://") ? URL(fileURLWithPath: dest) : baseURL.appendingPathComponent(dest)
|
||||
}
|
||||
|
||||
/// Caution: may put pressure on CPU, may have latency
|
||||
open var bytesSoFar: Int64 {
|
||||
assert(!Thread.isMainThread, "Don't run \(#function) method on main thread")
|
||||
switch operationType {
|
||||
case .modify:
|
||||
guard let url = sourceURL, url.isFileURL else { return 0 }
|
||||
if url.fileIsDirectory {
|
||||
return iterateDirectory(url, deep: true).totalsize
|
||||
} else {
|
||||
return url.fileSize
|
||||
}
|
||||
case .copy, .move:
|
||||
guard let url = destURL, url.isFileURL else { return 0 }
|
||||
if url.fileIsDirectory {
|
||||
return iterateDirectory(url, deep: true).totalsize
|
||||
} else {
|
||||
return url.fileSize
|
||||
}
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Caution: may put pressure on CPU, may have latency
|
||||
open var totalBytes: Int64 {
|
||||
assert(!Thread.isMainThread, "Don't run \(#function) method on main thread")
|
||||
switch operationType {
|
||||
case .copy, .move:
|
||||
guard let url = sourceURL, url.isFileURL else { return 0 }
|
||||
if url.fileIsDirectory {
|
||||
return iterateDirectory(url, deep: true).totalsize
|
||||
} else {
|
||||
return url.fileSize
|
||||
}
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/// Not usable in local provider
|
||||
open var inProgress: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
/// Not usable in local provider
|
||||
open func cancel() -> Bool{
|
||||
return false
|
||||
}
|
||||
|
||||
func iterateDirectory(_ pathURL: URL, deep: Bool) -> (folders: Int, files: Int, totalsize: Int64) {
|
||||
var folders = 0
|
||||
var files = 0
|
||||
var totalsize: Int64 = 0
|
||||
let keys: [URLResourceKey] = [.isDirectoryKey, .fileSizeKey]
|
||||
let enumOpt: FileManager.DirectoryEnumerationOptions = !deep ? [.skipsSubdirectoryDescendants, .skipsPackageDescendants] : []
|
||||
|
||||
let fp = FileManager()
|
||||
let filesList = fp.enumerator(at: pathURL, includingPropertiesForKeys: keys, options: enumOpt, errorHandler: nil)
|
||||
while let fileURL = filesList?.nextObject() as? URL {
|
||||
do {
|
||||
let values = try fileURL.resourceValues(forKeys: [.isDirectoryKey, .fileSizeKey])
|
||||
let isdir = values.isDirectory ?? false
|
||||
let size = Int64(values.fileSize ?? 0)
|
||||
if isdir {
|
||||
folders += 1
|
||||
} else {
|
||||
files += 1
|
||||
}
|
||||
totalsize += size
|
||||
} catch _ {
|
||||
}
|
||||
}
|
||||
|
||||
return (folders, files, totalsize)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class UndoBox: NSObject {
|
||||
weak var provider: FileProvideUndoable?
|
||||
let operation: FileOperationType
|
||||
let undoOperation: FileOperationType
|
||||
|
||||
init(provider: FileProvideUndoable, operation: FileOperationType, undoOperation: FileOperationType) {
|
||||
self.provider = provider
|
||||
self.operation = operation
|
||||
self.undoOperation = undoOperation
|
||||
}
|
||||
}
|
||||
|
||||
internal extension URL {
|
||||
var fileIsDirectory: Bool {
|
||||
return (try? self.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory ?? false
|
||||
}
|
||||
|
||||
var fileSize: Int64 {
|
||||
return Int64((try? self.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? -1)
|
||||
}
|
||||
|
||||
var fileExists: Bool {
|
||||
return self.isFileURL && FileManager.default.fileExists(atPath: self.path)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
|
||||
//
|
||||
// OneDriveFileProvider.swift
|
||||
// FileProvider
|
||||
//
|
||||
// Created by Amir Abbas Mousavian.
|
||||
// Copyright © 2017 Mousavian. Distributed under MIT license.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
|
||||
/**
|
||||
Allows accessing to OneDrive stored files, either hosted on Microsoft servers or business coprporate one.
|
||||
This provider doesn't cache or save files internally, however you can set `useCache` and `cache` properties
|
||||
to use Foundation `NSURLCache` system.
|
||||
|
||||
- Note: Uploading files and data are limited to 100MB, for now.
|
||||
*/
|
||||
open class OneDriveFileProvider: FileProviderBasicRemote {
|
||||
open class var type: String { return "OneDrive" }
|
||||
open let baseURL: URL?
|
||||
/// 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 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.
|
||||
*/
|
||||
public init(credential: URLCredential?, serverURL: URL? = nil, drive: String = "root", cache: URLCache? = nil) {
|
||||
let baseURL = serverURL ?? URL(string: "https://api.onedrive.com/")!
|
||||
self.baseURL = baseURL.path.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")
|
||||
self.drive = drive
|
||||
self.currentPath = ""
|
||||
self.useCache = false
|
||||
self.validatingCache = true
|
||||
self.cache = cache
|
||||
self.credential = credential
|
||||
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: .concurrent)
|
||||
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)) {
|
||||
var request = URLRequest(url: url(of: path))
|
||||
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)) {
|
||||
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 func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
|
||||
var foundFiles = [OneDriveFileObject]()
|
||||
var queryStr: String?
|
||||
queryStr = query.findValue(forKey: "name") as? String ?? query.findAllValues(forKey: nil).flatMap { $0.value as? String }.first
|
||||
guard let finalQueryStr = queryStr else { return }
|
||||
search(path, query: finalQueryStr, foundItem: { (file) in
|
||||
if query.evaluate(with: file.mapPredicate()) {
|
||||
foundFiles.append(file)
|
||||
foundItemHandler?(file)
|
||||
}
|
||||
}, completionHandler: { (error) in
|
||||
completionHandler(foundFiles, error)
|
||||
})
|
||||
}
|
||||
|
||||
open func url(of path: String? = nil, modifier: String? = nil) -> URL {
|
||||
var rpath: String
|
||||
if let path = path {
|
||||
rpath = path
|
||||
} else {
|
||||
rpath = self.currentPath
|
||||
}
|
||||
|
||||
if rpath.hasPrefix("/") {
|
||||
rpath.remove(at: rpath.startIndex)
|
||||
}
|
||||
if rpath.isEmpty {
|
||||
if let modifier = modifier {
|
||||
return baseURL!.appendingPathComponent("drive/\(drive)/\(modifier)")
|
||||
}
|
||||
return baseURL!.appendingPathComponent("drive/\(drive)")
|
||||
}
|
||||
let driveURL = baseURL!.appendingPathComponent("drive/\(drive):/")
|
||||
rpath = (rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? rpath)
|
||||
rpath = rpath.trimmingCharacters(in: pathTrimSet)
|
||||
if let modifier = modifier {
|
||||
rpath = rpath + ":/" + modifier
|
||||
}
|
||||
return URL(string: rpath, relativeTo: driveURL) ?? driveURL
|
||||
}
|
||||
|
||||
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
|
||||
var request = URLRequest(url: url())
|
||||
request.httpMethod = "HEAD"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
let status = (response as? HTTPURLResponse)?.statusCode ?? 400
|
||||
completionHandler(status == 200)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
|
||||
open weak var fileOperationDelegate: FileOperationDelegate?
|
||||
}
|
||||
|
||||
extension OneDriveFileProvider: FileProviderOperations {
|
||||
|
||||
|
||||
open 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)
|
||||
}
|
||||
|
||||
open 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)
|
||||
}
|
||||
|
||||
open func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
open func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
return doOperation(.copy(source: path, destination: toPath), completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
open 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(of: sourcePath))
|
||||
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])
|
||||
}
|
||||
|
||||
open 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)
|
||||
}
|
||||
|
||||
open 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
|
||||
}
|
||||
var request = URLRequest(url: self.url(of: path, modifier: "content"))
|
||||
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 {
|
||||
open 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)
|
||||
var request = URLRequest(url: self.url(of: path, modifier: "content"))
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
if length > 0 {
|
||||
request.setValue("bytes=\(offset)-\(offset + 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])
|
||||
}
|
||||
|
||||
open func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.modify(path: path)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
// FIXME: remove 150MB restriction
|
||||
return upload_simple(path, data: data, overwrite: overwrite, operation: opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
fileprivate func registerNotifcation(path: String, eventHandler: (() -> Void)) {
|
||||
/* There is two ways to monitor folders changing in OneDrive. Either using webooks
|
||||
* which means you have to implement a server to translate it to push notifications
|
||||
* or using apiv2 list_folder/longpoll method. The second one is implemeted here.
|
||||
* Tough webhooks are much more efficient, longpoll is much simpler to implement!
|
||||
* You can implemnt your own webhook service and replace this method accordingly.
|
||||
*/
|
||||
NotImplemented()
|
||||
}
|
||||
fileprivate func unregisterNotifcation(path: String) {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
Genrates a public url to a file to be shared with other users and can be downloaded without authentication.
|
||||
|
||||
- Parameters:
|
||||
- to: path of file, including file/directory name.
|
||||
- completionHandler: a closure with result of directory entries or error.
|
||||
`link`: a url returned by OneDrive to share.
|
||||
`attribute`: `nil` for OneDrive.
|
||||
`expiration`: `nil` for OneDrive, as it doesn't expires.
|
||||
`error`: Error returned by OneDrive.
|
||||
*/
|
||||
open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: OneDriveFileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
|
||||
var request = URLRequest(url: self.url(of: path, modifier: "action.createLink"))
|
||||
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 {
|
||||
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.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()
|
||||
}
|
||||
|
||||
open func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) {
|
||||
var request = URLRequest(url: url(of: path))
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
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,290 @@
|
||||
//
|
||||
// OneDriveHelper.swift
|
||||
// FileProvider
|
||||
//
|
||||
// Created by Amir Abbas Mousavian.
|
||||
// Copyright © 2017 Mousavian. Distributed under MIT license.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Error returned by OneDrive server when trying to access or do operations on a file or folder.
|
||||
public struct FileProviderOneDriveError: FileProviderHTTPError {
|
||||
public let code: FileProviderHTTPErrorCode
|
||||
public let path: String
|
||||
public let errorDescription: String?
|
||||
}
|
||||
|
||||
/// Containts path, url and attributes of a OneDrive file or resource.
|
||||
public final class OneDriveFileObject: FileObject {
|
||||
internal init(baseURL: URL?, name: String, path: String) {
|
||||
var rpath = path
|
||||
if path.hasPrefix("/") {
|
||||
rpath.remove(at: rpath.startIndex)
|
||||
}
|
||||
let url = URL(string: rpath, relativeTo: baseURL) ?? URL(string: path)!
|
||||
super.init(url: url, name: name, path: 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?, drive: String, json: [String: AnyObject]) {
|
||||
guard let name = json["name"] as? String else { return nil }
|
||||
guard let path = (json["parentReference"] as? NSDictionary)?["path"] as? String else { return nil }
|
||||
var lPath = path.replacingOccurrences(of: "/drive/\(drive)", with: "/", options: .anchored, range: nil)
|
||||
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.id = json["id"] as? String
|
||||
self.entryTag = json["eTag"] as? String
|
||||
}
|
||||
|
||||
/// The document identifier is a value assigned by the OneDrive to a file.
|
||||
/// This value is used to identify the document regardless of where it is moved on a volume.
|
||||
/// The identifier persists across system restarts.
|
||||
open internal(set) var id: String? {
|
||||
get {
|
||||
return allValues[.documentIdentifierKey] as? String
|
||||
}
|
||||
set {
|
||||
allValues[.documentIdentifierKey] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// MIME type of file contents returned by OneDrive server.
|
||||
open internal(set) var contentType: String {
|
||||
get {
|
||||
return allValues[.mimeType] as? String ?? ""
|
||||
}
|
||||
set {
|
||||
allValues[.mimeType] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP E-Tag, can be used to mark changed files.
|
||||
open internal(set) var entryTag: String? {
|
||||
get {
|
||||
return allValues[.entryTag] as? String
|
||||
}
|
||||
set {
|
||||
allValues[.entryTag] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// codebeat:disable[ARITY]
|
||||
internal extension OneDriveFileProvider {
|
||||
func list(_ path: String, cursor: URL? = nil, prevContents: [OneDriveFileObject] = [], completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) {
|
||||
let url = cursor ?? self.url(of: path, modifier: "children")
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
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: URL? = (json?["@odata.nextLink"] as? String).flatMap { URL(string: $0) }
|
||||
let hasmore = ncursor != nil
|
||||
if hasmore {
|
||||
self.list(path, cursor: ncursor, prevContents: files, completionHandler: completionHandler)
|
||||
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 = self.url(of: targetPath, modifier: "content\(queryStr)")
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PUT"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
|
||||
request.httpBody = data
|
||||
let task = session.uploadTask(with: request, from: data, 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 = self.url(of: targetPath, modifier: "content\(queryStr)")
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PUT"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
|
||||
let task = session.uploadTask(with: request, fromFile: localFile, 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: URL? = nil, foundItem:@escaping ((_ file: OneDriveFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) {
|
||||
let url: URL
|
||||
let q = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
|
||||
url = next ?? self.url(of: startPath, modifier: "view.search?q=\(q)")
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
|
||||
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: URL? = (json?["@odata.nextLink"] as? String).flatMap { URL(string: $0) }
|
||||
if let next = next {
|
||||
self.search(startPath, query: query, next: next, foundItem: foundItem, completionHandler: completionHandler)
|
||||
} else {
|
||||
completionHandler(responseError ?? error)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
completionHandler(responseError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
// codebeat:enable[ARITY]
|
||||
|
||||
internal extension OneDriveFileProvider {
|
||||
static let dateFormatter = DateFormatter()
|
||||
static let decimalFormatter = NumberFormatter()
|
||||
|
||||
func mapMediaInfo(_ json: [String: Any]) -> (dictionary: [String: Any], keys: [String]) {
|
||||
|
||||
func spaceCamelCase(_ text: String) -> String {
|
||||
var newString: String = ""
|
||||
|
||||
let upperCase = CharacterSet.uppercaseLetters
|
||||
for scalar in text.unicodeScalars {
|
||||
if upperCase.contains(scalar) {
|
||||
newString.append(" ")
|
||||
}
|
||||
let character = Character(scalar)
|
||||
newString.append(character)
|
||||
}
|
||||
|
||||
return newString.capitalized
|
||||
}
|
||||
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
|
||||
func add(key: String, value: Any?) {
|
||||
if let value = value {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if let parent = json["image"] as? [String: Any] ?? json["video"] as? [String: Any], let height = parent["height"] as? UInt64, let width = parent["width"] as? UInt64 {
|
||||
add(key: "Dimensions", value: "\(width)x\(height)")
|
||||
}
|
||||
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))
|
||||
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))
|
||||
}
|
||||
if let timeTakenStr = json["takenDateTime"] as? String, let timeTaken = resolve(dateString: timeTakenStr) {
|
||||
OneDriveFileProvider.dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
add(key: "Date taken", value: OneDriveFileProvider.dateFormatter.string(from: timeTaken))
|
||||
}
|
||||
|
||||
if let photo = json["photo"] as? [String: Any] {
|
||||
add(key: "Device make", value: photo["cameraMake"] as? String)
|
||||
add(key: "Device model", value: photo["cameraModel"] as? String)
|
||||
add(key: "focalLength", value: photo["focalLength"] as? Double)
|
||||
add(key: "fNumber", value: photo["fNumber"] as? Double)
|
||||
if let expNom = photo["exposureNumerator"] as? Double, let expDen = photo["exposureDenominator"] as? Double {
|
||||
add(key: "Exposure time", value: "\(Int(expNom))/\(Int(expDen))")
|
||||
}
|
||||
add(key: "ISO speed", value: photo["iso"] as? Int64)
|
||||
}
|
||||
|
||||
if let audio = json["audio"] as? [String: Any] {
|
||||
for (key, value) in audio {
|
||||
if key == "bitrate" || key == "isVariableBitrate" { continue }
|
||||
let casedKey = spaceCamelCase(key)
|
||||
add(key: casedKey, value: value)
|
||||
}
|
||||
}
|
||||
|
||||
add(key: "Bitrate", value: (json["video"] as? NSDictionary)?["bitrate"] as? Int)
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+101
-23
@@ -8,18 +8,16 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Allows to get progress or cancel an in-progress operation, for remote, `URLSession` based providers.
|
||||
open class RemoteOperationHandle: OperationHandle {
|
||||
|
||||
internal var tasks: [Weak<URLSessionTask>]
|
||||
|
||||
private var _inProgress = false
|
||||
open var inProgress: Bool {
|
||||
return _inProgress
|
||||
}
|
||||
open private(set) var operationType: FileOperationType
|
||||
|
||||
init(tasks: [URLSessionTask]) {
|
||||
init(operationType: FileOperationType, tasks: [URLSessionTask]) {
|
||||
self.operationType = operationType
|
||||
self.tasks = tasks.map { Weak<URLSessionTask>($0) }
|
||||
_inProgress = true
|
||||
}
|
||||
|
||||
internal func add(task: URLSessionTask) {
|
||||
@@ -30,12 +28,6 @@ open class RemoteOperationHandle: OperationHandle {
|
||||
self.tasks = tasks.filter { $0.value != nil }
|
||||
}
|
||||
|
||||
open var progress: Float {
|
||||
let bytesSoFar = self.bytesSoFar
|
||||
let totalBytes = self.totalBytes
|
||||
return totalBytes > 0 ? Float(Double(bytesSoFar) / Double(totalBytes)) : Float.nan
|
||||
}
|
||||
|
||||
open var bytesSoFar: Int64 {
|
||||
return tasks.reduce(0) {
|
||||
if let task = $1.value as? URLSessionUploadTask {
|
||||
@@ -51,16 +43,39 @@ open class RemoteOperationHandle: OperationHandle {
|
||||
if let task = $1.value as? URLSessionUploadTask {
|
||||
return $0 + task.countOfBytesExpectedToSend
|
||||
} else {
|
||||
return $0 + ($1.value?.countOfBytesExpectedToSend ?? 0)
|
||||
return $0 + ($1.value?.countOfBytesExpectedToReceive ?? 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open func cancel() {
|
||||
open func cancel() -> Bool {
|
||||
var canceled = false
|
||||
for taskbox in tasks {
|
||||
taskbox.value?.cancel()
|
||||
canceled = true
|
||||
}
|
||||
_inProgress = false
|
||||
return canceled
|
||||
}
|
||||
|
||||
open var inProgress: Bool {
|
||||
return tasks.reduce(false) { $0 || $1.value?.state ?? .canceling == .running }
|
||||
}
|
||||
}
|
||||
|
||||
/// A protocol defines properties for errors returned by HTTP/S based providers.
|
||||
/// Including Dropbox, OneDrive and WebDAV.
|
||||
public protocol FileProviderHTTPError: Error, CustomStringConvertible {
|
||||
/// HTTP status 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,74 +156,136 @@ class SessionDelegate: NSObject, URLSessionDataDelegate, URLSessionDownloadDeleg
|
||||
}
|
||||
}
|
||||
|
||||
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 = [100: "Continue", 101: "Switching Protocols", 102: "Processing"]
|
||||
fileprivate static let status2xx = [200: "OK", 201: "Created", 202: "Accepted", 203: "Non-Authoritative Information", 204: "No Content", 205: "Reset Content", 206: "Partial Content", 207: "Multi-Status", 208: "Already Reported", 226: "IM Used"]
|
||||
fileprivate static let status3xx = [300: "Multiple Choices", 301: "Moved Permanently", 302: "Found", 303: "See Other", 304: "Not Modified", 305: "Use Proxy", 306: "Switch Proxy", 307: "Temporary Redirect", 308: "Permanent Redirect"]
|
||||
fileprivate static let status4xx = [400: "Bad Request", 401: "Unauthorized/Expired Session", 402: "Payment Required", 403: "Forbidden", 404: "Not Found", 405: "Method Not Allowed", 406: "Not Acceptable", 407: "Proxy Authentication Required", 408: "Request Timeout", 409: "Conflict", 410: "Gone", 411: "Length Required", 412: "Precondition Failed", 413: "Payload Too Large", 414: "URI Too Long", 415: "Unsupported Media Type", 416: "Range Not Satisfiable", 417: "Expectation Failed", 421: "Misdirected Request", 422: "Unprocessable Entity", 423: "Locked", 424: "Failed Dependency", 425: "Unordered Collection", 426: "Upgrade Required", 428: "Precondition Required", 429: "Too Many Requests", 431: "Request Header Fields Too Large", 451: "Unavailable For Legal Reasons"]
|
||||
fileprivate static let status5xx = [500: "Internal Server Error", 501: "Not Implemented", 502: "Bad Gateway", 503: "Service Unavailable", 504: "Gateway Timeout", 505: "HTTP Version Not Supported", 506: "Variant Also Negotiates", 507: "Insufficient Storage", 508: "Loop Detected", 509: "Bandwidth Limit Exceeded", 510: "Not Extended", 511: "Network Authentication Required"]
|
||||
fileprivate static let status1xx: [Int: String] = [100: "Continue", 101: "Switching Protocols", 102: "Processing"]
|
||||
fileprivate static let status2xx: [Int: String] = [200: "OK", 201: "Created", 202: "Accepted", 203: "Non-Authoritative Information", 204: "No Content", 205: "Reset Content", 206: "Partial Content", 207: "Multi-Status", 208: "Already Reported", 226: "IM Used"]
|
||||
fileprivate static let status3xx: [Int: String] = [300: "Multiple Choices", 301: "Moved Permanently", 302: "Found", 303: "See Other", 304: "Not Modified", 305: "Use Proxy", 306: "Switch Proxy", 307: "Temporary Redirect", 308: "Permanent Redirect"]
|
||||
fileprivate static let status4xx: [Int: String] = [400: "Bad Request", 401: "Unauthorized/Expired Session", 402: "Payment Required", 403: "Forbidden", 404: "Not Found", 405: "Method Not Allowed", 406: "Not Acceptable", 407: "Proxy Authentication Required", 408: "Request Timeout", 409: "Conflict", 410: "Gone", 411: "Length Required", 412: "Precondition Failed", 413: "Payload Too Large", 414: "URI Too Long", 415: "Unsupported Media Type", 416: "Range Not Satisfiable", 417: "Expectation Failed", 421: "Misdirected Request", 422: "Unprocessable Entity", 423: "Locked", 424: "Failed Dependency", 425: "Unordered Collection", 426: "Upgrade Required", 428: "Precondition Required", 429: "Too Many Requests", 431: "Request Header Fields Too Large", 451: "Unavailable For Legal Reasons"]
|
||||
fileprivate static let status5xx: [Int: String] = [500: "Internal Server Error", 501: "Not Implemented", 502: "Bad Gateway", 503: "Service Unavailable", 504: "Gateway Timeout", 505: "HTTP Version Not Supported", 506: "Variant Also Negotiates", 507: "Insufficient Storage", 508: "Loop Detected", 509: "Bandwidth Limit Exceeded", 510: "Not Extended", 511: "Network Authentication Required"]
|
||||
|
||||
public var description: String {
|
||||
switch self.rawValue {
|
||||
@@ -222,6 +299,7 @@ public enum FileProviderHTTPErrorCode: Int {
|
||||
}
|
||||
}
|
||||
|
||||
/// Description of status based on first digit which indicated fail or success.
|
||||
public var typeDescription: String {
|
||||
switch self.rawValue {
|
||||
case 100...199: return "Informational"
|
||||
@@ -229,7 +307,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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+31
-25
@@ -12,12 +12,24 @@ import Foundation
|
||||
// For big-endian platforms like PowerPC, there must be a huge overhaul
|
||||
|
||||
protocol SMBProtocolClientDelegate: class {
|
||||
func receivedSMB2Response(_ header: SMB2.Header, response: SMBResponse)
|
||||
func receivedResponse(client: SMB2ProtocolClient, response: SMBResponse, for: SMBRequest)
|
||||
}
|
||||
|
||||
class SMB2ProtocolClient: FPSStreamTask {
|
||||
var currentMessageID: UInt64 = 0
|
||||
var sessionId: UInt64 = 0
|
||||
var timeout: TimeInterval = 30
|
||||
|
||||
private(set) var lastMessageID: UInt64 = 0
|
||||
private(set) var sessionId: UInt64 = 0
|
||||
private func messageId() -> UInt64 {
|
||||
defer {
|
||||
lastMessageID += 1
|
||||
}
|
||||
return lastMessageID
|
||||
}
|
||||
|
||||
private(set) var establishedTrees = Array<SMB2.TreeConnectResponse>()
|
||||
private(set) var requestStack = [Int: SMBRequest]()
|
||||
private(set) var responseStack = [Int: SMBResponse]()
|
||||
|
||||
weak var delegate: SMBProtocolClientDelegate?
|
||||
|
||||
@@ -40,7 +52,7 @@ class SMB2ProtocolClient: FPSStreamTask {
|
||||
let data = createSMB2Message(header: smbHeader, message: msg)
|
||||
self.writeData(data, timeout: 0, completionHandler: { (e) in
|
||||
if self.sessionId == 0 {
|
||||
self.readData(OfMinLength: 64, maxLength: 65536, timeout: 30, 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)
|
||||
})
|
||||
@@ -56,11 +68,8 @@ 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!)
|
||||
@@ -70,6 +79,7 @@ class SMB2ProtocolClient: FPSStreamTask {
|
||||
})
|
||||
return mId
|
||||
}
|
||||
|
||||
func sendTreeDisconnect(id treeId: UInt32, completionHandler: SimpleCompletionHandler) -> UInt64 {
|
||||
let mId = messageId()
|
||||
let smbHeader = SMB2.Header(command: .TREE_DISCONNECT, creditRequestResponse: 111, messageId: mId, treeId: treeId, sessionId: sessionId)
|
||||
@@ -92,15 +102,13 @@ class SMB2ProtocolClient: FPSStreamTask {
|
||||
return mId
|
||||
}
|
||||
|
||||
func messageId() -> UInt64 {
|
||||
defer {
|
||||
currentMessageID += 1
|
||||
}
|
||||
return currentMessageID
|
||||
func reset() {
|
||||
|
||||
}
|
||||
|
||||
// MARK: create and analyse messages
|
||||
|
||||
}
|
||||
|
||||
// MARK: create and analyse messages
|
||||
extension SMB2ProtocolClient {
|
||||
func determineSMBVersion(_ data: Data) -> Float {
|
||||
let smbverChar: Int8 = Int8(bitPattern: data.first ?? 0)
|
||||
let version = 0 - smbverChar
|
||||
@@ -116,7 +124,7 @@ class SMB2ProtocolClient: FPSStreamTask {
|
||||
throw SMBFileProviderError.incompatibleHeader
|
||||
}
|
||||
let headersize = MemoryLayout<SMB1.Header>.size
|
||||
let header: SMB1.Header = decode(data)
|
||||
let header: SMB1.Header = data.scanValue()!
|
||||
var blocks = [(params: [UInt16], message: Data?)]()
|
||||
var offset = headersize
|
||||
while offset < data.count {
|
||||
@@ -128,7 +136,7 @@ class SMB2ProtocolClient: FPSStreamTask {
|
||||
offset += MemoryLayout<UInt8>.size
|
||||
var rawParamWords = [UInt8](buffer[offset..<(offset + paramWordsCount * 2)])
|
||||
let paramData = Data(bytesNoCopy: UnsafeMutablePointer<UInt8>(&rawParamWords), count: rawParamWords.count, deallocator: .free)
|
||||
paramWords = decode(paramData)
|
||||
paramWords = paramData.scanValue()!
|
||||
offset += paramWordsCount * 2
|
||||
let messageBytesCount = Int(UInt16(buffer[0]) + UInt16(buffer[1]) << 8)
|
||||
offset += MemoryLayout<UInt16>.size
|
||||
@@ -143,7 +151,7 @@ class SMB2ProtocolClient: FPSStreamTask {
|
||||
return (header, blocks)
|
||||
}
|
||||
|
||||
func digestSMB2Message(_ data: Data) throws -> (header: SMB2.Header, message: SMBResponse?)? {
|
||||
func digestSMB2Message(_ data: Data) throws -> SMBResponse? {
|
||||
guard data.count > 65 else {
|
||||
throw URLError(.badServerResponse)
|
||||
}
|
||||
@@ -154,7 +162,7 @@ class SMB2ProtocolClient: FPSStreamTask {
|
||||
let headerData = data.subdata(in: 0..<headersize)
|
||||
let messageSize = data.count - headersize
|
||||
let messageData = data.subdata(in: headersize..<(headersize + messageSize))
|
||||
let header: SMB2.Header = decode(headerData)
|
||||
let header: SMB2.Header = headerData.scanValue()!
|
||||
switch header.command {
|
||||
case .NEGOTIATE:
|
||||
return (header, SMB2.NegotiateResponse(data: messageData))
|
||||
@@ -200,8 +208,7 @@ class SMB2ProtocolClient: FPSStreamTask {
|
||||
}
|
||||
|
||||
func createSMBMessage(header: SMB1.Header, blocks: [(params: Data?, message: Data?)]) -> Data {
|
||||
var headerv = header
|
||||
var result = encode(&headerv)
|
||||
var result = Data(value: header)
|
||||
for block in blocks {
|
||||
var paramWordsCount = UInt8(block.params?.count ?? 0)
|
||||
result.append(¶mWordsCount, count: MemoryLayout.size(ofValue: paramWordsCount))
|
||||
@@ -218,9 +225,8 @@ class SMB2ProtocolClient: FPSStreamTask {
|
||||
return result
|
||||
}
|
||||
|
||||
func createSMB2Message(header: SMB2.Header, message: SMBRequest) -> Data {
|
||||
var headerv = header
|
||||
var result = encode(&headerv)
|
||||
func createSMB2Message(header: SMB2.Header, message: SMBRequestBody) -> Data {
|
||||
var result = Data(value: header)
|
||||
result.append(message.data())
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
open class SMBFileProvider: FileProvider, FileProviderMonitor {
|
||||
open static var type: String = "Samba"
|
||||
open var isPathRelative: Bool = true
|
||||
class SMBFileProvider: FileProvider, FileProviderMonitor {
|
||||
open class var type: String { return "SMB" }
|
||||
open var baseURL: URL?
|
||||
open var currentPath: String = ""
|
||||
open var dispatch_queue: DispatchQueue
|
||||
open var operation_queue: OperationQueue
|
||||
open weak var delegate: FileProviderDelegate?
|
||||
open let credential: URLCredential?
|
||||
|
||||
@@ -23,9 +23,11 @@ open class SMBFileProvider: FileProvider, FileProviderMonitor {
|
||||
guard baseURL.uw_scheme.lowercased() == "smb" else {
|
||||
return nil
|
||||
}
|
||||
self.baseURL = baseURL
|
||||
dispatch_queue = DispatchQueue(label: "FileProvider.\(SMBFileProvider.type)", attributes: DispatchQueue.Attributes.concurrent)
|
||||
//let url = baseURL.uw_absoluteString
|
||||
self.baseURL = baseURL.appendingPathComponent("")
|
||||
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: .concurrent)
|
||||
operation_queue = OperationQueue()
|
||||
operation_queue.name = "FileProvider.\(type(of: self).type).Operation"
|
||||
|
||||
self.credential = credential
|
||||
}
|
||||
|
||||
@@ -41,6 +43,10 @@ open class SMBFileProvider: FileProvider, FileProviderMonitor {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
func isReachable(completionHandler: @escaping (Bool) -> Void) {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
open weak var fileOperationDelegate: FileOperationDelegate?
|
||||
|
||||
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
@@ -48,7 +54,7 @@ open class SMBFileProvider: FileProvider, FileProviderMonitor {
|
||||
return nil
|
||||
}
|
||||
|
||||
open func create(file fileAttribs: FileObject, at atPath: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
open func create(file fileName: String, at atPath: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
NotImplemented()
|
||||
return nil
|
||||
}
|
||||
@@ -68,7 +74,7 @@ open class SMBFileProvider: FileProvider, FileProviderMonitor {
|
||||
return nil
|
||||
}
|
||||
|
||||
open func copyItem(localFile: URL, to toPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
NotImplemented()
|
||||
return nil
|
||||
}
|
||||
@@ -88,12 +94,12 @@ open class SMBFileProvider: FileProvider, FileProviderMonitor {
|
||||
return nil
|
||||
}
|
||||
|
||||
open func writeContents(path: String, contents data: Data, atomically: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
open func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
NotImplemented()
|
||||
return nil
|
||||
}
|
||||
|
||||
open func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler:((FileObjectClass) -> Void)?, completionHandler: @escaping ((_ files: [FileObjectClass], _ error: Error?) -> Void)) {
|
||||
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler:((FileObjectClass) -> Void)?, completionHandler: @escaping ((_ files: [FileObjectClass], _ error: Error?) -> Void)) {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
@@ -108,10 +114,18 @@ open class SMBFileProvider: FileProvider, FileProviderMonitor {
|
||||
open func isRegisteredForNotification(path: String) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
open func copy(with zone: NSZone? = nil) -> Any {
|
||||
let copy = SMBFileProvider(baseURL: self.baseURL!, credential: self.credential!, afterInitialized: { _ in })!
|
||||
copy.currentPath = self.currentPath
|
||||
copy.delegate = self.delegate
|
||||
copy.fileOperationDelegate = self.fileOperationDelegate
|
||||
return copy
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: basic CIFS interactivity
|
||||
public enum SMBFileProviderError: Int, Error, CustomStringConvertible {
|
||||
enum SMBFileProviderError: Int, Error, CustomStringConvertible {
|
||||
case badHeader
|
||||
case incompatibleHeader
|
||||
case incorrectParamsLength
|
||||
|
||||
@@ -8,17 +8,35 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol SMBRequest {
|
||||
protocol SMBRequestBody {
|
||||
func data() -> Data
|
||||
}
|
||||
|
||||
protocol SMBResponse {
|
||||
extension SMBRequestBody {
|
||||
func data() -> Data {
|
||||
return Data(value: self)
|
||||
}
|
||||
}
|
||||
|
||||
protocol SMBResponseBody {
|
||||
init? (data: Data)
|
||||
}
|
||||
|
||||
extension SMBResponseBody {
|
||||
init? (data: Data) {
|
||||
if let v: Self = data.scanValue() {
|
||||
self = v
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protocol IOCtlRequestProtocol: SMBRequest {}
|
||||
protocol IOCtlResponseProtocol: SMBResponse {}
|
||||
typealias SMBRequest = (header: SMB2.Header, body: SMBRequestBody?)
|
||||
typealias SMBResponse = (header: SMB2.Header, body: SMBResponseBody?)
|
||||
|
||||
protocol IOCtlRequestProtocol: SMBRequestBody {}
|
||||
protocol IOCtlResponseProtocol: SMBResponseBody {}
|
||||
|
||||
|
||||
struct SMBTime {
|
||||
@@ -48,3 +66,32 @@ 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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import Foundation
|
||||
extension SMB2 {
|
||||
// MARK: SMB2 Create
|
||||
|
||||
struct CreateRequest: SMBRequest {
|
||||
struct CreateRequest: SMBRequestBody {
|
||||
let header: CreateRequest.Header
|
||||
let name: String?
|
||||
let contexts: [CreateContext]
|
||||
@@ -40,7 +40,7 @@ extension SMB2 {
|
||||
header.contextLength = 0
|
||||
//result.appendData(nameData)
|
||||
}
|
||||
var result = encode(&header)
|
||||
var result = Data(value: header)
|
||||
result.append(body)
|
||||
return result
|
||||
}
|
||||
@@ -158,7 +158,7 @@ extension SMB2 {
|
||||
}
|
||||
}
|
||||
|
||||
struct CreateResponse: SMBResponse {
|
||||
struct CreateResponse: SMBResponseBody {
|
||||
struct Header {
|
||||
let size: UInt16
|
||||
fileprivate let _oplockLevel: UInt8
|
||||
@@ -186,7 +186,7 @@ extension SMB2 {
|
||||
guard data.count >= MemoryLayout<CreateResponse.Header>.size else {
|
||||
return nil
|
||||
}
|
||||
self.header = decode(data)
|
||||
self.header = data.scanValue()!
|
||||
if self.header.contextsOffset > 0 {
|
||||
var contexts = [CreateContext]()
|
||||
var contextOffset = Int(self.header.contextsOffset) - MemoryLayout<SMB2.Header>.size
|
||||
@@ -195,14 +195,9 @@ extension SMB2 {
|
||||
self.contexts = contexts
|
||||
return
|
||||
}
|
||||
let contextDataHeader = data.subdata(in: contextOffset..<(contextOffset + MemoryLayout<CreateContext.Header>.size))
|
||||
if let lastContextHeader = CreateContext(data: contextDataHeader) {
|
||||
let lastContextLen = Int(lastContextHeader.header.dataOffset) + Int(lastContextHeader.header.dataLength) - contextOffset
|
||||
let lastContextData = data.subdata(in: contextOffset..<(contextOffset + lastContextLen))
|
||||
if let newContext = CreateContext(data: lastContextData) {
|
||||
contexts.append(newContext)
|
||||
}
|
||||
contextOffset = Int(lastContextHeader.header.next) - MemoryLayout<SMB2.Header>.size
|
||||
while contextOffset > 0, let context: CreateContext = data.scanValue(start: contextOffset) {
|
||||
contexts.append(context)
|
||||
contextOffset = Int(context.header.next) - MemoryLayout<SMB2.Header>.size
|
||||
}
|
||||
}
|
||||
self.contexts = contexts
|
||||
@@ -232,9 +227,8 @@ extension SMB2 {
|
||||
}
|
||||
|
||||
init(name: UUID, data: Data) {
|
||||
var uuid = uuid_t(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)
|
||||
(name as NSUUID).getBytes(&uuid.0)
|
||||
var nameData = Data(bytes: &uuid.0, count: 16)
|
||||
let uuid = name.uuid
|
||||
var nameData = Data(value: uuid)
|
||||
self.header = CreateContext.Header(next: 0, nameOffset: 32, nameLength: UInt16(nameData.count), reserved: 0, dataOffset: UInt16(nameData.count), dataLength: UInt32(data.count))
|
||||
self.buffer = data
|
||||
}
|
||||
@@ -244,12 +238,12 @@ extension SMB2 {
|
||||
guard data.count > headersize else {
|
||||
return nil
|
||||
}
|
||||
self.header = decode(data)
|
||||
self.header = data.scanValue()!
|
||||
self.buffer = data.subdata(in: headersize..<data.count)
|
||||
}
|
||||
|
||||
func data() -> Data {
|
||||
var result = encode(header)
|
||||
var result = Data(value: header)
|
||||
result.append(buffer)
|
||||
return result
|
||||
}
|
||||
@@ -363,7 +357,7 @@ extension SMB2 {
|
||||
|
||||
// MARK: SMB2 Close
|
||||
|
||||
struct CloseRequest: SMBRequest {
|
||||
struct CloseRequest: SMBRequestBody {
|
||||
let size: UInt16
|
||||
let flags: CloseFlags
|
||||
fileprivate let reserved2: UInt32
|
||||
@@ -377,13 +371,9 @@ extension SMB2 {
|
||||
self.flags = []
|
||||
self.reserved2 = 0
|
||||
}
|
||||
|
||||
func data() -> Data {
|
||||
return encode(self)
|
||||
}
|
||||
}
|
||||
|
||||
struct CloseResponse: SMBResponse {
|
||||
struct CloseResponse: SMBResponseBody {
|
||||
let size: UInt16
|
||||
let flags: CloseFlags
|
||||
fileprivate let reserved: UInt32
|
||||
@@ -394,10 +384,6 @@ extension SMB2 {
|
||||
let allocationSize: UInt64
|
||||
let endOfFile: UInt64
|
||||
let fileAttributes: FileAttributes
|
||||
|
||||
init? (data: Data) {
|
||||
self = decode(data)
|
||||
}
|
||||
}
|
||||
|
||||
struct CloseFlags: OptionSet {
|
||||
@@ -412,7 +398,7 @@ extension SMB2 {
|
||||
|
||||
// MARK: SMB2 Flush
|
||||
|
||||
struct FlushRequest: SMBRequest {
|
||||
struct FlushRequest: SMBRequestBody {
|
||||
let size: UInt16
|
||||
fileprivate let reserved: UInt16
|
||||
fileprivate let reserved2: UInt32
|
||||
@@ -426,13 +412,9 @@ extension SMB2 {
|
||||
self.reserved = 0
|
||||
self.reserved2 = 0
|
||||
}
|
||||
|
||||
func data() -> Data {
|
||||
return encode(self)
|
||||
}
|
||||
}
|
||||
|
||||
struct FlushResponse: SMBResponse {
|
||||
struct FlushResponse: SMBResponseBody {
|
||||
let size: UInt16
|
||||
let reserved: UInt16
|
||||
|
||||
@@ -440,9 +422,5 @@ extension SMB2 {
|
||||
self.size = 4
|
||||
self.reserved = 0
|
||||
}
|
||||
|
||||
init? (data: Data) {
|
||||
self = decode(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import Foundation
|
||||
extension SMB2 {
|
||||
// MARK: SMB2 Read
|
||||
|
||||
struct ReadRequest: SMBRequest {
|
||||
struct ReadRequest: SMBRequestBody {
|
||||
let size: UInt16
|
||||
fileprivate let padding: UInt8
|
||||
let flags: ReadRequest.Flags
|
||||
@@ -43,10 +43,6 @@ extension SMB2 {
|
||||
self.channelBuffer = 0
|
||||
}
|
||||
|
||||
func data() -> Data {
|
||||
return encode(read)
|
||||
}
|
||||
|
||||
struct Flags: OptionSet {
|
||||
let rawValue: UInt8
|
||||
|
||||
@@ -58,7 +54,7 @@ extension SMB2 {
|
||||
}
|
||||
}
|
||||
|
||||
struct ReadRespone: SMBResponse {
|
||||
struct ReadRespone: SMBResponseBody {
|
||||
struct Header {
|
||||
let size: UInt16
|
||||
let offset: UInt8
|
||||
@@ -75,7 +71,7 @@ extension SMB2 {
|
||||
guard data.count > 16 else {
|
||||
return nil
|
||||
}
|
||||
self.header = decode(data)
|
||||
self.header = data.scanValue()!
|
||||
let headersize = MemoryLayout<Header>.size
|
||||
self.buffer = data.subdata(in: headersize..<data.count)
|
||||
}
|
||||
@@ -89,7 +85,7 @@ extension SMB2 {
|
||||
|
||||
// MARK: SMB2 Write
|
||||
|
||||
struct WriteRequest: SMBRequest {
|
||||
struct WriteRequest: SMBRequestBody {
|
||||
let header: WriteRequest.Header
|
||||
let channelInfo: ChannelInfo?
|
||||
let fileData: Data
|
||||
@@ -110,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
|
||||
@@ -122,9 +119,10 @@ extension SMB2 {
|
||||
self.channelInfo = channelInfo
|
||||
self.fileData = data
|
||||
}
|
||||
// codebeat:enable[ARITY]
|
||||
|
||||
func data() -> Data {
|
||||
var result = encode(self.header)
|
||||
var result = Data(value: self.header)
|
||||
if let channelInfo = channelInfo {
|
||||
result.append(channelInfo.data())
|
||||
}
|
||||
@@ -144,41 +142,29 @@ extension SMB2 {
|
||||
}
|
||||
}
|
||||
|
||||
struct WriteResponse: SMBResponse {
|
||||
struct WriteResponse: SMBResponseBody {
|
||||
let size: UInt16
|
||||
fileprivate let reserved: UInt16
|
||||
let writtenBytes: UInt32
|
||||
fileprivate let remaining: UInt32
|
||||
fileprivate let channelInfoOffset: UInt16
|
||||
fileprivate let channelInfoLength: UInt16
|
||||
|
||||
init?(data: Data) {
|
||||
self = decode(data)
|
||||
}
|
||||
}
|
||||
|
||||
struct ChannelInfo: SMBRequest {
|
||||
struct ChannelInfo: SMBRequestBody {
|
||||
let offset: UInt64
|
||||
let token: UInt32
|
||||
let length: UInt32
|
||||
|
||||
func data() -> Data {
|
||||
return encode(data)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: SMB2 Lock
|
||||
|
||||
struct LockElement: SMBRequest {
|
||||
struct LockElement: SMBRequestBody {
|
||||
let offset: UInt64
|
||||
let length: UInt64
|
||||
let flags: LockElement.Flags
|
||||
fileprivate let reserved: UInt32
|
||||
|
||||
func data() -> Data {
|
||||
return encode(self)
|
||||
}
|
||||
|
||||
struct Flags: OptionSet {
|
||||
let rawValue: UInt32
|
||||
|
||||
@@ -193,7 +179,7 @@ extension SMB2 {
|
||||
}
|
||||
}
|
||||
|
||||
struct LockRequest: SMBRequest {
|
||||
struct LockRequest: SMBRequestBody {
|
||||
let header: LockRequest.Header
|
||||
let locks: [LockElement]
|
||||
|
||||
@@ -203,9 +189,9 @@ extension SMB2 {
|
||||
}
|
||||
|
||||
func data() -> Data {
|
||||
var result = encode(header)
|
||||
var result = Data(value: header)
|
||||
for lock in locks {
|
||||
result.append(encode(lock))
|
||||
result.append(Data(value: lock))
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -218,7 +204,7 @@ extension SMB2 {
|
||||
}
|
||||
}
|
||||
|
||||
struct LockResponse: SMBResponse {
|
||||
struct LockResponse: SMBResponseBody {
|
||||
let size: UInt16
|
||||
let reserved: UInt16
|
||||
|
||||
@@ -226,15 +212,11 @@ extension SMB2 {
|
||||
self.size = 4
|
||||
self.reserved = 0
|
||||
}
|
||||
|
||||
init? (data: Data) {
|
||||
self = decode(data)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: SMB2 Cancel
|
||||
|
||||
struct CancelRequest: SMBRequest {
|
||||
struct CancelRequest: SMBRequestBody {
|
||||
let size: UInt16
|
||||
let reserved: UInt16
|
||||
|
||||
@@ -242,9 +224,5 @@ extension SMB2 {
|
||||
self.size = 4
|
||||
self.reserved = 0
|
||||
}
|
||||
|
||||
func data() -> Data {
|
||||
return encode(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ extension SMB2 {
|
||||
* IOCtl usage is usually limited in SMB to pipe requests and duplicating file inside server
|
||||
*/
|
||||
|
||||
struct IOCtlRequest: SMBRequest {
|
||||
struct IOCtlRequest: SMBRequestBody {
|
||||
let header: Header
|
||||
let requestData: IOCtlRequestProtocol?
|
||||
|
||||
@@ -26,7 +26,7 @@ extension SMB2 {
|
||||
}
|
||||
|
||||
func data() -> Data {
|
||||
var result = encode(self.header)
|
||||
var result = Data(value: self.header)
|
||||
if let reqData = requestData?.data() {
|
||||
result.append(reqData)
|
||||
}
|
||||
@@ -63,12 +63,12 @@ extension SMB2 {
|
||||
}
|
||||
}
|
||||
|
||||
struct IOCtlResponse: SMBResponse {
|
||||
struct IOCtlResponse: SMBResponseBody {
|
||||
let header: Header
|
||||
let responseData: IOCtlResponseProtocol?
|
||||
|
||||
init?(data: Data) {
|
||||
self.header = decode(data)
|
||||
self.header = data.scanValue()!
|
||||
let endRange = Int(self.header.outputOffset - 64) + Int(self.header.outputCount)
|
||||
let response = data.subdata(in: Int(self.header.outputOffset - 64)..<endRange)
|
||||
switch self.header.ctlCode {
|
||||
@@ -140,10 +140,10 @@ extension SMB2 {
|
||||
let chunks: [Chunk]
|
||||
|
||||
func data() -> Data {
|
||||
var result = encode(sourceKey)
|
||||
result.append(encode(chunkCount))
|
||||
var reserved: UInt32 = 0
|
||||
result.append(encode(&reserved))
|
||||
var result = Data(value: sourceKey)
|
||||
result.append(Data(value: chunkCount))
|
||||
let reserved: UInt32 = 0
|
||||
result.append(Data(value: reserved))
|
||||
return Data()
|
||||
}
|
||||
|
||||
@@ -152,10 +152,6 @@ extension SMB2 {
|
||||
let targetOffset: UInt64
|
||||
let length: UInt32
|
||||
fileprivate let reserved: UInt32
|
||||
|
||||
func data() -> Data {
|
||||
return encode(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,10 +178,6 @@ extension SMB2 {
|
||||
self.length = length
|
||||
self.offset = offset
|
||||
}
|
||||
|
||||
func data() -> Data {
|
||||
return encode(self)
|
||||
}
|
||||
}
|
||||
|
||||
struct ResilencyRequest: IOCtlRequestProtocol {
|
||||
@@ -197,10 +189,6 @@ extension SMB2 {
|
||||
self.timeout = timeout
|
||||
self.reserved = 0
|
||||
}
|
||||
|
||||
func data() -> Data {
|
||||
return encode(self)
|
||||
}
|
||||
}
|
||||
|
||||
struct ValidateNegotiateInfo: IOCtlRequestProtocol {
|
||||
@@ -213,8 +201,8 @@ extension SMB2 {
|
||||
}
|
||||
|
||||
func data() -> Data {
|
||||
var result = encode(self.header)
|
||||
dialects.forEach { result.append(encode($0)) }
|
||||
var result = Data(value: self.header)
|
||||
dialects.forEach { result.append(Data(value: $0)) }
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -234,10 +222,6 @@ extension SMB2 {
|
||||
let chunksCount: UInt32
|
||||
let chunksBytesWritten: UInt32
|
||||
let totalBytesWriiten: UInt32
|
||||
|
||||
init?(data: Data) {
|
||||
self = decode(data)
|
||||
}
|
||||
}
|
||||
|
||||
// SRV_ENUMERATE_SNAPSHOTS
|
||||
@@ -247,8 +231,9 @@ extension SMB2 {
|
||||
let snapshots: [SMBTime]
|
||||
|
||||
init?(data: Data) {
|
||||
self.count = decode(data)
|
||||
self.returnedCount = decode(data.subdata(in: 4..<8))
|
||||
guard data.count > 8 else { return nil }
|
||||
self.count = data.scanValue()!
|
||||
self.returnedCount = data.scanValue(start: 4)!
|
||||
//let size: UInt32 = decode(data.subdataWithRange(NSRange(location: 8, length: 4)))
|
||||
var snapshots = [SMBTime]()
|
||||
let dateFormatter = DateFormatter()
|
||||
@@ -258,7 +243,7 @@ extension SMB2 {
|
||||
if data.count < offset + 48 {
|
||||
return nil
|
||||
}
|
||||
let datestring = String(data: data.subdata(in: offset..<(offset + 48)), encoding: .utf16)
|
||||
let datestring = data.scanString(start: offset, length: 48, encoding: .utf16)
|
||||
if let datestring = datestring, let date = dateFormatter.date(from: datestring) {
|
||||
snapshots.append(SMBTime(date: date))
|
||||
}
|
||||
@@ -271,32 +256,21 @@ extension SMB2 {
|
||||
let key: (UInt64, UInt64, UInt64)
|
||||
fileprivate let contextLength: UInt32
|
||||
fileprivate let context: UInt32
|
||||
|
||||
init?(data: Data) {
|
||||
self = decode(data)
|
||||
}
|
||||
}
|
||||
|
||||
struct ReadHash: IOCtlResponseProtocol {
|
||||
// TODO: Implement IOCTL READ_HASH
|
||||
|
||||
init?(data: Data) {
|
||||
self = decode(data)
|
||||
}
|
||||
}
|
||||
|
||||
struct NetworkInterfaceInfo: IOCtlResponseProtocol {
|
||||
let items: [NetworkInterfaceInfo.Item]
|
||||
|
||||
init?(data: Data) {
|
||||
let count = data.count / MemoryLayout<Item>.size
|
||||
guard count > 0 else {
|
||||
return nil
|
||||
}
|
||||
var items = [Item]()
|
||||
for i in 0..<count {
|
||||
let itemdata = data.subdata(in: (i * MemoryLayout<Item>.size)..<((i + 1) * MemoryLayout<Item>.size))
|
||||
items.append(decode(itemdata))
|
||||
var offset = 0
|
||||
while let item: Item = data.scanValue(start: offset) {
|
||||
items.append(item)
|
||||
offset += MemoryLayout<Item>.size
|
||||
}
|
||||
self.items = items
|
||||
}
|
||||
@@ -335,15 +309,11 @@ extension SMB2 {
|
||||
static let ipv6: sa_family_t = 0x17
|
||||
|
||||
var sockaddr: sockaddr_in {
|
||||
var sockaddrStorage = self.sockaddrStorage
|
||||
let data = Data(bytes: &sockaddrStorage, count: 16)
|
||||
return decode(data)
|
||||
return Data.mapMemory(from: self.sockaddrStorage)!
|
||||
}
|
||||
|
||||
var sockaddr6: sockaddr_in6 {
|
||||
var sockaddrStorage = self.sockaddrStorage
|
||||
let data = Data(bytes: &sockaddrStorage, count: 28)
|
||||
return decode(data)
|
||||
return Data.mapMemory(from: self.sockaddrStorage)!
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -356,10 +326,6 @@ extension SMB2 {
|
||||
var dialect: (major: Int, minor: Int) {
|
||||
return (major: Int(_dialect & 0xFF), minor: Int(_dialect >> 8))
|
||||
}
|
||||
|
||||
init?(data: Data) {
|
||||
self = decode(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import Foundation
|
||||
extension SMB2 {
|
||||
// MARK: SMB2 Change Notify
|
||||
|
||||
struct ChangeNotifyRequest: SMBRequest {
|
||||
struct ChangeNotifyRequest: SMBRequestBody {
|
||||
let size: UInt16
|
||||
let flags: ChangeNotifyRequest.Flags
|
||||
let outputBufferLength: UInt32
|
||||
@@ -28,10 +28,6 @@ extension SMB2 {
|
||||
self.reserved = 0
|
||||
}
|
||||
|
||||
func data() -> Data {
|
||||
return encode(self)
|
||||
}
|
||||
|
||||
struct Flags: OptionSet {
|
||||
let rawValue: UInt16
|
||||
|
||||
@@ -79,7 +75,7 @@ extension SMB2 {
|
||||
}
|
||||
}
|
||||
|
||||
struct ChangeNotifyResponse: SMBResponse {
|
||||
struct ChangeNotifyResponse: SMBResponseBody {
|
||||
let notifications: [(action: FileNotifyAction, fileName: String)]
|
||||
|
||||
init?(data: Data) {
|
||||
@@ -89,19 +85,14 @@ extension SMB2 {
|
||||
|
||||
var offset = 0
|
||||
while i < maxLoop {
|
||||
let nextOffsetData = data.subdata(in: offset..<(offset + 4))
|
||||
let nextOffset: UInt32 = decode(nextOffsetData)
|
||||
|
||||
let actionData = data.subdata(in: (offset + 4)..<(offset + 8))
|
||||
let actionValue: UInt32 = decode(actionData)
|
||||
let nextOffset: UInt32 = data.scanValue(start: offset) ?? 0
|
||||
let actionValue: UInt32 = data.scanValue(start: offset + 4) ?? 0
|
||||
guard let action = FileNotifyAction(rawValue: actionValue) else {
|
||||
continue
|
||||
}
|
||||
|
||||
let fileLenData = data.subdata(in: (offset + 8)..<(offset + 12))
|
||||
let fileNameLen = Int(decode(fileLenData) as UInt32)
|
||||
let fileNameData = data.subdata(in: (offset + 12)..<(offset + 12 + fileNameLen))
|
||||
let fileName = String(data: fileNameData, encoding: .utf16) ?? ""
|
||||
let fileNameLen = Int(data.scanValue(start: offset + 8) as UInt32? ?? 0)
|
||||
let fileName = data.scanString(start: offset + 12, length: fileNameLen, encoding: .utf16) ?? ""
|
||||
result.append((action: action, fileName: fileName))
|
||||
|
||||
offset += Int(nextOffset)
|
||||
|
||||
@@ -11,7 +11,7 @@ import Foundation
|
||||
extension SMB2 {
|
||||
// MARK: SMB2 Query Directory
|
||||
|
||||
struct QueryDirectoryRequest: SMBRequest {
|
||||
struct QueryDirectoryRequest: SMBRequestBody {
|
||||
let header: QueryDirectoryRequest.Header
|
||||
let searchPattern: String?
|
||||
|
||||
@@ -28,7 +28,7 @@ extension SMB2 {
|
||||
}
|
||||
|
||||
func data() -> Data {
|
||||
var result = encode(header)
|
||||
var result = Data(value: header)
|
||||
if let patternData = searchPattern?.data(using: .utf16) {
|
||||
result.append(patternData)
|
||||
}
|
||||
@@ -60,7 +60,7 @@ extension SMB2 {
|
||||
}
|
||||
}
|
||||
|
||||
struct QueryDirectoryResponse: SMBResponse {
|
||||
struct QueryDirectoryResponse: SMBResponseBody {
|
||||
let buffer: Data
|
||||
|
||||
func parseAs(type: FileInformationEnum) -> [(header: SMB2FilesInformationHeader, fileName: String)] {
|
||||
@@ -68,43 +68,24 @@ extension SMB2 {
|
||||
var result = [(header: SMB2FilesInformationHeader, fileName: String)]()
|
||||
while true {
|
||||
let header: SMB2FilesInformationHeader
|
||||
let headersize: Int
|
||||
switch type {
|
||||
case .fileDirectoryInformation:
|
||||
headersize = MemoryLayout<FileDirectoryInformationHeader>.size
|
||||
let headerData = buffer.subdata(in: offset..<(offset + headersize))
|
||||
let h: FileDirectoryInformationHeader = decode(headerData)
|
||||
header = h
|
||||
header = buffer.scanValue(start: offset) as FileDirectoryInformationHeader!
|
||||
case .fileFullDirectoryInformation:
|
||||
headersize = MemoryLayout<FileFullDirectoryInformationHeader>.size
|
||||
let headerData = buffer.subdata(in: offset..<(offset + headersize))
|
||||
let h: FileFullDirectoryInformationHeader = decode(headerData)
|
||||
header = h
|
||||
header = buffer.scanValue(start: offset) as FileFullDirectoryInformationHeader!
|
||||
case .fileIdFullDirectoryInformation:
|
||||
headersize = MemoryLayout<FileIdFullDirectoryInformationHeader>.size
|
||||
let headerData = buffer.subdata(in: offset..<(offset + headersize))
|
||||
let h: FileIdFullDirectoryInformationHeader = decode(headerData)
|
||||
header = h
|
||||
header = buffer.scanValue(start: offset) as FileIdFullDirectoryInformationHeader!
|
||||
case .fileBothDirectoryInformation:
|
||||
headersize = MemoryLayout<FileBothDirectoryInformationHeader>.size
|
||||
let headerData = buffer.subdata(in: offset..<(offset + headersize))
|
||||
let h: FileBothDirectoryInformationHeader = decode(headerData)
|
||||
header = h
|
||||
header = buffer.scanValue(start: offset) as FileBothDirectoryInformationHeader!
|
||||
case .fileIdBothDirectoryInformation:
|
||||
headersize = MemoryLayout<FileIdBothDirectoryInformationHeader>.size
|
||||
let headerData = buffer.subdata(in: offset..<(offset + headersize))
|
||||
let h: FileIdBothDirectoryInformationHeader = decode(headerData)
|
||||
header = h
|
||||
header = buffer.scanValue(start: offset) as FileIdBothDirectoryInformationHeader!
|
||||
case .fileNamesInformation:
|
||||
headersize = MemoryLayout<FileNamesInformationHeader>.size
|
||||
let headerData = buffer.subdata(in: offset..<(offset + headersize))
|
||||
let h: FileNamesInformationHeader = decode(headerData)
|
||||
header = h
|
||||
header = buffer.scanValue(start: offset) as FileNamesInformationHeader!
|
||||
default:
|
||||
return []
|
||||
}
|
||||
let fnData = buffer.subdata(in: (offset + headersize)..<(offset + headersize + Int(header.fileNameLength)))
|
||||
let fileName = String(data: fnData, encoding: .utf16) ?? ""
|
||||
let headersize = MemoryLayout.size(ofValue: header)
|
||||
let fileName = buffer.scanString(start: headersize, length: Int(header.fileNameLength), encoding: .utf16) ?? ""
|
||||
result.append((header: header, fileName: fileName))
|
||||
if header.nextEntryOffset == 0 {
|
||||
break
|
||||
@@ -115,8 +96,8 @@ extension SMB2 {
|
||||
}
|
||||
|
||||
init? (data: Data) {
|
||||
let offset = Int(decode(data.subdata(in: 2..<4)) as UInt16)
|
||||
let length = Int(decode(data.subdata(in: 4..<8)) as UInt32)
|
||||
let offset = Int(data.scanValue(start: 2) as UInt16!)
|
||||
let length = Int(data.scanValue(start: 4) as UInt32!)
|
||||
guard data.count > offset + length else {
|
||||
return nil
|
||||
}
|
||||
@@ -126,7 +107,7 @@ extension SMB2 {
|
||||
|
||||
// MARK: SMB2 Query Info
|
||||
|
||||
struct QueryInfoRequest: SMBRequest {
|
||||
struct QueryInfoRequest: SMBRequestBody {
|
||||
let header: Header
|
||||
let buffer: Data?
|
||||
|
||||
@@ -143,8 +124,8 @@ extension SMB2 {
|
||||
}
|
||||
let strLength = UInt8(strData.count)
|
||||
let nextOffset = UInt32(4 + 1 + strData.count)
|
||||
var data = encode(nextOffset)
|
||||
data.append(encode(strLength))
|
||||
var data = Data(value: nextOffset)
|
||||
data.append(Data(value: strLength))
|
||||
data.append(strData)
|
||||
data.count += 1
|
||||
let padSize = (data.count) % 4
|
||||
@@ -170,8 +151,7 @@ extension SMB2 {
|
||||
// TODO: Implement QUOTA_INFO init
|
||||
|
||||
func data() -> Data {
|
||||
let headerData = encode(header)
|
||||
var result = headerData
|
||||
var result = Data(value: header)
|
||||
if let buffer = buffer {
|
||||
result.append(buffer)
|
||||
}
|
||||
@@ -204,12 +184,11 @@ extension SMB2 {
|
||||
}
|
||||
}
|
||||
|
||||
struct QueryInfoResponse: SMBResponse {
|
||||
struct QueryInfoResponse: SMBResponseBody {
|
||||
let buffer: Data
|
||||
|
||||
init?(data: Data) {
|
||||
let structSizeData = data.subdata(in: 0..<2)
|
||||
let structSize: UInt16 = decode(structSizeData)
|
||||
let structSize: UInt16 = data.scanValue()!
|
||||
guard structSize == 9 else {
|
||||
return nil
|
||||
}
|
||||
@@ -217,10 +196,9 @@ extension SMB2 {
|
||||
/*let offsetData = data.subdataWithRange(NSRange(location: 2, length: 2))
|
||||
let offset: UInt16 = decode(offsetData)*/
|
||||
|
||||
let lengthData = data.subdata(in: 4..<8)
|
||||
let length = Int(decode(lengthData) as UInt32)
|
||||
let length = Int(data.scanValue(start: 4) as UInt32!)
|
||||
|
||||
guard data.count >= 8 + Int(length) else {
|
||||
guard data.count >= 8 + length else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -228,40 +206,38 @@ extension SMB2 {
|
||||
}
|
||||
|
||||
var asAccessInformation: FileAccessInformation {
|
||||
return decode(buffer)
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asAlignmentInformation: FileAlignmentInformation {
|
||||
return decode(buffer)
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asAllInformation: (header: FileAllInformationHeader, name: String) {
|
||||
let header: FileAllInformationHeader = decode(buffer)
|
||||
let header: FileAllInformationHeader = buffer.scanValue()!
|
||||
let headersize = MemoryLayout<FileAllInformationHeader>.size
|
||||
let nameData = buffer.subdata(in: headersize..<(headersize + Int(header.nameLength)))
|
||||
let name = String(data: nameData, encoding: .utf16) ?? ""
|
||||
let name = buffer.scanString(start: headersize, length: Int(header.nameLength), encoding: .utf16) ?? ""
|
||||
return (header, name)
|
||||
}
|
||||
|
||||
var asAlternateNameInformation: String {
|
||||
let b = (buffer as NSData).bytes.bindMemory(to: CChar.self, capacity: buffer.count)
|
||||
return String(cString: b, encoding: .utf16) ?? ""
|
||||
return buffer.scanString(start: 0, length: buffer.count, encoding: .utf16) ?? ""
|
||||
}
|
||||
|
||||
var asAttributeTagInformation: FileAttributeTagInformation {
|
||||
return decode(buffer)
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asBasicInformation: FileBasicInformation {
|
||||
return decode(buffer)
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asCompressionInformation: FileCompressionInformation {
|
||||
return decode(buffer)
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asEaInformation: FileEaInformation {
|
||||
return decode(buffer)
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asFullEaInformation: FileFullEaInformation {
|
||||
@@ -270,83 +246,80 @@ extension SMB2 {
|
||||
}
|
||||
|
||||
var asInternalInformation: FileInternalInformation {
|
||||
return decode(buffer)
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asModeInformation: FileModeInformation {
|
||||
return decode(buffer)
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asNetworkOpenInformation: FileNetworkOpenInformation {
|
||||
return decode(buffer)
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asPipeInformation: FilePipeInformation {
|
||||
return decode(buffer)
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asPipeLocalInformation: FilePipeLocalInformation {
|
||||
return decode(buffer)
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asPipeRemoteInformation: FilePipeRemoteInformation {
|
||||
return decode(buffer)
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asPositionInformation: FilePositionInformation {
|
||||
return decode(buffer)
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asStandardInformation: FileStandardInformation {
|
||||
return decode(buffer)
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asStreamInformation: (header: FileStreamInformationHeader, name: String) {
|
||||
let header: FileStreamInformationHeader = decode(buffer)
|
||||
let header: FileStreamInformationHeader = buffer.scanValue()!
|
||||
let headersize = MemoryLayout<FileStreamInformationHeader>.size
|
||||
let nameData = buffer.subdata(in: headersize..<(headersize + Int(header.streamNameLength)))
|
||||
let name = String(data: nameData, encoding: .utf16) ?? ""
|
||||
let name = buffer.scanString(start: headersize, length: Int(header.streamNameLength), encoding: .utf16) ?? ""
|
||||
return (header, name)
|
||||
}
|
||||
|
||||
var asFsVolumeInformation: (header: FileFsVolumeInformationHeader, name: String) {
|
||||
let header: FileFsVolumeInformationHeader = decode(buffer)
|
||||
let header: FileFsVolumeInformationHeader = buffer.scanValue()!
|
||||
let headersize = MemoryLayout<FileFsVolumeInformationHeader>.size
|
||||
let nameData = buffer.subdata(in: headersize..<(headersize + Int(header.labelLength)))
|
||||
let name = String(data: nameData, encoding: .utf16) ?? ""
|
||||
let name = buffer.scanString(start: headersize, length: Int(header.labelLength), encoding: .utf16) ?? ""
|
||||
return (header, name)
|
||||
}
|
||||
|
||||
var asFsSizeInformation: FileFsSizeInformation {
|
||||
return decode(buffer)
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asFsDeviceInformation: FileFsDeviceInformation {
|
||||
return decode(buffer)
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asFsAttributeInformation: (header: FileFsAttributeInformationHeader, name: String) {
|
||||
let header: FileFsAttributeInformationHeader = decode(buffer)
|
||||
let header: FileFsAttributeInformationHeader = buffer.scanValue()!
|
||||
let headersize = MemoryLayout<FileFsAttributeInformationHeader>.size
|
||||
let nameData = buffer.subdata(in: headersize..<(headersize + Int(header.nameLength)))
|
||||
let name = String(data: nameData, encoding: .utf16) ?? ""
|
||||
let name = buffer.scanString(start: headersize, length: Int(header.nameLength), encoding: .utf16) ?? ""
|
||||
return (header, name)
|
||||
}
|
||||
|
||||
var asFsControlInformation: FileFsControlInformation {
|
||||
return decode(buffer)
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asFsFullSizeInformation: FileFsFullSizeInformation {
|
||||
return decode(buffer)
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asFsObjectIdInformation: FileFsObjectIdInformation {
|
||||
return decode(buffer)
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
|
||||
var asFsSectorSizeInformation: FileFsSectorSizeInformation {
|
||||
return decode(buffer)
|
||||
return buffer.scanValue()!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol SMB2FilesInformationHeader: SMBResponse {
|
||||
protocol SMB2FilesInformationHeader: SMBResponseBody {
|
||||
var nextEntryOffset: UInt32 { get }
|
||||
var fileIndex: UInt32 { get }
|
||||
var fileNameLength : UInt32 { get }
|
||||
@@ -127,10 +127,6 @@ extension SMB2 {
|
||||
let allocationSize: UInt64
|
||||
let fileAttributes: FileAttributes
|
||||
let fileNameLength : UInt32
|
||||
|
||||
init?(data: Data) {
|
||||
self = decode(data)
|
||||
}
|
||||
}
|
||||
|
||||
struct FileFullDirectoryInformationHeader: SMB2FilesInformationHeader {
|
||||
@@ -145,10 +141,6 @@ extension SMB2 {
|
||||
let fileAttributes: FileAttributes
|
||||
let fileNameLength : UInt32
|
||||
let extendedAttributesSize: UInt32
|
||||
|
||||
init?(data: Data) {
|
||||
self = decode(data)
|
||||
}
|
||||
}
|
||||
|
||||
struct FileIdFullDirectoryInformationHeader: SMB2FilesInformationHeader {
|
||||
@@ -165,10 +157,6 @@ extension SMB2 {
|
||||
let extendedAttributesSize: UInt32
|
||||
fileprivate let reserved: UInt32
|
||||
let fileId: FileId
|
||||
|
||||
init?(data: Data) {
|
||||
self = decode(data)
|
||||
}
|
||||
}
|
||||
|
||||
struct FileBothDirectoryInformationHeader: SMB2FilesInformationHeader {
|
||||
@@ -187,14 +175,9 @@ extension SMB2 {
|
||||
fileprivate let reserved: UInt8
|
||||
fileprivate let _shortName: FileShortNameType
|
||||
var shortName: String? {
|
||||
let s = encode(_shortName)
|
||||
var d = s
|
||||
d.count = Int(shortNameLen)
|
||||
return String(data: d, encoding: .utf16)
|
||||
}
|
||||
|
||||
init?(data: Data) {
|
||||
self = decode(data)
|
||||
var data = Data(value: _shortName)
|
||||
data.count = Int(shortNameLen)
|
||||
return String(data: data, encoding: .utf16)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,27 +197,18 @@ extension SMB2 {
|
||||
fileprivate let reserved: UInt8
|
||||
fileprivate let _shortName: FileShortNameType
|
||||
var shortName: String? {
|
||||
let s = encode(_shortName)
|
||||
var d = s
|
||||
d.count = Int(shortNameLen)
|
||||
return String(data: d, encoding: .utf16)
|
||||
var data = Data(value: _shortName)
|
||||
data.count = Int(shortNameLen)
|
||||
return String(data: data, encoding: .utf16)
|
||||
}
|
||||
fileprivate let reserved2: UInt16
|
||||
let fileId : FileId
|
||||
|
||||
init?(data: Data) {
|
||||
self = decode(data)
|
||||
}
|
||||
}
|
||||
|
||||
struct FileNamesInformationHeader: SMB2FilesInformationHeader {
|
||||
let nextEntryOffset: UInt32
|
||||
let fileIndex: UInt32
|
||||
let fileNameLength : UInt32
|
||||
|
||||
init?(data: Data) {
|
||||
self = decode(data)
|
||||
}
|
||||
}
|
||||
|
||||
typealias FileShortNameType = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||
|
||||
@@ -11,7 +11,7 @@ import Foundation
|
||||
extension SMB2 {
|
||||
// MARK: SMB2 Negotiating
|
||||
|
||||
struct NegotiateRequest: SMBRequest {
|
||||
struct NegotiateRequest: SMBRequestBody {
|
||||
let header: NegotiateRequest.Header
|
||||
let dialects: [UInt16]
|
||||
let contexts: [(type: NegotiateContextType, data: Data)]
|
||||
@@ -43,13 +43,11 @@ extension SMB2 {
|
||||
|
||||
var contextData = Data()
|
||||
for context in contexts {
|
||||
var contextType = context.type.rawValue
|
||||
contextData.append(UnsafeBufferPointer(start: &contextType, count: 2))
|
||||
var dataLen = UInt16(context.data.count)
|
||||
contextData.append(Data(value: context.type.rawValue))
|
||||
contextData.count += 4
|
||||
contextData.append(UnsafeBufferPointer(start: &dataLen, count: 2))
|
||||
contextData.append(Data(value: UInt16(context.data.count)))
|
||||
}
|
||||
var result = encode(&header)
|
||||
var result = Data(value: header)
|
||||
result.append(dialectData as Data)
|
||||
result.append(contextData as Data)
|
||||
return result
|
||||
@@ -91,16 +89,16 @@ extension SMB2 {
|
||||
}
|
||||
}
|
||||
|
||||
struct NegotiateResponse: SMBResponse {
|
||||
struct NegotiateResponse: SMBResponseBody {
|
||||
let header: NegotiateResponse.Header
|
||||
let buffer: Data?
|
||||
let contexts: [(type: NegotiateContextType, data: Data)]
|
||||
|
||||
init? (data: Data) {
|
||||
if data.count < 64 {
|
||||
guard data.count >= 64 else {
|
||||
return nil
|
||||
}
|
||||
self.header = decode(data)
|
||||
self.header = data.scanValue()!
|
||||
if Int(header.size) != 65 {
|
||||
return nil
|
||||
}
|
||||
@@ -176,7 +174,7 @@ extension SMB2 {
|
||||
|
||||
// MARK: SMB2 Session Setup
|
||||
|
||||
struct SessionSetupRequest: SMBRequest {
|
||||
struct SessionSetupRequest: SMBRequestBody {
|
||||
let header: SessionSetupRequest.Header
|
||||
let buffer: Data?
|
||||
|
||||
@@ -194,7 +192,7 @@ extension SMB2 {
|
||||
var header = self.header
|
||||
header.bufferOffset = UInt16(MemoryLayout<SMB2.Header>.size + MemoryLayout<SessionSetupRequest.Header>.size)
|
||||
header.bufferLength = UInt16(buffer?.count ?? 0)
|
||||
var result = encode(&header)
|
||||
var result = Data(value: header)
|
||||
if let buffer = self.buffer {
|
||||
result.append(buffer)
|
||||
}
|
||||
@@ -235,15 +233,15 @@ extension SMB2 {
|
||||
}
|
||||
}
|
||||
|
||||
struct SessionSetupResponse: SMBResponse {
|
||||
struct SessionSetupResponse: SMBResponseBody {
|
||||
let header: SessionSetupResponse.Header
|
||||
let buffer: Data?
|
||||
|
||||
init? (data: Data) {
|
||||
if data.count < 64 {
|
||||
guard data.count >= 64 else {
|
||||
return nil
|
||||
}
|
||||
self.header = decode(data)
|
||||
self.header = data.scanValue()!
|
||||
if Int(header.size) != 9 {
|
||||
return nil
|
||||
}
|
||||
@@ -289,7 +287,7 @@ extension SMB2 {
|
||||
|
||||
// MARK: SMB2 Log off
|
||||
|
||||
struct LogOff: SMBRequest, SMBResponse {
|
||||
struct LogOff: SMBRequestBody, SMBResponseBody {
|
||||
let size: UInt16
|
||||
let reserved: UInt16
|
||||
|
||||
@@ -297,19 +295,11 @@ extension SMB2 {
|
||||
self.size = 4
|
||||
self.reserved = 0
|
||||
}
|
||||
|
||||
init? (data: Data) {
|
||||
self = decode(data)
|
||||
}
|
||||
|
||||
func data() -> Data {
|
||||
return encode(self)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: SMB2 Echo
|
||||
|
||||
struct Echo: SMBRequest, SMBResponse {
|
||||
struct Echo: SMBRequestBody, SMBResponseBody {
|
||||
let size: UInt16
|
||||
let reserved: UInt16
|
||||
|
||||
@@ -317,13 +307,5 @@ extension SMB2 {
|
||||
self.size = 4
|
||||
self.reserved = 0
|
||||
}
|
||||
|
||||
init? (data: Data) {
|
||||
self = decode(data)
|
||||
}
|
||||
|
||||
func data() -> Data {
|
||||
return encode(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,14 @@ import Foundation
|
||||
|
||||
extension SMB2 {
|
||||
// MARK: SMB2 Set Info
|
||||
struct SetInfoRequest: SMBRequest {
|
||||
struct SetInfoRequest: SMBRequestBody {
|
||||
let header: Header
|
||||
let buffer: Data?
|
||||
|
||||
|
||||
|
||||
func data() -> Data {
|
||||
return Data()
|
||||
var result = Data(value: header)
|
||||
result.append(buffer ?? Data())
|
||||
return result
|
||||
}
|
||||
|
||||
struct Header {
|
||||
@@ -32,15 +32,11 @@ extension SMB2 {
|
||||
}
|
||||
}
|
||||
|
||||
struct SetInfoResponse: SMBResponse {
|
||||
struct SetInfoResponse: SMBResponseBody {
|
||||
let size: UInt16
|
||||
|
||||
init() {
|
||||
self.size = 2
|
||||
}
|
||||
|
||||
init? (data: Data) {
|
||||
self = decode(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import Foundation
|
||||
extension SMB2 {
|
||||
// MARK: SMB2 Tree Connect
|
||||
|
||||
struct TreeConnectRequest: SMBRequest {
|
||||
struct TreeConnectRequest: SMBRequestBody {
|
||||
let header: TreeConnectRequest.Header
|
||||
let buffer: Data?
|
||||
var path: String {
|
||||
@@ -34,7 +34,7 @@ extension SMB2 {
|
||||
var header = self.header
|
||||
header.pathOffset = UInt16(MemoryLayout<SMB2.Header>.size + MemoryLayout<TreeConnectRequest.Header>.size)
|
||||
header.pathLength = UInt16(buffer?.count ?? 0)
|
||||
var result = encode(&header)
|
||||
var result = Data(value: header)
|
||||
if let buffer = self.buffer {
|
||||
result.append(buffer)
|
||||
}
|
||||
@@ -66,7 +66,7 @@ extension SMB2 {
|
||||
}
|
||||
}
|
||||
|
||||
struct TreeConnectResponse: SMBResponse {
|
||||
struct TreeConnectResponse: SMBResponseBody {
|
||||
let size: UInt16 // = 16
|
||||
fileprivate let _type: UInt8
|
||||
var type: ShareType {
|
||||
@@ -77,13 +77,6 @@ extension SMB2 {
|
||||
let capabilities: TreeConnectResponse.Capabilities
|
||||
let maximalAccess: FileAccessMask
|
||||
|
||||
init? (data: Data) {
|
||||
if data.count != 16 {
|
||||
return nil
|
||||
}
|
||||
self = decode(data)
|
||||
}
|
||||
|
||||
enum ShareType: UInt8 {
|
||||
case UNKNOWN = 0x00
|
||||
case DISK = 0x01
|
||||
@@ -131,7 +124,7 @@ extension SMB2 {
|
||||
|
||||
// MARK: SMB2 Tree Disconnect
|
||||
|
||||
struct TreeDisconnect: SMBRequest, SMBResponse {
|
||||
struct TreeDisconnect: SMBRequestBody, SMBResponseBody {
|
||||
let size: UInt16
|
||||
let reserved: UInt16
|
||||
|
||||
@@ -139,13 +132,5 @@ extension SMB2 {
|
||||
self.size = 4
|
||||
self.reserved = 0
|
||||
}
|
||||
|
||||
init? (data: Data) {
|
||||
self = decode(data)
|
||||
}
|
||||
|
||||
func data() -> Data {
|
||||
return encode(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,24 +8,6 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
internal func encode<T>(_ value: inout T) -> Data {
|
||||
return withUnsafePointer(to: &value) { p in
|
||||
Data(bytes: p, count: MemoryLayout.size(ofValue: value))
|
||||
}
|
||||
}
|
||||
|
||||
internal func encode<T>(_ value: T) -> Data {
|
||||
var value = value
|
||||
return encode(&value)
|
||||
}
|
||||
|
||||
internal func decode<T>(_ data: Data) -> T {
|
||||
let pointer = UnsafeMutablePointer<T>.allocate(capacity: MemoryLayout<T.Type>.size)
|
||||
(data as NSData).getBytes(pointer, length: MemoryLayout<T.Type>.size)
|
||||
|
||||
return pointer.move()
|
||||
}
|
||||
|
||||
protocol FileProviderSMBHeader {
|
||||
var protocolID: UInt32 { get }
|
||||
static var protocolConst: UInt32 { get }
|
||||
|
||||
@@ -10,7 +10,7 @@ import Foundation
|
||||
|
||||
/// Error Types and Description
|
||||
|
||||
public enum NTStatus: UInt32, Error, CustomStringConvertible {
|
||||
enum NTStatus: UInt32, Error, CustomStringConvertible {
|
||||
case SUCCESS = 0x00000000
|
||||
case NOT_IMPLEMENTED = 0xC0000002
|
||||
case INVALID_DEVICE_REQUEST = 0xC0000010
|
||||
|
||||
+368
-280
@@ -8,114 +8,131 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public final class WebDavFileObject: FileObject {
|
||||
public let contentType: String
|
||||
public let entryTag: String?
|
||||
|
||||
// codebeat:disable[ARITY]
|
||||
public init(absoluteURL: URL, name: String, path: String, size: Int64 = -1, contentType: String = "", createdDate: Date? = nil, modifiedDate: Date? = nil, fileType: FileType = .regular, isHidden: Bool = false, isReadOnly: Bool = false, entryTag: String? = nil) {
|
||||
self.contentType = contentType
|
||||
self.entryTag = entryTag
|
||||
super.init(absoluteURL: absoluteURL, name: name, path: path, size: size, createdDate: createdDate, modifiedDate: modifiedDate, fileType: fileType, isHidden: isHidden, isReadOnly: isReadOnly)
|
||||
}
|
||||
// codebeat:enable[ARITY]
|
||||
}
|
||||
|
||||
// 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: NSObject, FileProviderBasic {
|
||||
open static let type: String = "WebDAV"
|
||||
open let isPathRelative: Bool = true
|
||||
/**
|
||||
Allows accessing to WebDAV server files. This provider doesn't cache or save files internally, however you can
|
||||
set `useCache` and `cache` properties to use Foundation `NSURLCache` system.
|
||||
|
||||
WebDAV system supported by many cloud services including [Box.net](https://www.box.com/home)
|
||||
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: FileProviderBasicRemote {
|
||||
open class var type: String { return "WebDAV" }
|
||||
open let baseURL: URL?
|
||||
open var currentPath: String = ""
|
||||
open var dispatch_queue: DispatchQueue {
|
||||
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?
|
||||
|
||||
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?
|
||||
fileprivate var session: URLSession {
|
||||
public var session: URLSession {
|
||||
if _session == nil {
|
||||
self.sessionDelegate = SessionDelegate(fileProvider: self, credential: credential)
|
||||
let queue = OperationQueue()
|
||||
//queue.underlyingQueue = dispatch_queue
|
||||
_session = URLSession(configuration: URLSessionConfiguration.default, delegate: sessionDelegate as URLSessionDownloadDelegate?, delegateQueue: queue)
|
||||
let config = URLSessionConfiguration.default
|
||||
config.urlCache = cache
|
||||
config.requestCachePolicy = .returnCacheDataElseLoad
|
||||
_session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDownloadDelegate?, delegateQueue: queue)
|
||||
}
|
||||
return _session!
|
||||
}
|
||||
|
||||
public init? (baseURL: URL, credential: URLCredential?) {
|
||||
/**
|
||||
Initializes WebDAV provider.
|
||||
|
||||
- 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? = nil) {
|
||||
if !["http", "https"].contains(baseURL.uw_scheme.lowercased()) {
|
||||
return nil
|
||||
}
|
||||
self.baseURL = baseURL
|
||||
dispatch_queue = DispatchQueue(label: "FileProvider.\(WebDAVFileProvider.type)", attributes: DispatchQueue.Attributes.concurrent)
|
||||
//let url = baseURL.uw_absoluteString
|
||||
self.baseURL = baseURL.path.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")
|
||||
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 {
|
||||
_session?.invalidateAndCancel()
|
||||
if fileProviderCancelTasksOnInvalidating {
|
||||
_session?.invalidateAndCancel()
|
||||
} else {
|
||||
_session?.finishTasksAndInvalidate()
|
||||
}
|
||||
}
|
||||
|
||||
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
|
||||
let url = absoluteURL(path)
|
||||
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")
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
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)
|
||||
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
|
||||
}
|
||||
var fileObjects = [WebDavFileObject]()
|
||||
if let data = data {
|
||||
let xresponse = self.parseXMLResponse(data)
|
||||
var fileObjects = [WebDavFileObject]()
|
||||
for attr in xresponse {
|
||||
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(self.mapToFileObject(attr))
|
||||
fileObjects.append(WebDavFileObject(attr))
|
||||
}
|
||||
completionHandler(fileObjects, responseError ?? error)
|
||||
return
|
||||
}
|
||||
completionHandler([], responseError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
completionHandler(fileObjects, responseError ?? error)
|
||||
})
|
||||
}
|
||||
|
||||
open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
|
||||
let url = absoluteURL(path)
|
||||
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")
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
runDataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var responseError: FileProviderWebDavError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode, code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderWebDavError(code: rCode, url: url)
|
||||
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
|
||||
}
|
||||
if let data = data {
|
||||
let xresponse = self.parseXMLResponse(data)
|
||||
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
|
||||
if let attr = xresponse.first {
|
||||
completionHandler(self.mapToFileObject(attr), responseError ?? error)
|
||||
completionHandler(WebDavFileObject(attr), responseError ?? error)
|
||||
return
|
||||
}
|
||||
}
|
||||
completionHandler(nil, responseError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
|
||||
open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) {
|
||||
@@ -131,19 +148,63 @@ open class WebDAVFileProvider: NSObject, FileProviderBasic {
|
||||
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")
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
runDataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var totalSize: Int64 = -1
|
||||
var usedSize: Int64 = 0
|
||||
if let data = data {
|
||||
let xresponse = self.parseXMLResponse(data)
|
||||
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
|
||||
if let attr = xresponse.first {
|
||||
let totalSize = Int64(attr.prop["quota-available-bytes"] ?? "")
|
||||
let usedSize = Int64(attr.prop["quota-used-bytes"] ?? "")
|
||||
completionHandler(totalSize ?? -1, usedSize ?? 0)
|
||||
return
|
||||
totalSize = Int64(attr.prop["quota-available-bytes"] ?? "") ?? -1
|
||||
usedSize = Int64(attr.prop["quota-used-bytes"] ?? "") ?? 0
|
||||
}
|
||||
}
|
||||
completionHandler(-1, 0)
|
||||
})
|
||||
task.resume()
|
||||
completionHandler(totalSize, usedSize)
|
||||
})
|
||||
}
|
||||
|
||||
open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [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)
|
||||
runDataTask(with: request, completionHandler: { (data, response, error) in
|
||||
// FIXME: paginating results
|
||||
var responseError: FileProviderWebDavError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
|
||||
}
|
||||
if let data = data {
|
||||
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
|
||||
var fileObjects = [WebDavFileObject]()
|
||||
for attr in xresponse {
|
||||
let fileObject = WebDavFileObject(attr)
|
||||
if !query.evaluate(with: fileObject.mapPredicate()) {
|
||||
continue
|
||||
}
|
||||
|
||||
fileObjects.append(fileObject)
|
||||
foundItemHandler?(fileObject)
|
||||
}
|
||||
completionHandler(fileObjects, responseError ?? error)
|
||||
return
|
||||
}
|
||||
completionHandler([], responseError ?? error)
|
||||
})
|
||||
}
|
||||
|
||||
open func isReachable(completionHandler: @escaping (Bool) -> Void) {
|
||||
var request = URLRequest(url: baseURL!)
|
||||
request.httpMethod = "PROPFIND"
|
||||
request.setValue("0", forHTTPHeaderField: "Depth")
|
||||
request.setValue("text/xml; charset=\"utf-8\"", forHTTPHeaderField: "Content-Type")
|
||||
request.httpBody = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<D:propfind xmlns:D=\"DAV:\">\n<D:prop><D:quota-available-bytes/><D:quota-used-bytes/></D:prop>\n</D:propfind>".data(using: .utf8)
|
||||
request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length")
|
||||
runDataTask(with: request, completionHandler: { (data, response, error) in
|
||||
let status = (response as? HTTPURLResponse)?.statusCode ?? 400
|
||||
completionHandler(status < 300)
|
||||
})
|
||||
}
|
||||
|
||||
open weak var fileOperationDelegate: FileOperationDelegate?
|
||||
@@ -151,161 +212,159 @@ open class WebDAVFileProvider: NSObject, FileProviderBasic {
|
||||
|
||||
extension WebDAVFileProvider: FileProviderOperations {
|
||||
@discardableResult
|
||||
public func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: .create(path: (atPath as NSString).appendingPathComponent(folderName) + "/")) ?? true == true else {
|
||||
open 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 = absoluteURL((atPath as NSString).appendingPathComponent(folderName) + "/")
|
||||
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)
|
||||
}
|
||||
if let response = response as? HTTPURLResponse, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) , code != .ok {
|
||||
completionHandler?(FileProviderWebDavError(code: code, url: url))
|
||||
return
|
||||
responseError = FileProviderWebDavError(code: rCode, path: url.relativePath, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
|
||||
}
|
||||
completionHandler?(responseError ?? error)
|
||||
self.delegateNotify(.create(path: (atPath as NSString).appendingPathComponent(folderName) + "/"), error: responseError ?? error)
|
||||
})
|
||||
self.delegateNotify(opType, error: responseError ?? error)
|
||||
})
|
||||
task.taskDescription = opType.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(tasks: [task])
|
||||
return RemoteOperationHandle(operationType: opType, tasks: [task])
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func create(file fileAttribs: FileObject, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: .create(path: path)) ?? true == true else {
|
||||
open 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 = absoluteURL(path)
|
||||
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)
|
||||
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
|
||||
}
|
||||
completionHandler?(responseError ?? error)
|
||||
self.delegateNotify(.create(path: (path as NSString).appendingPathComponent(fileAttribs.name)), error: responseError ?? error)
|
||||
})
|
||||
task.taskDescription = dictionaryToJSON(["type": "Create" as NSString, "source": (path as NSString).appendingPathComponent(fileAttribs.name) as NSString])
|
||||
self.delegateNotify(opType, error: responseError ?? error)
|
||||
})
|
||||
task.taskDescription = opType.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(tasks: [task])
|
||||
return RemoteOperationHandle(operationType: opType, tasks: [task])
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: .move(source: path, destination: toPath)) ?? true == true else {
|
||||
open 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.copyMoveItem(move: true, path: path, toPath: toPath, overwrite: overwrite, completionHandler: completionHandler)
|
||||
return self.doOperation(operation: opType, overwrite: overwrite, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: .copy(source: path, destination: toPath)) ?? true == true else {
|
||||
open 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.copyMoveItem(move: false, path: path, toPath: toPath, overwrite: overwrite, completionHandler: completionHandler)
|
||||
return self.doOperation(operation: opType, overwrite: overwrite, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
fileprivate func copyMoveItem(move:Bool, path: String, toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let url = absoluteURL(path)
|
||||
var request = URLRequest(url: url)
|
||||
if move {
|
||||
request.httpMethod = "MOVE"
|
||||
} else {
|
||||
@discardableResult
|
||||
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.remove(path: path)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
return self.doOperation(operation: opType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
func doOperation(operation opType: FileOperationType, overwrite: Bool? = nil, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let source = opType.source!
|
||||
let sourceURL = self.url(of: source)
|
||||
var request = URLRequest(url: sourceURL)
|
||||
if let dest = opType.destination {
|
||||
request.setValue(url(of:dest).absoluteString, forHTTPHeaderField: "Destination")
|
||||
}
|
||||
switch opType {
|
||||
case .copy:
|
||||
request.httpMethod = "COPY"
|
||||
case .move:
|
||||
request.httpMethod = "MOVE"
|
||||
case .remove:
|
||||
request.httpMethod = "DELETE"
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
request.setValue(absoluteURL(path).absoluteString, forHTTPHeaderField: "Destination")
|
||||
if !overwrite {
|
||||
|
||||
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) {
|
||||
defer {
|
||||
let op = move ? FileOperationType.move(source: path, destination: toPath) : .copy(source: path, destination: toPath)
|
||||
self.delegateNotify(op, error: error)
|
||||
if response.statusCode >= 300 {
|
||||
responseError = FileProviderWebDavError(code: code, path: source, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: sourceURL)
|
||||
}
|
||||
if code == .multiStatus, let data = data {
|
||||
let xresponses = self.parseXMLResponse(data)
|
||||
let xresponses = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
|
||||
for xresponse in xresponses where (xresponse.status ?? 0) >= 300 {
|
||||
completionHandler?(FileProviderWebDavError(code: code, url: url))
|
||||
let error = FileProviderWebDavError(code: code, path: source, errorDescription: String(data: data, encoding: .utf8), url: sourceURL)
|
||||
completionHandler?(error)
|
||||
}
|
||||
} else {
|
||||
completionHandler?(FileProviderWebDavError(code: code, url: url))
|
||||
}
|
||||
return
|
||||
}
|
||||
completionHandler?(error)
|
||||
})
|
||||
if (response as? HTTPURLResponse)?.statusCode ?? 0 != FileProviderHTTPErrorCode.multiStatus.rawValue {
|
||||
completionHandler?(responseError ?? error)
|
||||
}
|
||||
|
||||
self.delegateNotify(opType, error: responseError ?? error)
|
||||
})
|
||||
task.taskDescription = opType.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(tasks: [task])
|
||||
return RemoteOperationHandle(operationType: opType, tasks: [task])
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: .remove(path: path)) ?? true == true else {
|
||||
open 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
|
||||
}
|
||||
let url = absoluteURL(path)
|
||||
let url = self.url(of:toPath)
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "DELETE"
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
if let response = response as? HTTPURLResponse, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) {
|
||||
defer {
|
||||
self.delegateNotify(.remove(path: path), error: error)
|
||||
}
|
||||
if code == .multiStatus, let data = data {
|
||||
let xresponses = self.parseXMLResponse(data)
|
||||
for xresponse in xresponses where (xresponse.status ?? 0) >= 300 {
|
||||
completionHandler?(FileProviderWebDavError(code: code, url: url))
|
||||
}
|
||||
} else {
|
||||
completionHandler?(FileProviderWebDavError(code: code, url: url))
|
||||
}
|
||||
return
|
||||
}
|
||||
completionHandler?(error)
|
||||
})
|
||||
task.resume()
|
||||
return RemoteOperationHandle(tasks: [task])
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func copyItem(localFile: URL, to toPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: .copy(source: localFile.absoluteString, destination: toPath)) ?? true == true else {
|
||||
return nil
|
||||
if !overwrite {
|
||||
request.setValue("F", forHTTPHeaderField: "Overwrite")
|
||||
}
|
||||
let url = absoluteURL(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)
|
||||
responseError = FileProviderWebDavError(code: rCode, path: toPath, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
|
||||
}
|
||||
completionHandler?(responseError ?? error)
|
||||
self.delegateNotify(.move(source: localFile.absoluteString, destination: toPath), error: responseError ?? error)
|
||||
self.delegateNotify(opType, error: responseError ?? error)
|
||||
})
|
||||
task.taskDescription = dictionaryToJSON(["type": "Copy" as NSString, "source": localFile.absoluteString as NSString, "dest": toPath as NSString])
|
||||
task.taskDescription = opType.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(tasks: [task])
|
||||
return RemoteOperationHandle(operationType: opType, tasks: [task])
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: .copy(source: path, destination: toLocalURL.absoluteString)) ?? true == true else {
|
||||
open 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 = absoluteURL(path)
|
||||
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)
|
||||
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: nil, url: url)
|
||||
}
|
||||
if let sourceFileURL = sourceFileURL {
|
||||
do {
|
||||
@@ -316,56 +375,64 @@ extension WebDAVFileProvider: FileProviderOperations {
|
||||
}
|
||||
}
|
||||
completionHandler?(responseError ?? error)
|
||||
self.delegateNotify(opType, error: responseError ?? error)
|
||||
})
|
||||
task.taskDescription = dictionaryToJSON(["type": "Copy" as NSString, "source": path as NSString, "dest": toLocalURL.absoluteString as NSString])
|
||||
task.taskDescription = opType.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(tasks: [task])
|
||||
return RemoteOperationHandle(operationType: opType, tasks: [task])
|
||||
}
|
||||
}
|
||||
|
||||
extension WebDAVFileProvider: FileProviderReadWrite {
|
||||
@discardableResult
|
||||
public func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
return self.contents(path: path, offset: 0, length: -1, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
let url = absoluteURL(path)
|
||||
open 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)", forHTTPHeaderField: "Range")
|
||||
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
|
||||
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)
|
||||
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url)
|
||||
}
|
||||
completionHandler(data, responseError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
return RemoteOperationHandle(tasks: [task])
|
||||
})
|
||||
return handle
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func writeContents(path: String, contents data: Data, atomically: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: .modify(path: path)) ?? true == true else {
|
||||
open func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.modify(path: path)
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
// FIXME: lock destination before writing process
|
||||
let url = atomically ? absoluteURL(path).appendingPathExtension("tmp") : absoluteURL(path)
|
||||
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.absoluteURL(path))
|
||||
responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: self.url(of: path))
|
||||
}
|
||||
defer {
|
||||
self.delegateNotify(.modify(path: path), error: responseError ?? error)
|
||||
self.delegateNotify(opType, error: responseError ?? error)
|
||||
}
|
||||
if let error = error {
|
||||
completionHandler?(error)
|
||||
@@ -375,45 +442,12 @@ extension WebDAVFileProvider: FileProviderReadWrite {
|
||||
self.moveItem(path: (path as NSString).appendingPathExtension("tmp")!, to: path, completionHandler: completionHandler)
|
||||
}
|
||||
})
|
||||
task.taskDescription = dictionaryToJSON(["type": "Modify" as NSString, "source": path as NSString])
|
||||
task.resume()
|
||||
return RemoteOperationHandle(tasks: [task])
|
||||
}
|
||||
|
||||
public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
|
||||
let url = absoluteURL(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")
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
// FIXME: paginating results
|
||||
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 data = data {
|
||||
let xresponse = self.parseXMLResponse(data)
|
||||
var fileObjects = [WebDavFileObject]()
|
||||
for attr in xresponse {
|
||||
let path = attr.href.path
|
||||
if !((path as NSString).lastPathComponent.contains(query)) {
|
||||
continue
|
||||
}
|
||||
let fileObject = self.mapToFileObject(attr)
|
||||
fileObjects.append(fileObject)
|
||||
foundItemHandler?(fileObject)
|
||||
}
|
||||
completionHandler(fileObjects, responseError ?? error)
|
||||
return
|
||||
}
|
||||
completionHandler([], responseError ?? error)
|
||||
})
|
||||
task.taskDescription = opType.json
|
||||
task.resume()
|
||||
return RemoteOperationHandle(operationType: opType, tasks: [task])
|
||||
}
|
||||
|
||||
/*
|
||||
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.
|
||||
@@ -421,49 +455,57 @@ extension WebDAVFileProvider: FileProviderReadWrite {
|
||||
* A messy approach is listing a directory with an interval period and compare
|
||||
* with previous results
|
||||
*/
|
||||
NotImplemented()
|
||||
}
|
||||
fileprivate func unregisterNotifcation(path: String) {
|
||||
|
||||
}
|
||||
NotImplemented()
|
||||
}*/
|
||||
// TODO: implements methods for lock mechanism
|
||||
}
|
||||
|
||||
extension WebDAVFileProvider: FileProvider {}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: WEBDAV XML response implementation
|
||||
|
||||
internal extension WebDAVFileProvider {
|
||||
struct DavResponse {
|
||||
let href: URL
|
||||
let hrefString: String
|
||||
let status: Int?
|
||||
let prop: [String: String]
|
||||
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
|
||||
let status: Int?
|
||||
let prop: [String: String]
|
||||
|
||||
fileprivate func parseXMLResponse(_ response: Data) -> [DavResponse] {
|
||||
var result = [DavResponse]()
|
||||
do {
|
||||
let xml = try AEXMLDocument(xml: response)
|
||||
var rootnode = xml.root
|
||||
var responsetag = "response"
|
||||
for node in rootnode.all ?? [] where node.name.lowercased().hasSuffix("multistatus") {
|
||||
rootnode = node
|
||||
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
|
||||
}
|
||||
for node in rootnode.children where node.name.lowercased().hasSuffix("response") {
|
||||
responsetag = node.name
|
||||
break
|
||||
}
|
||||
for responseNode in rootnode[responsetag].all ?? [] {
|
||||
if let davResponse = mapNodeToDavResponse(responseNode) {
|
||||
result.append(davResponse)
|
||||
}
|
||||
}
|
||||
} catch _ {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fileprivate func mapNodeToDavResponse(_ node: AEXMLElement) -> DavResponse? {
|
||||
|
||||
// find node names with namespace
|
||||
var hreftag = "href"
|
||||
var statustag = "status"
|
||||
var propstattag = "propstat"
|
||||
@@ -478,70 +520,116 @@ internal extension WebDAVFileProvider {
|
||||
propstattag = node.name
|
||||
}
|
||||
}
|
||||
let href = node[hreftag].value
|
||||
if let href = href, let hrefURL = URL(string: href) {
|
||||
var status: Int?
|
||||
let statusDesc = (node[statustag].string).components(separatedBy: " ")
|
||||
if statusDesc.count > 2 {
|
||||
status = Int(statusDesc[1])
|
||||
|
||||
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
|
||||
|
||||
guard let href = hrefURL?.standardized else { return nil }
|
||||
|
||||
// reading status and properties
|
||||
var status: Int?
|
||||
let statusDesc = (node[statustag].string).components(separatedBy: " ")
|
||||
if statusDesc.count > 2 {
|
||||
status = Int(statusDesc[1])
|
||||
}
|
||||
var propDic = [String: String]()
|
||||
let propStatNode = node[propstattag]
|
||||
for node in propStatNode.children where node.name.lowercased().hasSuffix("status"){
|
||||
statustag = node.name
|
||||
break
|
||||
}
|
||||
let statusDesc2 = (propStatNode[statustag].string).components(separatedBy: " ")
|
||||
if statusDesc2.count > 2 {
|
||||
status = Int(statusDesc2[1])
|
||||
}
|
||||
var proptag = "prop"
|
||||
for tnode in propStatNode.children where tnode.name.lowercased().hasSuffix("prop") {
|
||||
proptag = tnode.name
|
||||
break
|
||||
}
|
||||
for propItemNode in propStatNode[proptag].children {
|
||||
propDic[propItemNode.name.components(separatedBy: ":").last!.lowercased()] = propItemNode.value
|
||||
if propItemNode.name.hasSuffix("resourcetype") && propItemNode.xml.contains("collection") {
|
||||
propDic["getcontenttype"] = "httpd/unix-directory"
|
||||
}
|
||||
var propDic = [String: String]()
|
||||
let propStatNode = node[propstattag]
|
||||
for node in propStatNode.children where node.name.lowercased().hasSuffix("status"){
|
||||
statustag = node.name
|
||||
}
|
||||
self.href = href
|
||||
self.hrefString = hrefString
|
||||
self.status = status
|
||||
self.prop = propDic
|
||||
}
|
||||
|
||||
static func parse(xmlResponse: Data, baseURL: URL?) -> [DavResponse] {
|
||||
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
|
||||
}
|
||||
for node in rootnode.children where node.name.lowercased().hasSuffix("response") {
|
||||
responsetag = node.name
|
||||
break
|
||||
}
|
||||
let statusDesc2 = (propStatNode[statustag].string).components(separatedBy: " ")
|
||||
if statusDesc2.count > 2 {
|
||||
status = Int(statusDesc2[1])
|
||||
}
|
||||
var proptag = "prop"
|
||||
for tnode in propStatNode.children where tnode.name.lowercased().hasSuffix("prop") {
|
||||
proptag = tnode.name
|
||||
break
|
||||
}
|
||||
for propItemNode in propStatNode[proptag].children {
|
||||
propDic[propItemNode.name.components(separatedBy: ":").last!.lowercased()] = propItemNode.value
|
||||
if propItemNode.name.hasSuffix("resourcetype") && propItemNode.xml.contains("collection") {
|
||||
propDic["getcontenttype"] = "httpd/unix-directory"
|
||||
for responseNode in rootnode[responsetag].all ?? [] {
|
||||
if let davResponse = DavResponse(responseNode, baseURL: baseURL) {
|
||||
result.append(davResponse)
|
||||
}
|
||||
}
|
||||
return DavResponse(href: hrefURL, hrefString: href, status: status, prop: propDic)
|
||||
} catch _ {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
fileprivate func mapToFileObject(_ davResponse: DavResponse) -> WebDavFileObject {
|
||||
var href = davResponse.href
|
||||
if href.baseURL == nil {
|
||||
href = absoluteURL(href.path)
|
||||
}
|
||||
let name = davResponse.prop["displayname"] ?? (davResponse.hrefString.removingPercentEncoding! as NSString).lastPathComponent
|
||||
let size = Int64(davResponse.prop["getcontentlength"] ?? "-1") ?? NSURLSessionTransferSizeUnknown
|
||||
let createdDate = self.resolve(dateString: davResponse.prop["creationdate"] ?? "")
|
||||
let modifiedDate = self.resolve(dateString: davResponse.prop["getlastmodified"] ?? "")
|
||||
let contentType = davResponse.prop["getcontenttype"] ?? "octet/stream"
|
||||
let isDirectory = contentType == "httpd/unix-directory"
|
||||
let entryTag = davResponse.prop["getetag"]
|
||||
return WebDavFileObject(absoluteURL: href, name: name, path: href.path, size: size, contentType: contentType, createdDate: createdDate, modifiedDate: modifiedDate, fileType: isDirectory ? .directory : .regular, isHidden: false, isReadOnly: false, entryTag: entryTag)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
public struct FileProviderWebDavError: Error, CustomStringConvertible {
|
||||
public let code: FileProviderHTTPErrorCode
|
||||
public let url: URL
|
||||
/// 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 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.isHidden = (Int(davResponse.prop["ishidden"] ?? "0") ?? 0) > 0
|
||||
self.type = self.contentType == "httpd/unix-directory" ? .directory : .regular
|
||||
self.entryTag = davResponse.prop["getetag"]
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
return code.description
|
||||
/// MIME type of the file.
|
||||
open internal(set) var contentType: String {
|
||||
get {
|
||||
return allValues[.mimeType] as? String ?? ""
|
||||
}
|
||||
set {
|
||||
allValues[.mimeType] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP E-Tag, can be used to mark changed files.
|
||||
open internal(set) var entryTag: String? {
|
||||
get {
|
||||
return allValues[.entryTag] as? String
|
||||
}
|
||||
set {
|
||||
allValues[.entryTag] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error returned by WebDAV server when trying to access or do operations on a file or folder.
|
||||
public struct FileProviderWebDavError: FileProviderHTTPError {
|
||||
public let code: FileProviderHTTPErrorCode
|
||||
public let path: String
|
||||
public let errorDescription: String?
|
||||
/// URL of resource caused error.
|
||||
public let url: URL
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
Reference in New Issue
Block a user