@@ -36,6 +36,7 @@ import (
36
36
"oras.land/oras-go/v2/internal/descriptor"
37
37
"oras.land/oras-go/v2/internal/graph"
38
38
"oras.land/oras-go/v2/internal/resolver"
39
+ "oras.land/oras-go/v2/registry"
39
40
)
40
41
41
42
// Store implements `oras.Target`, and represents a content store
@@ -52,12 +53,26 @@ type Store struct {
52
53
// to manually call SaveIndex() when needed.
53
54
// - Default value: true.
54
55
AutoSaveIndex bool
55
- root string
56
- indexPath string
57
- index * ocispec.Index
58
- storage * Storage
59
- tagResolver * resolver.Memory
60
- graph * graph.Memory
56
+
57
+ // AutoGC controls if the OCI store will automatically clean newly produced
58
+ // dangling (unreferenced) blobs during Delete() operation. For example the
59
+ // blobs whose manifests have been deleted. Manifests in index.json will not
60
+ // be deleted.
61
+ // - Default value: true.
62
+ AutoGC bool
63
+
64
+ // AutoDeleteReferrers controls if the OCI store will automatically delete its
65
+ // referrers when a manifest is deleted. When set to true, the referrers will
66
+ // be deleted even if they exist in index.json.
67
+ // - Default value: true.
68
+ AutoDeleteReferrers bool
69
+
70
+ root string
71
+ indexPath string
72
+ index * ocispec.Index
73
+ storage * Storage
74
+ tagResolver * resolver.Memory
75
+ graph * graph.Memory
61
76
62
77
// sync ensures that most operations can be done concurrently, while Delete
63
78
// has the exclusive access to Store if a delete operation is underway. Operations
@@ -84,12 +99,14 @@ func NewWithContext(ctx context.Context, root string) (*Store, error) {
84
99
}
85
100
86
101
store := & Store {
87
- AutoSaveIndex : true ,
88
- root : rootAbs ,
89
- indexPath : filepath .Join (rootAbs , ocispec .ImageIndexFile ),
90
- storage : storage ,
91
- tagResolver : resolver .NewMemory (),
92
- graph : graph .NewMemory (),
102
+ AutoSaveIndex : true ,
103
+ AutoGC : true ,
104
+ AutoDeleteReferrers : true ,
105
+ root : rootAbs ,
106
+ indexPath : filepath .Join (rootAbs , ocispec .ImageIndexFile ),
107
+ storage : storage ,
108
+ tagResolver : resolver .NewMemory (),
109
+ graph : graph .NewMemory (),
93
110
}
94
111
95
112
if err := ensureDir (filepath .Join (rootAbs , ocispec .ImageBlobsDir )); err != nil {
@@ -143,11 +160,49 @@ func (s *Store) Exists(ctx context.Context, target ocispec.Descriptor) (bool, er
143
160
144
161
// Delete deletes the content matching the descriptor from the store. Delete may
145
162
// fail on certain systems (i.e. NTFS), if there is a process (i.e. an unclosed
146
- // Reader) using target.
163
+ // Reader) using target. If s.AutoGC is set to true, Delete will recursively
164
+ // remove the dangling blobs caused by the current delete. If s.AutoDeleteReferrers
165
+ // is set to true, Delete will recursively remove the referrers of the manifests
166
+ // being deleted.
147
167
func (s * Store ) Delete (ctx context.Context , target ocispec.Descriptor ) error {
148
168
s .sync .Lock ()
149
169
defer s .sync .Unlock ()
150
170
171
+ deleteQueue := []ocispec.Descriptor {target }
172
+ for len (deleteQueue ) > 0 {
173
+ head := deleteQueue [0 ]
174
+ deleteQueue = deleteQueue [1 :]
175
+
176
+ // get referrers if applicable
177
+ if s .AutoDeleteReferrers && descriptor .IsManifest (head ) {
178
+ referrers , err := registry .Referrers (ctx , & unsafeStore {s }, head , "" )
179
+ if err != nil {
180
+ return err
181
+ }
182
+ deleteQueue = append (deleteQueue , referrers ... )
183
+ }
184
+
185
+ // delete the head of queue
186
+ danglings , err := s .delete (ctx , head )
187
+ if err != nil {
188
+ return err
189
+ }
190
+ if s .AutoGC {
191
+ for _ , d := range danglings {
192
+ // do not delete existing manifests in tagResolver
193
+ _ , err = s .tagResolver .Resolve (ctx , string (d .Digest ))
194
+ if errors .Is (err , errdef .ErrNotFound ) {
195
+ deleteQueue = append (deleteQueue , d )
196
+ }
197
+ }
198
+ }
199
+ }
200
+
201
+ return nil
202
+ }
203
+
204
+ // delete deletes one node and returns the dangling nodes caused by the delete.
205
+ func (s * Store ) delete (ctx context.Context , target ocispec.Descriptor ) ([]ocispec.Descriptor , error ) {
151
206
resolvers := s .tagResolver .Map ()
152
207
untagged := false
153
208
for reference , desc := range resolvers {
@@ -156,16 +211,17 @@ func (s *Store) Delete(ctx context.Context, target ocispec.Descriptor) error {
156
211
untagged = true
157
212
}
158
213
}
159
- if err := s .graph .Remove (ctx , target ); err != nil {
160
- return err
161
- }
214
+ danglings := s .graph .Remove (target )
162
215
if untagged && s .AutoSaveIndex {
163
216
err := s .saveIndex ()
164
217
if err != nil {
165
- return err
218
+ return nil , err
166
219
}
167
220
}
168
- return s .storage .Delete (ctx , target )
221
+ if err := s .storage .Delete (ctx , target ); err != nil {
222
+ return nil , err
223
+ }
224
+ return danglings , nil
169
225
}
170
226
171
227
// Tag tags a descriptor with a reference string.
@@ -398,6 +454,19 @@ func (s *Store) writeIndexFile() error {
398
454
return os .WriteFile (s .indexPath , indexJSON , 0666 )
399
455
}
400
456
457
+ // unsafeStore is used to bypass lock restrictions in Delete.
458
+ type unsafeStore struct {
459
+ * Store
460
+ }
461
+
462
+ func (s * unsafeStore ) Fetch (ctx context.Context , target ocispec.Descriptor ) (io.ReadCloser , error ) {
463
+ return s .storage .Fetch (ctx , target )
464
+ }
465
+
466
+ func (s * unsafeStore ) Predecessors (ctx context.Context , node ocispec.Descriptor ) ([]ocispec.Descriptor , error ) {
467
+ return s .graph .Predecessors (ctx , node )
468
+ }
469
+
401
470
// validateReference validates ref.
402
471
func validateReference (ref string ) error {
403
472
if ref == "" {
0 commit comments