@chidchanun/bcp 0.1.26 → 0.1.28

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.
@@ -1,8 +1,10 @@
1
1
  # S3-Compatible Storage
2
2
 
3
- BCP Framework `0.1.26` adds an S3-compatible `StorageAdapter` implementation through `bcp/server`.
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
4
 
5
- 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 still differ, so production deployments should verify their chosen provider with representative uploads, ranges and overwrite rules.
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.
6
8
 
7
9
  ## Create an adapter
8
10
 
@@ -54,7 +56,7 @@ const storage =
54
56
 
55
57
  ## Prefix objects
56
58
 
57
- A deployment can reserve a logical prefix without changing application storage keys:
59
+ A deployment can reserve a logical provider prefix without changing application-facing keys:
58
60
 
59
61
  ```ts
60
62
  const storage =
@@ -73,24 +75,22 @@ await storage.put(
73
75
  );
74
76
  ```
75
77
 
76
- The application-facing key remains:
78
+ Application key:
77
79
 
78
80
  ```text
79
81
  avatars/user-101.webp
80
82
  ```
81
83
 
82
- while the provider object key becomes:
84
+ Provider key:
83
85
 
84
86
  ```text
85
87
  production/avatars/user-101.webp
86
88
  ```
87
89
 
88
- Prefixes and object keys reject traversal segments such as `..`.
90
+ All `0.1.27` list/copy/move/delete/signing operations preserve this logical/provider-key boundary.
89
91
 
90
92
  ## Standard operations
91
93
 
92
- The S3 adapter implements the same `StorageAdapter` surface as local storage:
93
-
94
94
  ```ts
95
95
  await storage.put(
96
96
  "documents/report.pdf",
@@ -124,7 +124,7 @@ Buffered `put()` uses a conditional S3 write when `overwrite` is not enabled. Ex
124
124
 
125
125
  ## Streaming upload
126
126
 
127
- `createS3Storage()` supports `putStream()` and works with the generic `putStorageStream()` helper:
127
+ `createS3Storage()` supports `putStream()` and works with `putStorageStream()`:
128
128
 
129
129
  ```ts
