From 41314d67fcc688acc2c6afd181bc26329374e597 Mon Sep 17 00:00:00 2001 From: Isvvc Date: Fri, 16 Apr 2021 17:37:44 -0600 Subject: [PATCH 1/9] Add description and encoded description properties to UnwrappedAccount --- Sources/WebDAV/Account.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Sources/WebDAV/Account.swift b/Sources/WebDAV/Account.swift index a2ac778..4316bf6 100644 --- a/Sources/WebDAV/Account.swift +++ b/Sources/WebDAV/Account.swift @@ -39,6 +39,22 @@ internal struct UnwrappedAccount: Hashable { self.username = username self.baseURL = baseURL } + + /// Description of the unwrapped account in the format "username@baseURL". + var description: String { + "\(username)@\(baseURL.absoluteString)" + } + + /// Description of the unwrapped account in the format "username@baseURL" + /// with the baseURL encoded. + /// + /// Replaces slashes with colons (for easier reading on macOS) + /// and other special characters with their percent encoding. + /// - Note: Only the baseURL is encoded. The username and @ symbol are unchanged. + var encodedDescription: String? { + guard let encodedURL = baseURL.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else { return nil } + return "\(username)@\(encodedURL.replacingOccurrences(of: "%2F", with: ":"))" + } } //MARK: AccountPath From fc6c4c901ec68b37d2c3e91640cc7afaf92ab72c Mon Sep 17 00:00:00 2001 From: Isvvc Date: Fri, 16 Apr 2021 17:38:44 -0600 Subject: [PATCH 2/9] Add functions to get cached data from disk --- Sources/WebDAV/Account.swift | 2 +- Sources/WebDAV/WebDAV+DiskCache.swift | 34 +++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/Sources/WebDAV/Account.swift b/Sources/WebDAV/Account.swift index 4316bf6..e5e8f7a 100644 --- a/Sources/WebDAV/Account.swift +++ b/Sources/WebDAV/Account.swift @@ -60,7 +60,7 @@ internal struct UnwrappedAccount: Hashable { //MARK: AccountPath public struct AccountPath: Hashable, Codable { - private static let slash = CharacterSet(charactersIn: "/") + static let slash = CharacterSet(charactersIn: "/") var username: String? var baseURL: String? diff --git a/Sources/WebDAV/WebDAV+DiskCache.swift b/Sources/WebDAV/WebDAV+DiskCache.swift index 1a9bc0f..18e6301 100644 --- a/Sources/WebDAV/WebDAV+DiskCache.swift +++ b/Sources/WebDAV/WebDAV+DiskCache.swift @@ -7,6 +7,27 @@ import Foundation +//MARK: Public + +public extension WebDAV { + + func cachedDataURL(forItemAtPath path: String, account: A) -> URL? { + guard let encodedDescription = UnwrappedAccount(account: account)?.encodedDescription, + let caches = cacheFolder else { return nil } + return caches + .appendingPathComponent(encodedDescription) + .appendingPathComponent(path.trimmingCharacters(in: AccountPath.slash)) + } + + func cachedDataURLIfExists(forItemAtPath path: String, account: A) -> URL? { + guard let url = cachedDataURL(forItemAtPath: path, account: account) else { return nil } + return FileManager.default.fileExists(atPath: url.path) ? url : nil + } + +} + +//MARK: Internal + extension WebDAV { var cacheFolder: URL? { @@ -25,6 +46,19 @@ extension WebDAV { return directory } + //MARK: Data Cache + + func loadCachedValueFromDisk(cache: Cache, forItemAtPath path: String, account: A, valueFromData: @escaping (_ data: Data) -> Value?) -> Value? { + guard let url = cachedDataURL(forItemAtPath: path, account: account), + FileManager.default.fileExists(atPath: url.path), + let data = try? Data(contentsOf: url), + let value = valueFromData(data) else { return nil } + cache[AccountPath(account: account, path: path)] = value + return value + } + + //MARK: Files Cache + var filesCacheURL: URL? { cacheFolder?.appendingPathComponent("files.plist") } From 2634005b34811177e21e9a9b43143bb9b5f2155e Mon Sep 17 00:00:00 2001 From: Isvvc Date: Fri, 16 Apr 2021 17:40:37 -0600 Subject: [PATCH 3/9] Update getCachedValue to read disk cache --- Sources/WebDAV/WebDAV+Images.swift | 4 ++-- Sources/WebDAV/WebDAV.swift | 23 +++++++++++++++++++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/Sources/WebDAV/WebDAV+Images.swift b/Sources/WebDAV/WebDAV+Images.swift index c6059a0..108dc2f 100644 --- a/Sources/WebDAV/WebDAV+Images.swift +++ b/Sources/WebDAV/WebDAV+Images.swift @@ -182,13 +182,13 @@ public extension WebDAV { //MARK: Image Cache func getCachedImage(forItemAtPath path: String, account: A) -> UIImage? { - getCachedValue(cache: imageCache, forItemAtPath: path, account: account) + getCachedValue(cache: imageCache, forItemAtPath: path, account: account, valueFromData: { UIImage(data: $0) }) } //MARK: Thumbnail Cache func getAllCachedThumbnails(forItemAtPath path: String, account: A) -> [ThumbnailProperties: UIImage]? { - getCachedValue(cache: thumbnailCache, forItemAtPath: path, account: account) + getCachedValue(from: thumbnailCache, forItemAtPath: path, account: account) } func getCachedThumbnail(forItemAtPath path: String, account: A, with properties: ThumbnailProperties) -> UIImage? { diff --git a/Sources/WebDAV/WebDAV.swift b/Sources/WebDAV/WebDAV.swift index 7bcc65e..b6a162f 100644 --- a/Sources/WebDAV/WebDAV.swift +++ b/Sources/WebDAV/WebDAV.swift @@ -272,13 +272,32 @@ public extension WebDAV { //MARK: Cache func getCachedData(forItemAtPath path: String, account: A) -> Data? { - getCachedValue(cache: dataCache, forItemAtPath: path, account: account) + getCachedValue(cache: dataCache, forItemAtPath: path, account: account, valueFromData: { $0 }) } - func getCachedValue(cache: Cache, forItemAtPath path: String, account: A) -> Value? { + /// Get the cached value for a specified path directly from the memory cache. + /// - Parameters: + /// - cache: The memory cache the data is stored in. + /// - path: The path used to download the data. + /// - account: The WebDAV account used to download the data. + /// - Returns: The cached data if it is available in the given memory cache. + func getCachedValue(from cache: Cache, forItemAtPath path: String, account: A) -> Value? { cache[AccountPath(account: account, path: path)] } + /// Get the cached value for a specified path from the memory cache if available. + /// Otherwise load it from disk and save to memory cache. + /// - Parameters: + /// - cache: The memory cache for the value. + /// - path: The path used to download the data. + /// - account: The WebDAV account used to download the data. + /// - valueFromData: Convert `Data` to the desired value type. + /// - Returns: The cached data if it is available. + func getCachedValue(cache: Cache, forItemAtPath path: String, account: A, valueFromData: @escaping (_ data: Data) -> Value?) -> Value? { + getCachedValue(from: cache, forItemAtPath: path, account: account) ?? + loadCachedValueFromDisk(cache: cache, forItemAtPath: path, account: account, valueFromData: valueFromData) + } + /// Deletes the cached data for a certain path. /// - Parameters: /// - path: The path used to download the data. From 8dfa937f1adb459b97ea797dd6b9411519224719 Mon Sep 17 00:00:00 2001 From: Isvvc Date: Fri, 16 Apr 2021 17:53:30 -0600 Subject: [PATCH 4/9] Add deleteCachedDataFromDisk function --- Sources/WebDAV/WebDAV+DiskCache.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/WebDAV/WebDAV+DiskCache.swift b/Sources/WebDAV/WebDAV+DiskCache.swift index 18e6301..5f31c21 100644 --- a/Sources/WebDAV/WebDAV+DiskCache.swift +++ b/Sources/WebDAV/WebDAV+DiskCache.swift @@ -24,6 +24,11 @@ public extension WebDAV { return FileManager.default.fileExists(atPath: url.path) ? url : nil } + func deleteCachedDataFromDisk(forItemAtPath path: String, account: A) throws { + guard let url = cachedDataURLIfExists(forItemAtPath: path, account: account) else { return } + try FileManager.default.removeItem(at: url) + } + } //MARK: Internal From fb25d5bc2981e15322b51d87fb6dfe9caae09ed0 Mon Sep 17 00:00:00 2001 From: Isvvc Date: Fri, 16 Apr 2021 17:54:07 -0600 Subject: [PATCH 5/9] Load data from cache in cachingDataTask --- Sources/WebDAV/WebDAV.swift | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/Sources/WebDAV/WebDAV.swift b/Sources/WebDAV/WebDAV.swift index b6a162f..93b7f0f 100644 --- a/Sources/WebDAV/WebDAV.swift +++ b/Sources/WebDAV/WebDAV.swift @@ -302,23 +302,13 @@ public extension WebDAV { /// - Parameters: /// - 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. + /// - Throws: An error if the file can't be deleted. func deleteCachedData(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. - /// Useful to find where a download image is located. - /// - Parameters: - /// - path: The path used to download the data. - /// - 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. - func getCachedDataURL(forItemAtPath path: String, account: A) throws -> URL? { - //TODO - return nil + + try deleteCachedDataFromDisk(forItemAtPath: path, account: account) } /// Deletes all downloaded data that has been cached. @@ -382,12 +372,12 @@ extension WebDAV { var cachedValue: Value? let accountPath = AccountPath(account: account, path: path) if !options.contains(.doNotReturnCachedResult) { - if let value = cache[accountPath] { + if let value = getCachedValue(cache: cache, forItemAtPath: path, account: account, valueFromData: valueFromData) { completion(value, nil) if !options.contains(.requestEvenIfCached) { if options.contains(.removeExistingCache) { - cache.removeValue(forKey: accountPath) + try? deleteCachedData(forItemAtPath: path, account: account) } return nil } else { @@ -399,7 +389,7 @@ extension WebDAV { } if options.contains(.removeExistingCache) { - cache.removeValue(forKey: accountPath) + try? deleteCachedData(forItemAtPath: path, account: account) } // Create network request From 3c3e30c60c75f4acbc0168752a9b14ca33c4cfa6 Mon Sep 17 00:00:00 2001 From: Isvvc Date: Fri, 16 Apr 2021 21:14:42 -0600 Subject: [PATCH 6/9] Implement saving downloaded data to cache --- Sources/WebDAV/WebDAV+DiskCache.swift | 10 ++++++++++ Sources/WebDAV/WebDAV.swift | 18 +++++++++++++----- Tests/WebDAVTests/WebDAVTests.swift | 6 +++--- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/Sources/WebDAV/WebDAV+DiskCache.swift b/Sources/WebDAV/WebDAV+DiskCache.swift index 5f31c21..40f86f3 100644 --- a/Sources/WebDAV/WebDAV+DiskCache.swift +++ b/Sources/WebDAV/WebDAV+DiskCache.swift @@ -62,6 +62,16 @@ extension WebDAV { return value } + func saveDataToDiskCache(_ data: Data, forItemAtPath path: String, account: A) throws { + guard let url = cachedDataURL(forItemAtPath: path, account: account) else { return } + let directory = url.deletingLastPathComponent() + + if !FileManager.default.fileExists(atPath: directory.path) { + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + } + try data.write(to: url) + } + //MARK: Files Cache var filesCacheURL: URL? { diff --git a/Sources/WebDAV/WebDAV.swift b/Sources/WebDAV/WebDAV.swift index 93b7f0f..147ec5e 100644 --- a/Sources/WebDAV/WebDAV.swift +++ b/Sources/WebDAV/WebDAV.swift @@ -401,16 +401,24 @@ extension WebDAV { // 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) + let task = URLSession(configuration: .ephemeral, delegate: self, delegateQueue: nil).dataTask(with: request) { [weak self] data, response, error in + var error = WebDAVError.getError(response: response, error: error) - if let data = data, + if let error = error { + return completion(nil, error) + } else if let data = data, let value = valueFromData(data) { // Cache result - //TODO: Cache to disk if !options.contains(.removeExistingCache), !options.contains(.doNotCacheResult) { + // Memory cache cache.set(value, forKey: accountPath) + // Disk cache + do { + try self?.saveDataToDiskCache(data, forItemAtPath: path, account: account) + } catch let cachingError { + error = .nsError(cachingError) + } } // Don't send a duplicate completion if the results are the same. @@ -418,7 +426,7 @@ extension WebDAV { completion(value, error) } } else { - completion(nil, error) + completion(nil, nil) } } diff --git a/Tests/WebDAVTests/WebDAVTests.swift b/Tests/WebDAVTests/WebDAVTests.swift index b210d1d..b5d77e4 100644 --- a/Tests/WebDAVTests/WebDAVTests.swift +++ b/Tests/WebDAVTests/WebDAVTests.swift @@ -375,13 +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 = webDAV.cachedDataURL(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) XCTAssertNil(webDAV.getCachedImage(forItemAtPath: imagePath, account: account)) -// XCTAssertFalse(FileManager.default.fileExists(atPath: cachedImageURL.path)) + XCTAssertFalse(FileManager.default.fileExists(atPath: cachedImageURL.path)) } func testDeleteAllCachedData() throws { From c08f7bd4a33b679afa167495282a584a070c3955 Mon Sep 17 00:00:00 2001 From: Isvvc Date: Fri, 16 Apr 2021 21:15:10 -0600 Subject: [PATCH 7/9] Reimplement deleteAllCachedData with deleting disk cache --- Sources/WebDAV/WebDAV+DiskCache.swift | 9 +++++++++ Sources/WebDAV/WebDAV.swift | 1 + Tests/WebDAVTests/WebDAVTests.swift | 5 ++--- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Sources/WebDAV/WebDAV+DiskCache.swift b/Sources/WebDAV/WebDAV+DiskCache.swift index 40f86f3..5e97fb0 100644 --- a/Sources/WebDAV/WebDAV+DiskCache.swift +++ b/Sources/WebDAV/WebDAV+DiskCache.swift @@ -29,6 +29,15 @@ public extension WebDAV { try FileManager.default.removeItem(at: url) } + func deleteAllDiskCachedData() throws { + guard let url = cacheFolder else { return } + let fm = FileManager.default + let filesCachePath = filesCacheURL?.path + for item in try fm.contentsOfDirectory(atPath: url.path) where item != filesCachePath { + try fm.removeItem(atPath: url.appendingPathComponent(item).path) + } + } + } //MARK: Internal diff --git a/Sources/WebDAV/WebDAV.swift b/Sources/WebDAV/WebDAV.swift index 147ec5e..151761e 100644 --- a/Sources/WebDAV/WebDAV.swift +++ b/Sources/WebDAV/WebDAV.swift @@ -316,6 +316,7 @@ public extension WebDAV { func deleteAllCachedData() throws { dataCache.removeAllValues() imageCache.removeAllValues() + try deleteAllDiskCachedData() } /// Get the total disk space for the contents of the image cache. diff --git a/Tests/WebDAVTests/WebDAVTests.swift b/Tests/WebDAVTests/WebDAVTests.swift index b5d77e4..f84ed93 100644 --- a/Tests/WebDAVTests/WebDAVTests.swift +++ b/Tests/WebDAVTests/WebDAVTests.swift @@ -393,15 +393,14 @@ final class WebDAVTests: XCTestCase { downloadImage(imagePath: imagePath, account: account, password: password) let accountPath = AccountPath(account: account, path: imagePath) -// let cachedImageURL = try webDAV.getCachedDataURL(forItemAtPath: imagePath, account: account)! + let cachedImageURL = webDAV.cachedDataURL(forItemAtPath: imagePath, account: account)! XCTAssertNotNil(webDAV.imageCache[accountPath]) XCTAssertNoThrow(try webDAV.deleteAllCachedData()) XCTAssertNil(webDAV.imageCache[accountPath]) -// XCTAssertFalse(FileManager.default.fileExists(atPath: cachedImageURL.path)) + XCTAssertFalse(FileManager.default.fileExists(atPath: cachedImageURL.path)) } - //MARK: Thumbnails func testDownloadThumbnail() { From 14cf8f15c4e56b749e29dd85d8baff9b1e008943 Mon Sep 17 00:00:00 2001 From: Isvvc Date: Sat, 17 Apr 2021 13:12:31 -0600 Subject: [PATCH 8/9] Implement cachedThumbnailURL function --- Sources/WebDAV/WebDAV+DiskCache.swift | 16 ++++++++++++++++ Tests/WebDAVTests/WebDAVTests.swift | 7 +++++++ 2 files changed, 23 insertions(+) diff --git a/Sources/WebDAV/WebDAV+DiskCache.swift b/Sources/WebDAV/WebDAV+DiskCache.swift index 5e97fb0..cabc3da 100644 --- a/Sources/WebDAV/WebDAV+DiskCache.swift +++ b/Sources/WebDAV/WebDAV+DiskCache.swift @@ -11,6 +11,8 @@ import Foundation public extension WebDAV { + //MARK: Data + func cachedDataURL(forItemAtPath path: String, account: A) -> URL? { guard let encodedDescription = UnwrappedAccount(account: account)?.encodedDescription, let caches = cacheFolder else { return nil } @@ -38,6 +40,20 @@ public extension WebDAV { } } + //MARK: Thumbnails + + func cachedThumbnailURL(forItemAtPath path: String, account: A, with properties: ThumbnailProperties) -> URL? { + guard let imageURL = cachedDataURL(forItemAtPath: path, account: account) else { return nil } + + var components = URLComponents(string: imageURL.absoluteString) + // The first query item is the path, but we're storing the + // thumbnail in its path already anyway, so we can remove it. + if let query = nextcloudPreviewQuery(at: path, properties: properties)?.dropFirst() { + components?.queryItems = Array(query) + } + return components?.url + } + } //MARK: Internal diff --git a/Tests/WebDAVTests/WebDAVTests.swift b/Tests/WebDAVTests/WebDAVTests.swift index f84ed93..8b8652f 100644 --- a/Tests/WebDAVTests/WebDAVTests.swift +++ b/Tests/WebDAVTests/WebDAVTests.swift @@ -414,6 +414,13 @@ final class WebDAVTests: XCTestCase { XCTAssertNoThrow(try webDAV.deleteCachedThumbnail(forItemAtPath: imagePath, account: account, with: .fill)) } + func testThumbnailCacheURL() { + guard let (account, _) = getAccount() else { return XCTFail() } + guard let url = webDAV.cachedThumbnailURL(forItemAtPath: "fakeImage.png", account: account, with: .init((width: 512, height: 512), contentMode: .fill)), + let query = url.query else { return XCTFail("Could not get URL") } + XCTAssertEqual("\(url.lastPathComponent)?\(query)", "fakeImage.png?mode=cover&x=512&y=512&a=1") + } + func testSpecificThumbnailCache() throws { guard let (account, password) = getAccount() else { return XCTFail() } guard let imagePath = ProcessInfo.processInfo.environment["image_path"] else { From df49540496ce33611a5cc300f859fb8bba57b290 Mon Sep 17 00:00:00 2001 From: Isvvc Date: Sat, 17 Apr 2021 14:32:41 -0600 Subject: [PATCH 9/9] Implement disk caching for thumbnails Restore tests with removed functionality --- Sources/WebDAV/WebDAV+DiskCache.swift | 62 +++++++++++++++++++++++---- Sources/WebDAV/WebDAV+Images.swift | 40 ++++++++++++----- Sources/WebDAV/WebDAV.swift | 2 +- Tests/WebDAVTests/WebDAVTests.swift | 49 ++++++++++----------- 4 files changed, 106 insertions(+), 47 deletions(-) diff --git a/Sources/WebDAV/WebDAV+DiskCache.swift b/Sources/WebDAV/WebDAV+DiskCache.swift index cabc3da..885c892 100644 --- a/Sources/WebDAV/WebDAV+DiskCache.swift +++ b/Sources/WebDAV/WebDAV+DiskCache.swift @@ -5,7 +5,7 @@ // Created by Isaac Lyons on 4/7/21. // -import Foundation +import UIKit //MARK: Public @@ -36,7 +36,7 @@ public extension WebDAV { let fm = FileManager.default let filesCachePath = filesCacheURL?.path for item in try fm.contentsOfDirectory(atPath: url.path) where item != filesCachePath { - try fm.removeItem(atPath: url.appendingPathComponent(item).path) + try fm.removeItem(at: url.appendingPathComponent(item)) } } @@ -45,13 +45,37 @@ public extension WebDAV { func cachedThumbnailURL(forItemAtPath path: String, account: A, with properties: ThumbnailProperties) -> URL? { guard let imageURL = cachedDataURL(forItemAtPath: path, account: account) else { return nil } - var components = URLComponents(string: imageURL.absoluteString) - // The first query item is the path, but we're storing the - // thumbnail in its path already anyway, so we can remove it. + // If the query is stored in the URL as an actualy query, it won't be included when + // saving to a file, so we have to manually add the query to the filename here. + let directory = imageURL.deletingLastPathComponent() + var filename = imageURL.lastPathComponent if let query = nextcloudPreviewQuery(at: path, properties: properties)?.dropFirst() { - components?.queryItems = Array(query) + filename = query.reduce(filename + "?") { $0 + ($0.last == "?" ? "" : "&") + $1.description} + } + return directory.appendingPathComponent(filename) + } + + func cachedThumbnailURLIfExists(forItemAtPath path: String, account: A, with properties: ThumbnailProperties) -> URL? { + guard let url = cachedThumbnailURL(forItemAtPath: path, account: account, with: properties) else { return nil } + return FileManager.default.fileExists(atPath: url.path) ? url : nil + } + + func deleteCachedThumbnailFromDisk(forItemAtPath path: String, account: A, with properties: ThumbnailProperties) throws { + guard let url = cachedThumbnailURLIfExists(forItemAtPath: path, account: account, with: properties) else { return } + try FileManager.default.removeItem(at: url) + } + + func deleteAllCachedThumbnailsFromDisk(forItemAtPath path: String, account: A) throws { + let fm = FileManager.default + guard let url = cachedDataURL(forItemAtPath: path, account: account) else { return } + + let filename = url.lastPathComponent + let directory = url.deletingLastPathComponent() + guard fm.fileExists(atPath: url.deletingLastPathComponent().path) else { return } + + for item in try fm.contentsOfDirectory(atPath: directory.path) where item != filename && item.contains(filename) { + try fm.removeItem(at: directory.appendingPathComponent(item)) } - return components?.url } } @@ -87,8 +111,7 @@ extension WebDAV { return value } - func saveDataToDiskCache(_ data: Data, forItemAtPath path: String, account: A) throws { - guard let url = cachedDataURL(forItemAtPath: path, account: account) else { return } + func saveDataToDiskCache(_ data: Data, url: URL) throws { let directory = url.deletingLastPathComponent() if !FileManager.default.fileExists(atPath: directory.path) { @@ -97,6 +120,27 @@ extension WebDAV { try data.write(to: url) } + func saveDataToDiskCache(_ data: Data, forItemAtPath path: String, account: A) throws { + guard let url = cachedDataURL(forItemAtPath: path, account: account) else { return } + try saveDataToDiskCache(data, url: url) + } + + //MARK: Thumbnail Cache + + func loadCachedThumbnailFromDisk(forItemAtPath path: String, account: A, with properties: ThumbnailProperties) -> UIImage? { + guard let url = cachedThumbnailURL(forItemAtPath: path, account: account, with: properties), + FileManager.default.fileExists(atPath: url.path), + let data = try? Data(contentsOf: url), + let thumbnail = UIImage(data: data) else { return nil } + saveToMemoryCache(thumbnail: thumbnail, forItemAtPath: path, account: account, with: properties) + return thumbnail + } + + func saveThumbnailToDiskCache(data: Data, forItemAtPath path: String, account: A, with properties: ThumbnailProperties) throws { + guard let url = cachedThumbnailURL(forItemAtPath: path, account: account, with: properties) else { return } + try saveDataToDiskCache(data, url: url) + } + //MARK: Files Cache var filesCacheURL: URL? { diff --git a/Sources/WebDAV/WebDAV+Images.swift b/Sources/WebDAV/WebDAV+Images.swift index 108dc2f..6d67762 100644 --- a/Sources/WebDAV/WebDAV+Images.swift +++ b/Sources/WebDAV/WebDAV+Images.swift @@ -116,9 +116,8 @@ public extension WebDAV { // Check cache var cachedThumbnail: UIImage? - let accountPath = AccountPath(account: account, path: path) if !options.contains(.doNotReturnCachedResult) { - if let thumbnail = thumbnailCache[accountPath]?[properties] { + if let thumbnail = getCachedThumbnail(forItemAtPath: path, account: account, with: properties) { completion(thumbnail, nil) if !options.contains(.requestEvenIfCached) { @@ -154,24 +153,30 @@ public extension WebDAV { // 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) + var error = WebDAVError.getError(response: response, error: error) - if let data = data, - let thumbnail = UIImage(data: data) { + if let error = error { + return completion(nil, error) + } else 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 + // Memory cache + self?.saveToMemoryCache(thumbnail: thumbnail, forItemAtPath: path, account: account, with: properties) + // Disk cache + do { + try self?.saveThumbnailToDiskCache(data: data, forItemAtPath: path, account: account, with: properties) + } catch let cachingError { + error = .nsError(cachingError) + } } if thumbnail != cachedThumbnail { completion(thumbnail, error) } } else { - completion(nil, error) + completion(nil, nil) } } @@ -192,7 +197,8 @@ public extension WebDAV { } func getCachedThumbnail(forItemAtPath path: String, account: A, with properties: ThumbnailProperties) -> UIImage? { - getAllCachedThumbnails(forItemAtPath: path, account: account)?[properties] + getAllCachedThumbnails(forItemAtPath: path, account: account)?[properties] ?? + loadCachedThumbnailFromDisk(forItemAtPath: path, account: account, with: properties) } func deleteCachedThumbnail(forItemAtPath path: String, account: A, with properties: ThumbnailProperties) throws { @@ -205,11 +211,14 @@ public extension WebDAV { thumbnailCache[accountPath] = cachedThumbnails } } + + try deleteCachedThumbnailFromDisk(forItemAtPath: path, account: account, with: properties) } func deleteAllCachedThumbnails(forItemAtPath path: String, account: A) throws { let accountPath = AccountPath(account: account, path: path) thumbnailCache.removeValue(forKey: accountPath) + try deleteAllCachedThumbnailsFromDisk(forItemAtPath: path, account: account) } } @@ -258,4 +267,13 @@ extension WebDAV { return components?.url } + //MARK: Thumbnail Cache + + func saveToMemoryCache(thumbnail: UIImage, forItemAtPath path: String, account: A, with properties: ThumbnailProperties) { + let accountPath = AccountPath(account: account, path: path) + var cachedThumbnails = thumbnailCache[accountPath] ?? [:] + cachedThumbnails[properties] = thumbnail + thumbnailCache[accountPath] = cachedThumbnails + } + } diff --git a/Sources/WebDAV/WebDAV.swift b/Sources/WebDAV/WebDAV.swift index 151761e..61f3a82 100644 --- a/Sources/WebDAV/WebDAV.swift +++ b/Sources/WebDAV/WebDAV.swift @@ -408,7 +408,7 @@ extension WebDAV { if let error = error { return completion(nil, error) } else if let data = data, - let value = valueFromData(data) { + let value = valueFromData(data) { // Cache result if !options.contains(.removeExistingCache), !options.contains(.doNotCacheResult) { diff --git a/Tests/WebDAVTests/WebDAVTests.swift b/Tests/WebDAVTests/WebDAVTests.swift index 8b8652f..0e0a6df 100644 --- a/Tests/WebDAVTests/WebDAVTests.swift +++ b/Tests/WebDAVTests/WebDAVTests.swift @@ -353,9 +353,6 @@ 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 { @@ -367,7 +364,7 @@ final class WebDAVTests: XCTestCase { XCTAssertNoThrow(try webDAV.deleteCachedData(forItemAtPath: imagePath, account: account)) } - func testImageCache() throws { + func testImageCache() { guard let (account, password) = getAccount() else { return XCTFail() } guard let imagePath = ProcessInfo.processInfo.environment["image_path"] else { return XCTFail("You need to set the image_path in the environment.") @@ -379,12 +376,12 @@ final class WebDAVTests: XCTestCase { XCTAssert(FileManager.default.fileExists(atPath: cachedImageURL.path)) XCTAssertNotNil(webDAV.getCachedImage(forItemAtPath: imagePath, account: account)) - try webDAV.deleteCachedData(forItemAtPath: imagePath, account: account) + XCTAssertNoThrow(try webDAV.deleteCachedData(forItemAtPath: imagePath, account: account)) XCTAssertNil(webDAV.getCachedImage(forItemAtPath: imagePath, account: account)) XCTAssertFalse(FileManager.default.fileExists(atPath: cachedImageURL.path)) } - func testDeleteAllCachedData() throws { + func testDeleteAllCachedData() { guard let (account, password) = getAccount() else { return XCTFail() } guard let imagePath = ProcessInfo.processInfo.environment["image_path"] else { return XCTFail("You need to set the image_path in the environment.") @@ -416,12 +413,11 @@ final class WebDAVTests: XCTestCase { func testThumbnailCacheURL() { guard let (account, _) = getAccount() else { return XCTFail() } - guard let url = webDAV.cachedThumbnailURL(forItemAtPath: "fakeImage.png", account: account, with: .init((width: 512, height: 512), contentMode: .fill)), - let query = url.query else { return XCTFail("Could not get URL") } - XCTAssertEqual("\(url.lastPathComponent)?\(query)", "fakeImage.png?mode=cover&x=512&y=512&a=1") + guard let url = webDAV.cachedThumbnailURL(forItemAtPath: "fakeImage.png", account: account, with: .init((width: 512, height: 512), contentMode: .fill)) else { return XCTFail("Could not get URL") } + XCTAssertEqual(url.lastPathComponent, "fakeImage.png?mode=cover&x=512&y=512&a=1") } - func testSpecificThumbnailCache() throws { + func testSpecificThumbnailCache() { guard let (account, password) = getAccount() else { return XCTFail() } guard let imagePath = ProcessInfo.processInfo.environment["image_path"] else { return XCTFail("You need to set the image_path in the environment.") @@ -429,36 +425,38 @@ final class WebDAVTests: XCTestCase { 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)) + let cachedThumbnailURL = webDAV.cachedThumbnailURL(forItemAtPath: imagePath, account: account, with: .fill)! + 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) + XCTAssertNoThrow(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)) + XCTAssertFalse(FileManager.default.fileExists(atPath: cachedThumbnailURL.path)) } - func testGeneralThumbnailCache() throws { + func testGeneralThumbnailCache() { guard let (account, password) = getAccount() else { return XCTFail() } guard let imagePath = ProcessInfo.processInfo.environment["image_path"] else { return XCTFail("You need to set the image_path in the environment.") } - downloadThumbnail(imagePath: imagePath, account: account, password: password, with: .fill) - downloadThumbnail(imagePath: imagePath, account: account, password: password, with: .fit) + let fillProperties = ThumbnailProperties((width: 512, height: 512), contentMode: .fill) + let fitProperties = ThumbnailProperties((width: 512, height: 512), contentMode: .fit) + downloadThumbnail(imagePath: imagePath, account: account, password: password, with: fillProperties) + downloadThumbnail(imagePath: imagePath, account: account, password: password, with: fitProperties) -// 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 = webDAV.cachedThumbnailURL(forItemAtPath: imagePath, account: account, with: fillProperties)! + let cachedThumbnailFitURL = webDAV.cachedThumbnailURL(forItemAtPath: imagePath, account: account, with: fitProperties)! -// XCTAssert(FileManager.default.fileExists(atPath: cachedThumbnailFillURL.path)) -// XCTAssert(FileManager.default.fileExists(atPath: cachedThumbnailFitURL.path)) + 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) + XCTAssertNoThrow(try webDAV.deleteAllCachedThumbnails(forItemAtPath: imagePath, account: account)) XCTAssertNil(webDAV.getAllCachedThumbnails(forItemAtPath: imagePath, account: account)) -// XCTAssertFalse(FileManager.default.fileExists(atPath: cachedThumbnailFillURL.path)) -// XCTAssertFalse(FileManager.default.fileExists(atPath: cachedThumbnailFitURL.path)) + XCTAssertFalse(FileManager.default.fileExists(atPath: cachedThumbnailFillURL.path)) + XCTAssertFalse(FileManager.default.fileExists(atPath: cachedThumbnailFitURL.path)) } //MARK: OCS @@ -619,14 +617,13 @@ final class WebDAVTests: XCTestCase { ("testFilesCacheDoubleRequest", testFilesCacheDoubleRequest), // Image Cache ("testDownloadImage", testDownloadImage), - /* ("testImageCache", testImageCache), ("testDeleteAllCachedData", testDeleteAllCachedData), // Thumbnails ("testDownloadThumbnail", testDownloadThumbnail), + ("testThumbnailCacheURL", testThumbnailCacheURL), ("testSpecificThumbnailCache", testSpecificThumbnailCache), ("testGeneralThumbnailCache", testGeneralThumbnailCache), - */ // OCS ("testTheme", testTheme), ("testColorHex", testColorHex)