diff --git a/config-hold.example.yaml b/config-hold.example.yaml index 778e39e..505ad74 100644 --- a/config-hold.example.yaml +++ b/config-hold.example.yaml @@ -31,6 +31,8 @@ storage: bucket: "" # Custom S3 endpoint for non-AWS providers (e.g. "https://gateway.storjshare.io"). endpoint: "" + # CDN pull zone URL for downloads. When set, presigned GET/HEAD URLs use this host instead of the S3 endpoint. Uploads and API calls still use the S3 endpoint. + pull_zone: "" # HTTP server and identity settings. server: # Listen address, e.g. ":8080" or "0.0.0.0:8080". @@ -39,6 +41,8 @@ server: public_url: "" # Allow unauthenticated blob reads. If false, readers need crew membership. public: false + # DID of successor hold for migration. Appview redirects all requests to the successor. + successor: "" # Use localhost for OAuth redirects during development. test_mode: false # Request crawl from this relay on startup to make the embedded PDS discoverable. diff --git a/deploy/upcloud/configs/hold.yaml.tmpl b/deploy/upcloud/configs/hold.yaml.tmpl index 950cd0b..62df3b6 100644 --- a/deploy/upcloud/configs/hold.yaml.tmpl +++ b/deploy/upcloud/configs/hold.yaml.tmpl @@ -13,10 +13,12 @@ storage: region: "{{.S3Region}}" bucket: "{{.S3Bucket}}" endpoint: "{{.S3Endpoint}}" + pull_zone: "" server: addr: :8080 public_url: "https://{{.HoldDomain}}" public: false + successor: "" test_mode: false relay_endpoint: "" read_timeout: 5m0s @@ -35,6 +37,8 @@ database: libsql_sync_interval: 1m0s admin: enabled: true +gc: + enabled: false quota: tiers: deckhand: diff --git a/pkg/hold/config.go b/pkg/hold/config.go index 391fbe1..1d958bb 100644 --- a/pkg/hold/config.go +++ b/pkg/hold/config.go @@ -81,6 +81,9 @@ type StorageConfig struct { // Custom S3 endpoint for non-AWS providers. Endpoint string `yaml:"endpoint" comment:"Custom S3 endpoint for non-AWS providers (e.g. \"https://gateway.storjshare.io\")."` + // CDN pull zone URL for presigned download URLs. + PullZone string `yaml:"pull_zone" comment:"CDN pull zone URL for downloads. When set, presigned GET/HEAD URLs use this host instead of the S3 endpoint. Uploads and API calls still use the S3 endpoint."` + // Internal distribution storage config, built from the above fields. distStorage configuration.Storage `yaml:"-"` } @@ -188,6 +191,7 @@ func setHoldDefaults(v *viper.Viper) { v.SetDefault("storage.region", "us-east-1") v.SetDefault("storage.bucket", "") v.SetDefault("storage.endpoint", "") + v.SetDefault("storage.pull_zone", "") // GC defaults v.SetDefault("gc.enabled", false) @@ -244,6 +248,7 @@ func LoadConfig(yamlPath string) (*Config, error) { _ = v.BindEnv("storage.region", "AWS_REGION") _ = v.BindEnv("storage.bucket", "S3_BUCKET") _ = v.BindEnv("storage.endpoint", "S3_ENDPOINT") + _ = v.BindEnv("storage.pull_zone", "S3_PULL_ZONE") // Bind legacy GC env vars (backward compat) _ = v.BindEnv("gc.enabled", "GC_ENABLED") @@ -298,6 +303,9 @@ func buildStorageConfigFromFields(sc StorageConfig) configuration.Storage { params["regionendpoint"] = sc.Endpoint params["forcepathstyle"] = true } + if sc.PullZone != "" { + params["pullzone"] = sc.PullZone + } storageCfg := configuration.Storage{} storageCfg["s3"] = configuration.Parameters(params) diff --git a/pkg/s3/types.go b/pkg/s3/types.go index 802d383..2adb457 100644 --- a/pkg/s3/types.go +++ b/pkg/s3/types.go @@ -33,8 +33,9 @@ type S3Client interface { // RealS3Client wraps AWS SDK v2 *s3.Client to implement S3Client interface type RealS3Client struct { - client *awss3.Client - presign *awss3.PresignClient + client *awss3.Client + presign *awss3.PresignClient + pullZone string // CDN pull zone URL for presigned GET/HEAD URLs } // NewRealS3Client creates a new RealS3Client wrapper @@ -64,6 +65,11 @@ func (r *RealS3Client) AbortMultipartUpload(ctx context.Context, input *awss3.Ab func (r *RealS3Client) PresignGetObject(ctx context.Context, input *awss3.GetObjectInput, expires time.Duration) (string, error) { result, err := r.presign.PresignGetObject(ctx, input, func(opts *awss3.PresignOptions) { opts.Expires = expires + if r.pullZone != "" { + opts.ClientOptions = append(opts.ClientOptions, func(o *awss3.Options) { + o.BaseEndpoint = aws.String(r.pullZone) + }) + } }) if err != nil { return "", err @@ -75,6 +81,11 @@ func (r *RealS3Client) PresignGetObject(ctx context.Context, input *awss3.GetObj func (r *RealS3Client) PresignHeadObject(ctx context.Context, input *awss3.HeadObjectInput, expires time.Duration) (string, error) { result, err := r.presign.PresignHeadObject(ctx, input, func(opts *awss3.PresignOptions) { opts.Expires = expires + if r.pullZone != "" { + opts.ClientOptions = append(opts.ClientOptions, func(o *awss3.Options) { + o.BaseEndpoint = aws.String(r.pullZone) + }) + } }) if err != nil { return "", err @@ -162,14 +173,21 @@ func NewS3Service(params map[string]any) (*S3Service, error) { s3PathPrefix = strings.TrimPrefix(rootDir, "/") } + // CDN pull zone for presigned download URLs + pullZone, _ := params["pullzone"].(string) + slog.Info("S3 presigned URLs enabled", "bucket", bucket, "region", region, - "pathPrefix", s3PathPrefix) + "pathPrefix", s3PathPrefix, + "pullZone", pullZone) // Create S3 client wrapped in RealS3Client for interface compatibility + realClient := NewRealS3Client(client) + realClient.pullZone = pullZone + return &S3Service{ - Client: NewRealS3Client(client), + Client: realClient, Bucket: bucket, PathPrefix: s3PathPrefix, }, nil