// // WebDAVFileProvider.swift // FileProvider // // Created by Amir Abbas Mousavian. // Copyright © 2016 Mousavian. Distributed under MIT license. // import Foundation import CoreGraphics /** Allows accessing to WebDAV server files. This provider doesn't cache or save files internally, however you can set `useCache` and `cache` properties to use Foundation `NSURLCache` system. WebDAV system supported by many cloud services including [Box.com](https://www.box.com/home) and [Yandex disk](https://disk.yandex.com) and [ownCloud](https://owncloud.org). - Important: Because this class uses `URLSession`, it's necessary to disable App Transport Security in case of using this class with unencrypted HTTP connection. [Read this to know how](http://iosdevtips.co/post/121756573323/ios-9-xcode-7-http-connect-server-error). */ open class WebDAVFileProvider: FileProviderBasicRemote { open class var type: String { return "WebDAV" } open let baseURL: URL? 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? public var credentialType: HTTPAuthenticationType = .digest open var credential: URLCredential? { didSet { sessionDelegate?.credential = credential } } open private(set) var cache: URLCache? public var useCache: Bool public var validatingCache: Bool fileprivate var _session: URLSession? fileprivate var sessionDelegate: SessionDelegate? public var session: URLSession { get { if _session == nil { self.sessionDelegate = SessionDelegate(fileProvider: self) let queue = OperationQueue() //queue.underlyingQueue = dispatch_queue let config = URLSessionConfiguration.default config.urlCache = cache config.requestCachePolicy = .returnCacheDataElseLoad _session = URLSession(configuration: config, delegate: sessionDelegate as URLSessionDownloadDelegate?, delegateQueue: queue) _session?.sessionDescription = UUID().uuidString initEmptySessionHandler(_session!.sessionDescription!) } return _session! } set { assert(newValue.delegate is SessionDelegate, "session instances should have a SessionDelegate instance as delegate.") _session = newValue if session.sessionDescription?.isEmpty ?? true { _session?.sessionDescription = UUID().uuidString } self.sessionDelegate = newValue.delegate as? SessionDelegate initEmptySessionHandler(_session!.sessionDescription!) } } /** Initializes WebDAV provider. - Parameters: - baseURL: Location of WebDAV server. - credential: An `URLCredential` object with `user` and `password`. - cache: A URLCache to cache downloaded files and contents. */ public init? (baseURL: URL, credential: URLCredential?, cache: URLCache? = nil) { if !["http", "https"].contains(baseURL.uw_scheme.lowercased()) { return nil } self.baseURL = (baseURL.absoluteString.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("")).absoluteURL 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" } public required convenience init?(coder aDecoder: NSCoder) { guard let baseURL = aDecoder.decodeObject(forKey: "baseURL") as? URL else { return nil } self.init(baseURL: baseURL, credential: aDecoder.decodeObject(forKey: "credential") as? URLCredential) self.currentPath = aDecoder.decodeObject(forKey: "currentPath") as? String ?? "" self.useCache = aDecoder.decodeBool(forKey: "useCache") self.validatingCache = aDecoder.decodeBool(forKey: "validatingCache") } open func encode(with aCoder: NSCoder) { aCoder.encode(self.baseURL, forKey: "baseURL") aCoder.encode(self.credential, forKey: "credential") aCoder.encode(self.currentPath, forKey: "currentPath") aCoder.encode(self.useCache, forKey: "useCache") aCoder.encode(self.validatingCache, forKey: "validatingCache") } public static var supportsSecureCoding: Bool { return true } open func copy(with zone: NSZone? = nil) -> Any { let copy = WebDAVFileProvider(baseURL: self.baseURL!, credential: self.credential, cache: self.cache)! copy.currentPath = self.currentPath copy.delegate = self.delegate copy.fileOperationDelegate = self.fileOperationDelegate copy.useCache = self.useCache copy.validatingCache = self.validatingCache return copy } deinit { if let sessionuuid = _session?.sessionDescription { removeSessionHandler(for: sessionuuid) } if fileProviderCancelTasksOnInvalidating { _session?.invalidateAndCancel() } else { _session?.finishTasksAndInvalidate() } } open func contentsOfDirectory(path: String, completionHandler: @escaping (([FileObject], Error?) -> Void)) { self.contentsOfDirectory(path: path, including: [], completionHandler: completionHandler) } /** Returns an Array of `FileObject`s identifying the the directory entries via asynchronous completion handler. If the directory contains no entries or an error is occured, this method will return the empty array. - Parameter path: path to target directory. If empty, `currentPath` value will be used. - Parameter including: An array which determines which file properties should be considered to fetch. - Parameter completionHandler: a closure with result of directory entries or error. - `contents`: An array of `FileObject` identifying the the directory entries. - `error`: Error returned by system. */ open func contentsOfDirectory(path: String, including: [URLResourceKey], completionHandler: @escaping ((_ contents: [FileObject], _ error: Error?) -> Void)) { let opType = FileOperationType.fetch(path: path) let url = self.url(of: path).appendingPathComponent("") var request = URLRequest(url: url) request.httpMethod = "PROPFIND" request.setValue("1", forHTTPHeaderField: "Depth") request.set(httpAuthentication: credential, with: credentialType) request.set(contentType: .xml) request.httpBody = "\n\n\(WebDavFileObject.propString(including))\n".data(using: .utf8) request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length") runDataTask(with: request, operationHandle: RemoteOperationHandle(operationType: opType, tasks: []), completionHandler: { (data, response, error) in var responseError: FileProviderWebDavError? if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url) } var fileObjects = [WebDavFileObject]() if let data = data { let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL) for attr in xresponse where attr.href != url { if attr.href.path == url.path { continue } fileObjects.append(WebDavFileObject(attr)) } } completionHandler(fileObjects, responseError ?? error) }) } open func attributesOfItem(path: String, completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) { self.attributesOfItem(path: path, including: [], completionHandler: completionHandler) } /** Returns a `FileObject` containing the attributes of the item (file, directory, symlink, etc.) at the path in question via asynchronous completion handler. If the directory contains no entries or an error is occured, this method will return the empty `FileObject`. - Parameter path: path to target directory. If empty, `currentPath` value will be used. - Parameter including: An array which determines which file properties should be considered to fetch. - Parameter completionHandler: a closure with result of directory entries or error. - `attributes`: A `FileObject` containing the attributes of the item. - `error`: Error returned by system. */ open func attributesOfItem(path: String, including: [URLResourceKey], completionHandler: @escaping ((_ attributes: FileObject?, _ error: Error?) -> Void)) { let url = self.url(of: path) var request = URLRequest(url: url) request.httpMethod = "PROPFIND" request.setValue("1", forHTTPHeaderField: "Depth") request.set(httpAuthentication: credential, with: credentialType) request.set(contentType: .xml) request.httpBody = "\n\n\(WebDavFileObject.propString(including))\n".data(using: .utf8) request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length") runDataTask(with: request, completionHandler: { (data, response, error) in var responseError: FileProviderWebDavError? if let code = (response as? HTTPURLResponse)?.statusCode, code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url) } if let data = data { let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL) if let attr = xresponse.first { completionHandler(WebDavFileObject(attr), responseError ?? error) return } } completionHandler(nil, responseError ?? error) }) } open func storageProperties(completionHandler: @escaping ((_ total: Int64, _ used: Int64) -> Void)) { // Not all WebDAV clients implements RFC2518 which allows geting storage quota. // In this case you won't get error. totalSize is NSURLSessionTransferSizeUnknown // and used space is zero. guard let baseURL = baseURL else { return } var request = URLRequest(url: baseURL) request.httpMethod = "PROPFIND" request.setValue("0", forHTTPHeaderField: "Depth") request.set(httpAuthentication: credential, with: credentialType) request.set(contentType: .xml) request.httpBody = "\n\n\n".data(using: .utf8) request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length") runDataTask(with: request, completionHandler: { (data, response, error) in var totalSize: Int64 = -1 var usedSize: Int64 = 0 if let data = data { let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL) if let attr = xresponse.first { totalSize = Int64(attr.prop["quota-available-bytes"] ?? "") ?? -1 usedSize = Int64(attr.prop["quota-used-bytes"] ?? "") ?? 0 } } completionHandler(totalSize, usedSize) }) } open func searchFiles(path: String, recursive: Bool, query: NSPredicate, foundItemHandler: ((FileObject) -> Void)?, completionHandler: @escaping ((_ files: [FileObject], _ error: Error?) -> Void)) { let url = self.url(of: path) var request = URLRequest(url: url) request.httpMethod = "PROPFIND" //request.setValue("1", forHTTPHeaderField: "Depth") request.set(httpAuthentication: credential, with: credentialType) request.set(contentType: .xml) request.httpBody = "\n\n".data(using: .utf8) runDataTask(with: request, completionHandler: { (data, response, error) in // FIXME: paginating results var responseError: FileProviderWebDavError? if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url) } if let data = data { let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL) var fileObjects = [WebDavFileObject]() for attr in xresponse { let fileObject = WebDavFileObject(attr) if !query.evaluate(with: fileObject.mapPredicate()) { continue } fileObjects.append(fileObject) foundItemHandler?(fileObject) } completionHandler(fileObjects, responseError ?? error) return } completionHandler([], responseError ?? error) }) } open func isReachable(completionHandler: @escaping (Bool) -> Void) { var request = URLRequest(url: baseURL!) request.httpMethod = "PROPFIND" request.setValue("0", forHTTPHeaderField: "Depth") request.set(httpAuthentication: credential, with: credentialType) request.set(contentType: .xml) request.httpBody = "\n\n\n".data(using: .utf8) request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length") runDataTask(with: request, completionHandler: { (data, response, error) in let status = (response as? HTTPURLResponse)?.statusCode ?? 400 completionHandler(status < 300) }) } open weak var fileOperationDelegate: FileOperationDelegate? } extension WebDAVFileProvider: FileProviderOperations { @discardableResult open func create(folder folderName: String, at atPath: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let opType = FileOperationType.create(path: (atPath as NSString).appendingPathComponent(folderName) + "/") guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil } let url = self.url(of: atPath).appendingPathComponent(folderName, isDirectory: true) var request = URLRequest(url: url) request.httpMethod = "MKCOL" request.set(httpAuthentication: credential, with: credentialType) let task = session.dataTask(with: request, completionHandler: { (data, response, error) in var responseError: FileProviderWebDavError? if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { responseError = FileProviderWebDavError(code: rCode, path: url.relativePath, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url) } completionHandler?(responseError ?? error) self.delegateNotify(opType, error: responseError ?? error) }) task.taskDescription = opType.json task.resume() return RemoteOperationHandle(operationType: opType, tasks: [task]) } @discardableResult open func moveItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let opType = FileOperationType.move(source: path, destination: toPath) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil } return self.doOperation(operation: opType, overwrite: overwrite, completionHandler: completionHandler) } @discardableResult open func copyItem(path: String, to toPath: String, overwrite: Bool = false, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let opType = FileOperationType.copy(source: path, destination: toPath) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil } return self.doOperation(operation: opType, overwrite: overwrite, completionHandler: completionHandler) } @discardableResult open func removeItem(path: String, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let opType = FileOperationType.remove(path: path) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil } return self.doOperation(operation: opType, completionHandler: completionHandler) } fileprivate func doOperation(operation opType: FileOperationType, overwrite: Bool? = nil, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let source = opType.source! let sourceURL = self.url(of: source) var request = URLRequest(url: sourceURL) if let dest = opType.destination { request.setValue(url(of:dest).absoluteString, forHTTPHeaderField: "Destination") } switch opType { case .copy: request.httpMethod = "COPY" case .move: request.httpMethod = "MOVE" case .remove: request.httpMethod = "DELETE" default: return nil } request.set(httpAuthentication: credential, with: credentialType) if let overwrite = overwrite, !overwrite { request.setValue("F", forHTTPHeaderField: "Overwrite") } let task = session.dataTask(with: request, completionHandler: { (data, response, error) in var responseError: FileProviderWebDavError? if let response = response as? HTTPURLResponse, let code = FileProviderHTTPErrorCode(rawValue: response.statusCode) { if response.statusCode >= 300 { responseError = FileProviderWebDavError(code: code, path: source, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: sourceURL) } if code == .multiStatus, let data = data { let xresponses = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL) for xresponse in xresponses where (xresponse.status ?? 0) >= 300 { let error = FileProviderWebDavError(code: code, path: source, errorDescription: String(data: data, encoding: .utf8), url: sourceURL) completionHandler?(error) } } } if (response as? HTTPURLResponse)?.statusCode ?? 0 != FileProviderHTTPErrorCode.multiStatus.rawValue { completionHandler?(responseError ?? error) } self.delegateNotify(opType, error: responseError ?? error) }) task.taskDescription = opType.json task.resume() return RemoteOperationHandle(operationType: opType, tasks: [task]) } @discardableResult open func copyItem(localFile: URL, to toPath: String, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { // check file is not a folder guard (try? localFile.resourceValues(forKeys: [.fileResourceTypeKey]))?.fileResourceType ?? .unknown == .regular else { dispatch_queue.async { completionHandler?(self.throwError(localFile.path, code: URLError.fileIsDirectory)) } return nil } let opType = FileOperationType.copy(source: localFile.absoluteString, destination: toPath) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil } let url = self.url(of:toPath) var request = URLRequest(url: url) if !overwrite { request.setValue("F", forHTTPHeaderField: "Overwrite") } request.httpMethod = "PUT" request.set(httpAuthentication: credential, with: credentialType) let task = session.uploadTask(with: request, fromFile: localFile) completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { [weak self] error in var responseError: FileProviderWebDavError? if let code = (task.response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { // We can't fetch server result from delegate! responseError = FileProviderWebDavError(code: rCode, path: toPath, errorDescription: nil, url: url) } completionHandler?(responseError ?? error) self?.delegateNotify(.create(path: toPath), error: responseError ?? error) } task.taskDescription = opType.json task.resume() return RemoteOperationHandle(operationType: opType, tasks: [task]) } @discardableResult open func copyItem(path: String, toLocalURL destURL: URL, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let opType = FileOperationType.copy(source: path, destination: destURL.absoluteString) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil } let url = self.url(of:path) var request = URLRequest(url: url) request.set(httpAuthentication: credential, with: credentialType) let task = session.downloadTask(with: request) completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = completionHandler downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { tempURL in guard let httpResponse = task.response as? HTTPURLResponse , httpResponse.statusCode < 300 else { let code = FileProviderHTTPErrorCode(rawValue: (task.response as? HTTPURLResponse)?.statusCode ?? -1) let serverError : FileProviderWebDavError? = code != nil ? FileProviderWebDavError(code: code!, path: path, errorDescription: code?.description, url: url) : nil completionHandler?(serverError) return } do { try FileManager.default.moveItem(at: tempURL, to: destURL) completionHandler?(nil) } catch let e { completionHandler?(e) } } task.taskDescription = opType.json task.resume() return RemoteOperationHandle(operationType: opType, tasks: [task]) } } extension WebDAVFileProvider: FileProviderReadWrite { @discardableResult open func contents(path: String, offset: Int64, length: Int, completionHandler: @escaping ((_ contents: Data?, _ error: Error?) -> Void)) -> OperationHandle? { if length == 0 || offset < 0 { dispatch_queue.async { completionHandler(Data(), nil) } return nil } let opType = FileOperationType.fetch(path: path) let url = self.url(of: path) var request = URLRequest(url: url) request.set(httpAuthentication: credential, with: credentialType) request.set(rangeWithOffset: offset, length: length) let task = session.downloadTask(with: request) let handle = RemoteOperationHandle(operationType: opType, tasks: [task]) completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { error in completionHandler(nil, error) } downloadCompletionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { tempURL in guard let httpResponse = task.response as? HTTPURLResponse , httpResponse.statusCode < 300 else { let code = FileProviderHTTPErrorCode(rawValue: (task.response as? HTTPURLResponse)?.statusCode ?? -1) let serverError : FileProviderWebDavError? = code != nil ? FileProviderWebDavError(code: code!, path: path, errorDescription: code?.description, url: url) : nil completionHandler(nil, serverError) return } do { let data = try Data(contentsOf: tempURL) self.dispatch_queue.async { completionHandler(data, nil) } } catch let e { completionHandler(nil, e) } } task.taskDescription = opType.json task.resume() return handle } @discardableResult open func writeContents(path: String, contents data: Data?, atomically: Bool, overwrite: Bool, completionHandler: SimpleCompletionHandler) -> OperationHandle? { let opType = FileOperationType.modify(path: path) guard fileOperationDelegate?.fileProvider(self, shouldDoOperation: opType) ?? true == true else { return nil } // FIXME: lock destination before writing process let url = atomically ? self.url(of: path).appendingPathExtension("tmp") : self.url(of: path) var request = URLRequest(url: url) request.httpMethod = "PUT" request.set(httpAuthentication: credential, with: credentialType) if !overwrite { request.setValue("F", forHTTPHeaderField: "Overwrite") } let task = session.uploadTask(with: request, from: data ?? Data()) completionHandlersForTasks[session.sessionDescription!]?[task.taskIdentifier] = { [weak self] error in var responseError: FileProviderWebDavError? if let code = (task.response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { // We can't fetch server result from delegate! responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: nil, url: url) } completionHandler?(responseError ?? error) self?.delegateNotify(opType, error: responseError ?? error) } task.taskDescription = opType.json task.resume() return RemoteOperationHandle(operationType: opType, tasks: [task]) } /* fileprivate func registerNotifcation(path: String, eventHandler: (() -> Void)) { /* There is no unified api for monitoring WebDAV server content change/update * Microsoft Exchange uses SUBSCRIBE method, Apple uses push notification system. * while both is unavailable in a mobile platform. * A messy approach is listing a directory with an interval period and compare * with previous results */ NotImplemented() } fileprivate func unregisterNotifcation(path: String) { NotImplemented() }*/ // TODO: implements methods for lock mechanism } extension WebDAVFileProvider: ExtendedFileProvider { open func thumbnailOfFileSupported(path: String) -> Bool { guard self.baseURL?.host?.contains("dav.yandex.") ?? false else { return false } let supportedExt: [String] = ["jpg", "jpeg", "png", "gif"] return supportedExt.contains((path as NSString).pathExtension) } open func thumbnailOfFile(path: String, dimension: CGSize?, completionHandler: @escaping ((ImageClass?, Error?) -> Void)) { guard self.baseURL?.host?.contains("dav.yandex.") ?? false else { dispatch_queue.async { completionHandler(nil, self.throwError(path, code: URLError.resourceUnavailable)) } return } let dimension = dimension ?? CGSize(width: 64, height: 64) let url = URL(string: self.url(of: path).absoluteString + "?preview&size=\(dimension.width)x\(dimension.height)")! var request = URLRequest(url: url) request.httpMethod = "GET" request.set(httpAuthentication: credential, with: credentialType) let task = session.dataTask(with: request, completionHandler: { (data, response, error) in var responseError: FileProviderWebDavError? if let code = (response as? HTTPURLResponse)?.statusCode , code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { responseError = FileProviderWebDavError(code: rCode, path: url.relativePath, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url) completionHandler(nil, responseError ?? error) return } completionHandler(data.flatMap({ ImageClass(data: $0) }), nil) }) task.resume() } open func propertiesOfFileSupported(path: String) -> Bool { return false } open func propertiesOfFile(path: String, completionHandler: @escaping (([String : Any], [String], Error?) -> Void)) { dispatch_queue.async { completionHandler([:], [], self.throwError(path, code: URLError.resourceUnavailable)) } } } extension WebDAVFileProvider: FileProviderSharing { open func publicLink(to path: String, completionHandler: @escaping ((URL?, FileObject?, Date?, Error?) -> Void)) { guard self.baseURL?.host?.contains("dav.yandex.") ?? false else { dispatch_queue.async { completionHandler(nil, nil, nil, self.throwError(path, code: URLError.resourceUnavailable)) } return } let url = self.url(of: path) var request = URLRequest(url: url) request.httpMethod = "PROPPATCH" request.set(httpAuthentication: credential, with: credentialType) request.set(contentType: .xml) let body = "\n\ntrue\n\n" request.httpBody = body.data(using: .utf8) request.setValue(String(request.httpBody!.count), forHTTPHeaderField: "Content-Length") runDataTask(with: request, completionHandler: { (data, response, error) in var responseError: FileProviderWebDavError? if let code = (response as? HTTPURLResponse)?.statusCode, code >= 300, let rCode = FileProviderHTTPErrorCode(rawValue: code) { responseError = FileProviderWebDavError(code: rCode, path: path, errorDescription: String(data: data ?? Data(), encoding: .utf8), url: url) } if let data = data { let xresponse = DavResponse.parse(xmlResponse: data, baseURL: self.baseURL) if let urlStr = xresponse.first?.prop["public_url"], let url = URL(string: urlStr) { completionHandler(url, nil, nil, nil) return } } completionHandler(nil, nil, nil, responseError ?? error) }) } } extension WebDAVFileProvider: FileProvider { } // MARK: WEBDAV XML response implementation internal extension WebDAVFileProvider { fileprivate func delegateNotify(_ operation: FileOperationType, error: Error?) { DispatchQueue.main.async(execute: { if error == nil { self.delegate?.fileproviderSucceed(self, operation: operation) } else { self.delegate?.fileproviderFailed(self, operation: operation) } }) } } struct DavResponse { let href: URL let hrefString: String let status: Int? let prop: [String: String] init? (_ node: AEXMLElement, baseURL: URL?) { func standardizePath(_ str: String) -> String { let trimmedStr = str.hasPrefix("/") ? str.substring(from: str.index(after: str.startIndex)) : str return trimmedStr.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? str } // find node names with namespace var hreftag = "href" var statustag = "status" var propstattag = "propstat" for node in node.children { if node.name.lowercased().hasSuffix("href") { hreftag = node.name } if node.name.lowercased().hasSuffix("status") { statustag = node.name } if node.name.lowercased().hasSuffix("propstat") { propstattag = node.name } } 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)?.absoluteURL let relativePath: String if hrefAbsolute?.host?.replacingOccurrences(of: "www.", with: "", options: .anchored) == baseURL?.host?.replacingOccurrences(of: "www.", with: "", options: .anchored) { relativePath = hrefAbsolute?.path.replacingOccurrences(of: baseURL?.absoluteURL.path ?? "", with: "", options: .anchored, range: nil) ?? hrefString } else { relativePath = hrefAbsolute?.absoluteString.replacingOccurrences(of: baseURL?.absoluteString ?? "", with: "", options: .anchored, range: nil) ?? hrefString } let hrefURL = URL(string: standardizePath(relativePath), relativeTo: baseURL) ?? baseURL guard let href = hrefURL?.standardized else { return nil } // 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" } } self.href = href self.hrefString = hrefString self.status = status self.prop = propDic } static func parse(xmlResponse: Data, baseURL: URL?) -> [DavResponse] { guard let xml = try? AEXMLDocument(xml: xmlResponse) else { return [] } var result = [DavResponse]() var rootnode = xml.root var responsetag = "response" for node in rootnode.all ?? [] where node.name.lowercased().hasSuffix("multistatus") { rootnode = node } for node in rootnode.children where node.name.lowercased().hasSuffix("response") { responsetag = node.name break } for responseNode in rootnode[responsetag].all ?? [] { if let davResponse = DavResponse(responseNode, baseURL: baseURL) { result.append(davResponse) } } return result } } /// Containts path, url and attributes of a WebDAV file or resource. public final class WebDavFileObject: FileObject { internal init(_ davResponse: DavResponse) { let href = davResponse.href let name = davResponse.prop["displayname"] ?? davResponse.href.lastPathComponent let relativePath = href.relativePath let path = relativePath.hasPrefix("/") ? relativePath : ("/" + relativePath) super.init(url: href, name: name, path: path) self.size = Int64(davResponse.prop["getcontentlength"] ?? "-1") ?? NSURLSessionTransferSizeUnknown self.creationDate = Date(rfcString: davResponse.prop["creationdate"] ?? "") self.modifiedDate = Date(rfcString: 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"] } /// MIME type of the file. open internal(set) var contentType: String { get { return allValues[.mimeTypeKey] as? String ?? "" } set { allValues[.mimeTypeKey] = newValue } } /// HTTP E-Tag, can be used to mark changed files. open internal(set) var entryTag: String? { get { return allValues[.entryTagKey] as? String } set { allValues[.entryTagKey] = newValue } } internal class func resourceKeyToDAVProp(_ key: URLResourceKey) -> String? { switch key { case URLResourceKey.fileSizeKey: return "getcontentlength" case URLResourceKey.creationDateKey: return "creationdate" case URLResourceKey.contentModificationDateKey: return "getlastmodified" case URLResourceKey.fileResourceTypeKey, URLResourceKey.mimeTypeKey: return "getcontenttype" case URLResourceKey.isHiddenKey: return "ishidden" case URLResourceKey.entryTagKey: return "getetag" default: return nil } } internal class func propString(_ keys: [URLResourceKey]) -> String { var propKeys = "" for item in keys { if let prop = WebDavFileObject.resourceKeyToDAVProp(item) { propKeys += "" } } if propKeys.isEmpty { propKeys = "" } return propKeys } } /// Error returned by WebDAV server when trying to access or do operations on a file or folder. public struct FileProviderWebDavError: FileProviderHTTPError { public let code: FileProviderHTTPErrorCode public let path: String public let errorDescription: String? /// URL of resource caused error. public let url: URL }