Add Last-Modified: header to /.git-pages/ metadata responses.

This commit is contained in:
Catherine
2025-11-19 22:37:06 +00:00
parent dd16818618
commit 0e342b11f6
7 changed files with 65 additions and 48 deletions

View File

@@ -39,7 +39,9 @@ type Backend interface {
EnableFeature(ctx context.Context, feature BackendFeature) error
// Retrieve a blob. Returns `reader, size, mtime, err`.
GetBlob(ctx context.Context, name string) (reader io.ReadSeeker, size uint64, mtime time.Time, err error)
GetBlob(ctx context.Context, name string) (
reader io.ReadSeeker, size uint64, mtime time.Time, err error,
)
// Store a blob. If a blob called `name` already exists, this function returns `nil` without
// regards to the old or new contents. It is expected that blobs are content-addressed, i.e.
@@ -50,7 +52,9 @@ type Backend interface {
DeleteBlob(ctx context.Context, name string) error
// Retrieve a manifest.
GetManifest(ctx context.Context, name string, opts GetManifestOptions) (*Manifest, error)
GetManifest(ctx context.Context, name string, opts GetManifestOptions) (
manifest *Manifest, mtime time.Time, err error,
)
// Stage a manifest. This operation stores a new version of a manifest, locking any blobs
// referenced from it in place (for garbage collection purposes) but without any other side

View File

@@ -89,13 +89,9 @@ func (fs *FSBackend) EnableFeature(ctx context.Context, feature BackendFeature)
}
func (fs *FSBackend) GetBlob(
ctx context.Context,
name string,
ctx context.Context, name string,
) (
reader io.ReadSeeker,
size uint64,
mtime time.Time,
err error,
reader io.ReadSeeker, size uint64, mtime time.Time, err error,
) {
blobPath := filepath.Join(splitBlobName(name)...)
stat, err := fs.blobRoot.Stat(blobPath)
@@ -168,15 +164,29 @@ func (b *FSBackend) ListManifests(ctx context.Context) (manifests []string, err
return
}
func (fs *FSBackend) GetManifest(ctx context.Context, name string, opts GetManifestOptions) (*Manifest, error) {
data, err := fs.siteRoot.ReadFile(name)
func (fs *FSBackend) GetManifest(
ctx context.Context, name string, opts GetManifestOptions,
) (
manifest *Manifest, mtime time.Time, err error,
) {
stat, err := fs.siteRoot.Stat(name)
if errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("%w: %s", ErrObjectNotFound, err.(*os.PathError).Path)
err = fmt.Errorf("%w: %s", ErrObjectNotFound, err.(*os.PathError).Path)
return
} else if err != nil {
return nil, err
err = fmt.Errorf("stat: %w", err)
return
}
return DecodeManifest(data)
data, err := fs.siteRoot.ReadFile(name)
if err != nil {
err = fmt.Errorf("read: %w", err)
return
}
manifest, err = DecodeManifest(data)
if err != nil {
return
}
return manifest, stat.ModTime(), nil
}
func stagedManifestName(manifestData []byte) string {

View File

@@ -117,6 +117,7 @@ func (c *CachedBlob) Weight() uint32 { return uint32(len(c.blob)) }
type CachedManifest struct {
manifest *Manifest
weight uint32
mtime time.Time
etag string
err error
}
@@ -262,13 +263,9 @@ func (s3 *S3Backend) EnableFeature(ctx context.Context, feature BackendFeature)
}
func (s3 *S3Backend) GetBlob(
ctx context.Context,
name string,
ctx context.Context, name string,
) (
reader io.ReadSeeker,
size uint64,
mtime time.Time,
err error,
reader io.ReadSeeker, size uint64, mtime time.Time, err error,
) {
loader := func(ctx context.Context, name string) (*CachedBlob, error) {
log.Printf("s3: get blob %s\n", name)
@@ -434,7 +431,7 @@ func (l s3ManifestLoader) load(ctx context.Context, name string, oldManifest *Ca
With(prometheus.Labels{"kind": "manifest"}).
Observe(time.Since(startTime).Seconds())
return &CachedManifest{manifest, uint32(len(data)), stat.ETag, nil}, nil
return &CachedManifest{manifest, uint32(len(data)), stat.LastModified, stat.ETag, nil}, nil
}
var cached *CachedManifest
@@ -443,7 +440,7 @@ func (l s3ManifestLoader) load(ctx context.Context, name string, oldManifest *Ca
if errResp := minio.ToErrorResponse(err); errResp.Code == "NoSuchKey" {
s3GetObjectErrorsCount.With(prometheus.Labels{"object_kind": "manifest"}).Inc()
err = fmt.Errorf("%w: %s", ErrObjectNotFound, errResp.Key)
return &CachedManifest{nil, 1, "", err}, nil
return &CachedManifest{nil, 1, time.Time{}, "", err}, nil
} else if errResp.StatusCode == http.StatusNotModified && oldManifest != nil {
return oldManifest, nil
} else {
@@ -457,7 +454,7 @@ func (l s3ManifestLoader) load(ctx context.Context, name string, oldManifest *Ca
func (s3 *S3Backend) GetManifest(
ctx context.Context, name string, opts GetManifestOptions,
) (
manifest *Manifest, err error,
manifest *Manifest, mtime time.Time, err error,
) {
if opts.BypassCache {
entry, found := s3.siteCache.Cache.GetEntry(name)
@@ -471,8 +468,8 @@ func (s3 *S3Backend) GetManifest(
if err != nil {
return
} else {
// This could be `manifest, nil` or `nil, ErrObjectNotFound`.
manifest, err = cached.manifest, cached.err
// This could be `manifest, mtime, nil` or `nil, time.Time{}, ErrObjectNotFound`.
manifest, mtime, err = cached.manifest, cached.mtime, cached.err
return
}
}

View File

@@ -160,7 +160,7 @@ func Main() {
webRoot += "/.index"
}
manifest, err := backend.GetManifest(context.Background(), webRoot, GetManifestOptions{})
manifest, _, err := backend.GetManifest(context.Background(), webRoot, GetManifestOptions{})
if err != nil {
log.Fatalln(err)
}

View File

@@ -288,13 +288,9 @@ func (backend *observedBackend) EnableFeature(ctx context.Context, feature Backe
}
func (backend *observedBackend) GetBlob(
ctx context.Context,
name string,
ctx context.Context, name string,
) (
reader io.ReadSeeker,
size uint64,
mtime time.Time,
err error,
reader io.ReadSeeker, size uint64, mtime time.Time, err error,
) {
span, ctx := ObserveFunction(ctx, "GetBlob", "blob.name", name)
if reader, size, mtime, err = backend.inner.GetBlob(ctx, name); err == nil {
@@ -331,18 +327,15 @@ func (backend *observedBackend) ListManifests(ctx context.Context) (manifests []
}
func (backend *observedBackend) GetManifest(
ctx context.Context,
name string,
opts GetManifestOptions,
ctx context.Context, name string, opts GetManifestOptions,
) (
manifest *Manifest,
err error,
manifest *Manifest, mtime time.Time, err error,
) {
span, ctx := ObserveFunction(ctx, "GetManifest",
"manifest.name", name,
"manifest.bypass_cache", opts.BypassCache,
)
if manifest, err = backend.inner.GetManifest(ctx, name, opts); err == nil {
if manifest, mtime, err = backend.inner.GetManifest(ctx, name, opts); err == nil {
manifestsRetrievedCount.Inc()
}
span.Finish()

View File

@@ -77,6 +77,7 @@ func getPage(w http.ResponseWriter, r *http.Request) error {
var err error
var sitePath string
var manifest *Manifest
var manifestMtime time.Time
cacheControl, err := cacheobject.ParseRequestCacheControl(r.Header.Get("Cache-Control"))
if err != nil {
@@ -95,33 +96,39 @@ func getPage(w http.ResponseWriter, r *http.Request) error {
}
type indexManifestResult struct {
manifest *Manifest
err error
manifest *Manifest
manifestMtime time.Time
err error
}
indexManifestCh := make(chan indexManifestResult, 1)
go func() {
manifest, err := backend.GetManifest(r.Context(), makeWebRoot(host, ".index"),
GetManifestOptions{BypassCache: bypassCache})
indexManifestCh <- (indexManifestResult{manifest, err})
manifest, mtime, err := backend.GetManifest(
r.Context(), makeWebRoot(host, ".index"),
GetManifestOptions{BypassCache: bypassCache},
)
indexManifestCh <- (indexManifestResult{manifest, mtime, err})
}()
err = nil
sitePath = strings.TrimPrefix(r.URL.Path, "/")
if projectName, projectPath, hasProjectSlash := strings.Cut(sitePath, "/"); projectName != "" {
var projectManifest *Manifest
projectManifest, err = backend.GetManifest(r.Context(), makeWebRoot(host, projectName),
GetManifestOptions{BypassCache: bypassCache})
var projectManifestMtime time.Time
projectManifest, projectManifestMtime, err = backend.GetManifest(
r.Context(), makeWebRoot(host, projectName),
GetManifestOptions{BypassCache: bypassCache},
)
if err == nil {
if !hasProjectSlash {
writeRedirect(w, http.StatusFound, r.URL.Path+"/")
return nil
}
sitePath, manifest = projectPath, projectManifest
sitePath, manifest, manifestMtime = projectPath, projectManifest, projectManifestMtime
}
}
if manifest == nil && (err == nil || errors.Is(err, ErrObjectNotFound)) {
result := <-indexManifestCh
manifest, err = result.manifest, result.err
manifest, manifestMtime, err = result.manifest, result.manifestMtime, result.err
if manifest == nil && errors.Is(err, ErrObjectNotFound) {
if found, fallbackErr := HandleWildcardFallback(w, r); found {
return fallbackErr
@@ -151,10 +158,13 @@ func getPage(w http.ResponseWriter, r *http.Request) error {
return nil
}
if metadataPath, found := strings.CutPrefix(sitePath, ".git-pages/"); found {
lastModified := manifestMtime.UTC().Format(http.TimeFormat)
switch metadataPath {
case "health":
w.Header().Add("Last-Modified", lastModified)
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "ok\n")
case "manifest.json":
// metadata requests require authorization to avoid making pushes from private
// repositories enumerable
@@ -162,9 +172,12 @@ func getPage(w http.ResponseWriter, r *http.Request) error {
if err != nil {
return err
}
w.Header().Add("Content-Type", "application/json; charset=utf-8")
w.Header().Add("Last-Modified", lastModified)
w.WriteHeader(http.StatusOK)
w.Write([]byte(ManifestDebugJSON(manifest)))
default:
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "not found\n")

View File

@@ -31,7 +31,7 @@ func Update(ctx context.Context, webRoot string, manifest *Manifest) UpdateResul
var err error
outcome := UpdateError
oldManifest, _ = backend.GetManifest(ctx, webRoot, GetManifestOptions{})
oldManifest, _, _ = backend.GetManifest(ctx, webRoot, GetManifestOptions{})
if IsManifestEmpty(manifest) {
newManifest, err = manifest, backend.DeleteManifest(ctx, webRoot)
if err == nil {