registry.go
⎇
Raw
1package db
2
3import (
4 "context"
5 "database/sql"
6 "errors"
7)
8
9// Manifest is one stored image manifest or index.
10type Manifest struct {
11 Digest string
12 MediaType string
13}
14
15// LinkBlob records that image (repo_id, image) references digest.
16func (d *DB) LinkBlob(ctx context.Context, repoID int64, image, digest string, size int64) error {
17 _, err := d.ExecContext(ctx,
18 `INSERT OR IGNORE INTO registry_blobs (repo_id, image, digest, size) VALUES (?, ?, ?, ?)`,
19 repoID, image, digest, size)
20 return err
21}
22
23// BlobLinked reports whether image references digest.
24func (d *DB) BlobLinked(ctx context.Context, repoID int64, image, digest string) (bool, error) {
25 var n int
26 err := d.QueryRowContext(ctx,
27 `SELECT COUNT(*) FROM registry_blobs WHERE repo_id = ? AND image = ? AND digest = ?`,
28 repoID, image, digest).Scan(&n)
29 return n > 0, err
30}
31
32// UnlinkBlob drops one image's reference to digest.
33func (d *DB) UnlinkBlob(ctx context.Context, repoID int64, image, digest string) error {
34 _, err := d.ExecContext(ctx,
35 `DELETE FROM registry_blobs WHERE repo_id = ? AND image = ? AND digest = ?`,
36 repoID, image, digest)
37 return err
38}
39
40// DigestReferenced reports whether any image still uses the digest as a
41// blob or as a manifest. Blobs and manifest bodies share one file store, so
42// both tables count before a file is removed.
43func (d *DB) DigestReferenced(ctx context.Context, digest string) (bool, error) {
44 var n int
45 err := d.QueryRowContext(ctx,
46 `SELECT (SELECT COUNT(*) FROM registry_blobs WHERE digest = ?) +
47 (SELECT COUNT(*) FROM registry_manifests WHERE digest = ?)`,
48 digest, digest).Scan(&n)
49 return n > 0, err
50}
51
52// PutManifest stores the manifest row and, for a tag reference, points the
53// tag at it.
54func (d *DB) PutManifest(ctx context.Context, repoID int64, image string, m Manifest, tag, now string) error {
55 if _, err := d.ExecContext(ctx,
56 `INSERT OR REPLACE INTO registry_manifests (repo_id, image, digest, media_type, created_at)
57 VALUES (?, ?, ?, ?, ?)`,
58 repoID, image, m.Digest, m.MediaType, now); err != nil {
59 return err
60 }
61 if tag == "" {
62 return nil
63 }
64 _, err := d.ExecContext(ctx,
65 `INSERT OR REPLACE INTO registry_tags (repo_id, image, tag, digest, updated_at) VALUES (?, ?, ?, ?, ?)`,
66 repoID, image, tag, m.Digest, now)
67 return err
68}
69
70// ManifestByDigest returns nil when the image has no such manifest.
71func (d *DB) ManifestByDigest(ctx context.Context, repoID int64, image, digest string) (*Manifest, error) {
72 m := &Manifest{Digest: digest}
73 err := d.QueryRowContext(ctx,
74 `SELECT media_type FROM registry_manifests WHERE repo_id = ? AND image = ? AND digest = ?`,
75 repoID, image, digest).Scan(&m.MediaType)
76 if errors.Is(err, sql.ErrNoRows) {
77 return nil, nil
78 }
79 return m, err
80}
81
82// ManifestByTag resolves a tag. It returns nil when the tag is unknown.
83func (d *DB) ManifestByTag(ctx context.Context, repoID int64, image, tag string) (*Manifest, error) {
84 m := &Manifest{}
85 err := d.QueryRowContext(ctx,
86 `SELECT m.digest, m.media_type FROM registry_tags t
87 JOIN registry_manifests m ON m.repo_id = t.repo_id AND m.image = t.image AND m.digest = t.digest
88 WHERE t.repo_id = ? AND t.image = ? AND t.tag = ?`,
89 repoID, image, tag).Scan(&m.Digest, &m.MediaType)
90 if errors.Is(err, sql.ErrNoRows) {
91 return nil, nil
92 }
93 return m, err
94}
95
96// DeleteManifest removes the manifest and every tag pointing at it. It
97// reports whether the manifest existed.
98func (d *DB) DeleteManifest(ctx context.Context, repoID int64, image, digest string) (bool, error) {
99 if _, err := d.ExecContext(ctx,
100 `DELETE FROM registry_tags WHERE repo_id = ? AND image = ? AND digest = ?`,
101 repoID, image, digest); err != nil {
102 return false, err
103 }
104 res, err := d.ExecContext(ctx,
105 `DELETE FROM registry_manifests WHERE repo_id = ? AND image = ? AND digest = ?`,
106 repoID, image, digest)
107 if err != nil {
108 return false, err
109 }
110 n, _ := res.RowsAffected()
111 return n > 0, nil
112}
113
114// DeleteTag removes one tag and reports whether it existed.
115func (d *DB) DeleteTag(ctx context.Context, repoID int64, image, tag string) (bool, error) {
116 res, err := d.ExecContext(ctx,
117 `DELETE FROM registry_tags WHERE repo_id = ? AND image = ? AND tag = ?`, repoID, image, tag)
118 if err != nil {
119 return false, err
120 }
121 n, _ := res.RowsAffected()
122 return n > 0, nil
123}
124
125// ListTags returns the image's tags in lexical order.
126func (d *DB) ListTags(ctx context.Context, repoID int64, image string) ([]string, error) {
127 rows, err := d.QueryContext(ctx,
128 `SELECT tag FROM registry_tags WHERE repo_id = ? AND image = ? ORDER BY tag`, repoID, image)
129 if err != nil {
130 return nil, err
131 }
132 defer rows.Close()
133 var out []string
134 for rows.Next() {
135 var t string
136 if err := rows.Scan(&t); err != nil {
137 return nil, err
138 }
139 out = append(out, t)
140 }
141 return out, rows.Err()
142}
143
144// ImageTag is one row of the Images tab.
145type ImageTag struct {
146 Image string
147 Tag string
148 Digest string
149 UpdatedAt string
150}
151
152// ListImageTags returns every tag of the repo, grouped by image path, the
153// most recently pushed tag first.
154func (d *DB) ListImageTags(ctx context.Context, repoID int64) ([]ImageTag, error) {
155 rows, err := d.QueryContext(ctx,
156 `SELECT image, tag, digest, updated_at FROM registry_tags WHERE repo_id = ?
157 ORDER BY image, updated_at DESC, tag`, repoID)
158 if err != nil {
159 return nil, err
160 }
161 defer rows.Close()
162 var out []ImageTag
163 for rows.Next() {
164 var t ImageTag
165 if err := rows.Scan(&t.Image, &t.Tag, &t.Digest, &t.UpdatedAt); err != nil {
166 return nil, err
167 }
168 out = append(out, t)
169 }
170 return out, rows.Err()
171}
172
173// ImageSizes sums the linked blob sizes per image path.
174func (d *DB) ImageSizes(ctx context.Context, repoID int64) (map[string]int64, error) {
175 rows, err := d.QueryContext(ctx,
176 `SELECT image, SUM(size) FROM registry_blobs WHERE repo_id = ? GROUP BY image`, repoID)
177 if err != nil {
178 return nil, err
179 }
180 defer rows.Close()
181 out := map[string]int64{}
182 for rows.Next() {
183 var image string
184 var size int64
185 if err := rows.Scan(&image, &size); err != nil {
186 return nil, err
187 }
188 out[image] = size
189 }
190 return out, rows.Err()
191}
192
193// DeleteUntaggedManifest removes the manifest when no tag of the image
194// points at it any more. It reports whether a row went.
195func (d *DB) DeleteUntaggedManifest(ctx context.Context, repoID int64, image, digest string) (bool, error) {
196 res, err := d.ExecContext(ctx,
197 `DELETE FROM registry_manifests WHERE repo_id = ?1 AND image = ?2 AND digest = ?3
198 AND NOT EXISTS (SELECT 1 FROM registry_tags WHERE repo_id = ?1 AND image = ?2 AND digest = ?3)`,
199 repoID, image, digest)
200 if err != nil {
201 return false, err
202 }
203 n, _ := res.RowsAffected()
204 return n > 0, nil
205}
206
207// RepoDigests lists every blob and manifest digest the repo references.
208// image narrows it to one image path; pass nil for the whole repo.
209func (d *DB) RepoDigests(ctx context.Context, repoID int64, image *string) ([]string, error) {
210 // A nil image binds as NULL and matches every row.
211 rows, err := d.QueryContext(ctx,
212 `SELECT digest FROM registry_blobs WHERE repo_id = ?1 AND (?2 IS NULL OR image = ?2)
213 UNION SELECT digest FROM registry_manifests WHERE repo_id = ?1 AND (?2 IS NULL OR image = ?2)`,
214 repoID, image)
215 if err != nil {
216 return nil, err
217 }
218 defer rows.Close()
219 var out []string
220 for rows.Next() {
221 var dg string
222 if err := rows.Scan(&dg); err != nil {
223 return nil, err
224 }
225 out = append(out, dg)
226 }
227 return out, rows.Err()
228}
229
230// DeleteImages removes the index rows of one image path, or of every image
231// when image is nil. Files are the caller's job.
232func (d *DB) DeleteImages(ctx context.Context, repoID int64, image *string) error {
233 for _, table := range []string{"registry_tags", "registry_manifests", "registry_blobs"} {
234 if _, err := d.ExecContext(ctx,
235 `DELETE FROM `+table+` WHERE repo_id = ?1 AND (?2 IS NULL OR image = ?2)`, repoID, image); err != nil {
236 return err
237 }
238 }
239 return nil
240}
241