Merge pull request #29 from Isvvc/custom-caching-disk

Custom caching disk
This commit is contained in:
Isaac Lyons 2021-04-19 14:42:33 -06:00 committed by GitHub
commit ce121ed0ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 250 additions and 77 deletions

View File

@ -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?

View File

@ -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 {
@ -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")
}

View File

@ -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)
}
}
@ -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 {
@ -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)
}
}
@ -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
}
}

View File

@ -272,34 +272,43 @@ 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)]
}
/// 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<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)
}
/// 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 cached object URL couldnt be created or the file can't be deleted.
/// - 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)
}
/// 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 couldnt be created.
/// - Returns: The URL where the resource is stored.
func getCachedDataURL<A: WebDAVAccount>(forItemAtPath path: String, account: A) throws -> URL? {
//TODO
return nil
try deleteCachedDataFromDisk(forItemAtPath: path, account: account)
}
/// Deletes all downloaded data that has been cached.
@ -307,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.
@ -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 {
@ -380,7 +390,7 @@ extension WebDAV {
}
if options.contains(.removeExistingCache) {
cache.removeValue(forKey: accountPath)
try? deleteCachedData(forItemAtPath: path, account: account)
}
// Create network request
@ -392,16 +402,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,
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.
@ -409,7 +427,7 @@ extension WebDAV {
completion(value, error)
}
} else {
completion(nil, error)
completion(nil, nil)
}
}

View File

@ -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.")
@ -375,16 +372,16 @@ 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)
XCTAssertNoThrow(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 {
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.")
@ -393,15 +390,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() {
@ -415,44 +411,52 @@ final class WebDAVTests: XCTestCase {
XCTAssertNoThrow(try webDAV.deleteCachedThumbnail(forItemAtPath: imagePath, account: account, with: .fill))
}
func testSpecificThumbnailCache() throws {
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)
// let cachedThumbnailURL = try webDAV.getCachedThumbnailURL(forItemAtPath: imagePath, account: account, with: nil, aspectFill: false)!
// 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)
XCTAssertNil(webDAV.getCachedThumbnail(forItemAtPath: imagePath, account: account, with: .fill))
// XCTAssertFalse(FileManager.default.fileExists(atPath: cachedThumbnailURL.path))
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)) else { return XCTFail("Could not get URL") }
XCTAssertEqual(url.lastPathComponent, "fakeImage.png?mode=cover&x=512&y=512&a=1")
}
func testGeneralThumbnailCache() 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.")
}
downloadThumbnail(imagePath: imagePath, account: account, password: password, with: .fill)
downloadThumbnail(imagePath: imagePath, account: account, password: password, with: .fit)
// 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 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))
// XCTAssert(FileManager.default.fileExists(atPath: cachedThumbnailFillURL.path))
// XCTAssert(FileManager.default.fileExists(atPath: cachedThumbnailFitURL.path))
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))
}
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.")
}
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 = 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))
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
@ -613,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)