import Foundation import CryptoKit final class FileUpdateChecker { private let hashesFileName = "hashes.json" private let input: URL /** 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] = [:] var numberOfFilesLoaded: Int { previousFiles.count } var numberOfFilesAccessed: Int { accessedFiles.count } init(input: URL) { self.input = input } enum LoadResult { case notLoaded case loaded case failed(String) } func loadPreviousRun(from folder: URL) -> LoadResult { let url = folder.appendingPathComponent(hashesFileName) guard url.exists else { return .notLoaded } let data: Data do { data = try Data(contentsOf: url) } catch { return .failed("Failed to read hashes from last run: \(error)") } do { self.previousFiles = try JSONDecoder().decode(from: data) } catch { return .failed("Failed to decode hashes from last run: \(error)") } return .loaded } 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 writeDetectedFileChanges(to folder: URL) -> String? { let url = folder.appendingPathComponent(hashesFileName) do { let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted let data = try encoder.encode(accessedFiles) try data.write(to: url) return nil } catch { return "Failed to save file hashes: \(error)" } } } var notFound = 0