feat: S3 client compatibility — virtual-hosted style, CORS, presigned multi-method, ListMultipartUploads, edge case fixes

- Virtual-hosted bucket detection from Host header (extractS3BucketFromHost)
- S3 CORS headers + OPTIONS preflight + x-amz-id-2/HostId everywhere
- Presigned GET/PUT/HEAD/DELETE via centralized auth (no GET-only restriction)
- ListMultipartUploads with DB/xml helpers
- UploadPart partNumber range validation (1-10000)
- CompleteMultipartUpload ETag matching and ascending order validation
- DeleteObjects quiet mode support
- CopyObject URL-decode and conditional if-match/if-none-match
- encoding-type=url support in ListObjects V1/V2 XML
- Safe range-based prefix matching (replaces SQL LIKE)
- STREAMING-AWS4-HMAC-SHA256-PAYLOAD → 501 NotImplemented
- Traefik wildcard HostRegex for virtual-hosted style
- S3_VHOST_DOMAINS config env var
This commit is contained in:
asepharyana
2026-07-07 05:39:23 +07:00
parent 6aee3723fb
commit 8e6ccf2110
12 changed files with 417 additions and 179 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
import { applyS3Headers } from './headers';
import { contentRange, type RangeParseResult } from './range';
export interface ObjectPartSource {
@@ -97,7 +98,7 @@ export const createGetObjectResponse = async (input: ObjectResponseInput): Promi
const end = input.range.type === 'valid' ? input.range.end : input.totalSize - 1;
const plannedParts = planParts(input.parts, start, end);
const contentLength = end >= start ? end - start + 1 : 0;
const headers = baseHeaders(input, contentLength);
const headers = applyS3Headers(baseHeaders(input, contentLength), input.reqId);
if (input.range.type === 'valid') {
headers.set('content-range', contentRange(start, end, input.totalSize));