Check for unused files in output folder

This commit is contained in:
Christoph Hagen 2025-02-16 14:53:00 +01:00
parent 2cad27b504
commit 6b6db702f1
9 changed files with 95 additions and 7 deletions

View File

@ -47,7 +47,9 @@ struct ImageVersion {
}
func wasNowGenerated() {
image.generatedImageVersions.insert(versionId)
DispatchQueue.main.async {
image.generatedImageVersions.insert(versionId)
}
}
}

View File

@ -1,3 +1,5 @@
import Foundation
final class PageGenerator {
private let content: Content
@ -50,7 +52,8 @@ final class PageGenerator {
url: tag.absoluteUrl(in: language))
}
let headers = makeHeaders(requiredItems: results.requiredHeaders, results: results)
let requiredHeaders = DispatchQueue.main.sync { results.requiredHeaders }
let headers = makeHeaders(requiredItems: requiredHeaders, results: results)
results.require(files: headers.compactMap { $0.requiredFile })
let iconUrl = content.settings.navigation.localized(in: language).rootUrl
@ -60,6 +63,7 @@ final class PageGenerator {
url: languageUrl)
let imageUrl = localized.linkPreview.image?.linkPreviewImage(results: results)
let icons = DispatchQueue.main.sync { results.requiredIcons }
let pageHeader = PageHeader(
language: language,
@ -71,11 +75,13 @@ final class PageGenerator {
languageButton: languageButton,
links: content.navigationBar(in: language),
headers: headers,
icons: results.requiredIcons)
icons: icons)
let footers = DispatchQueue.main.sync { results.requiredFooters }
let fullPage = GenericPage(
header: pageHeader,
additionalFooter: results.requiredFooters.sorted().joined()) { content in
additionalFooter: footers.sorted().joined()) { content in
content += "<article>"
if !localized.hideTitle {
if !page.hideDate {

View File

@ -46,6 +46,12 @@ final class GenerationResults: ObservableObject {
@Published
var emptyPages: Set<LocalizedPageId> = []
@Published
var outputFiles: Set<String> = []
@Published
var unusedFilesInOutput: Set<String> = []
/**
The url redirects to install to prevent broken links.
@ -114,6 +120,8 @@ final class GenerationResults: ObservableObject {
self.unsavedOutputFiles = []
self.emptyPages = []
self.redirects = [:]
self.outputFiles = []
self.unusedFilesInOutput = []
}
for result in cache.values {
result.reset()
@ -229,6 +237,15 @@ final class GenerationResults: ObservableObject {
func redirect(from originalUrl: String, to newUrl: String) {
update { self.redirects[originalUrl] = newUrl }
}
func created(outputFile: String) {
update { self.outputFiles.insert(outputFile.withLeadingSlashRemoved) }
}
func determineFiles(unusedIn existingFiles: Set<String>) {
let unused = existingFiles.subtracting(outputFiles)
update { self.unusedFilesInOutput = unused }
}
}
private extension Dictionary where Value == Set<LocalizedItemId> {

View File

@ -18,7 +18,6 @@ import SFSafeSymbols
- Graphs, Map, GPX for hikes
**Generation**
- Consistency: Check output folder for unused files
- Empty properties: Show warnings for empty link previews, etc.
**Fixes**

View File

@ -5,6 +5,9 @@ extension Content {
func generateWebsiteInAllLanguages() {
performGenerationIfIdle {
self.results.reset()
self.storage.writeNotification = { [weak self] in
self?.results.created(outputFile: $0)
}
self.generatePagesInternal()
self.generatePostFeedPagesInternal()
self.generateTagPagesInternal()
@ -15,6 +18,7 @@ extension Content {
self.results.recalculate()
self.generateListOfExternalFiles()
self.generateListOfUrlMappings()
self.updateUnusedFiles()
self.status("Generation completed")
}
}
@ -77,6 +81,7 @@ extension Content {
status("Generating required images: \(completed) / \(count)")
}
if imageGenerator.generate(version: image) {
results.created(outputFile: image.outputPath)
continue
}
results.failed(image: image)
@ -343,4 +348,11 @@ extension Content {
storage.write(content, to: redirectsListFileName)
}
private func updateUnusedFiles() {
let existing = storage.getAllOutputFiles()
DispatchQueue.main.async {
self.results.determineFiles(unusedIn: existing)
}
}
}

View File

@ -59,8 +59,6 @@ final class RemotePush: ObservableObject {
process.arguments = ["-c", argument]
print(argument)
let pipe = Pipe()
process.standardOutput = pipe
process.standardError = pipe

View File

@ -388,6 +388,36 @@ struct SecurityBookmark {
return operation(url)
}
func getAllFiles() -> Set<String> {
guard url.startAccessingSecurityScopedResource() else {
reportError("Failed to start security scope")
return []
}
guard let enumerator = FileManager.default.enumerator(at: url, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]) else {
reportError("Failed to get folder enumerator")
return []
}
var relativePaths = Set<String>()
let prefix = url.path().withTrailingSlash
for case let fileURL as URL in enumerator {
guard !fileURL.hasDirectoryPath else {
continue
}
let fullPath = fileURL.path()
guard fullPath.hasPrefix(prefix) else {
print("Expected prefix \(prefix) for \(fullPath)")
return []
}
let relativePath = fullPath.replacingOccurrences(of: prefix, with: "")
relativePaths.insert(relativePath)
}
url.stopAccessingSecurityScopedResource()
return relativePaths
}
// MARK: Unscoped helpers
private func create(folder: URL) -> Bool {

View File

@ -45,6 +45,8 @@ final class Storage: ObservableObject {
var errorNotification: StorageErrorCallback?
var writeNotification: ((String) -> Void)?
/**
Create the storage.
*/
@ -319,6 +321,7 @@ final class Storage: ObservableObject {
*/
func copy(file fileId: String, to relativeOutputPath: String) -> Bool {
guard let contentScope, let outputScope else { return false }
didWrite(outputFile: relativeOutputPath)
return contentScope.transfer(
file: filePath(file: fileId),
to: relativeOutputPath, of: outputScope)
@ -460,6 +463,7 @@ final class Storage: ObservableObject {
@discardableResult
func write(_ content: String, to relativeOutputPath: String) -> Bool {
guard let outputScope else { return false }
didWrite(outputFile: relativeOutputPath)
return outputScope.write(content, to: relativeOutputPath)
}
@ -468,6 +472,7 @@ final class Storage: ObservableObject {
*/
func write(_ data: Data, to relativeOutputPath: String) -> Bool {
guard let outputScope else { return false }
didWrite(outputFile: relativeOutputPath)
return outputScope.write(data, to: relativeOutputPath)
}
@ -584,4 +589,15 @@ final class Storage: ObservableObject {
}
return true
}
// MARK: Output notifications
func didWrite(outputFile: String) {
writeNotification?(outputFile)
}
func getAllOutputFiles() -> Set<String> {
guard let outputScope else { return [] }
return outputScope.getAllFiles()
}
}

View File

@ -86,6 +86,14 @@ struct GenerationContentView: View {
GenerationStringIssuesView(
text: "invalid blocks",
items: $content.results.invalidBlocks)
GenerationStringIssuesView(
text: "output files",
statusWhenNonEmpty: .nominal,
items: $content.results.outputFiles)
GenerationStringIssuesView(
text: "additional output files",
statusWhenNonEmpty: .warning,
items: $content.results.unusedFilesInOutput)
GenerationStringIssuesView(
text: "warnings",
statusWhenNonEmpty: .warning,