Tusker/Tusker/Caching/DiskCache.swift

150 lines
4.6 KiB
Swift

//
// DiskCache.swift
// Tusker
//
// Created by Shadowfacts on 1/18/21.
// Copyright © 2021 Shadowfacts. All rights reserved.
//
import Foundation
import CryptoKit
struct DiskCacheTransformer<T> {
let toData: (T) throws -> Data
let fromData: (Data) throws -> T
}
class DiskCache<T> {
let fileManager: FileManager
let path: String
let defaultExpiry: CacheExpiry
let transformer: DiskCacheTransformer<T>
private var fileStates = MultiThreadDictionary<String, FileState>()
init(name: String, defaultExpiry: CacheExpiry, transformer: DiskCacheTransformer<T>, fileManager: FileManager = .default) throws {
self.defaultExpiry = defaultExpiry
self.transformer = transformer
self.fileManager = fileManager
let cacheDir = try fileManager.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
self.path = cacheDir.appendingPathComponent(name, isDirectory: true).path
try createDirectory()
}
private func createDirectory() throws {
if !fileManager.fileExists(atPath: path) {
try fileManager.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil)
}
}
private func makeFileName(for key: String) -> String {
let ext = (key as NSString).pathExtension
let digest = Insecure.MD5.hash(data: key.data(using: .utf8)!)
let hash = digest.map { String($0, radix: 16) }.joined()
if ext.isEmpty {
return hash
} else {
return "\(hash).\(ext)"
}
}
private func makeFilePath(for key: String) -> String {
return (path as NSString).appendingPathComponent(makeFileName(for: key))
}
private func fileState(forKey key: String) -> FileState {
return fileStates[key] ?? .unknown
}
func setObject(_ object: T, forKey key: String) throws {
let data = try transformer.toData(object)
let path = makeFilePath(for: key)
guard fileManager.createFile(atPath: path, contents: data, attributes: [.modificationDate: defaultExpiry.date]) else {
throw Error.couldNotCreateFile
}
fileStates[key] = .exists
}
func removeObject(forKey key: String) throws {
let path = makeFilePath(for: key)
try fileManager.removeItem(atPath: path)
fileStates[key] = .doesNotExist
}
func existsObject(forKey key: String) throws -> Bool {
switch fileState(forKey: key) {
case .exists:
return true
case .doesNotExist:
return false
case .unknown:
let path = makeFilePath(for: key)
guard fileManager.fileExists(atPath: path) else {
return false
}
let attributes = try fileManager.attributesOfItem(atPath: path)
guard let date = attributes[.modificationDate] as? Date else {
throw Error.malformedFileAttributes
}
return date.timeIntervalSinceNow >= 0
}
}
func object(forKey key: String) throws -> T {
let path = makeFilePath(for: key)
let attributes = try fileManager.attributesOfItem(atPath: path)
guard let date = attributes[.modificationDate] as? Date else {
throw Error.malformedFileAttributes
}
guard date.timeIntervalSinceNow >= 0 else {
try fileManager.removeItem(atPath: path)
fileStates[key] = .doesNotExist
throw Error.expired
}
let data = try Data(contentsOf: URL(fileURLWithPath: path, isDirectory: false))
let object = try transformer.fromData(data)
return object
}
func removeAll() throws {
try fileManager.removeItem(atPath: path)
try createDirectory()
fileStates.withLock { dict in
dict.removeAll()
}
}
func getSizeInBytes() -> Int64? {
return fileManager.recursiveSize(url: URL(fileURLWithPath: path, isDirectory: true))
}
}
extension DiskCache {
enum Error: Swift.Error {
case malformedFileAttributes
case couldNotCreateFile
case expired
}
}
extension DiskCache {
enum FileState {
case exists, doesNotExist, unknown
}
}
extension DiskCache where T == Data {
convenience init(name: String, defaultExpiry: CacheExpiry) throws {
try self.init(name: name, defaultExpiry: defaultExpiry, transformer: DiskCacheTransformer(toData: { $0 }, fromData: { $0 }))
}
}