[mirror] Scalable static site server for Git forges (like GitHub Pages)
at main 6.8 kB view raw
1package git_pages 2 3import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "strings" 9 10 "google.golang.org/protobuf/proto" 11) 12 13type UpdateOutcome int 14 15const ( 16 UpdateError UpdateOutcome = iota 17 UpdateTimeout 18 UpdateCreated 19 UpdateReplaced 20 UpdateDeleted 21 UpdateNoChange 22) 23 24type UpdateResult struct { 25 outcome UpdateOutcome 26 manifest *Manifest 27 err error 28} 29 30func Update( 31 ctx context.Context, webRoot string, oldManifest, newManifest *Manifest, 32 opts ModifyManifestOptions, 33) UpdateResult { 34 var err error 35 var storedManifest *Manifest 36 37 outcome := UpdateError 38 if IsManifestEmpty(newManifest) { 39 storedManifest, err = newManifest, backend.DeleteManifest(ctx, webRoot, opts) 40 if err == nil { 41 if oldManifest == nil { 42 outcome = UpdateNoChange 43 } else { 44 outcome = UpdateDeleted 45 } 46 } 47 } else if err = PrepareManifest(ctx, newManifest); err == nil { 48 storedManifest, err = StoreManifest(ctx, webRoot, newManifest, opts) 49 if err == nil { 50 domain, _, _ := strings.Cut(webRoot, "/") 51 err = backend.CreateDomain(ctx, domain) 52 } 53 if err == nil { 54 if oldManifest == nil { 55 outcome = UpdateCreated 56 } else if CompareManifest(oldManifest, storedManifest) { 57 outcome = UpdateNoChange 58 } else { 59 outcome = UpdateReplaced 60 } 61 } 62 } 63 64 if err == nil { 65 status := "" 66 switch outcome { 67 case UpdateCreated: 68 status = "created" 69 case UpdateReplaced: 70 status = "replaced" 71 case UpdateDeleted: 72 status = "deleted" 73 case UpdateNoChange: 74 status = "unchanged" 75 } 76 if storedManifest.Commit != nil { 77 logc.Printf(ctx, "update %s ok: %s %s", webRoot, *storedManifest.Commit, status) 78 } else { 79 logc.Printf(ctx, "update %s ok: %s", webRoot, status) 80 } 81 } else { 82 logc.Printf(ctx, "update %s err: %s", webRoot, err) 83 } 84 85 return UpdateResult{outcome, storedManifest, err} 86} 87 88func UpdateFromRepository( 89 ctx context.Context, 90 webRoot string, 91 repoURL string, 92 branch string, 93) (result UpdateResult) { 94 span, ctx := ObserveFunction(ctx, "UpdateFromRepository", "repo.url", repoURL) 95 defer span.Finish() 96 97 logc.Printf(ctx, "update %s: %s %s\n", webRoot, repoURL, branch) 98 99 // Ignore errors; worst case we have to re-fetch all of the blobs. 100 oldManifest, _, _ := backend.GetManifest(ctx, webRoot, GetManifestOptions{}) 101 102 newManifest, err := FetchRepository(ctx, repoURL, branch, oldManifest) 103 if errors.Is(err, context.DeadlineExceeded) { 104 result = UpdateResult{UpdateTimeout, nil, fmt.Errorf("update timeout")} 105 } else if err != nil { 106 result = UpdateResult{UpdateError, nil, err} 107 } else { 108 result = Update(ctx, webRoot, oldManifest, newManifest, ModifyManifestOptions{}) 109 } 110 111 observeUpdateResult(result) 112 return result 113} 114 115var errArchiveFormat = errors.New("unsupported archive format") 116 117func UpdateFromArchive( 118 ctx context.Context, 119 webRoot string, 120 contentType string, 121 reader io.Reader, 122) (result UpdateResult) { 123 var err error 124 125 // Ignore errors; worst case we have to re-fetch all of the blobs. 126 oldManifest, _, _ := backend.GetManifest(ctx, webRoot, GetManifestOptions{}) 127 128 extractTar := func(ctx context.Context, reader io.Reader) (*Manifest, error) { 129 return ExtractTar(ctx, reader, oldManifest) 130 } 131 132 var newManifest *Manifest 133 switch contentType { 134 case "application/x-tar": 135 logc.Printf(ctx, "update %s: (tar)", webRoot) 136 newManifest, err = extractTar(ctx, reader) // yellow? 137 case "application/x-tar+gzip": 138 logc.Printf(ctx, "update %s: (tar.gz)", webRoot) 139 newManifest, err = ExtractGzip(ctx, reader, extractTar) // definitely yellow. 140 case "application/x-tar+zstd": 141 logc.Printf(ctx, "update %s: (tar.zst)", webRoot) 142 newManifest, err = ExtractZstd(ctx, reader, extractTar) 143 case "application/zip": 144 logc.Printf(ctx, "update %s: (zip)", webRoot) 145 newManifest, err = ExtractZip(ctx, reader, oldManifest) 146 default: 147 err = errArchiveFormat 148 } 149 150 if err != nil { 151 logc.Printf(ctx, "update %s err: %s", webRoot, err) 152 result = UpdateResult{UpdateError, nil, err} 153 } else { 154 result = Update(ctx, webRoot, oldManifest, newManifest, ModifyManifestOptions{}) 155 } 156 157 observeUpdateResult(result) 158 return 159} 160 161func PartialUpdateFromArchive( 162 ctx context.Context, 163 webRoot string, 164 contentType string, 165 reader io.Reader, 166 parents CreateParentsMode, 167) (result UpdateResult) { 168 var err error 169 170 // Here the old manifest is used both as a substrate to which a patch is applied, as well 171 // as a "load linked" operation for a future "store conditional" update which, taken together, 172 // create an atomic compare-and-swap operation. 173 oldManifest, oldMetadata, err := backend.GetManifest(ctx, webRoot, 174 GetManifestOptions{BypassCache: true}) 175 if err != nil { 176 logc.Printf(ctx, "patch %s err: %s", webRoot, err) 177 return UpdateResult{UpdateError, nil, err} 178 } 179 180 applyTarPatch := func(ctx context.Context, reader io.Reader) (*Manifest, error) { 181 // Clone the manifest before starting to mutate it. `GetManifest` may return cached 182 // `*Manifest` objects, which should never be mutated. 183 newManifest := &Manifest{} 184 proto.Merge(newManifest, oldManifest) 185 newManifest.RepoUrl = nil 186 newManifest.Branch = nil 187 newManifest.Commit = nil 188 if err := ApplyTarPatch(newManifest, reader, parents); err != nil { 189 return nil, err 190 } else { 191 return newManifest, nil 192 } 193 } 194 195 var newManifest *Manifest 196 switch contentType { 197 case "application/x-tar": 198 logc.Printf(ctx, "patch %s: (tar)", webRoot) 199 newManifest, err = applyTarPatch(ctx, reader) 200 case "application/x-tar+gzip": 201 logc.Printf(ctx, "patch %s: (tar.gz)", webRoot) 202 newManifest, err = ExtractGzip(ctx, reader, applyTarPatch) 203 case "application/x-tar+zstd": 204 logc.Printf(ctx, "patch %s: (tar.zst)", webRoot) 205 newManifest, err = ExtractZstd(ctx, reader, applyTarPatch) 206 default: 207 err = errArchiveFormat 208 } 209 210 if err != nil { 211 logc.Printf(ctx, "patch %s err: %s", webRoot, err) 212 result = UpdateResult{UpdateError, nil, err} 213 } else { 214 result = Update(ctx, webRoot, oldManifest, newManifest, 215 ModifyManifestOptions{ 216 IfUnmodifiedSince: oldMetadata.LastModified, 217 IfMatch: oldMetadata.ETag, 218 }) 219 // The `If-Unmodified-Since` precondition is internally generated here, which means its 220 // failure shouldn't be surfaced as-is in the HTTP response. If we also accepted options 221 // from the client, then that precondition failure should surface in the response. 222 if errors.Is(result.err, ErrPreconditionFailed) { 223 result.err = ErrWriteConflict 224 } 225 } 226 227 observeUpdateResult(result) 228 return 229} 230 231func observeUpdateResult(result UpdateResult) { 232 var unresolvedRefErr UnresolvedRefError 233 if errors.As(result.err, &unresolvedRefErr) { 234 // This error is an expected outcome of an incremental update's probe phase. 235 } else if errors.Is(result.err, ErrWriteConflict) { 236 // This error is an expected outcome of an incremental update losing a race. 237 } else if result.err != nil { 238 ObserveError(result.err) 239 } 240}