diff --git a/Sources/TranslationCatalogFilesystem/DirectoryCatalog.swift b/Sources/TranslationCatalogFilesystem/DirectoryCatalog.swift index 390e7e5..6829757 100644 --- a/Sources/TranslationCatalogFilesystem/DirectoryCatalog.swift +++ b/Sources/TranslationCatalogFilesystem/DirectoryCatalog.swift @@ -7,37 +7,16 @@ public typealias FilesystemCatalog = DirectoryCatalog /// Implementation of `Catalog` the reads/writes data from/to a filesystem directory. public class DirectoryCatalog: FilesystemContainer { - let url: URL + let medium: URL + let translationContainer: URL + let expressionContainer: URL + let projectContainer: URL var translationDocuments: [TranslationDocument] = [] var expressionDocuments: [ExpressionDocument] = [] var projectDocuments: [ProjectDocument] = [] - @available(*, deprecated, renamed: "url") - var medium: URL { url } - - var translationContainer: URL { - guard let url = try? directory(forPath: Self.translationsPath) else { - preconditionFailure("Invalid Translation Directory") - } - - return url - } - - var expressionContainer: URL { - guard let url = try? directory(forPath: Self.expressionsPath) else { - preconditionFailure("Invalid Expression Directory") - } - - return url - } - - var projectContainer: URL { - guard let url = try? directory(forPath: Self.projectsPath) else { - preconditionFailure("") - } - - return url - } + @available(*, deprecated, message: "Match 'FilesystemContainer' protocol requirements.", renamed: "medium") + var url: URL { medium } private let fileManager = FileManager.default @@ -46,7 +25,10 @@ public class DirectoryCatalog: FilesystemContainer { throw URLError(.unsupportedURL) } - self.url = url + medium = url + translationContainer = try FileManager.default.directory(forPath: Self.translationsPath, of: url) + expressionContainer = try FileManager.default.directory(forPath: Self.expressionsPath, of: url) + projectContainer = try FileManager.default.directory(forPath: Self.projectsPath, of: url) if let schemaVersion = getSchemaVersion() { try migrateSchema(from: schemaVersion, to: .current) try loadAllDocuments() @@ -56,14 +38,6 @@ public class DirectoryCatalog: FilesystemContainer { } } - private func directory(forPath path: String) throws -> URL { - let url = url.appending(path: path, directoryHint: .isDirectory) - if !fileManager.fileExists(atPath: url.path()) { - try fileManager.createDirectory(at: url, withIntermediateDirectories: true) - } - return url - } - func loadDocuments(from container: URL, using decoder: JSONDecoder) throws -> [T] { try fileManager .contentsOfDirectory(at: container, includingPropertiesForKeys: nil) @@ -105,7 +79,7 @@ public class DirectoryCatalog: FilesystemContainer { } func getSchemaVersion(using decoder: JSONDecoder) -> DocumentSchemaVersion? { - let url = url.appending(path: Self.versionPath, directoryHint: .notDirectory) + let url = medium.appending(path: Self.versionPath, directoryHint: .notDirectory) do { let data = try Data(contentsOf: url) @@ -117,7 +91,7 @@ public class DirectoryCatalog: FilesystemContainer { } func setSchemaVersion(_ version: DocumentSchemaVersion, using encoder: JSONEncoder) throws { - let url = url.appending(path: Self.versionPath, directoryHint: .notDirectory) + let url = medium.appending(path: Self.versionPath, directoryHint: .notDirectory) let data = try encoder.encode(version.rawValue) try data.write(to: url) } diff --git a/Sources/TranslationCatalogFilesystem/Extensions/FileManager+TranslationCatalogFilesystem.swift b/Sources/TranslationCatalogFilesystem/Extensions/FileManager+TranslationCatalogFilesystem.swift new file mode 100644 index 0000000..1129186 --- /dev/null +++ b/Sources/TranslationCatalogFilesystem/Extensions/FileManager+TranslationCatalogFilesystem.swift @@ -0,0 +1,11 @@ +import Foundation + +extension FileManager { + func directory(forPath path: String, of url: URL) throws -> URL { + let url = url.appending(path: path, directoryHint: .isDirectory) + if !fileExists(atPath: url.path()) { + try createDirectory(at: url, withIntermediateDirectories: true) + } + return url + } +} diff --git a/Sources/TranslationCatalogFilesystem/Extensions/FileWrapper+TranslationCatalogFilesystem.swift b/Sources/TranslationCatalogFilesystem/Extensions/FileWrapper+TranslationCatalogFilesystem.swift new file mode 100644 index 0000000..f00cdad --- /dev/null +++ b/Sources/TranslationCatalogFilesystem/Extensions/FileWrapper+TranslationCatalogFilesystem.swift @@ -0,0 +1,16 @@ +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) +import Foundation + +extension FileWrapper { + func directory(forPath path: String) -> FileWrapper { + guard let wrapper = fileWrappers?[path] else { + let directoryWrapper = FileWrapper(directoryWithFileWrappers: [:]) + directoryWrapper.preferredFilename = path + addFileWrapper(directoryWrapper) + return directoryWrapper + } + + return wrapper + } +} +#endif diff --git a/Sources/TranslationCatalogFilesystem/FileWrapperCatalog.swift b/Sources/TranslationCatalogFilesystem/FileWrapperCatalog.swift index f5cf0e5..5b14c4c 100644 --- a/Sources/TranslationCatalogFilesystem/FileWrapperCatalog.swift +++ b/Sources/TranslationCatalogFilesystem/FileWrapperCatalog.swift @@ -6,28 +6,23 @@ import TranslationCatalog public class FileWrapperCatalog: FilesystemContainer { let medium: FileWrapper + let translationContainer: FileWrapper + let expressionContainer: FileWrapper + let projectContainer: FileWrapper var translationDocuments: [TranslationDocument] = [] var expressionDocuments: [ExpressionDocument] = [] var projectDocuments: [ProjectDocument] = [] - var translationContainer: FileWrapper { - directory(forPath: Self.translationsPath) - } - - var expressionContainer: FileWrapper { - directory(forPath: Self.expressionsPath) - } - - var projectContainer: FileWrapper { - directory(forPath: Self.projectsPath) - } - public init(fileWrapper: FileWrapper) throws { guard fileWrapper.isDirectory else { throw CocoaError(.fileReadUnsupportedScheme) } medium = fileWrapper + translationContainer = fileWrapper.directory(forPath: Self.translationsPath) + expressionContainer = fileWrapper.directory(forPath: Self.expressionsPath) + projectContainer = fileWrapper.directory(forPath: Self.projectsPath) + if let schemaVersion = getSchemaVersion() { try migrateSchema(from: schemaVersion, to: .current) try loadAllDocuments() @@ -37,15 +32,76 @@ public class FileWrapperCatalog: FilesystemContainer { } } - private func directory(forPath path: String) -> FileWrapper { - guard let wrapper = medium.fileWrappers?[path] else { - let directoryWrapper = FileWrapper(directoryWithFileWrappers: [:]) - directoryWrapper.preferredFilename = path - medium.addFileWrapper(directoryWrapper) - return directoryWrapper + /// Add all catalog content to the provided `FileWrapper` + public func snapshot(to fileWrapper: FileWrapper, using encoder: JSONEncoder) throws { + let translationsWrapper = fileWrapper.directory(forPath: Self.translationsPath) + let expressionsWrapper = fileWrapper.directory(forPath: Self.expressionsPath) + let projectsWrapper = fileWrapper.directory(forPath: Self.projectsPath) + + let existingTranslations = Set((translationsWrapper.fileWrappers ?? [:]).keys) + let existingExpressions = Set((expressionsWrapper.fileWrappers ?? [:]).keys) + let existingProjects = Set((projectsWrapper.fileWrappers ?? [:]).keys) + + let nextTranslations = Set(translationDocuments.map(\.filename)) + let nextExpressions = Set(expressionDocuments.map(\.filename)) + let nextProjects = Set(projectDocuments.map(\.filename)) + + // Translations + let addedTranslations = nextTranslations.subtracting(existingTranslations) + let removedTranslations = existingTranslations.subtracting(nextTranslations) + for filename in removedTranslations { + if let wrapper = translationsWrapper.fileWrappers?[filename] { + translationsWrapper.removeFileWrapper(wrapper) + } + } + + for document in translationDocuments { + let data = try encoder.encode(document) + if !addedTranslations.contains(document.filename) { + if let wrapper = translationsWrapper.fileWrappers?[document.filename] { + translationsWrapper.removeFileWrapper(wrapper) + } + } + translationsWrapper.addRegularFile(withContents: data, preferredFilename: document.filename) + } + + // Expressions + let addedExpressions = nextExpressions.subtracting(existingExpressions) + let removedExpressions = existingExpressions.subtracting(nextExpressions) + for filename in removedExpressions { + if let wrapper = expressionsWrapper.fileWrappers?[filename] { + expressionsWrapper.removeFileWrapper(wrapper) + } + } + + for document in expressionDocuments { + let data = try encoder.encode(document) + if !addedExpressions.contains(document.filename) { + if let wrapper = expressionsWrapper.fileWrappers?[document.filename] { + expressionsWrapper.removeFileWrapper(wrapper) + } + } + expressionsWrapper.addRegularFile(withContents: data, preferredFilename: document.filename) + } + + // Projects + let addedProjects = nextProjects.subtracting(existingProjects) + let removedProjects = existingProjects.subtracting(nextProjects) + for filename in removedProjects { + if let wrapper = projectsWrapper.fileWrappers?[filename] { + projectsWrapper.removeFileWrapper(wrapper) + } } - return wrapper + for document in projectDocuments { + let data = try encoder.encode(document) + if !addedProjects.contains(document.filename) { + if let wrapper = expressionsWrapper.fileWrappers?[document.filename] { + expressionsWrapper.removeFileWrapper(wrapper) + } + } + projectsWrapper.addRegularFile(withContents: data, preferredFilename: document.filename) + } } func loadDocuments(from container: FileWrapper, using decoder: JSONDecoder) throws -> [T] {