WebDAV-Swift/Sources/WebDAV/WebDAV+DiskCache.swift

294 lines
14 KiB
Swift

//
// WebDAV+DiskCache.swift
// WebDAV-Swift
//
// Created by Isaac Lyons on 4/7/21.
//
import UIKit
//MARK: Public
public extension WebDAV {
//MARK: Data
/// Get the local cached data URL for the item at the specified path.
///
/// Gives the URL the data would be cached at wether or not there is any data cached there.
/// - Parameters:
/// - path: The path that would be used to download the data.
/// - account: The WebDAV account that would be used to download the data.
/// - Returns: The URL where the data is or would be cached.
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))
}
/// Get the local cached data URL for the item at the specified path if there is cached data there.
/// - Parameters:
/// - path: The path used to download the data.
/// - account: The WebDAV account used to download the data.
/// - Returns: The URL of the cached data, if it exists.
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
}
/// Delete the cached data for the item at the specified path from the disk cache.
/// - Parameters:
/// - path: The path used to download the data.
/// - account: The WebDAV account used to download the data.
/// - Throws: An error if the file couldn't be deleted.
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)
}
/// Delete all cached data from the disk cache.
/// - Throws: An error if the files couldn't be deleted.
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
/// Get the local cached thumbnail URL for the image at the specified path.
///
/// Gives the URL the thumbnail would be cached at wether or not there is any data cached there.
/// - Parameters:
/// - path: The path that would be used to download the thumbnail.
/// - account: The WebDAV account that would be used to download the thumbnail.
/// - properties: The properties of the thumbnail.
/// - Returns: The URL where the thumbnail is or would be cached.
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 actual 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)
}
/// Get the local cached thumbnail URL for the image at the specified path if there is a cached thumbnail there.
/// - Parameters:
/// - path: The path used to download the thumbnail.
/// - account: The WebDAV account used to download the thumbnail.
/// - properties: The properties of the thumbnail.
/// - Returns: The URL of the cached thumbnail, if it exists.
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
}
/// Get the URLs of the cached thumbnails for the image at the specified path from the disk cache.
/// - Parameters:
/// - path: The path used to download the thumbnails.
/// - account: The WebDAV account used to download the thumbnails.
/// - Throws: An error if the caches directory couldn't be listed.
func getAllCachedThumbnailURLs<A: WebDAVAccount>(forItemAtPath path: String, account: A) throws -> [URL]? {
let fm = FileManager.default
guard let url = cachedDataURL(forItemAtPath: path, account: account) else { return nil }
let filename = url.lastPathComponent
let directory = url.deletingLastPathComponent()
guard fm.fileExists(atPath: directory.path) else { return nil }
return try fm.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil, options: []).filter { $0.lastPathComponent != filename && $0.lastPathComponent.starts(with: filename) }
}
/// Delete the cached thumbnail for the image at the specified path from the disk cache.
/// - Parameters:
/// - path: The path used to download the thumbnail.
/// - account: The WebDAV account used to download the thumbnail.
/// - properties: The properties of the thumbnail.
/// - Throws: An error if the file couldn't be deleted.
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)
}
/// Delete the cached thumbnails for the image at the specified path from the disk cache.
/// - Parameters:
/// - path: The path used to download the thumbnails.
/// - account: The WebDAV account used to download the thumbnails.
/// - Throws: An error if the files couldn't be deleted.
func deleteAllCachedThumbnailsFromDisk<A: WebDAVAccount>(forItemAtPath path: String, account: A) throws {
try getAllCachedThumbnailURLs(forItemAtPath: path, account: account)?.forEach { try FileManager.default.removeItem(at: $0) }
}
}
//MARK: Internal
extension WebDAV {
var cacheFolder: URL? {
let fm = FileManager.default
guard let caches = fm.urls(for: .cachesDirectory, in: .userDomainMask).first else { return nil }
let directory = caches.appendingPathComponent(WebDAV.domain, isDirectory: true)
if !fm.fileExists(atPath: directory.path) {
do {
try fm.createDirectory(at: directory, withIntermediateDirectories: true)
} catch {
NSLog("\(error)")
return nil
}
}
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)
}
func cleanupDiskCache<A: WebDAVAccount>(at path: String, account: A, files: [WebDAVFile]) throws {
let fm = FileManager.default
guard let url = cachedDataURL(forItemAtPath: path, account: account),fm.fileExists(atPath: url.path) else { return }
let goodFilePaths = Set(files.compactMap { cachedDataURL(forItemAtPath: $0.path, account: account)?.path })
let infoPlist = filesCacheURL?.path
for path in try fm.contentsOfDirectory(atPath: url.path).map({ url.appendingPathComponent($0).path })
where !goodFilePaths.contains(path)
&& path != infoPlist {
try fm.removeItem(atPath: path)
}
}
//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 loadAllCachedThumbnailsFromDisk<A: WebDAVAccount>(forItemAtPath path: String, account: A) throws -> [ThumbnailProperties: UIImage]? {
guard let urls = try getAllCachedThumbnailURLs(forItemAtPath: path, account: account) else { return nil }
let thumbnails = try urls.compactMap { url -> (ThumbnailProperties, UIImage)? in
// Load the thumbnail
let data = try Data(contentsOf: url)
guard let thumbnail = UIImage(data: data) else { return nil }
// Decode the thumbnail properties
let properties: ThumbnailProperties
var contentMode = ThumbnailProperties.ContentMode.fit
let fileName = url.lastPathComponent
let range = NSRange(location: 0, length: fileName.utf16.count)
if fileName.range(of: "[?&]a=1", options: .regularExpression) != nil {
contentMode = .fill
}
let regex = try NSRegularExpression(pattern: "[?&]x=([0-9]*)&y=([0-9]*)")
if let match = regex.matches(in: fileName, options: [], range: range).last,
let xRange = Range(match.range(at: 1), in: fileName),
let yRange = Range(match.range(at: 2), in: fileName),
let x = Int(fileName[xRange]),
let y = Int(fileName[yRange]) {
properties = ThumbnailProperties((width: x, height: y), contentMode: contentMode)
} else {
properties = ThumbnailProperties(contentMode: contentMode)
}
return (properties, thumbnail)
}
// Save loaded thumbnails to memory cache
let accountPath = AccountPath(account: account, path: path)
var cachedThumbnails = thumbnailCache[accountPath] ?? [:]
cachedThumbnails.merge(thumbnails, uniquingKeysWith: { current, _ in current })
thumbnailCache[accountPath] = cachedThumbnails
return cachedThumbnails
}
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")
}
func saveFilesCacheToDisk() {
guard let fileURL = filesCacheURL else { return }
do {
let data = try PropertyListEncoder().encode(filesCache)
try data.write(to: fileURL)
} catch {
NSLog("Error saving files cache: \(error)")
}
}
func loadFilesCacheFromDisk() {
guard let fileURL = filesCacheURL,
FileManager.default.fileExists(atPath: fileURL.path) else { return }
do {
let data = try Data(contentsOf: fileURL)
let files = try PropertyListDecoder().decode([AccountPath: [WebDAVFile]].self, from: data)
filesCache.merge(files) { current, _ in current}
} catch {
NSLog("Error loading files cache: \(error)")
}
}
public func clearFilesDiskCache() {
guard let fileURL = filesCacheURL,
FileManager.default.fileExists(atPath: fileURL.path) else { return }
do {
try FileManager.default.removeItem(at: fileURL)
} catch {
NSLog("Error removing files disk cache: \(error)")
}
}
public func clearFilesCache() {
clearFilesMemoryCache()
clearFilesDiskCache()
}
}