@chidchanun/bcp 0.1.25 → 0.1.27

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.
@@ -0,0 +1,433 @@
1
+ # S3-Compatible Storage
2
+
3
+ BCP Framework `0.1.26` introduced an S3-compatible streaming `StorageAdapter`. BCP `0.1.27` extends the same `createS3Storage()` API with object listing, native copy/move, user metadata, bulk deletion and presigned read/write URLs.
4
+
5
+ > `0.1.27` behavior is an unreleased development target until RC validation and publication complete.
6
+
7
+ The adapter is designed for AWS S3 and services that implement the S3 API, including common S3-compatible providers such as Cloudflare R2 and MinIO. Provider-specific behavior can differ, so production deployments should verify their chosen service with representative object operations.
8
+
9
+ ## Create an adapter
10
+
11
+ ```ts
12
+ import {
13
+ createS3Storage,
14
+ } from "bcp/server";
15
+
16
+ export const storage =
17
+ createS3Storage({
18
+ bucket:
19
+ process.env.S3_BUCKET!,
20
+ region:
21
+ process.env.S3_REGION!,
22
+ endpoint:
23
+ process.env.S3_ENDPOINT,
24
+ accessKeyId:
25
+ process.env.S3_ACCESS_KEY_ID,
26
+ secretAccessKey:
27
+ process.env.S3_SECRET_ACCESS_KEY,
28
+ });
29
+ ```
30
+
31
+ When `accessKeyId` / `secretAccessKey` are omitted, the underlying AWS SDK client can use its normal server-side credential provider chain.
32
+
33
+ Do not expose S3 credentials through `BCP_PUBLIC_*` environment variables.
34
+
35
+ ## MinIO / path-style endpoints
36
+
37
+ Some S3-compatible endpoints require path-style addressing:
38
+
39
+ ```ts
40
+ const storage =
41
+ createS3Storage({
42
+ bucket:
43
+ "uploads",
44
+ region:
45
+ "us-east-1",
46
+ endpoint:
47
+ "http://127.0.0.1:9000",
48
+ forcePathStyle:
49
+ true,
50
+ accessKeyId:
51
+ process.env.MINIO_ACCESS_KEY,
52
+ secretAccessKey:
53
+ process.env.MINIO_SECRET_KEY,
54
+ });
55
+ ```
56
+
57
+ ## Prefix objects
58
+
59
+ A deployment can reserve a logical provider prefix without changing application-facing keys:
60
+
61
+ ```ts
62
+ const storage =
63
+ createS3Storage({
64
+ bucket:
65
+ "app-data",
66
+ region:
67
+ "auto",
68
+ prefix:
69
+ "production",
70
+ });
71
+
72
+ await storage.put(
73
+ "avatars/user-101.webp",
74
+ bytes
75
+ );
76
+ ```
77
+
78
+ Application key:
79
+
80
+ ```text
81
+ avatars/user-101.webp
82
+ ```
83
+
84
+ Provider key:
85
+
86
+ ```text
87
+ production/avatars/user-101.webp
88
+ ```
89
+
90
+ All `0.1.27` list/copy/move/delete/signing operations preserve this logical/provider-key boundary.
91
+
92
+ ## Standard operations
93
+
94
+ ```ts
95
+ await storage.put(
96
+ "documents/report.pdf",
97
+ bytes,
98
+ {
99
+ contentType:
100
+ "application/pdf",
101
+ }
102
+ );
103
+
104
+ const metadata =
105
+ await storage.stat(
106
+ "documents/report.pdf"
107
+ );
108
+
109
+ const bytes =
110
+ await storage.read(
111
+ "documents/report.pdf"
112
+ );
113
+
114
+ await storage.exists(
115
+ "documents/report.pdf"
116
+ );
117
+
118
+ await storage.delete(
119
+ "documents/report.pdf"
120
+ );
121
+ ```
122
+
123
+ Buffered `put()` uses a conditional S3 write when `overwrite` is not enabled. Existing objects therefore produce `StorageError` with code `OBJECT_EXISTS` instead of being silently replaced.
124
+
125
+ ## Streaming upload
126
+
127
+ `createS3Storage()` supports `putStream()` and works with `putStorageStream()`:
128
+
129
+ ```ts
130
+ import {
131
+ putStorageStream,
132
+ } from "bcp/server";
133
+
134
+ await putStorageStream(
135
+ storage,
136
+ "media/video.mp4",
137
+ request.body!,
138
+ {
139
+ contentType:
140
+ "video/mp4",
141
+ maxBytes:
142
+ 500 * 1024 * 1024,
143
+ }
144
+ );
145
+ ```
146
+
147
+ BCP uses AWS SDK multipart upload for streams whose total size may not be known when upload begins. Multipart cleanup remains enabled on failure.
148
+
149
+ ## Multipart tuning
150
+
151
+ ```ts
152
+ const storage =
153
+ createS3Storage({
154
+ bucket:
155
+ "uploads",
156
+ region:
157
+ "us-east-1",
158
+ multipart: {
159
+ partSize:
160
+ 8 * 1024 * 1024,
161
+ queueSize:
162
+ 2,
163
+ },
164
+ });
165
+ ```
166
+
167
+ `partSize` must be at least 5 MiB.
168
+
169
+ ## Ranged reads
170
+
171
+ ```ts
172
+ const bytes =
173
+ await storage.read(
174
+ "media/video.mp4",
175
+ {
176
+ start: 0,
177
+ end: 1023,
178
+ }
179
+ );
180
+ ```
181
+
182
+ `readStream()` also supports ranges and is used automatically by `createStorageResponse()`.
183
+
184
+ ## Listing
185
+
186
+ BCP `0.1.27` enables native S3 `ListObjectsV2` through the generic helper:
187
+
188
+ ```ts
189
+ import {
190
+ listStorageObjects,
191
+ } from "bcp/server";
192
+
193
+ const page =
194
+ await listStorageObjects(
195
+ storage,
196
+ {
197
+ prefix:
198
+ "documents/",
199
+ limit:
200
+ 100,
201
+ }
202
+ );
203
+ ```
204
+
205
+ When S3 returns another page, `page.cursor` contains its continuation token. Treat this cursor as opaque and pass it back unchanged.
206
+
207
+ S3 list responses do not include object content type, so listed `StorageObjectMetadata` uses `application/octet-stream` for `contentType`. Use `stat()` when exact object headers are required.
208
+
209
+ ## Native copy and move
210
+
211
+ ```ts
212
+ import {
213
+ copyStorageObject,
214
+ moveStorageObject,
215
+ } from "bcp/server";
216
+
217
+ await copyStorageObject(
218
+ storage,
219
+ "incoming/report.pdf",
220
+ "archive/report.pdf"
221
+ );
222
+
223
+ await moveStorageObject(
224
+ storage,
225
+ "incoming/avatar.webp",
226
+ "users/42/avatar.webp"
227
+ );
228
+ ```
229
+
230
+ The S3 adapter uses native `CopyObject`. User metadata and BCP checksum metadata are preserved by normal copies.
231
+
232
+ Move is copy followed by source deletion. It is not a distributed transaction; applications needing business-level atomicity should coordinate storage state with their database.
233
+
234
+ ## User metadata
235
+
236
+ ```ts
237
+ await storage.put(
238
+ "documents/report.pdf",
239
+ bytes,
240
+ {
241
+ contentType:
242
+ "application/pdf",
243
+ metadata: {
244
+ owner:
245
+ "user-42",
246
+ category:
247
+ "report",
248
+ },
249
+ }
250
+ );
251
+ ```
252
+
253
+ Read or replace metadata:
254
+
255
+ ```ts
256
+ import {
257
+ getStorageMetadata,
258
+ setStorageMetadata,
259
+ } from "bcp/server";
260
+
261
+ const metadata =
262
+ await getStorageMetadata(
263
+ storage,
264
+ "documents/report.pdf"
265
+ );
266
+
267
+ await setStorageMetadata(
268
+ storage,
269
+ "documents/report.pdf",
270
+ {
271
+ owner:
272
+ "user-42",
273
+ state:
274
+ "approved",
275
+ }
276
+ );
277
+ ```
278
+
279
+ BCP maps application metadata to S3 user metadata and reserves `bcp_sha256` for framework checksum data.
280
+
281
+ Replacing S3 metadata uses provider copy-to-self semantics with `MetadataDirective: REPLACE` while retaining BCP checksum metadata.
282
+
283
+ ## Bulk deletion
284
+
285
+ ```ts
286
+ import {
287
+ deleteStorageObjects,
288
+ } from "bcp/server";
289
+
290
+ const result =
291
+ await deleteStorageObjects(
292
+ storage,
293
+ keys
294
+ );
295
+ ```
296
+
297
+ BCP batches native S3 multi-delete requests at up to 1000 keys per request.
298
+
299
+ S3 delete semantics are idempotent: deleting a missing key is normally reported as successful. Therefore S3 results commonly place such keys in `deleted` rather than `missing`.
300
+
301
+ ## Presigned read URLs
302
+
303
+ ```ts
304
+ import {
305
+ createStorageSignedReadUrl,
306
+ } from "bcp/server";
307
+
308
+ const url =
309
+ await createStorageSignedReadUrl(
310
+ storage,
311
+ "videos/demo.mp4",
312
+ {
313
+ expiresIn:
314
+ 300,
315
+ }
316
+ );
317
+ ```
318
+
319
+ The default expiry is 15 minutes. The maximum accepted expiry is seven days, matching the SigV4 presigning boundary.
320
+
321
+ ## Presigned write URLs
322
+
323
+ ```ts
324
+ import {
325
+ createStorageSignedWriteUrl,
326
+ } from "bcp/server";
327
+
328
+ const url =
329
+ await createStorageSignedWriteUrl(
330
+ storage,
331
+ "uploads/video.mp4",
332
+ {
333
+ expiresIn:
334
+ 300,
335
+ contentType:
336
+ "video/mp4",
337
+ metadata: {
338
+ owner:
339
+ "user-42",
340
+ },
341
+ }
342
+ );
343
+ ```
344
+
345
+ The browser or another client can then upload directly to S3-compatible storage instead of proxying the file bytes through BCP.
346
+
347
+ When content type or metadata are included in the signed operation, the direct uploader must send the headers required by the generated signature/provider.
348
+
349
+ Never issue a signed URL before application authorization. Signed URLs should be treated as temporary credentials.
350
+
351
+ ## Capabilities
352
+
353
+ Use the original capability helper for compatibility:
354
+
355
+ ```ts
356
+ getStorageCapabilities(storage)
357
+ ```
358
+
359
+ For the `0.1.27` S3 wrapper it reports:
360
+
361
+ ```text
362
+ streamingRead: true
363
+ streamingWrite: true
364
+ ranges: true
365
+ signedUrls: true
366
+ listing: true
367
+ ```
368
+
369
+ For the complete ecosystem surface use:
370
+
371
+ ```ts
372
+ getStorageEcosystemCapabilities(storage)
373
+ ```
374
+
375
+ which additionally reports:
376
+
377
+ ```text
378
+ signedReadUrls: true
379
+ signedWriteUrls: true
380
+ copy: true
381
+ move: true
382
+ metadata: true
383
+ bulkDelete: true
384
+ ```
385
+
386
+ ## Overwrite semantics
387
+
388
+ Buffered `put()` retains the `0.1.26` conditional create-only behavior when `overwrite` is false.
389
+
390
+ Streaming multipart writes still use a preflight existence check because portable S3 multipart APIs do not expose the same atomic create-only condition across every compatible provider.
391
+
392
+ Copy/move destinations also reject existing objects by default unless `overwrite: true` is supplied.
393
+
394
+ ## Metadata and ETags
395
+
396
+ Do not assume an S3 ETag is a plain MD5 checksum. Multipart uploads and provider-specific implementations can use different ETag formats.
397
+
398
+ Buffered BCP writes persist a SHA-256 value in reserved S3 user metadata. Multipart streams may not expose a portable whole-object SHA-256 through the generic contract.
399
+
400
+ ## Client lifecycle
401
+
402
+ Adapters that create their own AWS SDK client expose:
403
+
404
+ ```ts
405
+ storage.destroy();
406
+ ```
407
+
408
+ Call it during application shutdown when appropriate.
409
+
410
+ When an existing `S3Client` is injected through `client`, BCP does not destroy that externally owned client.
411
+
412
+ ## Security
413
+
414
+ Storage keys are not authorization. Authenticate and authorize before exposing object operations.
415
+
416
+ Recommended production practices:
417
+
418
+ - keep buckets private by default,
419
+ - use scoped credentials / IAM policies,
420
+ - do not expose access keys to browser bundles,
421
+ - use short presigned URL expirations,
422
+ - avoid logging full signed query strings,
423
+ - validate uploaded content independently of MIME metadata,
424
+ - configure encryption/retention policies when required,
425
+ - keep application upload limits below proxy/infrastructure limits,
426
+ - configure CORS only for the browser origins and methods that need direct object access.
427
+
428
+ ## Related documentation
429
+
430
+ - [Storage and File Delivery](storage.md)
431
+ - [Storage Ecosystem](storage-ecosystem.md)
432
+ - [File Upload](file-upload.md)
433
+ - [BCP Framework 0.1.27](releases/0.1.27.md)