-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #28 from Isvvc/custom-caching-data
Custom data caching
- Loading branch information
Showing
8 changed files
with
527 additions
and
340 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.