feat: implement SQLite database layer

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
panw
2026-03-31 16:10:52 +08:00
parent 94d3952711
commit 5a7801bdc8

View File

@@ -0,0 +1,240 @@
package database
import (
"database/sql"
"fmt"
"time"
_ "github.com/mattn/go-sqlite3"
"gitm/internal/models"
)
var DB *sql.DB
func Initialize(dbPath string) error {
var err error
DB, err = sql.Open("sqlite3", dbPath+"?_foreign_keys=on")
if err != nil {
return fmt.Errorf("failed to open database: %w", err)
}
if err = DB.Ping(); err != nil {
return fmt.Errorf("failed to ping database: %w", err)
}
if err = createTables(); err != nil {
return fmt.Errorf("failed to create tables: %w", err)
}
return nil
}
func createTables() error {
queries := []string{
`CREATE TABLE IF NOT EXISTS gitea_servers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
url TEXT NOT NULL,
token TEXT NOT NULL,
sync_interval INTEGER DEFAULT 0,
last_sync_at DATETIME,
status TEXT DEFAULT 'active',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`,
`CREATE TABLE IF NOT EXISTS repos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
server_id INTEGER NOT NULL,
name TEXT,
full_name TEXT,
clone_url TEXT,
local_path TEXT,
size INTEGER DEFAULT 0,
last_sync_at DATETIME,
sync_status TEXT DEFAULT 'pending',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (server_id) REFERENCES gitea_servers(id) ON DELETE CASCADE
)`,
`CREATE TABLE IF NOT EXISTS sync_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
server_id INTEGER NOT NULL,
repo_id INTEGER,
status TEXT NOT NULL,
message TEXT,
started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
finished_at DATETIME,
FOREIGN KEY (server_id) REFERENCES gitea_servers(id) ON DELETE CASCADE,
FOREIGN KEY (repo_id) REFERENCES repos(id) ON DELETE SET NULL
)`,
`CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT
)`,
}
for _, q := range queries {
if _, err := DB.Exec(q); err != nil {
return fmt.Errorf("failed to create table: %w", err)
}
}
return nil
}
func Close() error {
if DB != nil {
return DB.Close()
}
return nil
}
// Settings
func GetSetting(key string) (string, error) {
var value string
err := DB.QueryRow("SELECT value FROM settings WHERE key = ?", key).Scan(&value)
if err == sql.ErrNoRows {
return "", nil
}
return value, err
}
func SetSetting(key, value string) error {
_, err := DB.Exec("INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value", key, value)
return err
}
// Server CRUD
func CreateServer(server *models.GiteaServer) error {
result, err := DB.Exec("INSERT INTO gitea_servers (name, url, token, sync_interval, status) VALUES (?, ?, ?, ?, ?)",
server.Name, server.URL, server.Token, server.SyncInterval, server.Status)
if err != nil {
return err
}
id, _ := result.LastInsertId()
server.ID = id
return nil
}
func GetServers() ([]models.GiteaServer, error) {
rows, err := DB.Query("SELECT id, name, url, token, sync_interval, last_sync_at, status, created_at FROM gitea_servers ORDER BY id")
if err != nil {
return nil, err
}
defer rows.Close()
var servers []models.GiteaServer
for rows.Next() {
var s models.GiteaServer
if err := rows.Scan(&s.ID, &s.Name, &s.URL, &s.Token, &s.SyncInterval, &s.LastSyncAt, &s.Status, &s.CreatedAt); err != nil {
return nil, err
}
servers = append(servers, s)
}
return servers, nil
}
func GetServer(id int64) (*models.GiteaServer, error) {
var s models.GiteaServer
err := DB.QueryRow("SELECT id, name, url, token, sync_interval, last_sync_at, status, created_at FROM gitea_servers WHERE id = ?", id).
Scan(&s.ID, &s.Name, &s.URL, &s.Token, &s.SyncInterval, &s.LastSyncAt, &s.Status, &s.CreatedAt)
if err == sql.ErrNoRows {
return nil, nil
}
return &s, err
}
func UpdateServer(server *models.GiteaServer) error {
_, err := DB.Exec("UPDATE gitea_servers SET name = ?, url = ?, token = ?, sync_interval = ?, status = ? WHERE id = ?",
server.Name, server.URL, server.Token, server.SyncInterval, server.Status, server.ID)
return err
}
func DeleteServer(id int64) error {
_, err := DB.Exec("DELETE FROM gitea_servers WHERE id = ?", id)
return err
}
func UpdateServerLastSync(id int64) error {
_, err := DB.Exec("UPDATE gitea_servers SET last_sync_at = ? WHERE id = ?", time.Now(), id)
return err
}
// Repo operations
func CreateRepo(repo *models.Repo) error {
result, err := DB.Exec("INSERT INTO repos (server_id, name, full_name, clone_url, local_path, sync_status) VALUES (?, ?, ?, ?, ?, ?)",
repo.ServerID, repo.Name, repo.FullName, repo.CloneURL, repo.LocalPath, repo.SyncStatus)
if err != nil {
return err
}
id, _ := result.LastInsertId()
repo.ID = id
return nil
}
func GetReposByServer(serverID int64) ([]models.Repo, error) {
rows, err := DB.Query("SELECT id, server_id, name, full_name, clone_url, local_path, size, last_sync_at, sync_status, created_at FROM repos WHERE server_id = ? ORDER BY full_name", serverID)
if err != nil {
return nil, err
}
defer rows.Close()
var repos []models.Repo
for rows.Next() {
var r models.Repo
if err := rows.Scan(&r.ID, &r.ServerID, &r.Name, &r.FullName, &r.CloneURL, &r.LocalPath, &r.Size, &r.LastSyncAt, &r.SyncStatus, &r.CreatedAt); err != nil {
return nil, err
}
repos = append(repos, r)
}
return repos, nil
}
func UpdateRepoSyncStatus(id int64, status string) error {
_, err := DB.Exec("UPDATE repos SET sync_status = ?, last_sync_at = ? WHERE id = ?", status, time.Now(), id)
return err
}
func GetRepoByFullName(serverID int64, fullName string) (*models.Repo, error) {
var r models.Repo
err := DB.QueryRow("SELECT id, server_id, name, full_name, clone_url, local_path, size, last_sync_at, sync_status, created_at FROM repos WHERE server_id = ? AND full_name = ?", serverID, fullName).
Scan(&r.ID, &r.ServerID, &r.Name, &r.FullName, &r.CloneURL, &r.LocalPath, &r.Size, &r.LastSyncAt, &r.SyncStatus, &r.CreatedAt)
if err == sql.ErrNoRows {
return nil, nil
}
return &r, err
}
// SyncLog operations
func CreateSyncLog(log *models.SyncLog) error {
result, err := DB.Exec("INSERT INTO sync_logs (server_id, repo_id, status, message, started_at) VALUES (?, ?, ?, ?, ?)",
log.ServerID, log.RepoID, log.Status, log.Message, log.StartedAt)
if err != nil {
return err
}
id, _ := result.LastInsertId()
log.ID = id
return nil
}
func UpdateSyncLog(id int64, status, message string, finishedAt *time.Time) error {
_, err := DB.Exec("UPDATE sync_logs SET status = ?, message = ?, finished_at = ? WHERE id = ?", status, message, finishedAt, id)
return err
}
func GetSyncLogs(serverID int64, limit, offset int) ([]models.SyncLog, error) {
query := "SELECT id, server_id, repo_id, status, message, started_at, finished_at FROM sync_logs"
var args []interface{}
if serverID > 0 {
query += " WHERE server_id = ?"
args = append(args, serverID)
}
query += " ORDER BY started_at DESC LIMIT ? OFFSET ?"
args = append(args, limit, offset)
rows, err := DB.Query(query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var logs []models.SyncLog
for rows.Next() {
var l models.SyncLog
if err := rows.Scan(&l.ID, &l.ServerID, &l.RepoID, &l.Status, &l.Message, &l.StartedAt, &l.FinishedAt); err != nil {
return nil, err
}
logs = append(logs, l)
}
return logs, nil
}