forked from shadowfacts/Tusker
150 lines
4.6 KiB
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 }))
|
|
}
|
|
}
|