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 }