@aws-blocks/bb-file-bucket 0.1.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/LICENSE +174 -0
- package/README.md +197 -0
- package/dist/bucket-name.d.ts +9 -0
- package/dist/bucket-name.d.ts.map +1 -0
- package/dist/bucket-name.js +62 -0
- package/dist/bucket-name.test.d.ts +2 -0
- package/dist/bucket-name.test.d.ts.map +1 -0
- package/dist/bucket-name.test.js +61 -0
- package/dist/errors.d.ts +20 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +21 -0
- package/dist/file-server.d.ts +14 -0
- package/dist/file-server.d.ts.map +1 -0
- package/dist/file-server.js +169 -0
- package/dist/file-server.test.d.ts +2 -0
- package/dist/file-server.test.d.ts.map +1 -0
- package/dist/file-server.test.js +307 -0
- package/dist/index.aws.d.ts +47 -0
- package/dist/index.aws.d.ts.map +1 -0
- package/dist/index.aws.js +183 -0
- package/dist/index.browser.d.ts +4 -0
- package/dist/index.browser.d.ts.map +1 -0
- package/dist/index.browser.js +6 -0
- package/dist/index.cdk.d.ts +16 -0
- package/dist/index.cdk.d.ts.map +1 -0
- package/dist/index.cdk.js +75 -0
- package/dist/index.cdk.test.d.ts +2 -0
- package/dist/index.cdk.test.d.ts.map +1 -0
- package/dist/index.cdk.test.js +69 -0
- package/dist/index.mock.d.ts +246 -0
- package/dist/index.mock.d.ts.map +1 -0
- package/dist/index.mock.js +502 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +318 -0
- package/dist/middleware.d.ts +3 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +62 -0
- package/dist/mock-middleware.d.ts +3 -0
- package/dist/mock-middleware.d.ts.map +1 -0
- package/dist/mock-middleware.js +62 -0
- package/dist/mock-utils.d.ts +11 -0
- package/dist/mock-utils.d.ts.map +1 -0
- package/dist/mock-utils.js +28 -0
- package/dist/path-containment.test.d.ts +2 -0
- package/dist/path-containment.test.d.ts.map +1 -0
- package/dist/path-containment.test.js +91 -0
- package/dist/paths.d.ts +25 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +67 -0
- package/dist/scan.test.d.ts +2 -0
- package/dist/scan.test.d.ts.map +1 -0
- package/dist/scan.test.js +107 -0
- package/dist/tokens.d.ts +12 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +42 -0
- package/dist/types.d.ts +170 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/url-encoding.test.d.ts +2 -0
- package/dist/url-encoding.test.d.ts.map +1 -0
- package/dist/url-encoding.test.js +88 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/package.json +57 -0
- package/src/bucket-name.test.ts +103 -0
- package/src/bucket-name.ts +83 -0
- package/src/errors.ts +22 -0
- package/src/file-server.test.ts +381 -0
- package/src/file-server.ts +203 -0
- package/src/index.aws.ts +219 -0
- package/src/index.browser.ts +7 -0
- package/src/index.cdk.test.ts +84 -0
- package/src/index.cdk.ts +89 -0
- package/src/index.mock.ts +531 -0
- package/src/index.test.ts +366 -0
- package/src/middleware.ts +66 -0
- package/src/mock-middleware.ts +66 -0
- package/src/mock-utils.ts +31 -0
- package/src/path-containment.test.ts +122 -0
- package/src/paths.ts +78 -0
- package/src/scan.test.ts +137 -0
- package/src/tokens.ts +61 -0
- package/src/types.ts +206 -0
- package/src/url-encoding.test.ts +120 -0
- package/src/version.ts +3 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, DeleteObjectsCommand, ListObjectsV2Command, ListObjectVersionsCommand, CopyObjectCommand, } from '@aws-sdk/client-s3';
|
|
4
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
5
|
+
import { Scope, registerSdkIdentifiers, getSdkIdentifiers } from '@aws-blocks/core';
|
|
6
|
+
import { BB_NAME, BB_VERSION } from './version.js';
|
|
7
|
+
import { Logger } from '@aws-blocks/bb-logger';
|
|
8
|
+
// Re-export public types
|
|
9
|
+
export { FileBucketErrors } from './errors.js';
|
|
10
|
+
/**
|
|
11
|
+
* File storage backed by Amazon S3.
|
|
12
|
+
*
|
|
13
|
+
* **When to use:** You need to store, retrieve, or serve binary files —
|
|
14
|
+
* user uploads, generated reports, images, videos, or static assets.
|
|
15
|
+
*
|
|
16
|
+
* **When NOT to use:** If you need structured key-value data with conditional
|
|
17
|
+
* writes, use `KVStore`. If you need queryable records with indexes, use
|
|
18
|
+
* `DistributedTable`.
|
|
19
|
+
*
|
|
20
|
+
* **Best practices:**
|
|
21
|
+
* - Use path prefixes to organize files (e.g., `uploads/{userId}/`, `reports/`)
|
|
22
|
+
* - Set `contentType` on `put()` to ensure correct MIME handling on download
|
|
23
|
+
* - Use `getFileHandle` / `createUploadHandle` for ergonomic browser file transfers
|
|
24
|
+
* - Use presigned URLs (`getUrl` / `putUrl`) when you need direct URL control
|
|
25
|
+
* - Prefer `scan({ prefix })` over unscoped `scan()` to limit enumeration cost
|
|
26
|
+
*
|
|
27
|
+
* **Scaling:** S3 scales automatically. No provisioned throughput. Costs are
|
|
28
|
+
* per-request plus storage. Individual objects up to 5 TB. For objects larger
|
|
29
|
+
* than ~100 MB, consider multipart upload.
|
|
30
|
+
*/
|
|
31
|
+
export class FileBucket extends Scope {
|
|
32
|
+
bbName = BB_NAME;
|
|
33
|
+
s3;
|
|
34
|
+
/** @internal Logger for internal operations. Defaults to error-level when not provided. */
|
|
35
|
+
log;
|
|
36
|
+
constructor(scope, id, options) {
|
|
37
|
+
super(id, { parent: scope, bbName: BB_NAME, bbVersion: BB_VERSION });
|
|
38
|
+
this.log = options?.logger ?? new Logger(this, 'logger', { level: 'error' });
|
|
39
|
+
this.registerClientMiddleware('@aws-blocks/bb-file-bucket/middleware');
|
|
40
|
+
const bucketName = options?.bucket ? options.bucket.bucketName : this.fullId;
|
|
41
|
+
registerSdkIdentifiers(this.fullId, { bucketName });
|
|
42
|
+
this.s3 = new S3Client({
|
|
43
|
+
customUserAgent: this.buildUserAgentChain(),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async put(path, body, options) {
|
|
47
|
+
await this.s3.send(new PutObjectCommand({
|
|
48
|
+
Bucket: getSdkIdentifiers(this).bucketName,
|
|
49
|
+
Key: path,
|
|
50
|
+
Body: typeof body === 'string' ? Buffer.from(body) : body,
|
|
51
|
+
ContentType: options?.contentType,
|
|
52
|
+
Metadata: options?.metadata,
|
|
53
|
+
CacheControl: options?.cacheControl,
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
async get(path, options) {
|
|
57
|
+
try {
|
|
58
|
+
const result = await this.s3.send(new GetObjectCommand({
|
|
59
|
+
Bucket: getSdkIdentifiers(this).bucketName, Key: path,
|
|
60
|
+
...(options ? { VersionId: options.versionId } : {}),
|
|
61
|
+
}));
|
|
62
|
+
const bytes = await result.Body.transformToByteArray();
|
|
63
|
+
return {
|
|
64
|
+
body: Buffer.from(bytes),
|
|
65
|
+
contentType: result.ContentType ?? 'application/octet-stream',
|
|
66
|
+
metadata: result.Metadata ?? {},
|
|
67
|
+
size: result.ContentLength ?? bytes.length,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
if (e.name === 'NoSuchKey')
|
|
72
|
+
return null;
|
|
73
|
+
throw e;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async delete(path, options) {
|
|
77
|
+
await this.s3.send(new DeleteObjectCommand({
|
|
78
|
+
Bucket: getSdkIdentifiers(this).bucketName, Key: path,
|
|
79
|
+
...(options ? { VersionId: options.versionId } : {}),
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
async deleteBatch(paths) {
|
|
83
|
+
const CHUNK_SIZE = 1000;
|
|
84
|
+
for (let i = 0; i < paths.length; i += CHUNK_SIZE) {
|
|
85
|
+
const chunk = paths.slice(i, i + CHUNK_SIZE);
|
|
86
|
+
await this.s3.send(new DeleteObjectsCommand({
|
|
87
|
+
Bucket: getSdkIdentifiers(this).bucketName,
|
|
88
|
+
Delete: { Objects: chunk.map(Key => ({ Key })), Quiet: true },
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async getUrl(path, options) {
|
|
93
|
+
const opts = options;
|
|
94
|
+
return getSignedUrl(this.s3, new GetObjectCommand({
|
|
95
|
+
Bucket: getSdkIdentifiers(this).bucketName, Key: path,
|
|
96
|
+
...(opts?.versionId ? { VersionId: opts.versionId } : {}),
|
|
97
|
+
}), {
|
|
98
|
+
expiresIn: opts?.expiresIn ?? 3600,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async putUrl(path, options) {
|
|
102
|
+
return getSignedUrl(this.s3, new PutObjectCommand({
|
|
103
|
+
Bucket: getSdkIdentifiers(this).bucketName, Key: path, ContentType: options?.contentType,
|
|
104
|
+
}), { expiresIn: options?.expiresIn ?? 3600 });
|
|
105
|
+
}
|
|
106
|
+
async getFileHandle(path, options) {
|
|
107
|
+
const url = await this.getUrl(path, options);
|
|
108
|
+
return {
|
|
109
|
+
download: async () => {
|
|
110
|
+
const res = await fetch(url);
|
|
111
|
+
if (!res.ok)
|
|
112
|
+
throw new Error(`Download failed: ${res.status}`);
|
|
113
|
+
return res.blob();
|
|
114
|
+
},
|
|
115
|
+
getUrl: () => url,
|
|
116
|
+
toJSON: () => ({ __blocks: 'file-bucket/download', url }),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
async createUploadHandle(path, options) {
|
|
120
|
+
const url = await this.putUrl(path, options);
|
|
121
|
+
const contentType = options?.contentType;
|
|
122
|
+
return {
|
|
123
|
+
upload: async (body) => {
|
|
124
|
+
const headers = {};
|
|
125
|
+
if (contentType)
|
|
126
|
+
headers['Content-Type'] = contentType;
|
|
127
|
+
const res = await fetch(url, { method: 'PUT', body, headers });
|
|
128
|
+
if (!res.ok)
|
|
129
|
+
throw new Error(`Upload failed: ${res.status}`);
|
|
130
|
+
},
|
|
131
|
+
getUrl: () => url,
|
|
132
|
+
toJSON: () => ({ __blocks: 'file-bucket/upload', url, contentType }),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
async *scan(options) {
|
|
136
|
+
let continuationToken;
|
|
137
|
+
do {
|
|
138
|
+
const result = await this.s3.send(new ListObjectsV2Command({
|
|
139
|
+
Bucket: getSdkIdentifiers(this).bucketName, Prefix: options?.prefix, ContinuationToken: continuationToken,
|
|
140
|
+
}));
|
|
141
|
+
for (const obj of result.Contents ?? []) {
|
|
142
|
+
yield { path: obj.Key, size: obj.Size ?? 0, lastModified: obj.LastModified ?? new Date() };
|
|
143
|
+
}
|
|
144
|
+
continuationToken = result.NextContinuationToken;
|
|
145
|
+
} while (continuationToken);
|
|
146
|
+
}
|
|
147
|
+
async listVersions(path) {
|
|
148
|
+
const versions = [];
|
|
149
|
+
let keyMarker;
|
|
150
|
+
let versionIdMarker;
|
|
151
|
+
do {
|
|
152
|
+
const result = await this.s3.send(new ListObjectVersionsCommand({
|
|
153
|
+
Bucket: getSdkIdentifiers(this).bucketName, Prefix: path, KeyMarker: keyMarker, VersionIdMarker: versionIdMarker,
|
|
154
|
+
}));
|
|
155
|
+
for (const v of result.Versions ?? []) {
|
|
156
|
+
if (v.Key !== path)
|
|
157
|
+
continue; // prefix match may include other keys
|
|
158
|
+
versions.push({
|
|
159
|
+
versionId: v.VersionId,
|
|
160
|
+
lastModified: v.LastModified ?? new Date(),
|
|
161
|
+
size: v.Size ?? 0,
|
|
162
|
+
isCurrent: v.IsLatest ?? false,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
keyMarker = result.NextKeyMarker;
|
|
166
|
+
versionIdMarker = result.NextVersionIdMarker;
|
|
167
|
+
} while (keyMarker);
|
|
168
|
+
// Newest first
|
|
169
|
+
versions.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
170
|
+
return versions;
|
|
171
|
+
}
|
|
172
|
+
async restoreVersion(path, versionId) {
|
|
173
|
+
const encodedPath = path.split('/').map(s => encodeURIComponent(s)).join('/');
|
|
174
|
+
await this.s3.send(new CopyObjectCommand({
|
|
175
|
+
Bucket: getSdkIdentifiers(this).bucketName,
|
|
176
|
+
Key: path,
|
|
177
|
+
CopySource: `${getSdkIdentifiers(this).bucketName}/${path}?versionId=${versionId}`,
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
static fromExisting(bucketName) {
|
|
181
|
+
return { __brand: 'ExternalBucketRef', bucketName };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.browser.d.ts","sourceRoot":"","sources":["../src/index.browser.ts"],"names":[],"mappings":"AAIA,qBAAa,UAAU;gBACT,GAAG,IAAI,EAAE,GAAG,EAAE;CAC3B"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Scope } from '@aws-blocks/core/cdk';
|
|
2
|
+
import type { ScopeParent } from '@aws-blocks/core';
|
|
3
|
+
import type { FileBucketOptions, ExternalBucketRef } from './types.js';
|
|
4
|
+
export { FileBucketErrors } from './errors.js';
|
|
5
|
+
export type { FileBucketOptions, PutOptions, GetUrlOptions, PutUrlOptions, ScanOptions, FileContent, FileInfo, CorsRule, LifecycleRule, ExternalBucketRef } from './types.js';
|
|
6
|
+
export declare class FileBucket<O extends FileBucketOptions = FileBucketOptions> extends Scope {
|
|
7
|
+
private bucket;
|
|
8
|
+
/**
|
|
9
|
+
* Reference an existing S3 bucket instead of provisioning a new one.
|
|
10
|
+
* Mirrors the same factory exposed by the runtime build so the same code
|
|
11
|
+
* works in both contexts.
|
|
12
|
+
*/
|
|
13
|
+
static fromExisting(bucketName: string): ExternalBucketRef;
|
|
14
|
+
constructor(scope: ScopeParent, id: string, options?: O);
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=index.cdk.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cdk.d.ts","sourceRoot":"","sources":["../src/index.cdk.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAC7C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,iBAAiB,EAA2B,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAGhG,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,YAAY,EAAE,iBAAiB,EAAE,UAAU,EAAE,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAU9K,qBAAa,UAAU,CAAC,CAAC,SAAS,iBAAiB,GAAG,iBAAiB,CAAE,SAAQ,KAAK;IACrF,OAAO,CAAC,MAAM,CAAa;IAE3B;;;;OAIG;IACH,MAAM,CAAC,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,iBAAiB;gBAI9C,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;CAsDvD"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import * as s3 from 'aws-cdk-lib/aws-s3';
|
|
4
|
+
import * as cdk from 'aws-cdk-lib';
|
|
5
|
+
import { Duration, RemovalPolicy } from 'aws-cdk-lib';
|
|
6
|
+
import { Scope } from '@aws-blocks/core/cdk';
|
|
7
|
+
import { validateBucketName } from './bucket-name.js';
|
|
8
|
+
export { FileBucketErrors } from './errors.js';
|
|
9
|
+
const httpMethodMap = {
|
|
10
|
+
GET: s3.HttpMethods.GET,
|
|
11
|
+
PUT: s3.HttpMethods.PUT,
|
|
12
|
+
POST: s3.HttpMethods.POST,
|
|
13
|
+
DELETE: s3.HttpMethods.DELETE,
|
|
14
|
+
HEAD: s3.HttpMethods.HEAD,
|
|
15
|
+
};
|
|
16
|
+
export class FileBucket extends Scope {
|
|
17
|
+
bucket;
|
|
18
|
+
/**
|
|
19
|
+
* Reference an existing S3 bucket instead of provisioning a new one.
|
|
20
|
+
* Mirrors the same factory exposed by the runtime build so the same code
|
|
21
|
+
* works in both contexts.
|
|
22
|
+
*/
|
|
23
|
+
static fromExisting(bucketName) {
|
|
24
|
+
return { __brand: 'ExternalBucketRef', bucketName };
|
|
25
|
+
}
|
|
26
|
+
constructor(scope, id, options) {
|
|
27
|
+
super(id, { parent: scope });
|
|
28
|
+
if (options?.bucket) {
|
|
29
|
+
// `fromExisting`: don't provision; bind to the pre-existing bucket and
|
|
30
|
+
// grant read/write to the Blocks runtime Lambda.
|
|
31
|
+
this.bucket = s3.Bucket.fromBucketName(this, 'bucket', options.bucket.bucketName);
|
|
32
|
+
this.bucket.grantReadWrite(this.handler);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// In sandbox mode, default to DESTROY + autoDeleteObjects so
|
|
36
|
+
// `cdk destroy` can fully clean up without manual bucket emptying.
|
|
37
|
+
// Explicit `removalPolicy` from the customer takes precedence.
|
|
38
|
+
// `autoDeleteObjects: true` is only valid paired with DESTROY (CDK
|
|
39
|
+
// validates this at construct time), so we tie the two together.
|
|
40
|
+
const isSandbox = cdk.Stack.of(this).node.tryGetContext('sandboxMode') === 'true';
|
|
41
|
+
const destroy = options?.removalPolicy === 'destroy' || (isSandbox && options?.removalPolicy === undefined);
|
|
42
|
+
// Bucket name is derived from the scope chain. Validate against S3's
|
|
43
|
+
// naming rules at synth so an invalid name fails here rather than at
|
|
44
|
+
// `cdk deploy` (where CloudFormation rejects it with a cryptic error).
|
|
45
|
+
validateBucketName(this.fullId);
|
|
46
|
+
this.bucket = new s3.Bucket(this, 'bucket', {
|
|
47
|
+
bucketName: this.fullId,
|
|
48
|
+
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
|
|
49
|
+
encryption: s3.BucketEncryption.S3_MANAGED,
|
|
50
|
+
versioned: options?.versioned ?? false,
|
|
51
|
+
removalPolicy: destroy
|
|
52
|
+
? RemovalPolicy.DESTROY
|
|
53
|
+
: options?.removalPolicy === 'retain'
|
|
54
|
+
? RemovalPolicy.RETAIN
|
|
55
|
+
: undefined,
|
|
56
|
+
autoDeleteObjects: destroy,
|
|
57
|
+
cors: options?.corsRules?.map((rule) => ({
|
|
58
|
+
allowedOrigins: rule.allowedOrigins,
|
|
59
|
+
allowedMethods: rule.allowedMethods.map(m => httpMethodMap[m]),
|
|
60
|
+
allowedHeaders: rule.allowedHeaders,
|
|
61
|
+
exposedHeaders: rule.exposedHeaders,
|
|
62
|
+
maxAge: rule.maxAge,
|
|
63
|
+
})),
|
|
64
|
+
lifecycleRules: options?.lifecycleRules?.map((rule) => ({
|
|
65
|
+
prefix: rule.prefix,
|
|
66
|
+
expiration: rule.expirationDays ? Duration.days(rule.expirationDays) : undefined,
|
|
67
|
+
transitions: rule.transitionToIaDays ? [{
|
|
68
|
+
storageClass: s3.StorageClass.INFREQUENT_ACCESS,
|
|
69
|
+
transitionAfter: Duration.days(rule.transitionToIaDays),
|
|
70
|
+
}] : undefined,
|
|
71
|
+
})),
|
|
72
|
+
});
|
|
73
|
+
this.bucket.grantReadWrite(this.handler);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cdk.test.d.ts","sourceRoot":"","sources":["../src/index.cdk.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* CDK-side regression tests for FileBucket.
|
|
5
|
+
*
|
|
6
|
+
* History: FileBucket.fromExisting was advertised in types but the CDK
|
|
7
|
+
* constructor unconditionally provisioned a new S3 bucket. These tests pin
|
|
8
|
+
* the fix.
|
|
9
|
+
*/
|
|
10
|
+
import { test } from 'node:test';
|
|
11
|
+
import assert from 'node:assert';
|
|
12
|
+
import * as cdk from 'aws-cdk-lib';
|
|
13
|
+
import { Template } from 'aws-cdk-lib/assertions';
|
|
14
|
+
import { Scope, DEFAULT_NODE_RUNTIME } from '@aws-blocks/core/cdk';
|
|
15
|
+
import { FileBucket } from './index.cdk.js';
|
|
16
|
+
class StubBlocksStack extends cdk.Stack {
|
|
17
|
+
handler;
|
|
18
|
+
id;
|
|
19
|
+
constructor(scope, id) {
|
|
20
|
+
super(scope, id);
|
|
21
|
+
this.id = id;
|
|
22
|
+
globalThis.CURRENT_BLOCKS_STACK = this;
|
|
23
|
+
this.handler = new cdk.aws_lambda.Function(this, 'StubHandler', {
|
|
24
|
+
runtime: DEFAULT_NODE_RUNTIME,
|
|
25
|
+
handler: 'index.handler',
|
|
26
|
+
code: cdk.aws_lambda.Code.fromInline('exports.handler = async () => {};'),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function setup() {
|
|
31
|
+
const app = new cdk.App();
|
|
32
|
+
// S3 bucket names must be lowercase. The default-mode FileBucket derives
|
|
33
|
+
// its bucket name from the scope chain, so keep ids lowercase.
|
|
34
|
+
const stack = new StubBlocksStack(app, 'teststack');
|
|
35
|
+
const parent = new Scope('app');
|
|
36
|
+
return { stack, parent };
|
|
37
|
+
}
|
|
38
|
+
test('CDK: default FileBucket provisions an S3 bucket', () => {
|
|
39
|
+
const { stack, parent } = setup();
|
|
40
|
+
new FileBucket(parent, 'uploads');
|
|
41
|
+
const template = Template.fromStack(stack);
|
|
42
|
+
template.resourceCountIs('AWS::S3::Bucket', 1);
|
|
43
|
+
});
|
|
44
|
+
test('CDK: FileBucket.fromExisting does NOT provision a bucket (regression)', () => {
|
|
45
|
+
const { stack, parent } = setup();
|
|
46
|
+
new FileBucket(parent, 'uploads', {
|
|
47
|
+
bucket: FileBucket.fromExisting('preexisting-bucket-123'),
|
|
48
|
+
});
|
|
49
|
+
const template = Template.fromStack(stack);
|
|
50
|
+
template.resourceCountIs('AWS::S3::Bucket', 0);
|
|
51
|
+
});
|
|
52
|
+
test('CDK: FileBucket.fromExisting returns a branded ref', () => {
|
|
53
|
+
const ref = FileBucket.fromExisting('foo');
|
|
54
|
+
assert.strictEqual(ref.bucketName, 'foo');
|
|
55
|
+
assert.strictEqual(ref.__brand, 'ExternalBucketRef');
|
|
56
|
+
});
|
|
57
|
+
test('CDK: default FileBucket with an over-long derived name throws at synth', () => {
|
|
58
|
+
const { parent } = setup();
|
|
59
|
+
// parent id "app" + "-" + a 60-char id => 64 chars, over the S3 limit.
|
|
60
|
+
assert.throws(() => new FileBucket(parent, 'u'.repeat(60)), (err) => err instanceof Error &&
|
|
61
|
+
err.name === 'ValidationFailed' &&
|
|
62
|
+
/63-character limit/.test(err.message));
|
|
63
|
+
});
|
|
64
|
+
test('CDK: fromExisting skips derived-name validation even when the chain is over-long', () => {
|
|
65
|
+
const { parent } = setup();
|
|
66
|
+
assert.doesNotThrow(() => new FileBucket(parent, 'u'.repeat(60), {
|
|
67
|
+
bucket: FileBucket.fromExisting('preexisting-bucket-123'),
|
|
68
|
+
}));
|
|
69
|
+
});
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { Scope } from '@aws-blocks/core';
|
|
2
|
+
import type { ScopeParent } from '@aws-blocks/core';
|
|
3
|
+
import type { FileBucketOptions, PutOptions, PutUrlOptions, ScanOptions, FileContent, FileInfo, ExternalBucketRef, FileDownloadClient, FileUploadClient, FileVersionInfo, GetOptionsFor, DeleteOptionsFor, GetUrlOptionsFor } from './types.js';
|
|
4
|
+
export type { FileBucketOptions, PutOptions, GetUrlOptions, PutUrlOptions, ScanOptions, FileContent, FileInfo, CorsRule, LifecycleRule, ExternalBucketRef, FileDownloadClient, FileUploadClient, FileVersionInfo, FileDownloadDescriptor, FileUploadDescriptor, VersionedGetOptions, VersionedDeleteOptions, VersionedGetUrlOptions, GetOptionsFor, DeleteOptionsFor, GetUrlOptionsFor, } from './types.js';
|
|
5
|
+
import type { ChildLogger } from '@aws-blocks/bb-logger';
|
|
6
|
+
export { FileBucketErrors } from './errors.js';
|
|
7
|
+
/**
|
|
8
|
+
* File storage backed by Amazon S3.
|
|
9
|
+
*
|
|
10
|
+
* **When to use:** You need to store, retrieve, or serve binary files —
|
|
11
|
+
* user uploads, generated reports, images, videos, or static assets.
|
|
12
|
+
*
|
|
13
|
+
* **When NOT to use:** If you need structured key-value data with conditional
|
|
14
|
+
* writes, use `KVStore`. If you need queryable records with indexes, use
|
|
15
|
+
* `DistributedTable`.
|
|
16
|
+
*
|
|
17
|
+
* **Best practices:**
|
|
18
|
+
* - Use path prefixes to organize files (e.g., `uploads/{userId}/`, `reports/`)
|
|
19
|
+
* - Set `contentType` on `put()` to ensure correct MIME handling on download
|
|
20
|
+
* - Use presigned URLs (`getUrl` / `putUrl`) for direct browser upload/download
|
|
21
|
+
* - Use `getFileHandle` / `createUploadHandle` for ergonomic browser file transfers
|
|
22
|
+
* - Prefer `scan({ prefix })` over unscoped `scan()` to limit enumeration cost
|
|
23
|
+
*
|
|
24
|
+
* **Scaling:** S3 scales automatically. No provisioned throughput. Costs are
|
|
25
|
+
* per-request plus storage. Individual objects up to 5 TB. For objects larger
|
|
26
|
+
* than ~100 MB, consider multipart upload.
|
|
27
|
+
*/
|
|
28
|
+
export declare class FileBucket<O extends FileBucketOptions = FileBucketOptions> extends Scope {
|
|
29
|
+
private dataDir;
|
|
30
|
+
private versioned;
|
|
31
|
+
/** @internal Logger for internal operations. Defaults to error-level when not provided. */
|
|
32
|
+
protected log: ChildLogger;
|
|
33
|
+
constructor(scope: ScopeParent, id: string, options?: O);
|
|
34
|
+
/**
|
|
35
|
+
* Upload a file.
|
|
36
|
+
*
|
|
37
|
+
* Overwrites any existing file at the given path.
|
|
38
|
+
*
|
|
39
|
+
* @param path - The object key (e.g., `uploads/photo.jpg`).
|
|
40
|
+
* @param body - The file content.
|
|
41
|
+
* @param options - Optional metadata and content settings.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* await bucket.put('reports/q1.pdf', pdfBuffer, {
|
|
46
|
+
* contentType: 'application/pdf',
|
|
47
|
+
* });
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
put(path: string, body: Buffer | string, options?: PutOptions): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Download a file.
|
|
53
|
+
*
|
|
54
|
+
* @param path - The object key.
|
|
55
|
+
* @param options - Optional. On versioned buckets, pass `{ versionId }` to retrieve a specific version.
|
|
56
|
+
* @returns The file content and metadata, or null if the file does not exist.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* const file = await bucket.get('reports/q1.pdf');
|
|
61
|
+
* if (file) {
|
|
62
|
+
* console.log(file.contentType); // 'application/pdf'
|
|
63
|
+
* }
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
get(path: string, options?: GetOptionsFor<O>): Promise<FileContent | null>;
|
|
67
|
+
/**
|
|
68
|
+
* Delete a file.
|
|
69
|
+
*
|
|
70
|
+
* On non-versioned buckets, permanently removes the file.
|
|
71
|
+
* On versioned buckets without `versionId`, places a delete marker (file appears deleted
|
|
72
|
+
* but previous versions are preserved). With `versionId`, permanently removes that version.
|
|
73
|
+
*
|
|
74
|
+
* No-op if the file does not exist (matches S3 behavior).
|
|
75
|
+
*
|
|
76
|
+
* @param path - The object key.
|
|
77
|
+
* @param options - Optional. On versioned buckets, pass `{ versionId }` to permanently delete a specific version.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* await bucket.delete('uploads/old-photo.jpg');
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
delete(path: string, options?: DeleteOptionsFor<O>): Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Delete multiple files in a single operation.
|
|
87
|
+
*
|
|
88
|
+
* Uses S3 DeleteObjects for efficient bulk deletion (up to 1,000 keys
|
|
89
|
+
* per request). Chunking is handled internally.
|
|
90
|
+
*
|
|
91
|
+
* @param paths - The object keys to delete.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```typescript
|
|
95
|
+
* await bucket.deleteBatch(['tmp/a.txt', 'tmp/b.txt', 'tmp/c.txt']);
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
deleteBatch(paths: string[]): Promise<void>;
|
|
99
|
+
/**
|
|
100
|
+
* Generate a presigned URL for downloading a file.
|
|
101
|
+
*
|
|
102
|
+
* @param path - The object key.
|
|
103
|
+
* @param options - URL generation options.
|
|
104
|
+
* @returns A presigned URL string.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* const url = await bucket.getUrl('reports/q1.pdf', { expiresIn: 3600 });
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
getUrl(path: string, options?: GetUrlOptionsFor<O>): Promise<string>;
|
|
112
|
+
/**
|
|
113
|
+
* Generate a presigned URL for uploading a file.
|
|
114
|
+
*
|
|
115
|
+
* @param path - The object key.
|
|
116
|
+
* @param options - URL generation options.
|
|
117
|
+
* @returns A presigned URL string.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```typescript
|
|
121
|
+
* const url = await bucket.putUrl('uploads/photo.jpg', {
|
|
122
|
+
* expiresIn: 600,
|
|
123
|
+
* contentType: 'image/jpeg',
|
|
124
|
+
* });
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
putUrl(path: string, options?: PutUrlOptions): Promise<string>;
|
|
128
|
+
/**
|
|
129
|
+
* Get a file handle for browser-side download.
|
|
130
|
+
*
|
|
131
|
+
* Returns a Transferable descriptor that the client middleware hydrates
|
|
132
|
+
* into an object with `.download()` and `.getUrl()` methods. Use this
|
|
133
|
+
* instead of `getUrl()` when you want the client to download files
|
|
134
|
+
* without manually managing presigned URLs.
|
|
135
|
+
*
|
|
136
|
+
* @param path - The object key.
|
|
137
|
+
* @param options - URL generation options.
|
|
138
|
+
* @returns A Transferable file download handle.
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```typescript
|
|
142
|
+
* // Backend
|
|
143
|
+
* async getReport(name: string) {
|
|
144
|
+
* return bucket.getFileHandle('reports/' + name);
|
|
145
|
+
* }
|
|
146
|
+
*
|
|
147
|
+
* // Frontend
|
|
148
|
+
* const file = await api.getReport('q1.pdf');
|
|
149
|
+
* const blob = await file.download();
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
getFileHandle(path: string, options?: GetUrlOptionsFor<O>): Promise<FileDownloadClient>;
|
|
153
|
+
/**
|
|
154
|
+
* Create an upload handle for browser-side file upload.
|
|
155
|
+
*
|
|
156
|
+
* Returns a Transferable descriptor that the client middleware hydrates
|
|
157
|
+
* into an object with `.upload(body)` and `.getUrl()` methods. Use this
|
|
158
|
+
* instead of `putUrl()` when you want the client to upload files
|
|
159
|
+
* without manually managing presigned URLs.
|
|
160
|
+
*
|
|
161
|
+
* @param path - The object key.
|
|
162
|
+
* @param options - URL generation options.
|
|
163
|
+
* @returns A Transferable file upload handle.
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```typescript
|
|
167
|
+
* // Backend
|
|
168
|
+
* async getUploadSlot(name: string) {
|
|
169
|
+
* return bucket.createUploadHandle('uploads/' + name, {
|
|
170
|
+
* contentType: 'image/jpeg',
|
|
171
|
+
* });
|
|
172
|
+
* }
|
|
173
|
+
*
|
|
174
|
+
* // Frontend
|
|
175
|
+
* const slot = await api.getUploadSlot('photo.jpg');
|
|
176
|
+
* await slot.upload(fileBlob);
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
createUploadHandle(path: string, options?: PutUrlOptions): Promise<FileUploadClient>;
|
|
180
|
+
/**
|
|
181
|
+
* List files in the bucket.
|
|
182
|
+
*
|
|
183
|
+
* Returns an `AsyncIterable` that paginates internally. Use `prefix`
|
|
184
|
+
* to scope the listing. Without a prefix, enumerates all files —
|
|
185
|
+
* this is expensive on large buckets.
|
|
186
|
+
*
|
|
187
|
+
* @param options - Listing options.
|
|
188
|
+
* @returns An async iterable of file info objects.
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* ```typescript
|
|
192
|
+
* for await (const file of bucket.scan({ prefix: 'uploads/' })) {
|
|
193
|
+
* console.log(file.path, file.size);
|
|
194
|
+
* }
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
scan(options?: ScanOptions): AsyncIterable<FileInfo>;
|
|
198
|
+
/**
|
|
199
|
+
* List all versions of a file. Only available on versioned buckets.
|
|
200
|
+
*
|
|
201
|
+
* Returns versions newest-first. Includes the current version and all
|
|
202
|
+
* previous versions. Does not include delete markers.
|
|
203
|
+
*
|
|
204
|
+
* @param path - The object key.
|
|
205
|
+
* @returns An array of version metadata, newest first.
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```typescript
|
|
209
|
+
* const versions = await bucket.listVersions('report.pdf');
|
|
210
|
+
* for (const v of versions) {
|
|
211
|
+
* console.log(v.versionId, v.lastModified, v.isCurrent);
|
|
212
|
+
* }
|
|
213
|
+
* ```
|
|
214
|
+
*/
|
|
215
|
+
listVersions(path: string): Promise<FileVersionInfo[]>;
|
|
216
|
+
/**
|
|
217
|
+
* Restore a previous version of a file, making it the current version.
|
|
218
|
+
*
|
|
219
|
+
* Creates a new version that is a copy of the specified old version.
|
|
220
|
+
* On S3, this is implemented as a CopyObject from the old version to the same key.
|
|
221
|
+
*
|
|
222
|
+
* @param path - The object key.
|
|
223
|
+
* @param versionId - The version to restore.
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* ```typescript
|
|
227
|
+
* const versions = await bucket.listVersions('report.pdf');
|
|
228
|
+
* await bucket.restoreVersion('report.pdf', versions[1].versionId);
|
|
229
|
+
* ```
|
|
230
|
+
*/
|
|
231
|
+
restoreVersion(path: string, versionId: string): Promise<void>;
|
|
232
|
+
/**
|
|
233
|
+
* Create a reference to an existing S3 bucket not managed by this scope.
|
|
234
|
+
*
|
|
235
|
+
* @param bucketName - The name of the existing S3 bucket.
|
|
236
|
+
*/
|
|
237
|
+
static fromExisting(bucketName: string): ExternalBucketRef;
|
|
238
|
+
private validateKey;
|
|
239
|
+
private writeMeta;
|
|
240
|
+
private readMeta;
|
|
241
|
+
private readVersionMeta;
|
|
242
|
+
private readMetaFile;
|
|
243
|
+
private walkDir;
|
|
244
|
+
private nextVersionId;
|
|
245
|
+
}
|
|
246
|
+
//# sourceMappingURL=index.mock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mock.d.ts","sourceRoot":"","sources":["../src/index.mock.ts"],"names":[],"mappings":"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"}
|