mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 15:49:16 -05:00
- GetProjectsUsingPackage no longer counts each project's components
per row; the package page only needs names and versions. The ?purl=
listing leaves component_count out accordingly.
- The delete button sends to DELETE /ui/api/projects/{project}, the same
handler as the API and still behind the token, so it sits under /ui
like the cache deletes.
- The project page answers 500 when its components can't be read,
instead of showing an empty project.
- DELETE of a path ending in /sbom that names no project answers 400
with a hint, since it's most likely the upload path.
- The upload endpoint lists JSON, XML and plain text (SPDX tag-value)
as accepted.
- The docs say that versions are compared as the SBOM spells them.
222 lines
7.2 KiB
Go
222 lines
7.2 KiB
Go
package database
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// Project is a repository that uploaded an SBOM to the project inventory.
|
|
type Project struct {
|
|
ID int64 `db:"id"`
|
|
Name string `db:"name"`
|
|
Ref sql.NullString `db:"ref"`
|
|
Branch sql.NullString `db:"branch"`
|
|
UploadedAt time.Time `db:"uploaded_at"`
|
|
// Components is the number of package versions recorded for it.
|
|
Components int64 `db:"components"`
|
|
}
|
|
|
|
// ProjectComponent is a package version a project uses. PackagePURL is the
|
|
// unversioned package URL, as packages.purl records it.
|
|
type ProjectComponent struct {
|
|
PackagePURL string `db:"package_purl"`
|
|
Ecosystem string `db:"ecosystem"`
|
|
Name string `db:"name"`
|
|
Version string `db:"version"`
|
|
}
|
|
|
|
// ProjectComponentInfo is a project's component together with the package
|
|
// record the proxy holds for it, if any.
|
|
type ProjectComponentInfo struct {
|
|
ProjectComponent
|
|
CachedEcosystem sql.NullString `db:"cached_ecosystem"`
|
|
CachedName sql.NullString `db:"cached_name"`
|
|
LatestVersion sql.NullString `db:"latest_version"`
|
|
}
|
|
|
|
// ProjectUsage is a project using a version of a package.
|
|
type ProjectUsage struct {
|
|
Name string `db:"name"`
|
|
Ref sql.NullString `db:"ref"`
|
|
Branch sql.NullString `db:"branch"`
|
|
UploadedAt time.Time `db:"uploaded_at"`
|
|
Version string `db:"version"`
|
|
}
|
|
|
|
const projectColumns = `
|
|
SELECT pr.id, pr.name, pr.ref, pr.branch, pr.uploaded_at,
|
|
(SELECT COUNT(*) FROM project_components pc WHERE pc.project_id = pr.id) AS components`
|
|
|
|
// SaveProject records the components of a project, replacing everything
|
|
// recorded for it before, in one transaction. Readers see either the old or
|
|
// the new set, never a mix.
|
|
func (db *DB) SaveProject(name, ref, branch string, uploadedAt time.Time, components []ProjectComponent) error {
|
|
tx, err := db.Beginx()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
|
|
// The upsert locks the project row, so concurrent uploads of the same
|
|
// project and pruning take turns.
|
|
if _, err := tx.Exec(db.Rebind(`
|
|
INSERT INTO projects (name, ref, branch, uploaded_at) VALUES (?, ?, ?, ?)
|
|
ON CONFLICT(name) DO UPDATE SET ref = excluded.ref, branch = excluded.branch, uploaded_at = excluded.uploaded_at
|
|
`), name, nullString(ref), nullString(branch), uploadedAt.UTC()); err != nil {
|
|
return fmt.Errorf("recording project: %w", err)
|
|
}
|
|
var id int64
|
|
if err := tx.Get(&id, db.Rebind(`SELECT id FROM projects WHERE name = ?`), name); err != nil {
|
|
return fmt.Errorf("reading project id: %w", err)
|
|
}
|
|
if _, err := tx.Exec(db.Rebind(`DELETE FROM project_components WHERE project_id = ?`), id); err != nil {
|
|
return fmt.Errorf("removing old components: %w", err)
|
|
}
|
|
|
|
insert, err := tx.Preparex(db.Rebind(`
|
|
INSERT INTO project_components (project_id, package_purl, ecosystem, name, version)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
ON CONFLICT(project_id, package_purl, version) DO NOTHING
|
|
`))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = insert.Close() }()
|
|
for _, c := range components {
|
|
if _, err := insert.Exec(id, c.PackagePURL, c.Ecosystem, c.Name, c.Version); err != nil {
|
|
return fmt.Errorf("recording component %s@%s: %w", c.PackagePURL, c.Version, err)
|
|
}
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
// DeleteProject removes a project and its components. It reports whether the
|
|
// project existed.
|
|
func (db *DB) DeleteProject(name string) (bool, error) {
|
|
tx, err := db.Beginx()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
|
|
var id int64
|
|
err = tx.Get(&id, db.Rebind(`DELETE FROM projects WHERE name = ? RETURNING id`), name)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return false, nil
|
|
}
|
|
if err != nil {
|
|
return false, fmt.Errorf("deleting project: %w", err)
|
|
}
|
|
if _, err := tx.Exec(db.Rebind(`DELETE FROM project_components WHERE project_id = ?`), id); err != nil {
|
|
return false, fmt.Errorf("deleting project components: %w", err)
|
|
}
|
|
return true, tx.Commit()
|
|
}
|
|
|
|
// PruneProjects removes the projects last uploaded before cutoff, with their
|
|
// components, and returns how many it removed. A project uploaded again while
|
|
// the prune runs is kept.
|
|
func (db *DB) PruneProjects(cutoff time.Time) (int, error) {
|
|
var ids []int64
|
|
if err := db.Select(&ids, db.Rebind(`SELECT id FROM projects WHERE uploaded_at < ?`), cutoff.UTC()); err != nil {
|
|
return 0, fmt.Errorf("listing expired projects: %w", err)
|
|
}
|
|
pruned := 0
|
|
for _, id := range ids {
|
|
ok, err := db.pruneProject(id, cutoff)
|
|
if err != nil {
|
|
return pruned, err
|
|
}
|
|
if ok {
|
|
pruned++
|
|
}
|
|
}
|
|
return pruned, nil
|
|
}
|
|
|
|
func (db *DB) pruneProject(id int64, cutoff time.Time) (bool, error) {
|
|
tx, err := db.Beginx()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
|
|
res, err := tx.Exec(db.Rebind(`DELETE FROM projects WHERE id = ? AND uploaded_at < ?`), id, cutoff.UTC())
|
|
if err != nil {
|
|
return false, fmt.Errorf("pruning project: %w", err)
|
|
}
|
|
if n, err := res.RowsAffected(); err != nil || n == 0 {
|
|
return false, err
|
|
}
|
|
if _, err := tx.Exec(db.Rebind(`DELETE FROM project_components WHERE project_id = ?`), id); err != nil {
|
|
return false, fmt.Errorf("pruning project components: %w", err)
|
|
}
|
|
return true, tx.Commit()
|
|
}
|
|
|
|
// ListProjects returns all projects ordered by name.
|
|
func (db *DB) ListProjects() ([]Project, error) {
|
|
projects := []Project{}
|
|
if err := db.Select(&projects, projectColumns+` FROM projects pr ORDER BY pr.name`); err != nil {
|
|
return nil, err
|
|
}
|
|
return projects, nil
|
|
}
|
|
|
|
// GetProject returns the named project, or nil if there is none.
|
|
func (db *DB) GetProject(name string) (*Project, error) {
|
|
var p Project
|
|
err := db.Get(&p, db.Rebind(projectColumns+` FROM projects pr WHERE pr.name = ?`), name)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &p, nil
|
|
}
|
|
|
|
// GetProjectComponents returns the components of a project, ordered by
|
|
// ecosystem, name and version, with the package record the proxy holds for
|
|
// each.
|
|
func (db *DB) GetProjectComponents(projectID int64) ([]ProjectComponentInfo, error) {
|
|
components := []ProjectComponentInfo{}
|
|
err := db.Select(&components, db.Rebind(`
|
|
SELECT c.package_purl, c.ecosystem, c.name, c.version,
|
|
p.ecosystem AS cached_ecosystem, p.name AS cached_name, p.latest_version
|
|
FROM project_components c
|
|
LEFT JOIN packages p ON p.purl = c.package_purl
|
|
WHERE c.project_id = ?
|
|
ORDER BY c.ecosystem, c.name, c.version
|
|
`), projectID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return components, nil
|
|
}
|
|
|
|
// GetProjectsUsingPackage returns the projects using a package, or only
|
|
// those using version when it is not empty, ordered by project and version.
|
|
func (db *DB) GetProjectsUsingPackage(packagePURL, version string) ([]ProjectUsage, error) {
|
|
query := `
|
|
SELECT pr.name, pr.ref, pr.branch, pr.uploaded_at, c.version
|
|
FROM project_components c
|
|
JOIN projects pr ON pr.id = c.project_id
|
|
WHERE c.package_purl = ?`
|
|
args := []any{packagePURL}
|
|
if version != "" {
|
|
query += ` AND c.version = ?`
|
|
args = append(args, version)
|
|
}
|
|
usages := []ProjectUsage{}
|
|
if err := db.Select(&usages, db.Rebind(query+` ORDER BY pr.name, c.version`), args...); err != nil {
|
|
return nil, err
|
|
}
|
|
return usages, nil
|
|
}
|
|
|
|
func nullString(s string) sql.NullString {
|
|
return sql.NullString{String: s, Valid: s != ""}
|
|
}
|