Compare commits

..

37 Commits

Author SHA1 Message Date
Amir Abbas Mousavian 5a7dfa1039 Merge commit 'b84e6ea7abbec6f9a83144d2dcd6744b568d682f'
* commit 'b84e6ea7abbec6f9a83144d2dcd6744b568d682f':
  Updated Date
2017-02-02 13:28:08 +03:30
Amir Abbas Mousavian be7a370a42 Added Documentation, get contents handling in Local provider optimized
- Changed default thumbnail dimension to 64x64 in `LocalFileProvider`
2017-02-02 13:25:40 +03:30
Amir Abbas Mousavian b84e6ea7ab Updated Date 2017-02-02 01:03:48 +03:30
Amir Abbas Mousavian 82b1d2c450 Added new badges 2017-02-01 22:18:20 +03:30
Amir Abbas Mousavian c38ee1ccd3 standardized FileObject.path property
- FileObject.path now has heading slash in all scenarios
- fixes #27, WebDAV fileObject.path was not relative
2017-02-01 21:23:11 +03:30
Amir Abbas Mousavian 1a44df3fd7 Refactoring, better webdav response handling
- Added Dropbox copy from reference method
- refactored `mapToFileObject` methods into `FileObject` initializers
- fixed `requestDictionary` type to `[String: AnyObject]`
2017-02-01 14:08:28 +03:30
Amir Abbas Mousavian eff3725680 Added release version badge 2017-02-01 00:21:48 +03:30
Amir Abbas Mousavian 9368f636d3 Added carthage badge 2017-01-31 23:53:06 +03:30
Amir Abbas Mousavian 3f2fda638f bugfix: path in LocalFileObject is not relative 2017-01-31 23:12:29 +03:30
Amir Abbas Mousavian 6737f98978 Updated travis.yml 2017-01-31 19:07:11 +03:30
Amir Abbas Mousavian 3e55cc60f7 Replaced absoluteURL with relative url (resolved #27), improving performance.
- renamed `DropboxFileProvider.copyItem(path:toRemoteURL:) ` to `DropboxFileProvider.copyItem(remoteURL:toPath:)` due to logic
2017-01-31 17:19:45 +03:30
Alek Slater d7ae709cf7 contentURL was missing the trailing slash causing it to fail for all contentURL calls with the dropbox api (#26) 2017-01-31 13:05:20 +03:30
Amir Abbas Mousavian 4f6e3c7bd5 Uncommented CloudFileProvider decription 2017-01-31 02:11:28 +03:30
Amir Abbas Mousavian b121fe6182 rectified macOS and tvOS version check 2017-01-31 01:42:03 +03:30
Amir Abbas Mousavian 29b7f720f5 Added iCloud provider change monitor and CloudOperationHandle
- fixed copyItem functions for local interoperability
2017-01-31 01:35:38 +03:30
Amir Abbas Mousavian 067e14b76f Added CloudFileProvider and coordination to LocalFileProvider
- changed `operation_queue` type to `OperationQueue`
- added `LocalFileProvider.init(sharedContainerId:)`
- added `destination(ofSymbolicLink:)` to LocalFileProvider
- added expirationDate to `temporaryLink(to:)` methods
- known issue: `CloudFileProvider` monitoring is not implemented
- known issue: `CloudFileProvider` copy from local and to local works imperfect
2017-01-30 18:51:06 +03:30
Amir Abbas Mousavian 09e39ada82 Resolved issue #24, off-by-one bug in Range header 2017-01-30 18:37:25 +03:30
Amir Abbas 6889b302b7 Removed CloudProvider due to incompleteness 2017-01-27 02:47:11 +03:30
Amir Abbas 96ce12f226 Added iCloud Drive provider CloudFileProvider
- Refactored initializers
- Replaced `NSSearchPathForDirectoriesInDomains` with `FileManager.urls(for:In:)`
2017-01-27 02:13:51 +03:30
Amir Abbas 091fd14a88 OneDrive drive name bug fix. Added sorting method to [FileObject] array.
- now return `Error` for uploading a file with size higher than limit in OneDrive / Dropbox instead of asserting
2017-01-25 16:04:16 +03:30
Amir Abbas 10aac532f7 Fixed crashing bug 2017-01-18 10:20:10 +03:30
Amir Abbas 3aac0a88c4 Fixed path in url percent encoding 2017-01-16 23:28:32 +03:30
Amir Abbas 24355a4c6c Added OneDrive support
- removed unnecessary MediaPlayer.framework dependency
- fixed multiple Dropbox provider bugs
2017-01-16 21:55:58 +03:30
Amir Abbas d2cf657ab2 Added Thumbnail and Meta-information generator to Local and Dropbox 2017-01-16 15:20:44 +03:30
Alek Slater f2d54c7f92 incorrect usage of string.remove in correctPath method
- string remove needs to be given the index before the endIndex or it will crash (#22) needs to be given the index before the endIndex or it will crash
2016-12-19 18:10:14 +03:30
Amir Abbas 0e9a82a288 Resolves #20, Dropbox/WebDav file size 2016-12-15 14:42:00 +03:30
Amir Abbas ea7e089718 Updated for Xcode 8.2 2016-12-13 19:12:08 +03:30
Amir Abbas 19ea14ebc1 Added FileObjectSorting to sort FileObjects conveniently 2016-12-12 01:57:34 +03:30
Amir Abbas 47885c0a4e Deprecated FileObject.fileType with .type 2016-12-09 21:06:11 +03:30
Alek Slater ac44aa190f Overwrite param added to copyItem + minor bugfix and improvements (#18)
- Added overwrite param to copyItem from local to remote
- fixed an issue with file upload on dropbox that wouldnt work for people in non UTC timezones.
- made filebased upload for dropbox not read file data into memory let URLSession stream from the disk instead
2016-12-09 20:58:50 +03:30
Amir Abbas 50f6a393a0 Compiler Optimizations, Dropbox readme 2016-12-09 19:07:46 +03:30
Amir Abbas 779b38f381 Default values implemented in protocol level 2016-12-09 10:04:41 +03:30
Amir Abbas a374841883 Refined SPM/manual installation guide in Readme 2016-12-08 20:41:52 +03:30
Amir Abbas 0123d8b117 Overwrite option to writeContents 2016-12-07 21:24:19 +03:30
Amir Abbas Mousavian fdc4bb818d Merge pull request #17 from skela/master
Added overwrite option to DropboxProvider.writeContents()
2016-12-07 20:13:17 +03:30
Amir Abbas Mousavian a08a9fe7a0 Updated semantics
and changed default overwrite value to false
2016-12-07 20:12:15 +03:30
Alek Slater 792ac6b015 Update DropboxFileProvider.swift
Exposes the overwrite parameter for writeContents in DropboxFileProvider
2016-12-07 11:27:51 +08:00
22 changed files with 3624 additions and 661 deletions
+15 -10
View File
@@ -1,9 +1,16 @@
language: objective-c
osx_image: xcode8
osx_image: xcode8.2
xcode_project: FileProvider.xcodeproj
env:
global:
- FRAMEWORK_NAME=FileProvider.framework
- PROJECTNAME="FileProvider"
- FRAMEWORK_NAME="FileProvider.framework"
matrix:
- SHCEME="FileProvider OSX" SDK="macosx" ACTION="build"
- SHCEME="FileProvider iOS" SDK="iphonesimulator" ACTION="build"
- SHCEME="FileProvider tvOS" SDK="appletvsimulator" ACTION="build"
before_install:
- gem install xcpretty --no-rdoc --no-ri --no-document --quiet
- brew update
@@ -11,17 +18,15 @@ before_install:
script:
- set 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
after_success:
- bash <(curl -s https://codecov.io/bash)
- xcodebuild -project $PROJECT.xcodeproj -scheme "$SCHEME" -sdk $SDK $ACTION ONLY_ACTIVE_ARCH=NO | xcpretty
# - pod lib lint --quick
# after_success:
# - bash <(curl -s https://codecov.io/bash)
before_deploy:
- carthage build --no-skip-current
- carthage archive FileProvider
- carthage archive $PROJECTNAME
deploy:
file: FileProvider.framework.zip
file: $PROJECTNAME.framework.zip
skip_cleanup: true
on:
tags: true
+2 -2
View File
@@ -16,8 +16,8 @@ Pod::Spec.new do |s|
#
s.name = "FileProvider"
s.version = "0.8.1"
s.summary = "FileManager replacement for Local and Remote (WebDAV/Dropbox/SMB2) files on iOS and macOS."
s.version = "0.12.0"
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?
+108 -8
View File
@@ -35,6 +35,9 @@
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 */; };
@@ -92,6 +95,29 @@
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 */
@@ -104,6 +130,7 @@
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>"; };
@@ -131,6 +158,24 @@
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; };
79BD63AD1E2CC2EB0035128C /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.2.sdk/System/Library/Frameworks/MediaPlayer.framework; sourceTree = DEVELOPER_DIR; };
79BD63AF1E2CC3300035128C /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.tbd; path = usr/lib/libxml2.tbd; sourceTree = SDKROOT; };
79BD63B11E2CC3350035128C /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = System/Library/Frameworks/ImageIO.framework; sourceTree = SDKROOT; };
79BD63B31E2CC33D0035128C /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = System/Library/Frameworks/MediaPlayer.framework; sourceTree = SDKROOT; };
79BD63B51E2CC3860035128C /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; };
79BD63B71E2CC38D0035128C /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; };
79BD63B91E2CC39B0035128C /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.tbd; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/usr/lib/libxml2.tbd; sourceTree = DEVELOPER_DIR; };
79BD63BB1E2CC3B90035128C /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/System/Library/Frameworks/MediaPlayer.framework; sourceTree = DEVELOPER_DIR; };
79BD63BD1E2CC3C20035128C /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/System/Library/Frameworks/ImageIO.framework; sourceTree = DEVELOPER_DIR; };
79BD63BF1E2CC3CD0035128C /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/System/Library/Frameworks/CoreGraphics.framework; sourceTree = DEVELOPER_DIR; };
79BD63C11E2CC3D30035128C /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS10.1.sdk/System/Library/Frameworks/AVFoundation.framework; sourceTree = DEVELOPER_DIR; };
79BD63C31E2D17880035128C /* OneDriveFileProvide.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OneDriveFileProvide.swift; sourceTree = "<group>"; };
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 */
@@ -138,6 +183,9 @@
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;
@@ -146,6 +194,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;
};
@@ -153,6 +205,10 @@
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;
};
@@ -162,6 +218,20 @@
791950F31DE58A5300B4426E /* Frameworks */ = {
isa = PBXGroup;
children = (
79BD63C11E2CC3D30035128C /* AVFoundation.framework */,
79BD63BF1E2CC3CD0035128C /* CoreGraphics.framework */,
79BD63BD1E2CC3C20035128C /* ImageIO.framework */,
79BD63BB1E2CC3B90035128C /* MediaPlayer.framework */,
79BD63B91E2CC39B0035128C /* libxml2.tbd */,
79BD63B71E2CC38D0035128C /* AVFoundation.framework */,
79BD63B51E2CC3860035128C /* CoreFoundation.framework */,
79BD63B31E2CC33D0035128C /* MediaPlayer.framework */,
79BD63B11E2CC3350035128C /* ImageIO.framework */,
79BD63AF1E2CC3300035128C /* libxml2.tbd */,
79BD63AD1E2CC2EB0035128C /* MediaPlayer.framework */,
79BD63AB1E2CC2C20035128C /* ImageIO.framework */,
79BD63A91E2CC2BB0035128C /* AVFoundation.framework */,
79BD63A71E2CC2940035128C /* CoreGraphics.framework */,
791950F41DE58A5400B4426E /* libxml2.tbd */,
);
name = Frameworks;
@@ -182,6 +252,7 @@
7993965B1D48B7BF00086753 = {
isa = PBXGroup;
children = (
79E34A101E2AC6C600E1293B /* Extra */,
799396911D48C02300086753 /* Sources */,
7993968A1D48B8C700086753 /* Pod */,
799396681D48B7F600086753 /* Products */,
@@ -214,13 +285,18 @@
children = (
7924B18B1D89DAE000589DB7 /* AEXML */,
799396991D48C02300086753 /* SMBTypes */,
799396931D48C02300086753 /* DropboxFileProvider.swift */,
794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */,
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 */,
@@ -249,6 +325,13 @@
path = SMBTypes;
sourceTree = "<group>";
};
79E34A101E2AC6C600E1293B /* Extra */ = {
isa = PBXGroup;
children = (
);
name = Extra;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
@@ -402,11 +485,15 @@
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 */,
@@ -422,6 +509,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 */,
@@ -437,11 +525,15 @@
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 */,
@@ -457,6 +549,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 */,
@@ -472,11 +565,15 @@
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 */,
@@ -492,6 +589,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 */,
@@ -505,7 +603,7 @@
799396601D48B7BF00086753 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_VERSION_STRING = 0.7.0;
BUNDLE_VERSION_STRING = 0.12.0;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_EMPTY_BODY = YES;
@@ -528,14 +626,14 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
ONLY_ACTIVE_ARCH = YES;
SWIFT_VERSION = 3.0.1;
SWIFT_VERSION = 3.0;
};
name = Debug;
};
799396611D48B7BF00086753 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_VERSION_STRING = 0.7.0;
BUNDLE_VERSION_STRING = 0.12.0;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_EMPTY_BODY = YES;
@@ -556,7 +654,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_VERSION = 3.0.1;
SWIFT_VERSION = 3.0;
};
name = Release;
};
@@ -565,7 +663,7 @@
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
APPLICATION_EXTENSION_API_ONLY = YES;
BUNDLE_VERSION_STRING = 0.8.0;
BUNDLE_VERSION_STRING = 0.8.2;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
@@ -618,7 +716,7 @@
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
APPLICATION_EXTENSION_API_ONLY = YES;
BUNDLE_VERSION_STRING = 0.8.0;
BUNDLE_VERSION_STRING = 0.8.2;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
@@ -686,6 +784,7 @@
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;
@@ -741,6 +840,7 @@
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;
+1 -1
View File
@@ -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
+100 -25
View File
@@ -3,20 +3,24 @@
>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]
[![Platform](https://img.shields.io/badge/Platform-iOS%2C%20macOS%2C%20tvOS-lightgray.svg)]()
[![Release versin][release-image]][release-url]
[![CocoaPods version](https://img.shields.io/cocoapods/v/FileProvider.svg)][cocoapods]
[![Carthage compatible][carthage-image]](https://github.com/Carthage/Carthage)
[![Build Status][travis-image]][travis-url]
[![CocoaPods Compatible](https://img.shields.io/cocoapods/v/FileProvider.svg)](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]
<!---
[![codecov](https://codecov.io/gh/amosavian/FileProvider/branch/master/graph/badge.svg)](https://codecov.io/gh/amosavian/FileProvider)
[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
[![codecov](https://codecov.io/gh/amosavian/FileProvider/branch/master/graph/badge.svg)](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.
@@ -26,10 +30,17 @@ Local and WebDAV providers are fully tested and can be used in production enviro
- [x] **LocalFileProvider** a wrapper around `FileManager` with some additions like searching and reading a portion of file.
- [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 Web API, works with `onedrive.com` and compatible (business) servers.
* For now it has limitation in uploading files up to 100MB.
- [x] **CloudFileProvider** A wrapper around app's ubiquitous container to iCloud Drive in iOS 8+ API.
- [ ] **SMBFileProvider** SMB2/3 introduced in 2006, which is a file and printer sharing protocol originated from Microsoft Windows and now is replacing AFP protocol on MacOS.
* Data types and some basic functions are implemented but *main interface is not implemented yet!*
* SMB1/CIFS is depericated and very tricky to be implemented
- [ ] **FTPFileProvider** while deprecated in 1990s, it's still in use on some Web hosts.
- [ ] **AmazonS3FileProvider**
- [ ] **GoogleDriveFileProvider**
## Requirements
@@ -43,21 +54,26 @@ Legacy version is available in swift-2 branch
### Cocoapods / Carthage / Swift Package Manager
FileProvider supports both CocoaPods.
Add this line to your pods file:
```ruby
pod "FileProvider"
```
Or add this to cartfile:
Or add this to Cartfile:
```
github "amosavian/FileProvider"
```
### Git
Or to use in Swift Package Manager add this line in `Dependencies`:
```swift
.Package(url: "https://github.com/amosavian/FileProvider.git", majorVersion: 0, minorVersion: 8)
```
### Manually
To have latest updates with ease, use this command on terminal to get a clone:
```bash
@@ -75,11 +91,11 @@ if you have a git based project, use this command in your projects directory to
```bash
git submodule add https://github.com/amosavian/FileProvider
```
Then you can do either:
### Manually
**First way:** Copy Source folder to your project and Voila!
* Copy Source folder to your project and Voila!
**Second way:** Drop FileProvider.xcodeproj to you Xcode workspace and add the framework to your Embeded Binaries in target.
* Drop FileProvider.xcodeproj to you Xcode workspace and add the framework to your Embeded Binaries in target.
## Usage
@@ -91,18 +107,31 @@ For LocalFileProvider if you want to deal with `Documents` folder
``` swift
let documentsProvider = LocalFileProvider()
```
is equal to:
// Equals with:
let documentsProvider = LocalFileProvider(directory: .documentDirectory, domainMask: = .userDomainMask)
``` swift
let documentPath = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true);
let documentsURL = URL(fileURLWithPath: documentPath);
// 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 with string above
```
You can't change the base url later. and all paths are related to this base url by default.
To initialize an iCloud Container provider use below code, This will automatically manager creating Documents folder in container:
```swift
let documentsProvider = CloudFileProvider(containerId: nil)
```
For remote file providers authentication may be necessary:
``` swift
@@ -112,7 +141,7 @@ let webdavProvider = WebDAVFileProvider(baseURL: URL(string: "http://www.example
* 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
@@ -323,6 +352,43 @@ 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, 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.
@@ -342,11 +408,20 @@ 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/
+483
View File
@@ -0,0 +1,483 @@
//
// CloudFileProvider.swift
// FileProvider
//
// Created by Amir Abbas Mousavian.
// Copyright © 2017 Mousavian. Distributed under MIT license.
//
import Foundation
open class CloudFileProvider: LocalFileProvider {
public var type: String {
return "iCloudDrive"
}
/// Actually is readonly, value is true
override open var isCoorinating: Bool {
get {
return true
}
set {
return
}
}
open var containerId: String?
public init? (containerId: String?) {
assert(!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
let baseURL = ubiquityURL.standardized.appendingPathComponent("Documents/")
super.init(baseURL: baseURL)
self.isCoorinating = true
dispatch_queue = DispatchQueue(label: "FileProvider.\(self.type)", attributes: DispatchQueue.Attributes.concurrent)
operation_queue = OperationQueue()
operation_queue.name = "FileProvider.\(self.type).Operation"
fileManager.url(forUbiquityContainerIdentifier: containerId)
opFileManager.url(forUbiquityContainerIdentifier: containerId)
try? fileManager.createDirectory(at: baseURL, withIntermediateDirectories: true)
}
open override func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
dispatch_queue.async {
let pathURL = self.url(of: path)
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: "%K BEGINSWITH %@", NSMetadataItemPathKey, pathURL.path)
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
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)
}
}
completionHandler(contents, nil)
})
query.start()
}
}
/// 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)
}
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.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
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)
completionHandler(nil, error)
return
}
if let file = self.mapFileObject(attributes: attribs) {
completionHandler(file, nil)
} else {
let noFileError = self.throwError(path, code: CocoaError.fileNoSuchFile)
completionHandler(nil, noFileError)
}
})
query.start()
}
}
@discardableResult
open override func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let r = super.create(folder: folderName, at: atPath, completionHandler: completionHandler)
return CloudOperationHandle(operationType: r!.operationType, baseURL: self.baseURL)
}
@discardableResult
open override func create(file fileName: String, at atPath: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let r = super.create(file: fileName, at: atPath, contents: data, completionHandler: completionHandler)
return CloudOperationHandle(operationType: r!.operationType, baseURL: self.baseURL)
}
@discardableResult
open override func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let r = super.moveItem(path: path, to: toPath, overwrite: overwrite, completionHandler: completionHandler)
return CloudOperationHandle(operationType: r!.operationType, baseURL: self.baseURL)
}
@discardableResult
open override func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let r = super.copyItem(path: path, to: toPath, overwrite: overwrite, completionHandler: completionHandler)
return CloudOperationHandle(operationType: r!.operationType, baseURL: self.baseURL)
}
@discardableResult
open override func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let r = super.removeItem(path: path, completionHandler: completionHandler)
return CloudOperationHandle(operationType: r!.operationType, baseURL: self.baseURL)
}
@discardableResult
open override func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
// TODO: Make use of overwrite parameter
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
operation_queue.addOperation {
let tempFolder: URL
if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) {
tempFolder = FileManager.default.temporaryDirectory
} 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)
}
@discardableResult
open override func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
do {
try self.opFileManager.startDownloadingUbiquitousItem(at: self.url(of: path))
} catch let e {
completionHandler?(e)
DispatchQueue.main.async(execute: {
self.delegate?.fileproviderFailed(self, operation: opType)
})
return nil
}
let r = super.copyItem(path: path, toLocalURL: toLocalURL, completionHandler: completionHandler)
return CloudOperationHandle(operationType: r!.operationType, baseURL: self.baseURL)
}
@discardableResult
open override func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
let r = super.contents(path: path, completionHandler: completionHandler)
return CloudOperationHandle(operationType: r!.operationType, baseURL: self.baseURL)
}
@discardableResult
open override func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
let r = super.contents(path: path, offset: offset, length: length, completionHandler: completionHandler)
return CloudOperationHandle(operationType: r!.operationType, baseURL: self.baseURL)
}
@discardableResult
open override func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let r = super.writeContents(path: path, contents: data, atomically: atomically, overwrite: overwrite, completionHandler: completionHandler)
return CloudOperationHandle(operationType: r!.operationType, baseURL: self.baseURL)
}
open override func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
dispatch_queue.async {
let pathURL = self.url(of: path)
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: "(%K BEGINSWITH %@) && (%K LIKE %@)", NSMetadataItemPathKey, pathURL.path, NSMetadataItemFSNameKey, query)
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
var lastReportedCount = 0
if let foundItemHandler = foundItemHandler {
var updateObserver: NSObjectProtocol?
updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryGatheringProgress, object: query, queue: nil, using: { (notification) in
query.disableUpdates()
guard query.resultCount > lastReportedCount else { return }
for index in lastReportedCount..<query.resultCount {
guard let attribs = (query.result(at: index) as? NSMetadataItem)?.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
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 = query.resultCount
query.enableUpdates()
})
}
var finishObserver: NSObjectProtocol?
finishObserver = NotificationCenter.default.addObserver(forName: .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, recursive || url.deletingLastPathComponent().path.trimmingCharacters(in: pathTrimSet) == pathURL.path.trimmingCharacters(in: pathTrimSet) else {
continue
}
if let file = self.mapFileObject(attributes: attribs) {
contents.append(file)
}
}
completionHandler(contents, nil)
})
query.start()
}
}
fileprivate var monitors = [URL: (NSMetadataQuery, NSObjectProtocol)]()
//
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.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
let updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidUpdate, object: query, queue: nil, using: { (notification) in
query.disableUpdates()
eventHandler()
query.enableUpdates()
})
query.start()
monitors[pathURL] = (query, updateObserver)
}
open override func unregisterNotifcation(path: String) {
let key = url(of: path)
guard let (query, observer) = monitors[key] else {
return
}
query.disableUpdates()
query.stop()
monitors.removeValue(forKey: key)
NotificationCenter.default.removeObserver(observer)
}
open override func isRegisteredForNotification(path: String) -> Bool {
return monitors[url(of: path)] != nil
}
/// may return nil
open override func copy(with zone: NSZone? = nil) -> Any {
let copy = CloudFileProvider(containerId: self.containerId)
copy?.currentPath = self.currentPath
copy?.delegate = self.delegate
copy?.fileOperationDelegate = self.fileOperationDelegate
copy?.isPathRelative = self.isPathRelative
return copy as Any
}
fileprivate func mapFileObject(attributes attribs: [String: Any]) -> FileObject? {
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardized, let name = attribs[NSMetadataItemFSNameKey] as? String else {
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)
}
}
}
/// Returns a pulic url with expiration date, can be shared with other people.
open func temporaryLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
operation_queue.addOperation {
do {
var expiration: NSDate?
let url = try self.opFileManager.url(forPublishingUbiquitousItemAt: self.url(of: path), expiration: &expiration)
completionHandler(url, nil, expiration as Date?, nil)
} catch let e {
completionHandler(nil, nil, nil, e)
}
}
}
}
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)
}
/// Caution: may put pressure on CPU, may have latency
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
}
/// Caution: may put pressure on CPU, may have latency
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]
var item: NSMetadataItem?
let group = DispatchGroup()
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()
})
group.enter()
query.start()
_ = group.wait(timeout: DispatchTime.now() + 30)
return item
}
}
+141 -65
View File
@@ -13,43 +13,57 @@ 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, FileProviderBasicRemote {
open static let type: String = "WebDAV"
open let isPathRelative: Bool = true
open class DropboxFileProvider: FileProviderBasicRemote {
open static let type: String = "DropBox"
open let isPathRelative: Bool
open let baseURL: URL?
open var currentPath: String = ""
open var dispatch_queue: DispatchQueue {
open var currentPath: String
open let apiURL: URL
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 = false
public var validatingCache: Bool = true
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)
_session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDelegate?, delegateQueue: self.operation_queue)
}
return _session!
}
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.credential = credential
self.isPathRelative = true
self.currentPath = ""
self.useCache = false
self.validatingCache = true
self.cache = cache
self.credential = credential
self.apiURL = URL(string: "https://api.dropboxapi.com/2/")!
self.contentURL = URL(string: "https://content.dropboxapi.com/2/")!
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: DispatchQueue.Attributes.concurrent)
operation_queue = OperationQueue()
operation_queue.name = "FileProvider.\(type(of: self).type).Operation"
}
deinit {
@@ -63,12 +77,12 @@ open class DropboxFileProvider: NSObject, FileProviderBasicRemote {
}
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 dbError: FileProviderDropboxError?
@@ -76,7 +90,7 @@ open class DropboxFileProvider: NSObject, FileProviderBasicRemote {
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
dbError = 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) {
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr), let file = DropboxFileObject(json: json) {
fileObject = file
}
}
@@ -86,7 +100,7 @@ open class DropboxFileProvider: NSObject, FileProviderBasicRemote {
}
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")
@@ -118,11 +132,11 @@ extension DropboxFileProvider: FileProviderOperations {
return self.writeContents(path: filePath, contents: data ?? Data(), completionHandler: completionHandler)
}
public func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
public func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler)
}
public func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
public func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
return doOperation(.copy(source: path, destination: toPath), completionHandler: completionHandler)
}
@@ -139,17 +153,17 @@ extension DropboxFileProvider: FileProviderOperations {
let destPath = operation.destination
switch operation {
case .create:
url = "https://api.dropboxapi.com/2/files/create_folder"
url = "files/create_folder"
case .copy:
url = "https://api.dropboxapi.com/2/files/copy"
url = "files/copy"
case .move:
url = "https://api.dropboxapi.com/2/files/move"
url = "files/move"
case .remove:
url = "https://api.dropboxapi.com/2/files/delete"
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")
@@ -174,17 +188,12 @@ extension DropboxFileProvider: FileProviderOperations {
return RemoteOperationHandle(operationType: operation, tasks: [task])
}
public func copyItem(localFile: URL, to toPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
public func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
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: opType, 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? {
@@ -192,12 +201,12 @@ extension DropboxFileProvider: FileProviderOperations {
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")
let requestDictionary = ["path": path]
let requestJson = dictionaryToJSON(requestDictionary as [String : AnyObject]) ?? ""
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 {
@@ -221,23 +230,23 @@ extension DropboxFileProvider: FileProviderOperations {
}
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? {
if length == 0 {
return nil
}
let opType = FileOperationType.fetch(path: path)
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")
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": correctPath(path)! as NSString]
request.setValue(dictionaryToJSON(requestDictionary as [String : AnyObject]), forHTTPHeaderField: "Dropbox-API-Arg")
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 dbError: FileProviderDropboxError?
if let httpResponse = response as? HTTPURLResponse , httpResponse.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: httpResponse.statusCode) {
@@ -251,13 +260,13 @@ extension DropboxFileProvider: FileProviderReadWrite {
return RemoteOperationHandle(operationType: opType, tasks: [task])
}
public func writeContents(path: String, contents data: Data, atomically: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
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: opType, completionHandler: completionHandler)
return upload_simple(path, data: data, overwrite: overwrite, operation: opType, completionHandler: completionHandler)
}
public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
@@ -287,13 +296,20 @@ extension DropboxFileProvider: FileProviderReadWrite {
}
extension DropboxFileProvider {
@available(*, deprecated, message: "Use DropboxFileProvider.temporaryLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?)) function instead.")
open func temporaryLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: DropboxFileObject?, _ error: Error?) -> Void)) {
let url = URL(string: "https://api.dropboxapi.com/2/files/get_temporary_link")!
self.temporaryLink(to: path) { (url, file, _, error) in
completionHandler(url, file, error)
}
}
open func temporaryLink(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 = ["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 dbError: FileProviderDropboxError?
@@ -307,22 +323,24 @@ extension DropboxFileProvider {
link = URL(string: linkStr)
}
if let attribDic = json["metadata"] as? [String: AnyObject] {
fileObject = self.mapToFileObject(attribDic)
fileObject = DropboxFileObject(json: attribDic)
}
}
}
completionHandler(link, fileObject, dbError ?? error)
let expiration: Date? = link != nil ? Date(timeIntervalSinceNow: 4 * 60 * 60) : nil
completionHandler(link, fileObject, expiration, dbError ?? error)
})
task.resume()
}
open func copyItem(path: String, toRemoteURL destURL: URL, completionHandler: @escaping ((_ jobId: String?, _ attribute: DropboxFileObject?, _ error: Error?) -> Void)) {
let url = URL(string: "https://api.dropboxapi.com/2/files/save_url")!
open func copyItem(remoteURL: URL, to toPath: String, completionHandler: @escaping ((_ jobId: String?, _ attribute: DropboxFileObject?, _ error: Error?) -> Void)) {
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 = ["path": correctPath(path)! as NSString, "url" : destURL.absoluteString as NSString]
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 dbError: FileProviderDropboxError?
@@ -330,11 +348,11 @@ extension DropboxFileProvider {
var fileObject: DropboxFileObject?
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
dbError = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
dbError = 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 = self.mapToFileObject(attribDic)
fileObject = DropboxFileObject(json: attribDic)
}
}
}
@@ -342,6 +360,25 @@ extension DropboxFileProvider {
})
task.resume()
}
open func copyItem(reference: String, to toPath: String, completionHandler:SimpleCompletionHandler) {
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, "copy_reference" : reference as NSString]
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var dbError: FileProviderDropboxError?
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
dbError = code != nil ? FileProviderDropboxError(code: code!, path: toPath, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
}
completionHandler?(dbError ?? error)
})
task.resume()
}
}
extension DropboxFileProvider: ExtendedFileProvider {
@@ -349,40 +386,53 @@ extension DropboxFileProvider: ExtendedFileProvider {
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
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
public 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")
let task = self.session.dataTask(with: request, completionHandler: { (data, response, error) in
var image: ImageClass? = nil
@@ -392,7 +442,13 @@ 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)
})
@@ -400,7 +456,27 @@ extension DropboxFileProvider: ExtendedFileProvider {
}
public func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) {
NotImplemented()
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 dbError: FileProviderDropboxError?
var dic = [String: Any]()
var keys = [String]()
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
dbError = 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, dbError ?? error)
})
task.resume()
}
}
+97 -32
View File
@@ -2,8 +2,8 @@
// 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
@@ -20,7 +20,25 @@ public struct FileProviderDropboxError: Error, CustomStringConvertible {
public final class DropboxFileObject: FileObject {
internal init(name: String, path: String) {
super.init(absoluteURL: URL(string: path), name: name, path: path)
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
}
open internal(set) var serverTime: Date? {
@@ -34,10 +52,10 @@ public final class DropboxFileObject: FileObject {
open internal(set) var id: String? {
get {
return allValues["NSURLDropboxDocumentIdentifyKey"] as? String
return allValues["NSURLDocumentIdentifyKey"] as? String
}
set {
allValues["NSURLDropboxDocumentIdentifyKey"] = newValue
allValues["NSURLDocumentIdentifyKey"] = newValue
}
}
@@ -57,10 +75,10 @@ internal extension DropboxFileProvider {
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?
}
@@ -79,7 +97,7 @@ internal extension DropboxFileProvider {
let json = jsonToDictionary(jsonStr)
if let entries = json?["entries"] as? [AnyObject] , entries.count > 0 {
for entry in entries {
if let entry = entry as? [String: AnyObject], let file = self.mapToFileObject(entry) {
if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry) {
files.append(file)
}
}
@@ -98,15 +116,18 @@ internal extension DropboxFileProvider {
}
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")
@@ -120,14 +141,46 @@ internal extension DropboxFileProvider {
}
completionHandler?(responseError ?? error)
self.delegateNotify(.create(path: targetPath), error: responseError ?? error)
})
})
task.taskDescription = operation.json
task.resume()
return RemoteOperationHandle(operationType: operation, tasks: [task])
}
func upload_simple(_ targetPath: String, localFile: URL, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let size = (try? localFile.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? -1
if size > 150 * 1024 * 1024 {
let error = FileProviderDropboxError(code: .payloadTooLarge, path: targetPath, errorDescription: nil)
completionHandler?(error)
self.delegateNotify(.create(path: targetPath), error: error)
return nil
}
var requestDictionary = [String: AnyObject]()
let url: URL
url = URL(string: "files/upload", relativeTo: contentURL)!
requestDictionary["path"] = correctPath(targetPath) as NSString?
requestDictionary["mode"] = (overwrite ? "overwrite" : "add") as NSString
requestDictionary["client_modified"] = rfc3339utc(of: modifiedDate) as NSString
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
request.setValue(dictionaryToJSON(requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
let task = session.uploadTask(with: request, fromFile: localFile, completionHandler: { (data, response, error) in
var responseError: FileProviderDropboxError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderDropboxError(code: rCode, path: targetPath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
}
completionHandler?(responseError ?? error)
self.delegateNotify(.create(path: targetPath), error: responseError ?? error)
})
task.taskDescription = operation.json
task.resume()
return RemoteOperationHandle(operationType: operation, tasks: [task])
}
func search(_ startPath: String = "", query: String, start: Int = 0, maxResultPerPage: Int = 25, maxResults: Int = -1, foundItem:@escaping ((_ file: DropboxFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) {
let url = URL(string: "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 +199,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,23 +221,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 fileObject = DropboxFileObject(name: name, path: path)
fileObject.size = (json["size"] as? NSNumber)?.int64Value ?? -1
fileObject.serverTime = resolve(dateString: json["server_modified"] as? String ?? "")
fileObject.modifiedDate = resolve(dateString: json["client_modified"] as? String ?? "")
fileObject.fileType = (json[".tag"] as? String) == "folder" ? .directory : .regular
fileObject.isReadOnly = (json["sharing_info"]?["read_only"] as? NSNumber)?.boolValue ?? false
fileObject.id = json["id"] as? String
fileObject.rev = json["id"] as? String
return fileObject
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?) {
+444
View File
@@ -0,0 +1,444 @@
//
// ExtendedLocalFileProvider.swift
// FileProvider
//
// Created by Amir Abbas Mousavian.
// Copyright © 2017 Mousavian. Distributed under MIT license.
//
import Foundation
import ImageIO
import CoreGraphics
import AVFoundation
#if os(iOS) || os(tvOS)
import UIKit
#elseif os(macOS)
import Cocoa
#endif
extension LocalFileProvider: ExtendedFileProvider {
public func thumbnailOfFileSupported(path: String) -> Bool {
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)
}
}
}
public struct LocalFileInformationGenerator {
static public var imageThumbnailExtensions: [String] = ["jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff", "ico"]
static public var audioThumbnailExtensions: [String] = ["mp3", "aac", "m4a"]
static public var videoThumbnailExtensions: [String] = ["mov", "mp4", "m4v", "mpg", "mpeg"]
static public var pdfThumbnailExtensions: [String] = ["pdf"]
static public var officeThumbnailExtensions: [String] = []
static public var customThumbnailExtensions: [String] = []
static public var imagePropertiesExtensions: [String] = ["jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff"]
static public var audioPropertiesExtensions: [String] = ["mp3", "aac", "m4a", "caf"]
static public var videoPropertiesExtensions: [String] = ["mp4", "mpg", "3gp", "mov", "avi"]
static public var pdfPropertiesExtensions: [String] = ["pdf"]
static public var archivePropertiesExtensions: [String] = []
static public var officePropertiesExtensions: [String] = []
static public var customPropertiesExtensions: [String] = []
static public var imageThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in
return ImageClass(contentsOfFile: fileURL.path)
}
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
}
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: NSSize.zero)
#else
return ImageClass(cgImage: cgImage)
#endif
}
return nil
}
static public var pdfThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in
guard let data = try? Data(contentsOf: fileURL) else { return nil }
return LocalFileProvider.convertToImage(pdfData: data)
}
static public var officeThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in
return nil
}
static public var customThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in
return nil
}
static public var imageProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in
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))
}
var dic = [String: Any]()
var keys = [String]()
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? [String : AnyObject] ?? [:]
let exifDict = imageDict[kCGImagePropertyExifDictionary as String] as? [String : AnyObject] ?? [:]
if let pixelWidth: AnyObject = imageDict.object(forKey: kCGImagePropertyPixelWidth) as? NSNumber, let pixelHeight: AnyObject = imageDict.object(forKey: kCGImagePropertyPixelHeight) as? NSNumber {
keys.append("Dimensions")
dic["Dimensions"] = "\(pixelWidth)x\(pixelHeight)"
}
if let dpi = imageDict[kCGImagePropertyDPIWidth as String] {
keys.append("DPI")
dic["DPI"] = dpi
}
if let devicemake = tiffDict[kCGImagePropertyTIFFMake as String] {
keys.append("Device make")
dic["Device make"] = devicemake
}
if let devicemodel = tiffDict[kCGImagePropertyTIFFModel as String] {
keys.append("Device model")
dic["Device model"] = devicemodel
}
if let lensmodel = exifDict[kCGImagePropertyExifLensModel as String] {
keys.append("Lens model")
dic["Lens model"] = lensmodel
}
if let artist = tiffDict[kCGImagePropertyTIFFArtist as String] as? String , !artist.isEmpty {
keys.append("Artist")
dic["Artist"] = artist
}
if let cr = tiffDict[kCGImagePropertyTIFFCopyright as String] as? String , !cr.isEmpty {
keys.append("Copyright")
dic["Copyright"] = cr
}
if let date = tiffDict[kCGImagePropertyTIFFDateTime as String] as? String , !date.isEmpty {
keys.append("Date taken")
dic["Date taken"] = date
}
if let latitude = tiffDict[kCGImagePropertyGPSLatitude as String]?.doubleValue, let longitude = tiffDict[kCGImagePropertyGPSLongitude as String]?.doubleValue {
keys.append("Location")
dic["Location"] = "\(latitude), \(longitude)"
}
if let colorspace = imageDict[kCGImagePropertyColorModel as String] {
keys.append("Color space")
dic["Color space"] = colorspace
}
if let focallen = exifDict[kCGImagePropertyExifFocalLength as String] {
keys.append("Focal length")
dic["Focal length"] = focallen
}
if let fnum = exifDict[kCGImagePropertyExifFNumber as String] {
keys.append("F number")
dic["F number"] = fnum
}
if let expprog = exifDict[kCGImagePropertyExifExposureProgram as String] {
keys.append("Exposure program")
dic["Exposure program"] = expprog
}
if let exp = exifDict[kCGImagePropertyExifExposureTime as String]?.doubleValue {
let expfrac = simplify(Int64(exp * 10_000_000_000_000), 10_000_000_000_000)
keys.append("Exposure time")
dic["Exposure time"] = "\(expfrac.newTop)/\(expfrac.newBottom)"
}
if let iso = exifDict[kCGImagePropertyExifISOSpeedRatings as String] as? NSArray , iso.count > 0 {
keys.append("ISO speed")
dic["ISO speed"] = iso[0]
}
return (dic, keys)
}
static var audioProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in
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
}
var dic = [String: Any]()
var keys = [String]()
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) {
keys.append("Duration")
dic["Duration"] = LocalFileProvider.formatshort(interval: ap.duration)
if let bitRate = ap.settings[AVSampleRateKey] as? Int {
keys.append("Bitrate")
dic["Bitrate"] = bitRate
}
}
}
return (dic, keys)
}
static public var videoProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in
var dic = [String: Any]()
var keys = [String]()
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 videoTracks.count > 0 {
var bitrate: Float = 0
let width = Int(videoTracks[0].naturalSize.width)
let height = Int(videoTracks[0].naturalSize.height)
keys.append("Dimensions")
dic["Dimensions"] = "\(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
}
keys.append("Duration")
dic["Duration"] = LocalFileProvider.formatshort(interval: TimeInterval(duration))
keys.append("Video Bitrate")
dic["Video Bitrate"] = "\(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
}
keys.append("Audio Bitrate")
dic["Audio Bitrate"] = "\(Int(ceil(bitrate / 1000))) kbps"
return (dic, keys)
}
static public var pdfProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in
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
}
var dic = [String: Any]()
var keys = [String]()
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 {
keys.append("Title")
dic["Title"] = title
}
if let author = getKey("Author", from: dict), !author.isEmpty {
keys.append("Author")
dic["Author"] = author
}
if let subject = getKey("Subject", from: dict), !subject.isEmpty {
keys.append("Subject")
dic["Subject"] = subject
}
var majorVersion: Int32 = 0
var minorVersion: Int32 = 0
reference.getVersion(majorVersion: &majorVersion, minorVersion: &minorVersion)
if majorVersion > 0 {
keys.append("Version")
dic["Version"] = String(majorVersion) + "." + String(minorVersion)
}
if reference.numberOfPages > 0 {
keys.append("Pages")
dic["Pages"] = reference.numberOfPages
}
if reference.numberOfPages > 0, let pageRef = reference.page(at: 1) {
let size = pageRef.getBoxRect(CGPDFBox.mediaBox).size
keys.append("Resolution")
dic["Resolution"] = "\(Int(size.width))x\(Int(size.height))"
}
if let creator = getKey("Creator", from: dict), !creator.isEmpty {
keys.append("Content creator")
dic["Content creator"] = creator
}
if let creationDateString = getKey("CreationDate", from: dict), let creationDate = convertDate(creationDateString) {
keys.append("Creation date")
dic["Creation date"] = creationDate
}
if let modifiedDateString = getKey("ModDate", from: dict), let modDate = convertDate(modifiedDateString) {
keys.append("Modified date")
dic["Modified date"] = modDate
}
keys.append("Security")
dic["Security"] = reference.isEncrypted ? "Present" : "None"
keys.append("Allows printing")
dic["Allows printing"] = reference.allowsPrinting ? "Yes" : "No"
keys.append("Allows copying")
dic["Allows copying"] = reference.allowsCopying ? "Yes" : "No"
}
return (dic, keys)
}
static public var archiveProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = nil
static public var officeProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = nil
static public var customProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = nil
}
func ~=<T : Equatable>(array: [T], value: T) -> Bool {
return array.contains(value)
}
+8 -22
View File
@@ -401,8 +401,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 +414,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)
}
}
@@ -503,21 +503,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]
+317
View File
@@ -0,0 +1,317 @@
//
// FileProvider.swift
// FileProvider
//
// Created by Amir Abbas Mousavian.
// Copyright © 2016 Mousavian. Distributed under MIT license.
//
import Foundation
/// Containts path and attributes of a file or resource.
open class FileObject {
/// A `Dictionary` contains file information, using `URLResourceKey` keys.
open internal(set) var allValues: [String: Any]
internal init(allValues: [String: Any]) {
self.allValues = allValues
}
internal init(url: URL, name: String, path: String) {
self.allValues = [String: Any]()
self.url = url
self.name = name
self.path = path
}
/// url to access the resource, not supported by Dropbox provider
@available(*, deprecated, message: "Use FileObject.url.absoluteURL instead.")
open var absoluteURL: URL? {
return url?.absoluteURL
}
open internal(set) var url: URL? {
get {
return allValues["NSURLFileURLKey"] as? URL
}
set {
allValues["NSURLFileURLKey"] = newValue
}
}
/// Name of the file, usually equals with the last path component
open internal(set) var name: String {
get {
return allValues[URLResourceKey.nameKey.rawValue] as! String
}
set {
allValues[URLResourceKey.nameKey.rawValue] = newValue
}
}
/// Relative path of file object
open internal(set) var path: String {
get {
return allValues[URLResourceKey.pathKey.rawValue] as! String
}
set {
allValues[URLResourceKey.pathKey.rawValue] = newValue
}
}
/// Size of file on disk, return -1 for directories.
open internal(set) var size: Int64 {
get {
return allValues[URLResourceKey.fileSizeKey.rawValue] as? Int64 ?? -1
}
set {
allValues[URLResourceKey.fileSizeKey.rawValue] = newValue
}
}
/// The time contents of file has been created, returns nil if not set
open internal(set) var creationDate: Date? {
get {
return allValues[URLResourceKey.creationDateKey.rawValue] as? Date
}
set {
allValues[URLResourceKey.creationDateKey.rawValue] = newValue
}
}
/// The time contents of file has been modified, returns nil if not set
open internal(set) var modifiedDate: Date? {
get {
return allValues[URLResourceKey.contentModificationDateKey.rawValue] as? Date
}
set {
allValues[URLResourceKey.contentModificationDateKey.rawValue] = newValue
}
}
/// return resource type of file, usually directory, regular or symLink
open internal(set) var type: URLFileResourceType? {
get {
return allValues[URLResourceKey.fileResourceTypeKey.rawValue] as? URLFileResourceType
}
set {
allValues[URLResourceKey.fileResourceTypeKey.rawValue] = newValue
}
}
@available(*, deprecated, message: "Use FileObject.type property instead.")
open var fileType: URLFileResourceType? {
return self.type
}
/// File is hidden either because begining with dot or filesystem flags
/// Setting this value on a file begining with dot has no effect
open internal(set) var isHidden: Bool {
get {
return allValues[URLResourceKey.isHiddenKey.rawValue] as? Bool ?? false
}
set {
allValues[URLResourceKey.isHiddenKey.rawValue] = newValue
}
}
/// File can not be written
open internal(set) var isReadOnly: Bool {
get {
return !(allValues[URLResourceKey.isWritableKey.rawValue] as? Bool ?? true)
}
set {
allValues[URLResourceKey.isWritableKey.rawValue] = !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
}
}
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 properties
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 properties.
public func sorted(by type: FileObjectSorting.SortType, ascending: Bool = true, isDirectoriesFirst: Bool = false) -> [Element] {
let sorting = FileObjectSorting(type: type, ascending: ascending, isDirectoriesFirst: isDirectoriesFirst)
return sorting.sort(self) as! [Element]
}
/// Sorts array of `FileObject`s by criterias set in properties
public mutating func sorted(by type: FileObjectSorting.SortType, ascending: Bool = true, isDirectoriesFirst: Bool = false) {
self = self.sorted(by: type, ascending: ascending, isDirectoriesFirst: isDirectoriesFirst)
}
}
extension URLFileResourceType {
public init(fileTypeValue: FileAttributeType) {
switch fileTypeValue {
case FileAttributeType.typeCharacterSpecial: self = .characterSpecial
case FileAttributeType.typeDirectory: self = .directory
case FileAttributeType.typeBlockSpecial: self = .blockSpecial
case FileAttributeType.typeRegular: self = .regular
case FileAttributeType.typeSymbolicLink: self = .symbolicLink
case FileAttributeType.typeSocket: self = .socket
case FileAttributeType.typeUnknown: self = .unknown
default: self = .unknown
}
}
}
internal extension URL {
var uw_scheme: String {
return self.scheme ?? ""
}
}
internal func jsonToDictionary(_ jsonString: String) -> [String: AnyObject]? {
guard let data = jsonString.data(using: .utf8) else {
return nil
}
if let dic = try? JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions()) as? [String: AnyObject] {
return dic
}
return nil
}
internal func dictionaryToJSON(_ dictionary: [String: AnyObject]) -> String? {
if let data = try? JSONSerialization.data(withJSONObject: dictionary, options: JSONSerialization.WritingOptions()) {
return String(data: data, encoding: .utf8)
}
return nil
}
+3
View File
@@ -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.
+606 -188
View File
@@ -10,7 +10,7 @@ import Foundation
#if os(iOS) || os(tvOS)
import UIKit
public typealias ImageClass = UIImage
#elseif os(OSX)
#elseif os(macOS)
import Cocoa
public typealias ImageClass = NSImage
#endif
@@ -18,27 +18,113 @@ public typealias ImageClass = NSImage
public typealias SimpleCompletionHandler = ((_ error: Error?) -> Void)?
public protocol FileProviderBasic: class {
/// An string to identify type of provider.
static var type: String { get }
/// An string to identify type of provider.
var type: String { get }
/// The paths in arguments should resolved against base url.
var isPathRelative: Bool { get }
/// The url of which paths should resolve against.
var baseURL: URL? { get }
/// Current active path used in `contentsOfDirectory(path:completionHandler:)` method.
var currentPath: String { get set }
/**
Dispatch queue usually used in query methods.
Set it to a new object to switch between cuncurrent and serial queues.
- **Default:** Cuncurrent `DispatchQueue` object.
*/
var dispatch_queue: DispatchQueue { get set }
/// Operation queue ususlly used in file operation methods.
/// use `maximumOperationTasks` property of provider to manage operation queue.
var operation_queue: OperationQueue { get set }
/// Delegate to update UI after finishing file operations.
var delegate: FileProviderDelegate? { get set }
/**
login credential for provider. Should be set in `init` method.
**Example initialization:**
````
let credential = URLCredential(user: "user", password: "password", persistence: .forSeession)
````
- Note: In OAuth based providers like `DropboxFileProvider` and `OneDriveFileProvider`, password is Token.
use [OAuthSwift](https://github.com/OAuthSwift/OAuthSwift) library to fetch clientId and Token of user.
*/
var credential: URLCredential? { get }
/**
*
*/
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 block with result of directory entries or error.
`contents`: An array of `FileObject` identifying the the directory entries.
`error`: Error returned by system.
*/
func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void))
/**
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 block with result of directory entries or error.
`attributes`: A `FileObject` containing the attributes of the item.
`error`: Error returned by system.
*/
func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void))
/// Returns total and used space in provider container asynchronously.
func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void))
/**
Returns an independent url to access the file. Some providers like `Dropbox` due to their nature.
don't return an absolute url to be used to access file directly.
- Parameter path: Relative path of file or directory.
- Returns: An url, can be used to access to file directly.
*/
func url(of path: String?) -> URL
}
public extension FileProviderBasic {
/// The maximum number of queued operations that can execute at the same time.
///
/// The default value of this property is `OperationQueue.defaultMaxConcurrentOperationCount`.
public var maximumOperationTasks: Int {
get {
return operation_queue.maxConcurrentOperationCount
}
set {
operation_queue.maxConcurrentOperationCount = newValue
}
}
}
public protocol FileProviderBasicRemote: FileProviderBasic {
///
var session: URLSession { get }
/// A `URLCache` to cache downloaded files and contents.
///
/// If set to nil, `URLCache.shared` object will be used.
/// - Note: It has no effect unless setting `useCache` property to `true`.
var cache: URLCache? { get }
/// Determine to use `cache` property to cache downloaded file objects. Doesn't have effect on query type methods.
var useCache: Bool { get set }
/// Validating cached data using E-Tag or Revision identifier if possible.
var validatingCache: Bool { get set }
}
@@ -93,73 +179,362 @@ internal extension FileProviderBasicRemote {
public protocol FileProviderOperations: FileProviderBasic {
var fileOperationDelegate : FileOperationDelegate? { get set }
/**
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.
*/
@discardableResult
func create(folder: String, at: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
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 `LocalFileProvider`.
*/
@discardableResult
func create(file: String, at: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
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. If file is already exist, an error will be returned via completionHandler.
- Parameters:
- path: original file or directory path.
- to: destination path of file or directory, including file/directory name.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func moveItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
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 `LocalFileProvider`.
*/
@discardableResult
func moveItem(path: String, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
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. If file is already exist, an error will be returned via completionHandler.
- Parameters:
- path: original file or directory path.
- to: destination path of file or directory, including file/directory name.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func copyItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
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 `LocalFileProvider`.
*/
@discardableResult
func copyItem(path: String, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
Removes the file or directory at the specified path.
- Parameters:
- path: file or directory path.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
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.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress.
*/
@discardableResult
func copyItem(localFile: URL, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
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
func copyItem(localFile: URL, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
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. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle?
}
extension FileProviderOperations {
@discardableResult
public func moveItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
return self.moveItem(path: path, to: to, overwrite: false, completionHandler: completionHandler)
}
@discardableResult
public func copyItem(localFile: URL, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
return self.copyItem(localFile: localFile, to: to, overwrite: false, completionHandler: completionHandler)
}
@discardableResult
public func copyItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
return self.copyItem(path: path, to: to, overwrite: false, completionHandler: completionHandler)
}
}
public protocol FileProviderReadWrite: FileProviderBasic {
/**
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 block with result of file contents or error.
`contents`: contents of file in a `Data` object.
`error`: Error returned by system.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle?
/**
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 block with result of file contents or error.
`contents`: contents of file in a `Data` object.
`error`: Error returned by system.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle?
/**
Write the contents of the `Data` to a location asynchronously.
It will return error if file is already exists.
Not attomically by default, unless the provider enforces it.
- Parameters:
- path: Path of target file.
- contents: Data to be written into file.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func writeContents(path: String, contents: Data, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
Write the contents of the `Data` to a location asynchronously.
It will return error if file is already exists.
- Parameters:
- path: Path of target file.
- contents: Data to be written into file.
- atomically: data will be written to a temporary file before writing to final location. Default is `false`.
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
*/
@discardableResult
func writeContents(path: String, contents: Data, atomically: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
Write the contents of the `Data` to a location asynchronously.
Not attomically by default, unless the provider enforces it.
- 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`.
- 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
func writeContents(path: String, contents: Data, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
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
func writeContents(path: String, contents: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
/**
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: Block which is called when a file is found
- completionHandler: Block which will be called after finishing search. Returns an arry of `FileObject` or error if occured.
*/
func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void))
}
extension 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 writeContents(path: String, contents: Data, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
return self.writeContents(path: path, contents: contents, atomically: false, overwrite: false, completionHandler: completionHandler)
}
@discardableResult
public func writeContents(path: String, contents: Data, atomically: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
return self.writeContents(path: path, contents: contents, atomically: atomically, overwrite: false, completionHandler: completionHandler)
}
@discardableResult
public func writeContents(path: String, contents: Data, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
return self.writeContents(path: path, contents: contents, atomically: false, overwrite: overwrite, completionHandler: completionHandler)
}
}
public protocol FileProviderMonitor: FileProviderBasic {
/**
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: Block executed after change, on a secondary thread.
*/
func registerNotifcation(path: String, eventHandler: @escaping (() -> Void))
/// Stops monitoring the path.
///
/// - Parameter path: path of directory.
func unregisterNotifcation(path: String)
/// Investigate either the path is registered for change notification or not.
///
/// - Parameter path: path of directory.
/// - Returns: Directory is being monitored or not.
func isRegisteredForNotification(path: String) -> Bool
}
public protocol FileProvider: FileProviderBasic, FileProviderOperations, FileProviderReadWrite, NSCopying {
}
internal let pathTrimSet = CharacterSet(charactersIn: " /")
extension FileProviderBasic {
public var type: String {
return Self.type
}
/// path without heading and trailing slash
public var bareCurrentPath: String {
return currentPath.trimmingCharacters(in: CharacterSet(charactersIn: ". /"))
return currentPath.trimmingCharacters(in: pathTrimSet)
}
func escaped(path: String) -> String {
return path.trimmingCharacters(in: pathTrimSet).addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)!
}
@available(*, deprecated, message: "Use FileProvider.url(of:).absoluteURL instead.")
public func absoluteURL(_ path: String? = nil) -> URL {
let rpath: String
return url(of: path).absoluteURL
}
public func url(of path: String? = nil) -> URL {
var rpath: String
if let path = path {
rpath = path
} else {
rpath = self.currentPath
}
if isPathRelative, let baseURL = baseURL {
if rpath.hasPrefix("/") && baseURL.absoluteString.hasSuffix("/") {
var npath = rpath
npath.remove(at: npath.startIndex)
return baseURL.appendingPathComponent(npath)
} else {
return baseURL.appendingPathComponent(rpath)
rpath = rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? rpath
if let baseURL = baseURL {
if isPathRelative && rpath.hasPrefix("/") {
rpath.remove(at: rpath.startIndex)
}
return URL(string: rpath, relativeTo: baseURL) ?? baseURL
} else {
return URL(fileURLWithPath: rpath).standardizedFileURL
return URL(string: rpath)!
}
}
public func relativePathOf(url: URL) -> String {
if url.baseURL == self.baseURL {
return url.relativePath.removingPercentEncoding!
}
guard let baseURL = self.baseURL else { return url.absoluteString }
return url.standardizedFileURL.absoluteString.replacingOccurrences(of: baseURL.absoluteString, with: "/").removingPercentEncoding!
}
@@ -168,11 +543,13 @@ extension FileProviderBasic {
guard let path = path else { return nil }
var p = path.hasPrefix("/") ? path : "/" + path
if p.hasSuffix("/") {
p.remove(at: p.endIndex)
p.remove(at: p.index(before:p.endIndex))
}
return p
}
/// Returns a file name supposed to be unique with adding numbers to end of file.
/// - Important: It's a synchronous method. Don't use it on matin thread.
public func fileByUniqueName(_ filePath: String) -> String {
let fileUrl = URL(fileURLWithPath: filePath)
let dirPath = fileUrl.deletingLastPathComponent().path
@@ -194,7 +571,7 @@ extension FileProviderBasic {
}
var i = number ?? 2
let similiar = contents.map {
$0.absoluteURL?.lastPathComponent ?? $0.name
$0.url?.lastPathComponent ?? $0.name
}.filter {
$0.hasPrefix(result)
}
@@ -210,7 +587,7 @@ extension FileProviderBasic {
}
internal func throwError(_ path: String, code: FoundationErrorEnum) -> NSError {
let fileURL = self.absoluteURL(path)
let fileURL = self.url(of: path)
let domain: String
switch code {
case is URLError:
@@ -221,47 +598,208 @@ extension FileProviderBasic {
return NSError(domain: domain, code: code.rawValue, userInfo: [NSURLErrorFailingURLErrorKey: fileURL, NSURLErrorFailingURLStringErrorKey: fileURL.absoluteString])
}
internal func NotImplemented() {
assert(false, "method not implemented")
}
internal func resolve(dateString: String) -> Date? {
let dateFor: DateFormatter = DateFormatter()
dateFor.locale = Locale(identifier: "en_US")
dateFor.dateFormat = "EEE',' dd' 'MMM' 'yyyy HH':'mm':'ss zzz"
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
}
dateFor.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ssz"
if let isotime = dateFor.date(from: dateString) {
return isotime
}
return nil
internal func NotImplemented(_ fn: String = #function, file: StaticString = #file) {
assert(false, "\(fn) method is not yet implemented. \(file)")
}
}
public protocol ExtendedFileProvider: FileProvider {
public protocol ExtendedFileProvider: FileProviderBasic {
/// Returuns true if thumbnail preview is supported by provider and file type accordingly.
///
/// - Parameter path: path of file.
/// - Returns: A `Bool` idicates path can have thumbnail.
func thumbnailOfFileSupported(path: String) -> Bool
/// Returns true if provider supports fetching properties of file like dimensions, duration, etc.
/// Usually media or document files support these meta-infotmations.
///
/// - Parameter path: path of file.
/// - Returns: A `Bool` idicates path can have properties.
func propertiesOfFileSupported(path: String) -> Bool
func thumbnailOfFile(path: String, dimension: CGSize, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void))
/**
Generates ans returns a thumbnail preview of document asynchronously. The defualt dimension of returned image is different
regarding provider type, usually 64x64 pixels.
- Parameters:
- path: path of file.
- completionHandler: a block with result of preview image or error.
`image`: `NSImage`/`UIImage` object contains preview.
`error`: Error returned by system.
*/
func thumbnailOfFile(path: String, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void))
/**
Generates ans returns a thumbnail preview of document asynchronously. The defualt dimension of returned image is different
regarding provider type, usually 64x64 pixels. Default value used when `dimenstion` is `nil`.
- Note: `LocalFileInformationGenerator` variables can be set to change default behavior of
thumbnail and properties generator of `LocalFileProvider`.
- Parameters:
- path: path of file.
- dimension: width and height of result preview image.
- completionHandler: a block with result of preview image or error.
`image`: `NSImage`/`UIImage` object contains preview.
`error`: Error returned by system.
*/
func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void))
/**
Fetching properties of file like dimensions, duration, etc. It's variant depending on file type.
Images, videos and audio files meta-information will be returned.
- Note: `LocalFileInformationGenerator` variables can be set to change default behavior of
thumbnail and properties generator of `LocalFileProvider`.
- Parameters:
- path: path of file.
- completionHandler: a block with result of preview image or error.
`propertiesDictionary`: A `Dictionary` of proprty keys and values.
`keys`: An `Array` contains ordering of keys.
`error`: Error returned by system.
*/
func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String: Any], _ keys: [String], _ error: Error?) -> Void))
}
extension ExtendedFileProvider {
public func thumbnailOfFile(path: String, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) {
self.thumbnailOfFile(path: path, dimension: nil, completionHandler: completionHandler)
}
internal static func formatshort(interval: TimeInterval) -> String {
var result = "0:00"
if interval < TimeInterval(Int32.max) {
result = ""
var time = DateComponents()
time.hour = Int(interval / 3600)
time.minute = Int((interval.truncatingRemainder(dividingBy: 3600)) / 60)
time.second = Int(interval.truncatingRemainder(dividingBy: 60))
let formatter = NumberFormatter()
formatter.paddingCharacter = "0"
formatter.minimumIntegerDigits = 2
formatter.maximumFractionDigits = 0
let formatterFirst = NumberFormatter()
formatterFirst.maximumFractionDigits = 0
if time.hour! > 0 {
result = "\(formatterFirst.string(from: NSNumber(value: time.hour!))!):\(formatter.string(from: NSNumber(value: time.minute!))!):\(formatter.string(from: NSNumber(value: time.second!))!)"
} else {
result = "\(formatterFirst.string(from: NSNumber(value: time.minute!))!):\(formatter.string(from: NSNumber(value: time.second!))!)"
}
}
result = result.trimmingCharacters(in: CharacterSet(charactersIn: ": "))
return result
}
internal static func dataIsPDF(_ data: Data) -> Bool {
return data.count > 4 && data.scanString(length: 4, encoding: .ascii) == "%PDF"
}
internal static func convertToImage(pdfData: Data?, page: Int = 1) -> ImageClass? {
guard let pdfData = pdfData else { return nil }
let cfPDFData: CFData = pdfData as CFData
if let provider = CGDataProvider(data: cfPDFData), let reference = CGPDFDocument(provider), let pageRef = reference.page(at: page) {
let frame = pageRef.getBoxRect(CGPDFBox.mediaBox)
var size = frame.size
let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
#if os(macOS)
let ppp = Int(NSScreen.main()?.backingScaleFactor ?? 1) // fetch device is retina or not
size.width *= CGFloat(ppp)
size.height *= CGFloat(ppp)
let rep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(size.width), pixelsHigh: Int(size.height),
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: NSCalibratedRGBColorSpace,
bytesPerRow: 0, bitsPerPixel: 0)
guard let context = NSGraphicsContext(bitmapImageRep: rep!) else {
return nil
}
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.setCurrent(context)
let transform = pageRef.getDrawingTransform(CGPDFBox.mediaBox, rect: rect, rotate: 0, preserveAspectRatio: true)
context.cgContext.concatenate(transform)
context.cgContext.translateBy(x: 0, y: size.height)
context.cgContext.scaleBy(x: CGFloat(ppp), y: CGFloat(-ppp))
context.cgContext.drawPDFPage(pageRef)
let resultingImage = NSImage(size: size)
resultingImage.addRepresentation(rep!)
return resultingImage
#else
let ppp = Int(UIScreen.main.scale) // fetch device is retina or not
guard let context = UIGraphicsGetCurrentContext() else {
return nil
}
size.width *= CGFloat(ppp)
size.height *= CGFloat(ppp)
UIGraphicsBeginImageContext(size)
context.saveGState()
let transform = pageRef.getDrawingTransform(CGPDFBox.mediaBox, rect: rect, rotate: 0, preserveAspectRatio: true)
context.concatenate(transform)
context.translateBy(x: 0, y: size.height)
context.scaleBy(x: CGFloat(ppp), y: CGFloat(-ppp))
context.drawPDFPage(pageRef)
context.restoreGState()
let resultingImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return resultingImage
#endif
}
return nil
}
internal static func scaleDown(image: ImageClass, toSize maxSize: CGSize) -> ImageClass {
let height, width: CGFloat
if image.size.width > image.size.height {
width = maxSize.width
height = (image.size.height / image.size.width) * width
} else {
height = maxSize.height
width = (image.size.width / image.size.height) * height
}
let newSize = CGSize(width: width, height: height)
#if os(macOS)
var imageRect = NSRect(origin: CGPoint.zero, size: image.size)
let imageRef = image.cgImage(forProposedRect: &imageRect, context: nil, hints: nil)
// Create NSImage from the CGImage using the new size
return NSImage(cgImage: imageRef!, size: newSize)
#else
UIGraphicsBeginImageContextWithOptions(newSize, false, 0.0)
image.draw(in: CGRect(origin: CGPoint.zero, size: newSize))
let newImage = UIGraphicsGetImageFromCurrentImageContext() ?? image
UIGraphicsEndImageContext()
return newImage
#endif
}
}
/// Operation type description of file operation, included files path in associated values.
public enum FileOperationType: CustomStringConvertible {
/// Creating a file or directory in path.
case create (path: String)
/// Copying a file or directory from source to destination.
case copy (source: String, destination: String)
/// Moving a file or directory from source to destination.
case move (source: String, destination: String)
/// Modifying data of a file o in path by writing new data.
case modify (path: String)
/// Deleting file or directory in path.
case remove (path: String)
/// Creating a symbolic link or alias to target.
case link (link: String, target: String)
/// Fetching data in file located in path.
case fetch (path: String)
public var description: String {
@@ -276,16 +814,24 @@ public enum FileOperationType: CustomStringConvertible {
}
}
/// present participle of action, like 'Copying`.
public var actionDescription: String {
return description.trimmingCharacters(in: CharacterSet(charactersIn: "e")) + "ing"
}
/// Path of subjecting file.
public var source: String? {
guard let reflect = Mirror(reflecting: self).children.first?.value else { return nil }
let mirror = Mirror(reflecting: reflect)
return reflect as? String ?? mirror.children.first?.value as? String
}
/// Path of subjecting file.
public var path: String? {
return source
}
/// Path of destination file.
public var destination: String? {
guard let reflect = Mirror(reflecting: self).children.first?.value else { return nil }
let mirror = Mirror(reflecting: reflect)
@@ -300,120 +846,24 @@ public enum FileOperationType: CustomStringConvertible {
}
}
open class FileObject {
open internal(set) var allValues: [String: Any]
internal init(allValues: [String: Any]) {
self.allValues = allValues
}
internal init(absoluteURL: URL? = nil, name: String, path: String) {
self.allValues = [String: Any]()
self.absoluteURL = absoluteURL
self.name = name
self.path = path
}
open internal(set) var absoluteURL: URL? {
get {
return allValues["NSURLAbsoluteURLKey"] as? URL
}
set {
allValues["NSURLAbsoluteURLKey"] = newValue
}
}
open internal(set) var name: String {
get {
return allValues[URLResourceKey.nameKey.rawValue] as! String
}
set {
allValues[URLResourceKey.nameKey.rawValue] = newValue
}
}
open internal(set) var path: String {
get {
return allValues[URLResourceKey.pathKey.rawValue] as! String
}
set {
allValues[URLResourceKey.pathKey.rawValue] = newValue
}
}
open internal(set) var size: Int64 {
get {
return allValues[URLResourceKey.fileSizeKey.rawValue] as? Int64 ?? -1
}
set {
allValues[URLResourceKey.fileSizeKey.rawValue] = Int(exactly: newValue) ?? Int.max
}
}
open internal(set) var creationDate: Date? {
get {
return allValues[URLResourceKey.creationDateKey.rawValue] as? Date
}
set {
allValues[URLResourceKey.creationDateKey.rawValue] = newValue
}
}
open internal(set) var modifiedDate: Date? {
get {
return allValues[URLResourceKey.contentModificationDateKey.rawValue] as? Date
}
set {
allValues[URLResourceKey.contentModificationDateKey.rawValue] = newValue
}
}
open internal(set) var fileType: URLFileResourceType? {
get {
return allValues[URLResourceKey.fileResourceTypeKey.rawValue] as? URLFileResourceType
}
set {
allValues[URLResourceKey.fileResourceTypeKey.rawValue] = newValue
}
}
open internal(set) var isHidden: Bool {
get {
return allValues[URLResourceKey.isHiddenKey.rawValue] as? Bool ?? false
}
set {
allValues[URLResourceKey.isHiddenKey.rawValue] = newValue
}
}
open internal(set) var isReadOnly: Bool {
get {
return !(allValues[URLResourceKey.isWritableKey.rawValue] as? Bool ?? true)
}
set {
allValues[URLResourceKey.isWritableKey.rawValue] = !newValue
}
}
open var isDirectory: Bool {
return self.fileType == .directory
}
open var isRegularFile: Bool {
return self.fileType == .regular
}
open var isSymLink: Bool {
return self.fileType == .symbolicLink
}
}
public protocol OperationHandle {
/// Operation supposed to be done on files. Contains file paths as associated value.
var operationType: FileOperationType { get }
/// Bytes written/read by operation so far.
var bytesSoFar: Int64 { get }
/// Total bytes of operation.
var totalBytes: Int64 { get }
/// Operation is progress or not, Returns false if operation is done or not initiated yet.
var inProgress: Bool { get }
/// Progress of operation, usually equals with `bytesSoFar/totalBytes`. or NaN if not available.
var progress: Float { get }
/// Cancels operation while in progress, or cancels data/download/upload url session task.
func cancel() -> Bool
}
@@ -426,8 +876,15 @@ public extension OperationHandle {
}
public protocol FileProviderDelegate: class {
/// fileproviderSucceed(_:operation:) gives delegate a notification when an operation finished with success.
/// This method is called in main thread to avoids UI bugs.
func fileproviderSucceed(_ fileProvider: FileProviderOperations, operation: FileOperationType)
/// fileproviderSucceed(_:operation:) gives delegate a notification when an operation finished with failure.
/// This method is called in main thread to avoids UI bugs.
func fileproviderFailed(_ fileProvider: FileProviderOperations, operation: FileOperationType)
/// fileproviderSucceed(_:operation:) gives delegate a notification when an operation progess.
/// Supported by some providers, especially remote ones.
/// This method is called in main thread to avoids UI bugs.
func fileproviderProgress(_ fileProvider: FileProviderOperations, operation: FileOperationType, progress: Float)
}
@@ -440,13 +897,6 @@ public protocol FileOperationDelegate: class {
func fileProvider(_ fileProvider: FileProviderOperations, shouldProceedAfterError error: Error, operation: FileOperationType) -> Bool
}
// THESE ARE METHODS TO PROVIDE COMPATIBILITY WITH SWIFT 2.3 SIMOULTANIOUSLY!
internal extension URL {
var uw_scheme: String {
return self.scheme ?? ""
}
}
internal class Weak<T: AnyObject> {
weak var value : T?
init (_ value: T) {
@@ -454,21 +904,6 @@ internal class Weak<T: AnyObject> {
}
}
extension URLFileResourceType {
public init(fileTypeValue: FileAttributeType) {
switch fileTypeValue {
case FileAttributeType.typeCharacterSpecial: self = .characterSpecial
case FileAttributeType.typeDirectory: self = .directory
case FileAttributeType.typeBlockSpecial: self = .blockSpecial
case FileAttributeType.typeRegular: self = .regular
case FileAttributeType.typeSymbolicLink: self = .symbolicLink
case FileAttributeType.typeSocket: self = .socket
case FileAttributeType.typeUnknown: self = .unknown
default: self = .unknown
}
}
}
public protocol FoundationErrorEnum {
init? (rawValue: Int)
var rawValue: Int { get }
@@ -476,20 +911,3 @@ public protocol FoundationErrorEnum {
extension URLError.Code: FoundationErrorEnum {}
extension CocoaError.Code: FoundationErrorEnum {}
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
}
+377 -114
View File
@@ -9,49 +9,85 @@
import Foundation
open class LocalFileProvider: FileProvider, FileProviderMonitor {
open static let type = "Local"
open var isPathRelative: Bool = true
open private(set) var baseURL: URL? = LocalFileProvider.defaultBaseURL()
open var currentPath: String = ""
open static let type: String = "Local"
open var isPathRelative: Bool
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 internal(set) var credential: URLCredential?
open private(set) var fileManager = FileManager()
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
/// Forces file operations to use NSFileCoordinating, should be set true if:
/// - Files are on ubiquity(iCloud) container
/// - Multiple processes are accessing same file, recommended always in macOS and when using app extensions in iOS/tvOS (shared container)
open var isCoorinating: Bool
/// default values are `directory: .documentDirectory, domainMask: .userDomainMask`
public convenience init (directory: FileManager.SearchPathDirectory = .documentDirectory, domainMask: FileManager.SearchPathDomainMask = .userDomainMask) {
self.init(baseURL: FileManager.default.urls(for: directory, in: domainMask).first!)
}
public convenience init ? (sharedContainerId: String, directory: FileManager.SearchPathDirectory = .userDirectory) {
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%20support")
default:
break
}
self.init(baseURL: finalBaseURL)
try? fileManager.createDirectory(at: finalBaseURL, withIntermediateDirectories: true)
}
public init (baseURL: 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.isPathRelative = true
self.currentPath = ""
self.credential = nil
self.isCoorinating = false
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: DispatchQueue.Attributes.concurrent)
operation_queue = OperationQueue()
operation_queue.name = "FileProvider.\(type(of: self).type).Operation"
fileProviderManagerDelegate = LocalFileProviderManagerDelegate(provider: self)
opFileManager.delegate = fileProviderManagerDelegate
}
open static func defaultBaseURL() -> URL {
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true);
return URL(fileURLWithPath: paths[0])
@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: [.nameKey, .fileSizeKey, .fileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .isHiddenKey, .volumeIsReadOnlyKey], options: .skipsSubdirectoryDescendants)
let contents = try self.fileManager.contentsOfDirectory(at: self.url(of: path), includingPropertiesForKeys: [.nameKey, .fileSizeKey, .fileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .isHiddenKey, .volumeIsReadOnlyKey], 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)
}
}
@@ -75,127 +111,223 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
@discardableResult
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(folderName) + "/")
operation_queue.async {
let url = self.url(of: atPath).appendingPathComponent(folderName)
let operationHandler: (URL) -> Void = { url in
do {
try self.opFileManager.createDirectory(at: self.absoluteURL(atPath).appendingPathComponent(folderName), withIntermediateDirectories: true, attributes: [:])
try self.opFileManager.createDirectory(at: url, withIntermediateDirectories: true, attributes: [:])
completionHandler?(nil)
DispatchQueue.main.async(execute: {
DispatchQueue.main.async {
self.delegate?.fileproviderSucceed(self, operation: opType)
})
} catch let e as NSError {
}
} catch let e {
completionHandler?(e)
DispatchQueue.main.async(execute: {
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
})
}
}
}
if isCoorinating {
let intent = NSFileAccessIntent.writingIntent(with: url, options: .forReplacing)
self.coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
completionHandler?(error)
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
}
})
} else {
operation_queue.addOperation {
operationHandler(url)
}
}
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
}
@discardableResult
open func create(file fileName: String, at atPath: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(fileName))
operation_queue.async {
let fileURL = self.absoluteURL(atPath).appendingPathComponent(fileName)
let success = self.opFileManager.createFile(atPath: fileURL.path, contents: data, attributes: nil)
let url = self.url(of: atPath).appendingPathComponent(fileName, isDirectory: false)
let operationHandler: (URL) -> Void = { url in
let success = self.opFileManager.createFile(atPath: url.path, contents: data, attributes: nil)
if success {
completionHandler?(nil)
DispatchQueue.main.async(execute: {
DispatchQueue.main.async {
self.delegate?.fileproviderSucceed(self, operation: opType)
})
}
} else {
completionHandler?(self.throwError(atPath, code: URLError.cannotCreateFile as FoundationErrorEnum))
DispatchQueue.main.async(execute: {
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
})
}
}
}
if isCoorinating {
let intent = NSFileAccessIntent.writingIntent(with:url, options: .forReplacing)
self.coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
completionHandler?(error)
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
}
})
} else {
operation_queue.addOperation {
operationHandler(url)
}
}
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
}
@discardableResult
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.move(source: path, destination: toPath)
operation_queue.async {
if !overwrite && self.fileManager.fileExists(atPath: self.absoluteURL(toPath).path) {
let sourceUrl = self.url(of: path)
let destUrl = self.url(of: toPath)
let sourceIntent = NSFileAccessIntent.writingIntent(with: sourceUrl, options: .forDeleting)
let destIntent = NSFileAccessIntent.writingIntent(with: destUrl, options: .forReplacing)
let operationHandler: (URL, URL) -> Void = { sourceUrl, destUrl in
if !overwrite && self.fileManager.fileExists(atPath: destUrl.path) {
completionHandler?(self.throwError(toPath, code: URLError.cannotMoveFile as FoundationErrorEnum))
return
}
do {
try self.opFileManager.moveItem(at: self.absoluteURL(path), to: self.absoluteURL(toPath))
try self.opFileManager.moveItem(at: sourceUrl, to: destUrl)
completionHandler?(nil)
DispatchQueue.main.async(execute: {
DispatchQueue.main.async {
self.delegate?.fileproviderSucceed(self, operation: opType)
})
} catch let e as NSError {
}
} catch let e {
completionHandler?(e)
DispatchQueue.main.async(execute: {
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
})
}
}
}
if isCoorinating {
coordinated(intents: [sourceIntent, destIntent], completionHandler: operationHandler) { (error) in
completionHandler?(error)
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
}
}
} else {
operation_queue.addOperation {
operationHandler(sourceUrl, destUrl)
}
}
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
}
@discardableResult
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.copy(source: path, destination: toPath)
operation_queue.async {
if !overwrite && self.fileManager.fileExists(atPath: self.absoluteURL(toPath).path) {
completionHandler?(self.throwError(toPath, code: URLError.cannotWriteToFile as FoundationErrorEnum))
let sourceUrl = self.url(of: path)
let destUrl = self.url(of: toPath)
let sourceIntent = NSFileAccessIntent.readingIntent(with: sourceUrl, options: .withoutChanges)
let destIntent = NSFileAccessIntent.writingIntent(with: destUrl, options: .forDeleting)
let operationHandler: (URL, URL) -> Void = { sourceUrl, destUrl in
if !overwrite && self.fileManager.fileExists(atPath: destUrl.path) {
completionHandler?(self.throwError(toPath, code: URLError.cannotMoveFile as FoundationErrorEnum))
return
}
do {
try self.opFileManager.copyItem(at: self.absoluteURL(path), to: self.absoluteURL(toPath))
try self.opFileManager.copyItem(at: sourceUrl, to: destUrl)
completionHandler?(nil)
DispatchQueue.main.async(execute: {
DispatchQueue.main.async {
self.delegate?.fileproviderSucceed(self, operation: opType)
})
} catch let e as NSError {
}
} catch let e {
completionHandler?(e)
DispatchQueue.main.async(execute: {
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
})
}
}
}
if isCoorinating {
coordinated(intents: [sourceIntent, destIntent], moving: true, completionHandler: operationHandler) { (error) in
completionHandler?(error)
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
}
}
} else {
operation_queue.addOperation {
operationHandler(sourceUrl, destUrl)
}
}
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
}
@discardableResult
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.remove(path: path)
operation_queue.async {
let url = self.url(of: path)
let operationHandler: (URL) -> Void = { url in
do {
try self.opFileManager.removeItem(at: self.absoluteURL(path))
let successfulSecurityScopedResourceAccess = url.startAccessingSecurityScopedResource()
try self.opFileManager.removeItem(at: url)
if successfulSecurityScopedResourceAccess {
url.stopAccessingSecurityScopedResource()
}
completionHandler?(nil)
DispatchQueue.main.async(execute: {
DispatchQueue.main.async {
self.delegate?.fileproviderSucceed(self, operation: opType)
})
} catch let e as NSError {
}
} catch let e {
completionHandler?(e)
DispatchQueue.main.async(execute: {
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
})
}
}
}
if isCoorinating {
let intent = NSFileAccessIntent.writingIntent(with:url, options: .forReplacing)
self.coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
completionHandler?(error)
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
}
})
} else {
operation_queue.addOperation {
operationHandler(url)
}
}
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
}
@discardableResult
open func copyItem(localFile: URL, to toPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
// TODO: Make use of overwrite parameter
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
operation_queue.async {
operation_queue.addOperation {
do {
try self.opFileManager.copyItem(at: localFile, to: self.absoluteURL(toPath))
try self.opFileManager.copyItem(at: localFile, to: self.url(of: toPath))
completionHandler?(nil)
DispatchQueue.main.async(execute: {
DispatchQueue.main.async {
self.delegate?.fileproviderSucceed(self, operation: opType)
})
} catch let e as NSError {
}
} catch let e {
completionHandler?(e)
DispatchQueue.main.async(execute: {
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
})
}
}
}
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
@@ -204,18 +336,18 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
@discardableResult
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
operation_queue.async {
operation_queue.addOperation {
do {
try self.opFileManager.copyItem(at: self.absoluteURL(path), to: toLocalURL)
try self.opFileManager.copyItem(at: self.url(of: path), to: toLocalURL)
completionHandler?(nil)
DispatchQueue.main.async(execute: {
DispatchQueue.main.async {
self.delegate?.fileproviderSucceed(self, operation: opType)
})
} catch let e as NSError {
}
} catch let e {
completionHandler?(e)
DispatchQueue.main.async(execute: {
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
})
}
}
}
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
@@ -223,52 +355,141 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
@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? {
let opType = FileOperationType.fetch(path: path)
dispatch_queue.async {
let aPath = self.absoluteURL(path).path
guard self.fileManager.fileExists(atPath: aPath) && !self.absoluteURL(path).fileIsDirectory else {
completionHandler(nil, self.throwError(path, code: URLError.cannotOpenFile as FoundationErrorEnum))
return
let url = self.url(of: path)
let operationHandler: (URL) -> Void = { url in
do {
let data = try Data(contentsOf: url)
completionHandler(data, nil)
} catch let e {
completionHandler(nil, e)
}
guard let handle = FileHandle(forReadingAtPath: aPath) else {
completionHandler(nil, self.throwError(path, code: URLError.cannotOpenFile as FoundationErrorEnum))
return
}
defer {
handle.closeFile()
}
handle.seek(toFileOffset: UInt64(offset))
let data = handle.readData(ofLength: length)
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, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.modify(path: path)
operation_queue.async {
try? data.write(to: self.absoluteURL(path), options: atomically ? [.atomic] : [])
DispatchQueue.main.async(execute: {
self.delegate?.fileproviderSucceed(self, operation: opType)
})
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
if length == 0 {
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 self.fileManager.fileExists(atPath: url.path) && !url.fileIsDirectory else {
completionHandler(nil, self.throwError(path, code: URLError.fileDoesNotExist as FoundationErrorEnum))
return
}
guard let handle = FileHandle(forReadingAtPath: url.path) else {
completionHandler(nil, self.throwError(path, code: URLError.cannotOpenFile as FoundationErrorEnum))
return
}
defer {
handle.closeFile()
}
handle.seek(toFileOffset: UInt64(offset))
guard Int64(handle.offsetInFile) == offset else {
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadUnknown as FoundationErrorEnum))
return
}
let data = handle.readData(ofLength: length)
guard length > 0 && data.count == length else {
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadTooLarge as FoundationErrorEnum))
return
}
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)
let url = self.url(of: path)
var options: Data.WritingOptions = []
if atomically {
options.insert(.atomic)
}
if overwrite {
options.insert(.withoutOverwriting)
}
let operationHandler: (URL) -> Void = { url in
do {
try data.write(to: url, options: atomically ? [.atomic] : [])
completionHandler?(nil)
DispatchQueue.main.async{
self.delegate?.fileproviderSucceed(self, operation: opType)
}
} catch let e {
completionHandler?(e)
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
}
}
}
if isCoorinating {
let intent = NSFileAccessIntent.writingIntent(with: url, options: .forReplacing)
coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
completionHandler?(error)
DispatchQueue.main.async {
self.delegate?.fileproviderFailed(self, operation: opType)
}
})
} else {
operation_queue.addOperation {
operationHandler(url)
}
}
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
}
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 ? [] : [.skipsSubdirectoryDescendants, .skipsPackageDescendants]) { (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
}
@@ -290,12 +511,12 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
open func registerNotifcation(path: String, eventHandler: @escaping (() -> Void)) {
self.unregisterNotifcation(path: path)
let absurl = self.absoluteURL(path)
let isdir = (try? absurl.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false) ?? 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()
@@ -329,18 +550,60 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
public extension LocalFileProvider {
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))
})
}
}
}
}
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 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
}
completionHandler(intents[0].url)
}
}
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
}
if moving {
coordinator.item(at: intents[0].url, willMoveTo: intents[1].url)
}
completionHandler(intents[0].url, intents[1].url)
if moving {
coordinator.item(at: intents[0].url, didMoveTo: intents[1].url)
}
}
}
+31 -17
View File
@@ -9,32 +9,38 @@
import Foundation
public final class LocalFileObject: FileObject {
internal init(absoluteURL: URL, name: String, path: String) {
super.init(absoluteURL: absoluteURL, name: name, path: path)
internal override init(url: URL, name: String, path: String) {
super.init(url: url, name: name, path: path)
}
public convenience init? (fileWithPath path: String, relativeTo relativeURL: URL?) {
let fileURL: URL
if let relativeURL = relativeURL {
fileURL = relativeURL.appendingPathComponent(path)
} else {
fileURL = URL(fileURLWithPath: path)
var fileURL: URL?
var rpath = path.replacingOccurrences(of: relativeURL?.absoluteString ?? "", with: "", options: .anchored)
if path.hasPrefix("/") {
rpath.remove(at: rpath.startIndex)
}
do {
let values = try fileURL.resourceValues(forKeys: [.nameKey, .fileSizeKey, .fileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .fileResourceTypeKey, .isHiddenKey, .isWritableKey])
self.init(absoluteURL: fileURL, name: values.name ?? fileURL.lastPathComponent, path: path)
for (key, value) in values.allValues {
self.allValues[key.rawValue] = value
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)
}
} catch {
}
if let fileURL = fileURL {
self.init(fileWithURL: fileURL)
} else {
return nil
}
}
public convenience init?(fileWithURL fileURL: URL) {
do {
let values = try fileURL.resourceValues(forKeys: [.nameKey, .fileSizeKey, .fileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .fileResourceTypeKey, .isHiddenKey, .isWritableKey, .typeIdentifierKey])
self.init(absoluteURL: fileURL, name: values.name ?? fileURL.lastPathComponent, path: fileURL.path)
let values = try fileURL.resourceValues(forKeys: [.nameKey, .fileSizeKey, .fileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .fileResourceTypeKey, .isHiddenKey, .isWritableKey, .typeIdentifierKey, .generationIdentifierKey])
let path = fileURL.relativePath.hasPrefix("/") ? fileURL.relativePath : "/" + fileURL.relativePath
self.init(url: fileURL, name: values.name ?? fileURL.lastPathComponent, path: path)
for (key, value) in values.allValues {
self.allValues[key.rawValue] = value
}
@@ -51,12 +57,20 @@ public final class LocalFileObject: FileObject {
allValues[URLResourceKey.fileAllocatedSizeKey.rawValue] = Int(exactly: newValue) ?? Int.max
}
}
open var rev: String? {
get {
let data = allValues[URLResourceKey.generationIdentifierKey.rawValue] as? Data
return data?.map { String(format: "%02hhx", $0) }.joined()
}
}
}
internal class LocalFolderMonitor {
fileprivate let source: DispatchSourceFileSystemObject
fileprivate let descriptor: CInt
fileprivate let qq: DispatchQueue = DispatchQueue.global(qos: DispatchQoS.QoSClass.default)
fileprivate let qq: DispatchQueue = DispatchQueue.global(qos: .default)
fileprivate var state: Bool = false
fileprivate var monitoredTime: TimeInterval = Date().timeIntervalSinceReferenceDate
var url: URL
@@ -190,7 +204,7 @@ open class LocalOperationHandle: OperationHandle {
public let operationType: FileOperationType
init (operationType: FileOperationType, baseURL: URL?) {
self.baseURL = baseURL ?? LocalFileProvider.defaultBaseURL()
self.baseURL = baseURL ?? URL(fileURLWithPath: "/")
self.operationType = operationType
}
+364
View File
@@ -0,0 +1,364 @@
//
// OneDriveFileProvider.swift
// FileProvider
//
// Created by Amir Abbas Mousavian.
// Copyright © 2017 Mousavian. Distributed under MIT license.
//
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 OneDriveFileProvider: FileProviderBasicRemote {
open static let type: String = "OneDrive"
open let isPathRelative: Bool
open let baseURL: URL?
open var serverURL: URL { return baseURL! }
open var drive: String
open var driveURL: URL {
return URL(string: "/drive/\(drive):/", relativeTo: baseURL)!
}
open var currentPath: String
open var dispatch_queue: DispatchQueue
open var operation_queue: OperationQueue {
willSet {
assert(_session == nil, "It's not effective to change dispatch_queue property after session is initialized.")
}
}
open weak var delegate: FileProviderDelegate?
open let credential: URLCredential?
open private(set) var cache: URLCache?
public var useCache: Bool
public var validatingCache: Bool
fileprivate var _session: URLSession?
fileprivate var sessionDelegate: SessionDelegate?
public var session: URLSession {
if _session == nil {
self.sessionDelegate = SessionDelegate(fileProvider: self, credential: credential)
let queue = OperationQueue()
//queue.underlyingQueue = dispatch_queue
let config = URLSessionConfiguration.default
config.urlCache = cache
config.requestCachePolicy = .returnCacheDataElseLoad
_session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDelegate?, delegateQueue: queue)
}
return _session!
}
public init? (credential: URLCredential?, serverURL: URL? = nil, drive: String = "root", cache: URLCache? = nil) {
self.baseURL = (serverURL ?? URL(string: "https://api.onedrive.com/")!).appendingPathComponent("")
self.drive = drive
self.isPathRelative = true
self.currentPath = ""
self.useCache = false
self.validatingCache = true
self.cache = cache
self.credential = credential
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: DispatchQueue.Attributes.concurrent)
operation_queue = OperationQueue()
operation_queue.name = "FileProvider.\(type(of: self).type).Operation"
}
deinit {
_session?.invalidateAndCancel()
}
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
list(path) { (contents, cursor, error) in
completionHandler(contents, error)
}
}
open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
let url = URL(string: escaped(path: path), relativeTo: driveURL)!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var dbError: FileProviderOneDriveError?
var fileObject: OneDriveFileObject?
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
dbError = 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, dbError ?? error)
})
task.resume()
}
open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) {
let url = URL(string: "/drive/root", relativeTo: baseURL)!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var totalSize: Int64 = -1
var usedSize: Int64 = 0
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr) {
totalSize = (json["total"] as? NSNumber)?.int64Value ?? -1
usedSize = (json["used"] as? NSNumber)?.int64Value ?? 0
}
completionHandler(totalSize, usedSize)
})
task.resume()
}
open weak var fileOperationDelegate: FileOperationDelegate?
}
extension OneDriveFileProvider: FileProviderOperations {
public func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let path = (atPath as NSString).appendingPathComponent(folderName) + "/"
return doOperation(.create(path: path), completionHandler: completionHandler)
}
public func create(file fileName: String, at path: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let filePath = (path as NSString).appendingPathComponent(fileName)
return self.writeContents(path: filePath, contents: data ?? Data(), completionHandler: completionHandler)
}
public func moveItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
return doOperation(.move(source: path, destination: toPath), completionHandler: completionHandler)
}
public func copyItem(path: String, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
return doOperation(.copy(source: path, destination: toPath), completionHandler: completionHandler)
}
public func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
return doOperation(.remove(path: path), completionHandler: completionHandler)
}
fileprivate func doOperation(_ operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: operation) ?? true == true else {
return nil
}
guard let sourcePath = operation.source else { return nil }
let destPath = operation.destination
var request = URLRequest(url: URL(string: sourcePath, relativeTo: driveURL)!)
switch operation {
case .create:
request.httpMethod = "CREATE"
case .copy:
request.httpMethod = "POST"
case .move:
request.httpMethod = "PATCH"
case .remove:
request.httpMethod = "DELETE"
default: // modify, link, fetch
return nil
}
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
var requestDictionary = [String: AnyObject]()
if let dest = correctPath(destPath) as NSString? {
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
requestDictionary["parentReference"] = ("/drive/\(drive):" + dest.deletingLastPathComponent) as NSString
requestDictionary["name"] = dest.lastPathComponent as NSString
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
}
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var dbError: FileProviderOneDriveError?
if let response = response as? HTTPURLResponse, response.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) {
dbError = FileProviderOneDriveError(code: code, path: sourcePath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
}
completionHandler?(dbError ?? error)
self.delegateNotify(operation, error: dbError ?? error)
})
task.taskDescription = operation.json
task.resume()
return RemoteOperationHandle(operationType: operation, tasks: [task])
}
public func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
return upload_simple(toPath, localFile: localFile, overwrite: overwrite, operation: opType, completionHandler: completionHandler)
}
public func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.copy(source: path, destination: destURL.absoluteString)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
let url = URL(string: escaped(path: path) + ":/content", relativeTo: driveURL)!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
let task = session.downloadTask(with: request, completionHandler: { (cacheURL, response, error) in
guard let cacheURL = cacheURL, let httpResponse = response as? HTTPURLResponse , httpResponse.statusCode < 300 else {
let code = FileProviderHTTPErrorCode(rawValue: (response as? HTTPURLResponse)?.statusCode ?? -1)
let errorData : Data? = nil //Data(contentsOf:cacheURL) // TODO: Figure out how to get error response data for the error description
let dbError : FileProviderOneDriveError? = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: errorData ?? Data(), encoding: .utf8)) : nil
completionHandler?(dbError ?? error)
return
}
do {
try FileManager.default.moveItem(at: cacheURL, to: destURL)
completionHandler?(nil)
} catch let e {
completionHandler?(e)
}
})
task.taskDescription = opType.json
task.resume()
return RemoteOperationHandle(operationType: opType, tasks: [task])
}
}
extension OneDriveFileProvider: FileProviderReadWrite {
public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
if length == 0 {
return nil
}
let opType = FileOperationType.fetch(path: path)
let url = URL(string: escaped(path: path) + ":/content", relativeTo: driveURL)!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
if length > 0 {
request.setValue("bytes=\(offset)-\(offset + length - 1)", forHTTPHeaderField: "Range")
} else if offset > 0 && length < 0 {
request.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
}
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var dbError: FileProviderOneDriveError?
if let httpResponse = response as? HTTPURLResponse , httpResponse.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: httpResponse.statusCode) {
dbError = FileProviderOneDriveError(code: code, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8))
}
let filedata = dbError ?? error == nil ? data : nil
completionHandler(filedata, dbError ?? error)
})
task.taskDescription = opType.json
task.resume()
return RemoteOperationHandle(operationType: opType, tasks: [task])
}
public func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.modify(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
// FIXME: remove 150MB restriction
return upload_simple(path, data: data, overwrite: overwrite, operation: opType, completionHandler: completionHandler)
}
public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
var foundFiles = [OneDriveFileObject]()
search(path, query: query, foundItem: { (file) in
foundFiles.append(file)
foundItemHandler?(file)
}, completionHandler: { (error) in
completionHandler(foundFiles, error)
})
}
fileprivate func registerNotifcation(path: String, eventHandler: (() -> Void)) {
/* There is two ways to monitor folders changing in OneDrive. Either using webooks
* which means you have to implement a server to translate it to push notifications
* or using apiv2 list_folder/longpoll method. The second one is implemeted here.
* Tough webhooks are much more efficient, longpoll is much simpler to implement!
* You can implemnt your own webhook service and replace this method accordingly.
*/
NotImplemented()
}
fileprivate func unregisterNotifcation(path: String) {
NotImplemented()
}
// TODO: Implement /copy_reference, /get_account & /get_current_account
}
extension OneDriveFileProvider: ExtendedFileProvider {
public func thumbnailOfFileSupported(path: String) -> Bool {
return true
}
public func propertiesOfFileSupported(path: String) -> Bool {
let fileExt = (path as NSString).pathExtension.lowercased()
switch fileExt {
case "jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff":
return true
case "mp3", "aac", "m4a", "wma":
return true
case "mp4", "mpg", "3gp", "mov", "avi", "wmv":
return true
default:
return false
}
}
public func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) {
let url: URL
if let dimension = dimension {
url = URL(string: escaped(path: path) + ":/thumbnails/0/=c\(dimension.width)x\(dimension.height)/content", relativeTo: driveURL)!
} else {
url = URL(string: escaped(path: path) + ":/thumbnails/0/small/content", relativeTo: driveURL)!
}
var request = URLRequest(url: url)
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
let task = self.session.dataTask(with: request, completionHandler: { (data, response, error) in
var image: ImageClass? = nil
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
let responseError = FileProviderOneDriveError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8))
completionHandler(nil, responseError)
return
}
if let data = data {
image = ImageClass(data: data)
}
completionHandler(image, error)
})
task.resume()
}
public func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String : Any], _ keys: [String], _ error: Error?) -> Void)) {
let url = URL(string: escaped(path: path), relativeTo: driveURL)!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var dbError: FileProviderOneDriveError?
var dic = [String: Any]()
var keys = [String]()
if let response = response as? HTTPURLResponse {
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
dbError = 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, dbError ?? 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
}
}
+317
View File
@@ -0,0 +1,317 @@
//
// OneDriveHelper.swift
// FileProvider
//
// Created by Amir Abbas Mousavian.
// Copyright © 2017 Mousavian. Distributed under MIT license.
//
import Foundation
public struct FileProviderOneDriveError: Error, CustomStringConvertible {
public let code: FileProviderHTTPErrorCode
public let path: String
public let errorDescription: String?
public var description: String {
return code.description
}
}
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 }
let lPath = path.replacingOccurrences(of: "/drive/\(drive):", with: "/", options: .anchored, range: nil)
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
}
open internal(set) var id: String? {
get {
return allValues["NSURLDocumentIdentifyKey"] as? String
}
set {
allValues["NSURLDocumentIdentifyKey"] = newValue
}
}
open internal(set) var contentType: String {
get {
return allValues["NSURLContentTypeKey"] as? String ?? ""
}
set {
allValues["NSURLContentTypeKey"] = newValue
}
}
open internal(set) var entryTag: String? {
get {
return allValues["NSURLEntryTagKey"] as? String
}
set {
allValues["NSURLEntryTagKey"] = newValue
}
}
}
// codebeat:disable[ARITY]
internal extension OneDriveFileProvider {
func list(_ path: String, cursor: String? = nil, prevContents: [OneDriveFileObject] = [], completionHandler: @escaping ((_ contents: [FileObject], _ cursor: String?, _ error: Error?) -> Void)) {
let url: URL
if let cursor = cursor {
url = URL(string: cursor)!
} else {
url = URL(string: escaped(path: path), relativeTo: driveURL)!
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var responseError: FileProviderOneDriveError?
var files = prevContents
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderOneDriveError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8))
}
if let data = data, let jsonStr = String(data: data, encoding: .utf8) {
let json = jsonToDictionary(jsonStr)
if let entries = json?["value"] as? [AnyObject] , entries.count > 0 {
for entry in entries {
if let entry = entry as? [String: AnyObject], let file = OneDriveFileObject(baseURL: self.baseURL, drive: self.drive, json: entry) {
files.append(file)
}
}
let ncursor = json?["@odata.nextLink"] as? String
let hasmore = ncursor != nil
if hasmore {
self.list(path, cursor: ncursor, prevContents: files, completionHandler: completionHandler)
return
}
}
}
completionHandler(files, nil, responseError ?? error)
})
task.taskDescription = FileOperationType.fetch(path: path).json
task.resume()
}
func upload_simple(_ targetPath: String, data: Data, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
if data.count > 100 * 1024 * 1024 {
let error = FileProviderOneDriveError(code: .payloadTooLarge, path: targetPath, errorDescription: nil)
completionHandler?(error)
self.delegateNotify(.create(path: targetPath), error: error)
return nil
}
let queryStr = overwrite ? "" : "?@name.conflictBehavior=fail"
let url = URL(string: escaped(path: targetPath) + ":/content" + queryStr, relativeTo: driveURL)!
var request = URLRequest(url: url)
request.httpMethod = "PUT"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
request.httpBody = data
let task = session.uploadTask(with: request, from: data, completionHandler: { (data, response, error) in
var responseError: FileProviderOneDriveError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderOneDriveError(code: rCode, path: targetPath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
}
completionHandler?(responseError ?? error)
self.delegateNotify(.create(path: targetPath), error: responseError ?? error)
})
task.taskDescription = operation.json
task.resume()
return RemoteOperationHandle(operationType: operation, tasks: [task])
}
func upload_simple(_ targetPath: String, localFile: URL, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let size = (try? localFile.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? -1
if size > 100 * 1024 * 1024 {
let error = FileProviderOneDriveError(code: .payloadTooLarge, path: targetPath, errorDescription: nil)
completionHandler?(error)
self.delegateNotify(.create(path: targetPath), error: error)
return nil
}
let queryStr = overwrite ? "" : "?@name.conflictBehavior=fail"
let url = URL(string: escaped(path: targetPath) + ":/content" + queryStr, relativeTo: driveURL)!
var request = URLRequest(url: url)
request.httpMethod = "PUT"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
let task = session.uploadTask(with: request, fromFile: localFile, completionHandler: { (data, response, error) in
var responseError: FileProviderOneDriveError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderOneDriveError(code: rCode, path: targetPath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
}
completionHandler?(responseError ?? error)
self.delegateNotify(.create(path: targetPath), error: responseError ?? error)
})
task.taskDescription = operation.json
task.resume()
return RemoteOperationHandle(operationType: operation, tasks: [task])
}
func search(_ startPath: String = "", query: String, next: String? = nil, foundItem:@escaping ((_ file: OneDriveFileObject) -> Void), completionHandler: @escaping ((_ error: Error?) -> Void)) {
let url: URL
if let next = next {
url = URL(string: next)!
} else if self.escaped(path: startPath) == "" {
url = URL(string: "/drive/\(drive)/view.search?q=\(query)", relativeTo: baseURL)!
} else {
url = URL(string: "\(escaped(path: startPath))/view.search?q=\(query)", relativeTo: driveURL)!
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
var responseError: FileProviderOneDriveError?
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
responseError = FileProviderOneDriveError(code: rCode, path: startPath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
}
if let data = data, let jsonStr = String(data: data, encoding: .utf8) {
let json = jsonToDictionary(jsonStr)
if let entries = json?["value"] as? [AnyObject] , entries.count > 0 {
for entry in entries {
if let entry = entry as? [String: AnyObject], let file = OneDriveFileObject(baseURL: self.baseURL, drive: self.drive, json: entry) {
foundItem(file)
}
}
let next = json?["@odata.nextLink"] as? String
let hasmore = next != nil
if hasmore, let next = next {
self.search(startPath, query: query, next: next, foundItem: foundItem, completionHandler: completionHandler)
} else {
completionHandler(responseError ?? error)
}
return
}
}
completionHandler(responseError ?? error)
})
task.resume()
}
}
// codebeat:enable[ARITY]
internal extension OneDriveFileProvider {
static let dateFormatter = DateFormatter()
static let decimalFormatter = NumberFormatter()
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]()
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 {
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 {
OneDriveFileProvider.decimalFormatter.numberStyle = .decimal
OneDriveFileProvider.decimalFormatter.maximumFractionDigits = 5
keys.append("Location")
let latStr = OneDriveFileProvider.decimalFormatter.string(from: NSNumber(value: latitude))
let longStr = OneDriveFileProvider.decimalFormatter.string(from: NSNumber(value: longitude))
dic["Location"] = "\(latStr), \(longStr)"
}
if let parent = json["image"] as? [String: Any] ?? json["video"] as? [String: Any], let duration = parent["duration"] as? UInt64 {
keys.append("Duration")
dic["Duration"] = OneDriveFileProvider.formatshort(interval: TimeInterval(duration) / 1000)
}
if let timeTakenStr = json["takenDateTime"] as? String, let timeTaken = resolve(dateString: timeTakenStr) {
keys.append("Date taken")
OneDriveFileProvider.dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
dic["Date taken"] = OneDriveFileProvider.dateFormatter.string(from: timeTaken)
}
if let photo = json["photo"] as? [String: Any] {
if let devicemake = photo["cameraMake"] as? String {
keys.append("Device make")
dic["Device make"] = devicemake
}
if let devicemodel = photo["cameraModel"] as? String {
keys.append("Device model")
dic["Device model"] = devicemodel
}
if let focallen = photo["focalLength"] as? Double {
keys.append("Focal length")
dic["Focal length"] = focallen
}
if let fnum = photo["fNumber"] as? Double {
keys.append("F number")
dic["F number"] = fnum
}
if let expNom = photo["exposureNumerator"] as? Double, let expDen = photo["exposureDenominator"] as? Double {
keys.append("Exposure time")
dic["Exposure time"] = "\(Int(expNom))/\(Int(expDen))"
}
if let iso = photo["iso"] as? Int64 {
keys.append("ISO speed")
dic["ISO speed"] = iso
}
}
if let audio = json["audio"] as? [String: Any] {
for (key, value) in audio {
if key == "bitrate" || key == "isVariableBitrate" { continue }
let casedKey = spaceCamelCase(key)
keys.append(casedKey)
dic[casedKey] = value
}
}
if let video = json["video"] as? [String: Any] {
if let bitRate = video["bitrate"] as? Int {
keys.append("Bitrate")
dic["Bitrate"] = bitRate
}
}
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)
}
})
}
}
+5 -5
View File
@@ -201,11 +201,11 @@ public enum FileProviderHTTPErrorCode: Int {
case notExtended = 510
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 {
+9 -5
View File
@@ -9,11 +9,13 @@
import Foundation
class SMBFileProvider: FileProvider, FileProviderMonitor {
open static var type: String = "Samba"
open var isPathRelative: Bool = true
open var baseURL: URL?
open var currentPath: String = ""
open var dispatch_queue: DispatchQueue
open var operation_queue: OperationQueue
open weak var delegate: FileProviderDelegate?
open let credential: URLCredential?
@@ -23,9 +25,11 @@ 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: DispatchQueue.Attributes.concurrent)
operation_queue = OperationQueue()
operation_queue.name = "FileProvider.\(type(of: self).type).Operation"
self.credential = credential
}
@@ -68,7 +72,7 @@ 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,7 +92,7 @@ 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
}
+29
View File
@@ -66,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()
}
}
-29
View File
@@ -8,35 +8,6 @@
import Foundation
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()
}
}
protocol FileProviderSMBHeader {
var protocolID: UInt32 { get }
static var protocolConst: UInt32 { get }
+167 -138
View File
@@ -8,48 +8,27 @@
import Foundation
public final class WebDavFileObject: FileObject {
internal init(absoluteURL: URL, name: String, path: String) {
super.init(absoluteURL: absoluteURL, name: name, path: path)
}
open internal(set) var contentType: String {
get {
return allValues["NSURLContentTypeKey"] as? String ?? ""
}
set {
allValues["NSURLContentTypeKey"] = newValue
}
}
open internal(set) var entryTag: String? {
get {
return allValues["NSURLEntryTagKey"] as? String
}
set {
allValues["NSURLEntryTagKey"] = newValue
}
}
}
/// 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, FileProviderBasicRemote {
open class WebDAVFileProvider: FileProviderBasicRemote {
open static let type: String = "WebDAV"
open let isPathRelative: Bool = true
open let isPathRelative: Bool
open let baseURL: URL?
open var currentPath: String = ""
public 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.")
}
}
public weak var delegate: FileProviderDelegate?
open let credential: URLCredential?
open private(set) var cache: URLCache?
public var useCache: Bool = false
public var validatingCache: Bool = true
public var useCache: Bool
public var validatingCache: Bool
fileprivate var _session: URLSession?
fileprivate var sessionDelegate: SessionDelegate?
@@ -70,11 +49,16 @@ open class WebDAVFileProvider: NSObject, FileProviderBasicRemote {
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.credential = credential
self.baseURL = baseURL.appendingPathComponent("")
self.isPathRelative = true
self.currentPath = ""
self.useCache = false
self.validatingCache = true
self.cache = cache
self.credential = credential
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: DispatchQueue.Attributes.concurrent)
operation_queue = OperationQueue()
operation_queue.name = "FileProvider.\(type(of: self).type).Operation"
}
deinit {
@@ -83,7 +67,7 @@ open class WebDAVFileProvider: NSObject, FileProviderBasicRemote {
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
let opType = FileOperationType.fetch(path: path)
let url = absoluteURL(path)
let url = self.url(of: path).appendingPathComponent("")
var request = URLRequest(url: url)
request.httpMethod = "PROPFIND"
request.setValue("1", forHTTPHeaderField: "Depth")
@@ -97,12 +81,12 @@ open class WebDAVFileProvider: NSObject, FileProviderBasicRemote {
}
var fileObjects = [WebDavFileObject]()
if let data = data {
let xresponse = self.parseXMLResponse(data)
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)
@@ -110,7 +94,7 @@ open class WebDAVFileProvider: NSObject, FileProviderBasicRemote {
}
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")
@@ -123,9 +107,9 @@ open class WebDAVFileProvider: NSObject, FileProviderBasicRemote {
responseError = FileProviderWebDavError(code: rCode, 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
}
}
@@ -150,7 +134,7 @@ open class WebDAVFileProvider: NSObject, FileProviderBasicRemote {
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 {
totalSize = Int64(attr.prop["quota-available-bytes"] ?? "") ?? -1
usedSize = Int64(attr.prop["quota-used-bytes"] ?? "") ?? 0
@@ -170,7 +154,7 @@ extension WebDAVFileProvider: FileProviderOperations {
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
@@ -192,7 +176,7 @@ extension WebDAVFileProvider: FileProviderOperations {
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
let url = absoluteURL(path).appendingPathComponent(fileName)
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
@@ -236,10 +220,10 @@ extension WebDAVFileProvider: FileProviderOperations {
}
func doOperation(operation opType: FileOperationType, overwrite: Bool? = nil, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let sourceURL = absoluteURL(opType.source!)
let sourceURL = self.url(of:opType.source!)
var request = URLRequest(url: sourceURL)
if let dest = opType.destination {
request.setValue(absoluteURL(dest).absoluteString, forHTTPHeaderField: "Destination")
request.setValue(url(of:dest).absoluteString, forHTTPHeaderField: "Destination")
}
switch opType {
case .copy:
@@ -262,7 +246,7 @@ extension WebDAVFileProvider: FileProviderOperations {
responseError = FileProviderWebDavError(code: code, 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: sourceURL))
}
@@ -280,12 +264,13 @@ extension WebDAVFileProvider: FileProviderOperations {
}
@discardableResult
public func copyItem(localFile: URL, to toPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
public func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
// TODO: Make use of overwrite parameter
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
let url = absoluteURL(toPath)
let url = self.url(of:toPath)
var request = URLRequest(url: url)
request.httpMethod = "PUT"
let task = session.uploadTask(with: request, fromFile: localFile, completionHandler: { (data, response, error) in
@@ -307,7 +292,7 @@ extension WebDAVFileProvider: FileProviderOperations {
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?
@@ -332,19 +317,18 @@ extension WebDAVFileProvider: FileProviderOperations {
}
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? {
if length == 0 {
return nil
}
let opType = FileOperationType.fetch(path: path)
let url = absoluteURL(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")
}
@@ -360,19 +344,22 @@ extension WebDAVFileProvider: FileProviderReadWrite {
}
@discardableResult
public func writeContents(path: String, contents data: Data, atomically: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
public func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
let opType = FileOperationType.modify(path: path)
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
return nil
}
// FIXME: lock destination before writing process
let url = atomically ? 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, url: self.url(of: path))
}
defer {
self.delegateNotify(opType, error: responseError ?? error)
@@ -391,7 +378,7 @@ extension WebDAVFileProvider: FileProviderReadWrite {
}
public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [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")
@@ -404,14 +391,14 @@ extension WebDAVFileProvider: FileProviderReadWrite {
responseError = FileProviderWebDavError(code: rCode, url: url)
}
if let data = data {
let xresponse = self.parseXMLResponse(data)
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
var fileObjects = [WebDavFileObject]()
for attr in xresponse {
let path = attr.href.path
if !((path as NSString).lastPathComponent.contains(query)) {
continue
}
let fileObject = self.mapToFileObject(attr)
let fileObject = WebDavFileObject(attr)
fileObjects.append(fileObject)
foundItemHandler?(fileObject)
}
@@ -429,9 +416,10 @@ 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
}
@@ -451,37 +439,34 @@ extension WebDAVFileProvider: FileProvider {
// 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"
@@ -496,63 +481,107 @@ 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
return result
}
fileprivate func mapToFileObject(_ davResponse: DavResponse) -> WebDavFileObject {
var href = davResponse.href
if href.baseURL == nil {
href = absoluteURL(href.path)
}
}
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 fileObject = WebDavFileObject(absoluteURL: href, name: name, path: href.path)
fileObject.size = Int64(davResponse.prop["getcontentlength"] ?? "-1") ?? NSURLSessionTransferSizeUnknown
fileObject.creationDate = self.resolve(dateString: davResponse.prop["creationdate"] ?? "")
fileObject.modifiedDate = self.resolve(dateString: davResponse.prop["getlastmodified"] ?? "")
fileObject.contentType = davResponse.prop["getcontenttype"] ?? "octet/stream"
fileObject.fileType = fileObject.contentType == "httpd/unix-directory" ? .directory : .regular
fileObject.entryTag = davResponse.prop["getetag"]
return fileObject
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"]
}
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)
}
})
/// MIME type of the file
open internal(set) var contentType: String {
get {
return allValues["NSURLContentTypeKey"] as? String ?? ""
}
set {
allValues["NSURLContentTypeKey"] = newValue
}
}
/// HTTP E-Tag, can be used to mark changed files
open internal(set) var entryTag: String? {
get {
return allValues["NSURLEntryTagKey"] as? String
}
set {
allValues["NSURLEntryTagKey"] = newValue
}
}
}