[mirror] Scalable static site server for Git forges (like GitHub Pages)

Add `EnumerateManifests` API and `-list-manifests` option.

The new API replaces the `ListManifests` API.

This also adds `Name` and `Size` to manifest metadata.

+5 -2
src/backend.go
··· 46 46 } 47 47 48 48 type ManifestMetadata struct { 49 + Name string 50 + Size int64 49 51 LastModified time.Time 50 52 ETag string 51 53 } ··· 122 124 // Delete a manifest. 123 125 DeleteManifest(ctx context.Context, name string, opts ModifyManifestOptions) error 124 126 125 - // List all manifests. 126 - ListManifests(ctx context.Context) (manifests []string, err error) 127 + // Iterate through all manifests. Whether manifests that are newly added during iteration 128 + // will appear in the results is unspecified. 129 + EnumerateManifests(ctx context.Context) iter.Seq2[ManifestMetadata, error] 127 130 128 131 // Check whether a domain has any deployments. 129 132 CheckDomain(ctx context.Context, domain string) (found bool, err error)
+31 -16
src/backend_fs.go
··· 207 207 } 208 208 } 209 209 210 - func (fs *FSBackend) ListManifests(ctx context.Context) (manifests []string, err error) { 211 - err = iofs.WalkDir(fs.siteRoot.FS(), ".", 212 - func(path string, entry iofs.DirEntry, err error) error { 213 - if strings.Count(path, "/") > 1 { 214 - return iofs.SkipDir 215 - } 216 - _, project, _ := strings.Cut(path, "/") 217 - if project == "" || strings.HasPrefix(project, ".") && project != ".index" { 218 - return nil 219 - } 220 - manifests = append(manifests, path) 221 - return nil 222 - }) 223 - return 224 - } 225 - 226 210 func (fs *FSBackend) GetManifest( 227 211 ctx context.Context, name string, opts GetManifestOptions, 228 212 ) ( ··· 409 393 return nil 410 394 } else { 411 395 return err 396 + } 397 + } 398 + 399 + func (fs *FSBackend) EnumerateManifests(ctx context.Context) iter.Seq2[ManifestMetadata, error] { 400 + return func(yield func(ManifestMetadata, error) bool) { 401 + iofs.WalkDir(fs.siteRoot.FS(), ".", 402 + func(path string, entry iofs.DirEntry, err error) error { 403 + _, project, _ := strings.Cut(path, "/") 404 + var metadata ManifestMetadata 405 + if err != nil { 406 + // report error 407 + } else if entry.IsDir() { 408 + // skip directory 409 + return nil 410 + } else if project == "" || strings.HasPrefix(project, ".") && project != ".index" { 411 + // skip internal 412 + return nil 413 + } else if info, err := entry.Info(); err != nil { 414 + // report error 415 + } else { 416 + // report blob 417 + metadata.Name = path 418 + metadata.Size = info.Size() 419 + metadata.LastModified = info.ModTime() 420 + // not setting metadata.ETag since it is too costly 421 + } 422 + if !yield(metadata, err) { 423 + return iofs.SkipAll 424 + } 425 + return nil 426 + }) 412 427 } 413 428 } 414 429
+35 -28
src/backend_s3.go
··· 397 397 return fmt.Sprintf("dirty/%x", sha256.Sum256(manifestData)) 398 398 } 399 399 400 - func (s3 *S3Backend) ListManifests(ctx context.Context) (manifests []string, err error) { 401 - logc.Print(ctx, "s3: list manifests") 402 - 403 - ctx, cancel := context.WithCancel(ctx) 404 - defer cancel() 405 - 406 - prefix := manifestObjectName("") 407 - for object := range s3.client.ListObjectsIter(ctx, s3.bucket, minio.ListObjectsOptions{ 408 - Prefix: prefix, 409 - Recursive: true, 410 - }) { 411 - if object.Err != nil { 412 - return nil, object.Err 413 - } 414 - key := strings.TrimRight(strings.TrimPrefix(object.Key, prefix), "/") 415 - if strings.Count(key, "/") > 1 { 416 - continue 417 - } 418 - _, project, _ := strings.Cut(key, "/") 419 - if project == "" || strings.HasPrefix(project, ".") && project != ".index" { 420 - continue 421 - } 422 - manifests = append(manifests, key) 423 - } 424 - 425 - return 426 - } 427 - 428 400 type s3ManifestLoader struct { 429 401 s3 *S3Backend 430 402 } ··· 666 638 minio.RemoveObjectOptions{}) 667 639 s3.siteCache.Cache.Invalidate(name) 668 640 return err 641 + } 642 + 643 + func (s3 *S3Backend) EnumerateManifests(ctx context.Context) iter.Seq2[ManifestMetadata, error] { 644 + return func(yield func(ManifestMetadata, error) bool) { 645 + logc.Print(ctx, "s3: enumerate manifests") 646 + 647 + ctx, cancel := context.WithCancel(ctx) 648 + defer cancel() 649 + 650 + prefix := "site/" 651 + for object := range s3.client.ListObjectsIter(ctx, s3.bucket, minio.ListObjectsOptions{ 652 + Prefix: prefix, 653 + Recursive: true, 654 + }) { 655 + var metadata ManifestMetadata 656 + var err error 657 + if err = object.Err; err == nil { 658 + key := strings.TrimPrefix(object.Key, prefix) 659 + _, project, _ := strings.Cut(key, "/") 660 + if strings.HasSuffix(key, "/") { 661 + continue // directory; skip 662 + } else if project == "" || strings.HasPrefix(project, ".") && project != ".index" { 663 + continue // internal; skip 664 + } else { 665 + metadata.Name = key 666 + metadata.Size = object.Size 667 + metadata.LastModified = object.LastModified 668 + metadata.ETag = object.ETag 669 + } 670 + } 671 + if !yield(metadata, err) { 672 + break 673 + } 674 + } 675 + } 669 676 } 670 677 671 678 func domainCheckObjectName(domain string) string {
+16 -1
src/main.go
··· 171 171 fmt.Fprintf(os.Stderr, "(server) "+ 172 172 "git-pages [-config <file>|-no-config]\n") 173 173 fmt.Fprintf(os.Stderr, "(debug) "+ 174 - "git-pages {-list-blobs}\n") 174 + "git-pages {-list-blobs|-list-manifests}\n") 175 175 fmt.Fprintf(os.Stderr, "(debug) "+ 176 176 "git-pages {-get-blob|-get-manifest|-get-archive|-update-site} <ref> [file]\n") 177 177 fmt.Fprintf(os.Stderr, "(admin) "+ ··· 203 203 "enumerate every blob with its metadata") 204 204 getManifest := flag.String("get-manifest", "", 205 205 "write manifest for `site` (either 'domain.tld' or 'domain.tld/dir') as ProtoJSON") 206 + listManifests := flag.Bool("list-manifests", false, 207 + "enumerate every manifest with its metadata") 206 208 getArchive := flag.String("get-archive", "", 207 209 "write archive for `site` (either 'domain.tld' or 'domain.tld/dir') in tar format") 208 210 updateSite := flag.String("update-site", "", ··· 225 227 *getBlob != "", 226 228 *listBlobs, 227 229 *getManifest != "", 230 + *listManifests, 228 231 *getArchive != "", 229 232 *updateSite != "", 230 233 *freezeDomain != "", ··· 316 319 logc.Fatalln(ctx, err) 317 320 } 318 321 fmt.Fprintln(fileOutputArg(), string(ManifestJSON(manifest))) 322 + 323 + case *listManifests: 324 + for metadata, err := range backend.EnumerateManifests(ctx) { 325 + if err != nil { 326 + logc.Fatalln(ctx, err) 327 + } 328 + fmt.Fprintf(color.Output, "%s %s %s\n", 329 + metadata.Name, 330 + color.HiWhiteString(metadata.LastModified.UTC().Format(time.RFC3339)), 331 + color.HiGreenString(fmt.Sprint(metadata.Size)), 332 + ) 333 + } 319 334 320 335 case *getArchive != "": 321 336 webRoot := webRootArg(*getArchive)
+7 -4
src/migrate.go
··· 22 22 return nil 23 23 } 24 24 25 - var manifests, domains []string 26 - manifests, err := backend.ListManifests(ctx) 27 - if err != nil { 28 - return fmt.Errorf("list manifests: %w", err) 25 + var manifests []string 26 + for metadata, err := range backend.EnumerateManifests(ctx) { 27 + if err != nil { 28 + return fmt.Errorf("enum manifests: %w", err) 29 + } 30 + manifests = append(manifests, metadata.Name) 29 31 } 30 32 slices.Sort(manifests) 33 + var domains []string 31 34 for _, manifest := range manifests { 32 35 domain, _, _ := strings.Cut(manifest, "/") 33 36 if len(domains) == 0 || domains[len(domains)-1] != domain {
+12 -7
src/observe.go
··· 385 385 } 386 386 } 387 387 388 - func (backend *observedBackend) ListManifests(ctx context.Context) (manifests []string, err error) { 389 - span, ctx := ObserveFunction(ctx, "ListManifests") 390 - manifests, err = backend.inner.ListManifests(ctx) 391 - span.Finish() 392 - return 393 - } 394 - 395 388 func (backend *observedBackend) GetManifest( 396 389 ctx context.Context, name string, opts GetManifestOptions, 397 390 ) ( ··· 431 424 err = backend.inner.DeleteManifest(ctx, name, opts) 432 425 span.Finish() 433 426 return 427 + } 428 + 429 + func (backend *observedBackend) EnumerateManifests(ctx context.Context) iter.Seq2[ManifestMetadata, error] { 430 + return func(yield func(ManifestMetadata, error) bool) { 431 + span, ctx := ObserveFunction(ctx, "EnumerateManifests") 432 + for metadata, err := range backend.inner.EnumerateManifests(ctx) { 433 + if !yield(metadata, err) { 434 + break 435 + } 436 + } 437 + span.Finish() 438 + } 434 439 } 435 440 436 441 func (backend *observedBackend) CheckDomain(ctx context.Context, domain string) (found bool, err error) {