Skip to content

Commit 98f78f5

Browse files
authored
Replace magic values with constants (#2)
1 parent 50f60aa commit 98f78f5

17 files changed

Lines changed: 144 additions & 82 deletions

File tree

internal/api/handler/handler.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,20 @@ import (
88
"go.uber.org/zap"
99
)
1010

11+
const (
12+
// contentTypeXML is the content type for XML responses
13+
contentTypeXML = "application/xml"
14+
15+
// headerXAmzMeta is the prefix for user-defined metadata headers
16+
headerXAmzMeta = "X-Amz-Meta-"
17+
// headerXAmzMetaLen is the length of the metadata header prefix
18+
headerXAmzMetaLen = len(headerXAmzMeta)
19+
20+
// minPartSize is the minimum allowed part size for multipart uploads (5MB)
21+
// This is an S3 constraint that we enforce across all backends for consistency
22+
minPartSize = 5 * 1024 * 1024
23+
)
24+
1125
// Handler handles S3 API requests
1226
type Handler struct {
1327
backend types.Backend
@@ -24,7 +38,7 @@ func New(be types.Backend, logger *zap.Logger) *Handler {
2438

2539
// sendXML sends an XML response
2640
func (h *Handler) sendXML(w http.ResponseWriter, status int, v any) {
27-
w.Header().Set("Content-Type", "application/xml")
41+
w.Header().Set("Content-Type", contentTypeXML)
2842
w.WriteHeader(status)
2943
_ = xml.NewEncoder(w).Encode(v)
3044
}

internal/api/handler/multipart.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,6 @@ import (
1212
"go.uber.org/zap"
1313
)
1414

15-
const (
16-
// minPartSize is the minimum allowed part size for multipart uploads (5MB)
17-
// This is an S3 constraint that we enforce across all backends for consistency
18-
minPartSize = 5 * 1024 * 1024 // 5MB
19-
)
20-
2115
// CreateMultipartUpload handles POST /{bucket}/{key}?uploads - initiate multipart upload
2216
func (h *Handler) CreateMultipartUpload(w http.ResponseWriter, r *http.Request) {
2317
vars := mux.Vars(r)
@@ -26,8 +20,8 @@ func (h *Handler) CreateMultipartUpload(w http.ResponseWriter, r *http.Request)
2620

2721
metadata := make(map[string]string)
2822
for k, v := range r.Header {
29-
if len(v) > 0 && len(k) > 11 && k[:11] == "X-Amz-Meta-" {
30-
metadata[k[11:]] = v[0]
23+
if len(v) > 0 && len(k) > headerXAmzMetaLen && k[:headerXAmzMetaLen] == headerXAmzMeta {
24+
metadata[k[headerXAmzMetaLen:]] = v[0]
3125
}
3226
}
3327

internal/api/handler/object.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func (h *Handler) HeadObject(w http.ResponseWriter, r *http.Request) {
3232
w.Header().Set("Last-Modified", info.LastModified.Format(http.TimeFormat))
3333

3434
for k, v := range info.Metadata {
35-
w.Header().Set("x-amz-meta-"+k, v)
35+
w.Header().Set(headerXAmzMeta+k, v)
3636
}
3737

3838
w.WriteHeader(http.StatusOK)
@@ -74,7 +74,7 @@ func (h *Handler) GetObject(w http.ResponseWriter, r *http.Request) {
7474
w.Header().Set("Last-Modified", obj.LastModified.Format(http.TimeFormat))
7575

7676
for k, v := range obj.Metadata {
77-
w.Header().Set("x-amz-meta-"+k, v)
77+
w.Header().Set(headerXAmzMeta+k, v)
7878
}
7979

8080
// Handle range response - check if this is a range request
@@ -175,8 +175,8 @@ func (h *Handler) PutObject(w http.ResponseWriter, r *http.Request) {
175175

176176
// Capture custom metadata (X-Amz-Meta-*)
177177
for k, v := range r.Header {
178-
if len(v) > 0 && len(k) > 11 && k[:11] == "X-Amz-Meta-" {
179-
metadata[k[11:]] = v[0]
178+
if len(v) > 0 && len(k) > headerXAmzMetaLen && k[:headerXAmzMetaLen] == headerXAmzMeta {
179+
metadata[k[headerXAmzMetaLen:]] = v[0]
180180
}
181181
}
182182

internal/auth/middleware.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import (
77
"go.uber.org/zap"
88
)
99

10+
const (
11+
// contentTypeXML is the content type for XML responses
12+
contentTypeXML = "application/xml"
13+
)
14+
1015
// Middleware provides authentication middleware for S3 API requests
1116
type Middleware struct {
1217
verifier *SignatureV4
@@ -96,15 +101,15 @@ type errorResponse struct {
96101
}
97102

98103
func sendAuthError(w http.ResponseWriter, code, message string) {
99-
w.Header().Set("Content-Type", "application/xml")
104+
w.Header().Set("Content-Type", contentTypeXML)
100105
w.WriteHeader(http.StatusForbidden)
101106
resp := errorResponse{Code: code, Message: message}
102107
_, _ = w.Write([]byte(xml.Header))
103108
_ = xml.NewEncoder(w).Encode(resp)
104109
}
105110

106111
func sendPayloadTooLargeError(w http.ResponseWriter) {
107-
w.Header().Set("Content-Type", "application/xml")
112+
w.Header().Set("Content-Type", contentTypeXML)
108113
w.WriteHeader(http.StatusBadRequest)
109114
resp := errorResponse{
110115
Code: "XAmzContentSHA256Mismatch",
@@ -115,7 +120,7 @@ func sendPayloadTooLargeError(w http.ResponseWriter) {
115120
}
116121

117122
func sendPayloadHashMismatchError(w http.ResponseWriter) {
118-
w.Header().Set("Content-Type", "application/xml")
123+
w.Header().Set("Content-Type", contentTypeXML)
119124
w.WriteHeader(http.StatusBadRequest)
120125
resp := errorResponse{
121126
Code: "XAmzContentSHA256Mismatch",

internal/auth/signature.go

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ import (
1616
"time"
1717
)
1818

19+
const (
20+
// defaultMaxClockSkew is the default maximum allowed clock skew for request timestamps (AWS default)
21+
defaultMaxClockSkew = 15 * time.Minute
22+
23+
// maxBodyReadSize is the maximum body size for signature calculation (10 MiB)
24+
// Requests larger than this must use UNSIGNED-PAYLOAD
25+
maxBodyReadSize = 10 * 1024 * 1024
26+
27+
// emptyPayloadHash is the SHA256 hash of an empty payload
28+
emptyPayloadHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
29+
)
30+
1931
var (
2032
// ErrMissingAuthHeader is returned when Authorization header is missing
2133
ErrMissingAuthHeader = errors.New("missing Authorization header")
@@ -88,7 +100,7 @@ func NewSignatureV4(store CredentialStore, region, service string) *SignatureV4
88100
credentialStore: store,
89101
region: region,
90102
service: service,
91-
maxClockSkew: 15 * time.Minute, // AWS default
103+
maxClockSkew: defaultMaxClockSkew,
92104
}
93105
}
94106

@@ -217,8 +229,6 @@ func (s *SignatureV4) Verify(r *http.Request) error {
217229
} else if payloadHash != "" {
218230
// Client provided a specific payload hash - verify it matches the body
219231
// Protect against memory exhaustion by limiting body read size
220-
const maxBodyReadSize = 10 * 1024 * 1024 // 10 MiB max for signature calculation
221-
222232
if r.ContentLength > maxBodyReadSize {
223233
// Request is too large to buffer for hash verification
224234
// Client should use X-Amz-Content-Sha256: UNSIGNED-PAYLOAD header
@@ -246,16 +256,13 @@ func (s *SignatureV4) Verify(r *http.Request) error {
246256
}
247257
} else {
248258
// Empty body - verify client sent empty payload hash
249-
emptyHash := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
250-
if subtle.ConstantTimeCompare([]byte(emptyHash), []byte(payloadHash)) != 1 {
259+
if subtle.ConstantTimeCompare([]byte(emptyPayloadHash), []byte(payloadHash)) != 1 {
251260
return ErrPayloadHashMismatch
252261
}
253262
}
254263
} else {
255264
// No payload hash header provided - we need to read body to calculate hash
256265
// Protect against memory exhaustion by limiting body read size
257-
const maxBodyReadSize = 10 * 1024 * 1024 // 10 MiB max for signature calculation
258-
259266
if r.ContentLength > maxBodyReadSize {
260267
// Request is too large to buffer for signature verification
261268
// Client should use X-Amz-Content-Sha256: UNSIGNED-PAYLOAD header
@@ -276,7 +283,7 @@ func (s *SignatureV4) Verify(r *http.Request) error {
276283
r.Body = io.NopCloser(bytes.NewReader(payload))
277284
payloadHash = hex.EncodeToString(sha256Hash(payload))
278285
} else {
279-
payloadHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" // empty payload hash
286+
payloadHash = emptyPayloadHash
280287
}
281288
}
282289

internal/backend/awss3/object.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import (
1818
// PutObject uploads an object using the S3 Upload Manager for efficient streaming.
1919
func (b *Backend) PutObject(ctx context.Context, bucket, key string, reader io.Reader, size int64, metadata map[string]string) (*types.ObjectInfo, error) {
2020
// Extract Content-Type from metadata
21-
contentType := "application/octet-stream"
21+
contentType := types.DefaultContentType
2222
if ct, ok := metadata["Content-Type"]; ok {
2323
contentType = ct
2424
delete(metadata, "Content-Type")
@@ -38,8 +38,8 @@ func (b *Backend) PutObject(ctx context.Context, bucket, key string, reader io.R
3838

3939
// Use S3 Upload Manager which handles streaming and multipart uploads automatically
4040
uploader := manager.NewUploader(b.client, func(u *manager.Uploader) {
41-
u.PartSize = 5 * 1024 * 1024 // 5 MiB minimum part size
42-
u.Concurrency = 1 // Single concurrency to minimize memory
41+
u.PartSize = types.PartSize5MiB
42+
u.Concurrency = 1 // Single concurrency to minimize memory
4343
})
4444

4545
input := &s3.PutObjectInput{
@@ -131,7 +131,7 @@ func (b *Backend) GetObject(ctx context.Context, bucket, key string, opts *types
131131
}
132132

133133
// Get content type
134-
contentType := "application/octet-stream"
134+
contentType := types.DefaultContentType
135135
if resp.ContentType != nil {
136136
contentType = *resp.ContentType
137137
}
@@ -189,7 +189,7 @@ func (b *Backend) HeadObject(ctx context.Context, bucket, key string) (*types.Ob
189189
}
190190

191191
// Get content type
192-
contentType := "application/octet-stream"
192+
contentType := types.DefaultContentType
193193
if resp.ContentType != nil {
194194
contentType = *resp.ContentType
195195
}

internal/backend/azblob/bucket.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,8 @@ func (b *Backend) ListObjects(ctx context.Context, bucket string, opts types.Lis
106106
containerClient := b.client.ServiceClient().NewContainerClient(bucket)
107107

108108
// Set up list options
109-
maxResults := int32(1000)
110-
if opts.MaxKeys > 0 && opts.MaxKeys < 1000 {
109+
maxResults := int32(types.DefaultMaxKeys)
110+
if opts.MaxKeys > 0 && opts.MaxKeys < types.DefaultMaxKeys {
111111
maxResults = int32(opts.MaxKeys)
112112
}
113113

@@ -178,8 +178,8 @@ func (b *Backend) ListObjects(ctx context.Context, bucket string, opts types.Lis
178178
func (b *Backend) listObjectsHierarchical(ctx context.Context, bucket string, opts types.ListObjectsOptions) (*types.ListObjectsResult, error) {
179179
containerClient := b.client.ServiceClient().NewContainerClient(bucket)
180180

181-
maxResults := int32(1000)
182-
if opts.MaxKeys > 0 && opts.MaxKeys < 1000 {
181+
maxResults := int32(types.DefaultMaxKeys)
182+
if opts.MaxKeys > 0 && opts.MaxKeys < types.DefaultMaxKeys {
183183
maxResults = int32(opts.MaxKeys)
184184
}
185185

internal/backend/azblob/multipart.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,7 @@ func (b *Backend) ListParts(ctx context.Context, bucket, key, uploadID string, o
467467
// Apply pagination
468468
maxParts := opts.MaxParts
469469
if maxParts <= 0 {
470-
maxParts = 1000
470+
maxParts = types.DefaultMaxParts
471471
}
472472

473473
// Filter parts after the marker
@@ -600,7 +600,7 @@ func (b *Backend) ListMultipartUploads(ctx context.Context, bucket string, opts
600600
// Apply max uploads limit
601601
maxUploads := opts.MaxUploads
602602
if maxUploads <= 0 {
603-
maxUploads = 1000
603+
maxUploads = types.DefaultMaxUploads
604604
}
605605

606606
isTruncated := false

internal/backend/azblob/object.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import (
1818
// PutObject uploads a blob using streaming upload with MD5 calculation.
1919
func (b *Backend) PutObject(ctx context.Context, bucket, key string, reader io.Reader, size int64, metadata map[string]string) (*types.ObjectInfo, error) {
2020
// Extract Content-Type from metadata
21-
contentType := "application/octet-stream"
21+
contentType := types.DefaultContentType
2222
if ct, ok := metadata["Content-Type"]; ok {
2323
contentType = ct
2424
delete(metadata, "Content-Type")
@@ -42,8 +42,8 @@ func (b *Backend) PutObject(ctx context.Context, bucket, key string, reader io.R
4242

4343
// Upload using UploadStream which handles chunking internally
4444
_, err := blockBlobClient.UploadStream(ctx, teeReader, &blockblob.UploadStreamOptions{
45-
BlockSize: 4 * 1024 * 1024, // 4 MiB blocks
46-
Concurrency: 1, // Single concurrency to minimize memory
45+
BlockSize: types.ChunkSize4MiB,
46+
Concurrency: 1, // Single concurrency to minimize memory
4747
Metadata: azureMetadata,
4848
HTTPHeaders: &blob.HTTPHeaders{
4949
BlobContentType: &contentType,
@@ -155,7 +155,7 @@ func (b *Backend) GetObject(ctx context.Context, bucket, key string, opts *types
155155
}
156156

157157
// Get content type
158-
contentType := "application/octet-stream"
158+
contentType := types.DefaultContentType
159159
if resp.ContentType != nil {
160160
contentType = *resp.ContentType
161161
}
@@ -219,7 +219,7 @@ func (b *Backend) HeadObject(ctx context.Context, bucket, key string) (*types.Ob
219219
}
220220

221221
// Get content type
222-
contentType := "application/octet-stream"
222+
contentType := types.DefaultContentType
223223
if props.ContentType != nil {
224224
contentType = *props.ContentType
225225
}

internal/backend/disk/bucket.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@ func (b *Backend) CreateBucket(ctx context.Context, bucket string) error {
2828
}
2929

3030
// Create bucket directory structure
31-
if err := os.MkdirAll(bucketPath, 0755); err != nil {
31+
if err := os.MkdirAll(bucketPath, dirPermissions); err != nil {
3232
return fmt.Errorf("failed to create bucket directory: %w", err)
3333
}
3434

3535
// Create subdirectory for multipart uploads
36-
if err := os.MkdirAll(filepath.Join(bucketPath, multipartDir), 0755); err != nil {
36+
if err := os.MkdirAll(filepath.Join(bucketPath, multipartDir), dirPermissions); err != nil {
3737
// Cleanup on failure
3838
_ = os.RemoveAll(bucketPath)
3939
return fmt.Errorf("failed to create bucket subdirectory %s: %w", multipartDir, err)
@@ -81,7 +81,7 @@ func (b *Backend) DeleteBucket(ctx context.Context, bucket string) error {
8181
}
8282
return nil
8383
}
84-
if strings.HasPrefix(d.Name(), ".") && strings.HasSuffix(d.Name(), ".object.json") {
84+
if strings.HasPrefix(d.Name(), ".") && strings.HasSuffix(d.Name(), objectMetadataSuffix) {
8585
hasObjects = true
8686
return filepath.SkipAll // Stop walking, we found an object
8787
}
@@ -180,7 +180,7 @@ func (b *Backend) ListObjects(ctx context.Context, bucket string, opts types.Lis
180180

181181
maxKeys := opts.MaxKeys
182182
if maxKeys <= 0 {
183-
maxKeys = 1000
183+
maxKeys = types.DefaultMaxKeys
184184
}
185185

186186
startAfter := opts.Marker
@@ -274,7 +274,7 @@ func (b *Backend) ListObjects(ctx context.Context, bucket string, opts types.Lis
274274
}
275275

276276
// Only process hidden .object.json files
277-
if !strings.HasPrefix(d.Name(), ".") || !strings.HasSuffix(d.Name(), ".object.json") {
277+
if !strings.HasPrefix(d.Name(), ".") || !strings.HasSuffix(d.Name(), objectMetadataSuffix) {
278278
return nil
279279
}
280280

0 commit comments

Comments
 (0)