Check for unused files in output folder
This commit is contained in:
parent
2cad27b504
commit
6b6db702f1
@ -47,7 +47,9 @@ struct ImageVersion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func wasNowGenerated() {
|
func wasNowGenerated() {
|
||||||
image.generatedImageVersions.insert(versionId)
|
DispatchQueue.main.async {
|
||||||
|
image.generatedImageVersions.insert(versionId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
final class PageGenerator {
|
final class PageGenerator {
|
||||||
|
|
||||||
private let content: Content
|
private let content: Content
|
||||||
@ -50,7 +52,8 @@ final class PageGenerator {
|
|||||||
url: tag.absoluteUrl(in: language))
|
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 })
|
results.require(files: headers.compactMap { $0.requiredFile })
|
||||||
|
|
||||||
let iconUrl = content.settings.navigation.localized(in: language).rootUrl
|
let iconUrl = content.settings.navigation.localized(in: language).rootUrl
|
||||||
@ -60,6 +63,7 @@ final class PageGenerator {
|
|||||||
url: languageUrl)
|
url: languageUrl)
|
||||||
|
|
||||||
let imageUrl = localized.linkPreview.image?.linkPreviewImage(results: results)
|
let imageUrl = localized.linkPreview.image?.linkPreviewImage(results: results)
|
||||||
|
let icons = DispatchQueue.main.sync { results.requiredIcons }
|
||||||
|
|
||||||
let pageHeader = PageHeader(
|
let pageHeader = PageHeader(
|
||||||
language: language,
|
language: language,
|
||||||
@ -71,11 +75,13 @@ final class PageGenerator {
|
|||||||
languageButton: languageButton,
|
languageButton: languageButton,
|
||||||
links: content.navigationBar(in: language),
|
links: content.navigationBar(in: language),
|
||||||
headers: headers,
|
headers: headers,
|
||||||
icons: results.requiredIcons)
|
icons: icons)
|
||||||
|
|
||||||
|
let footers = DispatchQueue.main.sync { results.requiredFooters }
|
||||||
|
|
||||||
let fullPage = GenericPage(
|
let fullPage = GenericPage(
|
||||||
header: pageHeader,
|
header: pageHeader,
|
||||||
additionalFooter: results.requiredFooters.sorted().joined()) { content in
|
additionalFooter: footers.sorted().joined()) { content in
|
||||||
content += "<article>"
|
content += "<article>"
|
||||||
if !localized.hideTitle {
|
if !localized.hideTitle {
|
||||||
if !page.hideDate {
|
if !page.hideDate {
|
||||||
|
@ -46,6 +46,12 @@ final class GenerationResults: ObservableObject {
|
|||||||
@Published
|
@Published
|
||||||
var emptyPages: Set<LocalizedPageId> = []
|
var emptyPages: Set<LocalizedPageId> = []
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var outputFiles: Set<String> = []
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var unusedFilesInOutput: Set<String> = []
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The url redirects to install to prevent broken links.
|
The url redirects to install to prevent broken links.
|
||||||
|
|
||||||
@ -114,6 +120,8 @@ final class GenerationResults: ObservableObject {
|
|||||||
self.unsavedOutputFiles = []
|
self.unsavedOutputFiles = []
|
||||||
self.emptyPages = []
|
self.emptyPages = []
|
||||||
self.redirects = [:]
|
self.redirects = [:]
|
||||||
|
self.outputFiles = []
|
||||||
|
self.unusedFilesInOutput = []
|
||||||
}
|
}
|
||||||
for result in cache.values {
|
for result in cache.values {
|
||||||
result.reset()
|
result.reset()
|
||||||
@ -229,6 +237,15 @@ final class GenerationResults: ObservableObject {
|
|||||||
func redirect(from originalUrl: String, to newUrl: String) {
|
func redirect(from originalUrl: String, to newUrl: String) {
|
||||||
update { self.redirects[originalUrl] = newUrl }
|
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> {
|
private extension Dictionary where Value == Set<LocalizedItemId> {
|
||||||
|
@ -18,7 +18,6 @@ import SFSafeSymbols
|
|||||||
- Graphs, Map, GPX for hikes
|
- Graphs, Map, GPX for hikes
|
||||||
|
|
||||||
**Generation**
|
**Generation**
|
||||||
- Consistency: Check output folder for unused files
|
|
||||||
- Empty properties: Show warnings for empty link previews, etc.
|
- Empty properties: Show warnings for empty link previews, etc.
|
||||||
|
|
||||||
**Fixes**
|
**Fixes**
|
||||||
|
@ -5,6 +5,9 @@ extension Content {
|
|||||||
func generateWebsiteInAllLanguages() {
|
func generateWebsiteInAllLanguages() {
|
||||||
performGenerationIfIdle {
|
performGenerationIfIdle {
|
||||||
self.results.reset()
|
self.results.reset()
|
||||||
|
self.storage.writeNotification = { [weak self] in
|
||||||
|
self?.results.created(outputFile: $0)
|
||||||
|
}
|
||||||
self.generatePagesInternal()
|
self.generatePagesInternal()
|
||||||
self.generatePostFeedPagesInternal()
|
self.generatePostFeedPagesInternal()
|
||||||
self.generateTagPagesInternal()
|
self.generateTagPagesInternal()
|
||||||
@ -15,6 +18,7 @@ extension Content {
|
|||||||
self.results.recalculate()
|
self.results.recalculate()
|
||||||
self.generateListOfExternalFiles()
|
self.generateListOfExternalFiles()
|
||||||
self.generateListOfUrlMappings()
|
self.generateListOfUrlMappings()
|
||||||
|
self.updateUnusedFiles()
|
||||||
self.status("Generation completed")
|
self.status("Generation completed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -77,6 +81,7 @@ extension Content {
|
|||||||
status("Generating required images: \(completed) / \(count)")
|
status("Generating required images: \(completed) / \(count)")
|
||||||
}
|
}
|
||||||
if imageGenerator.generate(version: image) {
|
if imageGenerator.generate(version: image) {
|
||||||
|
results.created(outputFile: image.outputPath)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
results.failed(image: image)
|
results.failed(image: image)
|
||||||
@ -343,4 +348,11 @@ extension Content {
|
|||||||
|
|
||||||
storage.write(content, to: redirectsListFileName)
|
storage.write(content, to: redirectsListFileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func updateUnusedFiles() {
|
||||||
|
let existing = storage.getAllOutputFiles()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.results.determineFiles(unusedIn: existing)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,8 +59,6 @@ final class RemotePush: ObservableObject {
|
|||||||
|
|
||||||
process.arguments = ["-c", argument]
|
process.arguments = ["-c", argument]
|
||||||
|
|
||||||
print(argument)
|
|
||||||
|
|
||||||
let pipe = Pipe()
|
let pipe = Pipe()
|
||||||
process.standardOutput = pipe
|
process.standardOutput = pipe
|
||||||
process.standardError = pipe
|
process.standardError = pipe
|
||||||
|
@ -388,6 +388,36 @@ struct SecurityBookmark {
|
|||||||
return operation(url)
|
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
|
// MARK: Unscoped helpers
|
||||||
|
|
||||||
private func create(folder: URL) -> Bool {
|
private func create(folder: URL) -> Bool {
|
||||||
|
@ -45,6 +45,8 @@ final class Storage: ObservableObject {
|
|||||||
|
|
||||||
var errorNotification: StorageErrorCallback?
|
var errorNotification: StorageErrorCallback?
|
||||||
|
|
||||||
|
var writeNotification: ((String) -> Void)?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Create the storage.
|
Create the storage.
|
||||||
*/
|
*/
|
||||||
@ -319,6 +321,7 @@ final class Storage: ObservableObject {
|
|||||||
*/
|
*/
|
||||||
func copy(file fileId: String, to relativeOutputPath: String) -> Bool {
|
func copy(file fileId: String, to relativeOutputPath: String) -> Bool {
|
||||||
guard let contentScope, let outputScope else { return false }
|
guard let contentScope, let outputScope else { return false }
|
||||||
|
didWrite(outputFile: relativeOutputPath)
|
||||||
return contentScope.transfer(
|
return contentScope.transfer(
|
||||||
file: filePath(file: fileId),
|
file: filePath(file: fileId),
|
||||||
to: relativeOutputPath, of: outputScope)
|
to: relativeOutputPath, of: outputScope)
|
||||||
@ -460,6 +463,7 @@ final class Storage: ObservableObject {
|
|||||||
@discardableResult
|
@discardableResult
|
||||||
func write(_ content: String, to relativeOutputPath: String) -> Bool {
|
func write(_ content: String, to relativeOutputPath: String) -> Bool {
|
||||||
guard let outputScope else { return false }
|
guard let outputScope else { return false }
|
||||||
|
didWrite(outputFile: relativeOutputPath)
|
||||||
return outputScope.write(content, to: relativeOutputPath)
|
return outputScope.write(content, to: relativeOutputPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -468,6 +472,7 @@ final class Storage: ObservableObject {
|
|||||||
*/
|
*/
|
||||||
func write(_ data: Data, to relativeOutputPath: String) -> Bool {
|
func write(_ data: Data, to relativeOutputPath: String) -> Bool {
|
||||||
guard let outputScope else { return false }
|
guard let outputScope else { return false }
|
||||||
|
didWrite(outputFile: relativeOutputPath)
|
||||||
return outputScope.write(data, to: relativeOutputPath)
|
return outputScope.write(data, to: relativeOutputPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -584,4 +589,15 @@ final class Storage: ObservableObject {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Output notifications
|
||||||
|
|
||||||
|
func didWrite(outputFile: String) {
|
||||||
|
writeNotification?(outputFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAllOutputFiles() -> Set<String> {
|
||||||
|
guard let outputScope else { return [] }
|
||||||
|
return outputScope.getAllFiles()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -86,6 +86,14 @@ struct GenerationContentView: View {
|
|||||||
GenerationStringIssuesView(
|
GenerationStringIssuesView(
|
||||||
text: "invalid blocks",
|
text: "invalid blocks",
|
||||||
items: $content.results.invalidBlocks)
|
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(
|
GenerationStringIssuesView(
|
||||||
text: "warnings",
|
text: "warnings",
|
||||||
statusWhenNonEmpty: .warning,
|
statusWhenNonEmpty: .warning,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user