@chidchanun/bcp 0.1.25 → 0.1.26

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,13 @@
1
1
  # File Upload
2
2
 
3
- BCP Framework 0.1.24 adds server-only multipart and file-storage helpers through `bcp/server`.
3
+ BCP Framework provides two server-only multipart upload paths through `bcp/server`:
4
4
 
5
- ## Basic API route
5
+ - the buffered `FormData` helpers introduced in `0.1.24`,
6
+ - direct multipart-to-storage streaming added in `0.1.26`.
7
+
8
+ Use the buffered API for small forms and workflows that need normal `FormData` access. Use `storeMultipartFile()` when large files should flow directly from the request body into a `StorageAdapter` without materializing the complete file in application memory.
9
+
10
+ ## Buffered API route
6
11
 
7
12
  ```ts
8
13
  import {
@@ -64,6 +69,125 @@ export async function POST(
64
69
  }
65
70
  ```
66
71
 
72
+ ## Production streaming upload
73
+
74
+ BCP `0.1.26` adds `storeMultipartFile()`:
75
+
76
+ ```ts
77
+ import {
78
+ createLocalStorage,
79
+ storeMultipartFile,
80
+ } from "bcp/server";
81
+
82
+ const uploads =
83
+ createLocalStorage({
84
+ directory:
85
+ "./uploads",
86
+ });
87
+
88
+ export async function POST(
89
+ request: Request
90
+ ) {
91
+ const stored =
92
+ await storeMultipartFile(
93
+ request,
94
+ {
95
+ storage:
96
+ uploads,
97
+ fieldName:
98
+ "file",
99
+ key:
100
+ "incoming/upload.bin",
101
+ maxBytes:
102
+ 100 * 1024 * 1024,
103
+ constraints: {
104
+ maxBytes:
105
+ 80 * 1024 * 1024,
106
+ allowedTypes: [
107
+ "application/octet-stream",
108
+ ],
109
+ },
110
+ }
111
+ );
112
+
113
+ return Response.json(
114
+ stored
115
+ );
116
+ }
117
+ ```
118
+
119
+ The request body is parsed incrementally. The target file stream is handed to the selected `StorageAdapter` while multipart data is still arriving.
120
+
121
+ This path is intended for large uploads and shared/object storage. It avoids calling `request.formData()` for the uploaded file.
122
+
123
+ ## Streaming directly to S3-compatible storage
124
+
125
+ The same API works with `createS3Storage()`:
126
+
127
+ ```ts
128
+ import {
129
+ createS3Storage,
130
+ storeMultipartFile,
131
+ } from "bcp/server";
132
+
133
+ const uploads =
134
+ createS3Storage({
135
+ bucket:
136
+ process.env.S3_BUCKET!,
137
+ region:
138
+ process.env.S3_REGION!,
139
+ endpoint:
140
+ process.env.S3_ENDPOINT,
141
+ accessKeyId:
142
+ process.env.S3_ACCESS_KEY_ID,
143
+ secretAccessKey:
144
+ process.env.S3_SECRET_ACCESS_KEY,
145
+ });
146
+
147
+ export async function POST(
148
+ request: Request
149
+ ) {
150
+ return Response.json(
151
+ await storeMultipartFile(
152
+ request,
153
+ {
154
+ storage:
155
+ uploads,
156
+ fieldName:
157
+ "file",
158
+ key: (
159
+ file
160
+ ) =>
161
+ `documents/${crypto.randomUUID()}${file.originalName.endsWith(".pdf") ? ".pdf" : ""}`,
162
+ constraints: {
163
+ maxBytes:
164
+ 50 * 1024 * 1024,
165
+ allowedTypes: [
166
+ "application/pdf",
167
+ ],
168
+ allowedExtensions: [
169
+ ".pdf",
170
+ ],
171
+ },
172
+ }
173
+ )
174
+ );
175
+ }
176
+ ```
177
+
178
+ The `key` option can be a fixed string or an async callback receiving:
179
+
180
+ ```ts
181
+ {
182
+ fieldName,
183
+ originalName,
184
+ encoding,
185
+ contentType,
186
+ }
187
+ ```
188
+
189
+ When `key` is omitted, BCP creates a UUID-based key and preserves a short sanitized extension.
190
+
67
191
  ## Request body limit
