Consolidate images and files

This commit is contained in:
Christoph Hagen
2024-12-09 12:18:55 +01:00
parent 394cf7a2e4
commit 4f08526978
77 changed files with 1970 additions and 1619 deletions

View File

@ -0,0 +1,13 @@
struct FileDescriptions {
let fileId: String
let german: String?
let english: String?
}
extension FileDescriptions: Codable {
}

View File

@ -10,7 +10,7 @@ struct FileOnDisk {
init(image: String, url: URL) {
let ext = image.fileExtension!
let type = ImageType(fileExtension: ext)!
let type = ImageFileType(fileExtension: ext)!
self.type = .image(type)
self.url = url
self.name = image

View File

@ -1,13 +0,0 @@
struct ImageDescriptions {
let imageId: String
let german: String?
let english: String?
}
extension ImageDescriptions: Codable {
}

View File

@ -101,16 +101,20 @@ final class Storage {
}
func createFolderStructure() throws {
try create(folder: pagesFolder)
try create(folder: filesFolder)
try create(folder: postsFolder)
try create(folder: tagsFolder)
try operate(in: .contentPath) { contentPath in
try create(folder: pagesFolder)
try create(folder: filesFolder(in: contentPath))
try create(folder: postsFolder)
try create(folder: tagsFolder)
}
}
// MARK: Pages
private let pagesFolderName = "pages"
/// The folder path where the markdown and metadata files of the pages are stored (by their id/url component)
private var pagesFolder: URL { subFolder("pages") }
private var pagesFolder: URL { subFolder(pagesFolderName) }
private func pageContentFileName(_ id: String, _ language: ContentLanguage) -> String {
"\(id)-\(language.rawValue).md"
@ -169,13 +173,15 @@ final class Storage {
for language in ContentLanguage.allCases {
files.formUnion(pages.map { pageContentFileName($0, language) })
}
try deleteFiles(in: pagesFolder, notIn: files)
try deleteFiles(in: pagesFolderName, notIn: files)
}
// MARK: Posts
private let postsFolderName = "posts"
/// The folder path where the markdown files of the posts are stored (by their unique id/url component)
private var postsFolder: URL { subFolder("posts") }
private var postsFolder: URL { subFolder(postsFolderName) }
private func postFileUrl(postId: String) -> URL {
postsFolder.appending(path: postId, directoryHint: .notDirectory).appendingPathExtension("json")
@ -202,13 +208,15 @@ final class Storage {
func deletePostFiles(notIn posts: [String]) throws {
let files = Set(posts.map { $0 + ".json" })
try deleteFiles(in: postsFolder, notIn: files)
try deleteFiles(in: postsFolderName, notIn: files)
}
// MARK: Tags
private let tagsFolderName = "tags"
/// The folder path where the source images are stored (by their unique name)
private var tagsFolder: URL { subFolder("tags") }
private var tagsFolder: URL { subFolder(tagsFolderName) }
private func tagFileUrl(tagId: String) -> URL {
tagsFolder.appending(path: tagId, directoryHint: .notDirectory)
@ -230,44 +238,44 @@ final class Storage {
func deleteTagFiles(notIn tags: [String]) throws {
let files = Set(tags.map { $0 + ".json" })
try deleteFiles(in: tagsFolder, notIn: files)
try deleteFiles(in: tagsFolderName, notIn: files)
}
// MARK: Files
// MARK: File descriptions
private var imageDescriptionFilename: String {
"image-descriptions.json"
}
private let fileDescriptionFilename = "file-descriptions.json"
private var imageDescriptionUrl: URL {
baseFolder.appending(path: "image-descriptions.json")
}
func loadImageDescriptions() -> [ImageDescriptions] {
func loadFileDescriptions() -> [FileDescriptions] {
do {
return try read(relativePath: imageDescriptionFilename)
return try read(relativePath: fileDescriptionFilename)
} catch {
print("Failed to read image descriptions: \(error)")
print("Failed to read file descriptions: \(error)")
return []
}
}
@discardableResult
func save(imageDescriptions: [ImageDescriptions]) -> Bool {
func save(fileDescriptions: [FileDescriptions]) -> Bool {
do {
try writeIfChanged(imageDescriptions, to: imageDescriptionFilename)
try writeIfChanged(fileDescriptions, to: fileDescriptionFilename)
return true
} catch {
print("Failed to write image descriptions: \(error)")
print("Failed to write file descriptions: \(error)")
return false
}
}
/// The folder path where other files are stored (by their unique name)
var filesFolder: URL { subFolder("files") }
// MARK: Files
private func fileUrl(file: String) -> URL {
filesFolder.appending(path: file, directoryHint: .notDirectory)
private let filesFolderName = "files"
/// The folder path where other files are stored (by their unique name)
func filesFolder(in folder: URL) -> URL {
folder.appending(path: filesFolderName, directoryHint: .isDirectory)
}
private func fileUrl(file: String, in folder: URL) -> URL {
filesFolder(in: folder).appending(path: file, directoryHint: .notDirectory)
}
/**
@ -275,8 +283,30 @@ final class Storage {
*/
@discardableResult
func copyFile(at url: URL, fileId: String) -> Bool {
let contentUrl = fileUrl(file: fileId)
return copy(file: url, to: contentUrl, type: "file", id: fileId)
do {
try operate(in: .contentPath) { contentPath in
let destination = fileUrl(file: fileId, in: contentPath)
try fm.copyItem(at: url, to: destination)
}
return true
} catch {
print("Failed to copy external file \(url.path()) to \(fileId): \(error)")
return false
}
}
func move(file fileId: String, to newFile: String) -> Bool {
do {
try operate(in: .contentPath) { contentPath in
let source = fileUrl(file: fileId, in: contentPath)
let destination = fileUrl(file: newFile, in: contentPath)
try fm.moveItem(at: source, to: destination)
}
return true
} catch {
print("Failed to move file \(fileId) to \(newFile): \(error)")
return false
}
}
func copy(file fileId: String, to relativeOutputPath: String) -> Bool {
@ -287,8 +317,9 @@ final class Storage {
if output.exists {
return
}
let input = contentPath.appending(path: "files/\(fileId)", directoryHint: .notDirectory)
try output.ensureParentFolderExistence()
let input = fileUrl(file: fileId, in: contentPath)
try FileManager.default.copyItem(at: input, to: output)
}
}
@ -299,14 +330,15 @@ final class Storage {
}
}
func loadAllFiles() throws -> [String : URL] {
try files(in: filesFolder).reduce(into: [:]) { files, url in
files[url.lastPathComponent] = url
func loadAllFiles() throws -> [String] {
try operate(in: .contentPath) { contentPath in
let folder = filesFolder(in: contentPath)
return try files(in: folder).map { $0.lastPathComponent }
}
}
func deleteFiles(notIn fileSet: [String]) throws {
try deleteFiles(in: filesFolder, notIn: Set(fileSet))
try deleteFiles(in: filesFolderName, notIn: Set(fileSet))
}
func fileContent(for file: String) throws -> String {
@ -318,6 +350,15 @@ final class Storage {
}
}
func fileData(for file: String) throws -> Data {
try operate(in: .contentPath) { folder in
let fileUrl = folder
.appending(path: "files", directoryHint: .isDirectory)
.appending(path: file, directoryHint: .notDirectory)
return try Data(contentsOf: fileUrl)
}
}
// MARK: Website data
private var settingsDataUrl: URL {
@ -403,13 +444,16 @@ final class Storage {
// MARK: Writing files
private func deleteFiles(in folder: URL, notIn fileSet: Set<String>) throws {
let filesToDelete = try files(in: folder)
.filter { !fileSet.contains($0.lastPathComponent) }
private func deleteFiles(in folder: String, notIn fileSet: Set<String>) throws {
try operate(in: .contentPath) { contentPath in
let subFolder = contentPath.appending(path: folder, directoryHint: .isDirectory)
let filesToDelete = try files(in: subFolder)
.filter { !fileSet.contains($0.lastPathComponent) }
for file in filesToDelete {
try fm.removeItem(at: file)
print("Deleted \(file.path())")
for file in filesToDelete {
try fm.removeItem(at: file)
print("Deleted \(file.path())")
}
}
}