// getOrCreateBlobWriter will track which blobs are currently being downloaded and enable client requesting // the same blob concurrently to read from the existing stream. func getOrCreateBlobWriter(ctx context.Context, blobs distribution.BlobService, desc distribution.Descriptor) (distribution.BlobWriter, bool, cleanupFunc, error) { mu.Lock() defer mu.Unlock() dgst := desc.Digest cleanup := func() { mu.Lock() defer mu.Unlock() inflight[dgst].refCount-- if inflight[dgst].refCount == 0 { defer delete(inflight, dgst) _, err := inflight[dgst].bw.Commit(ctx, desc) if err != nil { // There is a narrow race here where Commit can be called while this blob's TTL is expiring // and its being removed from storage. In that case, the client stream will continue // uninterruped and the blob will be pulled through on the next request, so just log it context.GetLogger(ctx).Errorf("Error committing blob: %q", err) } } } var bw distribution.BlobWriter _, ok := inflight[dgst] if ok { bw = inflight[dgst].bw inflight[dgst].refCount++ return bw, false, cleanup, nil } var err error bw, err = blobs.Create(ctx) if err != nil { return nil, false, nil, err } inflight[dgst] = &inflightBlob{refCount: 1, bw: bw} return bw, true, cleanup, nil }
func (p *v2Pusher) pushV2Image(bs distribution.BlobService, img *image.Image) (digest.Digest, error) { out := p.config.OutStream out.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), "Preparing", nil)) image, err := p.graph.Get(img.ID) if err != nil { return "", err } arch, err := p.graph.TarLayer(image) if err != nil { return "", err } defer arch.Close() // Send the layer layerUpload, err := bs.Create(context.Background()) if err != nil { return "", err } defer layerUpload.Close() reader := progressreader.New(progressreader.Config{ In: ioutil.NopCloser(arch), // we'll take care of close here. Out: out, Formatter: p.sf, // TODO(stevvooe): This may cause a size reporting error. Try to get // this from tar-split or elsewhere. The main issue here is that we // don't want to buffer to disk *just* to calculate the size. Size: img.Size, NewLines: false, ID: stringid.TruncateID(img.ID), Action: "Pushing", }) digester := digest.Canonical.New() // HACK: The MultiWriter doesn't write directly to layerUpload because // we must make sure the ReadFrom is used, not Write. Using Write would // send a PATCH request for every Write call. pipeReader, pipeWriter := io.Pipe() // Use a bufio.Writer to avoid excessive chunking in HTTP request. bufWriter := bufio.NewWriterSize(io.MultiWriter(pipeWriter, digester.Hash()), compressionBufSize) compressor := gzip.NewWriter(bufWriter) go func() { _, err := io.Copy(compressor, reader) if err == nil { err = compressor.Close() } if err == nil { err = bufWriter.Flush() } if err != nil { pipeWriter.CloseWithError(err) } else { pipeWriter.Close() } }() out.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), "Pushing", nil)) nn, err := layerUpload.ReadFrom(pipeReader) pipeReader.Close() if err != nil { return "", err } dgst := digester.Digest() if _, err := layerUpload.Commit(context.Background(), distribution.Descriptor{Digest: dgst}); err != nil { return "", err } logrus.Debugf("uploaded layer %s (%s), %d bytes", img.ID, dgst, nn) out.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), "Pushed", nil)) return dgst, nil }