130
130
  import {
@@ -144,21 +144,10 @@ await putStorageStream(
144
144
  );
145
145
  ```
146
146
 
147
- BCP uses the AWS SDK multipart upload helper for streams whose total size may not be known when the upload begins. This avoids requiring the complete object to be buffered in application memory.
148
-
149
- If a stream exceeds `maxBytes`, the upload is aborted and `StorageError` uses:
150
-
151
- ```text
152
- code: STREAM_TOO_LARGE
153
- status: 413
154
- ```
155
-
156
- Multipart uploads use automatic cleanup on failure (`leavePartsOnError: false`).
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.
157
148
 
158
149
  ## Multipart tuning
159
150
 
160
- The adapter accepts optional multipart tuning:
161
-
162
151
  ```ts
163
152
  const storage =
164
153
  createS3Storage({
@@ -175,12 +164,10 @@ const storage =
175
164
  });
176
165
  ```
177
166
 
178
- `partSize` must be at least 5 MiB. Higher `queueSize` can improve throughput, but it also increases concurrent network and memory usage.
167
+ `partSize` must be at least 5 MiB.
179
168
 
180
169
  ## Ranged reads
181
170
 
182
- The adapter preserves BCP's inclusive `start` / `end` range contract:
183
-
184
171
  ```ts
185
172
  const bytes =
186
173
  await storage.read(
@@ -194,60 +181,221 @@ const bytes =
194
181
 
195
182
  `readStream()` also supports ranges and is used automatically by `createStorageResponse()`.
196
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
+
197
211
  ```ts
198
- return createStorageResponse(
199
- request,
212
+ import {
213
+ copyStorageObject,
214
+ moveStorageObject,
215
+ } from "bcp/server";
216
+
217
+ await copyStorageObject(
200
218
  storage,
201
- "media/video.mp4"
219
+ "incoming/report.pdf",
220
+ "archive/report.pdf"
221
+ );
222
+
223
+ await moveStorageObject(
224
+ storage,
225
+ "incoming/avatar.webp",
226
+ "users/42/avatar.webp"
202
227
  );
203
228
  ```
204
229
 
205
- This allows the existing file-delivery API to serve local or S3-backed objects with the same `GET`, `HEAD`, ETag, Last-Modified and single-range behavior.
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.
206
350
 
207
351
  ## Capabilities
208
352
 
353
+ Use the original capability helper for compatibility:
354
+
209
355
  ```ts
210
356
  getStorageCapabilities(storage)
211
357
  ```
212
358
 
213
- returns native support for:
359
+ For the `0.1.27` S3 wrapper it reports:
214
360
 
215
361
  ```text
216
362
  streamingRead: true
217
363
  streamingWrite: true
218
364
  ranges: true
219
- signedUrls: false
220
- listing: false
365
+ signedUrls: true
366
+ listing: true
221
367
  ```
222
368
 
223
- Signed URLs and listing are intentionally deferred to a later storage milestone rather than expanding the `0.1.26` release surface further.
369
+ For the complete ecosystem surface use:
224
370
 
225
- ## Overwrite semantics
371
+ ```ts
372
+ getStorageEcosystemCapabilities(storage)
373
+ ```
226
374
 
227
- For buffered `put()`, BCP sends a conditional create-only request when `overwrite` is false.
375
+ which additionally reports:
228
376
 
229
- Streaming multipart uploads perform a `HeadObject` preflight before starting when overwrite is disabled. That preserves the normal application behavior, but it cannot provide the same atomic create-only guarantee across every S3-compatible multipart implementation if another writer creates the same key between the preflight and completion.
377
+ ```text
378
+ signedReadUrls: true
379
+ signedWriteUrls: true
380
+ copy: true
381
+ move: true
382
+ metadata: true
383
+ bulkDelete: true
384
+ ```
230
385
 
231
- Applications that require strict cross-writer serialization should enforce it in their database/business layer or use provider-specific conditional workflows.
386
+ ## Overwrite semantics
232
387
 
233
- ## Metadata and ETags
388
+ Buffered `put()` retains the `0.1.26` conditional create-only behavior when `overwrite` is false.
234
389
 
235
- S3 response metadata is mapped to `StorageObjectMetadata`:
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.
236
391
 
237
- ```ts
238
- {
239
- key,
240
- size,
241
- contentType,
242
- lastModified,
243
- etag,
244
- checksumSha256?,
245
- }
246
- ```
392
+ Copy/move destinations also reject existing objects by default unless `overwrite: true` is supplied.
393
+
394
+ ## Metadata and ETags
247
395
 
248
- Buffered BCP writes persist a SHA-256 value in S3 user metadata. Streaming multipart writes rely on provider metadata/ETag behavior and may not expose `checksumSha256` through the generic contract.
396
+ Do not assume an S3 ETag is a plain MD5 checksum. Multipart uploads and provider-specific implementations can use different ETag formats.
249
397
 
250
- Do not assume an S3 ETag is always a plain MD5 checksum; multipart and provider-specific ETag formats can differ.
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.
251
399
 
252
400
  ## Client lifecycle
253
401
 
@@ -263,14 +411,23 @@ When an existing `S3Client` is injected through `client`, BCP does not destroy t
263
411
 
264
412
  ## Security
265
413
 
266
- Storage keys are not authorization. Authenticate and authorize a user before allowing access to an object key.
414
+ Storage keys are not authorization. Authenticate and authorize before exposing object operations.
267
415
 
268
- Recommended production practices include:
416
+ Recommended production practices:
269
417
 
270
418
  - keep buckets private by default,
271
419
  - use scoped credentials / IAM policies,
272
420
  - do not expose access keys to browser bundles,
421
+ - use short presigned URL expirations,
422
+ - avoid logging full signed query strings,
273
423
  - validate uploaded content independently of MIME metadata,
274
- - configure provider-side encryption and retention policies when required,
275
- - keep application upload limits below infrastructure/proxy limits,
276
- - verify CORS only when browsers intentionally access the bucket directly.
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)