Merge pull request #28 from Isvvc/custom-caching-data
Custom data caching
This commit is contained in:
commit
87b3d073ad
|
@ -11,13 +11,12 @@ let package = Package(
|
|||
targets: ["WebDAV"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/drmohundro/SWXMLHash.git", .upToNextMajor(from: "5.0.0")),
|
||||
.package(url: "https://github.com/3lvis/Networking.git", .upToNextMajor(from: "5.1.0"))
|
||||
.package(url: "https://github.com/drmohundro/SWXMLHash.git", .upToNextMajor(from: "5.0.0"))
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "WebDAV",
|
||||
dependencies: ["SWXMLHash", "Networking"]),
|
||||
dependencies: ["SWXMLHash"]),
|
||||
.testTarget(
|
||||
name: "WebDAVTests",
|
||||
dependencies: ["WebDAV"]),
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
//
|
||||
// Cache.swift
|
||||
// WebDAV-Swift
|
||||
//
|
||||
// Created by Isaac Lyons on 4/8/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public final class Cache<Key: Hashable, Value> {
|
||||
|
||||
//MARK: Private
|
||||
|
||||
private let cache = NSCache<KeyWrapper, ContentWrapper>()
|
||||
|
||||
private final class KeyWrapper: NSObject {
|
||||
let key: Key
|
||||
|
||||
init(_ key: Key) {
|
||||
self.key = key
|
||||
}
|
||||
|
||||
override var hash: Int {
|
||||
key.hashValue
|
||||
}
|
||||
|
||||
override func isEqual(_ object: Any?) -> Bool {
|
||||
guard let value = object as? KeyWrapper else { return false }
|
||||
return value.key == key
|
||||
}
|
||||
}
|
||||
|
||||
private final class ContentWrapper {
|
||||
let value: Value
|
||||
|
||||
init(_ value: Value) {
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: Public
|
||||
|
||||
internal func value(forKey key: Key) -> Value? {
|
||||
guard let entry = cache.object(forKey: KeyWrapper(key)) else { return nil }
|
||||
return entry.value
|
||||
}
|
||||
|
||||
internal func set(_ value: Value, forKey key: Key) {
|
||||
let entry = ContentWrapper(value)
|
||||
cache.setObject(entry, forKey: KeyWrapper(key))
|
||||
}
|
||||
|
||||
internal func removeValue(forKey key: Key) {
|
||||
cache.removeObject(forKey: KeyWrapper(key))
|
||||
}
|
||||
|
||||
internal func removeAllValues() {
|
||||
cache.removeAllObjects()
|
||||
}
|
||||
|
||||
internal subscript(key: Key) -> Value? {
|
||||
get { value(forKey: key) }
|
||||
set {
|
||||
guard let value = newValue else {
|
||||
return removeValue(forKey: key)
|
||||
}
|
||||
set(value, forKey: key)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,261 @@
|
|||
//
|
||||
// WebDAV+Images.swift
|
||||
// WebDAV-Swift
|
||||
//
|
||||
// Created by Isaac Lyons on 4/9/21.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
//MARK: ThumbnailProperties
|
||||
|
||||
public struct ThumbnailProperties: Hashable {
|
||||
private var width: Int?
|
||||
private var height: Int?
|
||||
|
||||
public var contentMode: ContentMode
|
||||
|
||||
public var size: (width: Int, height: Int)? {
|
||||
get {
|
||||
if let width = width,
|
||||
let height = height {
|
||||
return (width, height)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
set {
|
||||
width = newValue?.width
|
||||
height = newValue?.height
|
||||
}
|
||||
}
|
||||
|
||||
/// Configurable default thumbnail properties. Initial value of content fill and server default dimensions.
|
||||
public static var `default` = ThumbnailProperties(contentMode: .fill)
|
||||
/// Content fill with the server's default dimensions.
|
||||
public static let fill = ThumbnailProperties(contentMode: .fill)
|
||||
/// Content fit with the server's default dimensions.
|
||||
public static let fit = ThumbnailProperties(contentMode: .fit)
|
||||
|
||||
/// Constants that define how the thumbnail fills the dimensions.
|
||||
public enum ContentMode: Hashable {
|
||||
case fill
|
||||
case fit
|
||||
}
|
||||
|
||||
/// - Parameters:
|
||||
/// - size: The size of the thumbnail. A nil value will use the server's default dimensions.
|
||||
/// - contentMode: A flag that indicates whether the thumbnail view fits or fills the dimensions.
|
||||
public init(_ size: (width: Int, height: Int)? = nil, contentMode: ThumbnailProperties.ContentMode) {
|
||||
if let size = size {
|
||||
width = size.width
|
||||
height = size.height
|
||||
}
|
||||
self.contentMode = contentMode
|
||||
}
|
||||
|
||||
/// - Parameters:
|
||||
/// - size: The size of the thumbnail. Width and height will be trucated to integer pixel counts.
|
||||
/// - contentMode: A flag that indicates whether the thumbnail view fits or fills the image of the given dimensions.
|
||||
public init(size: CGSize, contentMode: ThumbnailProperties.ContentMode) {
|
||||
width = Int(size.width)
|
||||
height = Int(size.height)
|
||||
self.contentMode = contentMode
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: Public
|
||||
|
||||
public extension WebDAV {
|
||||
|
||||
//MARK: Images
|
||||
|
||||
/// Download and cache an image from the specified file path.
|
||||
/// - Parameters:
|
||||
/// - path: The path of the image to download.
|
||||
/// - account: The WebDAV account.
|
||||
/// - password: The WebDAV account's password.
|
||||
/// - completion: If account properties are invalid, this will run immediately on the same thread.
|
||||
/// Otherwise, it runs when the nextwork call finishes on a background thread.
|
||||
/// - image: The image downloaded, if successful.
|
||||
/// The cached image if it has balready been downloaded.
|
||||
/// - cachedImageURL: The URL of the cached image.
|
||||
/// - error: A WebDAVError if the call was unsuccessful. `nil` if it was.
|
||||
/// - Returns: The request identifier.
|
||||
@discardableResult
|
||||
func downloadImage<A: WebDAVAccount>(path: String, account: A, password: String, caching options: WebDAVCachingOptions = [], completion: @escaping (_ image: UIImage?, _ error: WebDAVError?) -> Void) -> URLSessionDataTask? {
|
||||
cachingDataTask(cache: imageCache, path: path, account: account, password: password, caching: options, valueFromData: { UIImage(data: $0) }, completion: completion)
|
||||
}
|
||||
|
||||
//MARK: Thumbnails
|
||||
|
||||
/// Download and cache an image's thumbnail from the specified file path.
|
||||
///
|
||||
/// Only works with Nextcould or other instances that use Nextcloud's same thumbnail URL structure.
|
||||
/// - Parameters:
|
||||
/// - path: The path of the image to download the thumbnail of.
|
||||
/// - account: The WebDAV account.
|
||||
/// - password: The WebDAV account's password.
|
||||
/// - dimensions: The dimensions of the thumbnail. A value of `nil` will use the server's default.
|
||||
/// - aspectFill: Whether the thumbnail should fill the dimensions or fit within it.
|
||||
/// - completion: If account properties are invalid, this will run immediately on the same thread.
|
||||
/// Otherwise, it runs when the nextwork call finishes on a background thread.
|
||||
/// - image: The thumbnail downloaded, if successful.
|
||||
/// The cached thumbnail if it has balready been downloaded.
|
||||
/// - cachedImageURL: The URL of the cached thumbnail.
|
||||
/// - error: A WebDAVError if the call was unsuccessful. `nil` if it was.
|
||||
/// - Returns: The request identifier.
|
||||
@discardableResult
|
||||
func downloadThumbnail<A: WebDAVAccount>(
|
||||
path: String, account: A, password: String, with properties: ThumbnailProperties = .default,
|
||||
caching options: WebDAVCachingOptions = [], completion: @escaping (_ image: UIImage?, _ error: WebDAVError?) -> Void
|
||||
) -> URLSessionDataTask? {
|
||||
// This function looks a lot like cachingDataTask and authorizedRequest,
|
||||
// but generalizing both of those to support thumbnails would make them
|
||||
// so much more complicated that it's better to just have similar code here.
|
||||
|
||||
// Check cache
|
||||
|
||||
var cachedThumbnail: UIImage?
|
||||
let accountPath = AccountPath(account: account, path: path)
|
||||
if !options.contains(.doNotReturnCachedResult) {
|
||||
if let thumbnail = thumbnailCache[accountPath]?[properties] {
|
||||
completion(thumbnail, nil)
|
||||
|
||||
if !options.contains(.requestEvenIfCached) {
|
||||
if options.contains(.removeExistingCache) {
|
||||
try? deleteCachedThumbnail(forItemAtPath: path, account: account, with: properties)
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
cachedThumbnail = thumbnail
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if options.contains(.removeExistingCache) {
|
||||
try? deleteCachedThumbnail(forItemAtPath: path, account: account, with: properties)
|
||||
}
|
||||
|
||||
// Create Network request
|
||||
|
||||
guard let unwrappedAccount = UnwrappedAccount(account: account), let auth = self.auth(username: unwrappedAccount.username, password: password) else {
|
||||
completion(nil, .invalidCredentials)
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let url = nextcloudPreviewURL(for: unwrappedAccount.baseURL, at: path, with: properties) else {
|
||||
completion(nil, .unsupported)
|
||||
return nil
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.addValue("Basic \(auth)", forHTTPHeaderField: "Authorization")
|
||||
|
||||
// Perform the network request
|
||||
|
||||
let task = URLSession(configuration: .ephemeral, delegate: self, delegateQueue: nil).dataTask(with: request) { [weak self] data, response, error in
|
||||
let error = WebDAVError.getError(response: response, error: error)
|
||||
|
||||
if let data = data,
|
||||
let thumbnail = UIImage(data: data) {
|
||||
// Cache result
|
||||
//TODO: Cache to disk
|
||||
if !options.contains(.removeExistingCache),
|
||||
!options.contains(.doNotCacheResult) {
|
||||
var cachedThumbnails = self?.thumbnailCache[accountPath] ?? [:]
|
||||
cachedThumbnails[properties] = thumbnail
|
||||
self?.thumbnailCache[accountPath] = cachedThumbnails
|
||||
}
|
||||
|
||||
if thumbnail != cachedThumbnail {
|
||||
completion(thumbnail, error)
|
||||
}
|
||||
} else {
|
||||
completion(nil, error)
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
return task
|
||||
}
|
||||
|
||||
//MARK: Image Cache
|
||||
|
||||
func getCachedImage<A: WebDAVAccount>(forItemAtPath path: String, account: A) -> UIImage? {
|
||||
getCachedValue(cache: imageCache, forItemAtPath: path, account: account)
|
||||
}
|
||||
|
||||
//MARK: Thumbnail Cache
|
||||
|
||||
func getAllCachedThumbnails<A: WebDAVAccount>(forItemAtPath path: String, account: A) -> [ThumbnailProperties: UIImage]? {
|
||||
getCachedValue(cache: thumbnailCache, forItemAtPath: path, account: account)
|
||||
}
|
||||
|
||||
func getCachedThumbnail<A: WebDAVAccount>(forItemAtPath path: String, account: A, with properties: ThumbnailProperties) -> UIImage? {
|
||||
getAllCachedThumbnails(forItemAtPath: path, account: account)?[properties]
|
||||
}
|
||||
|
||||
func deleteCachedThumbnail<A: WebDAVAccount>(forItemAtPath path: String, account: A, with properties: ThumbnailProperties) throws {
|
||||
let accountPath = AccountPath(account: account, path: path)
|
||||
if var cachedThumbnails = thumbnailCache[accountPath] {
|
||||
cachedThumbnails.removeValue(forKey: properties)
|
||||
if cachedThumbnails.isEmpty {
|
||||
thumbnailCache.removeValue(forKey: accountPath)
|
||||
} else {
|
||||
thumbnailCache[accountPath] = cachedThumbnails
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deleteAllCachedThumbnails<A: WebDAVAccount>(forItemAtPath path: String, account: A) throws {
|
||||
let accountPath = AccountPath(account: account, path: path)
|
||||
thumbnailCache.removeValue(forKey: accountPath)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//MARK: Internal
|
||||
|
||||
extension WebDAV {
|
||||
|
||||
//MARK: Pathing
|
||||
|
||||
func nextcloudPreviewBaseURL(for baseURL: URL) -> URL? {
|
||||
return nextcloudBaseURL(for: baseURL)?
|
||||
.appendingPathComponent("index.php")
|
||||
.appendingPathComponent("core")
|
||||
.appendingPathComponent("preview.png")
|
||||
}
|
||||
|
||||
func nextcloudPreviewQuery(at path: String, properties: ThumbnailProperties) -> [URLQueryItem]? {
|
||||
var path = path
|
||||
|
||||
if path.hasPrefix("/") {
|
||||
path.removeFirst()
|
||||
}
|
||||
|
||||
var query = [
|
||||
URLQueryItem(name: "file", value: path),
|
||||
URLQueryItem(name: "mode", value: "cover")
|
||||
]
|
||||
|
||||
if let size = properties.size {
|
||||
query.append(URLQueryItem(name: "x", value: "\(size.width)"))
|
||||
query.append(URLQueryItem(name: "y", value: "\(size.height)"))
|
||||
}
|
||||
|
||||
if properties.contentMode == .fill {
|
||||
query.append(URLQueryItem(name: "a", value: "1"))
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
func nextcloudPreviewURL(for baseURL: URL, at path: String, with properties: ThumbnailProperties) -> URL? {
|
||||
guard let thumbnailURL = nextcloudPreviewBaseURL(for: baseURL) else { return nil }
|
||||
var components = URLComponents(string: thumbnailURL.absoluteString)
|
||||
components?.queryItems = nextcloudPreviewQuery(at: path, properties: properties)
|
||||
return components?.url
|
||||
}
|
||||
|
||||
}
|
|
@ -8,6 +8,8 @@
|
|||
import Foundation
|
||||
import SWXMLHash
|
||||
|
||||
//MARK: OCSTheme
|
||||
|
||||
/// Theming information from a WebDAV server that supports OCS.
|
||||
public struct OCSTheme {
|
||||
/// Name of the server.
|
||||
|
@ -48,7 +50,9 @@ public struct OCSTheme {
|
|||
}
|
||||
}
|
||||
|
||||
extension WebDAV {
|
||||
//MARK: Public
|
||||
|
||||
public extension WebDAV {
|
||||
|
||||
/// Get the theme information from a WebDAV server that supports OCS (including Nextcloud).
|
||||
/// - Parameters:
|
||||
|
@ -60,13 +64,16 @@ extension WebDAV {
|
|||
/// - error: A WebDAVError if the call was unsuccessful.
|
||||
/// - Returns: The data task for the request.
|
||||
@discardableResult
|
||||
public func getNextcloudTheme<A: WebDAVAccount>(account: A, password: String, completion: @escaping (_ theme: OCSTheme?, _ error: WebDAVError?) -> Void) -> URLSessionDataTask? {
|
||||
func getNextcloudTheme<A: WebDAVAccount>(account: A, password: String, completion: @escaping (_ theme: OCSTheme?, _ error: WebDAVError?) -> Void) -> URLSessionDataTask? {
|
||||
guard let unwrappedAccount = UnwrappedAccount(account: account),
|
||||
let auth = self.auth(username: unwrappedAccount.username, password: password),
|
||||
let baseURL = nextcloudBaseURL(for: unwrappedAccount.baseURL) else {
|
||||
let auth = self.auth(username: unwrappedAccount.username, password: password) else {
|
||||
completion(nil, .invalidCredentials)
|
||||
return nil
|
||||
}
|
||||
guard let baseURL = nextcloudBaseURL(for: unwrappedAccount.baseURL) else {
|
||||
completion(nil, .unsupported)
|
||||
return nil
|
||||
}
|
||||
|
||||
let url = baseURL.appendingPathComponent("ocs/v1.php/cloud/capabilities")
|
||||
var request = URLRequest(url: url)
|
||||
|
@ -102,7 +109,7 @@ extension WebDAV {
|
|||
/// - error: A WebDAVError if the call was unsuccessful.
|
||||
/// - Returns: The data task for the request.
|
||||
@discardableResult
|
||||
public func getNextcloudColorHex<A: WebDAVAccount>(account: A, password: String, completion: @escaping (_ color: String?, _ error: WebDAVError?) -> Void) -> URLSessionDataTask? {
|
||||
func getNextcloudColorHex<A: WebDAVAccount>(account: A, password: String, completion: @escaping (_ color: String?, _ error: WebDAVError?) -> Void) -> URLSessionDataTask? {
|
||||
getNextcloudTheme(account: account, password: password) { theme, error in
|
||||
completion(theme?.colorHex, error)
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import UIKit
|
||||
import SWXMLHash
|
||||
import Networking
|
||||
|
||||
public class WebDAV: NSObject, URLSessionDelegate {
|
||||
static let domain = "app.lyons.webdav-swift"
|
||||
|
@ -17,15 +16,34 @@ public class WebDAV: NSObject, URLSessionDelegate {
|
|||
/// The formatter used when rendering cache size in `getCacheSize`.
|
||||
public var byteCountFormatter = ByteCountFormatter()
|
||||
|
||||
var networkings: [UnwrappedAccount: Networking] = [:]
|
||||
var thumbnailNetworkings: [UnwrappedAccount: Networking] = [:]
|
||||
public var filesCache: [AccountPath: [WebDAVFile]] = [:]
|
||||
public var dataCache = Cache<AccountPath, Data>()
|
||||
public var imageCache = Cache<AccountPath, UIImage>()
|
||||
public var thumbnailCache = Cache<AccountPath, [ThumbnailProperties: UIImage]>()
|
||||
|
||||
public override init() {
|
||||
super.init()
|
||||
loadFilesCacheFromDisk()
|
||||
}
|
||||
|
||||
//MARK: Static
|
||||
|
||||
public static func sortedFiles(_ files: [WebDAVFile], foldersFirst: Bool, includeSelf: Bool) -> [WebDAVFile] {
|
||||
var files = files
|
||||
if !includeSelf, !files.isEmpty {
|
||||
files.removeFirst()
|
||||
}
|
||||
if foldersFirst {
|
||||
files = files.filter { $0.isDirectory } + files.filter { !$0.isDirectory }
|
||||
}
|
||||
return files
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: Public
|
||||
|
||||
public extension WebDAV {
|
||||
|
||||
//MARK: WebDAV Requests
|
||||
|
||||
/// List the files and directories at the specified path.
|
||||
|
@ -44,7 +62,7 @@ public class WebDAV: NSObject, URLSessionDelegate {
|
|||
/// - error: A WebDAVError if the call was unsuccessful.
|
||||
/// - Returns: The data task for the request.
|
||||
@discardableResult
|
||||
public func listFiles<A: WebDAVAccount>(atPath path: String, account: A, password: String, foldersFirst: Bool = true, includeSelf: Bool = false, caching options: WebDAVCacheOptions = [], completion: @escaping (_ files: [WebDAVFile]?, _ error: WebDAVError?) -> Void) -> URLSessionDataTask? {
|
||||
func listFiles<A: WebDAVAccount>(atPath path: String, account: A, password: String, foldersFirst: Bool = true, includeSelf: Bool = false, caching options: WebDAVCachingOptions = [], completion: @escaping (_ files: [WebDAVFile]?, _ error: WebDAVError?) -> Void) -> URLSessionDataTask? {
|
||||
// Check the cache
|
||||
var cachedResponse: [WebDAVFile]?
|
||||
let accountPath = AccountPath(account: account, path: path)
|
||||
|
@ -140,7 +158,7 @@ public class WebDAV: NSObject, URLSessionDelegate {
|
|||
/// - error: A WebDAVError if the call was unsuccessful. `nil` if it was.
|
||||
/// - Returns: The upload task for the request.
|
||||
@discardableResult
|
||||
public func upload<A: WebDAVAccount>(data: Data, toPath path: String, account: A, password: String, completion: @escaping (_ error: WebDAVError?) -> Void) -> URLSessionUploadTask? {
|
||||
func upload<A: WebDAVAccount>(data: Data, toPath path: String, account: A, password: String, completion: @escaping (_ error: WebDAVError?) -> Void) -> URLSessionUploadTask? {
|
||||
guard let request = authorizedRequest(path: path, account: account, password: password, method: .put) else {
|
||||
completion(.invalidCredentials)
|
||||
return nil
|
||||
|
@ -164,7 +182,7 @@ public class WebDAV: NSObject, URLSessionDelegate {
|
|||
/// - error: A WebDAVError if the call was unsuccessful. `nil` if it was.
|
||||
/// - Returns: The upload task for the request.
|
||||
@discardableResult
|
||||
public func upload<A: WebDAVAccount>(file: URL, toPath path: String, account: A, password: String, completion: @escaping (_ error: WebDAVError?) -> Void) -> URLSessionUploadTask? {
|
||||
func upload<A: WebDAVAccount>(file: URL, toPath path: String, account: A, password: String, completion: @escaping (_ error: WebDAVError?) -> Void) -> URLSessionUploadTask? {
|
||||
guard let request = authorizedRequest(path: path, account: account, password: password, method: .put) else {
|
||||
completion(.invalidCredentials)
|
||||
return nil
|
||||
|
@ -189,18 +207,8 @@ public class WebDAV: NSObject, URLSessionDelegate {
|
|||
/// - error: A WebDAVError if the call was unsuccessful. `nil` if it was.
|
||||
/// - Returns: The data task for the request.
|
||||
@discardableResult
|
||||
public func download<A: WebDAVAccount>(fileAtPath path: String, account: A, password: String, completion: @escaping (_ data: Data?, _ error: WebDAVError?) -> Void) -> URLSessionDataTask? {
|
||||
guard let request = authorizedRequest(path: path, account: account, password: password, method: .get) else {
|
||||
completion(nil, .invalidCredentials)
|
||||
return nil
|
||||
}
|
||||
|
||||
let task = URLSession(configuration: .ephemeral, delegate: self, delegateQueue: nil).dataTask(with: request) { data, response, error in
|
||||
completion(data, WebDAVError.getError(response: response, error: error))
|
||||
}
|
||||
|
||||
task.resume()
|
||||
return task
|
||||
func download<A: WebDAVAccount>(fileAtPath path: String, account: A, password: String, caching options: WebDAVCachingOptions = [], completion: @escaping (_ data: Data?, _ error: WebDAVError?) -> Void) -> URLSessionDataTask? {
|
||||
cachingDataTask(cache: dataCache, path: path, account: account, password: password, caching: options, valueFromData: { $0 }, completion: completion)
|
||||
}
|
||||
|
||||
/// Create a folder at the specified path
|
||||
|
@ -213,7 +221,7 @@ public class WebDAV: NSObject, URLSessionDelegate {
|
|||
/// - error: A WebDAVError if the call was unsuccessful. `nil` if it was.
|
||||
/// - Returns: The data task for the request.
|
||||
@discardableResult
|
||||
public func createFolder<A: WebDAVAccount>(atPath path: String, account: A, password: String, completion: @escaping (_ error: WebDAVError?) -> Void) -> URLSessionDataTask? {
|
||||
func createFolder<A: WebDAVAccount>(atPath path: String, account: A, password: String, completion: @escaping (_ error: WebDAVError?) -> Void) -> URLSessionDataTask? {
|
||||
basicDataTask(path: path, account: account, password: password, method: .mkcol, completion: completion)
|
||||
}
|
||||
|
||||
|
@ -227,7 +235,7 @@ public class WebDAV: NSObject, URLSessionDelegate {
|
|||
/// - error: A WebDAVError if the call was unsuccessful. `nil` if it was.
|
||||
/// - Returns: The data task for the request.
|
||||
@discardableResult
|
||||
public func deleteFile<A: WebDAVAccount>(atPath path: String, account: A, password: String, completion: @escaping (_ error: WebDAVError?) -> Void) -> URLSessionDataTask? {
|
||||
func deleteFile<A: WebDAVAccount>(atPath path: String, account: A, password: String, completion: @escaping (_ error: WebDAVError?) -> Void) -> URLSessionDataTask? {
|
||||
basicDataTask(path: path, account: account, password: password, method: .delete, completion: completion)
|
||||
}
|
||||
|
||||
|
@ -242,7 +250,7 @@ public class WebDAV: NSObject, URLSessionDelegate {
|
|||
/// - error: A WebDAVError if the call was unsuccessful. `nil` if it was.
|
||||
/// - Returns: The data task for the request.
|
||||
@discardableResult
|
||||
public func moveFile<A: WebDAVAccount>(fromPath path: String, to destination: String, account: A, password: String, completion: @escaping (_ error: WebDAVError?) -> Void) -> URLSessionDataTask? {
|
||||
func moveFile<A: WebDAVAccount>(fromPath path: String, to destination: String, account: A, password: String, completion: @escaping (_ error: WebDAVError?) -> Void) -> URLSessionDataTask? {
|
||||
basicDataTask(path: path, destination: destination, account: account, password: password, method: .move, completion: completion)
|
||||
}
|
||||
|
||||
|
@ -257,84 +265,18 @@ public class WebDAV: NSObject, URLSessionDelegate {
|
|||
/// - error: A WebDAVError if the call was unsuccessful. `nil` if it was.
|
||||
/// - Returns: The data task for the request.
|
||||
@discardableResult
|
||||
public func copyFile<A: WebDAVAccount>(fromPath path: String, to destination: String, account: A, password: String, completion: @escaping (_ error: WebDAVError?) -> Void) -> URLSessionDataTask? {
|
||||
func copyFile<A: WebDAVAccount>(fromPath path: String, to destination: String, account: A, password: String, completion: @escaping (_ error: WebDAVError?) -> Void) -> URLSessionDataTask? {
|
||||
basicDataTask(path: path, destination: destination, account: account, password: password, method: .copy, completion: completion)
|
||||
}
|
||||
|
||||
//MARK: Networking Requests
|
||||
// Somewhat confusing header title, but this refers to requests made using the Networking library
|
||||
//MARK: Cache
|
||||
|
||||
/// Download and cache an image from the specified file path.
|
||||
/// - Parameters:
|
||||
/// - path: The path of the image to download.
|
||||
/// - account: The WebDAV account.
|
||||
/// - password: The WebDAV account's password.
|
||||
/// - completion: If account properties are invalid, this will run immediately on the same thread.
|
||||
/// Otherwise, it runs when the nextwork call finishes on a background thread.
|
||||
/// - image: The image downloaded, if successful.
|
||||
/// The cached image if it has balready been downloaded.
|
||||
/// - cachedImageURL: The URL of the cached image.
|
||||
/// - error: A WebDAVError if the call was unsuccessful. `nil` if it was.
|
||||
/// - Returns: The request identifier.
|
||||
@discardableResult
|
||||
public func downloadImage<A: WebDAVAccount>(path: String, account: A, password: String, completion: @escaping (_ image: UIImage?, _ cachedImageURL: URL?, _ error: WebDAVError?) -> Void) -> String? {
|
||||
guard let networking = self.networking(for: account, password: password),
|
||||
let path = networkingPath(path) else {
|
||||
completion(nil, nil, .invalidCredentials)
|
||||
return nil
|
||||
}
|
||||
|
||||
let id = networking.downloadImage(path) { imageResult in
|
||||
switch imageResult {
|
||||
case .success(let imageResponse):
|
||||
let path = try? networking.destinationURL(for: path)
|
||||
completion(imageResponse.image, path, nil)
|
||||
case .failure(let response):
|
||||
completion(nil, nil, WebDAVError.getError(statusCode: response.statusCode, error: response.error))
|
||||
}
|
||||
}
|
||||
|
||||
return id
|
||||
func getCachedData<A: WebDAVAccount>(forItemAtPath path: String, account: A) -> Data? {
|
||||
getCachedValue(cache: dataCache, forItemAtPath: path, account: account)
|
||||
}
|
||||
|
||||
/// Download and cache an image's thumbnail from the specified file path.
|
||||
///
|
||||
/// Only works with Nextcould or other instances that use Nextcloud's same thumbnail URL structure.
|
||||
/// - Parameters:
|
||||
/// - path: The path of the image to download the thumbnail of.
|
||||
/// - account: The WebDAV account.
|
||||
/// - password: The WebDAV account's password.
|
||||
/// - dimensions: The dimensions of the thumbnail. A value of `nil` will use the server's default.
|
||||
/// - aspectFill: Whether the thumbnail should fill the dimensions or fit within it.
|
||||
/// - completion: If account properties are invalid, this will run immediately on the same thread.
|
||||
/// Otherwise, it runs when the nextwork call finishes on a background thread.
|
||||
/// - image: The thumbnail downloaded, if successful.
|
||||
/// The cached thumbnail if it has balready been downloaded.
|
||||
/// - cachedImageURL: The URL of the cached thumbnail.
|
||||
/// - error: A WebDAVError if the call was unsuccessful. `nil` if it was.
|
||||
/// - Returns: The request identifier.
|
||||
@discardableResult
|
||||
public func downloadThumbnail<A: WebDAVAccount>(
|
||||
path: String, account: A, password: String, with dimensions: CGSize?, aspectFill: Bool = true,
|
||||
completion: @escaping (_ image: UIImage?, _ cachedImageURL: URL?, _ error: WebDAVError?) -> Void
|
||||
) -> String? {
|
||||
guard let networking = thumbnailNetworking(for: account, password: password),
|
||||
let path = nextcloudPreviewPath(at: path, with: dimensions, aspectFill: aspectFill) else {
|
||||
completion(nil, nil, .invalidCredentials)
|
||||
return nil
|
||||
}
|
||||
|
||||
let id = networking.downloadImage(path) { imageResult in
|
||||
switch imageResult {
|
||||
case .success(let imageResponse):
|
||||
let path = try? networking.destinationURL(for: path)
|
||||
completion(imageResponse.image, path, nil)
|
||||
case .failure(let response):
|
||||
completion(nil, nil, WebDAVError.getError(statusCode: response.statusCode, error: response.error))
|
||||
}
|
||||
}
|
||||
|
||||
return id
|
||||
func getCachedValue<A: WebDAVAccount, Value: Equatable>(cache: Cache<AccountPath, Value>, forItemAtPath path: String, account: A) -> Value? {
|
||||
cache[AccountPath(account: account, path: path)]
|
||||
}
|
||||
|
||||
/// Deletes the cached data for a certain path.
|
||||
|
@ -342,15 +284,10 @@ public class WebDAV: NSObject, URLSessionDelegate {
|
|||
/// - path: The path used to download the data.
|
||||
/// - account: The WebDAV account used to download the data.
|
||||
/// - Throws: An error if the cached object URL couldn’t be created or the file can't be deleted.
|
||||
public func deleteCachedData<A: WebDAVAccount>(forItemAtPath path: String, account: A) throws {
|
||||
// It's OK to leave the password blank here, because it gets set before every call
|
||||
guard let networking = self.networking(for: account, password: ""),
|
||||
let path = networkingPath(path) else { return }
|
||||
|
||||
let destinationURL = try networking.destinationURL(for: path)
|
||||
if FileManager.default.fileExists(atPath: destinationURL.path) {
|
||||
try FileManager.default.removeItem(atPath: destinationURL.path)
|
||||
}
|
||||
func deleteCachedData<A: WebDAVAccount>(forItemAtPath path: String, account: A) throws {
|
||||
let accountPath = AccountPath(account: account, path: path)
|
||||
dataCache.removeValue(forKey: accountPath)
|
||||
imageCache.removeValue(forKey: accountPath)
|
||||
}
|
||||
|
||||
/// Get the URL used to store a resource for a certain path.
|
||||
|
@ -360,128 +297,23 @@ public class WebDAV: NSObject, URLSessionDelegate {
|
|||
/// - account: The WebDAV account used to download the data.
|
||||
/// - Throws: An error if the URL couldn’t be created.
|
||||
/// - Returns: The URL where the resource is stored.
|
||||
public func getCachedDataURL<A: WebDAVAccount>(forItemAtPath path: String, account: A) throws -> URL? {
|
||||
guard let path = networkingPath(path) else { return nil }
|
||||
return try self.networking(for: account, password: "")?.destinationURL(for: path)
|
||||
}
|
||||
|
||||
/// Get the image cached for a certain path.
|
||||
/// - Parameters:
|
||||
/// - path: The path used to download the image.
|
||||
/// - account: The WebDAV account used to download the image.
|
||||
/// - Returns: The image, if it is in the cache.
|
||||
public func getCachedImage<A: WebDAVAccount>(forItemAtPath path: String, account: A) -> UIImage? {
|
||||
guard let path = networkingPath(path) else { return nil }
|
||||
return self.networking(for: account, password: "")?.imageFromCache(path)
|
||||
}
|
||||
|
||||
/// Delete a specific cached thumbnail for a certain path and properties.
|
||||
/// - Parameters:
|
||||
/// - path: The path of the image to delete the thumbnail of.
|
||||
/// - account: The WebDAV account used to download the data.
|
||||
/// - dimensions: The dimensions of the thumbnail to delete.
|
||||
/// - aspectFill: Whether the thumbnail was fetched with aspectFill.
|
||||
/// - Throws: An error if the cached data URL couldn’t be created or the file couldn't be deleted.
|
||||
public func deleteCachedThumbnail<A: WebDAVAccount>(forItemAtPath path: String, account: A, with dimensions: CGSize?, aspectFill: Bool) throws {
|
||||
guard let networking = thumbnailNetworking(for: account, password: ""),
|
||||
let path = nextcloudPreviewPath(at: path, with: dimensions, aspectFill: aspectFill) else { return }
|
||||
|
||||
let destinationURL = try networking.destinationURL(for: path)
|
||||
if FileManager.default.fileExists(atPath: destinationURL.path) {
|
||||
try FileManager.default.removeItem(atPath: destinationURL.path)
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete all cached thumbnails for a certain path.
|
||||
/// - Parameters:
|
||||
/// - path: The path of the image to delete the thumbnails of.
|
||||
/// - account: The WebDAV account used to download the thumbnails.
|
||||
/// - Throws: An error if the cached thumbnail URLs couldn’t be created or the files couldn't be deleted.
|
||||
public func deleteAllCachedThumbnails<A: WebDAVAccount>(forItemAtPath path: String, account: A) throws {
|
||||
try getAllCachedThumbnailURLs(forItemAtPath: path, account: account).forEach { url in
|
||||
try FileManager.default.remove(at: url)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the URLs for the cached thumbnails for a certain path.
|
||||
/// - Parameters:
|
||||
/// - path: The path used to download the thumbnails.
|
||||
/// - account: The WebDAV account used to download the thumbnails.
|
||||
/// - Throws: An error if the cached thumbail URLs couldn't be created or the caches folder couldn't be accessed.
|
||||
/// - Returns: An array of the URLs of cached thumbnails for the given path.
|
||||
public func getAllCachedThumbnailURLs<A: WebDAVAccount>(forItemAtPath path: String, account: A) throws -> [URL] {
|
||||
// Getting the path with no dimensions and aspect fit will give the shortest form of the path with no extras added.
|
||||
guard let networking = thumbnailNetworking(for: account, password: ""),
|
||||
let path = nextcloudPreviewPath(at: path, with: nil, aspectFill: false),
|
||||
let networkingCacheURL = networkingCacheURL else { return [] }
|
||||
|
||||
let destinationURL = try networking.destinationURL(for: path)
|
||||
let name = destinationURL.deletingPathExtension().lastPathComponent
|
||||
|
||||
return try FileManager.default.contentsOfDirectory(at: networkingCacheURL, includingPropertiesForKeys: [], options: []).filter { url -> Bool in
|
||||
// Any cached thumbnail is going to start with this name.
|
||||
// It might also have dimensions and/or the aspect fill property after.
|
||||
url.lastPathComponent.starts(with: name)
|
||||
}
|
||||
}
|
||||
|
||||
public func getAllCachedThumbnails<A: WebDAVAccount>(forItemAtPath path: String, account: A) throws -> [UIImage] {
|
||||
// We can't use imageFromCache(path) to get the images from a memory cache
|
||||
// because we can't generate the path for every possible thumbnail. Instead
|
||||
// we'll get the URLs from getAllCachedThumbnailURLs and get the data from those.
|
||||
try getAllCachedThumbnailURLs(forItemAtPath: path, account: account).compactMap { url -> UIImage? in
|
||||
UIImage(data: try Data(contentsOf: url))
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the URL for the cached thumbnail for a certain path and properties.
|
||||
/// Useful to find where a download thumbnail is located.
|
||||
/// - Parameters:
|
||||
/// - path: The path used to download the thumbnail.
|
||||
/// - account: The WebDAV account used to download the thumbnail.
|
||||
/// - dimensions: The dimensions of the thumbnail to get.
|
||||
/// - aspectFill: Whether the thumbnail was fetched with aspectFill.
|
||||
/// - Throws: An error if the cached data URL couldn’t be created.
|
||||
/// - Returns: The URL where the thumbnail has been stored.
|
||||
public func getCachedThumbnailURL<A: WebDAVAccount>(forItemAtPath path: String, account: A, with dimensions: CGSize?, aspectFill: Bool) throws -> URL? {
|
||||
guard let path = nextcloudPreviewPath(at: path, with: dimensions, aspectFill: aspectFill) else { return nil }
|
||||
return try thumbnailNetworking(for: account, password: "")?.destinationURL(for: path)
|
||||
}
|
||||
|
||||
/// Get the thumbnail cached for a certain path and properties.
|
||||
/// - Parameters:
|
||||
/// - path: The path used to download the thumbnail.
|
||||
/// - account: The WebDAV account used to download the thumbnail.
|
||||
/// - dimensions: The dimensions of the thumbnail to get.
|
||||
/// - aspectFill: Whether the thumbnail was fetched with aspectFill.
|
||||
/// - Returns: The thumbnail, if it is in the cache.
|
||||
public func getCachedThumbnail<A: WebDAVAccount>(forItemAtPath path: String, account: A, with dimensions: CGSize?, aspectFill: Bool) -> UIImage? {
|
||||
guard let path = nextcloudPreviewPath(at: path, with: dimensions, aspectFill: aspectFill) else { return nil }
|
||||
return self.thumbnailNetworking(for: account, password: "")?.imageFromCache(path)
|
||||
func getCachedDataURL<A: WebDAVAccount>(forItemAtPath path: String, account: A) throws -> URL? {
|
||||
//TODO
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Deletes all downloaded data that has been cached.
|
||||
/// - Throws: An error if the resources couldn't be deleted.
|
||||
public func deleteAllCachedData() throws {
|
||||
guard let caches = networkingCacheURL else { return }
|
||||
try FileManager.default.remove(at: caches)
|
||||
}
|
||||
|
||||
/// Cancel a request.
|
||||
/// - Parameters:
|
||||
/// - id: The identifier of the request.
|
||||
/// - account: The WebDAV account the request was made on.
|
||||
public func cancelRequest<A: WebDAVAccount>(id: String, account: A) {
|
||||
guard let unwrappedAccount = UnwrappedAccount(account: account) else { return }
|
||||
networkings[unwrappedAccount]?.cancel(id)
|
||||
thumbnailNetworkings[unwrappedAccount]?.cancel(id)
|
||||
func deleteAllCachedData() throws {
|
||||
dataCache.removeAllValues()
|
||||
imageCache.removeAllValues()
|
||||
}
|
||||
|
||||
/// Get the total disk space for the contents of the image cache.
|
||||
/// For a formatted string of the size, see `getCacheSize`.
|
||||
/// - Returns: The total allocated space of the cache in bytes.
|
||||
public func getCacheByteCount() -> Int {
|
||||
guard let caches = networkingCacheURL,
|
||||
func getCacheByteCount() -> Int {
|
||||
guard let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first,
|
||||
let urls = FileManager.default.enumerator(at: caches, includingPropertiesForKeys: nil)?.allObjects as? [URL] else { return 0 }
|
||||
|
||||
return urls.lazy.reduce(0) { total, url -> Int in
|
||||
|
@ -494,29 +326,103 @@ public class WebDAV: NSObject, URLSessionDelegate {
|
|||
///
|
||||
/// This formats the size using this object's `byteCountFormatter` which can be modified.
|
||||
/// - Returns: A localized string of the total allocated space of the cache.
|
||||
public func getCacheSize() -> String {
|
||||
func getCacheSize() -> String {
|
||||
byteCountFormatter.string(fromByteCount: Int64(getCacheByteCount()))
|
||||
}
|
||||
|
||||
/// The URL to the directory that contains the cached image data.
|
||||
public var networkingCacheURL: URL? {
|
||||
/// The URL to the directory of the depricated Networking image data cache.
|
||||
var networkingCacheURL: URL? {
|
||||
FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.appendingPathComponent("com.3lvis.networking")
|
||||
}
|
||||
|
||||
//MARK: Cache
|
||||
/// The caching system has changed from WebDAV Swift v2 to v3.
|
||||
/// Run this function if upgrading from v2 to v3 to clear the old cache.
|
||||
/// - Throws: An error if the cache couldn't be deleted.
|
||||
func clearV2Cache() throws {
|
||||
guard let caches = networkingCacheURL,
|
||||
FileManager.default.fileExists(atPath: caches.path) else { return }
|
||||
try FileManager.default.removeItem(at: caches)
|
||||
}
|
||||
|
||||
public func clearFilesMemoryCache() {
|
||||
func clearFilesMemoryCache() {
|
||||
filesCache.removeAll()
|
||||
}
|
||||
|
||||
//MARK: Internal
|
||||
}
|
||||
|
||||
//MARK: Internal
|
||||
|
||||
extension WebDAV {
|
||||
|
||||
//MARK: Standard Requests
|
||||
|
||||
func cachingDataTask<A: WebDAVAccount, Value: Equatable>(cache: Cache<AccountPath, Value>, path: String, account: A, password: String, caching options: WebDAVCachingOptions, valueFromData: @escaping (_ data: Data) -> Value?, completion: @escaping (_ value: Value?, _ error: WebDAVError?) -> Void) -> URLSessionDataTask? {
|
||||
|
||||
// Check cache
|
||||
|
||||
var cachedValue: Value?
|
||||
let accountPath = AccountPath(account: account, path: path)
|
||||
if !options.contains(.doNotReturnCachedResult) {
|
||||
if let value = cache[accountPath] {
|
||||
completion(value, nil)
|
||||
|
||||
if !options.contains(.requestEvenIfCached) {
|
||||
if options.contains(.removeExistingCache) {
|
||||
cache.removeValue(forKey: accountPath)
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
// Remember the cached completion. If the fetched results
|
||||
// are the same, don't bother completing again.
|
||||
cachedValue = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if options.contains(.removeExistingCache) {
|
||||
cache.removeValue(forKey: accountPath)
|
||||
}
|
||||
|
||||
// Create network request
|
||||
|
||||
guard let request = authorizedRequest(path: path, account: account, password: password, method: .get) else {
|
||||
completion(nil, .invalidCredentials)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Perform network request
|
||||
|
||||
let task = URLSession(configuration: .ephemeral, delegate: self, delegateQueue: nil).dataTask(with: request) { data, response, error in
|
||||
let error = WebDAVError.getError(response: response, error: error)
|
||||
|
||||
if let data = data,
|
||||
let value = valueFromData(data) {
|
||||
// Cache result
|
||||
//TODO: Cache to disk
|
||||
if !options.contains(.removeExistingCache),
|
||||
!options.contains(.doNotCacheResult) {
|
||||
cache.set(value, forKey: accountPath)
|
||||
}
|
||||
|
||||
// Don't send a duplicate completion if the results are the same.
|
||||
if value != cachedValue {
|
||||
completion(value, error)
|
||||
}
|
||||
} else {
|
||||
completion(nil, error)
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
return task
|
||||
}
|
||||
|
||||
/// Creates a basic authentication credential.
|
||||
/// - Parameters:
|
||||
/// - username: The username
|
||||
/// - password: The password
|
||||
/// - Returns: A base-64 encoded credential if the provided credentials are valid (can be encoded as UTF-8).
|
||||
internal func auth(username: String, password: String) -> String? {
|
||||
func auth(username: String, password: String) -> String? {
|
||||
let authString = username + ":" + password
|
||||
let authData = authString.data(using: .utf8)
|
||||
return authData?.base64EncodedString()
|
||||
|
@ -529,7 +435,7 @@ public class WebDAV: NSObject, URLSessionDelegate {
|
|||
/// - password: The WebDAV password
|
||||
/// - method: The HTTP Method for the request.
|
||||
/// - Returns: The URL request if the credentials are valid (can be encoded as UTF-8).
|
||||
internal func authorizedRequest<A: WebDAVAccount>(path: String, account: A, password: String, method: HTTPMethod) -> URLRequest? {
|
||||
func authorizedRequest<A: WebDAVAccount>(path: String, account: A, password: String, method: HTTPMethod) -> URLRequest? {
|
||||
guard let unwrappedAccount = UnwrappedAccount(account: account),
|
||||
let auth = self.auth(username: unwrappedAccount.username, password: password) else { return nil }
|
||||
|
||||
|
@ -541,7 +447,7 @@ public class WebDAV: NSObject, URLSessionDelegate {
|
|||
return request
|
||||
}
|
||||
|
||||
internal func basicDataTask<A: WebDAVAccount>(path: String, destination: String? = nil, account: A, password: String, method: HTTPMethod, completion: @escaping (_ error: WebDAVError?) -> Void) -> URLSessionDataTask? {
|
||||
func basicDataTask<A: WebDAVAccount>(path: String, destination: String? = nil, account: A, password: String, method: HTTPMethod, completion: @escaping (_ error: WebDAVError?) -> Void) -> URLSessionDataTask? {
|
||||
guard var request = authorizedRequest(path: path, account: account, password: password, method: method),
|
||||
let unwrappedAccount = UnwrappedAccount(account: account) else {
|
||||
completion(.invalidCredentials)
|
||||
|
@ -561,18 +467,9 @@ public class WebDAV: NSObject, URLSessionDelegate {
|
|||
return task
|
||||
}
|
||||
|
||||
internal func networking<A: WebDAVAccount>(for account: A, password: String) -> Networking? {
|
||||
guard let unwrappedAccount = UnwrappedAccount(account: account) else { return nil }
|
||||
let networking = networkings[unwrappedAccount] ?? {
|
||||
let networking = Networking(baseURL: unwrappedAccount.baseURL.absoluteString)
|
||||
networkings[unwrappedAccount] = networking
|
||||
return networking
|
||||
}()
|
||||
networking.setAuthorizationHeader(username: unwrappedAccount.username, password: password)
|
||||
return networking
|
||||
}
|
||||
//MARK: Pathing
|
||||
|
||||
internal func nextcloudBaseURL(for baseURL: URL) -> URL? {
|
||||
func nextcloudBaseURL(for baseURL: URL) -> URL? {
|
||||
guard baseURL.absoluteString.lowercased().contains("remote.php/dav/files/"),
|
||||
let index = baseURL.pathComponents.map({ $0.lowercased() }).firstIndex(of: "remote.php") else { return nil }
|
||||
|
||||
|
@ -586,63 +483,4 @@ public class WebDAV: NSObject, URLSessionDelegate {
|
|||
return previewURL
|
||||
}
|
||||
|
||||
internal func nextcloudPreviewBaseURL(for baseURL: URL) -> URL? {
|
||||
return nextcloudBaseURL(for: baseURL)?
|
||||
.appendingPathComponent("index.php")
|
||||
.appendingPathComponent("core")
|
||||
.appendingPathComponent("preview.png")
|
||||
}
|
||||
|
||||
internal func thumbnailNetworking<A: WebDAVAccount>(for account: A, password: String) -> Networking? {
|
||||
guard let unwrappedAccount = UnwrappedAccount(account: account),
|
||||
let previewURL = nextcloudPreviewBaseURL(for: unwrappedAccount.baseURL) else { return nil }
|
||||
|
||||
let networking = thumbnailNetworkings[unwrappedAccount] ?? {
|
||||
let networking = Networking(baseURL: previewURL.absoluteString)
|
||||
thumbnailNetworkings[unwrappedAccount] = networking
|
||||
return networking
|
||||
}()
|
||||
|
||||
networking.setAuthorizationHeader(username: unwrappedAccount.username, password: password)
|
||||
return networking
|
||||
}
|
||||
|
||||
internal func networkingPath(_ path: String) -> String? {
|
||||
let slashPath = path.first == "/" ? path : "/" + path
|
||||
return slashPath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)
|
||||
}
|
||||
|
||||
internal func nextcloudPreviewPath(at path: String, with dimensions: CGSize?, aspectFill: Bool = true) -> String? {
|
||||
guard var encodedPath = path.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { return nil }
|
||||
|
||||
if encodedPath.hasPrefix("/") {
|
||||
encodedPath.removeFirst()
|
||||
}
|
||||
|
||||
var thumbnailPath = "?file=\(encodedPath)&mode=cover"
|
||||
|
||||
if let dimensions = dimensions {
|
||||
thumbnailPath += "&x=\(dimensions.width)&y=\(dimensions.height)"
|
||||
}
|
||||
|
||||
if aspectFill {
|
||||
thumbnailPath += "&a=1"
|
||||
}
|
||||
|
||||
return thumbnailPath
|
||||
}
|
||||
|
||||
//MARK: Static
|
||||
|
||||
public static func sortedFiles(_ files: [WebDAVFile], foldersFirst: Bool, includeSelf: Bool) -> [WebDAVFile] {
|
||||
var files = files
|
||||
if !includeSelf, !files.isEmpty {
|
||||
files.removeFirst()
|
||||
}
|
||||
if foldersFirst {
|
||||
files = files.filter { $0.isDirectory } + files.filter { !$0.isDirectory }
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import Foundation
|
|||
/// **Default behavior** (empty set):
|
||||
/// If there is a cached result, return that instead of making a request.
|
||||
/// Otherwise, make a request and cache the result.
|
||||
public struct WebDAVCacheOptions: OptionSet {
|
||||
public struct WebDAVCachingOptions: OptionSet {
|
||||
public let rawValue: Int
|
||||
|
||||
public init(rawValue: Int) {
|
||||
|
@ -18,18 +18,18 @@ public struct WebDAVCacheOptions: OptionSet {
|
|||
}
|
||||
|
||||
/// Do not cache the results of this request, if one is made.
|
||||
public static let doNotCacheResult = WebDAVCacheOptions(rawValue: 1 << 0)
|
||||
public static let doNotCacheResult = WebDAVCachingOptions(rawValue: 1 << 0)
|
||||
/// Remove the cached value for this request.
|
||||
public static let removeExistingCache = WebDAVCacheOptions(rawValue: 1 << 1)
|
||||
public static let removeExistingCache = WebDAVCachingOptions(rawValue: 1 << 1)
|
||||
/// If there is a cached result, ignore it and make a request.
|
||||
public static let doNotReturnCachedResult = WebDAVCacheOptions(rawValue: 1 << 2)
|
||||
public static let doNotReturnCachedResult = WebDAVCachingOptions(rawValue: 1 << 2)
|
||||
/// If there is a cached result, return that, then make a request, returing that result again if it is different.
|
||||
public static let requestEvenIfCached = WebDAVCacheOptions(rawValue: 1 << 3)
|
||||
public static let requestEvenIfCached = WebDAVCachingOptions(rawValue: 1 << 3)
|
||||
|
||||
/// Disable all caching for this request including deleting any existing cache for it.
|
||||
/// Same as `[.doNotCacheResult, .removeExistingCache, .doNotReturnCachedResult]`.
|
||||
public static let disableCache: WebDAVCacheOptions = [.doNotCacheResult, .removeExistingCache, .doNotReturnCachedResult]
|
||||
public static let disableCache: WebDAVCachingOptions = [.doNotCacheResult, .removeExistingCache, .doNotReturnCachedResult]
|
||||
/// Ignore the cached result if there is one, and don't cache the new result.
|
||||
/// Same as `[.doNotCacheResult, .doNotReturnCachedResult]`.
|
||||
public static let ignoreCache: WebDAVCacheOptions = [.doNotCacheResult, .doNotReturnCachedResult]
|
||||
public static let ignoreCache: WebDAVCachingOptions = [.doNotCacheResult, .doNotReturnCachedResult]
|
||||
}
|
|
@ -8,13 +8,15 @@
|
|||
import Foundation
|
||||
|
||||
public enum WebDAVError: Error {
|
||||
/// The DAVAccount was unable to be encoded to base 64.
|
||||
/// The credentials or path were unable to be encoded.
|
||||
/// No network request was called.
|
||||
case invalidCredentials
|
||||
/// The credentials were incorrect.
|
||||
case unauthorized
|
||||
/// The server was unable to store the data provided.
|
||||
case insufficientStorage
|
||||
/// The server does not support this feature.
|
||||
case unsupported
|
||||
/// Another unspecified Error occurred.
|
||||
case nsError(Error)
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import XCTest
|
||||
@testable import WebDAV
|
||||
import Networking
|
||||
|
||||
final class WebDAVTests: XCTestCase {
|
||||
var webDAV = WebDAV()
|
||||
|
@ -354,6 +353,9 @@ final class WebDAVTests: XCTestCase {
|
|||
|
||||
//MARK: Image Cache
|
||||
|
||||
// Commented out lines are lines that existed to test image cache in v2.x versions.
|
||||
// They will be added again when disk cache is reimplemented in v3.0.
|
||||
|
||||
func testDownloadImage() {
|
||||
guard let (account, password) = getAccount() else { return XCTFail() }
|
||||
guard let imagePath = ProcessInfo.processInfo.environment["image_path"] else {
|
||||
|
@ -373,11 +375,13 @@ final class WebDAVTests: XCTestCase {
|
|||
|
||||
downloadImage(imagePath: imagePath, account: account, password: password)
|
||||
|
||||
let cachedImageURL = try webDAV.getCachedDataURL(forItemAtPath: imagePath, account: account)!
|
||||
XCTAssert(FileManager.default.fileExists(atPath: cachedImageURL.path))
|
||||
// let cachedImageURL = try webDAV.getCachedDataURL(forItemAtPath: imagePath, account: account)!
|
||||
// XCTAssert(FileManager.default.fileExists(atPath: cachedImageURL.path))
|
||||
XCTAssertNotNil(webDAV.getCachedImage(forItemAtPath: imagePath, account: account))
|
||||
|
||||
try webDAV.deleteCachedData(forItemAtPath: imagePath, account: account)
|
||||
XCTAssertFalse(FileManager.default.fileExists(atPath: cachedImageURL.path))
|
||||
XCTAssertNil(webDAV.getCachedImage(forItemAtPath: imagePath, account: account))
|
||||
// XCTAssertFalse(FileManager.default.fileExists(atPath: cachedImageURL.path))
|
||||
}
|
||||
|
||||
func testDeleteAllCachedData() throws {
|
||||
|
@ -388,11 +392,16 @@ final class WebDAVTests: XCTestCase {
|
|||
|
||||
downloadImage(imagePath: imagePath, account: account, password: password)
|
||||
|
||||
let cachedImageURL = try webDAV.getCachedDataURL(forItemAtPath: imagePath, account: account)!
|
||||
let accountPath = AccountPath(account: account, path: imagePath)
|
||||
// let cachedImageURL = try webDAV.getCachedDataURL(forItemAtPath: imagePath, account: account)!
|
||||
XCTAssertNotNil(webDAV.imageCache[accountPath])
|
||||
|
||||
XCTAssertNoThrow(try webDAV.deleteAllCachedData())
|
||||
XCTAssertFalse(FileManager.default.fileExists(atPath: cachedImageURL.path))
|
||||
XCTAssertNil(webDAV.imageCache[accountPath])
|
||||
// XCTAssertFalse(FileManager.default.fileExists(atPath: cachedImageURL.path))
|
||||
}
|
||||
|
||||
|
||||
//MARK: Thumbnails
|
||||
|
||||
func testDownloadThumbnail() {
|
||||
|
@ -403,7 +412,7 @@ final class WebDAVTests: XCTestCase {
|
|||
|
||||
downloadThumbnail(imagePath: imagePath, account: account, password: password)
|
||||
|
||||
XCTAssertNoThrow(try webDAV.deleteCachedThumbnail(forItemAtPath: imagePath, account: account, with: nil, aspectFill: false))
|
||||
XCTAssertNoThrow(try webDAV.deleteCachedThumbnail(forItemAtPath: imagePath, account: account, with: .fill))
|
||||
}
|
||||
|
||||
func testSpecificThumbnailCache() throws {
|
||||
|
@ -412,13 +421,15 @@ final class WebDAVTests: XCTestCase {
|
|||
return XCTFail("You need to set the image_path in the environment.")
|
||||
}
|
||||
|
||||
downloadThumbnail(imagePath: imagePath, account: account, password: password)
|
||||
downloadThumbnail(imagePath: imagePath, account: account, password: password, with: .fill)
|
||||
|
||||
let cachedThumbnailURL = try webDAV.getCachedThumbnailURL(forItemAtPath: imagePath, account: account, with: nil, aspectFill: false)!
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: cachedThumbnailURL.path))
|
||||
XCTAssertNotNil(webDAV.getCachedThumbnail(forItemAtPath: imagePath, account: account, with: nil, aspectFill: false))
|
||||
try webDAV.deleteCachedThumbnail(forItemAtPath: imagePath, account: account, with: nil, aspectFill: false)
|
||||
XCTAssertFalse(FileManager.default.fileExists(atPath: cachedThumbnailURL.path))
|
||||
// let cachedThumbnailURL = try webDAV.getCachedThumbnailURL(forItemAtPath: imagePath, account: account, with: nil, aspectFill: false)!
|
||||
// XCTAssertTrue(FileManager.default.fileExists(atPath: cachedThumbnailURL.path))
|
||||
XCTAssertNotNil(webDAV.getCachedThumbnail(forItemAtPath: imagePath, account: account, with: .fill))
|
||||
|
||||
try webDAV.deleteCachedThumbnail(forItemAtPath: imagePath, account: account, with: .fill)
|
||||
XCTAssertNil(webDAV.getCachedThumbnail(forItemAtPath: imagePath, account: account, with: .fill))
|
||||
// XCTAssertFalse(FileManager.default.fileExists(atPath: cachedThumbnailURL.path))
|
||||
}
|
||||
|
||||
func testGeneralThumbnailCache() throws {
|
||||
|
@ -427,20 +438,21 @@ final class WebDAVTests: XCTestCase {
|
|||
return XCTFail("You need to set the image_path in the environment.")
|
||||
}
|
||||
|
||||
downloadThumbnail(imagePath: imagePath, account: account, password: password, with: nil, aspectFill: true)
|
||||
downloadThumbnail(imagePath: imagePath, account: account, password: password, with: nil, aspectFill: false)
|
||||
downloadThumbnail(imagePath: imagePath, account: account, password: password, with: .fill)
|
||||
downloadThumbnail(imagePath: imagePath, account: account, password: password, with: .fit)
|
||||
|
||||
let cachedThumbnailFillURL = try webDAV.getCachedThumbnailURL(forItemAtPath: imagePath, account: account, with: nil, aspectFill: true)!
|
||||
let cachedThumbnailFitURL = try webDAV.getCachedThumbnailURL(forItemAtPath: imagePath, account: account, with: nil, aspectFill: false)!
|
||||
// let cachedThumbnailFillURL = try webDAV.getCachedThumbnailURL(forItemAtPath: imagePath, account: account, with: nil, aspectFill: true)!
|
||||
// let cachedThumbnailFitURL = try webDAV.getCachedThumbnailURL(forItemAtPath: imagePath, account: account, with: nil, aspectFill: false)!
|
||||
|
||||
XCTAssert(FileManager.default.fileExists(atPath: cachedThumbnailFillURL.path))
|
||||
XCTAssert(FileManager.default.fileExists(atPath: cachedThumbnailFitURL.path))
|
||||
XCTAssertEqual(try webDAV.getAllCachedThumbnails(forItemAtPath: imagePath, account: account).count, 2)
|
||||
// XCTAssert(FileManager.default.fileExists(atPath: cachedThumbnailFillURL.path))
|
||||
// XCTAssert(FileManager.default.fileExists(atPath: cachedThumbnailFitURL.path))
|
||||
XCTAssertEqual(webDAV.getAllCachedThumbnails(forItemAtPath: imagePath, account: account)?.count, 2)
|
||||
|
||||
// Delete all cached thumbnails and check that they're both gone
|
||||
try webDAV.deleteAllCachedThumbnails(forItemAtPath: imagePath, account: account)
|
||||
XCTAssertFalse(FileManager.default.fileExists(atPath: cachedThumbnailFillURL.path))
|
||||
XCTAssertFalse(FileManager.default.fileExists(atPath: cachedThumbnailFitURL.path))
|
||||
XCTAssertNil(webDAV.getAllCachedThumbnails(forItemAtPath: imagePath, account: account))
|
||||
// XCTAssertFalse(FileManager.default.fileExists(atPath: cachedThumbnailFillURL.path))
|
||||
// XCTAssertFalse(FileManager.default.fileExists(atPath: cachedThumbnailFitURL.path))
|
||||
}
|
||||
|
||||
//MARK: OCS
|
||||
|
@ -555,29 +567,25 @@ final class WebDAVTests: XCTestCase {
|
|||
|
||||
try? webDAV.deleteCachedData(forItemAtPath: imagePath, account: account)
|
||||
|
||||
webDAV.downloadImage(path: imagePath, account: account, password: password) { image, cachedImageURL, error in
|
||||
webDAV.downloadImage(path: imagePath, account: account, password: password) { image, error in
|
||||
XCTAssertNil(error)
|
||||
XCTAssertNotNil(image)
|
||||
|
||||
XCTAssert(FileManager.default.fileExists(atPath: cachedImageURL!.path))
|
||||
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
wait(for: [expectation], timeout: 10.0)
|
||||
}
|
||||
|
||||
private func downloadThumbnail(imagePath: String, account: SimpleAccount, password: String, with dimensions: CGSize? = nil, aspectFill: Bool = false) {
|
||||
private func downloadThumbnail(imagePath: String, account: SimpleAccount, password: String, with properties: ThumbnailProperties = .fill) {
|
||||
let expectation = XCTestExpectation(description: "Download thumbnail from WebDAV")
|
||||
|
||||
try? webDAV.deleteCachedThumbnail(forItemAtPath: imagePath, account: account, with: dimensions, aspectFill: aspectFill)
|
||||
try? webDAV.deleteCachedThumbnail(forItemAtPath: imagePath, account: account, with: properties)
|
||||
|
||||
webDAV.downloadThumbnail(path: imagePath, account: account, password: password, with: dimensions, aspectFill: aspectFill) { image, cachedImageURL, error in
|
||||
webDAV.downloadThumbnail(path: imagePath, account: account, password: password, with: properties) { image, error in
|
||||
XCTAssertNil(error)
|
||||
XCTAssertNotNil(image)
|
||||
|
||||
XCTAssert(FileManager.default.fileExists(atPath: cachedImageURL!.path))
|
||||
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
|
@ -605,12 +613,14 @@ final class WebDAVTests: XCTestCase {
|
|||
("testFilesCacheDoubleRequest", testFilesCacheDoubleRequest),
|
||||
// Image Cache
|
||||
("testDownloadImage", testDownloadImage),
|
||||
/*
|
||||
("testImageCache", testImageCache),
|
||||
("testDeleteAllCachedData", testDeleteAllCachedData),
|
||||
// Thumbnails
|
||||
("testDownloadThumbnail", testDownloadThumbnail),
|
||||
("testSpecificThumbnailCache", testSpecificThumbnailCache),
|
||||
("testGeneralThumbnailCache", testGeneralThumbnailCache),
|
||||
*/
|
||||
// OCS
|
||||
("testTheme", testTheme),
|
||||
("testColorHex", testColorHex)
|
||||
|
|
Loading…
Reference in New Issue