ce8026d46e
- serialise publishes per crate with an advisory lock in one transaction - add a unique index on normalised name and version without build metadata - validate versions as strict SemVer 2.0 and reject an empty .crate - renumber the migration to 0005
112 lines
3.3 KiB
Go
112 lines
3.3 KiB
Go
package database
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/jackc/pgx/v5/pgconn"
|
|
|
|
"git.unkin.net/unkin/artifactapi/internal/provider"
|
|
)
|
|
|
|
var ErrCargoNameTaken = errors.New("crate name already taken")
|
|
|
|
func cargoNormName(name string) string {
|
|
return strings.ReplaceAll(strings.ToLower(name), "-", "_")
|
|
}
|
|
|
|
// InsertCargoIndexEntry rejects a name that differs from an existing crate only
|
|
// by case or -/_ (ErrCargoNameTaken) and a version equal up to build metadata
|
|
// (ErrAlreadyExists). A per-crate advisory lock serialises concurrent publishes.
|
|
func (db *DB) InsertCargoIndexEntry(ctx context.Context, e *provider.CargoIndexEntry) error {
|
|
tx, err := db.Pool.Begin(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = tx.Rollback(ctx) }()
|
|
|
|
norm := cargoNormName(e.Name)
|
|
if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtext('cargo:' || $1 || '/' || $2))`, e.RepoName, norm); err != nil {
|
|
return err
|
|
}
|
|
rows, err := tx.Query(ctx, `
|
|
SELECT name, version FROM cargo_index
|
|
WHERE repo_name = $1 AND replace(lower(name), '-', '_') = $2
|
|
`, e.RepoName, norm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
base, _, _ := strings.Cut(e.Version, "+")
|
|
for rows.Next() {
|
|
var name, version string
|
|
if err := rows.Scan(&name, &version); err != nil {
|
|
return err
|
|
}
|
|
if name != e.Name {
|
|
return fmt.Errorf("%w: %q exists as %q", ErrCargoNameTaken, e.Name, name)
|
|
}
|
|
if v, _, _ := strings.Cut(version, "+"); v == base {
|
|
return fmt.Errorf("%w: %s@%s", ErrAlreadyExists, name, version)
|
|
}
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return err
|
|
}
|
|
rows.Close()
|
|
|
|
if _, err := tx.Exec(ctx, `
|
|
INSERT INTO cargo_index (repo_name, file_path, name, version, line, yanked)
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
`, e.RepoName, e.FilePath, e.Name, e.Version, e.Line, e.Yanked); err != nil {
|
|
var pgErr *pgconn.PgError
|
|
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
|
|
return fmt.Errorf("%w: %s@%s", ErrAlreadyExists, e.Name, e.Version)
|
|
}
|
|
return err
|
|
}
|
|
return tx.Commit(ctx)
|
|
}
|
|
|
|
func (db *DB) ListCargoIndexEntries(ctx context.Context, repoName, name string) ([]provider.CargoIndexEntry, error) {
|
|
rows, err := db.Pool.Query(ctx, `
|
|
SELECT repo_name, file_path, name, version, line, yanked
|
|
FROM cargo_index
|
|
WHERE repo_name = $1 AND lower(name) = $2
|
|
ORDER BY id
|
|
`, repoName, strings.ToLower(name))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var result []provider.CargoIndexEntry
|
|
for rows.Next() {
|
|
var e provider.CargoIndexEntry
|
|
if err := rows.Scan(&e.RepoName, &e.FilePath, &e.Name, &e.Version, &e.Line, &e.Yanked); err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, e)
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
// SetCargoYanked reports false when no such crate version exists.
|
|
func (db *DB) SetCargoYanked(ctx context.Context, repoName, name, version string, yanked bool) (bool, error) {
|
|
tag, err := db.Pool.Exec(ctx, `
|
|
UPDATE cargo_index SET yanked = $4
|
|
WHERE repo_name = $1 AND lower(name) = $2 AND version = $3
|
|
`, repoName, strings.ToLower(name), version, yanked)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return tag.RowsAffected() > 0, nil
|
|
}
|
|
|
|
func (db *DB) DeleteCargoIndexEntry(ctx context.Context, repoName, filePath string) error {
|
|
_, err := db.Pool.Exec(ctx, `DELETE FROM cargo_index WHERE repo_name = $1 AND file_path = $2`, repoName, filePath)
|
|
return err
|
|
}
|