diff --git a/src/extract.go b/src/extract.go index 094f5bd..1326cd2 100644 --- a/src/extract.go +++ b/src/extract.go @@ -19,8 +19,6 @@ import ( var ErrArchiveTooLarge = errors.New("archive too large") -const BlobReferencePrefix = "/git/blobs/" - func boundArchiveStream(reader io.Reader) io.Reader { return ReadAtMost(reader, int64(config.Limits.MaxSiteSize.Bytes()), fmt.Errorf("%w: %s limit exceeded", ErrArchiveTooLarge, config.Limits.MaxSiteSize.HR())) @@ -52,6 +50,16 @@ func ExtractZstd( return next(ctx, boundArchiveStream(stream)) } +const BlobReferencePrefix = "/git/blobs/" + +type UnresolvedRefError struct { + missing []string +} + +func (err UnresolvedRefError) Error() string { + return fmt.Sprintf("%d unresolved blob references", len(err.missing)) +} + // Returns a map of git hash to entry. If `manifest` is nil, returns an empty map. func indexManifestByGitHash(manifest *Manifest) map[string]*Entry { index := map[string]*Entry{} @@ -68,14 +76,15 @@ func indexManifestByGitHash(manifest *Manifest) map[string]*Entry { } func addSymlinkOrBlobReference( - manifest *Manifest, fileName string, target string, index map[string]*Entry, + manifest *Manifest, fileName string, target string, + index map[string]*Entry, missing *[]string, ) *Entry { if hash, found := strings.CutPrefix(target, BlobReferencePrefix); found { if entry, found := index[hash]; found { manifest.Contents[fileName] = entry return entry } else { - AddProblem(manifest, fileName, "unresolved reference: %s", target) + *missing = append(*missing, hash) return nil } } else { @@ -90,6 +99,7 @@ func ExtractTar(ctx context.Context, reader io.Reader, oldManifest *Manifest) (* var dataBytesTransferred int64 index := indexManifestByGitHash(oldManifest) + missing := []string{} manifest := NewManifest() for { header, err := archive.Next() @@ -119,7 +129,8 @@ func ExtractTar(ctx context.Context, reader io.Reader, oldManifest *Manifest) (* AddFile(manifest, fileName, fileData) dataBytesTransferred += int64(len(fileData)) case tar.TypeSymlink: - entry := addSymlinkOrBlobReference(manifest, fileName, header.Linkname, index) + entry := addSymlinkOrBlobReference( + manifest, fileName, header.Linkname, index, &missing) dataBytesRecycled += entry.GetOriginalSize() case tar.TypeDir: AddDirectory(manifest, fileName) @@ -129,6 +140,10 @@ func ExtractTar(ctx context.Context, reader io.Reader, oldManifest *Manifest) (* } } + if len(missing) > 0 { + return nil, UnresolvedRefError{missing} + } + logc.Printf(ctx, "reuse: %s recycled, %s transferred\n", datasize.ByteSize(dataBytesRecycled).HR(), @@ -166,6 +181,7 @@ func ExtractZip(ctx context.Context, reader io.Reader, oldManifest *Manifest) (* var dataBytesTransferred int64 index := indexManifestByGitHash(oldManifest) + missing := []string{} manifest := NewManifest() for _, file := range archive.File { if strings.HasSuffix(file.Name, "/") { @@ -183,7 +199,8 @@ func ExtractZip(ctx context.Context, reader io.Reader, oldManifest *Manifest) (* } if file.Mode()&os.ModeSymlink != 0 { - entry := addSymlinkOrBlobReference(manifest, file.Name, string(fileData), index) + entry := addSymlinkOrBlobReference( + manifest, file.Name, string(fileData), index, &missing) dataBytesRecycled += entry.GetOriginalSize() } else { AddFile(manifest, file.Name, fileData) @@ -192,6 +209,10 @@ func ExtractZip(ctx context.Context, reader io.Reader, oldManifest *Manifest) (* } } + if len(missing) > 0 { + return nil, UnresolvedRefError{missing} + } + logc.Printf(ctx, "reuse: %s recycled, %s transferred\n", datasize.ByteSize(dataBytesRecycled).HR(), diff --git a/src/pages.go b/src/pages.go index c7d8a52..85ad62b 100644 --- a/src/pages.go +++ b/src/pages.go @@ -572,6 +572,7 @@ func patchPage(w http.ResponseWriter, r *http.Request) error { func reportUpdateResult(w http.ResponseWriter, result UpdateResult) error { switch result.outcome { case UpdateError: + var unresolvedRefErr UnresolvedRefError if errors.Is(result.err, ErrManifestTooLarge) { w.WriteHeader(http.StatusRequestEntityTooLarge) } else if errors.Is(result.err, errArchiveFormat) { @@ -586,6 +587,8 @@ func reportUpdateResult(w http.ResponseWriter, result UpdateResult) error { w.WriteHeader(http.StatusConflict) } else if errors.Is(result.err, ErrDomainFrozen) { w.WriteHeader(http.StatusForbidden) + } else if errors.As(result.err, &unresolvedRefErr) { + w.WriteHeader(http.StatusUnprocessableEntity) } else { w.WriteHeader(http.StatusServiceUnavailable) }