diff --git a/CHDataManagement/Generator/ImageVersion.swift b/CHDataManagement/Generator/ImageVersion.swift index 25c1b19..e4a4f79 100644 --- a/CHDataManagement/Generator/ImageVersion.swift +++ b/CHDataManagement/Generator/ImageVersion.swift @@ -47,7 +47,9 @@ struct ImageVersion { } func wasNowGenerated() { - image.generatedImageVersions.insert(versionId) + DispatchQueue.main.async { + image.generatedImageVersions.insert(versionId) + } } } diff --git a/CHDataManagement/Generator/Page Generators/PageGenerator.swift b/CHDataManagement/Generator/Page Generators/PageGenerator.swift index ea13948..eed989e 100644 --- a/CHDataManagement/Generator/Page Generators/PageGenerator.swift +++ b/CHDataManagement/Generator/Page Generators/PageGenerator.swift @@ -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 += "
" if !localized.hideTitle { if !page.hideDate { diff --git a/CHDataManagement/Generator/Results/GenerationResults.swift b/CHDataManagement/Generator/Results/GenerationResults.swift index 0ead354..bfda5f3 100644 --- a/CHDataManagement/Generator/Results/GenerationResults.swift +++ b/CHDataManagement/Generator/Results/GenerationResults.swift @@ -46,6 +46,12 @@ final class GenerationResults: ObservableObject { @Published var emptyPages: Set = [] + @Published + var outputFiles: Set = [] + + @Published + var unusedFilesInOutput: Set = [] + /** 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) { + let unused = existingFiles.subtracting(outputFiles) + update { self.unusedFilesInOutput = unused } + } } private extension Dictionary where Value == Set { diff --git a/CHDataManagement/Main/MainView.swift b/CHDataManagement/Main/MainView.swift index 15b456f..38d2a89 100644 --- a/CHDataManagement/Main/MainView.swift +++ b/CHDataManagement/Main/MainView.swift @@ -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** diff --git a/CHDataManagement/Model/Content+Generation.swift b/CHDataManagement/Model/Content+Generation.swift index 68bb424..134e2c6 100644 --- a/CHDataManagement/Model/Content+Generation.swift +++ b/CHDataManagement/Model/Content+Generation.swift @@ -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) + } + } } diff --git a/CHDataManagement/Push/RemotePush.swift b/CHDataManagement/Push/RemotePush.swift index fc6ac22..79e2ed5 100644 --- a/CHDataManagement/Push/RemotePush.swift +++ b/CHDataManagement/Push/RemotePush.swift @@ -59,8 +59,6 @@ final class RemotePush: ObservableObject { process.arguments = ["-c", argument] - print(argument) - let pipe = Pipe() process.standardOutput = pipe process.standardError = pipe diff --git a/CHDataManagement/Storage/SecurityBookmark.swift b/CHDataManagement/Storage/SecurityBookmark.swift index 9bcfb08..23bb1b0 100644 --- a/CHDataManagement/Storage/SecurityBookmark.swift +++ b/CHDataManagement/Storage/SecurityBookmark.swift @@ -388,6 +388,36 @@ struct SecurityBookmark { return operation(url) } + func getAllFiles() -> Set { + 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() + 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 { diff --git a/CHDataManagement/Storage/Storage.swift b/CHDataManagement/Storage/Storage.swift index ac1a45d..895def1 100644 --- a/CHDataManagement/Storage/Storage.swift +++ b/CHDataManagement/Storage/Storage.swift @@ -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 { + guard let outputScope else { return [] } + return outputScope.getAllFiles() + } } diff --git a/CHDataManagement/Views/Generation/GenerationContentView.swift b/CHDataManagement/Views/Generation/GenerationContentView.swift index a038d76..20171da 100644 --- a/CHDataManagement/Views/Generation/GenerationContentView.swift +++ b/CHDataManagement/Views/Generation/GenerationContentView.swift @@ -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,