Repository: gomods/athens
Pull request #1681 (2020-11-21 04:41:36)

Add Content-Length to .zip requests
This PR enables Athens to return an accurate byte count of a module@version's size via the Content-Length header. 

It also allows HEAD requests to get said header without returning the content of the zip file. 

This is needed for Athens to integrate with pkgsite, particularly here: https://github.com/golang/pkgsite/blob/fd89b15b08609658e2b6d1363f52fd2f691db311/internal/proxy/client.go#L97:L116

marwan-at-work: @arschles thanks for the quick review. Addressed all the comments :v: 

Patch:
diff --git a/pkg/download/addons/with_pool.go b/pkg/download/addons/with_pool.go
index 605dad80..b4de103a 100644
--- a/pkg/download/addons/with_pool.go
+++ b/pkg/download/addons/with_pool.go
@@ -2,7 +2,6 @@ package addons
 
 import (
 	"context"
-	"io"
 
 	"github.com/gomods/athens/pkg/download"
 	"github.com/gomods/athens/pkg/errors"
@@ -112,9 +111,9 @@ func (p *withpool) GoMod(ctx context.Context, mod, ver string) ([]byte, error) {
 	return goMod, nil
 }
 
-func (p *withpool) Zip(ctx context.Context, mod, ver string) (io.ReadCloser, error) {
+func (p *withpool) Zip(ctx context.Context, mod, ver string) (storage.SizeReadCloser, error) {
 	const op errors.Op = "pool.Zip"
-	var zip io.ReadCloser
+	var zip storage.SizeReadCloser
 	var err error
 	done := make(chan struct{}, 1)
 	p.jobCh <- func() {
diff --git a/pkg/download/handler.go b/pkg/download/handler.go
index 7d9f8d38..13106e62 100644
--- a/pkg/download/handler.go
+++ b/pkg/download/handler.go
@@ -53,7 +53,7 @@ func RegisterHandlers(r *mux.Router, opts *HandlerOpts) {
 
 	r.Handle(PathVersionInfo, LogEntryHandler(InfoHandler, opts)).Methods(http.MethodGet)
 	r.Handle(PathVersionModule, LogEntryHandler(ModuleHandler, opts)).Methods(http.MethodGet)
-	r.Handle(PathVersionZip, LogEntryHandler(ZipHandler, opts)).Methods(http.MethodGet)
+	r.Handle(PathVersionZip, LogEntryHandler(ZipHandler, opts)).Methods(http.MethodGet, http.MethodHead)
 }
 
 func getRedirectURL(base, downloadPath string) (string, error) {
diff --git a/pkg/download/protocol.go b/pkg/download/protocol.go
index e79b7b33..b2ce841f 100644
--- a/pkg/download/protocol.go
+++ b/pkg/download/protocol.go
@@ -2,7 +2,6 @@ package download
 
 import (
 	"context"
-	"io"
 	"regexp"
 	"strings"
 	"sync"
@@ -31,7 +30,7 @@ type Protocol interface {
 	GoMod(ctx context.Context, mod, ver string) ([]byte, error)
 
 	// Zip implements GET /{module}/@v/{version}.zip
-	Zip(ctx context.Context, mod, ver string) (io.ReadCloser, error)
+	Zip(ctx context.Context, mod, ver string) (storage.SizeReadCloser, error)
 }
 
 // Wrapper helps extend the main protocol's functionality with addons.
@@ -188,7 +187,7 @@ func (p *protocol) GoMod(ctx context.Context, mod, ver string) ([]byte, error) {
 	return goMod, nil
 }
 
-func (p *protocol) Zip(ctx context.Context, mod, ver string) (io.ReadCloser, error) {
+func (p *protocol) Zip(ctx context.Context, mod, ver string) (storage.SizeReadCloser, error) {
 	const op errors.Op = "protocol.Zip"
 	ctx, span := observ.StartSpan(ctx, op.String())
 	defer span.End()
diff --git a/pkg/download/version_zip.go b/pkg/download/version_zip.go
index f3f650dc..fe36673f 100644
--- a/pkg/download/version_zip.go
+++ b/pkg/download/version_zip.go
@@ -3,6 +3,7 @@ package download
 import (
 	"io"
 	"net/http"
+	"strconv"
 
 	"github.com/gomods/athens/pkg/download/mode"
 	"github.com/gomods/athens/pkg/errors"
@@ -43,6 +44,10 @@ func ZipHandler(dp Protocol, lggr log.Entry, df *mode.DownloadFile) http.Handler
 		defer zip.Close()
 
 		w.Header().Set("Content-Type", "application/zip")
+		w.Header().Set("Content-Length", strconv.FormatInt(zip.Size(), 10))
+		if r.Method == http.MethodHead {
+			return
+		}
 		_, err = io.Copy(w, zip)
 		if err != nil {
 			lggr.SystemErr(errors.E(op, errors.M(mod), errors.V(ver), err))
diff --git a/pkg/storage/azureblob/azureblob.go b/pkg/storage/azureblob/azureblob.go
index a058c8be..916623e4 100644
--- a/pkg/storage/azureblob/azureblob.go
+++ b/pkg/storage/azureblob/azureblob.go
@@ -12,6 +12,7 @@ import (
 	"github.com/gomods/athens/pkg/config"
 	"github.com/gomods/athens/pkg/errors"
 	"github.com/gomods/athens/pkg/observ"
+	"github.com/gomods/athens/pkg/storage"
 )
 
 type client interface {
@@ -87,15 +88,17 @@ func (c *azureBlobStoreClient) BlobExists(ctx context.Context, path string) (boo
 
 }
 
-// ReadBlob returns an io.ReadCloser for the contents of a blob
-func (c *azureBlobStoreClient) ReadBlob(ctx context.Context, path string) (io.ReadCloser, error) {
+// ReadBlob returns a storage.SizeReadCloser for the contents of a blob
+func (c *azureBlobStoreClient) ReadBlob(ctx context.Context, path string) (storage.SizeReadCloser, error) {
 	const op errors.Op = "azureblob.ReadBlob"
 	blobURL := c.containerURL.NewBlockBlobURL(path)
 	downloadResponse, err := blobURL.Download(ctx, 0, 0, azblob.BlobAccessConditions{}, false)
 	if err != nil {
 		return nil, errors.E(op, err)
 	}
-	return downloadResponse.Body(azblob.RetryReaderOptions{}), nil
+	rc := downloadResponse.Body(azblob.RetryReaderOptions{})
+	size := downloadResponse.ContentLength()
+	return storage.NewSizer(rc, size), nil
 }
 
 // ListBlobs will list all blobs which has the given prefix
diff --git a/pkg/storage/azureblob/getter.go b/pkg/storage/azureblob/getter.go
index 7742484d..5397be20 100644
--- a/pkg/storage/azureblob/getter.go
+++ b/pkg/storage/azureblob/getter.go
@@ -3,12 +3,12 @@ package azureblob
 import (
 	"context"
 	"fmt"
-	"io"
 	"io/ioutil"
 
 	"github.com/gomods/athens/pkg/config"
 	"github.com/gomods/athens/pkg/errors"
 	"github.com/gomods/athens/pkg/observ"
+	"github.com/gomods/athens/pkg/storage"
 )
 
 // Info implements the (./pkg/storage).Getter interface
@@ -76,7 +76,7 @@ func (s *Storage) GoMod(ctx context.Context, module string, version string) ([]b
 }
 
 // Zip implements the (./pkg/storage).Getter interface
-func (s *Storage) Zip(ctx context.Context, module string, version string) (io.ReadCloser, error) {
+func (s *Storage) Zip(ctx context.Context, module string, version string) (storage.SizeReadCloser, error) {
 	const op errors.Op = "azureblob.Zip"
 	ctx, span := observ.StartSpan(ctx, op.String())
 	defer span.End()
@@ -87,11 +87,9 @@ func (s *Storage) Zip(ctx context.Context, module string, version string) (io.Re
 	if !exists {
 		return nil, errors.E(op, errors.M(module), errors.V(version), errors.KindNotFound)
 	}
-
 	zipReader, err := s.client.ReadBlob(ctx, config.PackageVersionedName(module, version, "zip"))
 	if err != nil {
 		return nil, errors.E(op, err, errors.M(module), errors.V(version))
 	}
-
 	return zipReader, nil
 }
diff --git a/pkg/storage/external/client.go b/pkg/storage/external/client.go
index b131e812..ff214eb8 100644
--- a/pkg/storage/external/client.go
+++ b/pkg/storage/external/client.go
@@ -8,6 +8,7 @@ import (
 	"io/ioutil"
 	"mime/multipart"
 	"net/http"
+	"strconv"
 	"strings"
 
 	"github.com/gomods/athens/pkg/errors"
@@ -31,7 +32,7 @@ func NewClient(url string, c *http.Client) storage.Backend {
 
 func (s *service) List(ctx context.Context, mod string) ([]string, error) {
 	const op errors.Op = "external.List"
-	body, err := s.getRequest(ctx, mod, "list", "")
+	body, _, err := s.getRequest(ctx, mod, "list", "")
 	if err != nil {
 		return nil, errors.E(op, err)
 	}
@@ -48,7 +49,7 @@ func (s *service) List(ctx context.Context, mod string) ([]string, error) {
 
 func (s *service) Info(ctx context.Context, mod, ver string) ([]byte, error) {
 	const op errors.Op = "external.Info"
-	body, err := s.getRequest(ctx, mod, ver, "info")
+	body, _, err := s.getRequest(ctx, mod, ver, "info")
 	if err != nil {
 		return nil, errors.E(op, err)
 	}
@@ -61,7 +62,7 @@ func (s *service) Info(ctx context.Context, mod, ver string) ([]byte, error) {
 
 func (s *service) GoMod(ctx context.Context, mod, ver string) ([]byte, error) {
 	const op errors.Op = "external.GoMod"
-	body, err := s.getRequest(ctx, mod, ver, "mod")
+	body, _, err := s.getRequest(ctx, mod, ver, "mod")
 	if err != nil {
 		return nil, errors.E(op, err)
 	}
@@ -72,13 +73,13 @@ func (s *service) GoMod(ctx context.Context, mod, ver string) ([]byte, error) {
 	return modFile, nil
 }
 
-func (s *service) Zip(ctx context.Context, mod, ver string) (io.ReadCloser, error) {
+func (s *service) Zip(ctx context.Context, mod, ver string) (storage.SizeReadCloser, error) {
 	const op errors.Op = "external.Zip"
-	body, err := s.getRequest(ctx, mod, ver, "zip")
+	body, size, err := s.getRequest(ctx, mod, ver, "zip")
 	if err != nil {
 		return nil, errors.E(op, err)
 	}
-	return body, nil
+	return storage.NewSizer(body, size), nil
 }
 
 func (s *service) Save(ctx context.Context, mod, ver string, modFile []byte, zip io.Reader, info []byte) error {
@@ -114,7 +115,7 @@ func (s *service) Save(ctx context.Context, mod, ver string, modFile []byte, zip
 
 func (s *service) Delete(ctx context.Context, mod, ver string) error {
 	const op errors.Op = "external.Delete"
-	body, err := s.doRequest(ctx, "DELETE", mod, ver, "delete")
+	body, _, err := s.doRequest(ctx, "DELETE", mod, ver, "delete")
 	if err != nil {
 		return errors.E(op, err)
 	}
@@ -151,16 +152,16 @@ func upload(mw *multipart.Writer, mod, info []byte, zip io.Reader) error {
 	return nil
 }
 
-func (s *service) getRequest(ctx context.Context, mod, ver, ext string) (io.ReadCloser, error) {
+func (s *service) getRequest(ctx context.Context, mod, ver, ext string) (io.ReadCloser, int64, error) {
 	return s.doRequest(ctx, "GET", mod, ver, ext)
 }
 
-func (s *service) doRequest(ctx context.Context, method, mod, ver, ext string) (io.ReadCloser, error) {
+func (s *service) doRequest(ctx context.Context, method, mod, ver, ext string) (io.ReadCloser, int64, error) {
 	const op errors.Op = "external.doRequest"
 	var err error
 	mod, err = module.EscapePath(mod)
 	if err != nil {
-		return nil, errors.E(op, err)
+		return nil, 0, errors.E(op, err)
 	}
 	url := s.url + "/" + mod + "/@v/" + ver
 	if ext != "" {
@@ -168,16 +169,23 @@ func (s *service) doRequest(ctx context.Context, method, mod, ver, ext string) (
 	}
 	req, err := http.NewRequestWithContext(ctx, method, url, nil)
 	if err != nil {
-		return nil, errors.E(op, err)
+		return nil, 0, errors.E(op, err)
 	}
 	resp, err := s.c.Do(req)
 	if err != nil {
-		return nil, errors.E(op, err)
+		return nil, 0, errors.E(op, err)
 	}
 	if resp.StatusCode != 200 {
 		body, _ := ioutil.ReadAll(resp.Body)
 		resp.Body.Close()
-		return nil, errors.E(op, fmt.Errorf("none 200 status code: %v - body: %s", resp.StatusCode, body), resp.StatusCode)
+		return nil, 0, errors.E(op, fmt.Errorf("none 200 status code: %v - body: %s", resp.StatusCode, body), resp.StatusCode)
+	}
+	var size int64
+	if cl := resp.Header.Get("Content-Length"); cl != "" {
+		size, err = strconv.ParseInt(cl, 10, 64)
+		if err != nil {
+			return nil, 0, errors.E(op, fmt.Errorf("could not parse content-length(%q): %w", cl, err))
+		}
 	}
-	return resp.Body, nil
+	return resp.Body, size, nil
 }
diff --git a/pkg/storage/external/server.go b/pkg/storage/external/server.go
index 51023fd1..53e1b255 100644
--- a/pkg/storage/external/server.go
+++ b/pkg/storage/external/server.go
@@ -5,6 +5,7 @@ import (
 	"io"
 	"io/ioutil"
 	"net/http"
+	"strconv"
 	"strings"
 
 	"github.com/gomods/athens/pkg/download"
@@ -67,6 +68,7 @@ func NewServer(strg storage.Backend) http.Handler {
 			return
 		}
 		defer zip.Close()
+		w.Header().Set("Content-Length", strconv.FormatInt(zip.Size(), 10))
 		io.Copy(w, zip)
 	}).Methods(http.MethodGet)
 	r.HandleFunc("/{module:.+}/@v/{version}.save", func(w http.ResponseWriter, r *http.Request) {
diff --git a/pkg/storage/fs/getter.go b/pkg/storage/fs/getter.go
index 6d9b5c0f..513e2ea8 100644
--- a/pkg/storage/fs/getter.go
+++ b/pkg/storage/fs/getter.go
@@ -2,12 +2,12 @@ package fs
 
 import (
 	"context"
-	"io"
 	"os"
 	"path/filepath"
 
 	"github.com/gomods/athens/pkg/errors"
 	"github.com/gomods/athens/pkg/observ"
+	"github.com/gomods/athens/pkg/storage"
 	"github.com/spf13/afero"
 )
 
@@ -37,7 +37,7 @@ func (v *storageImpl) GoMod(ctx context.Context, module, version string) ([]byte
 	return mod, nil
 }
 
-func (v *storageImpl) Zip(ctx context.Context, module, version string) (io.ReadCloser, error) {
+func (v *storageImpl) Zip(ctx context.Context, module, version string) (storage.SizeReadCloser, error) {
 	const op errors.Op = "fs.Zip"
 	ctx, span := observ.StartSpan(ctx, op.String())
 	defer span.End()
@@ -47,6 +47,21 @@ func (v *storageImpl) Zip(ctx context.Context, module, version string) (io.ReadC
 	if err != nil {
 		return nil, errors.E(op, errors.M(module), errors.V(version), errors.KindNotFound)
 	}
+	fi, err := src.Stat()
+	if err != nil {
+		return nil, errors.E(op, err)
+	}
+	return storage.NewSizer(src, fi.Size()), nil
+}
 
-	return src, nil
+func (v *storageImpl) ZipSize(ctx context.Context, module, version string) (int64, error) {
+	const op errors.Op = "fs.ZipFileSize"
+	ctx, span := observ.StartSpan(ctx, op.String())
+	defer span.End()
+	versionedPath := v.versionLocation(module, version)
+	fi, err := v.filesystem.Stat(filepath.Join(versionedPath))
+	if err != nil {
+		return 0, errors.E(op, err, errors.M(module), errors.V(version), errors.KindNotFound)
+	}
+	return fi.Size(), nil
 }
diff --git a/pkg/storage/gcp/getter.go b/pkg/storage/gcp/getter.go
index 73ea088e..8049b2e2 100644
--- a/pkg/storage/gcp/getter.go
+++ b/pkg/storage/gcp/getter.go
@@ -3,13 +3,13 @@ package gcp
 import (
 	"context"
 	"fmt"
-	"io"
 	"io/ioutil"
 
 	"cloud.google.com/go/storage"
 	"github.com/gomods/athens/pkg/config"
 	"github.com/gomods/athens/pkg/errors"
 	"github.com/gomods/athens/pkg/observ"
+	pkgstorage "github.com/gomods/athens/pkg/storage"
 )
 
 // Info implements Getter
@@ -48,7 +48,7 @@ func (s *Storage) GoMod(ctx context.Context, module, version string) ([]byte, er
 }
 
 // Zip implements Getter
-func (s *Storage) Zip(ctx context.Context, module, version string) (io.ReadCloser, error) {
+func (s *Storage) Zip(ctx context.Context, module, version string) (pkgstorage.SizeReadCloser, error) {
 	const op errors.Op = "gcp.Zip"
 	ctx, span := observ.StartSpan(ctx, op.String())
 	defer span.End()
@@ -56,8 +56,7 @@ func (s *Storage) Zip(ctx context.Context, module, version string) (io.ReadClose
 	if err != nil {
 		return nil, errors.E(op, err, getErrorKind(err), errors.M(module), errors.V(version))
 	}
-
-	return zipReader, nil
+	return pkgstorage.NewSizer(zipReader, zipReader.Size()), nil
 }
 
 func getErrorKind(err error) int {
diff --git a/pkg/storage/getter.go b/pkg/storage/getter.go
index bae54c79..d26eb38b 100644
--- a/pkg/storage/getter.go
+++ b/pkg/storage/getter.go
@@ -9,5 +9,28 @@ import (
 type Getter interface {
 	Info(ctx context.Context, module, vsn string) ([]byte, error)
 	GoMod(ctx context.Context, module, vsn string) ([]byte, error)
-	Zip(ctx context.Context, module, vsn string) (io.ReadCloser, error)
+	Zip(ctx context.Context, module, vsn string) (SizeReadCloser, error)
+}
+
+// SizeReadCloser extends io.ReadCloser
+// with a Size() method that tells you the
+// length of the io.ReadCloser if read in full
+type SizeReadCloser interface {
+	io.ReadCloser
+	Size() int64
+}
+
+// NewSizer is a helper wrapper to return an implementation
+// of ReadCloserSizer
+func NewSizer(rc io.ReadCloser, size int64) SizeReadCloser {
+	return &sizeReadCloser{rc, size}
+}
+
+type sizeReadCloser struct {
+	io.ReadCloser
+	size int64
+}
+
+func (zf *sizeReadCloser) Size() int64 {
+	return zf.size
 }
diff --git a/pkg/storage/minio/getter.go b/pkg/storage/minio/getter.go
index 94f3f958..8e66f5d8 100644
--- a/pkg/storage/minio/getter.go
+++ b/pkg/storage/minio/getter.go
@@ -3,12 +3,12 @@ package minio
 import (
 	"context"
 	"fmt"
-	"io"
 	"io/ioutil"
 	"net/http"
 
 	"github.com/gomods/athens/pkg/errors"
 	"github.com/gomods/athens/pkg/observ"
+	"github.com/gomods/athens/pkg/storage"
 	minio "github.com/minio/minio-go/v6"
 )
 
@@ -45,7 +45,7 @@ func (v *storageImpl) GoMod(ctx context.Context, module, vsn string) ([]byte, er
 
 	return mod, nil
 }
-func (v *storageImpl) Zip(ctx context.Context, module, vsn string) (io.ReadCloser, error) {
+func (v *storageImpl) Zip(ctx context.Context, module, vsn string) (storage.SizeReadCloser, error) {
 	const op errors.Op = "minio.Zip"
 	ctx, span := observ.StartSpan(ctx, op.String())
 	defer span.End()
@@ -60,8 +60,11 @@ func (v *storageImpl) Zip(ctx context.Context, module, vsn string) (io.ReadClose
 	if err != nil {
 		return nil, errors.E(op, err)
 	}
-
-	return zipReader, nil
+	oi, err := zipReader.Stat()
+	if err != nil {
+		return nil, errors.E(op, err)
+	}
+	return storage.NewSizer(zipReader, oi.Size), nil
 }
 
 func transformNotFoundErr(op errors.Op, module, version string, err error) error {
diff --git a/pkg/storage/module.go b/pkg/storage/module.go
index 7667741d..1127a911 100644
--- a/pkg/storage/module.go
+++ b/pkg/storage/module.go
@@ -6,10 +6,11 @@ import (
 
 // Module represents a vgo module saved in a storage backend.
 type Module struct {
+	// TODO(marwan-at-work): ID is a mongo-specific field, it should not be
+	// in the generic storage.Module struct.
 	ID      primitive.ObjectID `bson:"_id,omitempty"`
 	Module  string             `bson:"module"`
 	Version string             `bson:"version"`
 	Mod     []byte             `bson:"mod"`
-	Zip     []byte             `bson:"zip"`
 	Info    []byte             `bson:"info"`
 }
diff --git a/pkg/storage/mongo/getter.go b/pkg/storage/mongo/getter.go
index d6e43d37..09a429e1 100644
--- a/pkg/storage/mongo/getter.go
+++ b/pkg/storage/mongo/getter.go
@@ -2,7 +2,6 @@ package mongo
 
 import (
 	"context"
-	"io"
 
 	"github.com/gomods/athens/pkg/errors"
 	"github.com/gomods/athens/pkg/observ"
@@ -11,6 +10,7 @@ import (
 	"go.mongodb.org/mongo-driver/mongo"
 	"go.mongodb.org/mongo-driver/mongo/gridfs"
 	"go.mongodb.org/mongo-driver/mongo/options"
+	"go.mongodb.org/mongo-driver/x/bsonx"
 )
 
 // Info implements storage.Getter
@@ -44,7 +44,7 @@ func (s *ModuleStore) GoMod(ctx context.Context, module, vsn string) ([]byte, er
 }
 
 // Zip implements storage.Getter
-func (s *ModuleStore) Zip(ctx context.Context, module, vsn string) (io.ReadCloser, error) {
+func (s *ModuleStore) Zip(ctx context.Context, module, vsn string) (storage.SizeReadCloser, error) {
 	const op errors.Op = "mongo.Zip"
 	ctx, span := observ.StartSpan(ctx, op.String())
 	defer span.End()
@@ -64,8 +64,19 @@ func (s *ModuleStore) Zip(ctx context.Context, module, vsn string) (io.ReadClose
 		}
 		return nil, errors.E(op, err, kind, errors.M(module), errors.V(vsn))
 	}
-
-	return dStream, nil
+	res := s.client.Database(s.db).Collection("fs.files").FindOne(ctx, bson.M{
+		"filename": zipName,
+	})
+	if res.Err() != nil {
+		return nil, errors.E(op, res.Err())
+	}
+	var m bsonx.Doc
+	err = res.Decode(&m)
+	if err != nil {
+		return nil, errors.E(op, err)
+	}
+	size, _ := m.Lookup("length").Int64OK()
+	return storage.NewSizer(dStream, size), nil
 }
 
 // Query connects to and queries storage module
diff --git a/pkg/storage/s3/getter.go b/pkg/storage/s3/getter.go
index 372e777a..0da45ba4 100644
--- a/pkg/storage/s3/getter.go
+++ b/pkg/storage/s3/getter.go
@@ -3,7 +3,6 @@ package s3
 import (
 	"context"
 	"fmt"
-	"io"
 	"io/ioutil"
 
 	"github.com/aws/aws-sdk-go/aws"
@@ -11,6 +10,7 @@ import (
 	"github.com/gomods/athens/pkg/config"
 	"github.com/gomods/athens/pkg/errors"
 	"github.com/gomods/athens/pkg/observ"
+	"github.com/gomods/athens/pkg/storage"
 )
 
 // Info implements the (./pkg/storage).Getter interface
@@ -67,7 +67,7 @@ func (s *Storage) GoMod(ctx context.Context, module, version string) ([]byte, er
 }
 
 // Zip implements the (./pkg/storage).Getter interface
-func (s *Storage) Zip(ctx context.Context, module, version string) (io.ReadCloser, error) {
+func (s *Storage) Zip(ctx context.Context, module, version string) (storage.SizeReadCloser, error) {
 	const op errors.Op = "s3.Zip"
 	ctx, span := observ.StartSpan(ctx, op.String())
 	defer span.End()
@@ -87,7 +87,7 @@ func (s *Storage) Zip(ctx context.Context, module, version string) (io.ReadClose
 	return zipReader, nil
 }
 
-func (s *Storage) open(ctx context.Context, path string) (io.ReadCloser, error) {
+func (s *Storage) open(ctx context.Context, path string) (storage.SizeReadCloser, error) {
 	const op errors.Op = "s3.open"
 	ctx, span := observ.StartSpan(ctx, op.String())
 	defer span.End()
@@ -100,6 +100,9 @@ func (s *Storage) open(ctx context.Context, path string) (io.ReadCloser, error)
 	if err != nil {
 		return nil, errors.E(op, err)
 	}
-
-	return goo.Body, nil
+	var size int64
+	if goo.ContentLength != nil {
+		size = *goo.ContentLength
+	}
+	return storage.NewSizer(goo.Body, size), nil
 }


Test patch:
diff --git a/pkg/download/addons/with_pool_test.go b/pkg/download/addons/with_pool_test.go
index a2a387cc..b227fb1f 100644
--- a/pkg/download/addons/with_pool_test.go
+++ b/pkg/download/addons/with_pool_test.go
@@ -4,7 +4,6 @@ import (
 	"bytes"
 	"context"
 	"fmt"
-	"io"
 	"reflect"
 	"sync"
 	"testing"
@@ -98,7 +97,7 @@ type mockDP struct {
 	info     []byte
 	latest   *storage.RevInfo
 	gomod    []byte
-	zip      io.ReadCloser
+	zip      storage.SizeReadCloser
 	inputMod string
 	inputVer string
 	catalog  []paths.AllPathParams
@@ -143,7 +142,7 @@ func (m *mockDP) GoMod(ctx context.Context, mod, ver string) ([]byte, error) {
 }
 
 // Zip implements GET /{module}/@v/{version}.zip
-func (m *mockDP) Zip(ctx context.Context, mod, ver string) (io.ReadCloser, error) {
+func (m *mockDP) Zip(ctx context.Context, mod, ver string) (storage.SizeReadCloser, error) {
 	if m.inputMod != mod {
 		return nil, fmt.Errorf("expected mod input %v but got %v", m.inputMod, mod)
 	}
diff --git a/pkg/download/handler_test.go b/pkg/download/handler_test.go
index 4d2a0c4e..732ad4c2 100644
--- a/pkg/download/handler_test.go
+++ b/pkg/download/handler_test.go
@@ -2,7 +2,6 @@ package download
 
 import (
 	"context"
-	"io"
 	"net/http"
 	"net/http/httptest"
 	"testing"
@@ -10,6 +9,7 @@ import (
 	"github.com/gomods/athens/pkg/download/mode"
 	"github.com/gomods/athens/pkg/errors"
 	"github.com/gomods/athens/pkg/log"
+	"github.com/gomods/athens/pkg/storage"
 	"github.com/gorilla/mux"
 )
 
@@ -58,7 +58,7 @@ func (mp *mockProtocol) GoMod(ctx context.Context, mod, ver string) ([]byte, err
 	return nil, errors.E(op, "not found", errors.KindRedirect)
 }
 
-func (mp *mockProtocol) Zip(ctx context.Context, mod, ver string) (io.ReadCloser, error) {
+func (mp *mockProtocol) Zip(ctx context.Context, mod, ver string) (storage.SizeReadCloser, error) {
 	const op errors.Op = "mockProtocol.Zip"
 	return nil, errors.E(op, "not found", errors.KindRedirect)
 }
diff --git a/pkg/storage/compliance/tests.go b/pkg/storage/compliance/tests.go
index 1e0835a7..9d554dac 100644
--- a/pkg/storage/compliance/tests.go
+++ b/pkg/storage/compliance/tests.go
@@ -153,6 +153,7 @@ func testGet(t *testing.T, b storage.Backend) {
 	givenZipBts, err := ioutil.ReadAll(zip)
 	require.NoError(t, err)
 	require.Equal(t, zipBts, givenZipBts)
+	require.Equal(t, int64(len(zipBts)), zip.Size())
 }
 
 func testExists(t *testing.T, b storage.Backend) {