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