diff --git a/cmd/server/api/api.go b/cmd/server/api/api.go index ea87bc3..73a63d3 100644 --- a/cmd/server/api/api.go +++ b/cmd/server/api/api.go @@ -43,6 +43,7 @@ func NewServer(data *storage.Repository, port int) *HTTPServer { // Get information about the server r.Get("/version", s.Information) r.Route("/projects", func(r chi.Router) { + r.Get("/all", s.ProjectsHandler) r.Get("/{name}", func(w http.ResponseWriter, r *http.Request) {}) r.Post("/{name}", s.ProjectPostHandler) r.Delete("/{name}", func(w http.ResponseWriter, r *http.Request) {}) @@ -91,3 +92,14 @@ func (s *HTTPServer) ProjectPostHandler(w http.ResponseWriter, r *http.Request) w.WriteHeader(201) } + +func (s *HTTPServer) ProjectsHandler(w http.ResponseWriter, r *http.Request) { + prs, err := s.data.List() + if err != nil { + slog.Error("failed to fetch all the projects from the database", "err", err) + internalServerError(err, w, r) + return + } + + ok(prs, w, r) +} diff --git a/cmd/server/core/storage/migrations/001_first_schema.sql b/cmd/server/core/storage/migrations/001_first_schema.sql index 145998c..598056b 100644 --- a/cmd/server/core/storage/migrations/001_first_schema.sql +++ b/cmd/server/core/storage/migrations/001_first_schema.sql @@ -1,18 +1,20 @@ -- +goose Up CREATE TABLE Projects ( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + uuid TEXT NOT NULL, name TEXT NOT NULL ); +CREATE INDEX Projects_uuid_IDX ON Projects (uuid); CREATE TABLE Repositories ( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + uuid TEXT NOT NULL, name TEXT NOT NULL, schedule TEXT NOT NULL, "source" TEXT NOT NULL, destination TEXT NOT NULL, project INTEGER NOT NULL ); - +CREATE INDEX Repositories_uuid_IDX ON Repositories (uuid); -- +goose Down -DROP TABLE Projects; \ No newline at end of file +DROP TABLE Projects; +DROP TABLE Repositories; \ No newline at end of file diff --git a/cmd/server/core/storage/storage.go b/cmd/server/core/storage/storage.go index c0be832..c5291ac 100644 --- a/cmd/server/core/storage/storage.go +++ b/cmd/server/core/storage/storage.go @@ -2,10 +2,12 @@ package storage import ( "database/sql" + "errors" "fmt" "mirror-sync/pkg/project" _ "github.com/glebarez/go-sqlite" + "github.com/google/uuid" ) type ( @@ -27,6 +29,70 @@ func OpenDB(path string) (*Repository, error) { } func (r *Repository) Save(pr project.Project) (err error) { + exists, err := r.ProjectExistsByName(pr.Name) + if err != nil { + return err + } + + if exists { + return r.Update(pr) + } + + return r.Create(pr) +} + +func (r *Repository) ProjectExistsByUUID(uuid string) (bool, error) { + row := r.db.QueryRow("SELECT uuid FROM Projects WHERE uuid = ?", uuid) + if row.Err() != nil { + return false, fmt.Errorf("failed to get row from database: %w", row.Err()) + } + + var id string + if err := row.Scan(&id); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return false, nil + } + return false, fmt.Errorf("failed to scan row: %w", err) + } + + return true, nil +} + +func (r *Repository) ProjectExistsByName(name string) (bool, error) { + row := r.db.QueryRow("SELECT uuid FROM Projects WHERE name = ?", name) + if row.Err() != nil { + return false, fmt.Errorf("failed to get row from database: %w", row.Err()) + } + + var uuid string + if err := row.Scan(&uuid); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return false, nil + } + return false, fmt.Errorf("failed to scan row: %w", err) + } + + return true, nil +} + +func (r *Repository) RepositoryExistsByName(name string) (bool, error) { + row := r.db.QueryRow("SELECT uuid FROM Repositories WHERE name = ?", name) + if row.Err() != nil { + return false, fmt.Errorf("failed to get row from database: %w", row.Err()) + } + + var uuid string + if err := row.Scan(&uuid); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return false, nil + } + return false, fmt.Errorf("failed to scan row: %w", err) + } + + return true, nil +} + +func (r *Repository) Create(pr project.Project) error { tx, err := r.db.Begin() if err != nil { return fmt.Errorf("failed to create transaction: %s", err) @@ -39,35 +105,159 @@ func (r *Repository) Save(pr project.Project) (err error) { tx.Commit() }() - stmt, err := tx.Prepare("INSERT INTO Projects (name) VALUES (?)") + // Create Project entry + projectUUID := uuid.NewString() + + stmt, err := tx.Prepare("INSERT INTO Projects (uuid, name) VALUES (?, ?)") if err != nil { return fmt.Errorf("failed to create statement: %s", err) } - if _, err := stmt.Exec(pr.Name); err != nil { + if _, err := stmt.Exec(projectUUID, pr.Name); err != nil { return fmt.Errorf("failed to execute sql query: %s", err) } - rows, err := tx.Query("SELECT id FROM Projects WHERE name = ?", pr.Name) + // Create repositories entries + stmt, err = tx.Prepare("INSERT INTO Repositories (uuid, name, source, destination, schedule, project) VALUES (?, ?, ?, ?, ?, ?)") if err != nil { - return fmt.Errorf("failed to query project id: %s", err) - } - defer rows.Close() - - var id int - rows.Next() - if err := rows.Scan(&id); err != nil { - return fmt.Errorf("failed to query project id: %s", err) + return fmt.Errorf("failed to create statement: %s", err) } for _, repo := range pr.Repositories { - stmt, err := tx.Prepare("INSERT INTO Repositories (name, source, destination, schedule, project) VALUES (?, ?, ?, ?, ?)") - if err != nil { - return fmt.Errorf("failed to create statement: %s", err) - } - if _, err := stmt.Exec(repo.Name, repo.Source, repo.Destination, repo.Schedule, id); err != nil { + repoUUID := uuid.NewString() + + if _, err := stmt.Exec(repoUUID, repo.Name, repo.Source, repo.Destination, repo.Schedule, projectUUID); err != nil { return fmt.Errorf("failed to execute sql query: %s", err) } } return nil } + +func (r *Repository) ProjectUUID(name string) (string, error) { + row := r.db.QueryRow("SELECT uuid FROM Projects WHERE name = ?", name) + if row.Err() != nil { + return "", fmt.Errorf("failed to get row from database: %w", row.Err()) + } + + var uuid string + if err := row.Scan(&uuid); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return "", nil + } + return "", fmt.Errorf("failed to scan row: %w", err) + } + + return uuid, nil +} + +func (r *Repository) RepositoryUUID(name string) (string, error) { + row := r.db.QueryRow("SELECT uuid FROM Repositories WHERE name = ?", name) + if row.Err() != nil { + return "", fmt.Errorf("failed to get row from database: %w", row.Err()) + } + + var uuid string + if err := row.Scan(&uuid); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return "", nil + } + return "", fmt.Errorf("failed to scan row: %w", err) + } + + return uuid, nil +} + +func (r *Repository) Update(pr project.Project) error { + tx, err := r.db.Begin() + if err != nil { + return fmt.Errorf("failed to create transaction: %s", err) + } + defer func() { + if err != nil { + tx.Rollback() + return + } + tx.Commit() + }() + + projectUUID, err := r.ProjectUUID(pr.Name) + if err != nil { + return fmt.Errorf("failed to get project uuid: %w", err) + } + + stmt, err := tx.Prepare("UPDATE Repositories SET schedule = ?, source = ?, destination = ? WHERE uuid = ?") + if err != nil { + return fmt.Errorf("failed to create statement: %w", err) + } + + // this loop does NOT remove orphan + for _, repo := range pr.Repositories { + // checks if the repo exists + exists, err := r.RepositoryExistsByName(repo.Name) + if err != nil { + return fmt.Errorf("failed to fetch uuid from the database: %w", err) + } + + if exists { + // if it exists, just update it + uuid, err := r.RepositoryUUID(repo.Name) + if err != nil { + return fmt.Errorf("failed to get uuid from database: %w", err) + } + + if _, err := stmt.Exec(repo.Schedule, repo.Source, repo.Destination, uuid); err != nil { + return fmt.Errorf("failed to update repository entry for %s::'%s'", uuid, repo.Name) + } + } else { + // if not, create a new uuid and create the entry + repoUUID := uuid.NewString() + + if _, err := stmt.Exec(repoUUID, repo.Name, repo.Source, repo.Destination, repo.Schedule, projectUUID); err != nil { + return fmt.Errorf("failed to execute sql query: %s", err) + } + } + if _, err := stmt.Exec(repo.Schedule, repo.Source, repo.Destination, repo.Name); err != nil { + return fmt.Errorf("failed to update repository entry for '%s'", repo.Name) + } + } + + return nil +} + +func (r *Repository) List() ([]project.Project, error) { + var prs []project.Project + + rows, err := r.db.Query("SELECT uuid, name WHERE Projects") + if err != nil { + return nil, fmt.Errorf("failed to get the list of projects: %w", err) + } + defer rows.Close() + + stmt, err := r.db.Prepare("SELECT name, schedule, source, destination WHERE uuid = ?") + if err != nil { + return nil, fmt.Errorf("invalid syntax: %w", err) + } + for rows.Next() { + var pr project.Project + var prUUID string + if err := rows.Scan(&prUUID, &pr.Name); err != nil { + return nil, fmt.Errorf("failed to scan project name: %w", err) + } + + for rows.Next() { + var repo project.Repository + rows, err := stmt.Query(prUUID) + if err != nil { + return nil, fmt.Errorf("failed to get the list of projects: %w", err) + } + if err := rows.Scan(&repo.Name, &repo.Schedule, &repo.Source, &repo.Destination); err != nil { + return nil, fmt.Errorf("failed to scan repository entry: %w", err) + } + + pr.Repositories = append(pr.Repositories, repo) + } + prs = append(prs, pr) + } + + return prs, nil +} diff --git a/go.mod b/go.mod index 7ff54d5..850a216 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/go-git/go-git/v6 v6.0.0-20250929195514-145daf2492dd github.com/goccy/go-yaml v1.18.0 github.com/google/subcommands v1.2.0 + github.com/google/uuid v1.6.0 github.com/pressly/goose/v3 v3.26.0 ) @@ -21,7 +22,6 @@ require ( github.com/go-git/gcfg/v2 v2.0.2 // indirect github.com/go-git/go-billy/v6 v6.0.0-20250627091229-31e2a16eef30 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/kevinburke/ssh_config v1.4.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect