@dimah-s3/server 0.8.6 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/express.js +13 -1
- package/dist/adapters/express.js.map +1 -1
- package/dist/adapters/fastify.js +13 -1
- package/dist/adapters/fastify.js.map +1 -1
- package/dist/adapters/node.js +13 -1
- package/dist/adapters/node.js.map +1 -1
- package/dist/adapters/svelte-kit.d.ts +2 -0
- package/dist/adapters/svelte-kit.d.ts.map +1 -1
- package/dist/adapters/svelte-kit.js.map +1 -1
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/api/assert-feature-enabled.d.ts.map +1 -1
- package/dist/api/bind-endpoints.d.ts.map +1 -1
- package/dist/api/context.d.ts.map +1 -1
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +218 -147
- package/dist/api/index.js.map +1 -1
- package/dist/{router.d.ts → api/router.d.ts} +3 -2
- package/dist/api/router.d.ts.map +1 -0
- package/dist/api/routes/confirm.d.ts.map +1 -1
- package/dist/api/routes/delete.d.ts.map +1 -1
- package/dist/api/routes/download.d.ts.map +1 -1
- package/dist/api/routes/index.d.ts +1 -7
- package/dist/api/routes/index.d.ts.map +1 -1
- package/dist/api/routes/multipart/abort.d.ts +1 -5
- package/dist/api/routes/multipart/abort.d.ts.map +1 -1
- package/dist/api/routes/multipart/complete.d.ts.map +1 -1
- package/dist/api/routes/multipart/init.d.ts +0 -2
- package/dist/api/routes/multipart/init.d.ts.map +1 -1
- package/dist/api/routes/multipart/list-parts.d.ts.map +1 -1
- package/dist/api/routes/multipart/part.d.ts.map +1 -1
- package/dist/api/routes/upload.d.ts.map +1 -1
- package/dist/errors.d.ts +4 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/helpers/expires.d.ts +6 -0
- package/dist/helpers/expires.d.ts.map +1 -0
- package/dist/helpers/features.d.ts +34 -0
- package/dist/helpers/features.d.ts.map +1 -0
- package/dist/helpers/head-object.d.ts +11 -0
- package/dist/helpers/head-object.d.ts.map +1 -0
- package/dist/{internal-helpers.d.ts → helpers/hooks.d.ts} +1 -4
- package/dist/helpers/hooks.d.ts.map +1 -0
- package/dist/helpers/index.d.ts +12 -1
- package/dist/helpers/index.d.ts.map +1 -1
- package/dist/helpers/is-aws-not-found.d.ts +3 -1
- package/dist/helpers/is-aws-not-found.d.ts.map +1 -1
- package/dist/helpers/list-parts.d.ts +8 -0
- package/dist/helpers/list-parts.d.ts.map +1 -0
- package/dist/helpers/request.d.ts +3 -0
- package/dist/helpers/request.d.ts.map +1 -0
- package/dist/helpers/resolve-request-acl.d.ts +10 -0
- package/dist/helpers/resolve-request-acl.d.ts.map +1 -0
- package/dist/helpers/resolve-target.d.ts +2 -33
- package/dist/helpers/resolve-target.d.ts.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +188 -111
- package/dist/index.js.map +1 -1
- package/dist/plugin/apply-plugins.d.ts.map +1 -1
- package/dist/plugin/types.d.ts +5 -5
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/plugins.js +184 -104
- package/dist/plugins.js.map +1 -1
- package/dist/types/config.d.ts +27 -9
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/hook-contexts.d.ts +8 -8
- package/dist/types/hook-contexts.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +6 -6
- package/dist/internal-helpers.d.ts.map +0 -1
- package/dist/router.d.ts.map +0 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Part, type S3Client } from "@aws-sdk/client-s3";
|
|
2
|
+
/** Page through ListParts until S3 reports no more parts. */
|
|
3
|
+
export declare function listAllParts(client: S3Client, input: {
|
|
4
|
+
bucket: string;
|
|
5
|
+
key: string;
|
|
6
|
+
uploadId: string;
|
|
7
|
+
}): Promise<Part[]>;
|
|
8
|
+
//# sourceMappingURL=list-parts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list-parts.d.ts","sourceRoot":"","sources":["../../src/helpers/list-parts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,IAAI,EAAE,KAAK,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAGhF,6DAA6D;AAC7D,wBAAsB,YAAY,CAChC,MAAM,EAAE,QAAQ,EAChB,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACvD,OAAO,CAAC,IAAI,EAAE,CAAC,CAsBjB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../../src/helpers/request.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,wBAAgB,kBAAkB,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAEjE"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { S3ObjectAcl } from "@dimah-s3/core";
|
|
2
|
+
import type { AclPolicy } from "../types/config";
|
|
3
|
+
export type { AclPolicy };
|
|
4
|
+
/**
|
|
5
|
+
* Resolve object ACL. Default is `private`. A server `acl` is forced.
|
|
6
|
+
* Client `acl` is used only when `allowClientAcl` is set and no server ACL
|
|
7
|
+
* is configured.
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveRequestAcl(policy: AclPolicy | undefined, requested?: S3ObjectAcl): S3ObjectAcl;
|
|
10
|
+
//# sourceMappingURL=resolve-request-acl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-request-acl.d.ts","sourceRoot":"","sources":["../../src/helpers/resolve-request-acl.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,YAAY,EAAE,SAAS,EAAE,CAAC;AAE1B;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,SAAS,GAAG,SAAS,EAC7B,SAAS,CAAC,EAAE,WAAW,GACtB,WAAW,CAIb"}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
type KeyPolicy
|
|
3
|
-
prefix?: KeyPrefix;
|
|
4
|
-
resolveKey?: (context: ResolveKeyContext) => string | Promise<string>;
|
|
5
|
-
};
|
|
1
|
+
import type { KeyPolicy, ResolvedDimahS3Config, ResolveKeyContext } from "../types";
|
|
2
|
+
export type { KeyPolicy };
|
|
6
3
|
/**
|
|
7
4
|
* Normalize a client-proposed object key: strip leading slashes, reject
|
|
8
5
|
* `.` / `..` segments, NUL, and backslashes.
|
|
@@ -20,32 +17,4 @@ export declare function resolveRequestTarget(config: ResolvedDimahS3Config, poli
|
|
|
20
17
|
key: string;
|
|
21
18
|
bucket: string;
|
|
22
19
|
}>;
|
|
23
|
-
export declare function normalizeFeature<T extends {
|
|
24
|
-
enabled?: boolean;
|
|
25
|
-
}>(value: FeatureToggle<T> | undefined): (T & {
|
|
26
|
-
enabled: boolean;
|
|
27
|
-
}) | undefined;
|
|
28
|
-
export declare function isFeatureEnabled(config: ResolvedDimahS3Config, feature: "upload" | "download" | "delete" | "multipart"): boolean;
|
|
29
|
-
export declare function applyMultipartDefault(config: ResolvedDimahS3Config, multipartInput: FeatureToggle<MultipartConfig> | undefined): ResolvedDimahS3Config;
|
|
30
|
-
export type NormalizedFeatures = {
|
|
31
|
-
upload?: UploadConfig & {
|
|
32
|
-
enabled: boolean;
|
|
33
|
-
};
|
|
34
|
-
download?: DownloadConfig & {
|
|
35
|
-
enabled: boolean;
|
|
36
|
-
};
|
|
37
|
-
delete?: DeleteConfig & {
|
|
38
|
-
enabled: boolean;
|
|
39
|
-
};
|
|
40
|
-
multipart?: MultipartConfig & {
|
|
41
|
-
enabled: boolean;
|
|
42
|
-
};
|
|
43
|
-
};
|
|
44
|
-
export declare function normalizeFeatures(config: {
|
|
45
|
-
upload?: FeatureToggle<UploadConfig>;
|
|
46
|
-
download?: FeatureToggle<DownloadConfig>;
|
|
47
|
-
delete?: FeatureToggle<DeleteConfig>;
|
|
48
|
-
multipart?: FeatureToggle<MultipartConfig>;
|
|
49
|
-
}): NormalizedFeatures;
|
|
50
|
-
export {};
|
|
51
20
|
//# sourceMappingURL=resolve-target.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve-target.d.ts","sourceRoot":"","sources":["../../src/helpers/resolve-target.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,
|
|
1
|
+
{"version":3,"file":"resolve-target.d.ts","sourceRoot":"","sources":["../../src/helpers/resolve-target.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,SAAS,EACT,qBAAqB,EACrB,iBAAiB,EAClB,MAAM,SAAS,CAAC;AAEjB,YAAY,EAAE,SAAS,EAAE,CAAC;AAE1B;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAavD;AASD,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,SAAS,GAAG,SAAS,EAC7B,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,MAAM,CAAC,CAYjB;AAED,wBAAgB,aAAa,CAC3B,MAAM,EAAE,qBAAqB,EAC7B,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,CAQR;AAED,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,qBAAqB,EAC7B,MAAM,EAAE,SAAS,GAAG,SAAS,EAC7B,KAAK,EAAE;IACL,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GACA,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAU1C"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
export { dimahS3 } from "./dimah-s3";
|
|
2
2
|
export type { DimahS3, DimahS3Api, MultipartAliasApi } from "./dimah-s3";
|
|
3
|
-
export { DimahS3Error, errors, isAPIError, isDimahS3Error, isS3ErrorCode, } from "./errors";
|
|
3
|
+
export { DimahS3Error, errors, isAPIError, isDimahS3Error, isS3ErrorCode, S3_ERROR_CODES, } from "./errors";
|
|
4
4
|
export type { ServerErrors } from "./errors";
|
|
5
|
-
export { resolveObjectAcl } from "./helpers/resolve-object-acl";
|
|
6
5
|
export { createS3Endpoint, createS3Middleware } from "./api";
|
|
7
6
|
export type { S3EndpointContext } from "./api";
|
|
8
7
|
export { definePlugin } from "./plugin";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACzE,OAAO,EACL,YAAY,EACZ,MAAM,EACN,UAAU,EACV,cAAc,EACd,aAAa,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACzE,OAAO,EACL,YAAY,EACZ,MAAM,EACN,UAAU,EACV,cAAc,EACd,aAAa,EACb,cAAc,GACf,MAAM,UAAU,CAAC;AAClB,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE7C,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,OAAO,CAAC;AAC7D,YAAY,EAAE,iBAAiB,EAAE,MAAM,OAAO,CAAC;AAE/C,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,YAAY,EACV,aAAa,EACb,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,GACd,MAAM,UAAU,CAAC;AAElB,YAAY,EACV,aAAa,EACb,cAAc,EACd,YAAY,EACZ,aAAa,EACb,SAAS,EACT,eAAe,EACf,qBAAqB,EACrB,YAAY,GACb,MAAM,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,36 +1,11 @@
|
|
|
1
|
-
import { S3_API_ROUTES, isAPIError, DimahS3Error, S3_ERROR_CODES, confirmBodySchema, deleteQuerySchema, downloadQuerySchema, multipartAbortBodySchema, multipartCompleteBodySchema, multipartInitBodySchema, multipartListPartsQuerySchema, multipartSignPartBodySchema, uploadBodySchema, parseFileName, buildContentDisposition, normalizeS3ApiBasePath, S3_API_BASE_PATH } from '@dimah-s3/core';
|
|
2
|
-
export { DimahS3Error, isAPIError, isDimahS3Error, isS3ErrorCode } from '@dimah-s3/core';
|
|
3
|
-
import {
|
|
1
|
+
import { S3_API_ROUTES, isAPIError, DimahS3Error, S3_ERROR_CODES, confirmBodySchema, deleteQuerySchema, downloadQuerySchema, multipartAbortBodySchema, multipartCompleteBodySchema, multipartInitBodySchema, multipartListPartsQuerySchema, multipartSignPartBodySchema, uploadBodySchema, parseFileName, S3_MAX_EXPIRES_IN, buildContentDisposition, S3_MAX_POST_OBJECT_BYTES, normalizeS3ApiBasePath, S3_API_BASE_PATH, S3_DEFAULT_EXPIRES_IN } from '@dimah-s3/core';
|
|
2
|
+
export { DimahS3Error, S3_ERROR_CODES, isAPIError, isDimahS3Error, isS3ErrorCode } from '@dimah-s3/core';
|
|
3
|
+
import { DeleteObjectCommand, GetObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, PutObjectCommand, GetObjectAclCommand, ListPartsCommand, CompleteMultipartUploadCommand, HeadObjectCommand, AbortMultipartUploadCommand } from '@aws-sdk/client-s3';
|
|
4
4
|
import { createMiddleware, createEndpoint, createRouter, toResponse } from 'better-call';
|
|
5
5
|
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
6
6
|
import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
|
|
7
7
|
|
|
8
8
|
// src/dimah-s3.ts
|
|
9
|
-
|
|
10
|
-
// src/helpers/is-aws-not-found.ts
|
|
11
|
-
function isAwsNotFound(err) {
|
|
12
|
-
const e = err;
|
|
13
|
-
return e?.name === "NoSuchKey" || e?.name === "NotFound" || e?.Code === "NoSuchKey" || e?.Code === "NotFound" || e?.$metadata?.httpStatusCode === 404;
|
|
14
|
-
}
|
|
15
|
-
var DEFAULT_ACL_LOOKUP_TIMEOUT_MS = 1500;
|
|
16
|
-
async function resolveObjectAcl(s3, bucket, key, timeoutMs = DEFAULT_ACL_LOOKUP_TIMEOUT_MS) {
|
|
17
|
-
const controller = new AbortController();
|
|
18
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
19
|
-
try {
|
|
20
|
-
const result = await s3.send(
|
|
21
|
-
new GetObjectAclCommand({ Bucket: bucket, Key: key }),
|
|
22
|
-
{ abortSignal: controller.signal }
|
|
23
|
-
);
|
|
24
|
-
const isPublic = result.Grants?.some(
|
|
25
|
-
(g) => g.Grantee?.URI === "http://acs.amazonaws.com/groups/global/AllUsers" && (g.Permission === "READ" || g.Permission === "FULL_CONTROL")
|
|
26
|
-
);
|
|
27
|
-
return isPublic ? "public-read" : "private";
|
|
28
|
-
} catch {
|
|
29
|
-
return void 0;
|
|
30
|
-
} finally {
|
|
31
|
-
clearTimeout(timeout);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
9
|
var errors = {
|
|
35
10
|
notFound: () => DimahS3Error.from("NOT_FOUND", S3_ERROR_CODES.NOT_FOUND),
|
|
36
11
|
unauthorized: () => DimahS3Error.from("UNAUTHORIZED", S3_ERROR_CODES.UNAUTHORIZED),
|
|
@@ -65,13 +40,69 @@ var errors = {
|
|
|
65
40
|
"BAD_REQUEST",
|
|
66
41
|
S3_ERROR_CODES.FILE_SIZE_REQUIRED_MULTIPART
|
|
67
42
|
),
|
|
43
|
+
multipartPartMissing: (partNumber) => DimahS3Error.from("BAD_REQUEST", {
|
|
44
|
+
code: S3_ERROR_CODES.MULTIPART_PART_MISSING.code,
|
|
45
|
+
message: S3_ERROR_CODES.MULTIPART_PART_MISSING.message.replaceAll(
|
|
46
|
+
"{partNumber}",
|
|
47
|
+
String(partNumber)
|
|
48
|
+
),
|
|
49
|
+
params: { partNumber }
|
|
50
|
+
}),
|
|
51
|
+
payloadTooLarge: (message = S3_ERROR_CODES.PAYLOAD_TOO_LARGE.message) => DimahS3Error.from("PAYLOAD_TOO_LARGE", {
|
|
52
|
+
...S3_ERROR_CODES.PAYLOAD_TOO_LARGE,
|
|
53
|
+
message
|
|
54
|
+
}),
|
|
68
55
|
validationError: (message) => DimahS3Error.from("BAD_REQUEST", {
|
|
69
56
|
code: S3_ERROR_CODES.VALIDATION_ERROR.code,
|
|
70
57
|
message
|
|
71
58
|
})
|
|
72
59
|
};
|
|
73
60
|
|
|
74
|
-
// src/
|
|
61
|
+
// src/helpers/is-aws-not-found.ts
|
|
62
|
+
function isAwsNotFound(err) {
|
|
63
|
+
const e = err;
|
|
64
|
+
return e?.name === "NoSuchKey" || e?.name === "NotFound" || e?.name === "NoSuchUpload" || e?.Code === "NoSuchKey" || e?.Code === "NotFound" || e?.Code === "NoSuchUpload" || e?.$metadata?.httpStatusCode === 404;
|
|
65
|
+
}
|
|
66
|
+
async function sendOrObjectNotFound(send) {
|
|
67
|
+
try {
|
|
68
|
+
return await send();
|
|
69
|
+
} catch (err) {
|
|
70
|
+
if (isAwsNotFound(err)) throw errors.objectNotFound();
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
var DEFAULT_ACL_LOOKUP_TIMEOUT_MS = 1500;
|
|
75
|
+
async function resolveObjectAcl(s3, bucket, key, timeoutMs = DEFAULT_ACL_LOOKUP_TIMEOUT_MS) {
|
|
76
|
+
const controller = new AbortController();
|
|
77
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
78
|
+
try {
|
|
79
|
+
const result = await s3.send(
|
|
80
|
+
new GetObjectAclCommand({ Bucket: bucket, Key: key }),
|
|
81
|
+
{ abortSignal: controller.signal }
|
|
82
|
+
);
|
|
83
|
+
const isPublic = result.Grants?.some(
|
|
84
|
+
(g) => g.Grantee?.URI === "http://acs.amazonaws.com/groups/global/AllUsers" && (g.Permission === "READ" || g.Permission === "FULL_CONTROL")
|
|
85
|
+
);
|
|
86
|
+
return isPublic ? "public-read" : "private";
|
|
87
|
+
} catch {
|
|
88
|
+
return void 0;
|
|
89
|
+
} finally {
|
|
90
|
+
clearTimeout(timeout);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/helpers/resolve-request-acl.ts
|
|
95
|
+
function resolveRequestAcl(policy, requested) {
|
|
96
|
+
if (policy?.acl) return policy.acl;
|
|
97
|
+
if (policy?.allowClientAcl && requested) return requested;
|
|
98
|
+
return "private";
|
|
99
|
+
}
|
|
100
|
+
function normalizeExpiresIn(value, maxExpiresIn = S3_MAX_EXPIRES_IN) {
|
|
101
|
+
const n = Number(value);
|
|
102
|
+
const expires = Number.isFinite(n) && n > 0 ? Math.floor(n) : S3_DEFAULT_EXPIRES_IN;
|
|
103
|
+
const max = Number.isFinite(maxExpiresIn) && maxExpiresIn > 0 ? Math.floor(maxExpiresIn) : S3_MAX_EXPIRES_IN;
|
|
104
|
+
return Math.min(expires, max);
|
|
105
|
+
}
|
|
75
106
|
async function runHook(hook, context) {
|
|
76
107
|
if (!hook) return;
|
|
77
108
|
try {
|
|
@@ -97,10 +128,8 @@ async function runLifecycleHook(hook, context) {
|
|
|
97
128
|
throw errors.internalError();
|
|
98
129
|
}
|
|
99
130
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 600;
|
|
103
|
-
}
|
|
131
|
+
|
|
132
|
+
// src/helpers/request.ts
|
|
104
133
|
function requestFromHeaders(headers) {
|
|
105
134
|
return new Request("http://dimah-s3.local", { headers });
|
|
106
135
|
}
|
|
@@ -156,11 +185,56 @@ async function resolveRequestTarget(config, policy, input) {
|
|
|
156
185
|
});
|
|
157
186
|
return { key, bucket };
|
|
158
187
|
}
|
|
188
|
+
async function headObjectOrNotFound(client, bucket, key) {
|
|
189
|
+
return sendOrObjectNotFound(
|
|
190
|
+
() => client.send(new HeadObjectCommand({ Bucket: bucket, Key: key }))
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
async function headObjectAfterMultipartComplete(client, bucket, key) {
|
|
194
|
+
const send = () => sendOrObjectNotFound(
|
|
195
|
+
() => client.send(new HeadObjectCommand({ Bucket: bucket, Key: key }))
|
|
196
|
+
);
|
|
197
|
+
let head = await send();
|
|
198
|
+
for (let attempt = 0; attempt < 4 && !head.ContentLength; attempt++) {
|
|
199
|
+
await new Promise((r) => setTimeout(r, 250 * 2 ** attempt));
|
|
200
|
+
head = await send();
|
|
201
|
+
}
|
|
202
|
+
return head;
|
|
203
|
+
}
|
|
204
|
+
function requireContentLength(head) {
|
|
205
|
+
if (typeof head.ContentLength !== "number") {
|
|
206
|
+
throw errors.internalError();
|
|
207
|
+
}
|
|
208
|
+
return head.ContentLength;
|
|
209
|
+
}
|
|
210
|
+
async function listAllParts(client, input) {
|
|
211
|
+
const parts = [];
|
|
212
|
+
let partNumberMarker;
|
|
213
|
+
let isTruncated = true;
|
|
214
|
+
while (isTruncated) {
|
|
215
|
+
const response = await sendOrObjectNotFound(
|
|
216
|
+
() => client.send(
|
|
217
|
+
new ListPartsCommand({
|
|
218
|
+
Bucket: input.bucket,
|
|
219
|
+
Key: input.key,
|
|
220
|
+
UploadId: input.uploadId,
|
|
221
|
+
...partNumberMarker ? { PartNumberMarker: partNumberMarker } : {}
|
|
222
|
+
})
|
|
223
|
+
)
|
|
224
|
+
);
|
|
225
|
+
parts.push(...response.Parts ?? []);
|
|
226
|
+
isTruncated = response.IsTruncated === true;
|
|
227
|
+
partNumberMarker = isTruncated ? response.NextPartNumberMarker : void 0;
|
|
228
|
+
}
|
|
229
|
+
return parts;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/helpers/features.ts
|
|
159
233
|
function normalizeFeature(value) {
|
|
160
234
|
if (value === void 0) return void 0;
|
|
161
235
|
if (value === false) return { enabled: false };
|
|
162
236
|
if (value === true) return { enabled: true };
|
|
163
|
-
return { ...value, enabled:
|
|
237
|
+
return { ...value, enabled: true };
|
|
164
238
|
}
|
|
165
239
|
function isFeatureEnabled(config, feature) {
|
|
166
240
|
return config[feature]?.enabled === true;
|
|
@@ -179,7 +253,10 @@ function applyMultipartDefault(config, multipartInput) {
|
|
|
179
253
|
multipart: {
|
|
180
254
|
...next.multipart,
|
|
181
255
|
prefix: next.multipart.prefix ?? next.upload.prefix,
|
|
182
|
-
resolveKey: next.multipart.resolveKey ?? next.upload.resolveKey
|
|
256
|
+
resolveKey: next.multipart.resolveKey ?? next.upload.resolveKey,
|
|
257
|
+
acl: next.multipart.acl ?? next.upload.acl,
|
|
258
|
+
allowClientAcl: next.multipart.allowClientAcl ?? next.upload.allowClientAcl,
|
|
259
|
+
requireFileSize: next.multipart.requireFileSize ?? next.upload.requireFileSize
|
|
183
260
|
}
|
|
184
261
|
};
|
|
185
262
|
}
|
|
@@ -193,6 +270,13 @@ function normalizeFeatures(config) {
|
|
|
193
270
|
multipart: normalizeFeature(config.multipart)
|
|
194
271
|
};
|
|
195
272
|
}
|
|
273
|
+
function assertExclusiveBucketFlags(config) {
|
|
274
|
+
if (config.allowClientBucket && config.buckets?.length) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
"dimahS3: set either allowClientBucket or buckets, not both. allowClientBucket honors any client-sent bucket; buckets is an allowlist."
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
196
280
|
|
|
197
281
|
// src/api/assert-feature-enabled.ts
|
|
198
282
|
function assertFeatureEnabled(config, feature) {
|
|
@@ -256,9 +340,7 @@ async function handleConfirm(config, input, request) {
|
|
|
256
340
|
key,
|
|
257
341
|
bucket
|
|
258
342
|
});
|
|
259
|
-
const head = await config.client
|
|
260
|
-
new HeadObjectCommand({ Bucket: bucket, Key: key })
|
|
261
|
-
);
|
|
343
|
+
const head = await headObjectOrNotFound(config.client, bucket, key);
|
|
262
344
|
const acl = config.resolveObjectAcl ? await resolveObjectAcl(config.client, bucket, key) : void 0;
|
|
263
345
|
const fileName = parseFileName(head.ContentDisposition);
|
|
264
346
|
const context = {
|
|
@@ -266,7 +348,7 @@ async function handleConfirm(config, input, request) {
|
|
|
266
348
|
key,
|
|
267
349
|
bucket,
|
|
268
350
|
contentType: head.ContentType,
|
|
269
|
-
contentLength: head
|
|
351
|
+
contentLength: requireContentLength(head),
|
|
270
352
|
eTag: head.ETag?.replace(/"/g, ""),
|
|
271
353
|
metadata: head.Metadata,
|
|
272
354
|
acl,
|
|
@@ -307,16 +389,7 @@ async function handleDelete(config, input, request) {
|
|
|
307
389
|
key,
|
|
308
390
|
bucket
|
|
309
391
|
});
|
|
310
|
-
|
|
311
|
-
await config.client.send(
|
|
312
|
-
new HeadObjectCommand({ Bucket: bucket, Key: key })
|
|
313
|
-
);
|
|
314
|
-
} catch (err) {
|
|
315
|
-
if (isAwsNotFound(err)) {
|
|
316
|
-
throw errors.objectNotFound();
|
|
317
|
-
}
|
|
318
|
-
throw err;
|
|
319
|
-
}
|
|
392
|
+
await headObjectOrNotFound(config.client, bucket, key);
|
|
320
393
|
await config.client.send(
|
|
321
394
|
new DeleteObjectCommand({ Bucket: bucket, Key: key })
|
|
322
395
|
);
|
|
@@ -338,7 +411,7 @@ async function handleDownload(config, input, request) {
|
|
|
338
411
|
bucket: input.bucket,
|
|
339
412
|
fileName: input.fileName
|
|
340
413
|
});
|
|
341
|
-
const expiresIn = normalizeExpiresIn(input.expiresIn);
|
|
414
|
+
const expiresIn = normalizeExpiresIn(input.expiresIn, config.maxExpiresIn);
|
|
342
415
|
const fileName = input.fileName;
|
|
343
416
|
await runHook(config.download?.guard, {
|
|
344
417
|
request,
|
|
@@ -346,16 +419,7 @@ async function handleDownload(config, input, request) {
|
|
|
346
419
|
bucket,
|
|
347
420
|
fileName
|
|
348
421
|
});
|
|
349
|
-
|
|
350
|
-
await config.client.send(
|
|
351
|
-
new HeadObjectCommand({ Bucket: bucket, Key: key })
|
|
352
|
-
);
|
|
353
|
-
} catch (err) {
|
|
354
|
-
if (isAwsNotFound(err)) {
|
|
355
|
-
throw errors.objectNotFound();
|
|
356
|
-
}
|
|
357
|
-
throw err;
|
|
358
|
-
}
|
|
422
|
+
await headObjectOrNotFound(config.client, bucket, key);
|
|
359
423
|
const url = await getSignedUrl(
|
|
360
424
|
config.client,
|
|
361
425
|
new GetObjectCommand({
|
|
@@ -396,12 +460,14 @@ async function handleAbort(config, input, request) {
|
|
|
396
460
|
bucket,
|
|
397
461
|
uploadId
|
|
398
462
|
});
|
|
399
|
-
await
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
463
|
+
await sendOrObjectNotFound(
|
|
464
|
+
() => config.client.send(
|
|
465
|
+
new AbortMultipartUploadCommand({
|
|
466
|
+
Bucket: bucket,
|
|
467
|
+
Key: key,
|
|
468
|
+
UploadId: uploadId
|
|
469
|
+
})
|
|
470
|
+
)
|
|
405
471
|
);
|
|
406
472
|
await runLifecycleHook(config.multipart?.onAbort, {
|
|
407
473
|
request,
|
|
@@ -435,32 +501,34 @@ async function handleComplete(config, input, request) {
|
|
|
435
501
|
uploadId,
|
|
436
502
|
parts: partRefs
|
|
437
503
|
});
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
504
|
+
const listedParts = await listAllParts(config.client, {
|
|
505
|
+
bucket,
|
|
506
|
+
key,
|
|
507
|
+
uploadId
|
|
508
|
+
});
|
|
442
509
|
const completeParts = parts.map((partNumber) => {
|
|
443
510
|
const found = listedParts.find((p) => p.PartNumber === partNumber);
|
|
444
|
-
|
|
511
|
+
if (!found?.ETag) {
|
|
512
|
+
throw errors.multipartPartMissing(partNumber);
|
|
513
|
+
}
|
|
514
|
+
return { PartNumber: partNumber, ETag: found.ETag };
|
|
445
515
|
});
|
|
446
|
-
const completeResult = await
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
516
|
+
const completeResult = await sendOrObjectNotFound(
|
|
517
|
+
() => config.client.send(
|
|
518
|
+
new CompleteMultipartUploadCommand({
|
|
519
|
+
Bucket: bucket,
|
|
520
|
+
Key: key,
|
|
521
|
+
UploadId: uploadId,
|
|
522
|
+
MultipartUpload: { Parts: completeParts }
|
|
523
|
+
})
|
|
524
|
+
)
|
|
453
525
|
);
|
|
454
|
-
|
|
455
|
-
|
|
526
|
+
const head = await headObjectAfterMultipartComplete(
|
|
527
|
+
config.client,
|
|
528
|
+
bucket,
|
|
529
|
+
key
|
|
456
530
|
);
|
|
457
|
-
|
|
458
|
-
await new Promise((r) => setTimeout(r, 250 * 2 ** attempt));
|
|
459
|
-
head = await config.client.send(
|
|
460
|
-
new HeadObjectCommand({ Bucket: bucket, Key: key })
|
|
461
|
-
);
|
|
462
|
-
}
|
|
463
|
-
const contentLength = head.ContentLength ?? 0;
|
|
531
|
+
const contentLength = requireContentLength(head);
|
|
464
532
|
const contentType = head.ContentType;
|
|
465
533
|
const eTag = (head.ETag ?? completeResult.ETag ?? "").replace(/"/g, "");
|
|
466
534
|
const metadata = head.Metadata ?? {};
|
|
@@ -512,7 +580,7 @@ async function handleMultipartInit(config, input, request) {
|
|
|
512
580
|
fileName: input.fileName,
|
|
513
581
|
contentType: input.contentType
|
|
514
582
|
});
|
|
515
|
-
const acl = input.acl
|
|
583
|
+
const acl = resolveRequestAcl(config.multipart, input.acl);
|
|
516
584
|
const fileSize = typeof input.fileSize === "number" && input.fileSize > 0 ? Math.floor(input.fileSize) : void 0;
|
|
517
585
|
if (config.multipart?.requireFileSize && fileSize === void 0) {
|
|
518
586
|
throw errors.fileSizeRequiredMultipart();
|
|
@@ -521,7 +589,11 @@ async function handleMultipartInit(config, input, request) {
|
|
|
521
589
|
request,
|
|
522
590
|
key,
|
|
523
591
|
bucket,
|
|
524
|
-
fileSize
|
|
592
|
+
fileSize,
|
|
593
|
+
contentType: input.contentType,
|
|
594
|
+
metadata: input.metadata,
|
|
595
|
+
acl,
|
|
596
|
+
fileName: input.fileName
|
|
525
597
|
});
|
|
526
598
|
const { UploadId } = await config.client.send(
|
|
527
599
|
new CreateMultipartUploadCommand({
|
|
@@ -533,6 +605,9 @@ async function handleMultipartInit(config, input, request) {
|
|
|
533
605
|
ACL: acl
|
|
534
606
|
})
|
|
535
607
|
);
|
|
608
|
+
if (!UploadId) {
|
|
609
|
+
throw errors.internalError();
|
|
610
|
+
}
|
|
536
611
|
await runLifecycleHook(config.multipart?.onInit, {
|
|
537
612
|
request,
|
|
538
613
|
key,
|
|
@@ -572,14 +647,8 @@ async function handleListParts(config, input, request) {
|
|
|
572
647
|
bucket,
|
|
573
648
|
uploadId
|
|
574
649
|
});
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
Bucket: bucket,
|
|
578
|
-
Key: key,
|
|
579
|
-
UploadId: uploadId
|
|
580
|
-
})
|
|
581
|
-
);
|
|
582
|
-
const parts = (response.Parts ?? []).map((p) => ({
|
|
650
|
+
const listed = await listAllParts(config.client, { bucket, key, uploadId });
|
|
651
|
+
const parts = listed.map((p) => ({
|
|
583
652
|
partNumber: p.PartNumber ?? 0,
|
|
584
653
|
size: p.Size ?? 0,
|
|
585
654
|
eTag: (p.ETag ?? "").replace(/"/g, "")
|
|
@@ -609,7 +678,7 @@ async function handleSignPart(config, input, request) {
|
|
|
609
678
|
});
|
|
610
679
|
const uploadId = input.uploadId;
|
|
611
680
|
const partNumber = input.partNumber;
|
|
612
|
-
const expiresIn = normalizeExpiresIn(input.expiresIn);
|
|
681
|
+
const expiresIn = normalizeExpiresIn(input.expiresIn, config.maxExpiresIn);
|
|
613
682
|
const partSize = typeof input.partSize === "number" && input.partSize > 0 ? Math.floor(input.partSize) : null;
|
|
614
683
|
await runHook(config.multipart?.partGuard, {
|
|
615
684
|
request,
|
|
@@ -650,6 +719,14 @@ var multipartPart = createS3Endpoint(
|
|
|
650
719
|
return handleSignPart(ctx.context.config, ctx.body, ctx.context.request);
|
|
651
720
|
}
|
|
652
721
|
);
|
|
722
|
+
function putMetadataHeaders(metadata) {
|
|
723
|
+
if (!metadata) return {};
|
|
724
|
+
const headers = {};
|
|
725
|
+
for (const [k, v] of Object.entries(metadata)) {
|
|
726
|
+
headers[`x-amz-meta-${k}`] = v;
|
|
727
|
+
}
|
|
728
|
+
return headers;
|
|
729
|
+
}
|
|
653
730
|
async function handleUpload(config, input, request) {
|
|
654
731
|
const { key, bucket } = await resolveRequestTarget(config, config.upload, {
|
|
655
732
|
request,
|
|
@@ -658,10 +735,13 @@ async function handleUpload(config, input, request) {
|
|
|
658
735
|
fileName: input.fileName,
|
|
659
736
|
contentType: input.contentType
|
|
660
737
|
});
|
|
661
|
-
const expiresIn = normalizeExpiresIn(input.expiresIn);
|
|
662
|
-
const acl = input.acl
|
|
738
|
+
const expiresIn = normalizeExpiresIn(input.expiresIn, config.maxExpiresIn);
|
|
739
|
+
const acl = resolveRequestAcl(config.upload, input.acl);
|
|
663
740
|
const contentType = input.contentType ?? "application/octet-stream";
|
|
664
741
|
const fileSize = typeof input.fileSize === "number" && input.fileSize > 0 ? Math.floor(input.fileSize) : null;
|
|
742
|
+
if (config.upload?.requireFileSize && fileSize === null) {
|
|
743
|
+
throw errors.fileSizeRequiredUpload();
|
|
744
|
+
}
|
|
665
745
|
await runHook(config.upload?.guard, {
|
|
666
746
|
request,
|
|
667
747
|
key,
|
|
@@ -673,12 +753,10 @@ async function handleUpload(config, input, request) {
|
|
|
673
753
|
fileName: input.fileName
|
|
674
754
|
});
|
|
675
755
|
const method = config.upload?.method ?? "POST";
|
|
676
|
-
if (method === "PUT" && config.upload?.requireFileSize && fileSize === null) {
|
|
677
|
-
throw errors.fileSizeRequiredUpload();
|
|
678
|
-
}
|
|
679
756
|
if (method === "PUT") {
|
|
680
757
|
const putHeaders = {
|
|
681
|
-
"Content-Type": contentType
|
|
758
|
+
"Content-Type": contentType,
|
|
759
|
+
...putMetadataHeaders(input.metadata)
|
|
682
760
|
};
|
|
683
761
|
if (input.fileName) {
|
|
684
762
|
putHeaders["Content-Disposition"] = buildContentDisposition(
|
|
@@ -692,6 +770,7 @@ async function handleUpload(config, input, request) {
|
|
|
692
770
|
Key: key,
|
|
693
771
|
ContentType: contentType,
|
|
694
772
|
ACL: acl,
|
|
773
|
+
Metadata: input.metadata,
|
|
695
774
|
...input.fileName ? { ContentDisposition: buildContentDisposition(input.fileName) } : {},
|
|
696
775
|
...fileSize !== null ? { ContentLength: fileSize } : {}
|
|
697
776
|
}),
|
|
@@ -737,7 +816,7 @@ async function handleUpload(config, input, request) {
|
|
|
737
816
|
{
|
|
738
817
|
Bucket: bucket,
|
|
739
818
|
Key: key,
|
|
740
|
-
Conditions: rangeMax !== void 0 ? [["content-length-range", rangeMin, rangeMax]] : [["content-length-range", rangeMin,
|
|
819
|
+
Conditions: rangeMax !== void 0 ? [["content-length-range", rangeMin, rangeMax]] : [["content-length-range", rangeMin, S3_MAX_POST_OBJECT_BYTES]],
|
|
741
820
|
Fields: fields,
|
|
742
821
|
Expires: expiresIn
|
|
743
822
|
}
|
|
@@ -939,6 +1018,7 @@ function applyPlugins(config) {
|
|
|
939
1018
|
}
|
|
940
1019
|
}
|
|
941
1020
|
const { plugins: _omit, ...rest } = config;
|
|
1021
|
+
assertExclusiveBucketFlags(rest);
|
|
942
1022
|
for (const plugin of plugins) {
|
|
943
1023
|
plugin.init?.({ config: rest, getPlugin });
|
|
944
1024
|
}
|
|
@@ -991,10 +1071,7 @@ function bindEndpoints(endpoints, context) {
|
|
|
991
1071
|
...input,
|
|
992
1072
|
headers: headers ?? request.headers,
|
|
993
1073
|
request,
|
|
994
|
-
context
|
|
995
|
-
...context,
|
|
996
|
-
...typeof input.context === "object" && input.context !== null ? input.context : {}
|
|
997
|
-
}
|
|
1074
|
+
context
|
|
998
1075
|
});
|
|
999
1076
|
});
|
|
1000
1077
|
bound.path = endpoint.path;
|
|
@@ -1004,7 +1081,7 @@ function bindEndpoints(endpoints, context) {
|
|
|
1004
1081
|
return api;
|
|
1005
1082
|
}
|
|
1006
1083
|
|
|
1007
|
-
// src/router.ts
|
|
1084
|
+
// src/api/router.ts
|
|
1008
1085
|
var NETWORK_CODES = /* @__PURE__ */ new Set([
|
|
1009
1086
|
"EAI_AGAIN",
|
|
1010
1087
|
"ECONNREFUSED",
|
|
@@ -1080,6 +1157,6 @@ function dimahS3(config) {
|
|
|
1080
1157
|
};
|
|
1081
1158
|
}
|
|
1082
1159
|
|
|
1083
|
-
export { createS3Endpoint, createS3Middleware, definePlugin, dimahS3, errors
|
|
1160
|
+
export { createS3Endpoint, createS3Middleware, definePlugin, dimahS3, errors };
|
|
1084
1161
|
//# sourceMappingURL=index.js.map
|
|
1085
1162
|
//# sourceMappingURL=index.js.map
|