Import old content, load from disk
This commit is contained in:
62
CHDataManagement/Storage/PageFile.swift
Normal file
62
CHDataManagement/Storage/PageFile.swift
Normal file
@ -0,0 +1,62 @@
|
||||
import Foundation
|
||||
|
||||
struct PageFile {
|
||||
|
||||
let isDraft: Bool
|
||||
|
||||
let tags: [String]
|
||||
|
||||
let createdDate: Date
|
||||
|
||||
let startDate: Date
|
||||
|
||||
let endDate: Date?
|
||||
|
||||
let german: LocalizedPageFile
|
||||
|
||||
let english: LocalizedPageFile
|
||||
}
|
||||
|
||||
extension PageFile: Codable {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
The structure to store the metadata of a localized page
|
||||
*/
|
||||
struct LocalizedPageFile {
|
||||
|
||||
let url: String
|
||||
|
||||
/**
|
||||
The files (images, videos, other files) used in the page.
|
||||
*/
|
||||
let files: Set<String>
|
||||
|
||||
/**
|
||||
The additional files required for the page to function correctly, but which are not stored with the content.
|
||||
*/
|
||||
let externalFiles: Set<String>
|
||||
|
||||
/**
|
||||
Specifies additional files which should be copied to the destination when generating the content.
|
||||
- Note: This property defaults to an empty set.
|
||||
*/
|
||||
let requiredFiles: Set<String>
|
||||
|
||||
let title: String
|
||||
|
||||
let linkPreviewImage: String?
|
||||
|
||||
let linkPreviewTitle: String?
|
||||
|
||||
let linkPreviewDescription: String?
|
||||
|
||||
let lastModifiedDate: Date?
|
||||
|
||||
let originalURL: String?
|
||||
}
|
||||
|
||||
extension LocalizedPageFile: Codable {
|
||||
|
||||
}
|
42
CHDataManagement/Storage/PostFile.swift
Normal file
42
CHDataManagement/Storage/PostFile.swift
Normal file
@ -0,0 +1,42 @@
|
||||
import Foundation
|
||||
|
||||
struct PostFile {
|
||||
|
||||
let isDraft: Bool
|
||||
|
||||
let createdDate: Date
|
||||
|
||||
let startDate: Date
|
||||
|
||||
let endDate: Date?
|
||||
|
||||
let tags: [String]
|
||||
|
||||
let german: LocalizedPostFile
|
||||
|
||||
let english: LocalizedPostFile
|
||||
|
||||
let linkedPageId: String?
|
||||
}
|
||||
|
||||
extension PostFile: Codable {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
The structure to store the metadata of a localized post
|
||||
*/
|
||||
struct LocalizedPostFile {
|
||||
|
||||
let images: Set<String>
|
||||
|
||||
let title: String?
|
||||
|
||||
let content: String
|
||||
|
||||
let lastModifiedDate: Date?
|
||||
}
|
||||
|
||||
extension LocalizedPostFile: Codable {
|
||||
|
||||
}
|
283
CHDataManagement/Storage/Storage.swift
Normal file
283
CHDataManagement/Storage/Storage.swift
Normal file
@ -0,0 +1,283 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
39
CHDataManagement/Storage/TagFile.swift
Normal file
39
CHDataManagement/Storage/TagFile.swift
Normal file
@ -0,0 +1,39 @@
|
||||
import Foundation
|
||||
|
||||
struct TagFile {
|
||||
|
||||
let id: String
|
||||
|
||||
let german: LocalizedTagFile
|
||||
|
||||
let english: LocalizedTagFile
|
||||
|
||||
}
|
||||
|
||||
extension TagFile: Codable {
|
||||
|
||||
}
|
||||
|
||||
struct LocalizedTagFile {
|
||||
|
||||
/// The id of the tag, used also as a url component
|
||||
let urlComponent: String
|
||||
|
||||
/// A custom name, different from the tag id
|
||||
let name: String
|
||||
|
||||
let subtitle: String?
|
||||
|
||||
let description: String?
|
||||
|
||||
/// The image id of the thumbnail
|
||||
let thumbnail: String?
|
||||
|
||||
/// The original url in the previous site layout
|
||||
let originalURL: String?
|
||||
|
||||
}
|
||||
|
||||
extension LocalizedTagFile: Codable {
|
||||
|
||||
}
|
Reference in New Issue
Block a user