From 44ad25d17ccf5370766ebdace51ed9a09dd33339 Mon Sep 17 00:00:00 2001 From: Richard Piazza Date: Sun, 19 Jul 2026 13:33:40 -0500 Subject: [PATCH 1/4] FileWrapper Snapshots --- .../DirectoryCatalog.swift | 38 +++------------- ...Manager+TranslationCatalogFilesystem.swift | 13 ++++++ ...Wrapper+TranslationCatalogFilesystem.swift | 16 +++++++ .../FileWrapperCatalog.swift | 44 +++++++++++-------- 4 files changed, 60 insertions(+), 51 deletions(-) create mode 100644 Sources/TranslationCatalogFilesystem/Extensions/FileManager+TranslationCatalogFilesystem.swift create mode 100644 Sources/TranslationCatalogFilesystem/Extensions/FileWrapper+TranslationCatalogFilesystem.swift diff --git a/Sources/TranslationCatalogFilesystem/DirectoryCatalog.swift b/Sources/TranslationCatalogFilesystem/DirectoryCatalog.swift index 390e7e5..a59e292 100644 --- a/Sources/TranslationCatalogFilesystem/DirectoryCatalog.swift +++ b/Sources/TranslationCatalogFilesystem/DirectoryCatalog.swift @@ -8,6 +8,9 @@ public typealias FilesystemCatalog = DirectoryCatalog public class DirectoryCatalog: FilesystemContainer { let url: URL + let translationContainer: URL + let expressionContainer: URL + let projectContainer: URL var translationDocuments: [TranslationDocument] = [] var expressionDocuments: [ExpressionDocument] = [] var projectDocuments: [ProjectDocument] = [] @@ -15,30 +18,6 @@ public class DirectoryCatalog: FilesystemContainer { @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 - } - private let fileManager = FileManager.default public init(url: URL) throws { @@ -47,6 +26,9 @@ public class DirectoryCatalog: FilesystemContainer { } self.url = 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) diff --git a/Sources/TranslationCatalogFilesystem/Extensions/FileManager+TranslationCatalogFilesystem.swift b/Sources/TranslationCatalogFilesystem/Extensions/FileManager+TranslationCatalogFilesystem.swift new file mode 100644 index 0000000..b0129b6 --- /dev/null +++ b/Sources/TranslationCatalogFilesystem/Extensions/FileManager+TranslationCatalogFilesystem.swift @@ -0,0 +1,13 @@ +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) +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 + } +} +#endif 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..2174f81 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,26 @@ 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) + + for document in translationDocuments { + let data = try encoder.encode(document) + translationsWrapper.addRegularFile(withContents: data, preferredFilename: document.filename) } - return wrapper + for document in expressionDocuments { + let data = try encoder.encode(document) + expressionsWrapper.addRegularFile(withContents: data, preferredFilename: document.filename) + } + + for document in projectDocuments { + let data = try encoder.encode(document) + projectsWrapper.addRegularFile(withContents: data, preferredFilename: document.filename) + } } func loadDocuments(from container: FileWrapper, using decoder: JSONDecoder) throws -> [T] { From ef374eb8050964f6a47fa1d921c3a13bb06f76fb Mon Sep 17 00:00:00 2001 From: Richard Piazza Date: Sun, 19 Jul 2026 13:59:09 -0500 Subject: [PATCH 2/4] FileWrapper syncing --- .../FileWrapperCatalog.swift | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/Sources/TranslationCatalogFilesystem/FileWrapperCatalog.swift b/Sources/TranslationCatalogFilesystem/FileWrapperCatalog.swift index 2174f81..e0ccca8 100644 --- a/Sources/TranslationCatalogFilesystem/FileWrapperCatalog.swift +++ b/Sources/TranslationCatalogFilesystem/FileWrapperCatalog.swift @@ -38,18 +38,68 @@ public class FileWrapperCatalog: FilesystemContainer { let expressionsWrapper = fileWrapper.directory(forPath: Self.expressionsPath) let projectsWrapper = fileWrapper.directory(forPath: Self.projectsPath) + let existingTranslations = Set((translationsWrapper.fileWrappers ?? [:]).keys) + let existingExpressions = Set((translationsWrapper.fileWrappers ?? [:]).keys) + let existingProjects = Set((translationsWrapper.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) + } + } + 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) } } From e67d7d0b12c15ec05cc6cfa7e469c1916ec74012 Mon Sep 17 00:00:00 2001 From: Richard Piazza Date: Sun, 19 Jul 2026 14:04:12 -0500 Subject: [PATCH 3/4] Corrected wrapper references --- Sources/TranslationCatalogFilesystem/FileWrapperCatalog.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/TranslationCatalogFilesystem/FileWrapperCatalog.swift b/Sources/TranslationCatalogFilesystem/FileWrapperCatalog.swift index e0ccca8..5b14c4c 100644 --- a/Sources/TranslationCatalogFilesystem/FileWrapperCatalog.swift +++ b/Sources/TranslationCatalogFilesystem/FileWrapperCatalog.swift @@ -39,8 +39,8 @@ public class FileWrapperCatalog: FilesystemContainer { let projectsWrapper = fileWrapper.directory(forPath: Self.projectsPath) let existingTranslations = Set((translationsWrapper.fileWrappers ?? [:]).keys) - let existingExpressions = Set((translationsWrapper.fileWrappers ?? [:]).keys) - let existingProjects = 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)) From a4c845f189d048e0936ab0642dc894b65b40c511 Mon Sep 17 00:00:00 2001 From: Richard Piazza Date: Sun, 19 Jul 2026 14:16:28 -0500 Subject: [PATCH 4/4] Corrected filemanager extension compilation --- .../DirectoryCatalog.swift | 12 ++++++------ .../FileManager+TranslationCatalogFilesystem.swift | 2 -- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Sources/TranslationCatalogFilesystem/DirectoryCatalog.swift b/Sources/TranslationCatalogFilesystem/DirectoryCatalog.swift index a59e292..6829757 100644 --- a/Sources/TranslationCatalogFilesystem/DirectoryCatalog.swift +++ b/Sources/TranslationCatalogFilesystem/DirectoryCatalog.swift @@ -7,7 +7,7 @@ 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 @@ -15,8 +15,8 @@ public class DirectoryCatalog: FilesystemContainer { var expressionDocuments: [ExpressionDocument] = [] var projectDocuments: [ProjectDocument] = [] - @available(*, deprecated, renamed: "url") - var medium: URL { url } + @available(*, deprecated, message: "Match 'FilesystemContainer' protocol requirements.", renamed: "medium") + var url: URL { medium } private let fileManager = FileManager.default @@ -25,7 +25,7 @@ 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) @@ -79,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) @@ -91,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 index b0129b6..1129186 100644 --- a/Sources/TranslationCatalogFilesystem/Extensions/FileManager+TranslationCatalogFilesystem.swift +++ b/Sources/TranslationCatalogFilesystem/Extensions/FileManager+TranslationCatalogFilesystem.swift @@ -1,4 +1,3 @@ -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) import Foundation extension FileManager { @@ -10,4 +9,3 @@ extension FileManager { return url } } -#endif