@@ -35,6 +35,7 @@ type Config struct { |
| 35 | 35 | Tracing TracingConfig `toml:"tracing"` |
| 36 | 36 | ErrorReporting ErrorReportingConfig `toml:"error_reporting"` |
| 37 | 37 | Session SessionConfig `toml:"session"` |
| 38 | + Storage StorageConfig `toml:"storage"` |
| 38 | 39 | } |
| 39 | 40 | |
| 40 | 41 | // WebConfig holds HTTP server settings. |
@@ -88,6 +89,25 @@ type SessionConfig struct { |
| 88 | 89 | Secure bool `toml:"secure"` |
| 89 | 90 | } |
| 90 | 91 | |
| 92 | +// StorageConfig configures repo filesystem storage and S3-compatible |
| 93 | +// object storage. The S3 block targets MinIO in dev/test and DigitalOcean |
| 94 | +// Spaces in prod (force_path_style true for MinIO, false for Spaces). |
| 95 | +type StorageConfig struct { |
| 96 | + ReposRoot string `toml:"repos_root"` // filesystem root for bare repos |
| 97 | + S3 S3StorageConfig `toml:"s3"` |
| 98 | +} |
| 99 | + |
| 100 | +// S3StorageConfig holds the S3-compatible endpoint settings. |
| 101 | +type S3StorageConfig struct { |
| 102 | + Endpoint string `toml:"endpoint"` // host[:port], no scheme |
| 103 | + Region string `toml:"region"` // e.g. "us-east-1", "nyc3" |
| 104 | + AccessKeyID string `toml:"access_key_id"` // |
| 105 | + SecretAccessKey string `toml:"secret_access_key"` // |
| 106 | + Bucket string `toml:"bucket"` // e.g. "shithub-dev" |
| 107 | + UseSSL bool `toml:"use_ssl"` // true for Spaces, false for local MinIO |
| 108 | + ForcePathStyle bool `toml:"force_path_style"` // true for MinIO, false for Spaces |
| 109 | +} |
| 110 | + |
| 91 | 111 | // Defaults returns the zero-config baseline. |
| 92 | 112 | func Defaults() Config { |
| 93 | 113 | return Config{ |
@@ -119,6 +139,13 @@ func Defaults() Config { |
| 119 | 139 | MaxAge: 30 * 24 * time.Hour, |
| 120 | 140 | Secure: false, |
| 121 | 141 | }, |
| 142 | + Storage: StorageConfig{ |
| 143 | + ReposRoot: "/data/repos", |
| 144 | + S3: S3StorageConfig{ |
| 145 | + Region: "us-east-1", |
| 146 | + ForcePathStyle: true, |
| 147 | + }, |
| 148 | + }, |
| 122 | 149 | } |
| 123 | 150 | } |
| 124 | 151 | |
@@ -189,6 +216,38 @@ func Validate(c *Config) error { |
| 189 | 216 | if c.Tracing.SampleRate < 0 || c.Tracing.SampleRate > 1 { |
| 190 | 217 | return fmt.Errorf("config: tracing.sample_rate: must be in [0, 1], got %v", c.Tracing.SampleRate) |
| 191 | 218 | } |
| 219 | + if c.Storage.ReposRoot == "" { |
| 220 | + return errors.New("config: storage.repos_root is required") |
| 221 | + } |
| 222 | + if err := validateS3(c.Storage.S3); err != nil { |
| 223 | + return err |
| 224 | + } |
| 225 | + return nil |
| 226 | +} |
| 227 | + |
| 228 | +// validateS3 enforces all-or-nothing on the S3 block: if any of |
| 229 | +// endpoint/bucket/access keys are set, all must be set. |
| 230 | +func validateS3(s S3StorageConfig) error { |
| 231 | + any := s.Endpoint != "" || s.Bucket != "" || s.AccessKeyID != "" || s.SecretAccessKey != "" |
| 232 | + if !any { |
| 233 | + return nil |
| 234 | + } |
| 235 | + missing := []string{} |
| 236 | + if s.Endpoint == "" { |
| 237 | + missing = append(missing, "endpoint") |
| 238 | + } |
| 239 | + if s.Bucket == "" { |
| 240 | + missing = append(missing, "bucket") |
| 241 | + } |
| 242 | + if s.AccessKeyID == "" { |
| 243 | + missing = append(missing, "access_key_id") |
| 244 | + } |
| 245 | + if s.SecretAccessKey == "" { |
| 246 | + missing = append(missing, "secret_access_key") |
| 247 | + } |
| 248 | + if len(missing) > 0 { |
| 249 | + return fmt.Errorf("config: storage.s3: incomplete configuration, missing: %s", strings.Join(missing, ", ")) |
| 250 | + } |
| 192 | 251 | return nil |
| 193 | 252 | } |
| 194 | 253 | |