Compare commits

...

73 Commits

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