diff --git a/internal/database/database.go b/internal/database/database.go new file mode 100644 index 0000000..267a940 --- /dev/null +++ b/internal/database/database.go @@ -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 +}