From 789a5e682e01fcae48c5e7df8c0b7c45e8125a7c Mon Sep 17 00:00:00 2001 From: Catherine Date: Mon, 22 Sep 2025 17:04:41 +0000 Subject: [PATCH] [breaking-change] Use type-safe representation for time durations. --- conf/config.example.toml | 4 ++-- src/config.go | 31 ++++++++++++++++++++++++++++--- src/pages.go | 4 ++-- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/conf/config.example.toml b/conf/config.example.toml index 2ff3d91..e51c0be 100644 --- a/conf/config.example.toml +++ b/conf/config.example.toml @@ -33,7 +33,7 @@ max-size = "256MB" [storage.s3.site-cache] max-size = "16MB" -max-age = 60 # seconds +max-age = "60s" [limits] max-site-size = "128M" @@ -41,5 +41,5 @@ max-manifest-size = "1M" max-inline-file-size = "256B" git-large-object-threshold = "1M" max-symlink-depth = 16 -update-timeout = 60 # seconds +update-timeout = "60s" max-heap-size-ratio = 0.5 # * RAM_size diff --git a/src/config.go b/src/config.go index d6a016c..daf9ed9 100644 --- a/src/config.go +++ b/src/config.go @@ -16,6 +16,24 @@ import ( "github.com/pelletier/go-toml/v2" ) +// For some reason, the standard `time.Duration` type doesn't implement the standard +// `encoding.{TextMarshaler,TextUnmarshaler}` interfaces. +type Duration time.Duration + +func (t Duration) String() string { + return fmt.Sprint(time.Duration(t)) +} + +func (t *Duration) UnmarshalText(data []byte) (err error) { + u, err := time.ParseDuration(string(data)) + *t = Duration(u) + return +} + +func (t *Duration) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + type Config struct { Insecure bool `toml:"-" env:"insecure"` Features []string `toml:"features"` @@ -41,7 +59,7 @@ type WildcardConfig struct { type CacheConfig struct { MaxSize datasize.ByteSize `toml:"max-size"` - MaxAge uint `toml:"max-age"` // in seconds + MaxAge Duration `toml:"max-age"` } type StorageConfig struct { @@ -62,7 +80,7 @@ type S3Config struct { Region string `toml:"region"` Bucket string `toml:"bucket"` BlobCache CacheConfig `toml:"blob-cache" default:"{\"MaxSize\":\"256MB\"}"` - SiteCache CacheConfig `toml:"site-cache" default:"{\"MaxAge\":60,\"MaxSize\":\"16MB\"}"` + SiteCache CacheConfig `toml:"site-cache" default:"{\"MaxAge\":\"60s\",\"MaxSize\":\"16MB\"}"` } type LimitsConfig struct { @@ -80,7 +98,7 @@ type LimitsConfig struct { MaxSymlinkDepth uint `toml:"max-symlink-depth" default:"16"` // Maximum time that an update operation (PUT or POST request) could take before being // interrupted. - UpdateTimeout time.Duration `toml:"update-timeout" default:"60s"` + UpdateTimeout Duration `toml:"update-timeout" default:"60s"` // Soft limit on Go heap size, expressed as a fraction of total available RAM. MaxHeapSizeRatio float64 `toml:"max-heap-size-ratio" default:"0.5"` } @@ -173,6 +191,11 @@ func setConfigValue(reflValue reflect.Value, repr string) (err error) { if valueCast, err = time.ParseDuration(repr); err == nil { reflValue.Set(reflect.ValueOf(valueCast)) } + case Duration: + var parsed time.Duration + if parsed, err = time.ParseDuration(repr); err == nil { + reflValue.Set(reflect.ValueOf(Duration(parsed))) + } case []WildcardConfig: var parsed []*WildcardConfig decoder := json.NewDecoder(bytes.NewReader([]byte(repr))) @@ -194,6 +217,7 @@ func setConfigValue(reflValue reflect.Value, repr string) (err error) { func PrintConfigEnvVars() { config := Config{} defaults.MustSet(&config) + walkConfig(&config, func(envName string, reflValue reflect.Value) (err error) { value := reflValue.Interface() reprBefore := fmt.Sprint(value) @@ -224,6 +248,7 @@ func Configure(tomlPath string) (config *Config, err error) { decoder := toml.NewDecoder(file) decoder.DisallowUnknownFields() + decoder.EnableUnmarshalerInterface() if err = decoder.Decode(&config); err != nil { return } diff --git a/src/pages.go b/src/pages.go index f64b780..867d8d6 100644 --- a/src/pages.go +++ b/src/pages.go @@ -216,7 +216,7 @@ func putPage(w http.ResponseWriter, r *http.Request) error { return err } - ctx, cancel := context.WithTimeout(r.Context(), config.Limits.UpdateTimeout) + ctx, cancel := context.WithTimeout(r.Context(), time.Duration(config.Limits.UpdateTimeout)) defer cancel() result = UpdateFromRepository(ctx, webRoot, repoURL, branch) } else { @@ -369,7 +369,7 @@ func postPage(w http.ResponseWriter, r *http.Request) error { return err } - ctx, cancel := context.WithTimeout(r.Context(), config.Limits.UpdateTimeout) + ctx, cancel := context.WithTimeout(r.Context(), time.Duration(config.Limits.UpdateTimeout)) defer cancel() result := UpdateFromRepository(ctx, webRoot, repoURL, "pages") switch result.outcome {