@@ -36,6 +36,7 @@ import (
3636 "strings"
3737
3838 "gocloud.dev/blob"
39+ "gocloud.dev/gcerrors"
3940 "golang.org/x/crypto/openpgp/clearsign"
4041 "zombiezen.com/go/aptblob/internal/deb"
4142)
@@ -135,7 +136,9 @@ func uploadIndex(ctx context.Context, bucket *blob.Bucket, key string, packages
135136 if err := deb .Save (buf , packages ); err != nil {
136137 return indexHashes {}, indexHashes {}, err
137138 }
138- uncompressed , err = upload (ctx , bucket , key , "text/plain; charset=utf-8" , "" , bytes .NewReader (buf .Bytes ()))
139+ uncompressed , err = upload (ctx , bucket , key , bytes .NewReader (buf .Bytes ()), uploadOptions {
140+ contentType : "text/plain; charset=utf-8" ,
141+ })
139142 if err != nil {
140143 return indexHashes {}, indexHashes {}, err
141144 }
@@ -147,7 +150,9 @@ func uploadIndex(ctx context.Context, bucket *blob.Bucket, key string, packages
147150 if err := zw .Close (); err != nil {
148151 return indexHashes {}, indexHashes {}, fmt .Errorf ("compress %s: %w" , key , err )
149152 }
150- gzipped , err = upload (ctx , bucket , key + gzipExtension , "application/gzip" , "" , bytes .NewReader (gzipBuf .Bytes ()))
153+ gzipped , err = upload (ctx , bucket , key + gzipExtension , bytes .NewReader (gzipBuf .Bytes ()), uploadOptions {
154+ contentType : "application/gzip" ,
155+ })
151156 if err != nil {
152157 return indexHashes {}, indexHashes {}, err
153158 }
@@ -179,7 +184,10 @@ func uploadBinaryPackage(ctx context.Context, bucket *blob.Bucket, debPath strin
179184 if arch == "" {
180185 return nil , fmt .Errorf ("upload binary package %s: missing Architecture field" , debName )
181186 }
182- packageHashes , err := upload (ctx , bucket , poolPath (debName ), "application/vnd.debian.binary-package" , "immutable" , debFile )
187+ packageHashes , err := upload (ctx , bucket , poolPath (debName ), debFile , uploadOptions {
188+ contentType : "application/vnd.debian.binary-package" ,
189+ cacheControl : immutable ,
190+ })
183191 if err != nil {
184192 return nil , fmt .Errorf ("upload binary package %s: %w" , debName , err )
185193 }
@@ -213,7 +221,10 @@ func uploadSourcePackage(ctx context.Context, bucket *blob.Bucket, dscPath strin
213221 return nil , fmt .Errorf ("upload source package %s: files: %w" , packageName , err )
214222 }
215223
216- _ , err = upload (ctx , bucket , dir + "/" + filepath .Base (dscPath ), "text/plain; charset=utf-8" , "immutable" , bytes .NewReader (dsc ))
224+ _ , err = upload (ctx , bucket , dir + "/" + filepath .Base (dscPath ), bytes .NewReader (dsc ), uploadOptions {
225+ contentType : "text/plain; charset=utf-8" ,
226+ cacheControl : immutable ,
227+ })
217228 if err != nil {
218229 return nil , fmt .Errorf ("upload source package %s: %s: %w" , packageName , filepath .Base (dscPath ), err )
219230 }
@@ -227,7 +238,10 @@ func uploadSourcePackage(ctx context.Context, bucket *blob.Bucket, dscPath strin
227238 if err != nil {
228239 return nil , fmt .Errorf ("upload source package %s: %s: %w" , packageName , fname , err )
229240 }
230- _ , uploadErr := upload (ctx , bucket , dir + "/" + fname , contentType , "immutable" , content )
241+ _ , uploadErr := upload (ctx , bucket , dir + "/" + fname , content , uploadOptions {
242+ contentType : contentType ,
243+ cacheControl : immutable ,
244+ })
231245 content .Close ()
232246 if uploadErr != nil {
233247 return nil , fmt .Errorf ("upload source package %s: %s: %w" , packageName , fname , err )
@@ -276,7 +290,15 @@ func poolPath(name string) string {
276290 return "pool/" + name
277291}
278292
279- func upload (ctx context.Context , bucket * blob.Bucket , key string , contentType , cacheControl string , content io.ReadSeeker ) (indexHashes , error ) {
293+ // immutable is the Cache-Control header that indicates that the content is immutable.
294+ const immutable = "immutable"
295+
296+ type uploadOptions struct {
297+ contentType string
298+ cacheControl string
299+ }
300+
301+ func upload (ctx context.Context , bucket * blob.Bucket , key string , content io.ReadSeeker , opts uploadOptions ) (indexHashes , error ) {
280302 if _ , err := content .Seek (0 , io .SeekStart ); err != nil {
281303 return indexHashes {}, fmt .Errorf ("upload %s: %w" , key , err )
282304 }
@@ -296,10 +318,27 @@ func upload(ctx context.Context, bucket *blob.Bucket, key string, contentType, c
296318 md5Hash .Sum (h .md5 [:0 ])
297319 sha1Hash .Sum (h .sha1 [:0 ])
298320 sha256Hash .Sum (h .sha256 [:0 ])
321+ if opts .cacheControl == immutable {
322+ attr , err := bucket .Attributes (ctx , key )
323+ if err == nil {
324+ // Immutable objects don't have to be uploaded if they already exist,
325+ // but they must match the existing object.
326+ if attr .Size != h .size || ! bytes .Equal (h .md5 [:], attr .MD5 ) {
327+ return indexHashes {}, fmt .Errorf ("upload %s: immutable object differs" , key )
328+ }
329+ return h , nil
330+ } else if gcerrors .Code (err ) != gcerrors .NotFound {
331+ return indexHashes {}, fmt .Errorf ("upload %s: %w" , key , err )
332+ }
333+ }
334+ if opts .cacheControl == "" {
335+ // Default to 5 minute cache.
336+ opts .cacheControl = "max-age=300"
337+ }
299338 w , err := bucket .NewWriter (ctx , key , & blob.WriterOptions {
300- ContentType : contentType ,
339+ ContentType : opts . contentType ,
301340 ContentMD5 : h .md5 [:],
302- CacheControl : cacheControl ,
341+ CacheControl : opts . cacheControl ,
303342 })
304343 if err != nil {
305344 return indexHashes {}, fmt .Errorf ("upload %s: %w" , key , err )
0 commit comments