+1
-1
src/backend_fs.go
+1
-1
src/backend_fs.go
···
212
return filepath.Join(domain, ".frozen")
213
}
214
215
-
func (fs *FSBackend) checkDomainFrozen(_ctx context.Context, domain string) error {
216
if _, err := fs.siteRoot.Stat(domainFrozenMarkerName(domain)); err == nil {
217
return ErrDomainFrozen
218
} else if !errors.Is(err, os.ErrNotExist) {
···
212
return filepath.Join(domain, ".frozen")
213
}
214
215
+
func (fs *FSBackend) checkDomainFrozen(ctx context.Context, domain string) error {
216
if _, err := fs.siteRoot.Stat(domainFrozenMarkerName(domain)); err == nil {
217
return ErrDomainFrozen
218
} else if !errors.Is(err, os.ErrNotExist) {
+13
-2
src/log.go
+13
-2
src/log.go
···
4
"context"
5
"fmt"
6
"log/slog"
7
"runtime"
8
"time"
9
)
···
25
// skip [runtime.Callers, this method, method calling this method]
26
runtime.Callers(3, pcs[:])
27
28
-
r := slog.NewRecord(time.Now(), level, msg, pcs[0])
29
-
logger.Handler().Handle(ctx, r)
30
}
31
32
func (l slogWithCtx) Print(ctx context.Context, v ...any) {
···
40
func (l slogWithCtx) Println(ctx context.Context, v ...any) {
41
l.log(ctx, slog.LevelInfo, fmt.Sprintln(v...))
42
}
···
4
"context"
5
"fmt"
6
"log/slog"
7
+
"os"
8
"runtime"
9
"time"
10
)
···
26
// skip [runtime.Callers, this method, method calling this method]
27
runtime.Callers(3, pcs[:])
28
29
+
record := slog.NewRecord(time.Now(), level, msg, pcs[0])
30
+
logger.Handler().Handle(ctx, record)
31
}
32
33
func (l slogWithCtx) Print(ctx context.Context, v ...any) {
···
41
func (l slogWithCtx) Println(ctx context.Context, v ...any) {
42
l.log(ctx, slog.LevelInfo, fmt.Sprintln(v...))
43
}
44
+
45
+
func (l slogWithCtx) Fatalf(ctx context.Context, format string, v ...any) {
46
+
l.log(ctx, slog.LevelError, fmt.Sprintf(format, v...))
47
+
os.Exit(1)
48
+
}
49
+
50
+
func (l slogWithCtx) Fatalln(ctx context.Context, v ...any) {
51
+
l.log(ctx, slog.LevelError, fmt.Sprintln(v...))
52
+
os.Exit(1)
53
+
}
+70
-67
src/main.go
+70
-67
src/main.go
···
27
var fallback http.Handler
28
var backend Backend
29
30
-
func configureFeatures() (err error) {
31
if len(config.Features) > 0 {
32
-
log.Println("features:", strings.Join(config.Features, ", "))
33
}
34
return
35
}
36
37
-
func configureMemLimit() (err error) {
38
// Avoid being OOM killed by not garbage collecting early enough.
39
memlimitBefore := datasize.ByteSize(debug.SetMemoryLimit(-1))
40
automemlimit.SetGoMemLimitWithOpts(
···
49
)
50
memlimitAfter := datasize.ByteSize(debug.SetMemoryLimit(-1))
51
if memlimitBefore == memlimitAfter {
52
-
log.Println("memlimit: now", memlimitBefore.HR())
53
} else {
54
-
log.Println("memlimit: was", memlimitBefore.HR(), "now", memlimitAfter.HR())
55
}
56
return
57
}
58
59
-
func configureWildcards() (err error) {
60
newWildcards, err := TranslateWildcards(config.Wildcard)
61
if err != nil {
62
return err
···
66
}
67
}
68
69
-
func configureFallback() (err error) {
70
if config.Fallback.ProxyTo != "" {
71
var fallbackURL *url.URL
72
fallbackURL, err = url.Parse(config.Fallback.ProxyTo)
···
91
return
92
}
93
94
-
func listen(name string, listen string) net.Listener {
95
if listen == "-" {
96
return nil
97
}
98
99
protocol, address, ok := strings.Cut(listen, "/")
100
if !ok {
101
-
log.Fatalf("%s: %s: malformed endpoint", name, listen)
102
}
103
104
listener, err := net.Listen(protocol, address)
105
if err != nil {
106
-
log.Fatalf("%s: %s\n", name, err)
107
}
108
109
return listener
···
125
})
126
}
127
128
-
func serve(listener net.Listener, handler http.Handler) {
129
if listener != nil {
130
handler = panicHandler(handler)
131
···
135
if config.Feature("serve-h2c") {
136
server.Protocols.SetUnencryptedHTTP2(true)
137
}
138
-
log.Fatalln(server.Serve(listener))
139
}
140
}
141
···
146
case 1:
147
return arg
148
default:
149
-
log.Fatalf("webroot argument must be either 'domain.tld' or 'domain.tld/dir")
150
return ""
151
}
152
}
···
158
} else {
159
writer, err = os.Create(flag.Arg(0))
160
if err != nil {
161
-
log.Fatalln(err)
162
}
163
}
164
return
···
178
}
179
180
func Main() {
181
flag.Usage = usage
182
printConfigEnvVars := flag.Bool("print-config-env-vars", false,
183
"print every recognized configuration environment variable and exit")
···
226
cliOperations += 1
227
}
228
if cliOperations > 1 {
229
-
log.Fatalln("-get-blob, -get-manifest, -get-archive, -update-site, -freeze, and -unfreeze are mutually exclusive")
230
}
231
232
if *configTomlPath != "" && *noConfig {
233
-
log.Fatalln("-no-config and -config are mutually exclusive")
234
}
235
236
if *printConfigEnvVars {
···
243
*configTomlPath = "config.toml"
244
}
245
if config, err = Configure(*configTomlPath); err != nil {
246
-
log.Fatalln("config:", err)
247
}
248
249
if *printConfig {
···
255
defer FiniObservability()
256
257
if err = errors.Join(
258
-
configureFeatures(),
259
-
configureMemLimit(),
260
-
configureWildcards(),
261
-
configureFallback(),
262
); err != nil {
263
-
log.Fatalln(err)
264
}
265
266
switch {
267
case *runMigration != "":
268
if backend, err = CreateBackend(&config.Storage); err != nil {
269
-
log.Fatalln(err)
270
}
271
272
-
if err := RunMigration(context.Background(), *runMigration); err != nil {
273
-
log.Fatalln(err)
274
}
275
276
case *getBlob != "":
277
if backend, err = CreateBackend(&config.Storage); err != nil {
278
-
log.Fatalln(err)
279
}
280
281
-
reader, _, _, err := backend.GetBlob(context.Background(), *getBlob)
282
if err != nil {
283
-
log.Fatalln(err)
284
}
285
io.Copy(fileOutputArg(), reader)
286
287
case *getManifest != "":
288
if backend, err = CreateBackend(&config.Storage); err != nil {
289
-
log.Fatalln(err)
290
}
291
292
webRoot := webRootArg(*getManifest)
293
-
manifest, _, err := backend.GetManifest(context.Background(), webRoot, GetManifestOptions{})
294
if err != nil {
295
-
log.Fatalln(err)
296
}
297
fmt.Fprintln(fileOutputArg(), ManifestDebugJSON(manifest))
298
299
case *getArchive != "":
300
if backend, err = CreateBackend(&config.Storage); err != nil {
301
-
log.Fatalln(err)
302
}
303
304
webRoot := webRootArg(*getArchive)
305
manifest, manifestMtime, err :=
306
-
backend.GetManifest(context.Background(), webRoot, GetManifestOptions{})
307
if err != nil {
308
-
log.Fatalln(err)
309
}
310
-
CollectTar(context.Background(), fileOutputArg(), manifest, manifestMtime)
311
312
case *updateSite != "":
313
if backend, err = CreateBackend(&config.Storage); err != nil {
314
-
log.Fatalln(err)
315
}
316
317
if flag.NArg() != 1 {
318
-
log.Fatalln("update source must be provided as the argument")
319
}
320
321
sourceURL, err := url.Parse(flag.Arg(0))
322
if err != nil {
323
-
log.Fatalln(err)
324
}
325
326
var result UpdateResult
327
if sourceURL.Scheme == "" {
328
file, err := os.Open(sourceURL.Path)
329
if err != nil {
330
-
log.Fatalln(err)
331
}
332
defer file.Close()
333
···
346
}
347
348
webRoot := webRootArg(*updateSite)
349
-
result = UpdateFromArchive(context.Background(), webRoot, contentType, file)
350
} else {
351
branch := "pages"
352
if sourceURL.Fragment != "" {
···
354
}
355
356
webRoot := webRootArg(*updateSite)
357
-
result = UpdateFromRepository(context.Background(), webRoot, sourceURL.String(), branch)
358
}
359
360
switch result.outcome {
361
case UpdateError:
362
-
log.Printf("error: %s\n", result.err)
363
os.Exit(2)
364
case UpdateTimeout:
365
-
log.Println("timeout")
366
os.Exit(1)
367
case UpdateCreated:
368
-
log.Println("created")
369
case UpdateReplaced:
370
-
log.Println("replaced")
371
case UpdateDeleted:
372
-
log.Println("deleted")
373
case UpdateNoChange:
374
-
log.Println("no-change")
375
}
376
377
case *freezeDomain != "" || *unfreezeDomain != "":
···
386
}
387
388
if backend, err = CreateBackend(&config.Storage); err != nil {
389
-
log.Fatalln(err)
390
}
391
392
-
if err = backend.FreezeDomain(context.Background(), domain, freeze); err != nil {
393
-
log.Fatalln(err)
394
}
395
if freeze {
396
log.Println("frozen")
···
408
// The backend is not recreated (this is intentional as it allows preserving the cache).
409
OnReload(func() {
410
if newConfig, err := Configure(*configTomlPath); err != nil {
411
-
log.Println("config: reload err:", err)
412
} else {
413
// From https://go.dev/ref/mem:
414
// > A read r of a memory location x holding a value that is not larger than
···
418
// > concurrent write.
419
config = newConfig
420
if err = errors.Join(
421
-
configureFeatures(),
422
-
configureMemLimit(),
423
-
configureWildcards(),
424
-
configureFallback(),
425
); err != nil {
426
// At this point the configuration is in an in-between, corrupted state, so
427
// the only reasonable choice is to crash.
428
-
log.Fatalln("config: reload fail:", err)
429
} else {
430
-
log.Println("config: reload ok")
431
}
432
}
433
})
···
436
// spends some time initializing (which the S3 backend does) a proxy like Caddy can race
437
// with git-pages on startup and return errors for requests that would have been served
438
// just 0.5s later.
439
-
pagesListener := listen("pages", config.Server.Pages)
440
-
caddyListener := listen("caddy", config.Server.Caddy)
441
-
metricsListener := listen("metrics", config.Server.Metrics)
442
443
if backend, err = CreateBackend(&config.Storage); err != nil {
444
-
log.Fatalln(err)
445
}
446
backend = NewObservedBackend(backend)
447
448
-
go serve(pagesListener, ObserveHTTPHandler(http.HandlerFunc(ServePages)))
449
-
go serve(caddyListener, ObserveHTTPHandler(http.HandlerFunc(ServeCaddy)))
450
-
go serve(metricsListener, promhttp.Handler())
451
452
if config.Insecure {
453
-
log.Println("serve: ready (INSECURE)")
454
} else {
455
-
log.Println("serve: ready")
456
}
457
458
WaitForInterrupt()
459
-
log.Println("serve: exiting")
460
}
461
}
···
27
var fallback http.Handler
28
var backend Backend
29
30
+
func configureFeatures(ctx context.Context) (err error) {
31
if len(config.Features) > 0 {
32
+
logc.Println(ctx, "features:", strings.Join(config.Features, ", "))
33
}
34
return
35
}
36
37
+
func configureMemLimit(ctx context.Context) (err error) {
38
// Avoid being OOM killed by not garbage collecting early enough.
39
memlimitBefore := datasize.ByteSize(debug.SetMemoryLimit(-1))
40
automemlimit.SetGoMemLimitWithOpts(
···
49
)
50
memlimitAfter := datasize.ByteSize(debug.SetMemoryLimit(-1))
51
if memlimitBefore == memlimitAfter {
52
+
logc.Println(ctx, "memlimit: now", memlimitBefore.HR())
53
} else {
54
+
logc.Println(ctx, "memlimit: was", memlimitBefore.HR(), "now", memlimitAfter.HR())
55
}
56
return
57
}
58
59
+
func configureWildcards(_ context.Context) (err error) {
60
newWildcards, err := TranslateWildcards(config.Wildcard)
61
if err != nil {
62
return err
···
66
}
67
}
68
69
+
func configureFallback(_ context.Context) (err error) {
70
if config.Fallback.ProxyTo != "" {
71
var fallbackURL *url.URL
72
fallbackURL, err = url.Parse(config.Fallback.ProxyTo)
···
91
return
92
}
93
94
+
func listen(ctx context.Context, name string, listen string) net.Listener {
95
if listen == "-" {
96
return nil
97
}
98
99
protocol, address, ok := strings.Cut(listen, "/")
100
if !ok {
101
+
logc.Fatalf(ctx, "%s: %s: malformed endpoint", name, listen)
102
}
103
104
listener, err := net.Listen(protocol, address)
105
if err != nil {
106
+
logc.Fatalf(ctx, "%s: %s\n", name, err)
107
}
108
109
return listener
···
125
})
126
}
127
128
+
func serve(ctx context.Context, listener net.Listener, handler http.Handler) {
129
if listener != nil {
130
handler = panicHandler(handler)
131
···
135
if config.Feature("serve-h2c") {
136
server.Protocols.SetUnencryptedHTTP2(true)
137
}
138
+
logc.Fatalln(ctx, server.Serve(listener))
139
}
140
}
141
···
146
case 1:
147
return arg
148
default:
149
+
logc.Fatalln(context.Background(),
150
+
"webroot argument must be either 'domain.tld' or 'domain.tld/dir")
151
return ""
152
}
153
}
···
159
} else {
160
writer, err = os.Create(flag.Arg(0))
161
if err != nil {
162
+
logc.Fatalln(context.Background(), err)
163
}
164
}
165
return
···
179
}
180
181
func Main() {
182
+
ctx := context.Background()
183
+
184
flag.Usage = usage
185
printConfigEnvVars := flag.Bool("print-config-env-vars", false,
186
"print every recognized configuration environment variable and exit")
···
229
cliOperations += 1
230
}
231
if cliOperations > 1 {
232
+
logc.Fatalln(ctx, "-get-blob, -get-manifest, -get-archive, -update-site, -freeze, and -unfreeze are mutually exclusive")
233
}
234
235
if *configTomlPath != "" && *noConfig {
236
+
logc.Fatalln(ctx, "-no-config and -config are mutually exclusive")
237
}
238
239
if *printConfigEnvVars {
···
246
*configTomlPath = "config.toml"
247
}
248
if config, err = Configure(*configTomlPath); err != nil {
249
+
logc.Fatalln(ctx, "config:", err)
250
}
251
252
if *printConfig {
···
258
defer FiniObservability()
259
260
if err = errors.Join(
261
+
configureFeatures(ctx),
262
+
configureMemLimit(ctx),
263
+
configureWildcards(ctx),
264
+
configureFallback(ctx),
265
); err != nil {
266
+
logc.Fatalln(ctx, err)
267
}
268
269
switch {
270
case *runMigration != "":
271
if backend, err = CreateBackend(&config.Storage); err != nil {
272
+
logc.Fatalln(ctx, err)
273
}
274
275
+
if err := RunMigration(ctx, *runMigration); err != nil {
276
+
logc.Fatalln(ctx, err)
277
}
278
279
case *getBlob != "":
280
if backend, err = CreateBackend(&config.Storage); err != nil {
281
+
logc.Fatalln(ctx, err)
282
}
283
284
+
reader, _, _, err := backend.GetBlob(ctx, *getBlob)
285
if err != nil {
286
+
logc.Fatalln(ctx, err)
287
}
288
io.Copy(fileOutputArg(), reader)
289
290
case *getManifest != "":
291
if backend, err = CreateBackend(&config.Storage); err != nil {
292
+
logc.Fatalln(ctx, err)
293
}
294
295
webRoot := webRootArg(*getManifest)
296
+
manifest, _, err := backend.GetManifest(ctx, webRoot, GetManifestOptions{})
297
if err != nil {
298
+
logc.Fatalln(ctx, err)
299
}
300
fmt.Fprintln(fileOutputArg(), ManifestDebugJSON(manifest))
301
302
case *getArchive != "":
303
if backend, err = CreateBackend(&config.Storage); err != nil {
304
+
logc.Fatalln(ctx, err)
305
}
306
307
webRoot := webRootArg(*getArchive)
308
manifest, manifestMtime, err :=
309
+
backend.GetManifest(ctx, webRoot, GetManifestOptions{})
310
if err != nil {
311
+
logc.Fatalln(ctx, err)
312
}
313
+
CollectTar(ctx, fileOutputArg(), manifest, manifestMtime)
314
315
case *updateSite != "":
316
if backend, err = CreateBackend(&config.Storage); err != nil {
317
+
logc.Fatalln(ctx, err)
318
}
319
320
if flag.NArg() != 1 {
321
+
logc.Fatalln(ctx, "update source must be provided as the argument")
322
}
323
324
sourceURL, err := url.Parse(flag.Arg(0))
325
if err != nil {
326
+
logc.Fatalln(ctx, err)
327
}
328
329
var result UpdateResult
330
if sourceURL.Scheme == "" {
331
file, err := os.Open(sourceURL.Path)
332
if err != nil {
333
+
logc.Fatalln(ctx, err)
334
}
335
defer file.Close()
336
···
349
}
350
351
webRoot := webRootArg(*updateSite)
352
+
result = UpdateFromArchive(ctx, webRoot, contentType, file)
353
} else {
354
branch := "pages"
355
if sourceURL.Fragment != "" {
···
357
}
358
359
webRoot := webRootArg(*updateSite)
360
+
result = UpdateFromRepository(ctx, webRoot, sourceURL.String(), branch)
361
}
362
363
switch result.outcome {
364
case UpdateError:
365
+
logc.Printf(ctx, "error: %s\n", result.err)
366
os.Exit(2)
367
case UpdateTimeout:
368
+
logc.Println(ctx, "timeout")
369
os.Exit(1)
370
case UpdateCreated:
371
+
logc.Println(ctx, "created")
372
case UpdateReplaced:
373
+
logc.Println(ctx, "replaced")
374
case UpdateDeleted:
375
+
logc.Println(ctx, "deleted")
376
case UpdateNoChange:
377
+
logc.Println(ctx, "no-change")
378
}
379
380
case *freezeDomain != "" || *unfreezeDomain != "":
···
389
}
390
391
if backend, err = CreateBackend(&config.Storage); err != nil {
392
+
logc.Fatalln(ctx, err)
393
}
394
395
+
if err = backend.FreezeDomain(ctx, domain, freeze); err != nil {
396
+
logc.Fatalln(ctx, err)
397
}
398
if freeze {
399
log.Println("frozen")
···
411
// The backend is not recreated (this is intentional as it allows preserving the cache).
412
OnReload(func() {
413
if newConfig, err := Configure(*configTomlPath); err != nil {
414
+
logc.Println(ctx, "config: reload err:", err)
415
} else {
416
// From https://go.dev/ref/mem:
417
// > A read r of a memory location x holding a value that is not larger than
···
421
// > concurrent write.
422
config = newConfig
423
if err = errors.Join(
424
+
configureFeatures(ctx),
425
+
configureMemLimit(ctx),
426
+
configureWildcards(ctx),
427
+
configureFallback(ctx),
428
); err != nil {
429
// At this point the configuration is in an in-between, corrupted state, so
430
// the only reasonable choice is to crash.
431
+
logc.Fatalln(ctx, "config: reload fail:", err)
432
} else {
433
+
logc.Println(ctx, "config: reload ok")
434
}
435
}
436
})
···
439
// spends some time initializing (which the S3 backend does) a proxy like Caddy can race
440
// with git-pages on startup and return errors for requests that would have been served
441
// just 0.5s later.
442
+
pagesListener := listen(ctx, "pages", config.Server.Pages)
443
+
caddyListener := listen(ctx, "caddy", config.Server.Caddy)
444
+
metricsListener := listen(ctx, "metrics", config.Server.Metrics)
445
446
if backend, err = CreateBackend(&config.Storage); err != nil {
447
+
logc.Fatalln(ctx, err)
448
}
449
backend = NewObservedBackend(backend)
450
451
+
go serve(ctx, pagesListener, ObserveHTTPHandler(http.HandlerFunc(ServePages)))
452
+
go serve(ctx, caddyListener, ObserveHTTPHandler(http.HandlerFunc(ServeCaddy)))
453
+
go serve(ctx, metricsListener, promhttp.Handler())
454
455
if config.Insecure {
456
+
logc.Println(ctx, "serve: ready (INSECURE)")
457
} else {
458
+
logc.Println(ctx, "serve: ready")
459
}
460
461
WaitForInterrupt()
462
+
logc.Println(ctx, "serve: exiting")
463
}
464
}