Files
damus/damus/Models/VideoCache.swift
William Casarin 052ea9b727 Revert "Custom video loader caching technique"
This reverts commit ba494f94ab.
2024-04-25 14:56:29 -07:00

115 lines
4.9 KiB
Swift

//
// VideoCache.swift
// damus
//
// Created by Daniel D'Aquino on 2024-04-01.
//
import Foundation
import CryptoKit
// Default expiry time of only 1 day to prevent using too much storage
fileprivate let DEFAULT_EXPIRY_TIME: TimeInterval = 60*60*24
// Default cache directory is in the system-provided caches directory, so that the operating system can delete files when it needs storage space
// (https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html)
fileprivate let DEFAULT_CACHE_DIRECTORY_PATH: URL? = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.appendingPathComponent("video_cache")
struct VideoCache {
private let cache_url: URL
private let expiry_time: TimeInterval
static let standard: VideoCache? = try? VideoCache()
init?(cache_url: URL? = nil, expiry_time: TimeInterval = DEFAULT_EXPIRY_TIME) throws {
guard let cache_url_to_apply = cache_url ?? DEFAULT_CACHE_DIRECTORY_PATH else { return nil }
self.cache_url = cache_url_to_apply
self.expiry_time = expiry_time
// Create the cache directory if it doesn't exist
do {
try FileManager.default.createDirectory(at: self.cache_url, withIntermediateDirectories: true, attributes: nil)
} catch {
Log.error("Could not create cache directory: %s", for: .storage, error.localizedDescription)
throw error
}
}
/// Checks for a cached video and returns its URL if available, otherwise downloads and caches the video.
func maybe_cached_url_for(video_url: URL) throws -> URL {
let cached_url = url_to_cached_url(url: video_url)
if FileManager.default.fileExists(atPath: cached_url.path) {
// Check if the cached video has expired
let file_attributes = try FileManager.default.attributesOfItem(atPath: cached_url.path)
if let modification_date = file_attributes[.modificationDate] as? Date, Date().timeIntervalSince(modification_date) <= expiry_time {
// Video is not expired
return cached_url
} else {
Task {
// Video is expired, delete and re-download on the background
try FileManager.default.removeItem(at: cached_url)
return try await download_and_cache_video(from: video_url)
}
return video_url
}
} else {
Task {
// Video is not cached, download and cache on the background
return try await download_and_cache_video(from: video_url)
}
return video_url
}
}
/// Downloads video content using URLSession and caches it to disk.
private func download_and_cache_video(from url: URL) async throws -> URL {
let (data, response) = try await URLSession.shared.data(from: url)
guard let http_response = response as? HTTPURLResponse,
200..<300 ~= http_response.statusCode else {
throw URLError(.badServerResponse)
}
let destination_url = url_to_cached_url(url: url)
try data.write(to: destination_url)
return destination_url
}
func url_to_cached_url(url: URL) -> URL {
let hashed_url = hash_url(url)
let file_extension = url.pathExtension
return self.cache_url.appendingPathComponent(hashed_url + "." + file_extension)
}
/// Deletes all cached videos older than the expiry time.
func periodic_purge(completion: ((Error?) -> Void)? = nil) {
DispatchQueue.global(qos: .background).async {
Log.info("Starting periodic video cache purge", for: .storage)
let file_manager = FileManager.default
do {
let cached_files = try file_manager.contentsOfDirectory(at: self.cache_url, includingPropertiesForKeys: [.contentModificationDateKey], options: .skipsHiddenFiles)
for file in cached_files {
let attributes = try file.resourceValues(forKeys: [.contentModificationDateKey])
if let modification_date = attributes.contentModificationDate, Date().timeIntervalSince(modification_date) > self.expiry_time {
try file_manager.removeItem(at: file)
}
}
DispatchQueue.main.async {
completion?(nil)
}
} catch {
DispatchQueue.main.async {
completion?(error)
}
}
}
}
/// Hashes the URL using SHA-256
private func hash_url(_ url: URL) -> String {
let data = Data(url.absoluteString.utf8)
let hashed_data = SHA256.hash(data: data)
return hashed_data.compactMap { String(format: "%02x", $0) }.joined()
}
}