Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 12 additions & 38 deletions Sources/TranslationCatalogFilesystem/DirectoryCatalog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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()
Expand All @@ -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<T: Document>(from container: URL, using decoder: JSONDecoder) throws -> [T] {
try fileManager
.contentsOfDirectory(at: container, includingPropertiesForKeys: nil)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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
94 changes: 75 additions & 19 deletions Sources/TranslationCatalogFilesystem/FileWrapperCatalog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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<T: Document>(from container: FileWrapper, using decoder: JSONDecoder) throws -> [T] {
Expand Down
Loading