3333 ErrEmptyArtifactName = errors .New ("artifact name cannot be empty" )
3434)
3535
36+ const ManifestSchemaVersion = 2
37+
3638type ArtifactStore struct {
3739 SystemContext * types.SystemContext
3840 storePath string
@@ -164,23 +166,70 @@ func (as ArtifactStore) Push(ctx context.Context, src, dest string, opts libimag
164166// Add takes one or more local files and adds them to the local artifact store. The empty
165167// string input is for possible custom artifact types.
166168func (as ArtifactStore ) Add (ctx context.Context , dest string , paths []string , options * libartTypes.AddOptions ) (* digest.Digest , error ) {
167- annots := maps .Clone (options .Annotations )
168169 if len (dest ) == 0 {
169170 return nil , ErrEmptyArtifactName
170171 }
171172
172- artifactManifestLayers := make ([]specV1.Descriptor , 0 )
173+ if options .Append && len (options .ArtifactType ) > 0 {
174+ return nil , errors .New ("append option is not compatible with ArtifactType option" )
175+ }
176+
177+ // currently we don't allow override of the filename ; if a user requirement emerges,
178+ // we could seemingly accommodate but broadens possibilities of something bad happening
179+ // for things like `artifact extract`
180+ if _ , hasTitle := options .Annotations [specV1 .AnnotationTitle ]; hasTitle {
181+ return nil , fmt .Errorf ("cannot override filename with %s annotation" , specV1 .AnnotationTitle )
182+ }
173183
174184 // Check if artifact already exists
175185 artifacts , err := as .getArtifacts (ctx , nil )
176186 if err != nil {
177187 return nil , err
178188 }
179189
180- // Check if artifact exists; in GetByName not getting an
181- // error means it exists
182- if _ , _ , err := artifacts .GetByNameOrDigest (dest ); err == nil {
183- return nil , fmt .Errorf ("artifact %s already exists" , dest )
190+ var artifactManifest specV1.Manifest
191+ var oldDigest * digest.Digest
192+ fileNames := map [string ]struct {}{}
193+
194+ if ! options .Append {
195+ // Check if artifact exists; in GetByName not getting an
196+ // error means it exists
197+ _ , _ , err := artifacts .GetByNameOrDigest (dest )
198+ if err == nil {
199+ return nil , fmt .Errorf ("artifact %s already exists" , dest )
200+ }
201+ artifactManifest = specV1.Manifest {
202+ Versioned : specs.Versioned {SchemaVersion : ManifestSchemaVersion },
203+ MediaType : specV1 .MediaTypeImageManifest ,
204+ ArtifactType : options .ArtifactType ,
205+ // TODO This should probably be configurable once the CLI is capable
206+ Config : specV1 .DescriptorEmptyJSON ,
207+ Layers : make ([]specV1.Descriptor , 0 ),
208+ }
209+ } else {
210+ artifact , _ , err := artifacts .GetByNameOrDigest (dest )
211+ if err != nil {
212+ return nil , err
213+ }
214+ artifactManifest = artifact .Manifest .Manifest
215+ oldDigest , err = artifact .GetDigest ()
216+ if err != nil {
217+ return nil , err
218+ }
219+
220+ for _ , layer := range artifactManifest .Layers {
221+ if value , ok := layer .Annotations [specV1 .AnnotationTitle ]; ok && value != "" {
222+ fileNames [value ] = struct {}{}
223+ }
224+ }
225+ }
226+
227+ for _ , path := range paths {
228+ fileName := filepath .Base (path )
229+ if _ , ok := fileNames [fileName ]; ok {
230+ return nil , fmt .Errorf ("file: %q already exists in artifact" , fileName )
231+ }
232+ fileNames [fileName ] = struct {}{}
184233 }
185234
186235 ir , err := layout .NewReference (as .storePath , dest )
@@ -194,14 +243,9 @@ func (as ArtifactStore) Add(ctx context.Context, dest string, paths []string, op
194243 }
195244 defer imageDest .Close ()
196245
246+ // ImageDestination, in general, requires the caller to write a full image; here we may write only the added layers.
247+ // This works for the oci/layout transport we hard-code.
197248 for _ , path := range paths {
198- // currently we don't allow override of the filename ; if a user requirement emerges,
199- // we could seemingly accommodate but broadens possibilities of something bad happening
200- // for things like `artifact extract`
201- if _ , hasTitle := options .Annotations [specV1 .AnnotationTitle ]; hasTitle {
202- return nil , fmt .Errorf ("cannot override filename with %s annotation" , specV1 .AnnotationTitle )
203- }
204-
205249 // get the new artifact into the local store
206250 newBlobDigest , newBlobSize , err := layout .PutBlobFromLocalFile (ctx , imageDest , path )
207251 if err != nil {
@@ -212,35 +256,25 @@ func (as ArtifactStore) Add(ctx context.Context, dest string, paths []string, op
212256 return nil , err
213257 }
214258
215- annots [ specV1 . AnnotationTitle ] = filepath . Base ( path )
216-
259+ annotations := maps . Clone ( options . Annotations )
260+ annotations [ specV1 . AnnotationTitle ] = filepath . Base ( path )
217261 newLayer := specV1.Descriptor {
218262 MediaType : detectedType ,
219263 Digest : newBlobDigest ,
220264 Size : newBlobSize ,
221- Annotations : annots ,
265+ Annotations : annotations ,
222266 }
223-
224- artifactManifestLayers = append (artifactManifestLayers , newLayer )
225- }
226-
227- artifactManifest := specV1.Manifest {
228- Versioned : specs.Versioned {SchemaVersion : 2 },
229- MediaType : specV1 .MediaTypeImageManifest ,
230- // TODO This should probably be configurable once the CLI is capable
231- Config : specV1 .DescriptorEmptyJSON ,
232- Layers : artifactManifestLayers ,
267+ artifactManifest .Layers = append (artifactManifest .Layers , newLayer )
233268 }
234269
235- artifactManifest .ArtifactType = options .ArtifactType
236-
237270 rawData , err := json .Marshal (artifactManifest )
238271 if err != nil {
239272 return nil , err
240273 }
241274 if err := imageDest .PutManifest (ctx , rawData , nil ); err != nil {
242275 return nil , err
243276 }
277+
244278 unparsed := newUnparsedArtifactImage (ir , artifactManifest )
245279 if err := imageDest .Commit (ctx , unparsed ); err != nil {
246280 return nil , err
@@ -253,6 +287,27 @@ func (as ArtifactStore) Add(ctx context.Context, dest string, paths []string, op
253287 if err := createEmptyStanza (filepath .Join (as .storePath , specV1 .ImageBlobsDir , artifactManifestDigest .Algorithm ().String (), artifactManifest .Config .Digest .Encoded ())); err != nil {
254288 logrus .Errorf ("failed to check or write empty stanza file: %v" , err )
255289 }
290+
291+ // Clean up after append. Remove previous artifact from store.
292+ if oldDigest != nil {
293+ lrs , err := layout .List (as .storePath )
294+ if err != nil {
295+ return nil , err
296+ }
297+
298+ for _ , l := range lrs {
299+ if oldDigest .String () == l .ManifestDescriptor .Digest .String () {
300+ if _ , ok := l .ManifestDescriptor .Annotations [specV1 .AnnotationRefName ]; ok {
301+ continue
302+ }
303+
304+ if err := l .Reference .DeleteImage (ctx , as .SystemContext ); err != nil {
305+ return nil , err
306+ }
307+ break
308+ }
309+ }
310+ }
256311 return & artifactManifestDigest , nil
257312}
258313
@@ -431,7 +486,7 @@ func (as ArtifactStore) readIndex() (*specV1.Index, error) { //nolint:unused
431486func (as ArtifactStore ) createEmptyManifest () error {
432487 index := specV1.Index {
433488 MediaType : specV1 .MediaTypeImageIndex ,
434- Versioned : specs.Versioned {SchemaVersion : 2 },
489+ Versioned : specs.Versioned {SchemaVersion : ManifestSchemaVersion },
435490 }
436491 rawData , err := json .Marshal (& index )
437492 if err != nil {
0 commit comments