Improve storage

This commit is contained in:
Christoph Hagen
2024-12-19 16:25:05 +01:00
parent 9c828ff80a
commit 41887a1401
30 changed files with 926 additions and 831 deletions

View File

@ -9,35 +9,18 @@ final class ImageGenerator {
private let settings: Settings
private var relativeImageOutputPath: String {
settings.paths.imagesOutputFolderPath
}
private var generatedImages: [String : [String]] = [:]
private var generatedImages: [String : Set<String>] = [:]
private var jobs: [ImageGenerationJob] = []
init(storage: Storage, settings: Settings) {
self.storage = storage
self.settings = settings
do {
self.generatedImages = try storage.loadListOfGeneratedImages()
} catch {
print("Failed to load list of previously generated images: \(error)")
self.generatedImages = [:]
}
self.generatedImages = storage.loadListOfGeneratedImages() ?? [:]
}
func prepareForGeneration() -> Bool {
inOutputImagesFolder { imagesFolder in
do {
try imagesFolder.createIfNeeded()
return true
} catch {
print("Failed to create output images folder: \(error)")
return false
}
}
private var outputFolder: String {
settings.paths.imagesOutputFolderPath
}
func runJobs(callback: (String) -> Void) -> Bool {
@ -45,7 +28,7 @@ final class ImageGenerator {
return true
}
print("Generating \(jobs.count) images...")
for job in jobs {
while let job = jobs.popLast() {
callback("Generating image \(job.version)")
guard generate(job: job) else {
return false
@ -55,13 +38,11 @@ final class ImageGenerator {
}
func save() -> Bool {
do {
try storage.save(listOfGeneratedImages: generatedImages)
return true
} catch {
print("Failed to save list of generated images: \(error)")
guard storage.save(listOfGeneratedImages: generatedImages) else {
print("Failed to save list of generated images")
return false
}
return true
}
private func versionFileName(image: String, type: ImageFileType, width: CGFloat, height: CGFloat) -> String {
@ -88,12 +69,12 @@ final class ImageGenerator {
func generateVersion(for image: String, type: ImageFileType, maximumWidth: CGFloat, maximumHeight: CGFloat) {
let version = versionFileName(image: image, type: type, width: maximumWidth, height: maximumHeight)
if exists(version) {
hasNowGenerated(version: version, for: image)
guard needsToGenerate(version: version, for: image) else {
// Image already present
return
}
if hasPreviouslyGenerated(version: version, for: image), exists(version) {
// Don't add job again
guard !jobs.contains(where: { $0.version == version }) else {
// Job already in queue
return
}
@ -108,15 +89,29 @@ final class ImageGenerator {
jobs.append(job)
}
private func hasPreviouslyGenerated(version: String, for image: String) -> Bool {
guard let versions = generatedImages[image] else {
return false
}
return versions.contains(version)
/**
Remove all versions of an image, so that they will be recreated on the next run.
This function does not remove the images from the output folder.
*/
func removeVersions(of image: String) {
generatedImages[image] = nil
}
private func exists(imageVersion version: String) -> Bool {
inOutputImagesFolder { $0.appendingPathComponent(version).exists }
func recalculateGeneratedImages(by images: Set<String>) {
self.generatedImages = storage.calculateImages(generatedBy: images, in: outputFolder)
let versionCount = generatedImages.values.reduce(0) { $0 + $1.count }
print("Image generator: \(generatedImages.count)/\(images.count) images (\(versionCount) versions)")
}
private func needsToGenerate(version: String, for image: String) -> Bool {
guard let versions = generatedImages[image] else {
return true
}
guard versions.contains(version) else {
return true
}
return !exists(version)
}
private func hasNowGenerated(version: String, for image: String) {
@ -124,7 +119,7 @@ final class ImageGenerator {
generatedImages[image] = [version]
return
}
versions.append(version)
versions.insert(version)
generatedImages[image] = versions
}
@ -132,19 +127,29 @@ final class ImageGenerator {
generatedImages[image] = nil
}
// MARK: Files
private func exists(_ image: String) -> Bool {
storage.hasFileInOutputFolder(relativePath(for: image))
}
private func relativePath(for image: String) -> String {
outputFolder + "/" + image
}
private func write(imageData data: Data, version: String) -> Bool {
return storage.write(data, to: relativePath(for: version))
}
// MARK: Image operations
private func generate(job: ImageGenerationJob) -> Bool {
if hasPreviouslyGenerated(version: job.version, for: job.image), exists(job.version),
exists(imageVersion: job.version) {
guard needsToGenerate(version: job.version, for: job.image) else {
return true
}
let data: Data
do {
data = try storage.fileData(for: job.image)
} catch {
print("Failed to load image \(job.image): \(error)")
guard let data = storage.fileData(for: job.image) else {
print("Failed to load image \(job.image)")
return false
}
@ -161,14 +166,10 @@ final class ImageGenerator {
}
if job.type == .avif {
return inOutputImagesFolder { folder in
let url = folder.appendingPathComponent(job.version)
let out = url.path()
let input = url.deletingPathExtension().appendingPathExtension(job.image.fileExtension!).path()
print("avifenc -q 70 \(input) \(out)")
hasNowGenerated(version: job.version, for: job.image)
return true
}
let input = job.version.fileNameAndExtension.fileName + "." + job.image.fileExtension!
print("avifenc -q 70 \(input) \(job.version)")
hasNowGenerated(version: job.version, for: job.image)
return true
}
guard write(imageData: data, version: job.version) else {
@ -206,32 +207,6 @@ final class ImageGenerator {
return representation
}
private func write(imageData data: Data, version: String) -> Bool {
inOutputImagesFolder { folder in
let url = folder.appendingPathComponent(version)
do {
try data.write(to: url)
return true
} catch {
print("Failed to write image \(version): \(error)")
return false
}
}
}
private func exists(_ relativePath: String) -> Bool {
inOutputImagesFolder { folder in
folder.appendingPathComponent(relativePath).exists
}
}
private func inOutputImagesFolder(perform operation: (URL) -> Bool) -> Bool {
storage.write(in: .outputPath) { outputFolder in
let imagesFolder = outputFolder.appendingPathComponent(relativeImageOutputPath)
return operation(imagesFolder)
}
}
// MARK: Avif images
private func create(image: NSBitmapImageRep, type: ImageFileType, quality: CGFloat) -> Data? {