Luminate/Sources/LuminateCore/SQLiteStore.swift

138 lines
4.3 KiB
Swift

//
// SQLiteStore.swift
// LuminateCore
//
// Created by Brendan Szymanski on 6/10/26.
//
import Foundation
import SQLite
extension Connection: @retroactive @unchecked Sendable {}
public actor SQLiteStore: PersistenceService {
private let db: Connection
public init(dbURL: URL) async throws {
let directory = dbURL.deletingLastPathComponent()
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
db = try Connection(dbURL.path)
db.busyTimeout = 5
try db.execute("PRAGMA journal_mode = WAL")
try migrate()
}
private nonisolated func migrate() throws {
let version = db.userVersion
switch version {
case 0:
try db.execute(
"""
CREATE TABLE IF NOT EXISTS auth (
id INTEGER PRIMARY KEY CHECK (id = 1),
server_url TEXT NOT NULL,
token TEXT NOT NULL,
user_id TEXT NOT NULL,
username TEXT NOT NULL
)
"""
)
try db.execute(
"""
CREATE TABLE IF NOT EXISTS preferences (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)
"""
)
db.userVersion = 1
fallthrough
default:
break
}
}
public static func defaultDatabaseURL() -> URL {
if #available(macOS 13, *) {
let appSupport = FileManager.default.urls(
for: .applicationSupportDirectory,
in: .userDomainMask
).first!
return
appSupport
.appendingPathComponent("dev.bscubed.Luminate")
.appendingPathComponent("db.sqlite")
} else {
#if os(Linux)
let xdgData =
ProcessInfo.processInfo.environment["XDG_DATA_HOME"]
?? "\(FileManager.default.homeDirectoryForCurrentUser.path).local/share"
return URL(fileURLWithPath: xdgData)
.appendingPathComponent("dev.bscubed.Luminate")
.appendingPathComponent("db.sqlite")
#else
let appSupport = FileManager.default.urls(
for: .applicationSupportDirectory,
in: .userDomainMask
).first!
return
appSupport
.appendingPathComponent("dev.bscubed.Luminate")
.appendingPathComponent("db.sqlite")
#endif
}
}
public func loadAuth() async throws -> AuthData {
let stmt = try db.prepare(
"SELECT server_url, token, user_id, username FROM auth WHERE id = 1")
for row in stmt {
guard let serverURL = row[0] as? String,
let token = row[1] as? String,
let userId = row[2] as? String,
let username = row[3] as? String
else {
throw PersistenceError.decodingFailed
}
return AuthData(
serverURL: serverURL,
token: token,
userId: userId,
username: username
)
}
throw PersistenceError.notFound
}
public func saveAuth(_ auth: AuthData) async throws {
try db.run(
"INSERT OR REPLACE INTO auth (id, server_url, token, user_id, username) VALUES (1, ?, ?, ?, ?)",
auth.serverURL, auth.token, auth.userId, auth.username
)
}
public func clearAuth() async throws {
try db.run("DELETE FROM auth WHERE id = 1")
}
public func getPreference(key: String) async throws -> String? {
let stmt = try db.prepare("SELECT value FROM preferences WHERE key = ?", key)
for row in stmt {
return row[0] as? String
}
return nil
}
public func setPreference(key: String, value: String) async throws {
try db.run(
"INSERT OR REPLACE INTO preferences (key, value) VALUES (?, ?)",
key, value
)
}
public func clearAll() async throws {
try db.run("DELETE FROM auth")
try db.run("DELETE FROM preferences")
}
}