Files
git-pages/src/update.go
Catherine b37ca8cd14 Fix combined partial and incremental updates.
It seems that I forgot to implement incremental update support for
partial updates entirely.
2026-03-25 05:08:42 +00:00

251 lines
6.9 KiB
Go

package git_pages
import (
"context"
"errors"
"fmt"
"io"
"strings"
"google.golang.org/protobuf/proto"
)
const BlobReferencePrefix = "/git/blobs/"
type UnresolvedRefError struct {
missing []string
}
func (err UnresolvedRefError) Error() string {
return fmt.Sprintf("%d unresolved blob references", len(err.missing))
}
type UpdateOutcome int
const (
UpdateError UpdateOutcome = iota
UpdateTimeout
UpdateCreated
UpdateReplaced
UpdateDeleted
UpdateNoChange
)
type UpdateResult struct {
outcome UpdateOutcome
manifest *Manifest
err error
}
func Update(
ctx context.Context, webRoot string, oldManifest, newManifest *Manifest,
opts ModifyManifestOptions,
) UpdateResult {
var err error
var storedManifest *Manifest
outcome := UpdateError
if IsManifestEmpty(newManifest) {
storedManifest, err = newManifest, backend.DeleteManifest(ctx, webRoot, opts)
if err == nil {
if oldManifest == nil {
outcome = UpdateNoChange
} else {
outcome = UpdateDeleted
}
}
} else if err = PrepareManifest(ctx, newManifest); err == nil {
storedManifest, err = StoreManifest(ctx, webRoot, newManifest, opts)
if err == nil {
domain, _, _ := strings.Cut(webRoot, "/")
err = backend.CreateDomain(ctx, domain)
}
if err == nil {
if oldManifest == nil {
outcome = UpdateCreated
} else if CompareManifest(oldManifest, storedManifest) {
outcome = UpdateNoChange
} else {
outcome = UpdateReplaced
}
}
}
if err == nil {
status := ""
switch outcome {
case UpdateCreated:
status = "created"
case UpdateReplaced:
status = "replaced"
case UpdateDeleted:
status = "deleted"
case UpdateNoChange:
status = "unchanged"
}
if storedManifest.Commit != nil {
logc.Printf(ctx, "update %s ok: %s %s", webRoot, *storedManifest.Commit, status)
} else {
logc.Printf(ctx, "update %s ok: %s", webRoot, status)
}
} else {
logc.Printf(ctx, "update %s err: %s", webRoot, err)
}
return UpdateResult{outcome, storedManifest, err}
}
func UpdateFromRepository(
ctx context.Context,
webRoot string,
repoURL string,
branch string,
) (result UpdateResult) {
span, ctx := ObserveFunction(ctx, "UpdateFromRepository", "repo.url", repoURL)
defer span.Finish()
logc.Printf(ctx, "update %s: %s %s\n", webRoot, repoURL, branch)
// Ignore errors; worst case we have to re-fetch all of the blobs.
oldManifest, _, _ := backend.GetManifest(ctx, webRoot, GetManifestOptions{})
newManifest, err := FetchRepository(ctx, repoURL, branch, oldManifest)
if errors.Is(err, context.DeadlineExceeded) {
result = UpdateResult{UpdateTimeout, nil, fmt.Errorf("update timeout")}
} else if err != nil {
result = UpdateResult{UpdateError, nil, err}
} else {
result = Update(ctx, webRoot, oldManifest, newManifest, ModifyManifestOptions{})
}
observeUpdateResult(result)
return result
}
var errArchiveFormat = errors.New("unsupported archive format")
func UpdateFromArchive(
ctx context.Context,
webRoot string,
contentType string,
reader io.Reader,
) (result UpdateResult) {
var err error
// Ignore errors; worst case we have to re-fetch all of the blobs.
oldManifest, _, _ := backend.GetManifest(ctx, webRoot, GetManifestOptions{})
extractTar := func(ctx context.Context, reader io.Reader) (*Manifest, error) {
return ExtractTar(ctx, reader, oldManifest)
}
var newManifest *Manifest
switch contentType {
case "application/x-tar":
logc.Printf(ctx, "update %s: (tar)", webRoot)
newManifest, err = extractTar(ctx, reader) // yellow?
case "application/x-tar+gzip":
logc.Printf(ctx, "update %s: (tar.gz)", webRoot)
newManifest, err = ExtractGzip(ctx, reader, extractTar) // definitely yellow.
case "application/x-tar+zstd":
logc.Printf(ctx, "update %s: (tar.zst)", webRoot)
newManifest, err = ExtractZstd(ctx, reader, extractTar)
case "application/zip":
logc.Printf(ctx, "update %s: (zip)", webRoot)
newManifest, err = ExtractZip(ctx, reader, oldManifest)
default:
err = errArchiveFormat
}
if err != nil {
logc.Printf(ctx, "update %s err: %s", webRoot, err)
result = UpdateResult{UpdateError, nil, err}
} else {
result = Update(ctx, webRoot, oldManifest, newManifest, ModifyManifestOptions{})
}
observeUpdateResult(result)
return
}
func PartialUpdateFromArchive(
ctx context.Context,
webRoot string,
contentType string,
reader io.Reader,
parents CreateParentsMode,
) (result UpdateResult) {
var err error
// Here the old manifest is used both as a substrate to which a patch is applied, as well
// as a "load linked" operation for a future "store conditional" update which, taken together,
// create an atomic compare-and-swap operation.
oldManifest, oldMetadata, err := backend.GetManifest(ctx, webRoot,
GetManifestOptions{BypassCache: true})
if err != nil {
logc.Printf(ctx, "patch %s err: %s", webRoot, err)
return UpdateResult{UpdateError, nil, err}
}
applyTarPatch := func(ctx context.Context, reader io.Reader) (*Manifest, error) {
// Clone the manifest before starting to mutate it. `GetManifest` may return cached
// `*Manifest` objects, which should never be mutated.
newManifest := &Manifest{}
proto.Merge(newManifest, oldManifest)
newManifest.RepoUrl = nil
newManifest.Branch = nil
newManifest.Commit = nil
if err := ApplyTarPatch(newManifest, reader, parents); err != nil {
return nil, err
} else {
return newManifest, nil
}
}
var newManifest *Manifest
switch contentType {
case "application/x-tar":
logc.Printf(ctx, "patch %s: (tar)", webRoot)
newManifest, err = applyTarPatch(ctx, reader)
case "application/x-tar+gzip":
logc.Printf(ctx, "patch %s: (tar.gz)", webRoot)
newManifest, err = ExtractGzip(ctx, reader, applyTarPatch)
case "application/x-tar+zstd":
logc.Printf(ctx, "patch %s: (tar.zst)", webRoot)
newManifest, err = ExtractZstd(ctx, reader, applyTarPatch)
default:
err = errArchiveFormat
}
if err != nil {
logc.Printf(ctx, "patch %s err: %s", webRoot, err)
result = UpdateResult{UpdateError, nil, err}
} else {
result = Update(ctx, webRoot, oldManifest, newManifest,
ModifyManifestOptions{
IfUnmodifiedSince: oldMetadata.LastModified,
IfMatch: oldMetadata.ETag,
})
// The `If-Unmodified-Since` precondition is internally generated here, which means its
// failure shouldn't be surfaced as-is in the HTTP response. If we also accepted options
// from the client, then that precondition failure should surface in the response.
if errors.Is(result.err, ErrPreconditionFailed) {
result.err = ErrWriteConflict
}
}
observeUpdateResult(result)
return
}
func observeUpdateResult(result UpdateResult) {
var unresolvedRefErr UnresolvedRefError
if errors.As(result.err, &unresolvedRefErr) {
// This error is an expected outcome of an incremental update's probe phase.
} else if errors.Is(result.err, ErrWriteConflict) {
// This error is an expected outcome of an incremental update losing a race.
} else if result.err != nil {
ObserveError(result.err)
}
}