package db import ( "context" "database/sql" "errors" ) // Manifest is one stored image manifest or index. type Manifest struct { Digest string MediaType string } // LinkBlob records that image (repo_id, image) references digest. func (d *DB) LinkBlob(ctx context.Context, repoID int64, image, digest string, size int64) error { _, err := d.ExecContext(ctx, `INSERT OR IGNORE INTO registry_blobs (repo_id, image, digest, size) VALUES (?, ?, ?, ?)`, repoID, image, digest, size) return err } // BlobLinked reports whether image references digest. func (d *DB) BlobLinked(ctx context.Context, repoID int64, image, digest string) (bool, error) { var n int err := d.QueryRowContext(ctx, `SELECT COUNT(*) FROM registry_blobs WHERE repo_id = ? AND image = ? AND digest = ?`, repoID, image, digest).Scan(&n) return n > 0, err } // UnlinkBlob drops one image's reference to digest. func (d *DB) UnlinkBlob(ctx context.Context, repoID int64, image, digest string) error { _, err := d.ExecContext(ctx, `DELETE FROM registry_blobs WHERE repo_id = ? AND image = ? AND digest = ?`, repoID, image, digest) return err } // DigestReferenced reports whether any image still uses the digest as a // blob or as a manifest. Blobs and manifest bodies share one file store, so // both tables count before a file is removed. func (d *DB) DigestReferenced(ctx context.Context, digest string) (bool, error) { var n int err := d.QueryRowContext(ctx, `SELECT (SELECT COUNT(*) FROM registry_blobs WHERE digest = ?) + (SELECT COUNT(*) FROM registry_manifests WHERE digest = ?)`, digest, digest).Scan(&n) return n > 0, err } // PutManifest stores the manifest row and, for a tag reference, points the // tag at it. func (d *DB) PutManifest(ctx context.Context, repoID int64, image string, m Manifest, tag, now string) error { if _, err := d.ExecContext(ctx, `INSERT OR REPLACE INTO registry_manifests (repo_id, image, digest, media_type, created_at) VALUES (?, ?, ?, ?, ?)`, repoID, image, m.Digest, m.MediaType, now); err != nil { return err } if tag == "" { return nil } _, err := d.ExecContext(ctx, `INSERT OR REPLACE INTO registry_tags (repo_id, image, tag, digest, updated_at) VALUES (?, ?, ?, ?, ?)`, repoID, image, tag, m.Digest, now) return err } // ManifestByDigest returns nil when the image has no such manifest. func (d *DB) ManifestByDigest(ctx context.Context, repoID int64, image, digest string) (*Manifest, error) { m := &Manifest{Digest: digest} err := d.QueryRowContext(ctx, `SELECT media_type FROM registry_manifests WHERE repo_id = ? AND image = ? AND digest = ?`, repoID, image, digest).Scan(&m.MediaType) if errors.Is(err, sql.ErrNoRows) { return nil, nil } return m, err } // ManifestByTag resolves a tag. It returns nil when the tag is unknown. func (d *DB) ManifestByTag(ctx context.Context, repoID int64, image, tag string) (*Manifest, error) { m := &Manifest{} err := d.QueryRowContext(ctx, `SELECT m.digest, m.media_type FROM registry_tags t JOIN registry_manifests m ON m.repo_id = t.repo_id AND m.image = t.image AND m.digest = t.digest WHERE t.repo_id = ? AND t.image = ? AND t.tag = ?`, repoID, image, tag).Scan(&m.Digest, &m.MediaType) if errors.Is(err, sql.ErrNoRows) { return nil, nil } return m, err } // DeleteManifest removes the manifest and every tag pointing at it. It // reports whether the manifest existed. func (d *DB) DeleteManifest(ctx context.Context, repoID int64, image, digest string) (bool, error) { if _, err := d.ExecContext(ctx, `DELETE FROM registry_tags WHERE repo_id = ? AND image = ? AND digest = ?`, repoID, image, digest); err != nil { return false, err } res, err := d.ExecContext(ctx, `DELETE FROM registry_manifests WHERE repo_id = ? AND image = ? AND digest = ?`, repoID, image, digest) if err != nil { return false, err } n, _ := res.RowsAffected() return n > 0, nil } // DeleteTag removes one tag and reports whether it existed. func (d *DB) DeleteTag(ctx context.Context, repoID int64, image, tag string) (bool, error) { res, err := d.ExecContext(ctx, `DELETE FROM registry_tags WHERE repo_id = ? AND image = ? AND tag = ?`, repoID, image, tag) if err != nil { return false, err } n, _ := res.RowsAffected() return n > 0, nil } // ListTags returns the image's tags in lexical order. func (d *DB) ListTags(ctx context.Context, repoID int64, image string) ([]string, error) { rows, err := d.QueryContext(ctx, `SELECT tag FROM registry_tags WHERE repo_id = ? AND image = ? ORDER BY tag`, repoID, image) if err != nil { return nil, err } defer rows.Close() var out []string for rows.Next() { var t string if err := rows.Scan(&t); err != nil { return nil, err } out = append(out, t) } return out, rows.Err() } // ImageTag is one row of the Images tab. type ImageTag struct { Image string Tag string Digest string UpdatedAt string } // ListImageTags returns every tag of the repo, grouped by image path. func (d *DB) ListImageTags(ctx context.Context, repoID int64) ([]ImageTag, error) { rows, err := d.QueryContext(ctx, `SELECT image, tag, digest, updated_at FROM registry_tags WHERE repo_id = ? ORDER BY image, tag`, repoID) if err != nil { return nil, err } defer rows.Close() var out []ImageTag for rows.Next() { var t ImageTag if err := rows.Scan(&t.Image, &t.Tag, &t.Digest, &t.UpdatedAt); err != nil { return nil, err } out = append(out, t) } return out, rows.Err() } // ImageSizes sums the linked blob sizes per image path. func (d *DB) ImageSizes(ctx context.Context, repoID int64) (map[string]int64, error) { rows, err := d.QueryContext(ctx, `SELECT image, SUM(size) FROM registry_blobs WHERE repo_id = ? GROUP BY image`, repoID) if err != nil { return nil, err } defer rows.Close() out := map[string]int64{} for rows.Next() { var image string var size int64 if err := rows.Scan(&image, &size); err != nil { return nil, err } out[image] = size } return out, rows.Err() } // DeleteUntaggedManifest removes the manifest when no tag of the image // points at it any more. It reports whether a row went. func (d *DB) DeleteUntaggedManifest(ctx context.Context, repoID int64, image, digest string) (bool, error) { res, err := d.ExecContext(ctx, `DELETE FROM registry_manifests WHERE repo_id = ?1 AND image = ?2 AND digest = ?3 AND NOT EXISTS (SELECT 1 FROM registry_tags WHERE repo_id = ?1 AND image = ?2 AND digest = ?3)`, repoID, image, digest) if err != nil { return false, err } n, _ := res.RowsAffected() return n > 0, nil } // RepoDigests lists every blob and manifest digest the repo references. // image narrows it to one image path; pass nil for the whole repo. func (d *DB) RepoDigests(ctx context.Context, repoID int64, image *string) ([]string, error) { // A nil image binds as NULL and matches every row. rows, err := d.QueryContext(ctx, `SELECT digest FROM registry_blobs WHERE repo_id = ?1 AND (?2 IS NULL OR image = ?2) UNION SELECT digest FROM registry_manifests WHERE repo_id = ?1 AND (?2 IS NULL OR image = ?2)`, repoID, image) if err != nil { return nil, err } defer rows.Close() var out []string for rows.Next() { var dg string if err := rows.Scan(&dg); err != nil { return nil, err } out = append(out, dg) } return out, rows.Err() } // DeleteImages removes the index rows of one image path, or of every image // when image is nil. Files are the caller's job. func (d *DB) DeleteImages(ctx context.Context, repoID int64, image *string) error { for _, table := range []string{"registry_tags", "registry_manifests", "registry_blobs"} { if _, err := d.ExecContext(ctx, `DELETE FROM `+table+` WHERE repo_id = ?1 AND (?2 IS NULL OR image = ?2)`, repoID, image); err != nil { return err } } return nil }