284 lines
8.9 KiB
Swift
284 lines
8.9 KiB
Swift
import Foundation
|
|
|
|
/**
|
|
A class that handles the storage of the website data.
|
|
|
|
BaseFolder
|
|
- pages: Contains the markdown files of the localized pages, file name is the url
|
|
- images: Contains the raw images
|
|
- files: Contains additional files
|
|
- videos: Contains raw video files
|
|
- posts: Contains the markdown files for localized posts, file name is the post id
|
|
-
|
|
*/
|
|
final class Storage {
|
|
|
|
private(set) var baseFolder: URL
|
|
|
|
private let encoder = JSONEncoder()
|
|
|
|
private let decoder = JSONDecoder()
|
|
|
|
private let fm = FileManager.default
|
|
|
|
/**
|
|
Create the storage.
|
|
*/
|
|
init(baseFolder: URL) {
|
|
self.baseFolder = baseFolder
|
|
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
|
}
|
|
|
|
// MARK: Helper
|
|
|
|
private func subFolder(_ name: String) -> URL {
|
|
baseFolder.appending(path: name, directoryHint: .isDirectory)
|
|
}
|
|
|
|
private func files(in folder: URL) throws -> [URL] {
|
|
do {
|
|
return try fm.contentsOfDirectory(at: folder, includingPropertiesForKeys: [.isDirectoryKey])
|
|
.filter { !$0.hasDirectoryPath }
|
|
} catch {
|
|
print("Failed to get files in folder \(folder.path): \(error)")
|
|
throw error
|
|
}
|
|
}
|
|
|
|
private func fileNames(in folder: URL) throws -> [String] {
|
|
try fm.contentsOfDirectory(atPath: folder.path())
|
|
.filter { !$0.hasPrefix(".") }
|
|
.sorted()
|
|
}
|
|
|
|
private func files(in folder: URL, type: String) throws -> [URL] {
|
|
try files(in: folder).filter { $0.pathExtension == type }
|
|
}
|
|
|
|
// MARK: Folders
|
|
|
|
func update(baseFolder: URL, moveContent: Bool) throws {
|
|
let oldFolder = self.baseFolder
|
|
self.baseFolder = baseFolder
|
|
try createFolderStructure()
|
|
guard moveContent else {
|
|
return
|
|
}
|
|
// TODO: Move all files
|
|
}
|
|
|
|
private func create(folder: URL) throws {
|
|
guard !FileManager.default.fileExists(atPath: folder.path) else {
|
|
return
|
|
}
|
|
try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
|
|
}
|
|
|
|
func createFolderStructure() throws {
|
|
try create(folder: pagesFolder)
|
|
try create(folder: imagesFolder)
|
|
try create(folder: filesFolder)
|
|
try create(folder: videosFolder)
|
|
try create(folder: postsFolder)
|
|
try create(folder: tagsFolder)
|
|
}
|
|
|
|
// MARK: 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 func pageFileUrl(pageId: String) -> URL {
|
|
pagesFolder.appending(path: pageId, directoryHint: .notDirectory)
|
|
}
|
|
|
|
private func pageContentUrl(pageId: String, language: ContentLanguage) -> URL {
|
|
pagesFolder.appending(path: "\(pageId)-\(language.rawValue).md", directoryHint: .notDirectory)
|
|
}
|
|
|
|
private func pageMetadataUrl(pageId: String) -> URL {
|
|
pagesFolder.appending(path: pageId + ".json", directoryHint: .notDirectory)
|
|
}
|
|
|
|
@discardableResult
|
|
func save(pageContent: String, for pageId: String, language: ContentLanguage) -> Bool {
|
|
let contentUrl = pageContentUrl(pageId: pageId, language: language)
|
|
return write(content: pageContent, to: contentUrl, type: "page", id: pageId)
|
|
}
|
|
|
|
@discardableResult
|
|
func save(pageMetadata: PageFile, for pageId: String) -> Bool {
|
|
let contentUrl = pageMetadataUrl(pageId: pageId)
|
|
return write(pageMetadata, type: "page", id: pageId, to: contentUrl)
|
|
}
|
|
|
|
@discardableResult
|
|
func copyPageContent(from url: URL, for pageId: String, language: ContentLanguage) -> Bool {
|
|
let contentUrl = pageContentUrl(pageId: pageId, language: language)
|
|
return copy(file: url, to: contentUrl, type: "page content", id: pageId)
|
|
}
|
|
|
|
func loadAllPages() throws -> [String : PageFile] {
|
|
try loadAll(in: pagesFolder)
|
|
}
|
|
|
|
// MARK: 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 func postFileUrl(postId: String) -> URL {
|
|
postsFolder.appending(path: postId, directoryHint: .notDirectory).appendingPathExtension("json")
|
|
}
|
|
|
|
@discardableResult
|
|
func save(post: PostFile, for postId: String) -> Bool {
|
|
let contentUrl = postFileUrl(postId: postId)
|
|
return write(post, type: "post", id: postId, to: contentUrl)
|
|
}
|
|
|
|
func loadAllPosts() throws -> [String : PostFile] {
|
|
try loadAll(in: postsFolder)
|
|
}
|
|
|
|
private func post(at url: URL) throws -> PostFile {
|
|
try read(at: url)
|
|
}
|
|
|
|
private func postContent(for postId: String) throws -> PostFile {
|
|
let url = postFileUrl(postId: postId)
|
|
return try post(at: url)
|
|
}
|
|
|
|
// MARK: Tags
|
|
|
|
/// The folder path where the source images are stored (by their unique name)
|
|
private var tagsFolder: URL { subFolder("tags") }
|
|
|
|
private func tagFileUrl(tagId: String) -> URL {
|
|
tagsFolder.appending(path: tagId, directoryHint: .notDirectory)
|
|
}
|
|
|
|
private func tagMetadataUrl(tagId: String) -> URL {
|
|
tagFileUrl(tagId: tagId).appendingPathExtension("json")
|
|
}
|
|
|
|
@discardableResult
|
|
func save(tagMetadata: TagFile, for tagId: String) -> Bool {
|
|
let contentUrl = tagMetadataUrl(tagId: tagId)
|
|
return write(tagMetadata, type: "tag", id: tagId, to: contentUrl)
|
|
}
|
|
|
|
func loadAllTags() throws -> [String : TagFile] {
|
|
try loadAll(in: tagsFolder)
|
|
}
|
|
|
|
// MARK: Images
|
|
|
|
/// The folder path where the source images are stored (by their unique name)
|
|
private var imagesFolder: URL { subFolder("images") }
|
|
|
|
private func imageUrl(image: String) -> URL {
|
|
imagesFolder.appending(path: image, directoryHint: .notDirectory)
|
|
}
|
|
|
|
@discardableResult
|
|
func copyImage(at url: URL, imageId: String) -> Bool {
|
|
let contentUrl = imageUrl(image: imageId)
|
|
return copy(file: url, to: contentUrl, type: "image", id: imageId)
|
|
}
|
|
|
|
// MARK: Files
|
|
|
|
/// The folder path where other files are stored (by their unique name)
|
|
private var filesFolder: URL { subFolder("files") }
|
|
|
|
private func fileUrl(file: String) -> URL {
|
|
filesFolder.appending(path: file, directoryHint: .notDirectory)
|
|
}
|
|
|
|
@discardableResult
|
|
func copyFile(at url: URL, fileId: String) -> Bool {
|
|
let contentUrl = fileUrl(file: fileId)
|
|
return copy(file: url, to: contentUrl, type: "file", id: fileId)
|
|
}
|
|
|
|
func loadAllFiles() throws -> [String : URL] {
|
|
try files(in: filesFolder).reduce(into: [:]) { files, url in
|
|
files[url.lastPathComponent] = url
|
|
}
|
|
}
|
|
|
|
// MARK: Videos
|
|
|
|
/// The folder path where source videos are stored (by their unique name)
|
|
private var videosFolder: URL { subFolder("videos") }
|
|
|
|
private func videoUrl(video: String) -> URL {
|
|
videosFolder.appending(path: video, directoryHint: .notDirectory)
|
|
}
|
|
|
|
@discardableResult
|
|
func copyVideo(at url: URL, videoId: String) -> Bool {
|
|
let contentUrl = videoUrl(video: videoId)
|
|
return copy(file: url, to: contentUrl, type: "video", id: videoId)
|
|
}
|
|
|
|
func loadAllVideos() throws -> [String] {
|
|
try fileNames(in: videosFolder)
|
|
}
|
|
|
|
// MARK: Writing files
|
|
|
|
private func write<T>(_ value: T, type: String, id: String, to file: URL) -> Bool where T: Encodable {
|
|
let content: Data
|
|
do {
|
|
content = try encoder.encode(value)
|
|
} catch {
|
|
print("Failed to encode content of \(type) '\(id)': \(error)")
|
|
return false
|
|
}
|
|
do {
|
|
try content.write(to: file, options: .atomic)
|
|
return true
|
|
} catch {
|
|
print("Failed to save content for \(type) '\(id)': \(error)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
private func copy(file: URL, to destination: URL, type: String, id: String) -> Bool {
|
|
do {
|
|
try fm.copyItem(at: file, to: destination)
|
|
return true
|
|
} catch {
|
|
print("Failed to copy content file for \(type) '\(id)': \(error)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
private func write(content: String, to file: URL, type: String, id: String) -> Bool {
|
|
do {
|
|
try content.write(to: file, atomically: true, encoding: .utf8)
|
|
return true
|
|
} catch {
|
|
print("Failed to save content for \(type) '\(id)': \(error)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
private func read<T>(at url: URL) throws -> T where T: Decodable {
|
|
let data = try Data(contentsOf: url)
|
|
return try decoder.decode(T.self, from: data)
|
|
}
|
|
|
|
private func loadAll<T>(in folder: URL) throws -> [String : T] where T: Decodable {
|
|
try files(in: folder, type: "json").reduce(into: [:]) { items, url in
|
|
let id = url.deletingPathExtension().lastPathComponent
|
|
let item: T = try read(at: url)
|
|
items[id] = item
|
|
}
|
|
}
|
|
|
|
}
|