[mirror] Scalable static site server for Git forges (like GitHub Pages)
1package git_pages
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "io"
8 "log"
9 "strings"
10)
11
12type UpdateOutcome int
13
14const (
15 UpdateError UpdateOutcome = iota
16 UpdateTimeout
17 UpdateCreated
18 UpdateReplaced
19 UpdateDeleted
20 UpdateNoChange
21)
22
23type UpdateResult struct {
24 outcome UpdateOutcome
25 manifest *Manifest
26 err error
27}
28
29func Update(ctx context.Context, webRoot string, manifest *Manifest) UpdateResult {
30 var oldManifest, newManifest *Manifest
31 var err error
32
33 outcome := UpdateError
34 oldManifest, _, _ = backend.GetManifest(ctx, webRoot, GetManifestOptions{})
35 if IsManifestEmpty(manifest) {
36 newManifest, err = manifest, backend.DeleteManifest(ctx, webRoot)
37 if err == nil {
38 if oldManifest == nil {
39 outcome = UpdateNoChange
40 } else {
41 outcome = UpdateDeleted
42 }
43 }
44 } else if err = PrepareManifest(ctx, manifest); err == nil {
45 newManifest, err = StoreManifest(ctx, webRoot, manifest)
46 if err == nil {
47 domain, _, _ := strings.Cut(webRoot, "/")
48 err = backend.CreateDomain(ctx, domain)
49 }
50 if err == nil {
51 if oldManifest == nil {
52 outcome = UpdateCreated
53 } else if CompareManifest(oldManifest, newManifest) {
54 outcome = UpdateNoChange
55 } else {
56 outcome = UpdateReplaced
57 }
58 }
59 }
60
61 if err == nil {
62 status := ""
63 switch outcome {
64 case UpdateCreated:
65 status = "created"
66 case UpdateReplaced:
67 status = "replaced"
68 case UpdateDeleted:
69 status = "deleted"
70 case UpdateNoChange:
71 status = "unchanged"
72 }
73 if newManifest.Commit != nil {
74 log.Printf("update %s ok: %s %s", webRoot, status, *newManifest.Commit)
75 } else {
76 log.Printf("update %s ok: %s", webRoot, status)
77 }
78 } else {
79 log.Printf("update %s err: %s", webRoot, err)
80 }
81
82 return UpdateResult{outcome, newManifest, err}
83}
84
85func UpdateFromRepository(
86 ctx context.Context,
87 webRoot string,
88 repoURL string,
89 branch string,
90) (result UpdateResult) {
91 span, ctx := ObserveFunction(ctx, "UpdateFromRepository", "repo.url", repoURL)
92 defer span.Finish()
93
94 log.Printf("update %s: %s %s\n", webRoot, repoURL, branch)
95
96 manifest, err := FetchRepository(ctx, repoURL, branch)
97 if errors.Is(err, context.DeadlineExceeded) {
98 result = UpdateResult{UpdateTimeout, nil, fmt.Errorf("update timeout")}
99 } else if err != nil {
100 result = UpdateResult{UpdateError, nil, err}
101 } else {
102 result = Update(ctx, webRoot, manifest)
103 }
104
105 observeUpdateResult(result)
106 return result
107}
108
109var errArchiveFormat = errors.New("unsupported archive format")
110
111func UpdateFromArchive(
112 ctx context.Context,
113 webRoot string,
114 contentType string,
115 reader io.Reader,
116) (result UpdateResult) {
117 var manifest *Manifest
118 var err error
119
120 switch contentType {
121 case "application/x-tar":
122 log.Printf("update %s: (tar)", webRoot)
123 manifest, err = ExtractTar(reader) // yellow?
124 case "application/x-tar+gzip":
125 log.Printf("update %s: (tar.gz)", webRoot)
126 manifest, err = ExtractTarGzip(reader) // definitely yellow.
127 case "application/x-tar+zstd":
128 log.Printf("update %s: (tar.zst)", webRoot)
129 manifest, err = ExtractTarZstd(reader)
130 case "application/zip":
131 log.Printf("update %s: (zip)", webRoot)
132 manifest, err = ExtractZip(reader)
133 default:
134 err = errArchiveFormat
135 }
136
137 if err != nil {
138 log.Printf("update %s err: %s", webRoot, err)
139 result = UpdateResult{UpdateError, nil, err}
140 } else {
141 result = Update(ctx, webRoot, manifest)
142 }
143
144 observeUpdateResult(result)
145 return
146}
147
148func observeUpdateResult(result UpdateResult) {
149 if result.err != nil {
150 ObserveError(result.err)
151 }
152}