@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.
- package/README.md +211 -647
- package/docs/README.md +160 -210
- package/docs/file-upload.md +194 -32
- package/docs/releases/0.1.26.md +281 -0
- package/docs/releases/0.1.27.md +350 -0
- package/docs/s3-storage.md +433 -0
- package/docs/storage-ecosystem.md +434 -0
- package/docs/storage.md +278 -162
- package/package.json +6 -2
- package/packages/client/src/database.mjs +236 -0
- package/packages/client/src/server.ts +54 -1
- package/packages/server/src/file-delivery.ts +8 -24
- package/packages/server/src/storage-ecosystem.ts +1326 -0
- package/packages/server/src/storage-s3-ecosystem.ts +947 -0
- package/packages/server/src/storage-s3.ts +1159 -0
- package/packages/server/src/storage.ts +740 -54
- package/packages/server/src/upload-stream.ts +846 -0
|
@@ -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)
|