Luminate/Sources/LuminateCore/SQLiteStore.swift

152 lines
5 KiB
Swift

//
// SQLiteStore.swift
//
// Copyright 2026 Brendan Szymanski <hello@bscubed.dev>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
import Foundation
import SQLite
extension Connection: @retroactive @unchecked Sendable {}
public actor SQLiteStore: PersistenceService {
private let db: Connection
public init(dbURL: URL) 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 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")
}
}