2022-08-19 18:05:06 +02:00
|
|
|
import Foundation
|
|
|
|
|
|
|
|
enum FileAccessError: Error {
|
|
|
|
case failedToReadFile(String, Error)
|
|
|
|
}
|
|
|
|
|
|
|
|
final class FileAccess {
|
|
|
|
|
|
|
|
static let accessTimesFileName = "access.json"
|
|
|
|
|
|
|
|
let errorOutput: ErrorOutput
|
|
|
|
|
|
|
|
let sourceFolder: URL
|
|
|
|
|
|
|
|
private let source = "FileAccess"
|
|
|
|
|
|
|
|
private var modificationTimeCacheFile: URL {
|
|
|
|
sourceFolder.appendingPathComponent(FileAccess.accessTimesFileName)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
The time stamps of last modified times for all accessed source files.
|
|
|
|
|
|
|
|
The key is the relative path to the file from the source
|
|
|
|
*/
|
|
|
|
private var sourceLastModifiedTimes: [String : Date] = [:]
|
|
|
|
|
|
|
|
private var changedFiles: Set<String> = []
|
|
|
|
|
2022-08-25 00:11:53 +02:00
|
|
|
private var accessedFiles: Set<String> = []
|
|
|
|
|
2022-08-19 18:05:06 +02:00
|
|
|
init(in root: URL, errorOutput: ErrorOutput) {
|
|
|
|
self.sourceFolder = root
|
|
|
|
self.errorOutput = errorOutput
|
|
|
|
|
|
|
|
loadSavedModificationTimes()
|
|
|
|
}
|
|
|
|
|
|
|
|
private func loadSavedModificationTimes() {
|
|
|
|
let url = modificationTimeCacheFile
|
|
|
|
guard url.exists else {
|
|
|
|
errorOutput.add(info: "No file modification times loaded, regarding all content as new", source: source)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
let data: Data
|
|
|
|
do {
|
|
|
|
data = try Data(contentsOf: url)
|
|
|
|
} catch {
|
|
|
|
errorOutput.add(
|
|
|
|
warning: "File modification times data not read, regarding all content as new",
|
|
|
|
source: source,
|
|
|
|
error: error)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
do {
|
|
|
|
self.sourceLastModifiedTimes = try JSONDecoder().decode(from: data)
|
|
|
|
} catch {
|
|
|
|
errorOutput.add(
|
|
|
|
warning: "File modification times not decoded, regarding all content as new",
|
|
|
|
source: source,
|
|
|
|
error: error)
|
|
|
|
try? url.delete()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func didAccess(inputPath: String, modified lastModified: Date, source: String) {
|
2022-08-25 00:11:53 +02:00
|
|
|
accessedFiles.insert(inputPath)
|
2022-08-19 18:05:06 +02:00
|
|
|
guard let previousDate = sourceLastModifiedTimes[inputPath] else {
|
|
|
|
// File not processed before, so mark as changed
|
|
|
|
changedFiles.insert(inputPath)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
guard lastModified > previousDate else {
|
|
|
|
// File is unchanged
|
|
|
|
return
|
|
|
|
}
|
|
|
|
changedFiles.insert(inputPath)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func lastModifiedTime(of url: URL) -> Date? {
|
|
|
|
guard url.exists else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
do {
|
|
|
|
let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
|
|
|
|
guard let date = attributes[.modificationDate] as? Date else {
|
|
|
|
errorOutput.add(warning: "Failed to read modification time of \(url.path)", source: source)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return date
|
|
|
|
} catch {
|
|
|
|
errorOutput.add(warning: "Failed to read file attributes of \(url.path)", source: source)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func loadStringContent(inputPath: String) throws -> String? {
|
|
|
|
try load(inputPath: inputPath, String.init)
|
|
|
|
}
|
|
|
|
|
|
|
|
func loadDataContent(inputPath: String) throws -> Data? {
|
|
|
|
try load(inputPath: inputPath) { try Data(contentsOf: $0) }
|
|
|
|
}
|
|
|
|
|
|
|
|
private func load<T>(inputPath: String, _ closure: (URL) throws -> T) rethrows -> T? {
|
|
|
|
let url = sourceFolder.appendingPathComponent(inputPath)
|
|
|
|
guard let modifiedDate = lastModifiedTime(of: url) else {
|
|
|
|
sourceLastModifiedTimes[inputPath] = nil
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
do {
|
|
|
|
let content = try closure(url)
|
|
|
|
didAccess(inputPath: inputPath, modified: modifiedDate, source: source)
|
|
|
|
return content
|
|
|
|
} catch {
|
|
|
|
throw FileAccessError.failedToReadFile(inputPath, error)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func didGenerateAllFiles() {
|
|
|
|
for file in changedFiles {
|
|
|
|
let url = sourceFolder.appendingPathComponent(file)
|
|
|
|
guard let date = lastModifiedTime(of: url) else {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
sourceLastModifiedTimes[file] = date
|
|
|
|
}
|
|
|
|
do {
|
|
|
|
let data = try JSONEncoder().encode(sourceLastModifiedTimes)
|
|
|
|
try data.write(to: modificationTimeCacheFile)
|
|
|
|
} catch {
|
|
|
|
errorOutput.add(warning: "Failed to save modification times", source: source, error: error)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func printChangedFilesOverview() {
|
|
|
|
let count = changedFiles.count
|
|
|
|
guard count > 0 else {
|
|
|
|
print("No files modified")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
print("\(count) files modified:")
|
|
|
|
changedFiles.prefix(10).forEach { print(" " + $0) }
|
|
|
|
if count > 10 {
|
|
|
|
print(" ...")
|
|
|
|
}
|
|
|
|
}
|
2022-08-25 00:11:53 +02:00
|
|
|
|
|
|
|
func printAccessedFilesOverview() {
|
|
|
|
let count = accessedFiles.count
|
|
|
|
guard count > 0 else {
|
|
|
|
print("No files accessed")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
print("\(count) files accessed:")
|
|
|
|
accessedFiles.prefix(10).forEach { print(" " + $0) }
|
|
|
|
if count > 10 {
|
|
|
|
print(" ...")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func printAllTouchedFiles() {
|
|
|
|
print("\(accessedFiles.count) files accessed:")
|
|
|
|
accessedFiles.sorted().forEach { file in
|
|
|
|
if changedFiles.contains(file) {
|
|
|
|
print(" \(file) (changed)")
|
|
|
|
} else {
|
|
|
|
print(" " + file)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-08-19 18:05:06 +02:00
|
|
|
}
|