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.
153func (d *DB) ListImageTags(ctx context.Context, repoID int64) ([]ImageTag, error) {
154 rows, err := d.QueryContext(ctx,
155 `SELECT image, tag, digest, updated_at FROM registry_tags WHERE repo_id = ? ORDER BY image, tag`, repoID)
156 if err != nil {
157 return nil, err
158 }
159 defer rows.Close()
160 var out []ImageTag
161 for rows.Next() {
162 var t ImageTag
163 if err := rows.Scan(&t.Image, &t.Tag, &t.Digest, &t.UpdatedAt); err != nil {
164 return nil, err
165 }
166 out = append(out, t)
167 }
168 return out, rows.Err()
169}
170
171// ImageSizes sums the linked blob sizes per image path.
172func (d *DB) ImageSizes(ctx context.Context, repoID int64) (map[string]int64, error) {
173 rows, err := d.QueryContext(ctx,
174 `SELECT image, SUM(size) FROM registry_blobs WHERE repo_id = ? GROUP BY image`, repoID)
175 if err != nil {
176 return nil, err
177 }
178 defer rows.Close()
179 out := map[string]int64{}
180 for rows.Next() {
181 var image string
182 var size int64
183 if err := rows.Scan(&image, &size); err != nil {
184 return nil, err
185 }
186 out[image] = size
187 }
188 return out, rows.Err()
189}
190
191// DeleteUntaggedManifest removes the manifest when no tag of the image
192// points at it any more. It reports whether a row went.
193func (d *DB) DeleteUntaggedManifest(ctx context.Context, repoID int64, image, digest string) (bool, error) {
194 res, err := d.ExecContext(ctx,
195 `DELETE FROM registry_manifests WHERE repo_id = ?1 AND image = ?2 AND digest = ?3
196 AND NOT EXISTS (SELECT 1 FROM registry_tags WHERE repo_id = ?1 AND image = ?2 AND digest = ?3)`,
197 repoID, image, digest)
198 if err != nil {
199 return false, err
200 }
201 n, _ := res.RowsAffected()
202 return n > 0, nil
203}
204
205// RepoDigests lists every blob and manifest digest the repo references.
206// image narrows it to one image path; pass nil for the whole repo.
207func (d *DB) RepoDigests(ctx context.Context, repoID int64, image *string) ([]string, error) {
208 // A nil image binds as NULL and matches every row.
209 rows, err := d.QueryContext(ctx,
210 `SELECT digest FROM registry_blobs WHERE repo_id = ?1 AND (?2 IS NULL OR image = ?2)
211 UNION SELECT digest FROM registry_manifests WHERE repo_id = ?1 AND (?2 IS NULL OR image = ?2)`,
212 repoID, image)
213 if err != nil {
214 return nil, err
215 }
216 defer rows.Close()
217 var out []string
218 for rows.Next() {
219 var dg string
220 if err := rows.Scan(&dg); err != nil {
221 return nil, err
222 }
223 out = append(out, dg)
224 }
225 return out, rows.Err()
226}
227
228// DeleteImages removes the index rows of one image path, or of every image
229// when image is nil. Files are the caller's job.
230func (d *DB) DeleteImages(ctx context.Context, repoID int64, image *string) error {
231 for _, table := range []string{"registry_tags", "registry_manifests", "registry_blobs"} {
232 if _, err := d.ExecContext(ctx,
233 `DELETE FROM `+table+` WHERE repo_id = ?1 AND (?2 IS NULL OR image = ?2)`, repoID, image); err != nil {
234 return err
235 }
236 }
237 return nil
238}
239