68
192
 
69
193
  BCP's security gateway enforces the framework-wide request body limit before the request reaches the application server.
@@ -79,16 +203,20 @@ import {
79
203
  export default defineConfig({
80
204
  server: {
81
205
  bodyLimit:
82
- 10 * 1024 * 1024,
206
+ 100 * 1024 * 1024,
83
207
  },
84
208
  });
85
209
  ```
86
210
 
87
211
  The same setting can be supplied with `BCP_BODY_LIMIT`.
88
212
 
89
- Keep `server.bodyLimit` greater than or equal to the multipart limit used by `parseMultipartFormData()`.
213
+ For buffered uploads, keep `server.bodyLimit` greater than or equal to the multipart limit used by `parseMultipartFormData()`.
214
+
215
+ For streaming uploads, keep the infrastructure/framework body limit greater than or equal to `storeMultipartFile({ maxBytes })`. Streaming avoids full application-memory buffering, but it does not bypass reverse-proxy, framework or platform request-size limits.
216
+
217
+ ## Multipart request limits
90
218
 
91
- ## Multipart parsing
219
+ Buffered parsing:
92
220
 
93
221
  ```ts
94
222
  const formData =
@@ -101,16 +229,31 @@ const formData =
101
229
  );
102
230
  ```
103
231
 
104
- The helper:
232
+ Streaming parsing:
233
+
234
+ ```ts
235
+ await storeMultipartFile(
236
+ request,
237
+ {
238
+ storage,
239
+ fieldName:
240
+ "file",
241
+ maxBytes:
242
+ 100 * 1024 * 1024,
243
+ }
244
+ );
245
+ ```
246
+
247
+ Both helpers:
105
248
 
106
- - requires `multipart/form-data`,
107
- - rejects a declared `Content-Length` above `maxBytes`,
108
- - validates the parsed form payload size,
109
- - throws `UploadError` with HTTP-oriented status information on validation failures.
249
+ - require `multipart/form-data`,
250
+ - reject a declared `Content-Length` above the configured request limit,
251
+ - enforce the request limit while processing,
252
+ - throw `UploadError` with HTTP-oriented status information on validation failures.
110
253
 
111
- `maxBytes` is optional. The framework-wide `server.bodyLimit` remains the outer request limit.
254
+ `storeMultipartFile()` additionally applies the file-level `constraints.maxBytes` while bytes are flowing into storage. Native streaming adapters can abort incomplete writes rather than keeping a partial object.
112
255
 
113
- ## Reading files
256
+ ## Reading buffered files
114
257
 
115
258
  Use `getUploadedFile()` for optional files:
116
259
 
@@ -140,7 +283,7 @@ A missing required file throws `UploadError` with code `FILE_REQUIRED` and statu
140
283
 
141
284
  ## File constraints
142
285
 
143
- `getUploadedFile()`, `requireUploadedFile()` and `saveUploadedFile()` can validate:
286
+ Buffered and streaming uploads can validate:
144
287
 
145
288
  ```ts
