feat: implement SQLite database layer
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
240
internal/database/database.go
Normal file
240
internal/database/database.go
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user