Skip to content

Commit

Permalink
Merge pull request #29 from Isvvc/custom-caching-disk
Browse files Browse the repository at this point in the history
Custom caching disk
  • Loading branch information
skjiisa authored Apr 19, 2021
2 parents 87b3d07 + df49540 commit ce121ed
Show file tree
Hide file tree
Showing 5 changed files with 241 additions and 68 deletions.
18 changes: 17 additions & 1 deletion Sources/WebDAV/Account.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,28 @@ 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

public struct AccountPath: Hashable, Codable {
private static let slash = CharacterSet(charactersIn: "/")
static let slash = CharacterSet(charactersIn: "/")

var username: String?
var baseURL: String?
Expand Down
120 changes: 119 additions & 1 deletion Sources/WebDAV/WebDAV+DiskCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,82 @@
// Created by Isaac Lyons on 4/7/21.
//

import Foundation
import UIKit

//MARK: Public

public extension WebDAV {

//MARK: Data

func cachedDataURL<A: WebDAVAccount>(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<A: WebDAVAccount>(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
}

func deleteCachedDataFromDisk<A: WebDAVAccount>(forItemAtPath path: String, account: A) throws {
guard let url = cachedDataURLIfExists(forItemAtPath: path, account: account) else { return }
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(at: url.appendingPathComponent(item))
}
}

//MARK: Thumbnails

func cachedThumbnailURL<A: WebDAVAccount>(forItemAtPath path: String, account: A, with properties: ThumbnailProperties) -> URL? {
guard let imageURL = cachedDataURL(forItemAtPath: path, account: account) else { return nil }

// 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() {
filename = query.reduce(filename + "?") { $0 + ($0.last == "?" ? "" : "&") + $1.description}
}
return directory.appendingPathComponent(filename)
}

func cachedThumbnailURLIfExists<A: WebDAVAccount>(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<A: WebDAVAccount>(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<A: WebDAVAccount>(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))
}
}

}

//MARK: Internal

extension WebDAV {

Expand All @@ -25,6 +100,49 @@ extension WebDAV {
return directory
}

//MARK: Data Cache

func loadCachedValueFromDisk<A: WebDAVAccount, Value: Equatable>(cache: Cache<AccountPath, Value>, 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
}

func saveDataToDiskCache(_ data: Data, url: URL) throws {
let directory = url.deletingLastPathComponent()

if !FileManager.default.fileExists(atPath: directory.path) {
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
}
try data.write(to: url)
}

func saveDataToDiskCache<A: WebDAVAccount>(_ 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<A: WebDAVAccount>(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<A: WebDAVAccount>(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? {
cacheFolder?.appendingPathComponent("files.plist")
}
Expand Down
44 changes: 31 additions & 13 deletions Sources/WebDAV/WebDAV+Images.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
}

Expand All @@ -182,17 +187,18 @@ public extension WebDAV {
//MARK: Image Cache

func getCachedImage<A: WebDAVAccount>(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<A: WebDAVAccount>(forItemAtPath path: String, account: A) -> [ThumbnailProperties: UIImage]? {
getCachedValue(cache: thumbnailCache, forItemAtPath: path, account: account)
getCachedValue(from: thumbnailCache, forItemAtPath: path, account: account)
}

func getCachedThumbnail<A: WebDAVAccount>(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<A: WebDAVAccount>(forItemAtPath path: String, account: A, with properties: ThumbnailProperties) throws {
Expand All @@ -205,11 +211,14 @@ public extension WebDAV {
thumbnailCache[accountPath] = cachedThumbnails
}
}

try deleteCachedThumbnailFromDisk(forItemAtPath: path, account: account, with: properties)
}

func deleteAllCachedThumbnails<A: WebDAVAccount>(forItemAtPath path: String, account: A) throws {
let accountPath = AccountPath(account: account, path: path)
thumbnailCache.removeValue(forKey: accountPath)
try deleteAllCachedThumbnailsFromDisk(forItemAtPath: path, account: account)
}

}
Expand Down Expand Up @@ -258,4 +267,13 @@ extension WebDAV {
return components?.url
}

//MARK: Thumbnail Cache

func saveToMemoryCache<A: WebDAVAccount>(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
}

}
66 changes: 42 additions & 24 deletions Sources/WebDAV/WebDAV.swift
Original file line number Diff line number Diff line change
Expand Up @@ -272,41 +272,51 @@ public extension WebDAV {
//MARK: Cache

func getCachedData<A: WebDAVAccount>(forItemAtPath path: String, account: A) -> Data? {
getCachedValue(cache: dataCache, forItemAtPath: path, account: account)
getCachedValue(cache: dataCache, forItemAtPath: path, account: account, valueFromData: { $0 })
}

func getCachedValue<A: WebDAVAccount, Value: Equatable>(cache: Cache<AccountPath, Value>, 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<A: WebDAVAccount, Value: Equatable>(from cache: Cache<AccountPath, Value>, forItemAtPath path: String, account: A) -> Value? {
cache[AccountPath(account: account, path: path)]
}

/// Deletes the cached data for a certain 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.
/// - Throws: An error if the cached object URL couldn’t be created or the file can't be deleted.
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)
/// - valueFromData: Convert `Data` to the desired value type.
/// - Returns: The cached data if it is available.
func getCachedValue<A: WebDAVAccount, Value: Equatable>(cache: Cache<AccountPath, Value>, 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)
}

/// Get the URL used to store a resource for a certain path.
/// Useful to find where a download image is located.
/// Deletes the cached data for a certain path.
/// - 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<A: WebDAVAccount>(forItemAtPath path: String, account: A) throws -> URL? {
//TODO
return nil
/// - Throws: An error if the file can't be deleted.
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)

try deleteCachedDataFromDisk(forItemAtPath: path, account: account)
}

/// Deletes all downloaded data that has been cached.
/// - Throws: An error if the resources couldn't be deleted.
func deleteAllCachedData() throws {
dataCache.removeAllValues()
imageCache.removeAllValues()
try deleteAllDiskCachedData()
}

/// Get the total disk space for the contents of the image cache.
Expand Down Expand Up @@ -363,12 +373,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 {
Expand All @@ -380,7 +390,7 @@ extension WebDAV {
}

if options.contains(.removeExistingCache) {
cache.removeValue(forKey: accountPath)
try? deleteCachedData(forItemAtPath: path, account: account)
}

// Create network request
Expand All @@ -392,24 +402,32 @@ 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,
let value = valueFromData(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.
if value != cachedValue {
completion(value, error)
}
} else {
completion(nil, error)
completion(nil, nil)
}
}

Expand Down
Loading

0 comments on commit ce121ed

Please sign in to comment.