Skip to content

Commit 87b3d07

Browse files
authored
Merge pull request #28 from Isvvc/custom-caching-data
Custom data caching
2 parents a30a030 + 7ac0be6 commit 87b3d07

File tree

8 files changed

+527
-340
lines changed

8 files changed

+527
-340
lines changed

Package.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,12 @@ let package = Package(
1111
targets: ["WebDAV"]),
1212
],
1313
dependencies: [
14-
.package(url: "https://github.com/drmohundro/SWXMLHash.git", .upToNextMajor(from: "5.0.0")),
15-
.package(url: "https://github.com/3lvis/Networking.git", .upToNextMajor(from: "5.1.0"))
14+
.package(url: "https://github.com/drmohundro/SWXMLHash.git", .upToNextMajor(from: "5.0.0"))
1615
],
1716
targets: [
1817
.target(
1918
name: "WebDAV",
20-
dependencies: ["SWXMLHash", "Networking"]),
19+
dependencies: ["SWXMLHash"]),
2120
.testTarget(
2221
name: "WebDAVTests",
2322
dependencies: ["WebDAV"]),

Sources/WebDAV/Cache.swift

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
//
2+
// Cache.swift
3+
// WebDAV-Swift
4+
//
5+
// Created by Isaac Lyons on 4/8/21.
6+
//
7+
8+
import Foundation
9+
10+
public final class Cache<Key: Hashable, Value> {
11+
12+
//MARK: Private
13+
14+
private let cache = NSCache<KeyWrapper, ContentWrapper>()
15+
16+
private final class KeyWrapper: NSObject {
17+
let key: Key
18+
19+
init(_ key: Key) {
20+
self.key = key
21+
}
22+
23+
override var hash: Int {
24+
key.hashValue
25+
}
26+
27+
override func isEqual(_ object: Any?) -> Bool {
28+
guard let value = object as? KeyWrapper else { return false }
29+
return value.key == key
30+
}
31+
}
32+
33+
private final class ContentWrapper {
34+
let value: Value
35+
36+
init(_ value: Value) {
37+
self.value = value
38+
}
39+
}
40+
41+
//MARK: Public
42+
43+
internal func value(forKey key: Key) -> Value? {
44+
guard let entry = cache.object(forKey: KeyWrapper(key)) else { return nil }
45+
return entry.value
46+
}
47+
48+
internal func set(_ value: Value, forKey key: Key) {
49+
let entry = ContentWrapper(value)
50+
cache.setObject(entry, forKey: KeyWrapper(key))
51+
}
52+
53+
internal func removeValue(forKey key: Key) {
54+
cache.removeObject(forKey: KeyWrapper(key))
55+
}
56+
57+
internal func removeAllValues() {
58+
cache.removeAllObjects()
59+
}
60+
61+
internal subscript(key: Key) -> Value? {
62+
get { value(forKey: key) }
63+
set {
64+
guard let value = newValue else {
65+
return removeValue(forKey: key)
66+
}
67+
set(value, forKey: key)
68+
}
69+
}
70+
}

Sources/WebDAV/WebDAV+Images.swift

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
//
2+
// WebDAV+Images.swift
3+
// WebDAV-Swift
4+
//
5+
// Created by Isaac Lyons on 4/9/21.
6+
//
7+
8+
import UIKit
9+
10+
//MARK: ThumbnailProperties
11+
12+
public struct ThumbnailProperties: Hashable {
13+
private var width: Int?
14+
private var height: Int?
15+
16+
public var contentMode: ContentMode
17+
18+
public var size: (width: Int, height: Int)? {
19+
get {
20+
if let width = width,
21+
let height = height {
22+
return (width, height)
23+
}
24+
return nil
25+
}
26+
set {
27+
width = newValue?.width
28+
height = newValue?.height
29+
}
30+
}
31+
32+
/// Configurable default thumbnail properties. Initial value of content fill and server default dimensions.
33+
public static var `default` = ThumbnailProperties(contentMode: .fill)
34+
/// Content fill with the server's default dimensions.
35+
public static let fill = ThumbnailProperties(contentMode: .fill)
36+
/// Content fit with the server's default dimensions.
37+
public static let fit = ThumbnailProperties(contentMode: .fit)
38+
39+
/// Constants that define how the thumbnail fills the dimensions.
40+
public enum ContentMode: Hashable {
41+
case fill
42+
case fit
43+
}
44+
45+
/// - Parameters:
46+
/// - size: The size of the thumbnail. A nil value will use the server's default dimensions.
47+
/// - contentMode: A flag that indicates whether the thumbnail view fits or fills the dimensions.
48+
public init(_ size: (width: Int, height: Int)? = nil, contentMode: ThumbnailProperties.ContentMode) {
49+
if let size = size {
50+
width = size.width
51+
height = size.height
52+
}
53+
self.contentMode = contentMode
54+
}
55+
56+
/// - Parameters:
57+
/// - size: The size of the thumbnail. Width and height will be trucated to integer pixel counts.
58+
/// - contentMode: A flag that indicates whether the thumbnail view fits or fills the image of the given dimensions.
59+
public init(size: CGSize, contentMode: ThumbnailProperties.ContentMode) {
60+
width = Int(size.width)
61+
height = Int(size.height)
62+
self.contentMode = contentMode
63+
}
64+
}
65+
66+
//MARK: Public
67+
68+
public extension WebDAV {
69+
70+
//MARK: Images
71+
72+
/// Download and cache an image from the specified file path.
73+
/// - Parameters:
74+
/// - path: The path of the image to download.
75+
/// - account: The WebDAV account.
76+
/// - password: The WebDAV account's password.
77+
/// - completion: If account properties are invalid, this will run immediately on the same thread.
78+
/// Otherwise, it runs when the nextwork call finishes on a background thread.
79+
/// - image: The image downloaded, if successful.
80+
/// The cached image if it has balready been downloaded.
81+
/// - cachedImageURL: The URL of the cached image.
82+
/// - error: A WebDAVError if the call was unsuccessful. `nil` if it was.
83+
/// - Returns: The request identifier.
84+
@discardableResult
85+
func downloadImage<A: WebDAVAccount>(path: String, account: A, password: String, caching options: WebDAVCachingOptions = [], completion: @escaping (_ image: UIImage?, _ error: WebDAVError?) -> Void) -> URLSessionDataTask? {
86+
cachingDataTask(cache: imageCache, path: path, account: account, password: password, caching: options, valueFromData: { UIImage(data: $0) }, completion: completion)
87+
}
88+
89+
//MARK: Thumbnails
90+
91+
/// Download and cache an image's thumbnail from the specified file path.
92+
///
93+
/// Only works with Nextcould or other instances that use Nextcloud's same thumbnail URL structure.
94+
/// - Parameters:
95+
/// - path: The path of the image to download the thumbnail of.
96+
/// - account: The WebDAV account.
97+
/// - password: The WebDAV account's password.
98+
/// - dimensions: The dimensions of the thumbnail. A value of `nil` will use the server's default.
99+
/// - aspectFill: Whether the thumbnail should fill the dimensions or fit within it.
100+
/// - completion: If account properties are invalid, this will run immediately on the same thread.
101+
/// Otherwise, it runs when the nextwork call finishes on a background thread.
102+
/// - image: The thumbnail downloaded, if successful.
103+
/// The cached thumbnail if it has balready been downloaded.
104+
/// - cachedImageURL: The URL of the cached thumbnail.
105+
/// - error: A WebDAVError if the call was unsuccessful. `nil` if it was.
106+
/// - Returns: The request identifier.
107+
@discardableResult
108+
func downloadThumbnail<A: WebDAVAccount>(
109+
path: String, account: A, password: String, with properties: ThumbnailProperties = .default,
110+
caching options: WebDAVCachingOptions = [], completion: @escaping (_ image: UIImage?, _ error: WebDAVError?) -> Void
111+
) -> URLSessionDataTask? {
112+
// This function looks a lot like cachingDataTask and authorizedRequest,
113+
// but generalizing both of those to support thumbnails would make them
114+
// so much more complicated that it's better to just have similar code here.
115+
116+
// Check cache
117+
118+
var cachedThumbnail: UIImage?
119+
let accountPath = AccountPath(account: account, path: path)
120+
if !options.contains(.doNotReturnCachedResult) {
121+
if let thumbnail = thumbnailCache[accountPath]?[properties] {
122+
completion(thumbnail, nil)
123+
124+
if !options.contains(.requestEvenIfCached) {
125+
if options.contains(.removeExistingCache) {
126+
try? deleteCachedThumbnail(forItemAtPath: path, account: account, with: properties)
127+
}
128+
return nil
129+
} else {
130+
cachedThumbnail = thumbnail
131+
}
132+
}
133+
}
134+
135+
if options.contains(.removeExistingCache) {
136+
try? deleteCachedThumbnail(forItemAtPath: path, account: account, with: properties)
137+
}
138+
139+
// Create Network request
140+
141+
guard let unwrappedAccount = UnwrappedAccount(account: account), let auth = self.auth(username: unwrappedAccount.username, password: password) else {
142+
completion(nil, .invalidCredentials)
143+
return nil
144+
}
145+
146+
guard let url = nextcloudPreviewURL(for: unwrappedAccount.baseURL, at: path, with: properties) else {
147+
completion(nil, .unsupported)
148+
return nil
149+
}
150+
151+
var request = URLRequest(url: url)
152+
request.addValue("Basic \(auth)", forHTTPHeaderField: "Authorization")
153+
154+
// Perform the network request
155+
156+
let task = URLSession(configuration: .ephemeral, delegate: self, delegateQueue: nil).dataTask(with: request) { [weak self] data, response, error in
157+
let error = WebDAVError.getError(response: response, error: error)
158+
159+
if let data = data,
160+
let thumbnail = UIImage(data: data) {
161+
// Cache result
162+
//TODO: Cache to disk
163+
if !options.contains(.removeExistingCache),
164+
!options.contains(.doNotCacheResult) {
165+
var cachedThumbnails = self?.thumbnailCache[accountPath] ?? [:]
166+
cachedThumbnails[properties] = thumbnail
167+
self?.thumbnailCache[accountPath] = cachedThumbnails
168+
}
169+
170+
if thumbnail != cachedThumbnail {
171+
completion(thumbnail, error)
172+
}
173+
} else {
174+
completion(nil, error)
175+
}
176+
}
177+
178+
task.resume()
179+
return task
180+
}
181+
182+
//MARK: Image Cache
183+
184+
func getCachedImage<A: WebDAVAccount>(forItemAtPath path: String, account: A) -> UIImage? {
185+
getCachedValue(cache: imageCache, forItemAtPath: path, account: account)
186+
}
187+
188+
//MARK: Thumbnail Cache
189+
190+
func getAllCachedThumbnails<A: WebDAVAccount>(forItemAtPath path: String, account: A) -> [ThumbnailProperties: UIImage]? {
191+
getCachedValue(cache: thumbnailCache, forItemAtPath: path, account: account)
192+
}
193+
194+
func getCachedThumbnail<A: WebDAVAccount>(forItemAtPath path: String, account: A, with properties: ThumbnailProperties) -> UIImage? {
195+
getAllCachedThumbnails(forItemAtPath: path, account: account)?[properties]
196+
}
197+
198+
func deleteCachedThumbnail<A: WebDAVAccount>(forItemAtPath path: String, account: A, with properties: ThumbnailProperties) throws {
199+
let accountPath = AccountPath(account: account, path: path)
200+
if var cachedThumbnails = thumbnailCache[accountPath] {
201+
cachedThumbnails.removeValue(forKey: properties)
202+
if cachedThumbnails.isEmpty {
203+
thumbnailCache.removeValue(forKey: accountPath)
204+
} else {
205+
thumbnailCache[accountPath] = cachedThumbnails
206+
}
207+
}
208+
}
209+
210+
func deleteAllCachedThumbnails<A: WebDAVAccount>(forItemAtPath path: String, account: A) throws {
211+
let accountPath = AccountPath(account: account, path: path)
212+
thumbnailCache.removeValue(forKey: accountPath)
213+
}
214+
215+
}
216+
217+
//MARK: Internal
218+
219+
extension WebDAV {
220+
221+
//MARK: Pathing
222+
223+
func nextcloudPreviewBaseURL(for baseURL: URL) -> URL? {
224+
return nextcloudBaseURL(for: baseURL)?
225+
.appendingPathComponent("index.php")
226+
.appendingPathComponent("core")
227+
.appendingPathComponent("preview.png")
228+
}
229+
230+
func nextcloudPreviewQuery(at path: String, properties: ThumbnailProperties) -> [URLQueryItem]? {
231+
var path = path
232+
233+
if path.hasPrefix("/") {
234+
path.removeFirst()
235+
}
236+
237+
var query = [
238+
URLQueryItem(name: "file", value: path),
239+
URLQueryItem(name: "mode", value: "cover")
240+
]
241+
242+
if let size = properties.size {
243+
query.append(URLQueryItem(name: "x", value: "\(size.width)"))
244+
query.append(URLQueryItem(name: "y", value: "\(size.height)"))
245+
}
246+
247+
if properties.contentMode == .fill {
248+
query.append(URLQueryItem(name: "a", value: "1"))
249+
}
250+
251+
return query
252+
}
253+
254+
func nextcloudPreviewURL(for baseURL: URL, at path: String, with properties: ThumbnailProperties) -> URL? {
255+
guard let thumbnailURL = nextcloudPreviewBaseURL(for: baseURL) else { return nil }
256+
var components = URLComponents(string: thumbnailURL.absoluteString)
257+
components?.queryItems = nextcloudPreviewQuery(at: path, properties: properties)
258+
return components?.url
259+
}
260+
261+
}

