Files

66 lines
2.8 KiB
Swift

// The last good feed, and the last detail of the 30 threads saved most recently, saved as JSON in
// the app's caches folder. Launch shows them at once while the first refresh runs. Signing out clears
// them, and demo mode never reads or writes them.
import Foundation
struct FeedCache: Sendable {
let directory: URL
/// How many thread details are kept. Saving one more deletes the oldest.
var threadLimit = 30
static let standard = FeedCache(directory: .cachesDirectory.appending(path: "WristCode", directoryHint: .isDirectory))
func feed() -> Feed? {
read(Feed.self, "feed.json")
}
func save(_ feed: Feed) {
write(feed, "feed.json")
}
func thread(hostId: String, threadId: String) -> ThreadDetail? {
read(ThreadDetail.self, Self.threadFile(hostId, threadId))
}
func save(_ detail: ThreadDetail) {
write(detail, Self.threadFile(detail.item.hostId, detail.item.threadId))
pruneThreads()
}
/// Deletes the thread details saved longest ago, past `threadLimit`.
private func pruneThreads() {
let fileManager = FileManager.default
let key = URLResourceKey.contentModificationDateKey
guard let files = try? fileManager.contentsOfDirectory(at: directory, includingPropertiesForKeys: [key]) else { return }
let threads = files.filter { $0.lastPathComponent.hasPrefix("thread-") }
guard threads.count > threadLimit else { return }
let modified = { (url: URL) in (try? url.resourceValues(forKeys: [key]).contentModificationDate) ?? .distantPast }
for url in threads.sorted(by: { modified($0) > modified($1) }).dropFirst(threadLimit) {
try? fileManager.removeItem(at: url)
}
}
/// Deletes everything saved, as when the watch unpairs.
func clear() {
try? FileManager.default.removeItem(at: directory)
}
/// `thread-<hostId>-<threadId>.json`, with the ids percent-encoded so any id is a safe file name.
private static func threadFile(_ hostId: String, _ threadId: String) -> String {
let safe = { (id: String) in id.addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? id }
return "thread-\(safe(hostId))-\(safe(threadId)).json"
}
private func read<Value: Decodable>(_ type: Value.Type, _ name: String) -> Value? {
guard let data = try? Data(contentsOf: directory.appending(path: name)) else { return nil }
return try? JSON.decoder().decode(type, from: data)
}
private func write(_ value: some Encodable, _ name: String) {
guard let data = try? JSON.encoder().encode(value) else { return }
try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
try? data.write(to: directory.appending(path: name), options: .atomic)
}
}