import Foundation import CryptoKit final class FileUpdateChecker { private static let hashesFileName = "hashes.json" private let input: URL private var hashesFile: URL { input.appendingPathComponent(FileUpdateChecker.hashesFileName) } /** The hashes of all accessed files from the previous run The key is the relative path to the file from the source */ private var previousFiles: [String : Data] = [:] /** The paths of all files which were accessed, with their new hashes This list is used to check if a file was modified, and to write all accessed files back to disk */ private var accessedFiles: [String : Data] = [:] private var source: String { "FileUpdateChecker" } init(input: URL) { self.input = input guard hashesFile.exists else { log.add(info: "No file hashes loaded, regarding all content as new", source: source) return } let data: Data do { data = try Data(contentsOf: hashesFile) } catch { log.add( warning: "File hashes could not be read, regarding all content as new", source: source, error: error) return } do { self.previousFiles = try JSONDecoder().decode(from: data) } catch { log.add( warning: "File hashes could not be decoded, regarding all content as new", source: source, error: error) return } } func fileHasChanged(at path: String) -> Bool { guard let oldHash = previousFiles[path] else { // Image wasn't used last time, so treat as new return true } guard let newHash = accessedFiles[path] else { // Each image should have been loaded once // before using this function fatalError() } return oldHash != newHash } func didLoad(_ data: Data, at path: String) { accessedFiles[path] = SHA256.hash(data: data).data } func writeDetectedFileChangesToDisk() { do { let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted let data = try encoder.encode(accessedFiles) try data.write(to: hashesFile) } catch { log.add(warning: "Failed to save file hashes", source: source, error: error) } } }