Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/BurntSushi/toml v1.6.0
github.com/CycloneDX/cyclonedx-go v0.11.0
github.com/git-pkgs/archives v0.5.1
github.com/git-pkgs/artifacts v0.1.0
github.com/git-pkgs/cooldown v0.1.1
github.com/git-pkgs/enrichment v0.6.5
github.com/git-pkgs/magic v0.2.0
Expand All @@ -17,6 +18,7 @@ require (
github.com/go-chi/chi/v5 v5.3.1
github.com/jmoiron/sqlx v1.4.0
github.com/lib/pq v1.12.3
github.com/opencontainers/go-digest v1.0.0
github.com/prometheus/client_golang v1.24.1
github.com/prometheus/client_model v0.6.2
github.com/spdx/tools-golang v0.5.7
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ github.com/ghostiam/protogetter v0.3.20 h1:oW7OPFit2FxZOpmMRPP9FffU4uUpfeE/rEdE1
github.com/ghostiam/protogetter v0.3.20/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI=
github.com/git-pkgs/archives v0.5.1 h1:qwu/vsoerQZF1iysRtfcxpy1KIUSJJSpXJ5JNxzNoQw=
github.com/git-pkgs/archives v0.5.1/go.mod h1:AKpkxnts49R9uAt1mL2ULYcHrmYujCDVu24IsFvW9so=
github.com/git-pkgs/artifacts v0.1.0 h1:es73XxrsJefxO/DqtergTt+IaL+xPhzE0gSVrb6AFwk=
github.com/git-pkgs/artifacts v0.1.0/go.mod h1:zWZ0mrFi2M6ajGwpbo1nBGQTSb7qptP+zNG/EwAemBE=
github.com/git-pkgs/cooldown v0.1.1 h1:9OqqzCB8gANz/y44SmqGD0Jp8Qtu81D1sCbKl6Ehg7w=
github.com/git-pkgs/cooldown v0.1.1/go.mod h1:v7APuK/UouTiu8mWQZbdDmj7DfxxkGUeuhjaRB5gv9E=
github.com/git-pkgs/enrichment v0.6.5 h1:U0SPzWVGoK4R8TwojCTASBRTEV+QSs0IitdLmzI/g/k=
Expand Down Expand Up @@ -520,6 +522,8 @@ github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI
github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw=
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
Expand Down
102 changes: 94 additions & 8 deletions internal/database/database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import (
"database/sql"
"os"
"path/filepath"
"strings"
"testing"
"time"
)

const testContentHash = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

func TestCreateAndOpen(t *testing.T) {
dir := t.TempDir()
dbPath := filepath.Join(dir, "test.db")
Expand Down Expand Up @@ -200,7 +203,7 @@ func TestArtifactCRUD(t *testing.T) {
t.Error("expected artifact to not be cached yet")
}

err = db.MarkArtifactCached(versionPURL, "lodash-4.17.21.tgz", "/cache/npm/lodash-4.17.21.tgz", "sha256-abc", 12345, "application/gzip")
err = db.MarkArtifactCached(versionPURL, "lodash-4.17.21.tgz", "/cache/npm/lodash-4.17.21.tgz", testContentHash, 12345, "application/gzip")
if err != nil {
t.Fatalf("MarkArtifactCached failed: %v", err)
}
Expand Down Expand Up @@ -257,7 +260,7 @@ func TestGetCachedArtifact(t *testing.T) {
}

if err := db.MarkArtifactCached(versionPURL, filename, "/cache/npm/"+filename,
"sha256-abc", 12345, "application/gzip"); err != nil {
testContentHash, 12345, "application/gzip"); err != nil {
t.Fatalf("MarkArtifactCached failed: %v", err)
}

Expand All @@ -274,14 +277,20 @@ func TestGetCachedArtifact(t *testing.T) {
if cached.StoragePath != "/cache/npm/"+filename {
t.Errorf("expected cached storage path, got %q", cached.StoragePath)
}
if cached.ContentHash.String != "sha256-abc" {
t.Errorf("expected cached content hash, got %q", cached.ContentHash.String)
if cached.Artifact.PURL != versionPURL {
t.Errorf("expected cached PURL %q, got %q", versionPURL, cached.Artifact.PURL)
}
if cached.Artifact.Digest.String() != "sha256:"+testContentHash {
t.Errorf("expected cached digest, got %q", cached.Artifact.Digest)
}
if cached.Size.Int64 != 12345 {
t.Errorf("expected cached size 12345, got %d", cached.Size.Int64)
if cached.Artifact.Size != 12345 {
t.Errorf("expected cached size 12345, got %d", cached.Artifact.Size)
}
if cached.ContentType.String != "application/gzip" {
t.Errorf("expected cached content type, got %q", cached.ContentType.String)
if cached.Artifact.Filename != filename {
t.Errorf("expected cached filename %q, got %q", filename, cached.Artifact.Filename)
}
if cached.Artifact.MediaType != "application/gzip" {
t.Errorf("expected cached content type, got %q", cached.Artifact.MediaType)
}
if cached.Integrity.String != "sha512-abc123" {
t.Errorf("expected cached integrity, got %q", cached.Integrity.String)
Expand All @@ -297,6 +306,83 @@ func TestGetCachedArtifact(t *testing.T) {
})
}

func TestCachedArtifactRowArtifact(t *testing.T) {
validRow := cachedArtifactRow{
Ecosystem: "npm",
StoragePath: "npm/example/1.0.0/example.tgz",
ContentHash: sql.NullString{String: testContentHash, Valid: true},
Size: sql.NullInt64{Int64: 0, Valid: true},
}
tests := []struct {
name string
row cachedArtifactRow
versionPURL string
wantErr string
}{
{name: "zero byte", row: validRow, versionPURL: "pkg:npm/example@1.0.0"},
{
name: "missing hash",
row: cachedArtifactRow{Size: sql.NullInt64{Int64: 1, Valid: true}},
versionPURL: "pkg:npm/example@1.0.0",
wantErr: "content hash is missing",
},
{
name: "malformed hash",
row: cachedArtifactRow{
ContentHash: sql.NullString{String: "not-a-hash", Valid: true},
Size: sql.NullInt64{Int64: 1, Valid: true},
},
versionPURL: "pkg:npm/example@1.0.0",
wantErr: "digest",
},
{
name: "missing size",
row: cachedArtifactRow{
ContentHash: sql.NullString{String: testContentHash, Valid: true},
},
versionPURL: "pkg:npm/example@1.0.0",
wantErr: "size is missing",
},
{
name: "negative size",
row: cachedArtifactRow{
ContentHash: sql.NullString{String: testContentHash, Valid: true},
Size: sql.NullInt64{Int64: -1, Valid: true},
},
versionPURL: "pkg:npm/example@1.0.0",
wantErr: "size",
},
{name: "malformed PURL", row: validRow, versionPURL: "not-a-purl", wantErr: "PURL"},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cached, err := test.row.artifact(test.versionPURL, "example.tgz")
if test.wantErr != "" {
if err == nil {
t.Fatal("artifact() error = nil")
}
if !strings.Contains(err.Error(), test.wantErr) {
t.Errorf("error = %q, want %q", err, test.wantErr)
}
if !strings.Contains(err.Error(), test.versionPURL) {
t.Errorf("error = %q, want version PURL %q", err, test.versionPURL)
}
if !strings.Contains(err.Error(), "example.tgz") {
t.Errorf("error = %q, want filename", err)
}
return
}
if err != nil {
t.Fatalf("artifact() error = %v", err)
}
if err := cached.Artifact.Validate(); err != nil {
t.Errorf("Artifact.Validate() error = %v", err)
}
})
}
}

func seedCachedArtifactTestData(t *testing.T, db *DB, packagePURL, versionPURL, filename string) {
t.Helper()

Expand Down
49 changes: 46 additions & 3 deletions internal/database/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import (
"database/sql"
"fmt"
"time"

"github.com/git-pkgs/artifacts"
"github.com/opencontainers/go-digest"
)

// Package queries
Expand Down Expand Up @@ -193,7 +196,7 @@ func (db *DB) GetArtifact(versionPURL, filename string) (*Artifact, error) {

// GetCachedArtifact returns the fields needed to serve a cached artifact.
func (db *DB) GetCachedArtifact(packagePURL, versionPURL, filename string) (*CachedArtifact, error) {
var artifact CachedArtifact
var row cachedArtifactRow
query := db.Rebind(`
SELECT packages.ecosystem, artifacts.storage_path, artifacts.content_hash, artifacts.size,
artifacts.content_type, versions.integrity
Expand All @@ -203,14 +206,54 @@ func (db *DB) GetCachedArtifact(packagePURL, versionPURL, filename string) (*Cac
WHERE packages.purl = ? AND artifacts.version_purl = ? AND artifacts.filename = ?
AND artifacts.storage_path IS NOT NULL AND artifacts.fetched_at IS NOT NULL
`)
err := db.Get(&artifact, query, packagePURL, versionPURL, filename)
err := db.Get(&row, query, packagePURL, versionPURL, filename)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return &artifact, nil
return row.artifact(versionPURL, filename)
}

type cachedArtifactRow struct {
Ecosystem string `db:"ecosystem"`
StoragePath string `db:"storage_path"`
ContentHash sql.NullString `db:"content_hash"`
Size sql.NullInt64 `db:"size"`
ContentType sql.NullString `db:"content_type"`
Integrity sql.NullString `db:"integrity"`
}

func (row cachedArtifactRow) artifact(versionPURL, filename string) (*CachedArtifact, error) {
if !row.ContentHash.Valid || row.ContentHash.String == "" {
return nil, fmt.Errorf("cached artifact for %q, filename %q: content hash is missing", versionPURL, filename)
}
if !row.Size.Valid {
return nil, fmt.Errorf("cached artifact for %q, filename %q: size is missing", versionPURL, filename)
}

mediaType := ""
if row.ContentType.Valid {
mediaType = row.ContentType.String
}
sharedArtifact, err := artifacts.New(
versionPURL,
digest.Digest("sha256:"+row.ContentHash.String),
row.Size.Int64,
filename,
mediaType,
)
if err != nil {
return nil, fmt.Errorf("invalid cached artifact for %q, filename %q: %w", versionPURL, filename, err)
}

return &CachedArtifact{
Ecosystem: row.Ecosystem,
StoragePath: row.StoragePath,
Artifact: sharedArtifact,
Integrity: row.Integrity,
}, nil
}

func (db *DB) GetArtifactByPath(storagePath string) (*Artifact, error) {
Expand Down
12 changes: 6 additions & 6 deletions internal/database/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"net/url"
"strings"
"time"

"github.com/git-pkgs/artifacts"
)

// Package represents a package in the database.
Expand Down Expand Up @@ -148,12 +150,10 @@ func (a *Artifact) IsCached() bool {

// CachedArtifact contains the fields needed to serve a cached artifact.
type CachedArtifact struct {
Ecosystem string `db:"ecosystem"`
StoragePath string `db:"storage_path"`
ContentHash sql.NullString `db:"content_hash"`
Size sql.NullInt64 `db:"size"`
ContentType sql.NullString `db:"content_type"`
Integrity sql.NullString `db:"integrity"`
Ecosystem string
StoragePath string
Artifact artifacts.Artifact
Integrity sql.NullString
}

// MetadataCacheEntry represents a cached metadata blob for offline serving.
Expand Down
5 changes: 3 additions & 2 deletions internal/handler/container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -436,8 +436,9 @@ func TestContainerHandler_BlobHead_DirectServeRedirects(t *testing.T) {
if got := w.Header().Get("Location"); got != store.signedURL {
t.Errorf("Location = %q, want %q", got, store.signedURL)
}
if got := w.Header().Get("ETag"); got != `"abc123"` {
t.Errorf("ETag = %q, want %q", got, `"abc123"`)
wantETag := `"` + testArtifact("cached blob", "", "", "").Digest.Encoded() + `"`
if got := w.Header().Get("ETag"); got != wantETag {
t.Errorf("ETag = %q, want %q", got, wantETag)
}
if w.Body.Len() != 0 {
t.Errorf("HEAD response body length = %d, want 0", w.Body.Len())
Expand Down
3 changes: 2 additions & 1 deletion internal/handler/download_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,14 @@ func seedPackageWithPURL(t *testing.T, db *database.DB, store *mockStorage, ecos

storagePath := storage.ArtifactPath(ecosystem, "", name, version, filename)
store.files[storagePath] = []byte(content)
sharedArtifact := testArtifact(content, versionPURL, filename, "application/octet-stream")

art := &database.Artifact{
VersionPURL: versionPURL,
Filename: filename,
UpstreamURL: "https://example.com/" + filename,
StoragePath: sql.NullString{String: storagePath, Valid: true},
ContentHash: sql.NullString{String: "abc123", Valid: true},
ContentHash: sql.NullString{String: sharedArtifact.Digest.Encoded(), Valid: true},
Size: sql.NullInt64{Int64: int64(len(content)), Valid: true},
ContentType: sql.NullString{String: "application/octet-stream", Valid: true},
FetchedAt: sql.NullTime{Time: time.Now(), Valid: true},
Expand Down
Loading
Loading