registry/storage/blobserver.go (83 lines of code) (raw):

package storage import ( "context" "errors" "fmt" "net/http" "time" "github.com/docker/distribution" "github.com/docker/distribution/log" "github.com/docker/distribution/notifications/meta" "github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/registry/storage/internal/metrics" "github.com/opencontainers/go-digest" ) const blobCacheControlMaxAge = 365 * 24 * time.Hour type redirect struct { enabled bool // allows toggling redirects to storage backends for blob downloads expiryDelay time.Duration // allows setting a custom delay for the presigned URLs expiration (defaults to 20m) } // blobServer simply serves blobs from a driver instance using a path function // to identify paths and a descriptor service to fill in metadata. type blobServer struct { driver driver.StorageDriver statter distribution.BlobStatter pathFn func(dgst digest.Digest) (string, error) redirect redirect } func (bs *blobServer) ServeBlob(ctx context.Context, w http.ResponseWriter, r *http.Request, dgst digest.Digest) (*meta.Blob, error) { desc, err := bs.statter.Stat(ctx, dgst) if err != nil { return nil, err } path, err := bs.pathFn(desc.Digest) if err != nil { return nil, err } l := log.GetLogger(log.WithContext(ctx)).WithFields(log.Fields{ "size_bytes": desc.Size, "digest": desc.Digest, }) var redirect bool if bs.redirect.enabled { opts := map[string]any{"method": r.Method} if bs.redirect.expiryDelay > 0 { opts["expiry"] = time.Now().Add(bs.redirect.expiryDelay) } injectCustomKeyOpts(ctx, opts, map[string]any{SizeBytesKey: desc.Size}) // TODO: The `driver` needs to be able to infer the redirect url's provider name that is used to obtain a redirect url. // This is required for notification purposes see: https://gitlab.com/gitlab-org/container-registry/-/issues/961. // Currently only the cofigured driver's name can be inferred. Example: using `gcs` storage driver the redirect url's provider // should be either the driver itself (e.g `gcs`) OR a pre-configured redirect middleware (e.g `googlecdn`) redirectURL, err := bs.driver.URLFor(ctx, path, opts) switch { case err == nil: // Redirect to storage URL. http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect) redirect = true metrics.BlobDownload(redirect, desc.Size) if r.Method == http.MethodGet { l.WithFields(log.Fields{"redirect": redirect}).Info("blob downloaded") } return &meta.Blob{StorageBackend: bs.driver.Name(), Redirected: redirect}, nil case errors.As(err, new(driver.ErrUnsupportedMethod)): // Fallback to serving the content directly. default: // Some unexpected error. return nil, err } } br := newFileReader(ctx, bs.driver, path, desc.Size) if err != nil { return nil, err } defer br.Close() w.Header().Set("ETag", fmt.Sprintf(`"%s"`, desc.Digest)) // If-None-Match handled by ServeContent w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%.f", blobCacheControlMaxAge.Seconds())) if w.Header().Get("Docker-Content-Digest") == "" { w.Header().Set("Docker-Content-Digest", desc.Digest.String()) } if w.Header().Get("Content-Type") == "" { // Set the content type if not already set. w.Header().Set("Content-Type", desc.MediaType) } if w.Header().Get("Content-Length") == "" { // Set the content length if not already set. w.Header().Set("Content-Length", fmt.Sprint(desc.Size)) } http.ServeContent(w, r, desc.Digest.String(), time.Time{}, br) metrics.BlobDownload(redirect, desc.Size) if r.Method == http.MethodGet { l.WithFields(log.Fields{"redirect": redirect}).Info("blob downloaded") } return &meta.Blob{StorageBackend: bs.driver.Name(), Redirected: redirect}, nil }