Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 525825ff5d | |||
| a35384dc31 | |||
| 0c558fdbb4 | |||
| e30861ad18 | |||
| 538e9bb42d | |||
| ef2d69c54c | |||
| 9f210ef356 | |||
| 5a7dfa1039 | |||
| be7a370a42 | |||
| b84e6ea7ab | |||
| 82b1d2c450 | |||
| c38ee1ccd3 | |||
| 1a44df3fd7 | |||
| eff3725680 | |||
| 9368f636d3 | |||
| 3f2fda638f | |||
| 6737f98978 | |||
| 3e55cc60f7 | |||
| d7ae709cf7 | |||
| 4f6e3c7bd5 | |||
| b121fe6182 | |||
| 29b7f720f5 | |||
| 067e14b76f | |||
| 09e39ada82 | |||
| 6889b302b7 | |||
| 96ce12f226 | |||
| 091fd14a88 | |||
| 10aac532f7 |
+15
-10
@@ -1,9 +1,16 @@
|
||||
language: objective-c
|
||||
osx_image: xcode8
|
||||
|
||||
osx_image: xcode8.2
|
||||
|
||||
xcode_project: FileProvider.xcodeproj
|
||||
env:
|
||||
global:
|
||||
- FRAMEWORK_NAME=FileProvider.framework
|
||||
- PROJECTNAME="FileProvider"
|
||||
- FRAMEWORK_NAME="FileProvider.framework"
|
||||
matrix:
|
||||
- SHCEME="FileProvider OSX" SDK="macosx" ACTION="build"
|
||||
- SHCEME="FileProvider iOS" SDK="iphonesimulator" ACTION="build"
|
||||
- SHCEME="FileProvider tvOS" SDK="appletvsimulator" ACTION="build"
|
||||
before_install:
|
||||
- gem install xcpretty --no-rdoc --no-ri --no-document --quiet
|
||||
- brew update
|
||||
@@ -11,17 +18,15 @@ before_install:
|
||||
script:
|
||||
- set pipefail
|
||||
- xcodebuild -version
|
||||
- xcodebuild -project FileProvider.xcodeproj -scheme "FileProvider OSX" -sdk macosx | xcpretty
|
||||
- xcodebuild -project FileProvider.xcodeproj -scheme "FileProvider iOS" -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO | xcpretty
|
||||
- xcodebuild -project FileProvider.xcodeproj -scheme "FileProvider tvOS" -sdk appletvsimulator ONLY_ACTIVE_ARCH=NO | xcpretty
|
||||
- pod lib lint --quick
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
- xcodebuild -project $PROJECT.xcodeproj -scheme "$SCHEME" -sdk $SDK $ACTION ONLY_ACTIVE_ARCH=NO | xcpretty
|
||||
# - pod lib lint --quick
|
||||
# after_success:
|
||||
# - bash <(curl -s https://codecov.io/bash)
|
||||
before_deploy:
|
||||
- carthage build --no-skip-current
|
||||
- carthage archive FileProvider
|
||||
- carthage archive $PROJECTNAME
|
||||
deploy:
|
||||
file: FileProvider.framework.zip
|
||||
file: $PROJECTNAME.framework.zip
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
@@ -16,7 +16,7 @@ Pod::Spec.new do |s|
|
||||
#
|
||||
|
||||
s.name = "FileProvider"
|
||||
s.version = "0.10.0"
|
||||
s.version = "0.12.2"
|
||||
s.summary = "FileManager replacement for Local and Remote (WebDAV/Dropbox/OneDrive/SMB2) files on iOS and macOS."
|
||||
|
||||
# This description is used to generate tags and improve search results.
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
792572411DF23BDA006A1526 /* LocalHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792572401DF23BDA006A1526 /* LocalHelper.swift */; };
|
||||
792572421DF23BDA006A1526 /* LocalHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792572401DF23BDA006A1526 /* LocalHelper.swift */; };
|
||||
792572431DF23BDA006A1526 /* LocalHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792572401DF23BDA006A1526 /* LocalHelper.swift */; };
|
||||
79480FF61E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79480FF51E3ABDD0007E7275 /* CloudFileProvider.swift */; };
|
||||
79480FF71E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79480FF51E3ABDD0007E7275 /* CloudFileProvider.swift */; };
|
||||
79480FF81E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79480FF51E3ABDD0007E7275 /* CloudFileProvider.swift */; };
|
||||
794C21FE1D58912A00EC49B8 /* DropboxHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */; };
|
||||
794C21FF1D58912A00EC49B8 /* DropboxHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */; };
|
||||
794C22001D58912A00EC49B8 /* DropboxHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */; };
|
||||
@@ -127,6 +130,7 @@
|
||||
7924B1921D89DAE000589DB7 /* Parser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = "<group>"; };
|
||||
7924B1A81D89F79200589DB7 /* FPSStreamTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FPSStreamTask.swift; sourceTree = "<group>"; };
|
||||
792572401DF23BDA006A1526 /* LocalHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalHelper.swift; sourceTree = "<group>"; };
|
||||
79480FF51E3ABDD0007E7275 /* CloudFileProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudFileProvider.swift; sourceTree = "<group>"; };
|
||||
794C21FD1D58912A00EC49B8 /* DropboxHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DropboxHelper.swift; sourceTree = "<group>"; };
|
||||
794C22091D5893F800EC49B8 /* SMB2Notification.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMB2Notification.swift; sourceTree = "<group>"; };
|
||||
794C220D1D591A4B00EC49B8 /* SMB2QueryTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMB2QueryTypes.swift; sourceTree = "<group>"; };
|
||||
@@ -286,6 +290,7 @@
|
||||
79F5745A1DFDB10A00179ABF /* FileObject.swift */,
|
||||
799396961D48C02300086753 /* LocalFileProvider.swift */,
|
||||
792572401DF23BDA006A1526 /* LocalHelper.swift */,
|
||||
79480FF51E3ABDD0007E7275 /* CloudFileProvider.swift */,
|
||||
79BD638B1E2CC2300035128C /* ExtendedLocalFileProvider.swift */,
|
||||
7902C0851D61B56D00564440 /* RemoteSession.swift */,
|
||||
799396931D48C02300086753 /* DropboxFileProvider.swift */,
|
||||
@@ -480,6 +485,7 @@
|
||||
799396D41D48C02300086753 /* SMB2Tree.swift in Sources */,
|
||||
7924B1A21D89DAE000589DB7 /* Options.swift in Sources */,
|
||||
792572411DF23BDA006A1526 /* LocalHelper.swift in Sources */,
|
||||
79480FF61E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */,
|
||||
79F5745B1DFDB10B00179ABF /* FileObject.swift in Sources */,
|
||||
79BD63C51E2D17880035128C /* OneDriveFileProvide.swift in Sources */,
|
||||
7924B1991D89DAE000589DB7 /* Element.swift in Sources */,
|
||||
@@ -519,6 +525,7 @@
|
||||
799396D51D48C02300086753 /* SMB2Tree.swift in Sources */,
|
||||
7924B1A31D89DAE000589DB7 /* Options.swift in Sources */,
|
||||
792572421DF23BDA006A1526 /* LocalHelper.swift in Sources */,
|
||||
79480FF71E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */,
|
||||
79F5745C1DFDB10B00179ABF /* FileObject.swift in Sources */,
|
||||
79BD63C61E2D17880035128C /* OneDriveFileProvide.swift in Sources */,
|
||||
7924B1B01D89F7DE00589DB7 /* FPSStreamTask.swift in Sources */,
|
||||
@@ -558,6 +565,7 @@
|
||||
799396D61D48C02300086753 /* SMB2Tree.swift in Sources */,
|
||||
7924B1A41D89DAE000589DB7 /* Options.swift in Sources */,
|
||||
792572431DF23BDA006A1526 /* LocalHelper.swift in Sources */,
|
||||
79480FF81E3ABDD0007E7275 /* CloudFileProvider.swift in Sources */,
|
||||
79F5745D1DFDB10B00179ABF /* FileObject.swift in Sources */,
|
||||
79BD63C71E2D17880035128C /* OneDriveFileProvide.swift in Sources */,
|
||||
7924B1B11D89F7DF00589DB7 /* FPSStreamTask.swift in Sources */,
|
||||
@@ -595,7 +603,7 @@
|
||||
799396601D48B7BF00086753 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_VERSION_STRING = 0.10.0;
|
||||
BUNDLE_VERSION_STRING = 0.12.2;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
@@ -625,7 +633,7 @@
|
||||
799396611D48B7BF00086753 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_VERSION_STRING = 0.10.0;
|
||||
BUNDLE_VERSION_STRING = 0.12.2;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Amir Abbas Mousavian
|
||||
Copyright (c) 2016-17 Amir Abbas Mousavian
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -3,17 +3,21 @@
|
||||
>This Swift library provide a swifty way to deal with local and remote files and directories in a unified way.
|
||||
|
||||
[![Swift Version][swift-image]][swift-url]
|
||||
[![Platform][platform-image]](#)
|
||||
[![License][license-image]][license-url]
|
||||
[]()
|
||||
|
||||
[![Release versin][release-image]][release-url]
|
||||
[][cocoapods]
|
||||
[![Carthage compatible][carthage-image]](https://github.com/Carthage/Carthage)
|
||||
|
||||
[![Build Status][travis-image]][travis-url]
|
||||
[](https://cocoapods.org/pods/FileProvider)
|
||||
[![codebeat badge][codebeat-image]][codebeat-url]
|
||||
[![Codebeat Badge][codebeat-image]][codebeat-url]
|
||||
[![Cocoapods Docs][docs-image]][docs-url]
|
||||
[![Cocoapods Downloads][cocoapods-downloads]][cocoapods]
|
||||
[![Cocoapods Apps][cocoapods-apps]][cocoapods]
|
||||
|
||||
<!---
|
||||
|
||||
[](https://codecov.io/gh/amosavian/FileProvider)
|
||||
[](https://github.com/Carthage/Carthage)
|
||||
[](https://codecov.io/gh/amosavian/FileProvider)
|
||||
--->
|
||||
|
||||
This library provides implementaion of WebDav, Dropbox, OneDrive and SMB2 (incomplete) and local files.
|
||||
@@ -25,12 +29,18 @@ Local and WebDAV providers are fully tested and can be used in production enviro
|
||||
## Features
|
||||
|
||||
- [x] **LocalFileProvider** a wrapper around `FileManager` with some additions like searching and reading a portion of file.
|
||||
- [x] **CloudFileProvider** A wrapper around app's ubiquitous container to iCloud Drive in iOS 8+ API.
|
||||
- [x] **WebDAVFileProvider** WebDAV protocol is defacto file transmission standard, replaced FTP.
|
||||
- [x] **DropboxFileProvider** A wrapper around Dropbox Web API. For now it has limitation in uploading files up to 150MB.
|
||||
- [x] **OneDriveFileProvider** A wrapper around OneDrive Web API, works with `onedrive.com` and compatible servers. For now it has limitation in uploading files up to 100MB.
|
||||
- [ ] **SMBFileProvider** SMB2/3 introduced in 2006, which is a file and printer sharing protocol originated from Microsoft Windows and now is replacing AFP protocol on MacOS. I implemented data types and some basic functions but *main interface is not implemented yet!* SMB1/CIFS is depericated and very tricky to be implemented
|
||||
- [x] **DropboxFileProvider** A wrapper around Dropbox Web API.
|
||||
* For now it has limitation in uploading files up to 150MB.
|
||||
- [x] **OneDriveFileProvider** A wrapper around OneDrive REST API, works with `onedrive.com` and compatible (business) servers.
|
||||
* For now it has limitation in uploading files up to 100MB.
|
||||
- [ ] **GoogleFileProvider** A wrapper around Goodle Drive REST API.
|
||||
- [ ] **AmazonS3FileProvider** Amazon storage backend. Used by many sites.
|
||||
- [ ] **SMBFileProvider** SMB2/3 introduced in 2006, which is a file and printer sharing protocol originated from Microsoft Windows and now is replacing AFP protocol on macOS.
|
||||
* Data types and some basic functions are implemented but *main interface is not implemented yet!*.
|
||||
* SMB1/CIFS is deprecated and very tricky to be implemented.
|
||||
- [ ] **FTPFileProvider** while deprecated in 1990s, it's still in use on some Web hosts.
|
||||
- [ ] **AmazonS3FileProvider**
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -59,7 +69,7 @@ github "amosavian/FileProvider"
|
||||
Or to use in Swift Package Manager add this line in `Dependencies`:
|
||||
|
||||
```swift
|
||||
.Package(url: "https://github.com/amosavian/FileProvider.git", majorVersion: 0, minorVersion: 8)
|
||||
.Package(url: "https://github.com/amosavian/FileProvider.git", majorVersion: 0, minorVersion: 12)
|
||||
```
|
||||
|
||||
### Manually
|
||||
@@ -97,18 +107,31 @@ For LocalFileProvider if you want to deal with `Documents` folder
|
||||
|
||||
``` swift
|
||||
let documentsProvider = LocalFileProvider()
|
||||
```
|
||||
|
||||
is equal to:
|
||||
// Equals with:
|
||||
let documentsProvider = LocalFileProvider(directory: .documentDirectory, domainMask: = .userDomainMask)
|
||||
|
||||
``` swift
|
||||
let documentPath = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true);
|
||||
let documentsURL = URL(fileURLWithPath: documentPath);
|
||||
// Equals with:
|
||||
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
let documentsProvider = LocalFileProvider(baseURL: documentsURL)
|
||||
```
|
||||
|
||||
Also for using group shared container:
|
||||
|
||||
```swift
|
||||
let documentsProvider = LocalFileProvider(sharedContainerId: "group.yourcompany.appContainer")
|
||||
// Replace your group identifier with string above
|
||||
```
|
||||
|
||||
You can't change the base url later. and all paths are related to this base url by default.
|
||||
|
||||
To initialize an iCloud Container provider use below code, This will automatically manager creating Documents folder in container:
|
||||
|
||||
```swift
|
||||
let documentsProvider = CloudFileProvider(containerId: nil)
|
||||
```
|
||||
|
||||
|
||||
For remote file providers authentication may be necessary:
|
||||
|
||||
``` swift
|
||||
@@ -118,7 +141,7 @@ let webdavProvider = WebDAVFileProvider(baseURL: URL(string: "http://www.example
|
||||
|
||||
* In case you want to connect non-secure servers for WebDAV (http) in iOS 9+ / macOS 10.11+ you should disable App Transport Security (ATS) according to [this guide.](https://gist.github.com/mlynch/284699d676fe9ed0abfa)
|
||||
|
||||
* For Dropbox & OneDrive, user is clientID and password is Token which both must be retrieved via [OAuth2 API of Dropbox](https://www.dropbox.com/developers/reference/oauth-guide). There are libraries like [p2/OAuth2](https://github.com/p2/OAuth2) or [OAuthSwift](https://github.com/OAuthSwift/OAuthSwift) which can facilate the procedure to retrieve token. The latter is easier to use and prefered.
|
||||
* For Dropbox & OneDrive, user is clientID and password is Token which both must be retrieved via [OAuth2 API of Dropbox](https://www.dropbox.com/developers/reference/oauth-guide). There are libraries like [p2/OAuth2](https://github.com/p2/OAuth2) or [OAuthSwift](https://github.com/OAuthSwift/OAuthSwift) which can facilate the procedure to retrieve token. The latter is easier to use and prefered. Also you can use [auth0/Lock](https://github.com/auth0/Lock.iOS-OSX) which provides graphical user interface.
|
||||
|
||||
For interaction with UI, set delegate variable of `FileProvider` object
|
||||
|
||||
@@ -385,11 +408,20 @@ Distributed under the MIT license. See `LICENSE` for more information.
|
||||
|
||||
[https://github.com/amosavian/](https://github.com/amosavian/)
|
||||
|
||||
[swift-image]:https://img.shields.io/badge/swift-3.0-orange.svg
|
||||
[cocoapods]: https://cocoapods.org/pods/FileProvider
|
||||
[swift-image]: https://img.shields.io/badge/swift-3.0-orange.svg
|
||||
[swift-url]: https://swift.org/
|
||||
[license-image]: https://img.shields.io/badge/License-MIT-blue.svg
|
||||
[platform-image]: https://img.shields.io/cocoapods/p/FileProvider.svg
|
||||
[license-image]: https://img.shields.io/github/license/amosavian/FileProvider.svg
|
||||
[license-url]: LICENSE
|
||||
[codebeat-image]: https://codebeat.co/badges/7b359f48-78eb-4647-ab22-56262a827517
|
||||
[codebeat-url]: https://codebeat.co/projects/github-com-amosavian-fileprovider
|
||||
[travis-image]: https://img.shields.io/travis/amosavian/FileProvider/master.svg
|
||||
[travis-url]: https://travis-ci.org/amosavian/FileProvider
|
||||
[travis-url]: https://travis-ci.org/amosavian/FileProvider
|
||||
[release-url]: https://github.com/amosavian/FileProvider/releases
|
||||
[release-image]: https://img.shields.io/github/release/amosavian/FileProvider.svg
|
||||
[carthage-image]: https://img.shields.io/badge/Carthage-compatible-4BC51D.svg
|
||||
[cocoapods-downloads]: https://img.shields.io/cocoapods/dt/FileProvider.svg
|
||||
[cocoapods-apps]: https://img.shields.io/cocoapods/at/FileProvider.svg
|
||||
[docs-image]: https://img.shields.io/cocoapods/metrics/doc-percent/FileProvider.svg
|
||||
[docs-url]: http://cocoadocs.org/docsets/FileProvider/
|
||||
@@ -1,29 +0,0 @@
|
||||
//
|
||||
// AEXML.h
|
||||
//
|
||||
// Copyright (c) 2014 Marko Tadić <tadija@me.com> http://tadija.net
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
FOUNDATION_EXPORT double AEXMLVersionNumber;
|
||||
FOUNDATION_EXPORT const unsigned char AEXMLVersionString[];
|
||||
|
||||
Executable → Regular
+1
-1
@@ -29,7 +29,7 @@ import Foundation
|
||||
|
||||
XML Parsing is also done with this object.
|
||||
*/
|
||||
open class AEXMLDocument: AEXMLElement {
|
||||
internal class AEXMLDocument: AEXMLElement {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
|
||||
Executable → Regular
+2
-2
@@ -30,7 +30,7 @@ import Foundation
|
||||
You can access its structure by using subscript like this: `element["foo"]["bar"]` which would
|
||||
return `<bar></bar>` element from `<element><foo><bar></bar></foo></element>` XML as an `AEXMLElement` object.
|
||||
*/
|
||||
open class AEXMLElement {
|
||||
internal class AEXMLElement {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
@@ -86,7 +86,7 @@ open class AEXMLElement {
|
||||
/// The first element with given name **(Empty element with error if not exists)**.
|
||||
open subscript(key: String) -> AEXMLElement {
|
||||
guard let
|
||||
first = children.filter({ $0.name == key }).first
|
||||
first = children.first(where: { $0.name == key })
|
||||
else {
|
||||
let errorElement = AEXMLElement(name: key)
|
||||
errorElement.error = AEXMLError.elementNotFound
|
||||
|
||||
Executable → Regular
+1
-1
@@ -25,7 +25,7 @@
|
||||
import Foundation
|
||||
|
||||
/// A type representing error value that can be thrown or inside `error` property of `AEXMLElement`.
|
||||
public enum AEXMLError: Error {
|
||||
internal enum AEXMLError: Error {
|
||||
/// This will be inside `error` property of `AEXMLElement` when subscript is used for not-existing element.
|
||||
case elementNotFound
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>3.0.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
Executable → Regular
+1
-1
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
|
||||
/// Options used in `AEXMLDocument`
|
||||
public struct AEXMLOptions {
|
||||
internal struct AEXMLOptions {
|
||||
|
||||
/// Values used in XML Document header
|
||||
public struct DocumentHeader {
|
||||
|
||||
Executable → Regular
@@ -0,0 +1,549 @@
|
||||
//
|
||||
// CloudFileProvider.swift
|
||||
// FileProvider
|
||||
//
|
||||
// Created by Amir Abbas Mousavian.
|
||||
// Copyright © 2017 Mousavian. Distributed under MIT license.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
open class CloudFileProvider: LocalFileProvider {
|
||||
open override class var type: String { return "iCloudDrive" }
|
||||
|
||||
/// Forces file operations to use `NSFileCoordinating`,
|
||||
/// Actually this is readonly, and value is always true.
|
||||
override open var isCoorinating: Bool {
|
||||
get {
|
||||
return true
|
||||
}
|
||||
set {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/// The fully-qualified container identifier for an iCloud container directory.
|
||||
open fileprivate(set) var containerId: String?
|
||||
|
||||
/// Scope of container, indicates user can manipulate data/files or not.
|
||||
open fileprivate(set) var scope: UbiquitousScope
|
||||
|
||||
/**
|
||||
Initializes the provider for the iCloud container associated with the specified identifier and
|
||||
establishes access to that container.
|
||||
|
||||
- Important: Do not call this method from your app’s main thread. Because this method might take a nontrivial amount of time to set up iCloud and return the requested URL, you should always call it from a secondary thread.
|
||||
|
||||
- Parameter containerId: The fully-qualified container identifier for an iCloud container directory. The string you specify must not contain wildcards and must be of the form `<TEAMID>.<CONTAINER>`, where `<TEAMID>` is your development team ID and `<CONTAINER>` is the bundle identifier of the container you want to access.\
|
||||
The container identifiers for your app must be declared in the `com.apple.developer.ubiquity-container-identifiers` array of the `.entitlements` property list file in your Xcode project.\
|
||||
If you specify nil for this parameter, this method uses the first container listed in the `com.apple.developer.ubiquity-container-identifiers` entitlement array.
|
||||
- Parameter scope: Use `.documents` (default) to put documents that the user is allowed to access inside a Documents subdirectory. Otherwise use `.data` to store user-related data files that your app needs to share but that are not files you want the user to manipulate directly.
|
||||
*/
|
||||
public init? (containerId: String?, scope: UbiquitousScope = .documents) {
|
||||
assert(!Thread.isMainThread, "LocalFileProvider.init(containerId:) is not recommended to be executed on Main Thread.")
|
||||
guard FileManager.default.ubiquityIdentityToken == nil else {
|
||||
return nil
|
||||
}
|
||||
guard let ubiquityURL = FileManager.default.url(forUbiquityContainerIdentifier: containerId) else {
|
||||
return nil
|
||||
}
|
||||
self.containerId = containerId
|
||||
self.scope = scope
|
||||
let baseURL: URL
|
||||
if scope == .documents {
|
||||
baseURL = ubiquityURL.standardized.appendingPathComponent("Documents/")
|
||||
} else {
|
||||
baseURL = ubiquityURL.standardized
|
||||
}
|
||||
|
||||
super.init(baseURL: baseURL)
|
||||
self.isCoorinating = true
|
||||
|
||||
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: .concurrent)
|
||||
operation_queue = OperationQueue()
|
||||
operation_queue.name = "FileProvider.\(type(of: self).type).Operation"
|
||||
|
||||
fileManager.url(forUbiquityContainerIdentifier: containerId)
|
||||
opFileManager.url(forUbiquityContainerIdentifier: containerId)
|
||||
|
||||
try? fileManager.createDirectory(at: baseURL, withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
// FIXME: create runloop for dispatch_queue, start query on it
|
||||
open override func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
|
||||
dispatch_queue.async {
|
||||
let pathURL = self.url(of: path)
|
||||
|
||||
let query = NSMetadataQuery()
|
||||
query.predicate = NSPredicate(format: "%K BEGINSWITH %@", NSMetadataItemPathKey, pathURL.path)
|
||||
query.searchScopes = [self.scope.rawValue]
|
||||
var finishObserver: NSObjectProtocol?
|
||||
finishObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
|
||||
defer {
|
||||
query.stop()
|
||||
NotificationCenter.default.removeObserver(finishObserver!)
|
||||
}
|
||||
|
||||
guard let results = query.results as? [NSMetadataItem] else {
|
||||
return
|
||||
}
|
||||
|
||||
query.disableUpdates()
|
||||
|
||||
var contents = [FileObject]()
|
||||
for result in results {
|
||||
guard let attribs = result.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
|
||||
continue
|
||||
}
|
||||
|
||||
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardized, url.deletingLastPathComponent().path.trimmingCharacters(in: pathTrimSet) == pathURL.path.trimmingCharacters(in: pathTrimSet) else {
|
||||
continue
|
||||
}
|
||||
|
||||
if let file = self.mapFileObject(attributes: attribs) {
|
||||
contents.append(file)
|
||||
}
|
||||
}
|
||||
|
||||
query.stop()
|
||||
completionHandler(contents, nil)
|
||||
})
|
||||
DispatchQueue.main.async {
|
||||
if !query.start() {
|
||||
completionHandler([], self.throwError(path, code: CocoaError.fileReadNoPermission))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// - Important: iCloud Storage size and free space is unavailable, it returns local space
|
||||
open override func storageProperties(completionHandler: (@escaping (_ total: Int64, _ used: Int64) -> Void)) {
|
||||
super.storageProperties(completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
open override func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
|
||||
dispatch_queue.async {
|
||||
let pathURL = self.url(of: path)
|
||||
let query = NSMetadataQuery()
|
||||
query.predicate = NSPredicate(format: "%K LIKE %@", NSMetadataItemPathKey, pathURL.path)
|
||||
query.searchScopes = [self.scope.rawValue]
|
||||
var finishObserver: NSObjectProtocol?
|
||||
finishObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
|
||||
defer {
|
||||
query.stop()
|
||||
NotificationCenter.default.removeObserver(finishObserver!)
|
||||
}
|
||||
|
||||
query.disableUpdates()
|
||||
|
||||
guard let result = (query.results as? [NSMetadataItem])?.first, let attribs = result.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
|
||||
let error = self.throwError(path, code: CocoaError.fileNoSuchFile)
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
if let file = self.mapFileObject(attributes: attribs) {
|
||||
completionHandler(file, nil)
|
||||
} else {
|
||||
let noFileError = self.throwError(path, code: CocoaError.fileNoSuchFile)
|
||||
completionHandler(nil, noFileError)
|
||||
}
|
||||
})
|
||||
DispatchQueue.main.async {
|
||||
if !query.start() {
|
||||
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadNoPermission))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open override func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard let r = super.create(folder: folderName, at: atPath, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open override func create(file fileName: String, at atPath: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard let r = super.create(file: fileName, at: atPath, contents: data, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open override func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard let r = super.moveItem(path: path, to: toPath, overwrite: overwrite, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open override func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard let r = super.copyItem(path: path, to: toPath, overwrite: overwrite, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open override func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard let r = super.removeItem(path: path, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open override func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
// TODO: Make use of overwrite parameter
|
||||
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
|
||||
operation_queue.addOperation {
|
||||
let tempFolder: URL
|
||||
if #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) {
|
||||
tempFolder = FileManager.default.temporaryDirectory
|
||||
} else {
|
||||
tempFolder = URL(fileURLWithPath: NSTemporaryDirectory())
|
||||
}
|
||||
let tmpFile = tempFolder.appendingPathComponent(UUID().uuidString)
|
||||
|
||||
do {
|
||||
try self.opFileManager.copyItem(at: localFile, to: tmpFile)
|
||||
let toUrl = self.url(of: toPath)
|
||||
try self.opFileManager.setUbiquitous(true, itemAt: tmpFile, destinationURL: toUrl)
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
})
|
||||
} catch let e {
|
||||
if self.opFileManager.fileExists(atPath: tmpFile.path) {
|
||||
try? self.opFileManager.removeItem(at: tmpFile)
|
||||
}
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
})
|
||||
}
|
||||
}
|
||||
return CloudOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open override func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
|
||||
|
||||
do {
|
||||
try self.opFileManager.startDownloadingUbiquitousItem(at: self.url(of: path))
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let r = super.copyItem(path: path, toLocalURL: toLocalURL, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open override func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
guard let r = super.contents(path: path, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open override func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
guard let r = super.contents(path: path, offset: offset, length: length, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open override func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
guard let r = super.writeContents(path: path, contents: data, atomically: atomically, overwrite: overwrite, completionHandler: completionHandler) else { return nil }
|
||||
return CloudOperationHandle(operationType: r.operationType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
open override func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
|
||||
dispatch_queue.async {
|
||||
let pathURL = self.url(of: path)
|
||||
let query = NSMetadataQuery()
|
||||
query.predicate = NSPredicate(format: "(%K BEGINSWITH %@) && (%K LIKE %@)", NSMetadataItemPathKey, pathURL.path, NSMetadataItemFSNameKey, query)
|
||||
query.searchScopes = [self.scope.rawValue]
|
||||
|
||||
var lastReportedCount = 0
|
||||
|
||||
if let foundItemHandler = foundItemHandler {
|
||||
var updateObserver: NSObjectProtocol?
|
||||
|
||||
updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryGatheringProgress, object: query, queue: nil, using: { (notification) in
|
||||
|
||||
query.disableUpdates()
|
||||
|
||||
guard query.resultCount > lastReportedCount else { return }
|
||||
|
||||
for index in lastReportedCount..<query.resultCount {
|
||||
guard let attribs = (query.result(at: index) as? NSMetadataItem)?.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
|
||||
continue
|
||||
}
|
||||
|
||||
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardized, recursive || url.deletingLastPathComponent().path.trimmingCharacters(in: pathTrimSet) == pathURL.path.trimmingCharacters(in: pathTrimSet) else {
|
||||
continue
|
||||
}
|
||||
|
||||
if let file = self.mapFileObject(attributes: attribs) {
|
||||
foundItemHandler(file)
|
||||
}
|
||||
}
|
||||
lastReportedCount = query.resultCount
|
||||
|
||||
query.enableUpdates()
|
||||
})
|
||||
}
|
||||
|
||||
var finishObserver: NSObjectProtocol?
|
||||
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
|
||||
defer {
|
||||
query.stop()
|
||||
NotificationCenter.default.removeObserver(finishObserver!)
|
||||
}
|
||||
|
||||
guard let results = query.results as? [NSMetadataItem] else {
|
||||
return
|
||||
}
|
||||
|
||||
query.disableUpdates()
|
||||
|
||||
var contents = [FileObject]()
|
||||
for result in results {
|
||||
guard let attribs = result.values(forAttributes: [NSMetadataItemURLKey, NSMetadataItemFSNameKey, NSMetadataItemPathKey, NSMetadataItemFSSizeKey, NSMetadataItemContentTypeTreeKey, NSMetadataItemFSCreationDateKey, NSMetadataItemFSContentChangeDateKey]) else {
|
||||
continue
|
||||
}
|
||||
|
||||
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardized, recursive || url.deletingLastPathComponent().path.trimmingCharacters(in: pathTrimSet) == pathURL.path.trimmingCharacters(in: pathTrimSet) else {
|
||||
continue
|
||||
}
|
||||
|
||||
if let file = self.mapFileObject(attributes: attribs) {
|
||||
contents.append(file)
|
||||
}
|
||||
}
|
||||
|
||||
completionHandler(contents, nil)
|
||||
})
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if !query.start() {
|
||||
completionHandler([], self.throwError(path, code: CocoaError.fileReadNoPermission))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var monitors = [String: (NSMetadataQuery, NSObjectProtocol)]()
|
||||
|
||||
open override func registerNotifcation(path: String, eventHandler: @escaping (() -> Void)) {
|
||||
self.unregisterNotifcation(path: path)
|
||||
let pathURL = self.url(of: path)
|
||||
let query = NSMetadataQuery()
|
||||
query.predicate = NSPredicate(format: "(%K BEGINSWITH %@)", NSMetadataItemPathKey, pathURL.path)
|
||||
query.searchScopes = [self.scope.rawValue]
|
||||
|
||||
let updateObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidUpdate, object: query, queue: nil, using: { (notification) in
|
||||
|
||||
query.disableUpdates()
|
||||
|
||||
eventHandler()
|
||||
|
||||
query.enableUpdates()
|
||||
})
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if query.start() {
|
||||
self.monitors[path] = (query, updateObserver)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open override func unregisterNotifcation(path: String) {
|
||||
guard let (query, observer) = monitors[path] else {
|
||||
return
|
||||
}
|
||||
query.disableUpdates()
|
||||
query.stop()
|
||||
NotificationCenter.default.removeObserver(observer)
|
||||
monitors.removeValue(forKey: path)
|
||||
}
|
||||
|
||||
open override func isRegisteredForNotification(path: String) -> Bool {
|
||||
return monitors[path] != nil
|
||||
}
|
||||
|
||||
open override func copy(with zone: NSZone? = nil) -> Any {
|
||||
let copy = CloudFileProvider(containerId: self.containerId)
|
||||
copy?.currentPath = self.currentPath
|
||||
copy?.delegate = self.delegate
|
||||
copy?.fileOperationDelegate = self.fileOperationDelegate
|
||||
copy?.isPathRelative = self.isPathRelative
|
||||
return copy as Any
|
||||
}
|
||||
|
||||
fileprivate func mapFileObject(attributes attribs: [String: Any]) -> FileObject? {
|
||||
guard let url = (attribs[NSMetadataItemURLKey] as? URL)?.standardized, let name = attribs[NSMetadataItemFSNameKey] as? String else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let path = self.relativePathOf(url: url)
|
||||
let rpath = path.hasPrefix("/") ? path.substring(from: path.index(after: path.startIndex)) : path
|
||||
let relativeUrl = URL(string: rpath, relativeTo: self.baseURL)
|
||||
let file = FileObject(url: relativeUrl ?? url, name: name, path: path)
|
||||
|
||||
file.size = (attribs[NSMetadataItemFSSizeKey] as? NSNumber)?.int64Value ?? -1
|
||||
file.creationDate = attribs[NSMetadataItemFSCreationDateKey] as? Date
|
||||
file.modifiedDate = attribs[NSMetadataItemFSContentChangeDateKey] as? Date
|
||||
let isFolder = (attribs[NSMetadataItemContentTypeTreeKey] as? [String])?.contains("public.folder") ?? false
|
||||
let isSymbolic = (attribs[NSMetadataItemContentTypeTreeKey] as? [String])?.contains("public.symlink") ?? false
|
||||
file.type = isFolder ? .directory : (isSymbolic ? .symbolicLink : .regular)
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
/// Removes local copy of file, but spares cloud copy/
|
||||
/// - Parameter path: Path of file or directory to be remoed from local
|
||||
/// - Parameter completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
open func evictItem(path: String, completionHandler: SimpleCompletionHandler) {
|
||||
operation_queue.addOperation {
|
||||
do {
|
||||
try self.opFileManager.evictUbiquitousItem(at: self.url(of: path))
|
||||
completionHandler?(nil)
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a pulic url with expiration date, can be shared with other people.
|
||||
open func temporaryLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: FileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
|
||||
operation_queue.addOperation {
|
||||
do {
|
||||
var expiration: NSDate?
|
||||
let url = try self.opFileManager.url(forPublishingUbiquitousItemAt: self.url(of: path), expiration: &expiration)
|
||||
completionHandler(url, nil, expiration as Date?, nil)
|
||||
} catch let e {
|
||||
completionHandler(nil, nil, nil, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum UbiquitousScope: RawRepresentable {
|
||||
/// Search all files not in the Documents directories of the app’s iCloud container directories.
|
||||
/// Use this scope to store user-related data files that your app needs to share
|
||||
/// but that are not files you want the user to manipulate directly.
|
||||
case data
|
||||
/// Search all files in the Documents directories of the app’s iCloud container directories.
|
||||
/// Put documents that the user is allowed to access inside a Documents subdirectory.
|
||||
case documents
|
||||
|
||||
public typealias RawValue = String
|
||||
|
||||
public init? (rawValue: String) {
|
||||
switch rawValue {
|
||||
case NSMetadataQueryUbiquitousDataScope:
|
||||
self = .data
|
||||
case NSMetadataQueryUbiquitousDocumentsScope:
|
||||
self = .documents
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public var rawValue: String {
|
||||
switch self {
|
||||
case .data:
|
||||
return NSMetadataQueryUbiquitousDataScope
|
||||
case .documents:
|
||||
return NSMetadataQueryUbiquitousDocumentsScope
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class CloudOperationHandle: OperationHandle {
|
||||
public let baseURL: URL?
|
||||
public let operationType: FileOperationType
|
||||
|
||||
init (operationType: FileOperationType, baseURL: URL?) {
|
||||
self.baseURL = baseURL
|
||||
self.operationType = operationType
|
||||
}
|
||||
|
||||
private var sourceURL: URL? {
|
||||
guard let source = operationType.source, let baseURL = baseURL else { return nil }
|
||||
return source.hasPrefix("file://") ? URL(fileURLWithPath: source) : baseURL.appendingPathComponent(source)
|
||||
}
|
||||
|
||||
private var destURL: URL? {
|
||||
guard let dest = operationType.destination, let baseURL = baseURL else { return nil }
|
||||
return dest.hasPrefix("file://") ? URL(fileURLWithPath: dest) : baseURL.appendingPathComponent(dest)
|
||||
}
|
||||
|
||||
open var bytesSoFar: Int64 {
|
||||
assert(!Thread.isMainThread, "Don't run \(#function) method on main thread")
|
||||
|
||||
guard let url = destURL ?? sourceURL, let item = CloudOperationHandle.getMetadataItem(url: url) else { return 0 }
|
||||
let downloaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentDownloadedKey) as? Double ?? 0
|
||||
let uploaded = item.value(forAttribute: NSMetadataUbiquitousItemPercentUploadedKey) as? Double ?? 0
|
||||
guard let size = item.value(forAttribute: NSMetadataItemFSSizeKey) as? Int64 else { return -1 }
|
||||
if (downloaded == 0 || downloaded == 100) && (uploaded > 0 && uploaded < 100) {
|
||||
return Int64(uploaded * (Double(size) / 100))
|
||||
} else if (uploaded == 0 || uploaded == 100) && (downloaded > 0 && downloaded < 100) {
|
||||
return Int64(downloaded * (Double(size) / 100))
|
||||
} else if uploaded == 100 || downloaded == 100 {
|
||||
return size
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
open var totalBytes: Int64 {
|
||||
assert(!Thread.isMainThread, "Don't run \(#function) method on main thread")
|
||||
guard let url = destURL ?? sourceURL, let item = CloudOperationHandle.getMetadataItem(url: url) else { return -1 }
|
||||
return item.value(forAttribute: NSMetadataItemFSSizeKey) as? Int64 ?? -1
|
||||
}
|
||||
|
||||
open var inProgress: Bool {
|
||||
guard let url = destURL ?? sourceURL, let item = CloudOperationHandle.getMetadataItem(url: url) else { return false }
|
||||
let downloadStatus = item.value(forAttribute: NSMetadataUbiquitousItemDownloadingStatusKey) as? String ?? NSMetadataUbiquitousItemDownloadingStatusNotDownloaded
|
||||
let isUploading = item.value(forAttribute: NSMetadataUbiquitousItemIsUploadingKey) as? Bool ?? false
|
||||
return downloadStatus == NSMetadataUbiquitousItemDownloadingStatusCurrent || isUploading
|
||||
}
|
||||
|
||||
/// Not usable in local provider
|
||||
open func cancel() -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
fileprivate static func getMetadataItem(url: URL) -> NSMetadataItem? {
|
||||
let query = NSMetadataQuery()
|
||||
query.predicate = NSPredicate(format: "(%K LIKE %@)", NSMetadataItemPathKey, url.path)
|
||||
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope, NSMetadataQueryUbiquitousDataScope]
|
||||
|
||||
var item: NSMetadataItem?
|
||||
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
var finishObserver: NSObjectProtocol?
|
||||
finishObserver = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: nil, using: { (notification) in
|
||||
defer {
|
||||
query.stop()
|
||||
group.leave()
|
||||
NotificationCenter.default.removeObserver(finishObserver!)
|
||||
}
|
||||
|
||||
if query.resultCount > 0 {
|
||||
item = query.result(at: 0) as? NSMetadataItem
|
||||
}
|
||||
|
||||
query.disableUpdates()
|
||||
|
||||
})
|
||||
|
||||
DispatchQueue.main.async {
|
||||
query.start()
|
||||
}
|
||||
_ = group.wait(timeout: DispatchTime.now() + 30)
|
||||
return item
|
||||
}
|
||||
}
|
||||
@@ -10,54 +10,77 @@
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
|
||||
// Because this class uses NSURLSession, it's necessary to disable App Transport Security
|
||||
// in case of using this class with unencrypted HTTP connection.
|
||||
|
||||
open class DropboxFileProvider: NSObject, FileProviderBasicRemote {
|
||||
open static let type: String = "DropBox"
|
||||
open let isPathRelative: Bool = true
|
||||
open class DropboxFileProvider: FileProviderBasicRemote {
|
||||
open class var type: String { return "DropBox" }
|
||||
open let isPathRelative: Bool
|
||||
open let baseURL: URL?
|
||||
open var currentPath: String = ""
|
||||
open var currentPath: String
|
||||
|
||||
open let apiURL = URL(string: "https://api.dropboxapi.com/2/")!
|
||||
open let contentURL = URL(string: "https://content.dropboxapi.com/2")!
|
||||
/// Dropbox RPC API URL, which is equal with [https://api.dropboxapi.com/2/](https://api.dropboxapi.com/2/)
|
||||
open let apiURL: URL
|
||||
/// Dropbox contents download/upload API URL, which is equal with [https://content.dropboxapi.com/2/](https://content.dropboxapi.com/2/)
|
||||
open let contentURL: URL
|
||||
|
||||
open var dispatch_queue: DispatchQueue {
|
||||
open var dispatch_queue: DispatchQueue
|
||||
open var operation_queue: OperationQueue {
|
||||
willSet {
|
||||
assert(_session == nil, "It's not effective to change dispatch_queue property after session is initialized.")
|
||||
}
|
||||
}
|
||||
|
||||
open weak var delegate: FileProviderDelegate?
|
||||
open let credential: URLCredential?
|
||||
open private(set) var cache: URLCache?
|
||||
public var useCache: Bool = false
|
||||
public var validatingCache: Bool = true
|
||||
public var useCache: Bool
|
||||
public var validatingCache: Bool
|
||||
|
||||
fileprivate var _session: URLSession?
|
||||
fileprivate var sessionDelegate: SessionDelegate?
|
||||
public var session: URLSession {
|
||||
if _session == nil {
|
||||
self.sessionDelegate = SessionDelegate(fileProvider: self, credential: credential)
|
||||
let queue = OperationQueue()
|
||||
//queue.underlyingQueue = dispatch_queue
|
||||
let config = URLSessionConfiguration.default
|
||||
config.urlCache = cache
|
||||
config.requestCachePolicy = .returnCacheDataElseLoad
|
||||
_session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDelegate?, delegateQueue: queue)
|
||||
_session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDelegate?, delegateQueue: self.operation_queue)
|
||||
}
|
||||
return _session!
|
||||
}
|
||||
|
||||
public init? (credential: URLCredential?, cache: URLCache? = nil) {
|
||||
/**
|
||||
Initializer for Dropbox provider with given client ID and Token.
|
||||
These parameters must be retrieved via [OAuth2 API of Dropbox](https://www.dropbox.com/developers/reference/oauth-guide).
|
||||
|
||||
There are libraries like [p2/OAuth2](https://github.com/p2/OAuth2) or [OAuthSwift](https://github.com/OAuthSwift/OAuthSwift) which can facilate the procedure to retrieve token.
|
||||
The latter is easier to use and prefered. Also you can use [auth0/Lock](https://github.com/auth0/Lock.iOS-OSX) which provides graphical user interface.
|
||||
|
||||
- Parameter credential: a `URLCredential` object with Client ID set as `user` and Token set as `password`.
|
||||
- Parameter cache: A URLCache to cache downloaded files and contents. If set to nil, URLCache.shared object will be used.
|
||||
*/
|
||||
public init(credential: URLCredential?, cache: URLCache? = nil) {
|
||||
self.baseURL = nil
|
||||
dispatch_queue = DispatchQueue(label: "FileProvider.\(DropboxFileProvider.type)", attributes: DispatchQueue.Attributes.concurrent)
|
||||
//let url = baseURL.uw_absoluteString
|
||||
self.credential = credential
|
||||
self.isPathRelative = true
|
||||
self.currentPath = ""
|
||||
self.useCache = false
|
||||
self.validatingCache = true
|
||||
self.cache = cache
|
||||
self.credential = credential
|
||||
|
||||
self.apiURL = URL(string: "https://api.dropboxapi.com/2/")!
|
||||
self.contentURL = URL(string: "https://content.dropboxapi.com/2/")!
|
||||
|
||||
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: .concurrent)
|
||||
operation_queue = OperationQueue()
|
||||
operation_queue.name = "FileProvider.\(type(of: self).type).Operation"
|
||||
|
||||
}
|
||||
|
||||
deinit {
|
||||
_session?.invalidateAndCancel()
|
||||
if fileProviderCancelTasksOnInvalidating {
|
||||
_session?.invalidateAndCancel()
|
||||
} else {
|
||||
_session?.finishTasksAndInvalidate()
|
||||
}
|
||||
}
|
||||
|
||||
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
|
||||
@@ -72,19 +95,19 @@ open class DropboxFileProvider: NSObject, FileProviderBasicRemote {
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
let requestDictionary = ["path": correctPath(path)! as NSString]
|
||||
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString]
|
||||
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var dbError: FileProviderDropboxError?
|
||||
var serverError: FileProviderDropboxError?
|
||||
var fileObject: DropboxFileObject?
|
||||
if let response = response as? HTTPURLResponse {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
|
||||
dbError = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
|
||||
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr), let file = self.mapToFileObject(json) {
|
||||
serverError = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
|
||||
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr), let file = DropboxFileObject(json: json) {
|
||||
fileObject = file
|
||||
}
|
||||
}
|
||||
completionHandler(fileObject, dbError ?? error)
|
||||
completionHandler(fileObject, serverError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
@@ -110,8 +133,6 @@ open class DropboxFileProvider: NSObject, FileProviderBasicRemote {
|
||||
}
|
||||
|
||||
extension DropboxFileProvider: FileProviderOperations {
|
||||
|
||||
|
||||
public func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let path = (atPath as NSString).appendingPathComponent(folderName) + "/"
|
||||
return doOperation(.create(path: path), completionHandler: completionHandler)
|
||||
@@ -166,12 +187,12 @@ extension DropboxFileProvider: FileProviderOperations {
|
||||
}
|
||||
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var dbError: FileProviderDropboxError?
|
||||
var serverError: FileProviderDropboxError?
|
||||
if let response = response as? HTTPURLResponse, response.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) {
|
||||
dbError = FileProviderDropboxError(code: code, path: sourcePath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
serverError = FileProviderDropboxError(code: code, path: sourcePath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
}
|
||||
completionHandler?(dbError ?? error)
|
||||
self.delegateNotify(operation, error: dbError ?? error)
|
||||
completionHandler?(serverError ?? error)
|
||||
self.delegateNotify(operation, error: serverError ?? error)
|
||||
})
|
||||
task.taskDescription = operation.json
|
||||
task.resume()
|
||||
@@ -195,15 +216,15 @@ extension DropboxFileProvider: FileProviderOperations {
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
let requestDictionary = ["path": path]
|
||||
let requestJson = dictionaryToJSON(requestDictionary as [String : AnyObject]) ?? ""
|
||||
let requestDictionary: [String: AnyObject] = ["path": path as NSString]
|
||||
let requestJson = dictionaryToJSON(requestDictionary) ?? ""
|
||||
request.setValue(requestJson, forHTTPHeaderField: "Dropbox-API-Arg")
|
||||
let task = session.downloadTask(with: request, completionHandler: { (cacheURL, response, error) in
|
||||
guard let cacheURL = cacheURL, let httpResponse = response as? HTTPURLResponse , httpResponse.statusCode < 300 else {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: (response as? HTTPURLResponse)?.statusCode ?? -1)
|
||||
let errorData : Data? = nil //Data(contentsOf:cacheURL) // TODO: Figure out how to get error response data for the error description
|
||||
let dbError : FileProviderDropboxError? = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: errorData ?? Data(), encoding: .utf8)) : nil
|
||||
completionHandler?(dbError ?? error)
|
||||
let serverError : FileProviderDropboxError? = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: errorData ?? Data(), encoding: .utf8)) : nil
|
||||
completionHandler?(serverError ?? error)
|
||||
return
|
||||
}
|
||||
do {
|
||||
@@ -221,25 +242,32 @@ extension DropboxFileProvider: FileProviderOperations {
|
||||
|
||||
extension DropboxFileProvider: FileProviderReadWrite {
|
||||
public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
if length == 0 || offset < 0 {
|
||||
dispatch_queue.async {
|
||||
completionHandler(Data(), nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
let opType = FileOperationType.fetch(path: path)
|
||||
let url = URL(string: "files/download", relativeTo: contentURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
if length > 0 {
|
||||
request.setValue("bytes=\(offset)-\(offset + length)", forHTTPHeaderField: "Range")
|
||||
request.setValue("bytes=\(offset)-\(offset + length - 1)", forHTTPHeaderField: "Range")
|
||||
} else if offset > 0 && length < 0 {
|
||||
request.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
|
||||
}
|
||||
let requestDictionary = ["path": correctPath(path)! as NSString]
|
||||
request.setValue(dictionaryToJSON(requestDictionary as [String : AnyObject]), forHTTPHeaderField: "Dropbox-API-Arg")
|
||||
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString]
|
||||
request.setValue(dictionaryToJSON(requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var dbError: FileProviderDropboxError?
|
||||
var serverError: FileProviderDropboxError?
|
||||
if let httpResponse = response as? HTTPURLResponse , httpResponse.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: httpResponse.statusCode) {
|
||||
dbError = FileProviderDropboxError(code: code, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
serverError = FileProviderDropboxError(code: code, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
}
|
||||
let filedata = dbError ?? error == nil ? data : nil
|
||||
completionHandler(filedata, dbError ?? error)
|
||||
let filedata = serverError ?? error == nil ? data : nil
|
||||
completionHandler(filedata, serverError ?? error)
|
||||
})
|
||||
task.taskDescription = opType.json
|
||||
task.resume()
|
||||
@@ -278,62 +306,135 @@ extension DropboxFileProvider: FileProviderReadWrite {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
// TODO: Implement /copy_reference, /get_account & /get_current_account
|
||||
// TODO: Implement /get_account & /get_current_account
|
||||
}
|
||||
|
||||
extension DropboxFileProvider {
|
||||
/// *DEPRECATED:* Use `publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?))` function instead.
|
||||
@available(*, deprecated, renamed: "publicLink(to:completionHandler:)", message: "Use publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?)) function instead.")
|
||||
open func temporaryLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: DropboxFileObject?, _ error: Error?) -> Void)) {
|
||||
self.publicLink(to: path) { (url, file, _, error) in
|
||||
completionHandler(url, file, error)
|
||||
}
|
||||
}
|
||||
|
||||
/// *DEPRECATED:* Use `publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?))` function instead.
|
||||
@available(*, deprecated, renamed: "publicLink(to:completionHandler:)", message: "Use publicLink(to:, completionHandler: (URL?, DropboxFileObject?, Date?, Error?)) function instead.")
|
||||
open func temporaryLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: DropboxFileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
|
||||
self.publicLink(to: path) { (url, file, expiration, error) in
|
||||
completionHandler(url, file, expiration, error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Genrates a public url to a file to be shared with other users and can be downloaded without authentication.
|
||||
|
||||
- Important: URL will be available for a limitied time (4 hours according to Dropbox documentation).
|
||||
|
||||
- Parameters:
|
||||
- to: path of file, including file/directory name.
|
||||
- completionHandler: a block with result of directory entries or error.
|
||||
`link`: a url returned by Dropbox to share.
|
||||
`attribute`: a `FileObject` containing the attributes of the item.
|
||||
`expiration`: a `Date` object, determines when the public url will expires.
|
||||
`error`: Error returned by Dropbox.
|
||||
*/
|
||||
open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: DropboxFileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
|
||||
let url = URL(string: "files/get_temporary_link", relativeTo: apiURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
let requestDictionary = ["path": correctPath(path)! as NSString]
|
||||
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString]
|
||||
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var dbError: FileProviderDropboxError?
|
||||
var serverError: FileProviderDropboxError?
|
||||
var link: URL?
|
||||
var fileObject: DropboxFileObject?
|
||||
if let response = response as? HTTPURLResponse {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
|
||||
dbError = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
|
||||
serverError = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
|
||||
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr) {
|
||||
if let linkStr = json["link"] as? String {
|
||||
link = URL(string: linkStr)
|
||||
}
|
||||
if let attribDic = json["metadata"] as? [String: AnyObject] {
|
||||
fileObject = self.mapToFileObject(attribDic)
|
||||
fileObject = DropboxFileObject(json: attribDic)
|
||||
}
|
||||
}
|
||||
}
|
||||
completionHandler(link, fileObject, dbError ?? error)
|
||||
|
||||
let expiration: Date? = link != nil ? Date(timeIntervalSinceNow: 4 * 60 * 60) : nil
|
||||
completionHandler(link, fileObject, expiration, serverError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
|
||||
open func copyItem(path: String, toRemoteURL destURL: URL, completionHandler: @escaping ((_ jobId: String?, _ attribute: DropboxFileObject?, _ error: Error?) -> Void)) {
|
||||
/**
|
||||
Downloads a file from remote url to designated path asynchronously.
|
||||
|
||||
- Parameters:
|
||||
- remoteURL: a valid remote url to file.
|
||||
- to: Destination path of file, including file/directory name.
|
||||
- completionHandler: a block with result of directory entries or error.
|
||||
`jobId`: Job ID returned by Dropbox to monitor the copy/download progress.
|
||||
`attribute`: A `FileObject` containing the attributes of the item.
|
||||
`error`: Error returned by Dropbox.
|
||||
*/
|
||||
open func copyItem(remoteURL: URL, to toPath: String, completionHandler: @escaping ((_ jobId: String?, _ attribute: DropboxFileObject?, _ error: Error?) -> Void)) {
|
||||
if remoteURL.isFileURL {
|
||||
completionHandler(nil, nil, self.throwError(remoteURL.path, code: URLError.badURL))
|
||||
return
|
||||
}
|
||||
let url = URL(string: "files/save_url", relativeTo: apiURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
let requestDictionary = ["path": correctPath(path)! as NSString, "url" : destURL.absoluteString as NSString]
|
||||
let requestDictionary: [String: AnyObject] = ["path": correctPath(toPath)! as NSString, "url" : remoteURL.absoluteString as NSString]
|
||||
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var dbError: FileProviderDropboxError?
|
||||
var serverError: FileProviderDropboxError?
|
||||
var jobId: String?
|
||||
var fileObject: DropboxFileObject?
|
||||
if let response = response as? HTTPURLResponse {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
|
||||
dbError = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
|
||||
serverError = code != nil ? FileProviderDropboxError(code: code!, path: toPath, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
|
||||
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr) {
|
||||
jobId = json["async_job_id"] as? String
|
||||
if let attribDic = json["metadata"] as? [String: AnyObject] {
|
||||
fileObject = self.mapToFileObject(attribDic)
|
||||
fileObject = DropboxFileObject(json: attribDic)
|
||||
}
|
||||
}
|
||||
}
|
||||
completionHandler(jobId, fileObject, dbError ?? error)
|
||||
completionHandler(jobId, fileObject, serverError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
|
||||
/**
|
||||
Copys a file from another user Dropbox storage to designated path asynchronously.
|
||||
|
||||
- Parameters:
|
||||
- reference: a valid reference string from another user via `copy_reference/get` REST method.
|
||||
- to: Destination path of file, including file/directory name.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
*/
|
||||
open func copyItem(reference: String, to toPath: String, completionHandler: SimpleCompletionHandler) {
|
||||
let url = URL(string: "files/copy_reference/save", relativeTo: apiURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
let requestDictionary: [String: AnyObject] = ["path": correctPath(toPath)! as NSString, "copy_reference" : reference as NSString]
|
||||
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderDropboxError?
|
||||
if let response = response as? HTTPURLResponse {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
|
||||
serverError = code != nil ? FileProviderDropboxError(code: code!, path: toPath, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
|
||||
}
|
||||
completionHandler?(serverError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
@@ -386,7 +487,7 @@ extension DropboxFileProvider: ExtendedFileProvider {
|
||||
}
|
||||
var request = URLRequest(url: url)
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
var requestDictionary = ["path": path as NSString]
|
||||
var requestDictionary: [String: AnyObject] = ["path": path as NSString]
|
||||
requestDictionary["format"] = "jpeg" as NSString
|
||||
if let dimension = dimension {
|
||||
requestDictionary["size"] = "w\(Int(dimension.width))h\(Int(dimension.height))" as NSString
|
||||
@@ -419,20 +520,20 @@ extension DropboxFileProvider: ExtendedFileProvider {
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
let requestDictionary = ["path": correctPath(path)! as NSString, "include_media_info": NSNumber(value: true)]
|
||||
let requestDictionary: [String: AnyObject] = ["path": correctPath(path)! as NSString, "include_media_info": NSNumber(value: true)]
|
||||
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var dbError: FileProviderDropboxError?
|
||||
var serverError: FileProviderDropboxError?
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
if let response = response as? HTTPURLResponse {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
|
||||
dbError = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
|
||||
serverError = code != nil ? FileProviderDropboxError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
|
||||
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr), let properties = json["media_info"] as? [String: Any] {
|
||||
(dic, keys) = self.mapMediaInfo(properties)
|
||||
}
|
||||
}
|
||||
completionHandler(dic, keys, dbError ?? error)
|
||||
completionHandler(dic, keys, serverError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
@@ -440,7 +541,7 @@ extension DropboxFileProvider: ExtendedFileProvider {
|
||||
|
||||
extension DropboxFileProvider: FileProvider {
|
||||
open func copy(with zone: NSZone? = nil) -> Any {
|
||||
let copy = DropboxFileProvider(credential: self.credential, cache: self.cache)!
|
||||
let copy = DropboxFileProvider(credential: self.credential, cache: self.cache)
|
||||
copy.currentPath = self.currentPath
|
||||
copy.delegate = self.delegate
|
||||
copy.fileOperationDelegate = self.fileOperationDelegate
|
||||
|
||||
+41
-30
@@ -20,7 +20,25 @@ public struct FileProviderDropboxError: Error, CustomStringConvertible {
|
||||
|
||||
public final class DropboxFileObject: FileObject {
|
||||
internal init(name: String, path: String) {
|
||||
super.init(absoluteURL: URL(string: path), name: name, path: path)
|
||||
super.init(url: URL(string: path) ?? URL(string: "/")!, name: name, path: path)
|
||||
}
|
||||
|
||||
internal convenience init? (jsonStr: String) {
|
||||
guard let json = jsonToDictionary(jsonStr) else { return nil }
|
||||
self.init(json: json)
|
||||
}
|
||||
|
||||
internal convenience init? (json: [String: AnyObject]) {
|
||||
guard let name = json["name"] as? String else { return nil }
|
||||
guard let path = json["path_display"] as? String else { return nil }
|
||||
self.init(name: name, path: path)
|
||||
self.size = (json["size"] as? NSNumber)?.int64Value ?? -1
|
||||
self.serverTime = resolve(dateString: json["server_modified"] as? String ?? "")
|
||||
self.modifiedDate = resolve(dateString: json["client_modified"] as? String ?? "")
|
||||
self.type = (json[".tag"] as? String) == "folder" ? .directory : .regular
|
||||
self.isReadOnly = (json["sharing_info"]?["read_only"] as? NSNumber)?.boolValue ?? false
|
||||
self.id = json["id"] as? String
|
||||
self.rev = json["rev"] as? String
|
||||
}
|
||||
|
||||
open internal(set) var serverTime: Date? {
|
||||
@@ -79,7 +97,7 @@ internal extension DropboxFileProvider {
|
||||
let json = jsonToDictionary(jsonStr)
|
||||
if let entries = json?["entries"] as? [AnyObject] , entries.count > 0 {
|
||||
for entry in entries {
|
||||
if let entry = entry as? [String: AnyObject], let file = self.mapToFileObject(entry) {
|
||||
if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry) {
|
||||
files.append(file)
|
||||
}
|
||||
}
|
||||
@@ -98,18 +116,23 @@ internal extension DropboxFileProvider {
|
||||
}
|
||||
|
||||
func upload_simple(_ targetPath: String, data: Data, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
assert(data.count < 150*1024*1024, "Maximum size of allowed size to upload is 150MB")
|
||||
var requestDictionary = [String: Any]()
|
||||
if data.count > 150 * 1024 * 1024 {
|
||||
let error = FileProviderDropboxError(code: .payloadTooLarge, path: targetPath, errorDescription: nil)
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(.create(path: targetPath), error: error)
|
||||
return nil
|
||||
}
|
||||
var requestDictionary = [String: AnyObject]()
|
||||
let url: URL
|
||||
url = URL(string: "files/upload", relativeTo: contentURL)!
|
||||
requestDictionary["path"] = correctPath(targetPath) as NSString?
|
||||
requestDictionary["mode"] = (overwrite ? "overwrite" : "add") as NSString
|
||||
requestDictionary["client_modified"] = string(from:modifiedDate)
|
||||
requestDictionary["client_modified"] = rfc3339utc(of: modifiedDate) as NSString
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue(dictionaryToJSON(requestDictionary as [String : AnyObject]), forHTTPHeaderField: "Dropbox-API-Arg")
|
||||
request.setValue(dictionaryToJSON(requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
|
||||
request.httpBody = data
|
||||
let task = session.uploadTask(with: request, from: data, completionHandler: { (data, response, error) in
|
||||
var responseError: FileProviderDropboxError?
|
||||
@@ -125,17 +148,24 @@ internal extension DropboxFileProvider {
|
||||
}
|
||||
|
||||
func upload_simple(_ targetPath: String, localFile: URL, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
var requestDictionary = [String: Any]()
|
||||
let size = (try? localFile.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? -1
|
||||
if size > 150 * 1024 * 1024 {
|
||||
let error = FileProviderDropboxError(code: .payloadTooLarge, path: targetPath, errorDescription: nil)
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(.create(path: targetPath), error: error)
|
||||
return nil
|
||||
}
|
||||
var requestDictionary = [String: AnyObject]()
|
||||
let url: URL
|
||||
url = URL(string: "files/upload", relativeTo: contentURL)!
|
||||
requestDictionary["path"] = correctPath(targetPath) as NSString?
|
||||
requestDictionary["mode"] = (overwrite ? "overwrite" : "add") as NSString
|
||||
requestDictionary["client_modified"] = string(from:modifiedDate)
|
||||
requestDictionary["client_modified"] = rfc3339utc(of: modifiedDate) as NSString
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue(dictionaryToJSON(requestDictionary as [String : AnyObject]), forHTTPHeaderField: "Dropbox-API-Arg")
|
||||
request.setValue(dictionaryToJSON(requestDictionary), forHTTPHeaderField: "Dropbox-API-Arg")
|
||||
let task = session.uploadTask(with: request, fromFile: localFile, completionHandler: { (data, response, error) in
|
||||
var responseError: FileProviderDropboxError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
@@ -169,7 +199,7 @@ internal extension DropboxFileProvider {
|
||||
let json = jsonToDictionary(jsonStr)
|
||||
if let entries = json?["matches"] as? [AnyObject] , entries.count > 0 {
|
||||
for entry in entries {
|
||||
if let entry = entry as? [String: AnyObject], let file = self.mapToFileObject(entry) {
|
||||
if let entry = entry as? [String: AnyObject], let file = DropboxFileObject(json: entry) {
|
||||
foundItem(file)
|
||||
}
|
||||
}
|
||||
@@ -191,25 +221,6 @@ internal extension DropboxFileProvider {
|
||||
// codebeat:enable[ARITY]
|
||||
|
||||
internal extension DropboxFileProvider {
|
||||
func mapToFileObject(_ jsonStr: String) -> DropboxFileObject? {
|
||||
guard let json = jsonToDictionary(jsonStr) else { return nil }
|
||||
return self.mapToFileObject(json)
|
||||
}
|
||||
|
||||
func mapToFileObject(_ json: [String: AnyObject]) -> DropboxFileObject? {
|
||||
guard let name = json["name"] as? String else { return nil }
|
||||
guard let path = json["path_display"] as? String else { return nil }
|
||||
let fileObject = DropboxFileObject(name: name, path: path)
|
||||
fileObject.size = (json["size"] as? NSNumber)?.int64Value ?? -1
|
||||
fileObject.serverTime = resolve(dateString: json["server_modified"] as? String ?? "")
|
||||
fileObject.modifiedDate = resolve(dateString: json["client_modified"] as? String ?? "")
|
||||
fileObject.type = (json[".tag"] as? String) == "folder" ? .directory : .regular
|
||||
fileObject.isReadOnly = (json["sharing_info"]?["read_only"] as? NSNumber)?.boolValue ?? false
|
||||
fileObject.id = json["id"] as? String
|
||||
fileObject.rev = json["rev"] as? String
|
||||
return fileObject
|
||||
}
|
||||
|
||||
static let dateFormatter = DateFormatter()
|
||||
static let decimalFormatter = NumberFormatter()
|
||||
|
||||
@@ -229,7 +240,7 @@ internal extension DropboxFileProvider {
|
||||
let longStr = DropboxFileProvider.decimalFormatter.string(from: NSNumber(value: longitude))
|
||||
dic["Location"] = "\(latStr), \(longStr)"
|
||||
}
|
||||
if let timeTakenStr = json["time_taken"] as? String, let timeTaken = self.resolve(dateString: timeTakenStr) {
|
||||
if let timeTakenStr = json["time_taken"] as? String, let timeTaken = resolve(dateString: timeTakenStr) {
|
||||
keys.append("Date taken")
|
||||
DropboxFileProvider.dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
dic["Date taken"] = DropboxFileProvider.dateFormatter.string(from: timeTaken)
|
||||
|
||||
@@ -61,10 +61,11 @@ extension LocalFileProvider: ExtendedFileProvider {
|
||||
}
|
||||
|
||||
public func thumbnailOfFile(path: String, dimension: CGSize? = nil, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void)) {
|
||||
let dimension = dimension ?? CGSize(width: 64, height: 64)
|
||||
(dispatch_queue).async {
|
||||
var thumbnailImage: ImageClass? = nil
|
||||
// Check cache
|
||||
let fileURL = self.absoluteURL(path)
|
||||
let fileURL = self.url(of: path)
|
||||
// Create Thumbnail and cache
|
||||
switch fileURL.pathExtension.lowercased() {
|
||||
case LocalFileInformationGenerator.videoThumbnailExtensions:
|
||||
@@ -85,7 +86,7 @@ extension LocalFileProvider: ExtendedFileProvider {
|
||||
}
|
||||
|
||||
if let image = thumbnailImage {
|
||||
let scaledImage = dimension != nil ? LocalFileProvider.scaleDown(image: image, toSize: dimension!) : image
|
||||
let scaledImage = LocalFileProvider.scaleDown(image: image, toSize: dimension)
|
||||
completionHandler(scaledImage, nil)
|
||||
}
|
||||
}
|
||||
@@ -117,7 +118,7 @@ extension LocalFileProvider: ExtendedFileProvider {
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
if let getterMethod = getter {
|
||||
(dic, keys) = getterMethod(self.absoluteURL(path))
|
||||
(dic, keys) = getterMethod(self.url(of: path))
|
||||
}
|
||||
|
||||
completionHandler(dic, keys, nil)
|
||||
@@ -127,20 +128,20 @@ extension LocalFileProvider: ExtendedFileProvider {
|
||||
}
|
||||
|
||||
public struct LocalFileInformationGenerator {
|
||||
static public var imageThumbnailExtensions: [String] = ["jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff", "ico"]
|
||||
static public var audioThumbnailExtensions: [String] = ["mp3", "aac", "m4a"]
|
||||
static public var videoThumbnailExtensions: [String] = ["mov", "mp4", "m4v", "mpg", "mpeg"]
|
||||
static public var pdfThumbnailExtensions: [String] = ["pdf"]
|
||||
static public var imageThumbnailExtensions: [String] = ["jpg", "jpeg", "gif", "bmp", "png", "tif", "tiff", "ico"]
|
||||
static public var audioThumbnailExtensions: [String] = ["mp3", "aac", "m4a"]
|
||||
static public var videoThumbnailExtensions: [String] = ["mov", "mp4", "m4v", "mpg", "mpeg"]
|
||||
static public var pdfThumbnailExtensions: [String] = ["pdf"]
|
||||
static public var officeThumbnailExtensions: [String] = []
|
||||
static public var customThumbnailExtensions: [String] = []
|
||||
|
||||
static public var imagePropertiesExtensions: [String] = ["jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff"]
|
||||
static public var audioPropertiesExtensions: [String] = ["mp3", "aac", "m4a", "caf"]
|
||||
static public var videoPropertiesExtensions: [String] = ["mp4", "mpg", "3gp", "mov", "avi"]
|
||||
static public var pdfPropertiesExtensions: [String] = ["pdf"]
|
||||
static public var imagePropertiesExtensions: [String] = ["jpg", "jpeg", "bmp", "gif", "png", "tif", "tiff"]
|
||||
static public var audioPropertiesExtensions: [String] = ["mp3", "aac", "m4a", "caf"]
|
||||
static public var videoPropertiesExtensions: [String] = ["mp4", "mpg", "3gp", "mov", "avi"]
|
||||
static public var pdfPropertiesExtensions: [String] = ["pdf"]
|
||||
static public var archivePropertiesExtensions: [String] = []
|
||||
static public var officePropertiesExtensions: [String] = []
|
||||
static public var customPropertiesExtensions: [String] = []
|
||||
static public var officePropertiesExtensions: [String] = []
|
||||
static public var customPropertiesExtensions: [String] = []
|
||||
|
||||
static public var imageThumbnail: (_ fileURL: URL) -> ImageClass? = { fileURL in
|
||||
return ImageClass(contentsOfFile: fileURL.path)
|
||||
@@ -188,6 +189,16 @@ public struct LocalFileInformationGenerator {
|
||||
}
|
||||
|
||||
static public var imageProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
|
||||
func add(key: String, value: Any?) {
|
||||
if let value = value {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
func simplify(_ top:Int64, _ bottom:Int64) -> (newTop:Int, newBottom:Int) {
|
||||
var x = top
|
||||
var y = bottom
|
||||
@@ -202,79 +213,57 @@ public struct LocalFileInformationGenerator {
|
||||
return(Int(newTopVal), Int(newBottomVal))
|
||||
}
|
||||
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
guard let cgDataRef = CGImageSourceCreateWithURL(fileURL as CFURL, nil), let cfImageDict = CGImageSourceCopyPropertiesAtIndex(cgDataRef, 0, nil) else {
|
||||
return (dic, keys)
|
||||
}
|
||||
let imageDict = cfImageDict as NSDictionary
|
||||
let tiffDict = imageDict[kCGImagePropertyTIFFDictionary as String] as? [String : AnyObject] ?? [:]
|
||||
let exifDict = imageDict[kCGImagePropertyExifDictionary as String] as? [String : AnyObject] ?? [:]
|
||||
if let pixelWidth: AnyObject = imageDict.object(forKey: kCGImagePropertyPixelWidth) as? NSNumber, let pixelHeight: AnyObject = imageDict.object(forKey: kCGImagePropertyPixelHeight) as? NSNumber {
|
||||
keys.append("Dimensions")
|
||||
dic["Dimensions"] = "\(pixelWidth)x\(pixelHeight)"
|
||||
}
|
||||
if let dpi = imageDict[kCGImagePropertyDPIWidth as String] {
|
||||
keys.append("DPI")
|
||||
dic["DPI"] = dpi
|
||||
}
|
||||
if let devicemake = tiffDict[kCGImagePropertyTIFFMake as String] {
|
||||
keys.append("Device make")
|
||||
dic["Device make"] = devicemake
|
||||
}
|
||||
if let devicemodel = tiffDict[kCGImagePropertyTIFFModel as String] {
|
||||
keys.append("Device model")
|
||||
dic["Device model"] = devicemodel
|
||||
}
|
||||
if let lensmodel = exifDict[kCGImagePropertyExifLensModel as String] {
|
||||
keys.append("Lens model")
|
||||
dic["Lens model"] = lensmodel
|
||||
}
|
||||
if let artist = tiffDict[kCGImagePropertyTIFFArtist as String] as? String , !artist.isEmpty {
|
||||
keys.append("Artist")
|
||||
dic["Artist"] = artist
|
||||
let tiffDict = imageDict[kCGImagePropertyTIFFDictionary as String] as? NSDictionary ?? [:]
|
||||
let exifDict = imageDict[kCGImagePropertyExifDictionary as String] as? NSDictionary ?? [:]
|
||||
if let pixelWidth = imageDict.object(forKey: kCGImagePropertyPixelWidth) as? NSNumber, let pixelHeight = imageDict.object(forKey: kCGImagePropertyPixelHeight) as? NSNumber {
|
||||
add(key: "Dimensions", value: "\(pixelWidth)x\(pixelHeight)")
|
||||
}
|
||||
|
||||
add(key: "DPI", value: imageDict[kCGImagePropertyDPIWidth as String])
|
||||
add(key: "Device make", value: tiffDict[kCGImagePropertyTIFFMake as String])
|
||||
add(key: "Device model", value: tiffDict[kCGImagePropertyTIFFModel as String])
|
||||
add(key: "Lens model", value: exifDict[kCGImagePropertyExifLensModel as String])
|
||||
add(key: "Artist", value: tiffDict[kCGImagePropertyTIFFArtist as String] as? String)
|
||||
if let cr = tiffDict[kCGImagePropertyTIFFCopyright as String] as? String , !cr.isEmpty {
|
||||
keys.append("Copyright")
|
||||
dic["Copyright"] = cr
|
||||
add(key: "Copyright", value: cr)
|
||||
|
||||
}
|
||||
if let date = tiffDict[kCGImagePropertyTIFFDateTime as String] as? String , !date.isEmpty {
|
||||
keys.append("Date taken")
|
||||
dic["Date taken"] = date
|
||||
add(key: "Date taken", value: date)
|
||||
}
|
||||
if let latitude = tiffDict[kCGImagePropertyGPSLatitude as String]?.doubleValue, let longitude = tiffDict[kCGImagePropertyGPSLongitude as String]?.doubleValue {
|
||||
keys.append("Location")
|
||||
dic["Location"] = "\(latitude), \(longitude)"
|
||||
if let latitude = tiffDict[kCGImagePropertyGPSLatitude as String] as? NSNumber, let longitude = tiffDict[kCGImagePropertyGPSLongitude as String] as? NSNumber {
|
||||
add(key: "Location", value: "\(latitude), \(longitude)")
|
||||
}
|
||||
if let colorspace = imageDict[kCGImagePropertyColorModel as String] {
|
||||
keys.append("Color space")
|
||||
dic["Color space"] = colorspace
|
||||
}
|
||||
if let focallen = exifDict[kCGImagePropertyExifFocalLength as String] {
|
||||
keys.append("Focal length")
|
||||
dic["Focal length"] = focallen
|
||||
}
|
||||
if let fnum = exifDict[kCGImagePropertyExifFNumber as String] {
|
||||
keys.append("F number")
|
||||
dic["F number"] = fnum
|
||||
}
|
||||
if let expprog = exifDict[kCGImagePropertyExifExposureProgram as String] {
|
||||
keys.append("Exposure program")
|
||||
dic["Exposure program"] = expprog
|
||||
}
|
||||
if let exp = exifDict[kCGImagePropertyExifExposureTime as String]?.doubleValue {
|
||||
let expfrac = simplify(Int64(exp * 10_000_000_000_000), 10_000_000_000_000)
|
||||
keys.append("Exposure time")
|
||||
dic["Exposure time"] = "\(expfrac.newTop)/\(expfrac.newBottom)"
|
||||
add(key: "Color space", value: imageDict[kCGImagePropertyColorModel as String])
|
||||
add(key: "Focal length", value: exifDict[kCGImagePropertyExifFocalLength as String])
|
||||
add(key: "F number", value: exifDict[kCGImagePropertyExifFNumber as String])
|
||||
add(key: "Exposure program", value: exifDict[kCGImagePropertyExifExposureProgram as String])
|
||||
|
||||
if let exp = exifDict[kCGImagePropertyExifExposureTime as String] as? NSNumber {
|
||||
let expfrac = simplify(Int64(exp.doubleValue * 10_000_000_000_000), 10_000_000_000_000)
|
||||
add(key: "Exposure time", value: "\(expfrac.newTop)/\(expfrac.newBottom)")
|
||||
}
|
||||
if let iso = exifDict[kCGImagePropertyExifISOSpeedRatings as String] as? NSArray , iso.count > 0 {
|
||||
keys.append("ISO speed")
|
||||
dic["ISO speed"] = iso[0]
|
||||
add(key: "ISO speed", value: iso[0])
|
||||
}
|
||||
return (dic, keys)
|
||||
}
|
||||
|
||||
static var audioProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
|
||||
func add(key: String, value: Any?) {
|
||||
if let value = value {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
func makeDescription(_ key: String?) -> String? {
|
||||
guard let key = key else {
|
||||
return nil
|
||||
@@ -286,8 +275,6 @@ public struct LocalFileInformationGenerator {
|
||||
return newKey.capitalized
|
||||
}
|
||||
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
if FileManager.default.fileExists(atPath: fileURL.path) {
|
||||
let playerItem = AVPlayerItem(url: fileURL)
|
||||
let metadataList = playerItem.asset.commonMetadata
|
||||
@@ -300,12 +287,8 @@ public struct LocalFileInformationGenerator {
|
||||
}
|
||||
}
|
||||
if let ap = try? AVAudioPlayer(contentsOf: fileURL) {
|
||||
keys.append("Duration")
|
||||
dic["Duration"] = LocalFileProvider.formatshort(interval: ap.duration)
|
||||
if let bitRate = ap.settings[AVSampleRateKey] as? Int {
|
||||
keys.append("Bitrate")
|
||||
dic["Bitrate"] = bitRate
|
||||
}
|
||||
add(key: "Duration", value: LocalFileProvider.formatshort(interval: ap.duration))
|
||||
add(key: "Bitrate", value: ap.settings[AVSampleRateKey] as? Int)
|
||||
}
|
||||
}
|
||||
return (dic, keys)
|
||||
@@ -314,6 +297,14 @@ public struct LocalFileInformationGenerator {
|
||||
static public var videoProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
|
||||
func add(key: String, value: Any?) {
|
||||
if let value = value {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if let audioprops = LocalFileInformationGenerator.audioProperties?(fileURL) {
|
||||
dic = audioprops.prop
|
||||
keys = audioprops.keys
|
||||
@@ -328,17 +319,14 @@ public struct LocalFileInformationGenerator {
|
||||
var bitrate: Float = 0
|
||||
let width = Int(videoTracks[0].naturalSize.width)
|
||||
let height = Int(videoTracks[0].naturalSize.height)
|
||||
keys.append("Dimensions")
|
||||
dic["Dimensions"] = "\(width)x\(height)"
|
||||
add(key: "Dimensions", value: "\(width)x\(height)")
|
||||
var duration: Int64 = 0
|
||||
for track in videoTracks {
|
||||
duration += track.timeRange.duration.timescale > 0 ? track.timeRange.duration.value / Int64(track.timeRange.duration.timescale) : 0
|
||||
bitrate += track.estimatedDataRate
|
||||
}
|
||||
keys.append("Duration")
|
||||
dic["Duration"] = LocalFileProvider.formatshort(interval: TimeInterval(duration))
|
||||
keys.append("Video Bitrate")
|
||||
dic["Video Bitrate"] = "\(Int(ceil(bitrate / 1000))) kbps"
|
||||
add(key: "Duration", value: LocalFileProvider.formatshort(interval: TimeInterval(duration)))
|
||||
add(key: "Video Bitrate", value: "\(Int(ceil(bitrate / 1000))) kbps")
|
||||
}
|
||||
let audioTracks = asset.tracks(withMediaType: AVMediaTypeAudio)
|
||||
// dic["Audio channels"] = audioTracks.count
|
||||
@@ -346,12 +334,21 @@ public struct LocalFileInformationGenerator {
|
||||
for track in audioTracks {
|
||||
bitrate += track.estimatedDataRate
|
||||
}
|
||||
keys.append("Audio Bitrate")
|
||||
dic["Audio Bitrate"] = "\(Int(ceil(bitrate / 1000))) kbps"
|
||||
add(key: "Audio Bitrate", value: "\(Int(ceil(bitrate / 1000))) kbps")
|
||||
return (dic, keys)
|
||||
}
|
||||
|
||||
static public var pdfProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = { fileURL in
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
|
||||
func add(key: String, value: Any?) {
|
||||
if let value = value {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
func getKey(_ key: String, from dict: CGPDFDictionaryRef) -> String? {
|
||||
var cfValue: CGPDFStringRef? = nil
|
||||
if (CGPDFDictionaryGetString(dict, key, &cfValue)), let value = CGPDFStringCopyTextString(cfValue!) {
|
||||
@@ -377,56 +374,40 @@ public struct LocalFileInformationGenerator {
|
||||
return nil
|
||||
}
|
||||
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
if let data = try? Data(contentsOf: fileURL), let provider = CGDataProvider(data: data as CFData), let reference = CGPDFDocument(provider), let dict = reference.info {
|
||||
if let title = getKey("Title", from: dict), !title.isEmpty {
|
||||
keys.append("Title")
|
||||
dic["Title"] = title
|
||||
add(key: "Title", value: title)
|
||||
}
|
||||
if let author = getKey("Author", from: dict), !author.isEmpty {
|
||||
keys.append("Author")
|
||||
dic["Author"] = author
|
||||
add(key: "Author", value: author)
|
||||
}
|
||||
if let subject = getKey("Subject", from: dict), !subject.isEmpty {
|
||||
keys.append("Subject")
|
||||
dic["Subject"] = subject
|
||||
add(key: "Subject", value: subject)
|
||||
}
|
||||
var majorVersion: Int32 = 0
|
||||
var minorVersion: Int32 = 0
|
||||
reference.getVersion(majorVersion: &majorVersion, minorVersion: &minorVersion)
|
||||
if majorVersion > 0 {
|
||||
keys.append("Version")
|
||||
dic["Version"] = String(majorVersion) + "." + String(minorVersion)
|
||||
}
|
||||
if reference.numberOfPages > 0 {
|
||||
keys.append("Pages")
|
||||
dic["Pages"] = reference.numberOfPages
|
||||
add(key: "Version", value: String(majorVersion) + "." + String(minorVersion))
|
||||
}
|
||||
add(key: "Pages", value: reference.numberOfPages)
|
||||
|
||||
if reference.numberOfPages > 0, let pageRef = reference.page(at: 1) {
|
||||
let size = pageRef.getBoxRect(CGPDFBox.mediaBox).size
|
||||
keys.append("Resolution")
|
||||
dic["Resolution"] = "\(Int(size.width))x\(Int(size.height))"
|
||||
add(key: "Resolution", value: "\(Int(size.width))x\(Int(size.height))")
|
||||
}
|
||||
if let creator = getKey("Creator", from: dict), !creator.isEmpty {
|
||||
keys.append("Content creator")
|
||||
dic["Content creator"] = creator
|
||||
add(key: "Content creator", value: creator)
|
||||
}
|
||||
if let creationDateString = getKey("CreationDate", from: dict), let creationDate = convertDate(creationDateString) {
|
||||
keys.append("Creation date")
|
||||
dic["Creation date"] = creationDate
|
||||
if let creationDateString = getKey("CreationDate", from: dict) {
|
||||
add(key: "Creation date", value: convertDate(creationDateString))
|
||||
}
|
||||
if let modifiedDateString = getKey("ModDate", from: dict), let modDate = convertDate(modifiedDateString) {
|
||||
keys.append("Modified date")
|
||||
dic["Modified date"] = modDate
|
||||
if let modifiedDateString = getKey("ModDate", from: dict) {
|
||||
add(key: "Modified date", value: convertDate(modifiedDateString))
|
||||
}
|
||||
keys.append("Security")
|
||||
dic["Security"] = reference.isEncrypted ? "Present" : "None"
|
||||
keys.append("Allows printing")
|
||||
dic["Allows printing"] = reference.allowsPrinting ? "Yes" : "No"
|
||||
keys.append("Allows copying")
|
||||
dic["Allows copying"] = reference.allowsCopying ? "Yes" : "No"
|
||||
add(key: "Security", value: reference.isEncrypted ? "Present" : "None")
|
||||
add(key: "Allows printing", value: reference.allowsPrinting ? "Yes" : "No")
|
||||
add(key: "Allows copying", value: reference.allowsCopying ? "Yes" : "No")
|
||||
}
|
||||
return (dic, keys)
|
||||
}
|
||||
@@ -438,6 +419,6 @@ public struct LocalFileInformationGenerator {
|
||||
static public var customProperties: ((_ fileURL: URL) -> (prop: [String: Any], keys: [String]))? = nil
|
||||
}
|
||||
|
||||
func ~=<T : Equatable>(array: [T], value: T) -> Bool {
|
||||
fileprivate func ~=<T : Equatable>(array: [T], value: T) -> Bool {
|
||||
return array.contains(value)
|
||||
}
|
||||
|
||||
@@ -10,26 +10,34 @@ import Foundation
|
||||
|
||||
/// Containts path and attributes of a file or resource.
|
||||
open class FileObject {
|
||||
/// A `Dictionary` contains file information, using `URLResourceKey` keys.
|
||||
open internal(set) var allValues: [String: Any]
|
||||
|
||||
internal init(allValues: [String: Any]) {
|
||||
self.allValues = allValues
|
||||
}
|
||||
|
||||
internal init(absoluteURL: URL? = nil, name: String, path: String) {
|
||||
internal init(url: URL, name: String, path: String) {
|
||||
self.allValues = [String: Any]()
|
||||
self.absoluteURL = absoluteURL
|
||||
self.url = url
|
||||
self.name = name
|
||||
self.path = path
|
||||
}
|
||||
|
||||
/// url to access the resource, not supported by Dropbox provider
|
||||
open internal(set) var absoluteURL: URL? {
|
||||
@available(*, deprecated, renamed: "url", message: "Use url.absoluteURL instead.")
|
||||
open var absoluteURL: URL? {
|
||||
return url?.absoluteURL
|
||||
}
|
||||
|
||||
/// URL to access the resource, can be a relative URL against base URL.
|
||||
/// not supported by Dropbox provider.
|
||||
open internal(set) var url: URL? {
|
||||
get {
|
||||
return allValues["NSURLAbsoluteURLKey"] as? URL
|
||||
return allValues["NSURLFileURLKey"] as? URL
|
||||
}
|
||||
set {
|
||||
allValues["NSURLAbsoluteURLKey"] = newValue
|
||||
allValues["NSURLFileURLKey"] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +101,8 @@ open class FileObject {
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "Use FileObject.type property instead.")
|
||||
/// **OBSOLETED:** Use `type` property instead.
|
||||
@available(*, obsoleted: 1.0, renamed: "type", message: "Use type property instead.")
|
||||
open var fileType: URLFileResourceType? {
|
||||
return self.type
|
||||
}
|
||||
@@ -135,7 +144,38 @@ open class FileObject {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sorting FileObject array by given criteria, not thread-safe
|
||||
internal func resolve(dateString: String) -> Date? {
|
||||
let dateFor: DateFormatter = DateFormatter()
|
||||
dateFor.locale = Locale(identifier: "en_US")
|
||||
dateFor.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ssZ"
|
||||
if let rfc3339 = dateFor.date(from: dateString) {
|
||||
return rfc3339
|
||||
}
|
||||
dateFor.dateFormat = "EEE',' dd' 'MMM' 'yyyy HH':'mm':'ss z"
|
||||
if let rfc1123 = dateFor.date(from: dateString) {
|
||||
return rfc1123
|
||||
}
|
||||
dateFor.dateFormat = "EEEE',' dd'-'MMM'-'yy HH':'mm':'ss z"
|
||||
if let rfc850 = dateFor.date(from: dateString) {
|
||||
return rfc850
|
||||
}
|
||||
dateFor.dateFormat = "EEE MMM d HH':'mm':'ss yyyy"
|
||||
if let asctime = dateFor.date(from: dateString) {
|
||||
return asctime
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
internal func rfc3339utc(of date:Date) -> String {
|
||||
let fm = DateFormatter()
|
||||
fm.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
|
||||
fm.timeZone = TimeZone(identifier:"UTC")
|
||||
fm.locale = Locale(identifier:"en_US_POSIX")
|
||||
return fm.string(from:date)
|
||||
}
|
||||
|
||||
/// Sorting FileObject array by given criteria, **not thread-safe**
|
||||
public struct FileObjectSorting {
|
||||
|
||||
/// Determines sort kind by which item of File object
|
||||
@@ -179,13 +219,19 @@ public struct FileObjectSorting {
|
||||
public static let createdAscending = FileObjectSorting(type: .creationDate, ascending: true)
|
||||
public static let createdDesceding = FileObjectSorting(type: .creationDate, ascending: false)
|
||||
|
||||
/// Initializes a `FileObjectSorting` allows to sort an `Array` of `FileObject`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - type: Determines to sort based on which file property.
|
||||
/// - ascending: `true` of resulting `Array` is ascending
|
||||
/// - isDirectoriesFirst: Puts directoris on the top of resulting `Array`.
|
||||
public init (type: SortType, ascending: Bool = true, isDirectoriesFirst: Bool = false) {
|
||||
self.sortType = type
|
||||
self.ascending = ascending
|
||||
self.isDirectoriesFirst = isDirectoriesFirst
|
||||
}
|
||||
|
||||
/// Sorts array of FileObjects by criterias set in properties
|
||||
/// Sorts array of `FileObject`s by criterias set in properties
|
||||
public func sort(_ files: [FileObject]) -> [FileObject] {
|
||||
return files.sorted {
|
||||
if isDirectoriesFirst {
|
||||
@@ -222,6 +268,19 @@ public struct FileObjectSorting {
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element: FileObject {
|
||||
/// Returns a sorted array of `FileObject`s by criterias set in properties.
|
||||
public func sorted(by type: FileObjectSorting.SortType, ascending: Bool = true, isDirectoriesFirst: Bool = false) -> [Element] {
|
||||
let sorting = FileObjectSorting(type: type, ascending: ascending, isDirectoriesFirst: isDirectoriesFirst)
|
||||
return sorting.sort(self) as! [Element]
|
||||
}
|
||||
|
||||
/// Sorts array of `FileObject`s by criterias set in properties
|
||||
public mutating func sorted(by type: FileObjectSorting.SortType, ascending: Bool = true, isDirectoriesFirst: Bool = false) {
|
||||
self = self.sorted(by: type, ascending: ascending, isDirectoriesFirst: isDirectoriesFirst)
|
||||
}
|
||||
}
|
||||
|
||||
extension URLFileResourceType {
|
||||
public init(fileTypeValue: FileAttributeType) {
|
||||
switch fileTypeValue {
|
||||
|
||||
+429
-41
@@ -18,26 +18,115 @@ public typealias ImageClass = NSImage
|
||||
public typealias SimpleCompletionHandler = ((_ error: Error?) -> Void)?
|
||||
|
||||
public protocol FileProviderBasic: class {
|
||||
/// An string to identify type of provider.
|
||||
static var type: String { get }
|
||||
|
||||
/// An string to identify type of provider.
|
||||
var type: String { get }
|
||||
|
||||
/// The paths in arguments should resolved against base url.
|
||||
var isPathRelative: Bool { get }
|
||||
|
||||
/// The url of which paths should resolve against.
|
||||
var baseURL: URL? { get }
|
||||
|
||||
/// Current active path used in `contentsOfDirectory(path:completionHandler:)` method.
|
||||
var currentPath: String { get set }
|
||||
|
||||
/**
|
||||
Dispatch queue usually used in query methods.
|
||||
Set it to a new object to switch between cuncurrent and serial queues.
|
||||
- **Default:** Cuncurrent `DispatchQueue` object.
|
||||
*/
|
||||
var dispatch_queue: DispatchQueue { get set }
|
||||
|
||||
/// Operation queue ususlly used in file operation methods.
|
||||
/// use `maximumOperationTasks` property of provider to manage operation queue.
|
||||
var operation_queue: OperationQueue { get set }
|
||||
|
||||
/// Delegate to update UI after finishing file operations.
|
||||
var delegate: FileProviderDelegate? { get set }
|
||||
|
||||
/**
|
||||
login credential for provider. Should be set in `init` method.
|
||||
|
||||
**Example initialization:**
|
||||
````
|
||||
let credential = URLCredential(user: "user", password: "password", persistence: .forSeession)
|
||||
````
|
||||
|
||||
- Note: In OAuth based providers like `DropboxFileProvider` and `OneDriveFileProvider`, password is Token.
|
||||
use [OAuthSwift](https://github.com/OAuthSwift/OAuthSwift) library to fetch clientId and Token of user.
|
||||
*/
|
||||
var credential: URLCredential? { get }
|
||||
|
||||
/**
|
||||
Returns an Array of `FileObject`s identifying the the directory entries via asynchronous completion handler.
|
||||
|
||||
If the directory contains no entries or an error is occured, this method will return the empty array.
|
||||
|
||||
- Parameter path: path to target directory. If empty, `currentPath` value will be used.
|
||||
- Parameter completionHandler: a block with result of directory entries or error.
|
||||
`contents`: An array of `FileObject` identifying the the directory entries.
|
||||
`error`: Error returned by system.
|
||||
*/
|
||||
func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void))
|
||||
|
||||
/**
|
||||
Returns a `FileObject` containing the attributes of the item (file, directory, symlink, etc.) at the path in question via asynchronous completion handler.
|
||||
|
||||
If the directory contains no entries or an error is occured, this method will return the empty `FileObject`.
|
||||
|
||||
- Parameter path: path to target directory. If empty, `currentPath` value will be used.
|
||||
- Parameter completionHandler: a block with result of directory entries or error.
|
||||
`attributes`: A `FileObject` containing the attributes of the item.
|
||||
`error`: Error returned by system.
|
||||
*/
|
||||
func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void))
|
||||
|
||||
|
||||
/// Returns total and used space in provider container asynchronously.
|
||||
func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void))
|
||||
|
||||
/**
|
||||
Returns an independent url to access the file. Some providers like `Dropbox` due to their nature.
|
||||
don't return an absolute url to be used to access file directly.
|
||||
- Parameter path: Relative path of file or directory.
|
||||
- Returns: An url, can be used to access to file directly.
|
||||
*/
|
||||
func url(of path: String?) -> URL
|
||||
}
|
||||
|
||||
public extension FileProviderBasic {
|
||||
/// The maximum number of queued operations that can execute at the same time.
|
||||
///
|
||||
/// The default value of this property is `OperationQueue.defaultMaxConcurrentOperationCount`.
|
||||
public var maximumOperationTasks: Int {
|
||||
get {
|
||||
return operation_queue.maxConcurrentOperationCount
|
||||
}
|
||||
set {
|
||||
operation_queue.maxConcurrentOperationCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var fileProviderCancelTasksOnInvalidating = true
|
||||
|
||||
public protocol FileProviderBasicRemote: FileProviderBasic {
|
||||
/// Underlying URLSession instance used for HTTP/S requests
|
||||
var session: URLSession { get }
|
||||
|
||||
/// A `URLCache` to cache downloaded files and contents.
|
||||
///
|
||||
/// If set to nil, `URLCache.shared` object will be used.
|
||||
/// - Note: It has no effect unless setting `useCache` property to `true`.
|
||||
var cache: URLCache? { get }
|
||||
|
||||
/// Determine to use `cache` property to cache downloaded file objects. Doesn't have effect on query type methods.
|
||||
var useCache: Bool { get set }
|
||||
|
||||
/// Validating cached data using E-Tag or Revision identifier if possible.
|
||||
var validatingCache: Bool { get set }
|
||||
}
|
||||
|
||||
@@ -90,26 +179,143 @@ internal extension FileProviderBasicRemote {
|
||||
}
|
||||
|
||||
public protocol FileProviderOperations: FileProviderBasic {
|
||||
/// Delgate for managing operations involving the copying, moving, linking, or removal of files and directories. When you use an FileManager object to initiate a copy, move, link, or remove operation, the file provider asks its delegate whether the operation should begin at all and whether it should proceed when an error occurs.
|
||||
var fileOperationDelegate : FileOperationDelegate? { get set }
|
||||
|
||||
/**
|
||||
Creates a new directory at the specified path asynchronously.
|
||||
This will create any necessary intermediate directories.
|
||||
|
||||
- Parameters:
|
||||
- folder: Directory name.
|
||||
- at: Parent path of new directory.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
*/
|
||||
@discardableResult
|
||||
func create(folder: String, at: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
|
||||
|
||||
/**
|
||||
Creates an new file with data passed to method asynchronously.
|
||||
Returns error via completionHandler if file is already exists.
|
||||
|
||||
- Parameters:
|
||||
- file: New file name with extension separated by period.
|
||||
- at: Parent path of new file.
|
||||
- data: Data of new files. Pass nil or `Data()` to create empty file.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
|
||||
func create(file: String, at: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle?
|
||||
|
||||
/**
|
||||
Moves a file or directory from `path` to designated path asynchronously.
|
||||
When you want move a file, destination path should also consists of file name.
|
||||
Either a new name or the old one. If file is already exist, an error will be returned via completionHandler.
|
||||
|
||||
- Parameters:
|
||||
- path: original file or directory path.
|
||||
- to: destination path of file or directory, including file/directory name.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
func moveItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
|
||||
|
||||
/**
|
||||
Moves a file or directory from `path` to designated path asynchronously.
|
||||
When you want move a file, destination path should also consists of file name.
|
||||
Either a new name or the old one.
|
||||
|
||||
- Parameters:
|
||||
- path: original file or directory path.
|
||||
- to: destination path of file or directory, including file/directory name.
|
||||
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
func moveItem(path: String, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
|
||||
|
||||
/**
|
||||
Copies a file or directory from `path` to designated path asynchronously.
|
||||
When want copy a file, destination path should also consists of file name.
|
||||
Either a new name or the old one. If file is already exist, an error will be returned via completionHandler.
|
||||
|
||||
- Parameters:
|
||||
- path: original file or directory path.
|
||||
- to: destination path of file or directory, including file/directory name.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
func copyItem(path: String, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
|
||||
|
||||
/**
|
||||
Copies a file or directory from `path` to designated path asynchronously.
|
||||
When want copy a file, destination path should also consists of file name.
|
||||
Either a new name or the old one.
|
||||
|
||||
- Parameters:
|
||||
- path: original file or directory path.
|
||||
- to: destination path of file or directory, including file/directory name.
|
||||
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
func copyItem(path: String, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
|
||||
|
||||
/**
|
||||
Removes the file or directory at the specified path.
|
||||
|
||||
- Parameters:
|
||||
- path: file or directory path.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
|
||||
|
||||
*/
|
||||
@discardableResult
|
||||
func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
|
||||
|
||||
/**
|
||||
Uploads a file from local file url to designated path asynchronously.
|
||||
Method will fail if source is not a local url with `file://` scheme.
|
||||
|
||||
- Parameters:
|
||||
- localFile: a file url to file.
|
||||
- to: destination path of file, including file/directory name.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress.
|
||||
*/
|
||||
@discardableResult
|
||||
func copyItem(localFile: URL, to: String, completionHandler: SimpleCompletionHandler) -> OperationHandle?
|
||||
|
||||
/**
|
||||
Uploads a file from local file url to designated path asynchronously.
|
||||
Method will fail if source is not a local url with `file://` scheme.
|
||||
|
||||
- Parameters:
|
||||
- localFile: a file url to file.
|
||||
- to: destination path of file, including file/directory name.
|
||||
- overwrite: Destination file should be overwritten if file is already exists. **Default** is `false`.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress.
|
||||
*/
|
||||
@discardableResult
|
||||
func copyItem(localFile: URL, to: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
|
||||
|
||||
/**
|
||||
Download a file from `path` to designated local file url asynchronously.
|
||||
Method will fail if destination is not a local url with `file://` scheme.
|
||||
|
||||
- Parameters:
|
||||
- path: original file or directory path.
|
||||
- toLocalURL: destination local url of file, including file/directory name.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle?
|
||||
}
|
||||
@@ -132,20 +338,104 @@ extension FileProviderOperations {
|
||||
}
|
||||
|
||||
public protocol FileProviderReadWrite: FileProviderBasic {
|
||||
/**
|
||||
Retreives a `Data` object with the contents of the file asynchronously vis contents argument of completion handler.
|
||||
If path specifies a directory, or if some other error occurs, data will be nil.
|
||||
|
||||
- Parameters:
|
||||
- path: Path of file.
|
||||
- completionHandler: a block with result of file contents or error.
|
||||
`contents`: contents of file in a `Data` object.
|
||||
`error`: Error returned by system.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle?
|
||||
|
||||
/**
|
||||
Retreives a `Data` object with a portion contents of the file asynchronously vis contents argument of completion handler.
|
||||
If path specifies a directory, or if some other error occurs, data will be nil.
|
||||
|
||||
- Parameters:
|
||||
- path: Path of file.
|
||||
- offset: First byte index which should be read. **Starts from 0.**
|
||||
- length: Bytes count of data. Pass `-1` to read until the end of file.
|
||||
- completionHandler: a block with result of file contents or error.
|
||||
`contents`: contents of file in a `Data` object.
|
||||
`error`: Error returned by system.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle?
|
||||
|
||||
/**
|
||||
Write the contents of the `Data` to a location asynchronously.
|
||||
It will return error if file is already exists.
|
||||
Not attomically by default, unless the provider enforces it.
|
||||
|
||||
- Parameters:
|
||||
- path: Path of target file.
|
||||
- contents: Data to be written into file.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
func writeContents(path: String, contents: Data, completionHandler: SimpleCompletionHandler) -> OperationHandle?
|
||||
|
||||
/**
|
||||
Write the contents of the `Data` to a location asynchronously.
|
||||
It will return error if file is already exists.
|
||||
|
||||
- Parameters:
|
||||
- path: Path of target file.
|
||||
- contents: Data to be written into file.
|
||||
- atomically: data will be written to a temporary file before writing to final location. Default is `false`.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
func writeContents(path: String, contents: Data, atomically: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
|
||||
|
||||
/**
|
||||
Write the contents of the `Data` to a location asynchronously.
|
||||
Not attomically by default, unless the provider enforces it.
|
||||
|
||||
- Parameters:
|
||||
- path: Path of target file.
|
||||
- contents: Data to be written into file.
|
||||
- overwrite: Destination file should be overwritten if file is already exists. Default is `false`.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
func writeContents(path: String, contents: Data, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
|
||||
|
||||
/**
|
||||
Write the contents of the `Data` to a location asynchronously.
|
||||
|
||||
- Parameters:
|
||||
- path: Path of target file.
|
||||
- contents: Data to be written into file.
|
||||
- overwrite: Destination file should be overwritten if file is already exists. Default is `false`.
|
||||
- atomically: data will be written to a temporary file before writing to final location. Default is `false`.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
- Returns: An `OperationHandle` to get progress or cancel progress. Doesn't work on `LocalFileProvider`.
|
||||
*/
|
||||
@discardableResult
|
||||
func writeContents(path: String, contents: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle?
|
||||
|
||||
/**
|
||||
Search files inside directory using query asynchronously.
|
||||
|
||||
- Note: For now only it's limited to file names. `query` parameter may take `NSPredicate` format in near future.
|
||||
|
||||
- Parameters:
|
||||
- path: location of directory to start search
|
||||
- recursive: Searching subdirectories of path
|
||||
- query: Simple string of file name to be search (for now).
|
||||
- foundItemHandler: Block which is called when a file is found
|
||||
- completionHandler: Block which will be called after finishing search. Returns an arry of `FileObject` or error if occured.
|
||||
*/
|
||||
func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void))
|
||||
}
|
||||
|
||||
@@ -172,20 +462,46 @@ extension FileProviderReadWrite {
|
||||
}
|
||||
|
||||
public protocol FileProviderMonitor: FileProviderBasic {
|
||||
|
||||
/**
|
||||
Starts monitoring a path and its subpaths, including files and folders, for any change,
|
||||
including copy, move/rename, content changes, etc.
|
||||
To avoid thread congestion, `evetHandler` will be triggered with 0.2 seconds interval,
|
||||
and has a 0.25 second delay, to ensure it's called after updates.
|
||||
|
||||
- Note: this functionality is available only in `LocalFileProvider` and `CloudFileProvider`.
|
||||
- Note: `eventHandler` is not called on main thread, for updating UI. dispatch routine to main thread.
|
||||
- Important: `eventHandler` may be called if file is changed in recursive subpaths of registered path.
|
||||
This may cause negative impact on performance if a root path is being monitored.
|
||||
|
||||
- Parameters:
|
||||
- path: path of directory.
|
||||
- eventHandler: Block executed after change, on a secondary thread.
|
||||
*/
|
||||
func registerNotifcation(path: String, eventHandler: @escaping (() -> Void))
|
||||
|
||||
/// Stops monitoring the path.
|
||||
///
|
||||
/// - Parameter path: path of directory.
|
||||
func unregisterNotifcation(path: String)
|
||||
|
||||
/// Investigate either the path is registered for change notification or not.
|
||||
///
|
||||
/// - Parameter path: path of directory.
|
||||
/// - Returns: Directory is being monitored or not.
|
||||
func isRegisteredForNotification(path: String) -> Bool
|
||||
}
|
||||
|
||||
public protocol FileProvider: FileProviderBasic, FileProviderOperations, FileProviderReadWrite, NSCopying {
|
||||
}
|
||||
|
||||
fileprivate let pathTrimSet = CharacterSet(charactersIn: " /")
|
||||
internal let pathTrimSet = CharacterSet(charactersIn: " /")
|
||||
extension FileProviderBasic {
|
||||
public var type: String {
|
||||
return Self.type
|
||||
}
|
||||
|
||||
/// path without heading and trailing slash
|
||||
public var bareCurrentPath: String {
|
||||
return currentPath.trimmingCharacters(in: pathTrimSet)
|
||||
}
|
||||
@@ -194,6 +510,8 @@ extension FileProviderBasic {
|
||||
return path.trimmingCharacters(in: pathTrimSet).addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)!
|
||||
}
|
||||
|
||||
/// **OBSOLETED:** Use `url(of:).absoluteURL` instead.
|
||||
@available(*, obsoleted: 1.0, renamed: "url(of:)", message: "Use url(of:).absoluteURL instead.")
|
||||
public func absoluteURL(_ path: String? = nil) -> URL {
|
||||
return url(of: path).absoluteURL
|
||||
}
|
||||
@@ -205,18 +523,32 @@ extension FileProviderBasic {
|
||||
} else {
|
||||
rpath = self.currentPath
|
||||
}
|
||||
if isPathRelative, let baseURL = baseURL {
|
||||
if rpath.hasPrefix("/") {
|
||||
rpath = rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? rpath
|
||||
if let baseURL = baseURL {
|
||||
if isPathRelative && rpath.hasPrefix("/") {
|
||||
rpath.remove(at: rpath.startIndex)
|
||||
}
|
||||
return URL(string: rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)!, relativeTo: baseURL)!
|
||||
return URL(string: rpath, relativeTo: baseURL) ?? baseURL
|
||||
} else {
|
||||
return URL(string: rpath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)!)!.standardizedFileURL
|
||||
return URL(string: rpath)!
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Returns the relative path of url, wothout percent encoding. Even if url is absolute or
|
||||
/// retrieved from another provider, it will try to resolve the url against `baseURL` of
|
||||
/// current provider. It's highly recomended to use this method for displaying purposes.
|
||||
///
|
||||
/// - Parameter url: Absolute url to file or directory.
|
||||
/// - Returns: A `String` contains relative path of url against base url.
|
||||
public func relativePathOf(url: URL) -> String {
|
||||
guard let baseURL = self.baseURL else { return url.absoluteString }
|
||||
// check if url derieved from current base url
|
||||
if url.relativeString.isEmpty, url.baseURL == self.baseURL {
|
||||
return url.relativePath.removingPercentEncoding!
|
||||
}
|
||||
|
||||
// resolve url string against baseurl
|
||||
guard let baseURL = self.baseURL?.standardizedFileURL else { return url.absoluteString }
|
||||
return url.standardizedFileURL.absoluteString.replacingOccurrences(of: baseURL.absoluteString, with: "/").removingPercentEncoding!
|
||||
}
|
||||
|
||||
@@ -229,6 +561,8 @@ extension FileProviderBasic {
|
||||
return p
|
||||
}
|
||||
|
||||
/// Returns a file name supposed to be unique with adding numbers to end of file.
|
||||
/// - Important: It's a synchronous method. Don't use it on matin thread.
|
||||
public func fileByUniqueName(_ filePath: String) -> String {
|
||||
let fileUrl = URL(fileURLWithPath: filePath)
|
||||
let dirPath = fileUrl.deletingLastPathComponent().path
|
||||
@@ -250,7 +584,7 @@ extension FileProviderBasic {
|
||||
}
|
||||
var i = number ?? 2
|
||||
let similiar = contents.map {
|
||||
$0.absoluteURL?.lastPathComponent ?? $0.name
|
||||
$0.url?.lastPathComponent ?? $0.name
|
||||
}.filter {
|
||||
$0.hasPrefix(result)
|
||||
}
|
||||
@@ -266,7 +600,7 @@ extension FileProviderBasic {
|
||||
}
|
||||
|
||||
internal func throwError(_ path: String, code: FoundationErrorEnum) -> NSError {
|
||||
let fileURL = self.absoluteURL(path)
|
||||
let fileURL = self.url(of: path)
|
||||
let domain: String
|
||||
switch code {
|
||||
case is URLError:
|
||||
@@ -277,47 +611,67 @@ extension FileProviderBasic {
|
||||
return NSError(domain: domain, code: code.rawValue, userInfo: [NSURLErrorFailingURLErrorKey: fileURL, NSURLErrorFailingURLStringErrorKey: fileURL.absoluteString])
|
||||
}
|
||||
|
||||
internal func NotImplemented() {
|
||||
assert(false, "method not implemented")
|
||||
internal func NotImplemented(_ fn: String = #function, file: StaticString = #file) {
|
||||
assert(false, "\(fn) method is not yet implemented. \(file)")
|
||||
}
|
||||
|
||||
internal func resolve(dateString: String) -> Date? {
|
||||
let dateFor: DateFormatter = DateFormatter()
|
||||
dateFor.locale = Locale(identifier: "en_US")
|
||||
dateFor.dateFormat = "EEE',' dd' 'MMM' 'yyyy HH':'mm':'ss zzz"
|
||||
if let rfc1123 = dateFor.date(from: dateString) {
|
||||
return rfc1123
|
||||
}
|
||||
dateFor.dateFormat = "EEEE',' dd'-'MMM'-'yy HH':'mm':'ss z"
|
||||
if let rfc850 = dateFor.date(from: dateString) {
|
||||
return rfc850
|
||||
}
|
||||
dateFor.dateFormat = "EEE MMM d HH':'mm':'ss yyyy"
|
||||
if let asctime = dateFor.date(from: dateString) {
|
||||
return asctime
|
||||
}
|
||||
dateFor.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ssz"
|
||||
if let isotime = dateFor.date(from: dateString) {
|
||||
return isotime
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func string(from date:Date) -> String {
|
||||
let fm = DateFormatter()
|
||||
fm.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
|
||||
fm.timeZone = TimeZone(identifier:"UTC")
|
||||
fm.locale = Locale(identifier:"en_US_POSIX")
|
||||
return fm.string(from:date)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public protocol ExtendedFileProvider: FileProviderBasic {
|
||||
/// Returuns true if thumbnail preview is supported by provider and file type accordingly.
|
||||
///
|
||||
/// - Parameter path: path of file.
|
||||
/// - Returns: A `Bool` idicates path can have thumbnail.
|
||||
func thumbnailOfFileSupported(path: String) -> Bool
|
||||
|
||||
/// Returns true if provider supports fetching properties of file like dimensions, duration, etc.
|
||||
/// Usually media or document files support these meta-infotmations.
|
||||
///
|
||||
/// - Parameter path: path of file.
|
||||
/// - Returns: A `Bool` idicates path can have properties.
|
||||
func propertiesOfFileSupported(path: String) -> Bool
|
||||
|
||||
/**
|
||||
Generates ans returns a thumbnail preview of document asynchronously. The defualt dimension of returned image is different
|
||||
regarding provider type, usually 64x64 pixels.
|
||||
|
||||
- Parameters:
|
||||
- path: path of file.
|
||||
- completionHandler: a block with result of preview image or error.
|
||||
`image`: `NSImage`/`UIImage` object contains preview.
|
||||
`error`: Error returned by system.
|
||||
*/
|
||||
func thumbnailOfFile(path: String, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void))
|
||||
|
||||
/**
|
||||
Generates ans returns a thumbnail preview of document asynchronously. The defualt dimension of returned image is different
|
||||
regarding provider type, usually 64x64 pixels. Default value used when `dimenstion` is `nil`.
|
||||
|
||||
- Note: `LocalFileInformationGenerator` variables can be set to change default behavior of
|
||||
thumbnail and properties generator of `LocalFileProvider`.
|
||||
|
||||
- Parameters:
|
||||
- path: path of file.
|
||||
- dimension: width and height of result preview image.
|
||||
- completionHandler: a block with result of preview image or error.
|
||||
`image`: `NSImage`/`UIImage` object contains preview.
|
||||
`error`: Error returned by system.
|
||||
*/
|
||||
func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((_ image: ImageClass?, _ error: Error?) -> Void))
|
||||
|
||||
/**
|
||||
Fetching properties of file like dimensions, duration, etc. It's variant depending on file type.
|
||||
Images, videos and audio files meta-information will be returned.
|
||||
|
||||
- Note: `LocalFileInformationGenerator` variables can be set to change default behavior of
|
||||
thumbnail and properties generator of `LocalFileProvider`.
|
||||
|
||||
- Parameters:
|
||||
- path: path of file.
|
||||
- completionHandler: a block with result of preview image or error.
|
||||
`propertiesDictionary`: A `Dictionary` of proprty keys and values.
|
||||
`keys`: An `Array` contains ordering of keys.
|
||||
`error`: Error returned by system.
|
||||
*/
|
||||
func propertiesOfFile(path: String, completionHandler: @escaping ((_ propertiesDictionary: [String: Any], _ keys: [String], _ error: Error?) -> Void))
|
||||
}
|
||||
|
||||
@@ -444,13 +798,21 @@ extension ExtendedFileProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/// Operation type description of file operation, included files path in associated values.
|
||||
public enum FileOperationType: CustomStringConvertible {
|
||||
/// Creating a file or directory in path.
|
||||
case create (path: String)
|
||||
/// Copying a file or directory from source to destination.
|
||||
case copy (source: String, destination: String)
|
||||
/// Moving a file or directory from source to destination.
|
||||
case move (source: String, destination: String)
|
||||
/// Modifying data of a file o in path by writing new data.
|
||||
case modify (path: String)
|
||||
/// Deleting file or directory in path.
|
||||
case remove (path: String)
|
||||
/// Creating a symbolic link or alias to target.
|
||||
case link (link: String, target: String)
|
||||
/// Fetching data in file located in path.
|
||||
case fetch (path: String)
|
||||
|
||||
public var description: String {
|
||||
@@ -465,16 +827,24 @@ public enum FileOperationType: CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
/// present participle of action, like 'Copying`.
|
||||
public var actionDescription: String {
|
||||
return description.trimmingCharacters(in: CharacterSet(charactersIn: "e")) + "ing"
|
||||
}
|
||||
|
||||
/// Path of subjecting file.
|
||||
public var source: String? {
|
||||
guard let reflect = Mirror(reflecting: self).children.first?.value else { return nil }
|
||||
let mirror = Mirror(reflecting: reflect)
|
||||
return reflect as? String ?? mirror.children.first?.value as? String
|
||||
}
|
||||
|
||||
/// Path of subjecting file.
|
||||
public var path: String? {
|
||||
return source
|
||||
}
|
||||
|
||||
/// Path of destination file.
|
||||
public var destination: String? {
|
||||
guard let reflect = Mirror(reflecting: self).children.first?.value else { return nil }
|
||||
let mirror = Mirror(reflecting: reflect)
|
||||
@@ -491,11 +861,22 @@ public enum FileOperationType: CustomStringConvertible {
|
||||
|
||||
|
||||
public protocol OperationHandle {
|
||||
/// Operation supposed to be done on files. Contains file paths as associated value.
|
||||
var operationType: FileOperationType { get }
|
||||
|
||||
/// Bytes written/read by operation so far.
|
||||
var bytesSoFar: Int64 { get }
|
||||
|
||||
/// Total bytes of operation.
|
||||
var totalBytes: Int64 { get }
|
||||
|
||||
/// Operation is progress or not, Returns false if operation is done or not initiated yet.
|
||||
var inProgress: Bool { get }
|
||||
|
||||
/// Progress of operation, usually equals with `bytesSoFar/totalBytes`. or NaN if not available.
|
||||
var progress: Float { get }
|
||||
|
||||
/// Cancels operation while in progress, or cancels data/download/upload url session task.
|
||||
func cancel() -> Bool
|
||||
}
|
||||
|
||||
@@ -508,8 +889,15 @@ public extension OperationHandle {
|
||||
}
|
||||
|
||||
public protocol FileProviderDelegate: class {
|
||||
/// fileproviderSucceed(_:operation:) gives delegate a notification when an operation finished with success.
|
||||
/// This method is called in main thread to avoids UI bugs.
|
||||
func fileproviderSucceed(_ fileProvider: FileProviderOperations, operation: FileOperationType)
|
||||
/// fileproviderSucceed(_:operation:) gives delegate a notification when an operation finished with failure.
|
||||
/// This method is called in main thread to avoids UI bugs.
|
||||
func fileproviderFailed(_ fileProvider: FileProviderOperations, operation: FileOperationType)
|
||||
/// fileproviderSucceed(_:operation:) gives delegate a notification when an operation progess.
|
||||
/// Supported by some providers, especially remote ones.
|
||||
/// This method is called in main thread to avoids UI bugs.
|
||||
func fileproviderProgress(_ fileProvider: FileProviderOperations, operation: FileOperationType, progress: Float)
|
||||
}
|
||||
|
||||
|
||||
+403
-101
@@ -9,49 +9,116 @@
|
||||
import Foundation
|
||||
|
||||
open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
open static let type = "Local"
|
||||
open var isPathRelative: Bool = true
|
||||
open private(set) var baseURL: URL? = LocalFileProvider.defaultBaseURL()
|
||||
open var currentPath: String = ""
|
||||
open class var type: String { return "Local" }
|
||||
open var isPathRelative: Bool
|
||||
open fileprivate(set) var baseURL: URL?
|
||||
open var currentPath: String
|
||||
open var dispatch_queue: DispatchQueue
|
||||
open var operation_queue: DispatchQueue
|
||||
open var operation_queue: OperationQueue
|
||||
open weak var delegate: FileProviderDelegate?
|
||||
open let credential: URLCredential? = nil
|
||||
open internal(set) var credential: URLCredential?
|
||||
|
||||
open private(set) var fileManager = FileManager()
|
||||
open private(set) var opFileManager = FileManager()
|
||||
fileprivate var fileProviderManagerDelegate: LocalFileProviderManagerDelegate? = nil
|
||||
|
||||
public init () {
|
||||
dispatch_queue = DispatchQueue(label: "FileProvider.\(LocalFileProvider.type)", attributes: DispatchQueue.Attributes.concurrent)
|
||||
operation_queue = DispatchQueue(label: "FileProvider.\(LocalFileProvider.type).Operation", attributes: [])
|
||||
fileProviderManagerDelegate = LocalFileProviderManagerDelegate(provider: self)
|
||||
opFileManager.delegate = fileProviderManagerDelegate
|
||||
/**
|
||||
Forces file operations to use `NSFileCoordinating`, should be set `true` if:
|
||||
- Files are on ubiquity (iCloud) container.
|
||||
- Multiple processes are accessing same file, recommended when accessing a shared/public
|
||||
user document in macOS and when using app extensions in iOS/tvOS (shared container).
|
||||
|
||||
By default it's `true` when using iCloud or shared container (App Group) initializers,
|
||||
otherwise it's `false` to accelerate operations.
|
||||
*/
|
||||
open var isCoorinating: Bool
|
||||
|
||||
/**
|
||||
Initializes provider for the specified common directory in the requested domains.
|
||||
default values are `directory: .documentDirectory, domainMask: .userDomainMask`.
|
||||
|
||||
- Parameters:
|
||||
- directory: The search path directory. The supported values are described in `FileManager.SearchPathDirectory`.
|
||||
- domainMask: The file system domain to search. The value for this parameter is one or more of the constants described in `FileManager.SearchPathDomainMask`.
|
||||
*/
|
||||
public convenience init (directory: FileManager.SearchPathDirectory = .documentDirectory, domainMask: FileManager.SearchPathDomainMask = .userDomainMask) {
|
||||
self.init(baseURL: FileManager.default.urls(for: directory, in: domainMask).first!)
|
||||
}
|
||||
|
||||
/**
|
||||
Failable initializer for the specified shared container directory, allows data and files to be shared among app
|
||||
and extensions regarding sandbox requirements. Container ID is same with app group specified in project `Capabilities`
|
||||
tab under `App Group` item. If you don't have enough privilage to access container or the app group imply does't exist,
|
||||
initialing will fail.
|
||||
default values are `directory: .documentDirectory`.
|
||||
|
||||
- Parameters:
|
||||
- sharedContainerId: Same with `App Group` identifier defined in project settings.
|
||||
- directory: The search path directory. The supported values are described in `FileManager.SearchPathDirectory`.
|
||||
*/
|
||||
public convenience init? (sharedContainerId: String, directory: FileManager.SearchPathDirectory = .documentDirectory) {
|
||||
guard let baseURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: sharedContainerId) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var finalBaseURL = baseURL
|
||||
|
||||
switch directory {
|
||||
case .documentDirectory:
|
||||
finalBaseURL = baseURL.appendingPathComponent("Documents")
|
||||
case .libraryDirectory:
|
||||
finalBaseURL = baseURL.appendingPathComponent("Library")
|
||||
case .cachesDirectory:
|
||||
finalBaseURL = baseURL.appendingPathComponent("Library/Caches")
|
||||
case .applicationSupportDirectory:
|
||||
finalBaseURL = baseURL.appendingPathComponent("Library/Application%20support")
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
self.init(baseURL: finalBaseURL)
|
||||
|
||||
try? fileManager.createDirectory(at: finalBaseURL, withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
/// Initializes provider for the specified local URL.
|
||||
///
|
||||
/// - Parameter baseURL: Local URL location for base directory.
|
||||
public init (baseURL: URL) {
|
||||
guard baseURL.isFileURL else {
|
||||
fatalError("Cannot initialize a Local provider from remote URL.")
|
||||
}
|
||||
self.baseURL = baseURL
|
||||
dispatch_queue = DispatchQueue(label: "FileProvider.\(LocalFileProvider.type)", attributes: DispatchQueue.Attributes.concurrent)
|
||||
operation_queue = DispatchQueue(label: "FileProvider.\(LocalFileProvider.type).Operation", attributes: [])
|
||||
self.isPathRelative = true
|
||||
self.currentPath = ""
|
||||
self.credential = nil
|
||||
self.isCoorinating = false
|
||||
|
||||
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: .concurrent)
|
||||
operation_queue = OperationQueue()
|
||||
operation_queue.name = "FileProvider.\(type(of: self).type).Operation"
|
||||
|
||||
fileProviderManagerDelegate = LocalFileProviderManagerDelegate(provider: self)
|
||||
opFileManager.delegate = fileProviderManagerDelegate
|
||||
|
||||
}
|
||||
|
||||
open static func defaultBaseURL() -> URL {
|
||||
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true);
|
||||
return URL(fileURLWithPath: paths[0])
|
||||
/// **DEPRECATED:** No longer is in use and overriding this method has no effect anymore.
|
||||
@available(*, deprecated, message: "Overriding this method has no effect anymore.")
|
||||
open class func defaultBaseURL() -> URL {
|
||||
return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
}
|
||||
|
||||
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
|
||||
dispatch_queue.async {
|
||||
do {
|
||||
let contents = try self.fileManager.contentsOfDirectory(at: self.absoluteURL(path), includingPropertiesForKeys: [.nameKey, .fileSizeKey, .fileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .isHiddenKey, .volumeIsReadOnlyKey], options: .skipsSubdirectoryDescendants)
|
||||
let contents = try self.fileManager.contentsOfDirectory(at: self.url(of: path), includingPropertiesForKeys: [.nameKey, .fileSizeKey, .fileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .isHiddenKey, .volumeIsReadOnlyKey], options: .skipsSubdirectoryDescendants)
|
||||
let filesAttributes = contents.flatMap({ (fileURL) -> LocalFileObject? in
|
||||
let path = self.relativePathOf(url: fileURL)
|
||||
return LocalFileObject(fileWithPath: path, relativeTo: self.baseURL)
|
||||
})
|
||||
completionHandler(filesAttributes, nil)
|
||||
} catch let e as NSError {
|
||||
} catch let e {
|
||||
completionHandler([], e)
|
||||
}
|
||||
}
|
||||
@@ -75,109 +142,204 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
@discardableResult
|
||||
open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(folderName) + "/")
|
||||
operation_queue.async {
|
||||
let url = self.url(of: atPath).appendingPathComponent(folderName)
|
||||
|
||||
let operationHandler: (URL) -> Void = { url in
|
||||
do {
|
||||
try self.opFileManager.createDirectory(at: self.absoluteURL(atPath).appendingPathComponent(folderName), withIntermediateDirectories: true, attributes: [:])
|
||||
try self.opFileManager.createDirectory(at: url, withIntermediateDirectories: true, attributes: [:])
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async(execute: {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
})
|
||||
} catch let e as NSError {
|
||||
}
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async(execute: {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isCoorinating {
|
||||
let intent = NSFileAccessIntent.writingIntent(with: url, options: .forReplacing)
|
||||
self.coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
|
||||
completionHandler?(error)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
operation_queue.addOperation {
|
||||
operationHandler(url)
|
||||
}
|
||||
}
|
||||
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func create(file fileName: String, at atPath: String, contents data: Data?, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(fileName))
|
||||
operation_queue.async {
|
||||
let fileURL = self.absoluteURL(atPath).appendingPathComponent(fileName)
|
||||
let success = self.opFileManager.createFile(atPath: fileURL.path, contents: data, attributes: nil)
|
||||
let url = self.url(of: atPath).appendingPathComponent(fileName, isDirectory: false)
|
||||
|
||||
let operationHandler: (URL) -> Void = { url in
|
||||
let success = self.opFileManager.createFile(atPath: url.path, contents: data, attributes: nil)
|
||||
if success {
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async(execute: {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
completionHandler?(self.throwError(atPath, code: URLError.cannotCreateFile as FoundationErrorEnum))
|
||||
DispatchQueue.main.async(execute: {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isCoorinating {
|
||||
let intent = NSFileAccessIntent.writingIntent(with:url, options: .forReplacing)
|
||||
self.coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
|
||||
completionHandler?(error)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
operation_queue.addOperation {
|
||||
operationHandler(url)
|
||||
}
|
||||
}
|
||||
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.move(source: path, destination: toPath)
|
||||
operation_queue.async {
|
||||
if !overwrite && self.fileManager.fileExists(atPath: self.absoluteURL(toPath).path) {
|
||||
let sourceUrl = self.url(of: path)
|
||||
let destUrl = self.url(of: toPath)
|
||||
|
||||
let sourceIntent = NSFileAccessIntent.writingIntent(with: sourceUrl, options: .forDeleting)
|
||||
let destIntent = NSFileAccessIntent.writingIntent(with: destUrl, options: .forReplacing)
|
||||
|
||||
let operationHandler: (URL, URL) -> Void = { sourceUrl, destUrl in
|
||||
if !overwrite && self.fileManager.fileExists(atPath: destUrl.path) {
|
||||
completionHandler?(self.throwError(toPath, code: URLError.cannotMoveFile as FoundationErrorEnum))
|
||||
return
|
||||
}
|
||||
do {
|
||||
try self.opFileManager.moveItem(at: self.absoluteURL(path), to: self.absoluteURL(toPath))
|
||||
try self.opFileManager.moveItem(at: sourceUrl, to: destUrl)
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async(execute: {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
})
|
||||
} catch let e as NSError {
|
||||
}
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async(execute: {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isCoorinating {
|
||||
coordinated(intents: [sourceIntent, destIntent], completionHandler: operationHandler) { (error) in
|
||||
completionHandler?(error)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
operation_queue.addOperation {
|
||||
operationHandler(sourceUrl, destUrl)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.copy(source: path, destination: toPath)
|
||||
operation_queue.async {
|
||||
if !overwrite && self.fileManager.fileExists(atPath: self.absoluteURL(toPath).path) {
|
||||
completionHandler?(self.throwError(toPath, code: URLError.cannotWriteToFile as FoundationErrorEnum))
|
||||
let sourceUrl = self.url(of: path)
|
||||
let destUrl = self.url(of: toPath)
|
||||
|
||||
let sourceIntent = NSFileAccessIntent.readingIntent(with: sourceUrl, options: .withoutChanges)
|
||||
let destIntent = NSFileAccessIntent.writingIntent(with: destUrl, options: .forDeleting)
|
||||
|
||||
let operationHandler: (URL, URL) -> Void = { sourceUrl, destUrl in
|
||||
if !overwrite && self.fileManager.fileExists(atPath: destUrl.path) {
|
||||
completionHandler?(self.throwError(toPath, code: URLError.cannotMoveFile as FoundationErrorEnum))
|
||||
return
|
||||
}
|
||||
do {
|
||||
try self.opFileManager.copyItem(at: self.absoluteURL(path), to: self.absoluteURL(toPath))
|
||||
try self.opFileManager.copyItem(at: sourceUrl, to: destUrl)
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async(execute: {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
})
|
||||
} catch let e as NSError {
|
||||
}
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async(execute: {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isCoorinating {
|
||||
coordinated(intents: [sourceIntent, destIntent], moving: true, completionHandler: operationHandler) { (error) in
|
||||
completionHandler?(error)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
operation_queue.addOperation {
|
||||
operationHandler(sourceUrl, destUrl)
|
||||
}
|
||||
}
|
||||
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.remove(path: path)
|
||||
operation_queue.async {
|
||||
let url = self.url(of: path)
|
||||
|
||||
let operationHandler: (URL) -> Void = { url in
|
||||
do {
|
||||
try self.opFileManager.removeItem(at: self.absoluteURL(path))
|
||||
let successfulSecurityScopedResourceAccess = url.startAccessingSecurityScopedResource()
|
||||
try self.opFileManager.removeItem(at: url)
|
||||
if successfulSecurityScopedResourceAccess {
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async(execute: {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
})
|
||||
} catch let e as NSError {
|
||||
}
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async(execute: {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isCoorinating {
|
||||
let intent = NSFileAccessIntent.writingIntent(with:url, options: .forReplacing)
|
||||
self.coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
|
||||
completionHandler?(error)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
operation_queue.addOperation {
|
||||
operationHandler(url)
|
||||
}
|
||||
}
|
||||
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@@ -185,18 +347,18 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
// TODO: Make use of overwrite parameter
|
||||
let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath)
|
||||
operation_queue.async {
|
||||
operation_queue.addOperation {
|
||||
do {
|
||||
try self.opFileManager.copyItem(at: localFile, to: self.absoluteURL(toPath))
|
||||
try self.opFileManager.copyItem(at: localFile, to: self.url(of: toPath))
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async(execute: {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
})
|
||||
} catch let e as NSError {
|
||||
}
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async(execute: {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
@@ -205,18 +367,18 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
@discardableResult
|
||||
open func copyItem(path: String, toLocalURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.copy(source: path, destination: toLocalURL.absoluteString)
|
||||
operation_queue.async {
|
||||
operation_queue.addOperation {
|
||||
do {
|
||||
try self.opFileManager.copyItem(at: self.absoluteURL(path), to: toLocalURL)
|
||||
try self.opFileManager.copyItem(at: self.url(of: path), to: toLocalURL)
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async(execute: {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
})
|
||||
} catch let e as NSError {
|
||||
}
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async(execute: {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
@@ -224,43 +386,101 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
|
||||
@discardableResult
|
||||
open func contents(path: String, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
dispatch_queue.async {
|
||||
let data = self.fileManager.contents(atPath: self.absoluteURL(path).path)
|
||||
completionHandler(data, nil)
|
||||
let opType = FileOperationType.fetch(path: path)
|
||||
let url = self.url(of: path)
|
||||
|
||||
let operationHandler: (URL) -> Void = { url in
|
||||
do {
|
||||
let data = try Data(contentsOf: url)
|
||||
completionHandler(data, nil)
|
||||
} catch let e {
|
||||
completionHandler(nil, e)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
if isCoorinating {
|
||||
let intent = NSFileAccessIntent.readingIntent(with: url, options: .withoutChanges)
|
||||
coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
|
||||
completionHandler(nil, error)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
dispatch_queue.async {
|
||||
operationHandler(url)
|
||||
}
|
||||
}
|
||||
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
if length < 0 {
|
||||
if length == 0 || offset < 0 {
|
||||
dispatch_queue.async {
|
||||
completionHandler(Data(), nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if offset == 0 && length < 0 {
|
||||
return self.contents(path: path, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
let opType = FileOperationType.fetch(path: path)
|
||||
dispatch_queue.async {
|
||||
let aPath = self.absoluteURL(path).path
|
||||
guard self.fileManager.fileExists(atPath: aPath) && !self.absoluteURL(path).fileIsDirectory else {
|
||||
completionHandler(nil, self.throwError(path, code: URLError.cannotOpenFile as FoundationErrorEnum))
|
||||
return
|
||||
}
|
||||
guard let handle = FileHandle(forReadingAtPath: aPath) else {
|
||||
let url = self.url(of: path)
|
||||
|
||||
let operationHandler: (URL) -> Void = { url in
|
||||
guard self.fileManager.fileExists(atPath: url.path) && !url.fileIsDirectory else {
|
||||
completionHandler(nil, self.throwError(path, code: URLError.fileDoesNotExist as FoundationErrorEnum))
|
||||
return
|
||||
}
|
||||
guard let handle = FileHandle(forReadingAtPath: url.path) else {
|
||||
completionHandler(nil, self.throwError(path, code: URLError.cannotOpenFile as FoundationErrorEnum))
|
||||
return
|
||||
}
|
||||
|
||||
defer {
|
||||
handle.closeFile()
|
||||
}
|
||||
handle.seek(toFileOffset: UInt64(offset))
|
||||
let data = handle.readData(ofLength: length)
|
||||
completionHandler(data, nil)
|
||||
|
||||
handle.seek(toFileOffset: UInt64(offset))
|
||||
guard Int64(handle.offsetInFile) == offset else {
|
||||
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadUnknown as FoundationErrorEnum))
|
||||
return
|
||||
}
|
||||
|
||||
let data = handle.readData(ofLength: length)
|
||||
guard length > 0 && data.count == length else {
|
||||
completionHandler(nil, self.throwError(path, code: CocoaError.fileReadTooLarge as FoundationErrorEnum))
|
||||
return
|
||||
}
|
||||
|
||||
completionHandler(data, nil)
|
||||
}
|
||||
|
||||
if isCoorinating {
|
||||
let intent = NSFileAccessIntent.readingIntent(with: url, options: .withoutChanges)
|
||||
coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
|
||||
completionHandler(nil, error)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
dispatch_queue.async {
|
||||
operationHandler(url)
|
||||
}
|
||||
}
|
||||
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
open func writeContents(path: String, contents data: Data, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let opType = FileOperationType.modify(path: path)
|
||||
let url = self.url(of: path)
|
||||
var options: Data.WritingOptions = []
|
||||
if atomically {
|
||||
options.insert(.atomic)
|
||||
@@ -268,18 +488,42 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
if overwrite {
|
||||
options.insert(.withoutOverwriting)
|
||||
}
|
||||
operation_queue.async {
|
||||
try? data.write(to: self.absoluteURL(path), options: atomically ? [.atomic] : [])
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
})
|
||||
|
||||
let operationHandler: (URL) -> Void = { url in
|
||||
do {
|
||||
try data.write(to: url, options: atomically ? [.atomic] : [])
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async{
|
||||
self.delegate?.fileproviderSucceed(self, operation: opType)
|
||||
}
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isCoorinating {
|
||||
let intent = NSFileAccessIntent.writingIntent(with: url, options: .forReplacing)
|
||||
coordinated(intents: [intent], completionHandler: operationHandler, errorHandler: { error in
|
||||
completionHandler?(error)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: opType)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
operation_queue.addOperation {
|
||||
operationHandler(url)
|
||||
}
|
||||
}
|
||||
|
||||
return LocalOperationHandle(operationType: opType, baseURL: self.baseURL)
|
||||
}
|
||||
|
||||
open func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
|
||||
dispatch_queue.async {
|
||||
let iterator = self.fileManager.enumerator(at: self.absoluteURL(path), includingPropertiesForKeys: nil, options: recursive ? [] : [.skipsSubdirectoryDescendants, .skipsPackageDescendants]) { (url, e) -> Bool in
|
||||
let iterator = self.fileManager.enumerator(at: self.url(of: path), includingPropertiesForKeys: nil, options: recursive ? [] : [.skipsSubdirectoryDescendants, .skipsPackageDescendants]) { (url, e) -> Bool in
|
||||
completionHandler([], e)
|
||||
return true
|
||||
}
|
||||
@@ -301,12 +545,12 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
|
||||
open func registerNotifcation(path: String, eventHandler: @escaping (() -> Void)) {
|
||||
self.unregisterNotifcation(path: path)
|
||||
let absurl = self.absoluteURL(path)
|
||||
let isdir = (try? absurl.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false) ?? false
|
||||
let dirurl = self.url(of: path)
|
||||
let isdir = (try? dirurl.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false) ?? false
|
||||
if !isdir {
|
||||
return
|
||||
}
|
||||
let monitor = LocalFolderMonitor(url: absurl) {
|
||||
let monitor = LocalFolderMonitor(url: dirurl) {
|
||||
eventHandler()
|
||||
}
|
||||
monitor.start()
|
||||
@@ -339,19 +583,77 @@ open class LocalFileProvider: FileProvider, FileProviderMonitor {
|
||||
}
|
||||
|
||||
public extension LocalFileProvider {
|
||||
/**
|
||||
Creates a symbolic link at the specified path that points to an item at the given path.
|
||||
This method does not traverse symbolic links contained in destURL, making it possible
|
||||
to create symbolic links to locations that do not yet exist.
|
||||
Also, if the final path component in url is a symbolic link, that link is not followed.
|
||||
|
||||
- Parameters:
|
||||
- path: The file path at which to create the new symbolic link. The last component of the path issued as the name of the link.
|
||||
- destPath: The path that contains the item to be pointed to by the link. In other words, this is the destination of the link.
|
||||
- completionHandler: If an error parameter was provided, a presentable `Error` will be returned.
|
||||
*/
|
||||
public func create(symbolicLink path: String, withDestinationPath destPath: String, completionHandler: SimpleCompletionHandler) {
|
||||
operation_queue.async {
|
||||
operation_queue.addOperation {
|
||||
do {
|
||||
try self.opFileManager.createSymbolicLink(at: self.absoluteURL(path), withDestinationURL: self.absoluteURL(destPath))
|
||||
try self.opFileManager.createSymbolicLink(at: self.url(of: path), withDestinationURL: self.url(of: destPath))
|
||||
completionHandler?(nil)
|
||||
DispatchQueue.main.async(execute: {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderSucceed(self, operation: .link(link: path, target: destPath))
|
||||
})
|
||||
} catch let e as NSError {
|
||||
}
|
||||
} catch let e {
|
||||
completionHandler?(e)
|
||||
DispatchQueue.main.async(execute: {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.fileproviderFailed(self, operation: .link(link: path, target: destPath))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the path of the item pointed to by a symbolic link.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - path: The path of a file or directory.
|
||||
/// - completionHandler: Returns destination url of given symbolic link, or an `Error` object if it fails.
|
||||
public func destination(ofSymbolicLink path: String, completionHandler: @escaping (_ url: URL?, _ error: Error?) -> Void) {
|
||||
dispatch_queue.async {
|
||||
do {
|
||||
let destPath = try self.opFileManager.destinationOfSymbolicLink(atPath: self.url(of: path).path)
|
||||
let destUrl = URL(fileURLWithPath: destPath)
|
||||
completionHandler(destUrl, nil)
|
||||
} catch let e{
|
||||
completionHandler(nil, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal extension LocalFileProvider {
|
||||
func coordinated(intents: [NSFileAccessIntent], completionHandler: @escaping (_ url: URL) -> Void, errorHandler: ((_ error: Error) -> Void)? = nil) {
|
||||
let coordinator = NSFileCoordinator(filePresenter: nil)
|
||||
coordinator.coordinate(with: intents, queue: operation_queue) { (error) in
|
||||
if let error = error {
|
||||
errorHandler?(error)
|
||||
return
|
||||
}
|
||||
completionHandler(intents[0].url)
|
||||
}
|
||||
}
|
||||
|
||||
func coordinated(intents: [NSFileAccessIntent], moving: Bool = false, completionHandler: @escaping (_ sourceUrl: URL, _ destURL: URL) -> Void, errorHandler: ((_ error: Error) -> Void)? = nil) {
|
||||
let coordinator = NSFileCoordinator(filePresenter: nil)
|
||||
coordinator.coordinate(with: intents, queue: operation_queue) { (error) in
|
||||
if let error = error {
|
||||
errorHandler?(error)
|
||||
return
|
||||
}
|
||||
if moving {
|
||||
coordinator.item(at: intents[0].url, willMoveTo: intents[1].url)
|
||||
}
|
||||
completionHandler(intents[0].url, intents[1].url)
|
||||
if moving {
|
||||
coordinator.item(at: intents[0].url, didMoveTo: intents[1].url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+31
-17
@@ -9,32 +9,38 @@
|
||||
import Foundation
|
||||
|
||||
public final class LocalFileObject: FileObject {
|
||||
internal init(absoluteURL: URL, name: String, path: String) {
|
||||
super.init(absoluteURL: absoluteURL, name: name, path: path)
|
||||
internal override init(url: URL, name: String, path: String) {
|
||||
super.init(url: url, name: name, path: path)
|
||||
}
|
||||
|
||||
public convenience init? (fileWithPath path: String, relativeTo relativeURL: URL?) {
|
||||
let fileURL: URL
|
||||
if let relativeURL = relativeURL {
|
||||
fileURL = relativeURL.appendingPathComponent(path)
|
||||
} else {
|
||||
fileURL = URL(fileURLWithPath: path)
|
||||
var fileURL: URL?
|
||||
var rpath = path.replacingOccurrences(of: relativeURL?.absoluteString ?? "", with: "", options: .anchored)
|
||||
if path.hasPrefix("/") {
|
||||
rpath.remove(at: rpath.startIndex)
|
||||
}
|
||||
do {
|
||||
let values = try fileURL.resourceValues(forKeys: [.nameKey, .fileSizeKey, .fileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .fileResourceTypeKey, .isHiddenKey, .isWritableKey])
|
||||
self.init(absoluteURL: fileURL, name: values.name ?? fileURL.lastPathComponent, path: path)
|
||||
for (key, value) in values.allValues {
|
||||
self.allValues[key.rawValue] = value
|
||||
if rpath.isEmpty {
|
||||
fileURL = relativeURL
|
||||
} else {
|
||||
if #available(iOS 9.0, macOS 10.11, tvOS 9.0, *) {
|
||||
fileURL = URL(fileURLWithPath: rpath, relativeTo: relativeURL)
|
||||
} else {
|
||||
fileURL = URL(string: rpath, relativeTo: relativeURL)
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
if let fileURL = fileURL {
|
||||
self.init(fileWithURL: fileURL)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public convenience init?(fileWithURL fileURL: URL) {
|
||||
do {
|
||||
let values = try fileURL.resourceValues(forKeys: [.nameKey, .fileSizeKey, .fileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .fileResourceTypeKey, .isHiddenKey, .isWritableKey, .typeIdentifierKey])
|
||||
self.init(absoluteURL: fileURL, name: values.name ?? fileURL.lastPathComponent, path: fileURL.path)
|
||||
let values = try fileURL.resourceValues(forKeys: [.nameKey, .fileSizeKey, .fileAllocatedSizeKey, .creationDateKey, .contentModificationDateKey, .fileResourceTypeKey, .isHiddenKey, .isWritableKey, .typeIdentifierKey, .generationIdentifierKey])
|
||||
let path = fileURL.relativePath.hasPrefix("/") ? fileURL.relativePath : "/" + fileURL.relativePath
|
||||
|
||||
self.init(url: fileURL, name: values.name ?? fileURL.lastPathComponent, path: path)
|
||||
for (key, value) in values.allValues {
|
||||
self.allValues[key.rawValue] = value
|
||||
}
|
||||
@@ -51,9 +57,17 @@ public final class LocalFileObject: FileObject {
|
||||
allValues[URLResourceKey.fileAllocatedSizeKey.rawValue] = Int(exactly: newValue) ?? Int.max
|
||||
}
|
||||
}
|
||||
|
||||
open var rev: String? {
|
||||
get {
|
||||
let data = allValues[URLResourceKey.generationIdentifierKey.rawValue] as? Data
|
||||
return data?.map { String(format: "%02hhx", $0) }.joined()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal class LocalFolderMonitor {
|
||||
internal final class LocalFolderMonitor {
|
||||
fileprivate let source: DispatchSourceFileSystemObject
|
||||
fileprivate let descriptor: CInt
|
||||
fileprivate let qq: DispatchQueue = DispatchQueue.global(qos: .default)
|
||||
@@ -190,7 +204,7 @@ open class LocalOperationHandle: OperationHandle {
|
||||
public let operationType: FileOperationType
|
||||
|
||||
init (operationType: FileOperationType, baseURL: URL?) {
|
||||
self.baseURL = baseURL ?? LocalFileProvider.defaultBaseURL()
|
||||
self.baseURL = baseURL ?? URL(fileURLWithPath: "/")
|
||||
self.operationType = operationType
|
||||
}
|
||||
|
||||
|
||||
@@ -10,29 +10,32 @@
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
|
||||
// Because this class uses NSURLSession, it's necessary to disable App Transport Security
|
||||
// in case of using this class with unencrypted HTTP connection.
|
||||
|
||||
open class OneDriveFileProvider: NSObject, FileProviderBasicRemote {
|
||||
open static let type: String = "OneDrive"
|
||||
open let isPathRelative: Bool = true
|
||||
open class OneDriveFileProvider: FileProviderBasicRemote {
|
||||
open class var type: String { return "OneDrive" }
|
||||
open let isPathRelative: Bool
|
||||
open let baseURL: URL?
|
||||
/// OneDrive server url, equals with unwrapped `baseURL`
|
||||
open var serverURL: URL { return baseURL! }
|
||||
/// Drive name for user, default is `root`. Changing its value will effect on new operations.
|
||||
open var drive: String
|
||||
/// Generated storage url from server url and drive name
|
||||
open var driveURL: URL {
|
||||
return URL(string: "/drive/root:/", relativeTo: baseURL)!
|
||||
return URL(string: "/drive/\(drive):/", relativeTo: baseURL)!
|
||||
}
|
||||
open var currentPath: String = ""
|
||||
open var currentPath: String
|
||||
|
||||
open var dispatch_queue: DispatchQueue {
|
||||
open var dispatch_queue: DispatchQueue
|
||||
open var operation_queue: OperationQueue {
|
||||
willSet {
|
||||
assert(_session == nil, "It's not effective to change dispatch_queue property after session is initialized.")
|
||||
}
|
||||
}
|
||||
|
||||
open weak var delegate: FileProviderDelegate?
|
||||
open let credential: URLCredential?
|
||||
open private(set) var cache: URLCache?
|
||||
public var useCache: Bool = false
|
||||
public var validatingCache: Bool = true
|
||||
public var useCache: Bool
|
||||
public var validatingCache: Bool
|
||||
|
||||
fileprivate var _session: URLSession?
|
||||
fileprivate var sessionDelegate: SessionDelegate?
|
||||
@@ -49,17 +52,40 @@ open class OneDriveFileProvider: NSObject, FileProviderBasicRemote {
|
||||
return _session!
|
||||
}
|
||||
|
||||
public init? (baseURL: URL?, drive: String = "root", credential: URLCredential?, cache: URLCache? = nil) {
|
||||
self.baseURL = baseURL ?? URL(string: "https://api.onedrive.com")
|
||||
/**
|
||||
Initializer for Onedrive provider with given client ID and Token.
|
||||
These parameters must be retrieved via [Authentication for the OneDrive API](https://dev.onedrive.com/auth/readme.htm).
|
||||
|
||||
There are libraries like [p2/OAuth2](https://github.com/p2/OAuth2) or [OAuthSwift](https://github.com/OAuthSwift/OAuthSwift) which can facilate the procedure to retrieve token.
|
||||
The latter is easier to use and prefered. Also you can use [auth0/Lock](https://github.com/auth0/Lock.iOS-OSX) which provides graphical user interface.
|
||||
|
||||
- Parameters:
|
||||
- credential: a `URLCredential` object with Client ID set as `user` and Token set as `password`.
|
||||
- serverURL: server url, Set it if you are trying to connect OneDrive Business server, otherwise leave it
|
||||
`nil` to connect to OneDrive Personal uses.
|
||||
- drive: drive name for user on server, default value is `root`.
|
||||
- cache: A URLCache to cache downloaded files and contents. If set to nil, URLCache.shared object will be used.
|
||||
*/
|
||||
public init(credential: URLCredential?, serverURL: URL? = nil, drive: String = "root", cache: URLCache? = nil) {
|
||||
self.baseURL = (serverURL ?? URL(string: "https://api.onedrive.com/")!).appendingPathComponent("")
|
||||
self.drive = drive
|
||||
dispatch_queue = DispatchQueue(label: "FileProvider.\(OneDriveFileProvider.type)", attributes: DispatchQueue.Attributes.concurrent)
|
||||
//let url = baseURL.uw_absoluteString
|
||||
self.credential = credential
|
||||
self.isPathRelative = true
|
||||
self.currentPath = ""
|
||||
self.useCache = false
|
||||
self.validatingCache = true
|
||||
self.cache = cache
|
||||
self.credential = credential
|
||||
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: .concurrent)
|
||||
operation_queue = OperationQueue()
|
||||
operation_queue.name = "FileProvider.\(type(of: self).type).Operation"
|
||||
}
|
||||
|
||||
deinit {
|
||||
_session?.invalidateAndCancel()
|
||||
if fileProviderCancelTasksOnInvalidating {
|
||||
_session?.invalidateAndCancel()
|
||||
} else {
|
||||
_session?.finishTasksAndInvalidate()
|
||||
}
|
||||
}
|
||||
|
||||
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
|
||||
@@ -74,16 +100,16 @@ open class OneDriveFileProvider: NSObject, FileProviderBasicRemote {
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var dbError: FileProviderOneDriveError?
|
||||
var serverError: FileProviderOneDriveError?
|
||||
var fileObject: OneDriveFileObject?
|
||||
if let response = response as? HTTPURLResponse {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
|
||||
dbError = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
|
||||
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr), let file = self.mapToFileObject(json) {
|
||||
serverError = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
|
||||
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr), let file = OneDriveFileObject(baseURL: self.baseURL, drive: self.drive, json: json) {
|
||||
fileObject = file
|
||||
}
|
||||
}
|
||||
completionHandler(fileObject, dbError ?? error)
|
||||
completionHandler(fileObject, serverError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
@@ -162,12 +188,12 @@ extension OneDriveFileProvider: FileProviderOperations {
|
||||
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
|
||||
}
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var dbError: FileProviderOneDriveError?
|
||||
var serverError: FileProviderOneDriveError?
|
||||
if let response = response as? HTTPURLResponse, response.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) {
|
||||
dbError = FileProviderOneDriveError(code: code, path: sourcePath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
serverError = FileProviderOneDriveError(code: code, path: sourcePath, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
}
|
||||
completionHandler?(dbError ?? error)
|
||||
self.delegateNotify(operation, error: dbError ?? error)
|
||||
completionHandler?(serverError ?? error)
|
||||
self.delegateNotify(operation, error: serverError ?? error)
|
||||
})
|
||||
task.taskDescription = operation.json
|
||||
task.resume()
|
||||
@@ -195,8 +221,8 @@ extension OneDriveFileProvider: FileProviderOperations {
|
||||
guard let cacheURL = cacheURL, let httpResponse = response as? HTTPURLResponse , httpResponse.statusCode < 300 else {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: (response as? HTTPURLResponse)?.statusCode ?? -1)
|
||||
let errorData : Data? = nil //Data(contentsOf:cacheURL) // TODO: Figure out how to get error response data for the error description
|
||||
let dbError : FileProviderOneDriveError? = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: errorData ?? Data(), encoding: .utf8)) : nil
|
||||
completionHandler?(dbError ?? error)
|
||||
let serverError : FileProviderOneDriveError? = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: errorData ?? Data(), encoding: .utf8)) : nil
|
||||
completionHandler?(serverError ?? error)
|
||||
return
|
||||
}
|
||||
do {
|
||||
@@ -214,23 +240,30 @@ extension OneDriveFileProvider: FileProviderOperations {
|
||||
|
||||
extension OneDriveFileProvider: FileProviderReadWrite {
|
||||
public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
if length == 0 || offset < 0 {
|
||||
dispatch_queue.async {
|
||||
completionHandler(Data(), nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
let opType = FileOperationType.fetch(path: path)
|
||||
let url = URL(string: escaped(path: path) + ":/content", relativeTo: driveURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
if length > 0 {
|
||||
request.setValue("bytes=\(offset)-\(offset + length)", forHTTPHeaderField: "Range")
|
||||
request.setValue("bytes=\(offset)-\(offset + length - 1)", forHTTPHeaderField: "Range")
|
||||
} else if offset > 0 && length < 0 {
|
||||
request.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
|
||||
}
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var dbError: FileProviderOneDriveError?
|
||||
var serverError: FileProviderOneDriveError?
|
||||
if let httpResponse = response as? HTTPURLResponse , httpResponse.statusCode >= 300, let code = FileProviderHTTPErrorCode(rawValue: httpResponse.statusCode) {
|
||||
dbError = FileProviderOneDriveError(code: code, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
serverError = FileProviderOneDriveError(code: code, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8))
|
||||
}
|
||||
let filedata = dbError ?? error == nil ? data : nil
|
||||
completionHandler(filedata, dbError ?? error)
|
||||
let filedata = serverError ?? error == nil ? data : nil
|
||||
completionHandler(filedata, serverError ?? error)
|
||||
})
|
||||
task.taskDescription = opType.json
|
||||
task.resume()
|
||||
@@ -269,7 +302,40 @@ extension OneDriveFileProvider: FileProviderReadWrite {
|
||||
NotImplemented()
|
||||
}
|
||||
|
||||
// TODO: Implement /copy_reference, /get_account & /get_current_account
|
||||
/**
|
||||
Genrates a public url to a file to be shared with other users and can be downloaded without authentication.
|
||||
|
||||
- Parameters:
|
||||
- to: path of file, including file/directory name.
|
||||
- completionHandler: a block with result of directory entries or error.
|
||||
`link`: a url returned by OneDrive to share.
|
||||
`attribute`: `nil` for OneDrive.
|
||||
`expiration`: `nil` for OneDrive, as it doesn't expires.
|
||||
`error`: Error returned by OneDrive.
|
||||
*/
|
||||
open func publicLink(to path: String, completionHandler: @escaping ((_ link: URL?, _ attribute: OneDriveFileObject?, _ expiration: Date?, _ error: Error?) -> Void)) {
|
||||
let url = URL(string: escaped(path: path) + ":/action.createLink", relativeTo: driveURL)!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
let requestDictionary: [String: AnyObject] = ["type": "view" as NSString]
|
||||
request.httpBody = dictionaryToJSON(requestDictionary)?.data(using: .utf8)
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var serverError: FileProviderOneDriveError?
|
||||
var link: URL?
|
||||
if let response = response as? HTTPURLResponse {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
|
||||
serverError = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
|
||||
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr) {
|
||||
if let linkDic = json["link"] as? NSDictionary, let linkStr = linkDic["webUrl"] as? String {
|
||||
link = URL(string: linkStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
completionHandler(link, nil, nil, serverError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -323,17 +389,17 @@ extension OneDriveFileProvider: ExtendedFileProvider {
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(credential?.password ?? "")", forHTTPHeaderField: "Authorization")
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
var dbError: FileProviderOneDriveError?
|
||||
var serverError: FileProviderOneDriveError?
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
if let response = response as? HTTPURLResponse {
|
||||
let code = FileProviderHTTPErrorCode(rawValue: response.statusCode)
|
||||
dbError = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
|
||||
serverError = code != nil ? FileProviderOneDriveError(code: code!, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8)) : nil
|
||||
if let data = data, let jsonStr = String(data: data, encoding: .utf8), let json = jsonToDictionary(jsonStr) {
|
||||
(dic, keys) = self.mapMediaInfo(json)
|
||||
}
|
||||
}
|
||||
completionHandler(dic, keys, dbError ?? error)
|
||||
completionHandler(dic, keys, serverError ?? error)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
@@ -341,7 +407,7 @@ extension OneDriveFileProvider: ExtendedFileProvider {
|
||||
|
||||
extension OneDriveFileProvider: FileProvider {
|
||||
open func copy(with zone: NSZone? = nil) -> Any {
|
||||
let copy = OneDriveFileProvider(baseURL: self.baseURL, drive: self.drive, credential: self.credential, cache: self.cache)!
|
||||
let copy = OneDriveFileProvider(credential: self.credential, serverURL: self.baseURL, drive: self.drive, cache: self.cache)
|
||||
copy.currentPath = self.currentPath
|
||||
copy.delegate = self.delegate
|
||||
copy.fileOperationDelegate = self.fileOperationDelegate
|
||||
|
||||
@@ -19,10 +19,32 @@ public struct FileProviderOneDriveError: Error, CustomStringConvertible {
|
||||
}
|
||||
|
||||
public final class OneDriveFileObject: FileObject {
|
||||
internal init(name: String, path: String) {
|
||||
super.init(absoluteURL: URL(string: path), name: name, path: path)
|
||||
internal init(baseURL: URL?, name: String, path: String) {
|
||||
var rpath = path
|
||||
if path.hasPrefix("/") {
|
||||
rpath.remove(at: rpath.startIndex)
|
||||
}
|
||||
let url = URL(string: rpath, relativeTo: baseURL) ?? URL(string: path)!
|
||||
super.init(url: url, name: name, path: path)
|
||||
}
|
||||
|
||||
internal convenience init? (baseURL: URL?, drive: String, jsonStr: String) {
|
||||
guard let json = jsonToDictionary(jsonStr) else { return nil }
|
||||
self.init(baseURL: baseURL, drive: drive, json: json)
|
||||
}
|
||||
|
||||
internal convenience init? (baseURL: URL?, drive: String, json: [String: AnyObject]) {
|
||||
guard let name = json["name"] as? String else { return nil }
|
||||
guard let path = (json["parentReference"] as? NSDictionary)?["path"] as? String else { return nil }
|
||||
let lPath = path.replacingOccurrences(of: "/drive/\(drive):", with: "/", options: .anchored, range: nil)
|
||||
self.init(baseURL: baseURL, name: name, path: lPath)
|
||||
self.size = (json["size"] as? NSNumber)?.int64Value ?? -1
|
||||
self.modifiedDate = resolve(dateString: json["lastModifiedDateTime"] as? String ?? "")
|
||||
self.creationDate = resolve(dateString: json["createdDateTime"] as? String ?? "")
|
||||
self.type = (json["folder"] as? String) != nil ? .directory : .regular
|
||||
self.id = json["id"] as? String
|
||||
self.entryTag = json["eTag"] as? String
|
||||
}
|
||||
|
||||
open internal(set) var id: String? {
|
||||
get {
|
||||
@@ -74,7 +96,7 @@ internal extension OneDriveFileProvider {
|
||||
let json = jsonToDictionary(jsonStr)
|
||||
if let entries = json?["value"] as? [AnyObject] , entries.count > 0 {
|
||||
for entry in entries {
|
||||
if let entry = entry as? [String: AnyObject], let file = self.mapToFileObject(entry) {
|
||||
if let entry = entry as? [String: AnyObject], let file = OneDriveFileObject(baseURL: self.baseURL, drive: self.drive, json: entry) {
|
||||
files.append(file)
|
||||
}
|
||||
}
|
||||
@@ -93,7 +115,12 @@ internal extension OneDriveFileProvider {
|
||||
}
|
||||
|
||||
func upload_simple(_ targetPath: String, data: Data, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
assert(data.count < 100*1024*1024, "Maximum size of allowed size to upload is 100MB")
|
||||
if data.count > 100 * 1024 * 1024 {
|
||||
let error = FileProviderOneDriveError(code: .payloadTooLarge, path: targetPath, errorDescription: nil)
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(.create(path: targetPath), error: error)
|
||||
return nil
|
||||
}
|
||||
let queryStr = overwrite ? "" : "?@name.conflictBehavior=fail"
|
||||
let url = URL(string: escaped(path: targetPath) + ":/content" + queryStr, relativeTo: driveURL)!
|
||||
var request = URLRequest(url: url)
|
||||
@@ -115,6 +142,13 @@ internal extension OneDriveFileProvider {
|
||||
}
|
||||
|
||||
func upload_simple(_ targetPath: String, localFile: URL, modifiedDate: Date = Date(), overwrite: Bool, operation: FileOperationType, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let size = (try? localFile.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? -1
|
||||
if size > 100 * 1024 * 1024 {
|
||||
let error = FileProviderOneDriveError(code: .payloadTooLarge, path: targetPath, errorDescription: nil)
|
||||
completionHandler?(error)
|
||||
self.delegateNotify(.create(path: targetPath), error: error)
|
||||
return nil
|
||||
}
|
||||
let queryStr = overwrite ? "" : "?@name.conflictBehavior=fail"
|
||||
let url = URL(string: escaped(path: targetPath) + ":/content" + queryStr, relativeTo: driveURL)!
|
||||
var request = URLRequest(url: url)
|
||||
@@ -156,7 +190,7 @@ internal extension OneDriveFileProvider {
|
||||
let json = jsonToDictionary(jsonStr)
|
||||
if let entries = json?["value"] as? [AnyObject] , entries.count > 0 {
|
||||
for entry in entries {
|
||||
if let entry = entry as? [String: AnyObject], let file = self.mapToFileObject(entry) {
|
||||
if let entry = entry as? [String: AnyObject], let file = OneDriveFileObject(baseURL: self.baseURL, drive: self.drive, json: entry) {
|
||||
foundItem(file)
|
||||
}
|
||||
}
|
||||
@@ -178,28 +212,6 @@ internal extension OneDriveFileProvider {
|
||||
// codebeat:enable[ARITY]
|
||||
|
||||
internal extension OneDriveFileProvider {
|
||||
func mapToFileObject(_ jsonStr: String) -> OneDriveFileObject? {
|
||||
guard let json = jsonToDictionary(jsonStr) else { return nil }
|
||||
return self.mapToFileObject(json)
|
||||
}
|
||||
|
||||
func mapToFileObject(_ json: [String: AnyObject]) -> OneDriveFileObject? {
|
||||
guard let name = json["name"] as? String else { return nil }
|
||||
guard let path = (json["parentReference"] as? NSDictionary)?["path"] as? String else { return nil }
|
||||
let lPath = path.replacingOccurrences(of: "/drive/\(drive):", with: "/", options: .anchored, range: nil)
|
||||
let fileObject = OneDriveFileObject(name: name, path: lPath)
|
||||
if let webURL = json["webUrl"] as? String, let absolluteURL = URL(string: webURL) {
|
||||
fileObject.absoluteURL = absolluteURL
|
||||
}
|
||||
fileObject.size = (json["size"] as? NSNumber)?.int64Value ?? -1
|
||||
fileObject.modifiedDate = resolve(dateString: json["lastModifiedDateTime"] as? String ?? "")
|
||||
fileObject.creationDate = resolve(dateString: json["createdDateTime"] as? String ?? "")
|
||||
fileObject.type = (json["folder"] as? String) != nil ? .directory : .regular
|
||||
fileObject.id = json["id"] as? String
|
||||
fileObject.entryTag = json["eTag"] as? String
|
||||
return fileObject
|
||||
}
|
||||
|
||||
static let dateFormatter = DateFormatter()
|
||||
static let decimalFormatter = NumberFormatter()
|
||||
|
||||
@@ -223,72 +235,51 @@ internal extension OneDriveFileProvider {
|
||||
var dic = [String: Any]()
|
||||
var keys = [String]()
|
||||
|
||||
func add(key: String, value: Any?) {
|
||||
if let value = value {
|
||||
keys.append(key)
|
||||
dic[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if let parent = json["image"] as? [String: Any] ?? json["video"] as? [String: Any], let height = parent["height"] as? UInt64, let width = parent["width"] as? UInt64 {
|
||||
keys.append("Dimensions")
|
||||
dic["Dimensions"] = "\(width)x\(height)"
|
||||
add(key: "Dimensions", value: "\(width)x\(height)")
|
||||
}
|
||||
if let location = json["location"] as? [String: Any], let latitude = location["latitude"] as? Double, let longitude = location["longitude"] as? Double {
|
||||
|
||||
OneDriveFileProvider.decimalFormatter.numberStyle = .decimal
|
||||
OneDriveFileProvider.decimalFormatter.maximumFractionDigits = 5
|
||||
keys.append("Location")
|
||||
let latStr = OneDriveFileProvider.decimalFormatter.string(from: NSNumber(value: latitude))
|
||||
let longStr = OneDriveFileProvider.decimalFormatter.string(from: NSNumber(value: longitude))
|
||||
dic["Location"] = "\(latStr), \(longStr)"
|
||||
add(key: "Location", value: "\(latStr), \(longStr)")
|
||||
}
|
||||
if let parent = json["image"] as? [String: Any] ?? json["video"] as? [String: Any], let duration = parent["duration"] as? UInt64 {
|
||||
keys.append("Duration")
|
||||
dic["Duration"] = OneDriveFileProvider.formatshort(interval: TimeInterval(duration) / 1000)
|
||||
add(key: "Duration", value: OneDriveFileProvider.formatshort(interval: TimeInterval(duration) / 1000))
|
||||
}
|
||||
if let timeTakenStr = json["takenDateTime"] as? String, let timeTaken = self.resolve(dateString: timeTakenStr) {
|
||||
keys.append("Date taken")
|
||||
if let timeTakenStr = json["takenDateTime"] as? String, let timeTaken = resolve(dateString: timeTakenStr) {
|
||||
OneDriveFileProvider.dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
dic["Date taken"] = OneDriveFileProvider.dateFormatter.string(from: timeTaken)
|
||||
add(key: "Date taken", value: OneDriveFileProvider.dateFormatter.string(from: timeTaken))
|
||||
}
|
||||
|
||||
if let photo = json["photo"] as? [String: Any] {
|
||||
if let devicemake = photo["cameraMake"] as? String {
|
||||
keys.append("Device make")
|
||||
dic["Device make"] = devicemake
|
||||
}
|
||||
if let devicemodel = photo["cameraModel"] as? String {
|
||||
keys.append("Device model")
|
||||
dic["Device model"] = devicemodel
|
||||
}
|
||||
if let focallen = photo["focalLength"] as? Double {
|
||||
keys.append("Focal length")
|
||||
dic["Focal length"] = focallen
|
||||
}
|
||||
if let fnum = photo["fNumber"] as? Double {
|
||||
keys.append("F number")
|
||||
dic["F number"] = fnum
|
||||
}
|
||||
add(key: "Device make", value: photo["cameraMake"] as? String)
|
||||
add(key: "Device model", value: photo["cameraModel"] as? String)
|
||||
add(key: "focalLength", value: photo["focalLength"] as? Double)
|
||||
add(key: "fNumber", value: photo["fNumber"] as? Double)
|
||||
if let expNom = photo["exposureNumerator"] as? Double, let expDen = photo["exposureDenominator"] as? Double {
|
||||
keys.append("Exposure time")
|
||||
dic["Exposure time"] = "\(Int(expNom))/\(Int(expDen))"
|
||||
add(key: "Exposure time", value: "\(Int(expNom))/\(Int(expDen))")
|
||||
}
|
||||
if let iso = photo["iso"] as? Int64 {
|
||||
keys.append("ISO speed")
|
||||
dic["ISO speed"] = iso
|
||||
}
|
||||
|
||||
add(key: "ISO speed", value: photo["iso"] as? Int64)
|
||||
}
|
||||
|
||||
if let audio = json["audio"] as? [String: Any] {
|
||||
for (key, value) in audio {
|
||||
if key == "bitrate" || key == "isVariableBitrate" { continue }
|
||||
let casedKey = spaceCamelCase(key)
|
||||
keys.append(casedKey)
|
||||
dic[casedKey] = value
|
||||
add(key: casedKey, value: value)
|
||||
}
|
||||
}
|
||||
|
||||
if let video = json["video"] as? [String: Any] {
|
||||
if let bitRate = video["bitrate"] as? Int {
|
||||
keys.append("Bitrate")
|
||||
dic["Bitrate"] = bitRate
|
||||
}
|
||||
}
|
||||
add(key: "Bitrate", value: (json["video"] as? NSDictionary)?["bitrate"] as? Int)
|
||||
|
||||
return (dic, keys)
|
||||
}
|
||||
|
||||
@@ -9,11 +9,12 @@
|
||||
import Foundation
|
||||
|
||||
class SMBFileProvider: FileProvider, FileProviderMonitor {
|
||||
open static var type: String = "Samba"
|
||||
open class var type: String { return "SMB" }
|
||||
open var isPathRelative: Bool = true
|
||||
open var baseURL: URL?
|
||||
open var currentPath: String = ""
|
||||
open var dispatch_queue: DispatchQueue
|
||||
open var operation_queue: OperationQueue
|
||||
open weak var delegate: FileProviderDelegate?
|
||||
open let credential: URLCredential?
|
||||
|
||||
@@ -23,9 +24,11 @@ class SMBFileProvider: FileProvider, FileProviderMonitor {
|
||||
guard baseURL.uw_scheme.lowercased() == "smb" else {
|
||||
return nil
|
||||
}
|
||||
self.baseURL = baseURL
|
||||
dispatch_queue = DispatchQueue(label: "FileProvider.\(SMBFileProvider.type)", attributes: DispatchQueue.Attributes.concurrent)
|
||||
//let url = baseURL.uw_absoluteString
|
||||
self.baseURL = baseURL.appendingPathComponent("")
|
||||
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: .concurrent)
|
||||
operation_queue = OperationQueue()
|
||||
operation_queue.name = "FileProvider.\(type(of: self).type).Operation"
|
||||
|
||||
self.credential = credential
|
||||
}
|
||||
|
||||
|
||||
+178
-133
@@ -8,48 +8,27 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public final class WebDavFileObject: FileObject {
|
||||
internal init(absoluteURL: URL, name: String, path: String) {
|
||||
super.init(absoluteURL: absoluteURL, name: name, path: path)
|
||||
}
|
||||
|
||||
open internal(set) var contentType: String {
|
||||
get {
|
||||
return allValues["NSURLContentTypeKey"] as? String ?? ""
|
||||
}
|
||||
set {
|
||||
allValues["NSURLContentTypeKey"] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
open internal(set) var entryTag: String? {
|
||||
get {
|
||||
return allValues["NSURLEntryTagKey"] as? String
|
||||
}
|
||||
set {
|
||||
allValues["NSURLEntryTagKey"] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Because this class uses NSURLSession, it's necessary to disable App Transport Security
|
||||
/// in case of using this class with unencrypted HTTP connection.
|
||||
|
||||
open class WebDAVFileProvider: NSObject, FileProviderBasicRemote {
|
||||
open static let type: String = "WebDAV"
|
||||
open let isPathRelative: Bool = true
|
||||
open class WebDAVFileProvider: FileProviderBasicRemote {
|
||||
open class var type: String { return "WebDAV" }
|
||||
open let isPathRelative: Bool
|
||||
open let baseURL: URL?
|
||||
open var currentPath: String = ""
|
||||
public var dispatch_queue: DispatchQueue {
|
||||
open var currentPath: String
|
||||
|
||||
open var dispatch_queue: DispatchQueue
|
||||
open var operation_queue: OperationQueue {
|
||||
willSet {
|
||||
assert(_session == nil, "It's not effective to change dispatch_queue property after session is initialized.")
|
||||
}
|
||||
}
|
||||
|
||||
public weak var delegate: FileProviderDelegate?
|
||||
open let credential: URLCredential?
|
||||
open private(set) var cache: URLCache?
|
||||
public var useCache: Bool = false
|
||||
public var validatingCache: Bool = true
|
||||
public var useCache: Bool
|
||||
public var validatingCache: Bool
|
||||
|
||||
fileprivate var _session: URLSession?
|
||||
fileprivate var sessionDelegate: SessionDelegate?
|
||||
@@ -66,24 +45,41 @@ open class WebDAVFileProvider: NSObject, FileProviderBasicRemote {
|
||||
return _session!
|
||||
}
|
||||
|
||||
/**
|
||||
Initializes WebDAV provider.
|
||||
|
||||
- Parameters:
|
||||
- baseURL: Location of WebDAV server.
|
||||
- credential: An `URLCredential` object with `user` and `password`.
|
||||
- cache: A URLCache to cache downloaded files and contents. If set to nil, URLCache.shared object will be used.
|
||||
*/
|
||||
public init? (baseURL: URL, credential: URLCredential?, cache: URLCache? = nil) {
|
||||
if !["http", "https"].contains(baseURL.uw_scheme.lowercased()) {
|
||||
return nil
|
||||
}
|
||||
self.baseURL = baseURL
|
||||
dispatch_queue = DispatchQueue(label: "FileProvider.\(WebDAVFileProvider.type)", attributes: DispatchQueue.Attributes.concurrent)
|
||||
//let url = baseURL.uw_absoluteString
|
||||
self.credential = credential
|
||||
self.baseURL = baseURL.appendingPathComponent("")
|
||||
self.isPathRelative = true
|
||||
self.currentPath = ""
|
||||
self.useCache = false
|
||||
self.validatingCache = true
|
||||
self.cache = cache
|
||||
self.credential = credential
|
||||
dispatch_queue = DispatchQueue(label: "FileProvider.\(type(of: self).type)", attributes: .concurrent)
|
||||
operation_queue = OperationQueue()
|
||||
operation_queue.name = "FileProvider.\(type(of: self).type).Operation"
|
||||
}
|
||||
|
||||
deinit {
|
||||
_session?.invalidateAndCancel()
|
||||
if fileProviderCancelTasksOnInvalidating {
|
||||
_session?.invalidateAndCancel()
|
||||
} else {
|
||||
_session?.finishTasksAndInvalidate()
|
||||
}
|
||||
}
|
||||
|
||||
open func contentsOfDirectory(path: String, completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) {
|
||||
let opType = FileOperationType.fetch(path: path)
|
||||
let url = absoluteURL(path)
|
||||
let url = self.url(of: path).appendingPathComponent("")
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PROPFIND"
|
||||
request.setValue("1", forHTTPHeaderField: "Depth")
|
||||
@@ -97,12 +93,12 @@ open class WebDAVFileProvider: NSObject, FileProviderBasicRemote {
|
||||
}
|
||||
var fileObjects = [WebDavFileObject]()
|
||||
if let data = data {
|
||||
let xresponse = self.parseXMLResponse(data)
|
||||
for attr in xresponse {
|
||||
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
|
||||
for attr in xresponse where attr.href != url {
|
||||
if attr.href.path == url.path {
|
||||
continue
|
||||
}
|
||||
fileObjects.append(self.mapToFileObject(attr))
|
||||
fileObjects.append(WebDavFileObject(attr))
|
||||
}
|
||||
}
|
||||
completionHandler(fileObjects, responseError ?? error)
|
||||
@@ -110,7 +106,7 @@ open class WebDAVFileProvider: NSObject, FileProviderBasicRemote {
|
||||
}
|
||||
|
||||
open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) {
|
||||
let url = absoluteURL(path)
|
||||
let url = self.url(of: path)
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PROPFIND"
|
||||
request.setValue("1", forHTTPHeaderField: "Depth")
|
||||
@@ -123,9 +119,9 @@ open class WebDAVFileProvider: NSObject, FileProviderBasicRemote {
|
||||
responseError = FileProviderWebDavError(code: rCode, url: url)
|
||||
}
|
||||
if let data = data {
|
||||
let xresponse = self.parseXMLResponse(data)
|
||||
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
|
||||
if let attr = xresponse.first {
|
||||
completionHandler(self.mapToFileObject(attr), responseError ?? error)
|
||||
completionHandler(WebDavFileObject(attr), responseError ?? error)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -150,7 +146,7 @@ open class WebDAVFileProvider: NSObject, FileProviderBasicRemote {
|
||||
var totalSize: Int64 = -1
|
||||
var usedSize: Int64 = 0
|
||||
if let data = data {
|
||||
let xresponse = self.parseXMLResponse(data)
|
||||
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
|
||||
if let attr = xresponse.first {
|
||||
totalSize = Int64(attr.prop["quota-available-bytes"] ?? "") ?? -1
|
||||
usedSize = Int64(attr.prop["quota-used-bytes"] ?? "") ?? 0
|
||||
@@ -170,7 +166,7 @@ extension WebDAVFileProvider: FileProviderOperations {
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
let url = absoluteURL((atPath as NSString).appendingPathComponent(folderName) + "/")
|
||||
let url = self.url(of: atPath).appendingPathComponent(folderName, isDirectory: true)
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "MKCOL"
|
||||
let task = session.dataTask(with: request, completionHandler: { (data, response, error) in
|
||||
@@ -192,7 +188,7 @@ extension WebDAVFileProvider: FileProviderOperations {
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
let url = absoluteURL(path).appendingPathComponent(fileName)
|
||||
let url = self.url(of: path).appendingPathComponent(fileName)
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PUT"
|
||||
let task = session.uploadTask(with: request, from: data, completionHandler: { (data, response, error) in
|
||||
@@ -236,10 +232,10 @@ extension WebDAVFileProvider: FileProviderOperations {
|
||||
}
|
||||
|
||||
func doOperation(operation opType: FileOperationType, overwrite: Bool? = nil, completionHandler: SimpleCompletionHandler) -> OperationHandle? {
|
||||
let sourceURL = absoluteURL(opType.source!)
|
||||
let sourceURL = self.url(of:opType.source!)
|
||||
var request = URLRequest(url: sourceURL)
|
||||
if let dest = opType.destination {
|
||||
request.setValue(absoluteURL(dest).absoluteString, forHTTPHeaderField: "Destination")
|
||||
request.setValue(url(of:dest).absoluteString, forHTTPHeaderField: "Destination")
|
||||
}
|
||||
switch opType {
|
||||
case .copy:
|
||||
@@ -262,7 +258,7 @@ extension WebDAVFileProvider: FileProviderOperations {
|
||||
responseError = FileProviderWebDavError(code: code, url: sourceURL)
|
||||
}
|
||||
if code == .multiStatus, let data = data {
|
||||
let xresponses = self.parseXMLResponse(data)
|
||||
let xresponses = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
|
||||
for xresponse in xresponses where (xresponse.status ?? 0) >= 300 {
|
||||
completionHandler?(FileProviderWebDavError(code: code, url: sourceURL))
|
||||
}
|
||||
@@ -286,7 +282,7 @@ extension WebDAVFileProvider: FileProviderOperations {
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
let url = absoluteURL(toPath)
|
||||
let url = self.url(of:toPath)
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PUT"
|
||||
let task = session.uploadTask(with: request, fromFile: localFile, completionHandler: { (data, response, error) in
|
||||
@@ -308,7 +304,7 @@ extension WebDAVFileProvider: FileProviderOperations {
|
||||
guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else {
|
||||
return nil
|
||||
}
|
||||
let url = absoluteURL(path)
|
||||
let url = self.url(of:path)
|
||||
let request = URLRequest(url: url)
|
||||
let task = session.downloadTask(with: request, completionHandler: { (sourceFileURL, response, error) in
|
||||
var responseError: FileProviderWebDavError?
|
||||
@@ -335,12 +331,19 @@ extension WebDAVFileProvider: FileProviderOperations {
|
||||
extension WebDAVFileProvider: FileProviderReadWrite {
|
||||
@discardableResult
|
||||
public func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? {
|
||||
if length == 0 || offset < 0 {
|
||||
dispatch_queue.async {
|
||||
completionHandler(Data(), nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
let opType = FileOperationType.fetch(path: path)
|
||||
let url = absoluteURL(path)
|
||||
let url = self.url(of: path)
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
if length > 0 {
|
||||
request.setValue("bytes=\(offset)-\(offset + length)", forHTTPHeaderField: "Range")
|
||||
request.setValue("bytes=\(offset)-\(offset + length - 1)", forHTTPHeaderField: "Range")
|
||||
} else if offset > 0 && length < 0 {
|
||||
request.setValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
|
||||
}
|
||||
@@ -362,7 +365,7 @@ extension WebDAVFileProvider: FileProviderReadWrite {
|
||||
return nil
|
||||
}
|
||||
// FIXME: lock destination before writing process
|
||||
let url = atomically ? absoluteURL(path).appendingPathExtension("tmp") : absoluteURL(path)
|
||||
let url = atomically ? self.url(of: path).appendingPathExtension("tmp") : self.url(of: path)
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PUT"
|
||||
if !overwrite {
|
||||
@@ -371,7 +374,7 @@ extension WebDAVFileProvider: FileProviderReadWrite {
|
||||
let task = session.uploadTask(with: request, from: data, completionHandler: { (data, response, error) in
|
||||
var responseError: FileProviderWebDavError?
|
||||
if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) {
|
||||
responseError = FileProviderWebDavError(code: rCode, url: self.absoluteURL(path))
|
||||
responseError = FileProviderWebDavError(code: rCode, url: self.url(of: path))
|
||||
}
|
||||
defer {
|
||||
self.delegateNotify(opType, error: responseError ?? error)
|
||||
@@ -390,7 +393,7 @@ extension WebDAVFileProvider: FileProviderReadWrite {
|
||||
}
|
||||
|
||||
public func searchFiles(path: String, recursive: Bool, query: String, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) {
|
||||
let url = absoluteURL(path)
|
||||
let url = self.url(of: path)
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PROPFIND"
|
||||
//request.setValue("1", forHTTPHeaderField: "Depth")
|
||||
@@ -403,14 +406,14 @@ extension WebDAVFileProvider: FileProviderReadWrite {
|
||||
responseError = FileProviderWebDavError(code: rCode, url: url)
|
||||
}
|
||||
if let data = data {
|
||||
let xresponse = self.parseXMLResponse(data)
|
||||
let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL)
|
||||
var fileObjects = [WebDavFileObject]()
|
||||
for attr in xresponse {
|
||||
let path = attr.href.path
|
||||
if !((path as NSString).lastPathComponent.contains(query)) {
|
||||
continue
|
||||
}
|
||||
let fileObject = self.mapToFileObject(attr)
|
||||
let fileObject = WebDavFileObject(attr)
|
||||
fileObjects.append(fileObject)
|
||||
foundItemHandler?(fileObject)
|
||||
}
|
||||
@@ -428,9 +431,10 @@ extension WebDAVFileProvider: FileProviderReadWrite {
|
||||
* A messy approach is listing a directory with an interval period and compare
|
||||
* with previous results
|
||||
*/
|
||||
NotImplemented()
|
||||
}
|
||||
fileprivate func unregisterNotifcation(path: String) {
|
||||
|
||||
NotImplemented()
|
||||
}
|
||||
// TODO: implements methods for lock mechanism
|
||||
}
|
||||
@@ -450,37 +454,34 @@ extension WebDAVFileProvider: FileProvider {
|
||||
// MARK: WEBDAV XML response implementation
|
||||
|
||||
internal extension WebDAVFileProvider {
|
||||
struct DavResponse {
|
||||
let href: URL
|
||||
let hrefString: String
|
||||
let status: Int?
|
||||
let prop: [String: String]
|
||||
fileprivate func delegateNotify(_ operation: FileOperationType, error: Error?) {
|
||||
DispatchQueue.main.async(execute: {
|
||||
if error == nil {
|
||||
self.delegate?.fileproviderSucceed(self, operation: operation)
|
||||
} else {
|
||||
self.delegate?.fileproviderFailed(self, operation: operation)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct DavResponse {
|
||||
let href: URL
|
||||
let hrefString: String
|
||||
let status: Int?
|
||||
let prop: [String: String]
|
||||
|
||||
fileprivate func parseXMLResponse(_ response: Data) -> [DavResponse] {
|
||||
var result = [DavResponse]()
|
||||
do {
|
||||
let xml = try AEXMLDocument(xml: response)
|
||||
var rootnode = xml.root
|
||||
var responsetag = "response"
|
||||
for node in rootnode.all ?? [] where node.name.lowercased().hasSuffix("multistatus") {
|
||||
rootnode = node
|
||||
init? (_ node: AEXMLElement, baseURL: URL?) {
|
||||
|
||||
func removeSlash(_ str: String) -> String {
|
||||
if str.hasPrefix("/") {
|
||||
return str.substring(from: str.index(after: str.startIndex))
|
||||
} else {
|
||||
return str
|
||||
}
|
||||
for node in rootnode.children where node.name.lowercased().hasSuffix("response") {
|
||||
responsetag = node.name
|
||||
break
|
||||
}
|
||||
for responseNode in rootnode[responsetag].all ?? [] {
|
||||
if let davResponse = mapNodeToDavResponse(responseNode) {
|
||||
result.append(davResponse)
|
||||
}
|
||||
}
|
||||
} catch _ {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fileprivate func mapNodeToDavResponse(_ node: AEXMLElement) -> DavResponse? {
|
||||
|
||||
// find node names with namespace
|
||||
var hreftag = "href"
|
||||
var statustag = "status"
|
||||
var propstattag = "propstat"
|
||||
@@ -495,63 +496,107 @@ internal extension WebDAVFileProvider {
|
||||
propstattag = node.name
|
||||
}
|
||||
}
|
||||
let href = node[hreftag].value
|
||||
if let href = href, let hrefURL = URL(string: href) {
|
||||
var status: Int?
|
||||
let statusDesc = (node[statustag].string).components(separatedBy: " ")
|
||||
if statusDesc.count > 2 {
|
||||
status = Int(statusDesc[1])
|
||||
|
||||
guard let hrefString = node[hreftag].value else { return nil }
|
||||
|
||||
// trying to figure out relative path out of href
|
||||
let hrefAbsolute = URL(string: hrefString, relativeTo: baseURL)?.absoluteString ?? hrefString
|
||||
let relativePath = hrefAbsolute.replacingOccurrences(of: baseURL?.absoluteString ?? "", with: "", options: .anchored, range: nil)
|
||||
let hrefURL = URL(string: removeSlash(relativePath), relativeTo: baseURL) ?? baseURL
|
||||
|
||||
guard let href = hrefURL?.standardized else { return nil }
|
||||
|
||||
// reading status and properties
|
||||
var status: Int?
|
||||
let statusDesc = (node[statustag].string).components(separatedBy: " ")
|
||||
if statusDesc.count > 2 {
|
||||
status = Int(statusDesc[1])
|
||||
}
|
||||
var propDic = [String: String]()
|
||||
let propStatNode = node[propstattag]
|
||||
for node in propStatNode.children where node.name.lowercased().hasSuffix("status"){
|
||||
statustag = node.name
|
||||
break
|
||||
}
|
||||
let statusDesc2 = (propStatNode[statustag].string).components(separatedBy: " ")
|
||||
if statusDesc2.count > 2 {
|
||||
status = Int(statusDesc2[1])
|
||||
}
|
||||
var proptag = "prop"
|
||||
for tnode in propStatNode.children where tnode.name.lowercased().hasSuffix("prop") {
|
||||
proptag = tnode.name
|
||||
break
|
||||
}
|
||||
for propItemNode in propStatNode[proptag].children {
|
||||
propDic[propItemNode.name.components(separatedBy: ":").last!.lowercased()] = propItemNode.value
|
||||
if propItemNode.name.hasSuffix("resourcetype") && propItemNode.xml.contains("collection") {
|
||||
propDic["getcontenttype"] = "httpd/unix-directory"
|
||||
}
|
||||
var propDic = [String: String]()
|
||||
let propStatNode = node[propstattag]
|
||||
for node in propStatNode.children where node.name.lowercased().hasSuffix("status"){
|
||||
statustag = node.name
|
||||
}
|
||||
self.href = href
|
||||
self.hrefString = hrefString
|
||||
self.status = status
|
||||
self.prop = propDic
|
||||
}
|
||||
|
||||
static func parse(xmlResponse: Data, baseURL: URL?) -> [DavResponse] {
|
||||
var result = [DavResponse]()
|
||||
do {
|
||||
let xml = try AEXMLDocument(xml: xmlResponse)
|
||||
var rootnode = xml.root
|
||||
var responsetag = "response"
|
||||
for node in rootnode.all ?? [] where node.name.lowercased().hasSuffix("multistatus") {
|
||||
rootnode = node
|
||||
}
|
||||
for node in rootnode.children where node.name.lowercased().hasSuffix("response") {
|
||||
responsetag = node.name
|
||||
break
|
||||
}
|
||||
let statusDesc2 = (propStatNode[statustag].string).components(separatedBy: " ")
|
||||
if statusDesc2.count > 2 {
|
||||
status = Int(statusDesc2[1])
|
||||
}
|
||||
var proptag = "prop"
|
||||
for tnode in propStatNode.children where tnode.name.lowercased().hasSuffix("prop") {
|
||||
proptag = tnode.name
|
||||
break
|
||||
}
|
||||
for propItemNode in propStatNode[proptag].children {
|
||||
propDic[propItemNode.name.components(separatedBy: ":").last!.lowercased()] = propItemNode.value
|
||||
if propItemNode.name.hasSuffix("resourcetype") && propItemNode.xml.contains("collection") {
|
||||
propDic["getcontenttype"] = "httpd/unix-directory"
|
||||
for responseNode in rootnode[responsetag].all ?? [] {
|
||||
if let davResponse = DavResponse(responseNode, baseURL: baseURL) {
|
||||
result.append(davResponse)
|
||||
}
|
||||
}
|
||||
return DavResponse(href: hrefURL, hrefString: href, status: status, prop: propDic)
|
||||
} catch _ {
|
||||
}
|
||||
return nil
|
||||
return result
|
||||
}
|
||||
|
||||
fileprivate func mapToFileObject(_ davResponse: DavResponse) -> WebDavFileObject {
|
||||
var href = davResponse.href
|
||||
if href.baseURL == nil {
|
||||
href = absoluteURL(href.path)
|
||||
}
|
||||
}
|
||||
|
||||
public final class WebDavFileObject: FileObject {
|
||||
internal init(_ davResponse: DavResponse) {
|
||||
let href = davResponse.href
|
||||
let name = davResponse.prop["displayname"] ?? (davResponse.hrefString.removingPercentEncoding! as NSString).lastPathComponent
|
||||
let fileObject = WebDavFileObject(absoluteURL: href, name: name, path: href.path)
|
||||
fileObject.size = Int64(davResponse.prop["getcontentlength"] ?? "-1") ?? NSURLSessionTransferSizeUnknown
|
||||
fileObject.creationDate = self.resolve(dateString: davResponse.prop["creationdate"] ?? "")
|
||||
fileObject.modifiedDate = self.resolve(dateString: davResponse.prop["getlastmodified"] ?? "")
|
||||
fileObject.contentType = davResponse.prop["getcontenttype"] ?? "octet/stream"
|
||||
fileObject.type = fileObject.contentType == "httpd/unix-directory" ? .directory : .regular
|
||||
fileObject.entryTag = davResponse.prop["getetag"]
|
||||
return fileObject
|
||||
let relativePath = href.relativePath
|
||||
let path = relativePath.hasPrefix("/") ? relativePath : ("/" + relativePath)
|
||||
super.init(url: href, name: name, path: path)
|
||||
self.size = Int64(davResponse.prop["getcontentlength"] ?? "-1") ?? NSURLSessionTransferSizeUnknown
|
||||
self.creationDate = resolve(dateString: davResponse.prop["creationdate"] ?? "")
|
||||
self.modifiedDate = resolve(dateString: davResponse.prop["getlastmodified"] ?? "")
|
||||
self.contentType = davResponse.prop["getcontenttype"] ?? "octet/stream"
|
||||
self.isHidden = (Int(davResponse.prop["ishidden"] ?? "0") ?? 0) > 0
|
||||
self.type = self.contentType == "httpd/unix-directory" ? .directory : .regular
|
||||
self.entryTag = davResponse.prop["getetag"]
|
||||
}
|
||||
|
||||
fileprivate func delegateNotify(_ operation: FileOperationType, error: Error?) {
|
||||
DispatchQueue.main.async(execute: {
|
||||
if error == nil {
|
||||
self.delegate?.fileproviderSucceed(self, operation: operation)
|
||||
} else {
|
||||
self.delegate?.fileproviderFailed(self, operation: operation)
|
||||
}
|
||||
})
|
||||
/// MIME type of the file
|
||||
open internal(set) var contentType: String {
|
||||
get {
|
||||
return allValues["NSURLContentTypeKey"] as? String ?? ""
|
||||
}
|
||||
set {
|
||||
allValues["NSURLContentTypeKey"] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP E-Tag, can be used to mark changed files
|
||||
open internal(set) var entryTag: String? {
|
||||
get {
|
||||
return allValues["NSURLEntryTagKey"] as? String
|
||||
}
|
||||
set {
|
||||
allValues["NSURLEntryTagKey"] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user