// // PersistenceService.swift // Zyquo Cloud // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // // JSON persistence in ~/Library/Application Support/ZyquoCloud/: // Conversations/.json one file per conversation // personas.json, templates.json, favorites.json, settings.json // import Foundation struct PersistenceService { static let shared = PersistenceService() let rootDirectory: URL var conversationsDirectory: URL { rootDirectory.appendingPathComponent("Conversations") } private let encoder: JSONEncoder private let decoder: JSONDecoder init(rootDirectory: URL? = nil) { let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] self.rootDirectory = rootDirectory ?? base.appendingPathComponent("ZyquoCloud") encoder = JSONEncoder() encoder.outputFormatting = [.prettyPrinted, .sortedKeys] encoder.dateEncodingStrategy = .iso8601 decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 try? FileManager.default.createDirectory(at: conversationsDirectory, withIntermediateDirectories: true) } // MARK: - Conversations func loadConversations() -> [Conversation] { guard let files = try? FileManager.default.contentsOfDirectory( at: conversationsDirectory, includingPropertiesForKeys: nil ) else { return [] } return files .filter { $0.pathExtension == "json" } .compactMap { url in guard let data = try? Data(contentsOf: url) else { return nil } return try? decoder.decode(Conversation.self, from: data) } .sorted { $0.updatedAt > $1.updatedAt } } func save(_ conversation: Conversation) { let url = conversationsDirectory.appendingPathComponent("\(conversation.id.uuidString).json") guard let data = try? encoder.encode(conversation) else { return } try? data.write(to: url, options: .atomic) } func delete(_ conversation: Conversation) { let url = conversationsDirectory.appendingPathComponent("\(conversation.id.uuidString).json") try? FileManager.default.removeItem(at: url) } // MARK: - Generic documents (personas, templates, settings…) func load(_ type: T.Type, from fileName: String) -> T? { let url = rootDirectory.appendingPathComponent(fileName) guard let data = try? Data(contentsOf: url) else { return nil } return try? decoder.decode(type, from: data) } func save(_ value: T, to fileName: String) { let url = rootDirectory.appendingPathComponent(fileName) guard let data = try? encoder.encode(value) else { return } try? data.write(to: url, options: .atomic) } }