@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.
Files changed (87) hide show
  1. package/LICENSE +174 -0
  2. package/README.md +197 -0
  3. package/dist/bucket-name.d.ts +9 -0
  4. package/dist/bucket-name.d.ts.map +1 -0
  5. package/dist/bucket-name.js +62 -0
  6. package/dist/bucket-name.test.d.ts +2 -0
  7. package/dist/bucket-name.test.d.ts.map +1 -0
  8. package/dist/bucket-name.test.js +61 -0
  9. package/dist/errors.d.ts +20 -0
  10. package/dist/errors.d.ts.map +1 -0
  11. package/dist/errors.js +21 -0
  12. package/dist/file-server.d.ts +14 -0
  13. package/dist/file-server.d.ts.map +1 -0
  14. package/dist/file-server.js +169 -0
  15. package/dist/file-server.test.d.ts +2 -0
  16. package/dist/file-server.test.d.ts.map +1 -0
  17. package/dist/file-server.test.js +307 -0
  18. package/dist/index.aws.d.ts +47 -0
  19. package/dist/index.aws.d.ts.map +1 -0
  20. package/dist/index.aws.js +183 -0
  21. package/dist/index.browser.d.ts +4 -0
  22. package/dist/index.browser.d.ts.map +1 -0
  23. package/dist/index.browser.js +6 -0
  24. package/dist/index.cdk.d.ts +16 -0
  25. package/dist/index.cdk.d.ts.map +1 -0
  26. package/dist/index.cdk.js +75 -0
  27. package/dist/index.cdk.test.d.ts +2 -0
  28. package/dist/index.cdk.test.d.ts.map +1 -0
  29. package/dist/index.cdk.test.js +69 -0
  30. package/dist/index.mock.d.ts +246 -0
  31. package/dist/index.mock.d.ts.map +1 -0
  32. package/dist/index.mock.js +502 -0
  33. package/dist/index.test.d.ts +2 -0
  34. package/dist/index.test.d.ts.map +1 -0
  35. package/dist/index.test.js +318 -0
  36. package/dist/middleware.d.ts +3 -0
  37. package/dist/middleware.d.ts.map +1 -0
  38. package/dist/middleware.js +62 -0
  39. package/dist/mock-middleware.d.ts +3 -0
  40. package/dist/mock-middleware.d.ts.map +1 -0
  41. package/dist/mock-middleware.js +62 -0
  42. package/dist/mock-utils.d.ts +11 -0
  43. package/dist/mock-utils.d.ts.map +1 -0
  44. package/dist/mock-utils.js +28 -0
  45. package/dist/path-containment.test.d.ts +2 -0
  46. package/dist/path-containment.test.d.ts.map +1 -0
  47. package/dist/path-containment.test.js +91 -0
  48. package/dist/paths.d.ts +25 -0
  49. package/dist/paths.d.ts.map +1 -0
  50. package/dist/paths.js +67 -0
  51. package/dist/scan.test.d.ts +2 -0
  52. package/dist/scan.test.d.ts.map +1 -0
  53. package/dist/scan.test.js +107 -0
  54. package/dist/tokens.d.ts +12 -0
  55. package/dist/tokens.d.ts.map +1 -0
  56. package/dist/tokens.js +42 -0
  57. package/dist/types.d.ts +170 -0
  58. package/dist/types.d.ts.map +1 -0
  59. package/dist/types.js +3 -0
  60. package/dist/url-encoding.test.d.ts +2 -0
  61. package/dist/url-encoding.test.d.ts.map +1 -0
  62. package/dist/url-encoding.test.js +88 -0
  63. package/dist/version.d.ts +3 -0
  64. package/dist/version.d.ts.map +1 -0
  65. package/dist/version.js +3 -0
  66. package/package.json +57 -0
  67. package/src/bucket-name.test.ts +103 -0
  68. package/src/bucket-name.ts +83 -0
  69. package/src/errors.ts +22 -0
  70. package/src/file-server.test.ts +381 -0
  71. package/src/file-server.ts +203 -0
  72. package/src/index.aws.ts +219 -0
  73. package/src/index.browser.ts +7 -0
  74. package/src/index.cdk.test.ts +84 -0
  75. package/src/index.cdk.ts +89 -0
  76. package/src/index.mock.ts +531 -0
  77. package/src/index.test.ts +366 -0
  78. package/src/middleware.ts +66 -0
  79. package/src/mock-middleware.ts +66 -0
  80. package/src/mock-utils.ts +31 -0
  81. package/src/path-containment.test.ts +122 -0
  82. package/src/paths.ts +78 -0
  83. package/src/scan.test.ts +137 -0
  84. package/src/tokens.ts +61 -0
  85. package/src/types.ts +206 -0
  86. package/src/url-encoding.test.ts +120 -0
  87. 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,4 @@
1
+ export declare class FileBucket {
2
+ constructor(...args: any[]);
3
+ }
4
+ //# sourceMappingURL=index.browser.d.ts.map
@@ -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,6 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ // Browser stub - FileBucket runs server-side only
4
+ export class FileBucket {
5
+ constructor(...args) { }
6
+ }
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.cdk.test.d.ts.map
@@ -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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"}