Sources/WebDAV/WebDAV+OCS.swift

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import Foundation
99
import SWXMLHash
1010

11+
//MARK: OCSTheme
12+
1113
/// Theming information from a WebDAV server that supports OCS.
1214
public struct OCSTheme {
1315
/// Name of the server.
@@ -48,7 +50,9 @@ public struct OCSTheme {
4850
}
4951
}
5052

51-
extension WebDAV {
53+
//MARK: Public
54+
55+
public extension WebDAV {
5256

5357
/// Get the theme information from a WebDAV server that supports OCS (including Nextcloud).
5458
/// - Parameters:
@@ -60,13 +64,16 @@ extension WebDAV {
6064
/// - error: A WebDAVError if the call was unsuccessful.
6165
/// - Returns: The data task for the request.
6266
@discardableResult
63-
public func getNextcloudTheme<A: WebDAVAccount>(account: A, password: String, completion: @escaping (_ theme: OCSTheme?, _ error: WebDAVError?) -> Void) -> URLSessionDataTask? {
67+
func getNextcloudTheme<A: WebDAVAccount>(account: A, password: String, completion: @escaping (_ theme: OCSTheme?, _ error: WebDAVError?) -> Void) -> URLSessionDataTask? {
6468
guard let unwrappedAccount = UnwrappedAccount(account: account),
65-
let auth = self.auth(username: unwrappedAccount.username, password: password),
66-
let baseURL = nextcloudBaseURL(for: unwrappedAccount.baseURL) else {
69+
let auth = self.auth(username: unwrappedAccount.username, password: password) else {
6770
completion(nil, .invalidCredentials)
6871
return nil
6972
}
73+
guard let baseURL = nextcloudBaseURL(for: unwrappedAccount.baseURL) else {
74+
completion(nil, .unsupported)
75+
return nil
76+
}
7077

7178
let url = baseURL.appendingPathComponent("ocs/v1.php/cloud/capabilities")
7279
var request = URLRequest(url: url)
@@ -102,7 +109,7 @@ extension WebDAV {
102109
/// - error: A WebDAVError if the call was unsuccessful.
103110
/// - Returns: The data task for the request.
104111
@discardableResult
105-
public func getNextcloudColorHex<A: WebDAVAccount>(account: A, password: String, completion: @escaping (_ color: String?, _ error: WebDAVError?) -> Void) -> URLSessionDataTask? {
112+
func getNextcloudColorHex<A: WebDAVAccount>(account: A, password: String, completion: @escaping (_ color: String?, _ error: WebDAVError?) -> Void) -> URLSessionDataTask? {
106113
getNextcloudTheme(account: account, password: password) { theme, error in
107114
completion(theme?.colorHex, error)
108115
}

0 commit comments

Comments
 (0)