146
289
  {
@@ -160,9 +303,9 @@ Supported checks are:
160
303
  - MIME type allowlist,
161
304
  - extension allowlist.
162
305
 
163
- MIME type and file extension are metadata supplied with the upload. They are useful validation signals but are not content-signature verification. Applications accepting security-sensitive formats should inspect the file content or magic bytes before trusting the file type.
306
+ MIME type and file extension are multipart metadata. They are useful validation signals but are not content-signature verification. Applications accepting security-sensitive formats should inspect the file content or magic bytes before trusting the file type.
164
307
 
165
- ## Saving files
308
+ ## Saving buffered files
166
309
 
167
310
  ```ts
168
311
  const saved =
@@ -192,25 +335,31 @@ The result contains:
192
335
 
193
336
  Files are created with exclusive-write behavior by default. An existing destination produces `FILE_EXISTS` instead of silently overwriting data.
194
337
 
195
- Use `overwrite: true` only when replacement is intentional:
338
+ Use `overwrite: true` only when replacement is intentional.
339
+
340
+ ## Streaming result
341
+
342
+ `storeMultipartFile()` returns storage metadata together with multipart identity:
196
343
 
197
344
  ```ts
198
- await saveUploadedFile(
199
- file,
200
- {
201
- directory:
202
- "./uploads",
203
- fileName:
204
- "avatar.webp",
205
- overwrite:
206
- true,
207
- }
208
- );
345
+ {
346
+ key: string;
347
+ size: number;
348
+ contentType: string;
349
+ lastModified: Date;
350
+ etag: string;
351
+ checksumSha256?: string;
352
+ fieldName: string;
353
+ originalName: string;
354
+ encoding: string;
355
+ }
209
356
  ```
210
357
 
358
+ The checksum is optional because not every storage provider exposes a portable whole-object SHA-256 value for multipart uploads.
359
+
211
360
  ## File-name safety
212
361
 
213
- `saveUploadedFile()` sanitizes the final file name and verifies that the resolved destination remains inside the configured directory.
362
+ `saveUploadedFile()` sanitizes the final local file name and verifies that the resolved destination remains inside the configured directory.
214
363
 
215
364
  You can use the same sanitizer independently:
216
365
 
@@ -225,7 +374,7 @@ const safeName =
225
374
  );
226
375
  ```
227
376
 
228
- Path separators, control characters and operating-system-invalid filename characters are removed or replaced.
377
+ Streaming storage uses logical storage keys instead of direct operating-system paths. Storage adapters validate their own key rules; local/S3 BCP adapters reject traversal segments.
229
378
 
230
379
  ## Upload errors
231
380
 
@@ -273,8 +422,21 @@ INVALID_FILE_NAME
273
422
  FILE_EXISTS
274
423
  ```
275
424
 
276
- ## Storage model
425
+ The streaming helper maps storage `STREAM_TOO_LARGE` and `OBJECT_EXISTS` failures into the corresponding upload-facing error codes.
426
+
427
+ ## Choosing buffered vs streaming
428
+
429
+ Prefer the buffered API when:
430
+
431
+ - uploads are small,
432
+ - the route needs several regular form fields/files as `FormData`,
433
+ - application code needs random access to the parsed file before storage.
434
+
435
+ Prefer `storeMultipartFile()` when:
277
436
 
278
- The 0.1.24 foundation writes to the local filesystem. It does not provide S3/object-storage adapters, multipart streaming directly to object storage, virus scanning or image transcoding yet.
437
+ - files can be large,
438
+ - the file should be written directly to local/shared/object storage,
439
+ - memory usage should stay bounded,
440
+ - S3 multipart upload is required for streams with unknown final size.
279
441
 
280
- For horizontally scaled applications, use shared/object storage rather than relying on instance-local disk.
442
+ The `0.1.26` streaming API intentionally targets one named file field per call. Multiple-file orchestration and richer streaming form-field APIs can be added later without changing the storage contract.
@@ -0,0 +1,281 @@
1
+ # BCP Framework 0.1.26
2
+
3
+ BCP Framework `0.1.26` is the cloud-storage and production-streaming milestone.
4
+
5
+ > Release state: release candidate source complete; not published until the full RC checks pass and npm publication completes.
6
+
7
+ ## Highlights
8
+
9
+ - Added additive storage streaming capabilities without breaking `0.1.25` adapters.
10
+ - Added native local filesystem streaming reads/writes.
11
+ - Added S3-compatible storage through `createS3Storage()`.
12
+ - Added direct multipart-to-storage streaming through `storeMultipartFile()`.
13
+ - Added streamed full and ranged file delivery.
14
+ - Added stream size limits and abort propagation.
15
+ - Added cleanup/abort behavior for incomplete local and S3 multipart writes.
16
+ - Added S3 integration coverage using a local S3-compatible test endpoint.
17
+ - Added package smoke coverage for cloud-storage dependencies and public exports.
18
+
19
+ ## Storage streaming foundation
20
+
21
+ New public helpers:
22
+
23
+ ```ts
24
+ import {
25
+ getStorageCapabilities,
26
+ putStorageStream,
27
+ readStorageStream,
28
+ } from "bcp/server";
29
+ ```
30
+
31
+ New public types:
32
+
33
+ ```ts
34
+ StorageAdapterCapabilities
35
+ StoragePutStreamOptions
36
+ StorageReadableStream
37
+ ```
38
+
39
+ The streaming methods on `StorageAdapter` are optional:
40
+
41
+ ```ts
42
+ interface StorageAdapter {
43
+ put(...): Promise<StorageObjectMetadata>;
44
+ putStream?(...): Promise<StorageObjectMetadata>;
45
+
46
+ read(...): Promise<Uint8Array>;
47
+ readStream?(...): Promise<StorageReadableStream>;
48
+
49
+ stat(...): Promise<StorageObjectMetadata | null>;
50
+ exists(...): Promise<boolean>;
51
+ delete(...): Promise<boolean>;
52
+ }
53
+ ```
54
+
55
+ `putStorageStream()` / `readStorageStream()` automatically use native streaming methods when available and preserve compatibility with legacy buffered adapters.
56
+
57
+ ## Local storage streaming
58
+
59
+ `createLocalStorage()` now advertises:
60
+
61
+ ```ts
62
+ {
63
+ streamingRead: true,
64
+ streamingWrite: true,
65
+ ranges: true,
66
+ signedUrls: false,
67
+ listing: false,
68
+ }
69
+ ```
70
+
71
+ Streaming local writes:
72
+
73
+ - write chunks directly to disk,
74
+ - calculate SHA-256 while consuming the stream,
75
+ - enforce `maxBytes`,
76
+ - honor `AbortSignal`,
77
+ - remove incomplete objects when the operation fails.
78
+
79
+ Streaming local reads support full objects and inclusive byte ranges without first reading the complete file into memory.
80
+
81
+ ## S3-compatible storage
82
+
83
+ New API:
84
+
85
+ ```ts
86
+ import {
87
+ createS3Storage,
88
+ } from "bcp/server";
89
+
90
+ const storage =
91
+ createS3Storage({
92
+ bucket:
93
+ process.env.S3_BUCKET!,
94
+ region:
95
+ process.env.S3_REGION!,
96
+ endpoint:
97
+ process.env.S3_ENDPOINT,
98
+ accessKeyId:
99
+ process.env.S3_ACCESS_KEY_ID,
100
+ secretAccessKey:
101
+ process.env.S3_SECRET_ACCESS_KEY,
102
+ });
103
+ ```
104
+
105
+ The adapter supports:
106
+
107
+ - `put()` / conditional create-only writes,
108
+ - `putStream()` through AWS SDK multipart upload,
109
+ - `stat()` through `HeadObject`,
110
+ - `read()` / `readStream()` through `GetObject`,
111
+ - inclusive ranges,
112
+ - `exists()`,
113
+ - `delete()`,
114
+ - logical key prefixes,
115
+ - custom endpoints,
116
+ - path-style endpoints for MinIO/S3-compatible services,
117
+ - AWS SDK default credential provider behavior when explicit credentials are omitted,
118
+ - injected `S3Client` instances for advanced deployments.
119
+
120
+ The S3 adapter exposes `destroy()` for internally owned AWS SDK clients.
121
+
122
+ Buffered BCP writes persist SHA-256 as S3 user metadata. Whole-object SHA-256 remains optional for multipart streams because cloud providers do not expose one portable checksum format across all multipart implementations.
123
+
124
+ ## Production multipart streaming
125
+
126
+ New API:
127
+
128
+ ```ts
129
+ import {
130
+ storeMultipartFile,
131
+ } from "bcp/server";
132
+ ```
133
+
134
+ Example:
135
+
136
+ ```ts
137
+ const stored =
138
+ await storeMultipartFile(
139
+ request,
140
+ {
141
+ storage,
142
+ fieldName:
143
+ "file",
144
+ key:
145
+ "documents/report.pdf",
146
+ maxBytes:
147
+ 100 * 1024 * 1024,
148
+ constraints: {
149
+ maxBytes:
150
+ 80 * 1024 * 1024,
151
+ allowedTypes: [
152
+ "application/pdf",
153
+ ],
154
+ allowedExtensions: [
155
+ ".pdf",
156
+ ],
157
+ },
158
+ }
159
+ );
160
+ ```
161
+
162
+ The multipart parser consumes `Request.body` incrementally and sends the selected file field directly into `putStorageStream()`.
163
+
164
+ This avoids calling `request.formData()` and avoids requiring the complete file to exist in application memory before storage begins.
165
+
166
+ The existing buffered APIs remain supported:
167
+
168
+ ```text
169
+ parseMultipartFormData()
170
+ getUploadedFile()
171
+ requireUploadedFile()
172
+ saveUploadedFile()
173
+ storeUploadedFile()
174
+ ```
175
+
176
+ ## Upload validation and cleanup
177
+
178
+ `storeMultipartFile()` supports:
179
+
180
+ - total multipart request limit (`maxBytes`),
181
+ - per-file limit (`constraints.maxBytes`),
182
+ - MIME allowlists,
183
+ - extension allowlists,
184
+ - fixed or callback-generated storage keys,
185
+ - caller `AbortSignal`,
186
+ - one required named file field per operation.
187
+
188
+ Storage failures are translated where appropriate:
189
+
190
+ ```text
191
+ STREAM_TOO_LARGE -> FILE_TOO_LARGE
192
+ OBJECT_EXISTS -> FILE_EXISTS
193
+ ```
194
+
195
+ Partial local objects are removed on failure. S3 multipart uploads use automatic part cleanup when upload completion fails.
196
+
197
+ ## File delivery
198
+
199
+ `createStorageResponse()` uses `readStorageStream()` for response bodies.
200
+
201
+ Streaming-capable adapters therefore support:
202
+
203
+ ```text
204
+ 200 full object
205
+ 206 single byte range
206
+ HEAD without body read
207
+ 304 validators
208
+ 416 invalid ranges
209
+ ```
210
+
211
+ The existing ETag, Last-Modified, If-Range, safe Content-Disposition and conservative private-cache behavior remain unchanged.
212
+
213
+ ## Package/runtime dependencies
214
+
215
+ The published framework package now carries the runtime dependencies required by this milestone:
216
+
217
+ ```text
218
+ @aws-sdk/client-s3
219
+ @aws-sdk/lib-storage
220
+ busboy
221
+ ```
222
+
223
+ Package smoke tests verify these dependencies are present in the staged npm artifact.
224
+
225
+ ## Tests
226
+
227
+ `0.1.26` adds coverage for:
228
+
229
+ - local streaming writes and ranges,
230
+ - legacy adapter streaming fallbacks,
231
+ - stream size cleanup,
232
+ - streaming multipart uploads to local storage,
233
+ - multipart MIME and size failures,
234
+ - S3 buffered writes,
235
+ - S3 no-overwrite behavior,
236
+ - S3 streaming writes,
237
+ - S3 ranged reads,
238
+ - S3 streamed reads,
239
+ - S3 deletes,
240
+ - public `bcp/server` exports,
241
+ - packaged cloud-storage dependencies/artifacts.
242
+
243
+ The S3 integration test uses a local S3-compatible HTTP fixture; it does not require an AWS account.
244
+
245
+ ## Backward compatibility
246
+
247
+ `0.1.26` is additive to the `0.1.25` storage contract.
248
+
249
+ Existing applications can continue using:
250
+
251
+ ```ts
252
+ storage.put(...)
253
+ storage.read(...)
254
+ storeUploadedFile(...)
255
+ createStorageResponse(...)
256
+ ```
257
+
258
+ Custom `StorageAdapter` implementations do not need to implement streaming methods immediately. Generic helpers fall back to the required buffer APIs.
259
+
260
+ ## Intentional limitations
261
+
262
+ The following are not part of `0.1.26`:
263
+
264
+ - presigned URLs,
265
+ - object listing,
266
+ - copy/move helpers,
267
+ - multiple HTTP byte ranges,
268
+ - multi-file streaming multipart orchestration,
269
+ - generated cloud-provider-specific adapters beyond the S3 API.
270
+
271
+ These can be added in later milestones without expanding this release further.
272
+
273
+ ## Documentation
274
+
275
+ - [File Upload](../file-upload.md)
276
+ - [Storage and File Delivery](../storage.md)
277
+ - [S3-Compatible Storage](../s3-storage.md)
278
+
279
+ ## Next milestone
280
+
281
+ The recommended next storage/DX milestone is `0.1.27`, focusing on higher-level storage operations such as signed URLs/listing where needed, plus broader production hardening rather than changing the `0.1.26` core streaming contract.