@chidchanun/bcp 0.1.24 → 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.
- package/README.md +587 -202
- package/docs/README.md +278 -86
- package/docs/file-upload.md +194 -32
- package/docs/releases/0.1.25.md +132 -0
- package/docs/releases/0.1.26.md +281 -0
- package/docs/s3-storage.md +276 -0
- package/docs/storage.md +401 -0
- package/package.json +5 -2
- package/packages/client/src/database.mjs +236 -0
- package/packages/client/src/server.ts +46 -0
- package/packages/server/src/file-delivery.ts +613 -0
- package/packages/server/src/storage-s3.ts +1159 -0
- package/packages/server/src/storage.ts +1515 -0
- package/packages/server/src/upload-stream.ts +846 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# S3-Compatible Storage
|
|
2
|
+
|
|
3
|
+
BCP Framework `0.1.26` adds an S3-compatible `StorageAdapter` implementation through `bcp/server`.
|
|
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.
|
|
6
|
+
|
|
7
|
+
## Create an adapter
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import {
|
|
11
|
+
createS3Storage,
|
|
12
|
+
} from "bcp/server";
|
|
13
|
+
|
|
14
|
+
export const storage =
|
|
15
|
+
createS3Storage({
|
|
16
|
+
bucket:
|
|
17
|
+
process.env.S3_BUCKET!,
|
|
18
|
+
region:
|
|
19
|
+
process.env.S3_REGION!,
|
|
20
|
+
endpoint:
|
|
21
|
+
process.env.S3_ENDPOINT,
|
|
22
|
+
accessKeyId:
|
|
23
|
+
process.env.S3_ACCESS_KEY_ID,
|
|
24
|
+
secretAccessKey:
|
|
25
|
+
process.env.S3_SECRET_ACCESS_KEY,
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
When `accessKeyId` / `secretAccessKey` are omitted, the underlying AWS SDK client can use its normal server-side credential provider chain.
|
|
30
|
+
|
|
31
|
+
Do not expose S3 credentials through `BCP_PUBLIC_*` environment variables.
|
|
32
|
+
|
|
33
|
+
## MinIO / path-style endpoints
|
|
34
|
+
|
|
35
|
+
Some S3-compatible endpoints require path-style addressing:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
const storage =
|
|
39
|
+
createS3Storage({
|
|
40
|
+
bucket:
|
|
41
|
+
"uploads",
|
|
42
|
+
region:
|
|
43
|
+
"us-east-1",
|
|
44
|
+
endpoint:
|
|
45
|
+
"http://127.0.0.1:9000",
|
|
46
|
+
forcePathStyle:
|
|
47
|
+
true,
|
|
48
|
+
accessKeyId:
|
|
49
|
+
process.env.MINIO_ACCESS_KEY,
|
|
50
|
+
secretAccessKey:
|
|
51
|
+
process.env.MINIO_SECRET_KEY,
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Prefix objects
|
|
56
|
+
|
|
57
|
+
A deployment can reserve a logical prefix without changing application storage keys:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
const storage =
|
|
61
|
+
createS3Storage({
|
|
62
|
+
bucket:
|
|
63
|
+
"app-data",
|
|
64
|
+
region:
|
|
65
|
+
"auto",
|
|
66
|
+
prefix:
|
|
67
|
+
"production",
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await storage.put(
|
|
71
|
+
"avatars/user-101.webp",
|
|
72
|
+
bytes
|
|
73
|
+
);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The application-facing key remains:
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
avatars/user-101.webp
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
while the provider object key becomes:
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
production/avatars/user-101.webp
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Prefixes and object keys reject traversal segments such as `..`.
|
|
89
|
+
|
|
90
|
+
## Standard operations
|
|
91
|
+
|
|
92
|
+
The S3 adapter implements the same `StorageAdapter` surface as local storage:
|
|
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 the generic `putStorageStream()` helper:
|
|
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 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`).
|
|
157
|
+
|
|
158
|
+
## Multipart tuning
|
|
159
|
+
|
|
160
|
+
The adapter accepts optional multipart tuning:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
const storage =
|
|
164
|
+
createS3Storage({
|
|
165
|
+
bucket:
|
|
166
|
+
"uploads",
|
|
167
|
+
region:
|
|
168
|
+
"us-east-1",
|
|
169
|
+
multipart: {
|
|
170
|
+
partSize:
|
|
171
|
+
8 * 1024 * 1024,
|
|
172
|
+
queueSize:
|
|
173
|
+
2,
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`partSize` must be at least 5 MiB. Higher `queueSize` can improve throughput, but it also increases concurrent network and memory usage.
|
|
179
|
+
|
|
180
|
+
## Ranged reads
|
|
181
|
+
|
|
182
|
+
The adapter preserves BCP's inclusive `start` / `end` range contract:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
const bytes =
|
|
186
|
+
await storage.read(
|
|
187
|
+
"media/video.mp4",
|
|
188
|
+
{
|
|
189
|
+
start: 0,
|
|
190
|
+
end: 1023,
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`readStream()` also supports ranges and is used automatically by `createStorageResponse()`.
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
return createStorageResponse(
|
|
199
|
+
request,
|
|
200
|
+
storage,
|
|
201
|
+
"media/video.mp4"
|
|
202
|
+
);
|
|
203
|
+
```
|
|
204
|
+
|
|
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.
|
|
206
|
+
|
|
207
|
+
## Capabilities
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
getStorageCapabilities(storage)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
returns native support for:
|
|
214
|
+
|
|
215
|
+
```text
|
|
216
|
+
streamingRead: true
|
|
217
|
+
streamingWrite: true
|
|
218
|
+
ranges: true
|
|
219
|
+
signedUrls: false
|
|
220
|
+
listing: false
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Signed URLs and listing are intentionally deferred to a later storage milestone rather than expanding the `0.1.26` release surface further.
|
|
224
|
+
|
|
225
|
+
## Overwrite semantics
|
|
226
|
+
|
|
227
|
+
For buffered `put()`, BCP sends a conditional create-only request when `overwrite` is false.
|
|
228
|
+
|
|
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.
|
|
230
|
+
|
|
231
|
+
Applications that require strict cross-writer serialization should enforce it in their database/business layer or use provider-specific conditional workflows.
|
|
232
|
+
|
|
233
|
+
## Metadata and ETags
|
|
234
|
+
|
|
235
|
+
S3 response metadata is mapped to `StorageObjectMetadata`:
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
{
|
|
239
|
+
key,
|
|
240
|
+
size,
|
|
241
|
+
contentType,
|
|
242
|
+
lastModified,
|
|
243
|
+
etag,
|
|
244
|
+
checksumSha256?,
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
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.
|
|
249
|
+
|
|
250
|
+
Do not assume an S3 ETag is always a plain MD5 checksum; multipart and provider-specific ETag formats can differ.
|
|
251
|
+
|
|
252
|
+
## Client lifecycle
|
|
253
|
+
|
|
254
|
+
Adapters that create their own AWS SDK client expose:
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
storage.destroy();
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Call it during application shutdown when appropriate.
|
|
261
|
+
|
|
262
|
+
When an existing `S3Client` is injected through `client`, BCP does not destroy that externally owned client.
|
|
263
|
+
|
|
264
|
+
## Security
|
|
265
|
+
|
|
266
|
+
Storage keys are not authorization. Authenticate and authorize a user before allowing access to an object key.
|
|
267
|
+
|
|
268
|
+
Recommended production practices include:
|
|
269
|
+
|
|
270
|
+
- keep buckets private by default,
|
|
271
|
+
- use scoped credentials / IAM policies,
|
|
272
|
+
- do not expose access keys to browser bundles,
|
|
273
|
+
- 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.
|
package/docs/storage.md
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
# Storage and File Delivery
|
|
2
|
+
|
|
3
|
+
BCP Framework `0.1.25` introduced the server-only `StorageAdapter` abstraction and production file-delivery helpers through `bcp/server`.
|
|
4
|
+
|
|
5
|
+
BCP Framework `0.1.26` completes the next storage milestone with:
|
|
6
|
+
|
|
7
|
+
- additive streaming reads/writes,
|
|
8
|
+
- capability discovery,
|
|
9
|
+
- local streaming I/O,
|
|
10
|
+
- S3-compatible storage,
|
|
11
|
+
- production multipart-to-storage streaming,
|
|
12
|
+
- streamed full/ranged file delivery.
|
|
13
|
+
|
|
14
|
+
Existing adapters written for `0.1.25` remain valid because `putStream()` and `readStream()` are optional.
|
|
15
|
+
|
|
16
|
+
## Local storage
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import {
|
|
20
|
+
createLocalStorage,
|
|
21
|
+
} from "bcp/server";
|
|
22
|
+
|
|
23
|
+
export const storage =
|
|
24
|
+
createLocalStorage({
|
|
25
|
+
directory:
|
|
26
|
+
"./uploads",
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Storage keys are logical relative paths:
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
avatars/user-101.webp
|
|
34
|
+
documents/2026/report.pdf
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Absolute paths, traversal segments and BCP's reserved metadata directory are rejected.
|
|
38
|
+
|
|
39
|
+
## S3-compatible storage
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import {
|
|
43
|
+
createS3Storage,
|
|
44
|
+
} from "bcp/server";
|
|
45
|
+
|
|
46
|
+
export const storage =
|
|
47
|
+
createS3Storage({
|
|
48
|
+
bucket:
|
|
49
|
+
process.env.S3_BUCKET!,
|
|
50
|
+
region:
|
|
51
|
+
process.env.S3_REGION!,
|
|
52
|
+
endpoint:
|
|
53
|
+
process.env.S3_ENDPOINT,
|
|
54
|
+
accessKeyId:
|
|
55
|
+
process.env.S3_ACCESS_KEY_ID,
|
|
56
|
+
secretAccessKey:
|
|
57
|
+
process.env.S3_SECRET_ACCESS_KEY,
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The same application-facing `StorageAdapter` surface works with local or S3-compatible backends.
|
|
62
|
+
|
|
63
|
+
Read more: [S3-Compatible Storage](s3-storage.md).
|
|
64
|
+
|
|
65
|
+
## Storage adapter contract
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
interface StorageAdapter {
|
|
69
|
+
capabilities?: Partial<
|
|
70
|
+
StorageAdapterCapabilities
|
|
71
|
+
>;
|
|
72
|
+
|
|
73
|
+
put(
|
|
74
|
+
key: string,
|
|
75
|
+
value: StorageWriteValue,
|
|
76
|
+
options?: StoragePutOptions
|
|
77
|
+
): Promise<StorageObjectMetadata>;
|
|
78
|
+
|
|
79
|
+
putStream?(
|
|
80
|
+
key: string,
|
|
81
|
+
stream: ReadableStream<Uint8Array>,
|
|
82
|
+
options?: StoragePutStreamOptions
|
|
83
|
+
): Promise<StorageObjectMetadata>;
|
|
84
|
+
|
|
85
|
+
stat(
|
|
86
|
+
key: string
|
|
87
|
+
): Promise<StorageObjectMetadata | null>;
|
|
88
|
+
|
|
89
|
+
read(
|
|
90
|
+
key: string,
|
|
91
|
+
options?: StorageReadOptions
|
|
92
|
+
): Promise<Uint8Array>;
|
|
93
|
+
|
|
94
|
+
readStream?(
|
|
95
|
+
key: string,
|
|
96
|
+
options?: StorageReadOptions
|
|
97
|
+
): Promise<StorageReadableStream>;
|
|
98
|
+
|
|
99
|
+
exists(
|
|
100
|
+
key: string
|
|
101
|
+
): Promise<boolean>;
|
|
102
|
+
|
|
103
|
+
delete(
|
|
104
|
+
key: string
|
|
105
|
+
): Promise<boolean>;
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The original buffered methods remain required. Streaming methods are additive optional capabilities.
|
|
110
|
+
|
|
111
|
+
## Capabilities
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import {
|
|
115
|
+
getStorageCapabilities,
|
|
116
|
+
} from "bcp/server";
|
|
117
|
+
|
|
118
|
+
const capabilities =
|
|
119
|
+
getStorageCapabilities(
|
|
120
|
+
storage
|
|
121
|
+
);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The capability model currently reports:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
{
|
|
128
|
+
streamingRead,
|
|
129
|
+
streamingWrite,
|
|
130
|
+
ranges,
|
|
131
|
+
signedUrls,
|
|
132
|
+
listing,
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`createLocalStorage()` and `createS3Storage()` in `0.1.26` report:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
{
|
|
140
|
+
streamingRead: true,
|
|
141
|
+
streamingWrite: true,
|
|
142
|
+
ranges: true,
|
|
143
|
+
signedUrls: false,
|
|
144
|
+
listing: false,
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Signed URLs and listing are deferred to a later milestone.
|
|
149
|
+
|
|
150
|
+
## Buffered writes
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
const stored =
|
|
154
|
+
await storage.put(
|
|
155
|
+
"notes/hello.txt",
|
|
156
|
+
"Hello BCP",
|
|
157
|
+
{
|
|
158
|
+
contentType:
|
|
159
|
+
"text/plain",
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`put()` does not overwrite an existing object unless `overwrite: true` is supplied.
|
|
165
|
+
|
|
166
|
+
Local storage persists content type and SHA-256 identity in sidecar metadata. Buffered S3 writes persist SHA-256 in S3 user metadata.
|
|
167
|
+
|
|
168
|
+
## Streaming writes
|
|
169
|
+
|
|
170
|
+
Use the generic helper so application code works with native or legacy adapters:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
import {
|
|
174
|
+
putStorageStream,
|
|
175
|
+
} from "bcp/server";
|
|
176
|
+
|
|
177
|
+
await putStorageStream(
|
|
178
|
+
storage,
|
|
179
|
+
"imports/data.bin",
|
|
180
|
+
request.body!,
|
|
181
|
+
{
|
|
182
|
+
contentType:
|
|
183
|
+
"application/octet-stream",
|
|
184
|
+
maxBytes:
|
|
185
|
+
100 * 1024 * 1024,
|
|
186
|
+
}
|
|
187
|
+
);
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
When an adapter provides `putStream()`, BCP uses it. Legacy adapters fall back to collecting the stream and calling `put()`.
|
|
191
|
+
|
|
192
|
+
The local adapter writes chunks directly to disk and calculates SHA-256 while streaming.
|
|
193
|
+
|
|
194
|
+
The S3 adapter uses the AWS SDK multipart upload helper so streams whose final size is not known at the start can still be uploaded without buffering the complete object.
|
|
195
|
+
|
|
196
|
+
### Size limits and abort
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
await putStorageStream(
|
|
200
|
+
storage,
|
|
201
|
+
key,
|
|
202
|
+
source,
|
|
203
|
+
{
|
|
204
|
+
maxBytes:
|
|
205
|
+
50 * 1024 * 1024,
|
|
206
|
+
signal:
|
|
207
|
+
abortController.signal,
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
If the stream exceeds `maxBytes`, BCP throws:
|
|
213
|
+
|
|
214
|
+
```text
|
|
215
|
+
StorageError
|
|
216
|
+
code: STREAM_TOO_LARGE
|
|
217
|
+
status: 413
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Native adapters clean up/abort incomplete writes where supported.
|
|
221
|
+
|
|
222
|
+
## Streaming reads
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
import {
|
|
226
|
+
readStorageStream,
|
|
227
|
+
} from "bcp/server";
|
|
228
|
+
|
|
229
|
+
const stream =
|
|
230
|
+
await readStorageStream(
|
|
231
|
+
storage,
|
|
232
|
+
"videos/demo.mp4"
|
|
233
|
+
);
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Inclusive byte ranges are supported:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
const stream =
|
|
240
|
+
await readStorageStream(
|
|
241
|
+
storage,
|
|
242
|
+
"videos/demo.mp4",
|
|
243
|
+
{
|
|
244
|
+
start: 0,
|
|
245
|
+
end: 1023,
|
|
246
|
+
}
|
|
247
|
+
);
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Legacy adapters fall back to `read()` and expose the result as a stream.
|
|
251
|
+
|
|
252
|
+
## Multipart uploads
|
|
253
|
+
|
|
254
|
+
Small forms can continue using the buffered `FormData` API:
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
const formData =
|
|
258
|
+
await parseMultipartFormData(
|
|
259
|
+
request
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
const file =
|
|
263
|
+
requireUploadedFile(
|
|
264
|
+
formData,
|
|
265
|
+
"file"
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
await storeUploadedFile(
|
|
269
|
+
file,
|
|
270
|
+
{
|
|
271
|
+
storage,
|
|
272
|
+
}
|
|
273
|
+
);
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
For large production uploads, `0.1.26` adds direct multipart-to-storage streaming:
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
import {
|
|
280
|
+
storeMultipartFile,
|
|
281
|
+
} from "bcp/server";
|
|
282
|
+
|
|
283
|
+
const stored =
|
|
284
|
+
await storeMultipartFile(
|
|
285
|
+
request,
|
|
286
|
+
{
|
|
287
|
+
storage,
|
|
288
|
+
fieldName:
|
|
289
|
+
"file",
|
|
290
|
+
key:
|
|
291
|
+
"documents/report.pdf",
|
|
292
|
+
maxBytes:
|
|
293
|
+
100 * 1024 * 1024,
|
|
294
|
+
constraints: {
|
|
295
|
+
maxBytes:
|
|
296
|
+
80 * 1024 * 1024,
|
|
297
|
+
allowedTypes: [
|
|
298
|
+
"application/pdf",
|
|
299
|
+
],
|
|
300
|
+
allowedExtensions: [
|
|
301
|
+
".pdf",
|
|
302
|
+
],
|
|
303
|
+
},
|
|
304
|
+
}
|
|
305
|
+
);
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
The multipart parser and storage write operate concurrently, so the complete file is not converted to a `File` / `ArrayBuffer` first.
|
|
309
|
+
|
|
310
|
+
Read more: [File Upload](file-upload.md).
|
|
311
|
+
|
|
312
|
+
## File delivery
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
import {
|
|
316
|
+
createStorageResponse,
|
|
317
|
+
} from "bcp/server";
|
|
318
|
+
|
|
319
|
+
export function GET(
|
|
320
|
+
request: Request
|
|
321
|
+
) {
|
|
322
|
+
return createStorageResponse(
|
|
323
|
+
request,
|
|
324
|
+
storage,
|
|
325
|
+
"documents/report.pdf",
|
|
326
|
+
{
|
|
327
|
+
disposition:
|
|
328
|
+
"attachment",
|
|
329
|
+
downloadName:
|
|
330
|
+
"report.pdf",
|
|
331
|
+
}
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
`createStorageResponse()` uses `readStorageStream()` and supports:
|
|
337
|
+
|
|
338
|
+
- `GET` and `HEAD`,
|
|
339
|
+
- `ETag` / `If-None-Match`,
|
|
340
|
+
- `Last-Modified` / `If-Modified-Since`,
|
|
341
|
+
- `If-Range`,
|
|
342
|
+
- single byte ranges with `206 Partial Content`,
|
|
343
|
+
- `304 Not Modified`,
|
|
344
|
+
- `416 Range Not Satisfiable`,
|
|
345
|
+
- safe `Content-Disposition` filenames,
|
|
346
|
+
- configurable cache control.
|
|
347
|
+
|
|
348
|
+
The default cache policy is:
|
|
349
|
+
|
|
350
|
+
```text
|
|
351
|
+
Cache-Control: private, max-age=0, must-revalidate
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Public immutable caching must be enabled explicitly.
|
|
355
|
+
|
|
356
|
+
Multiple byte ranges are intentionally unsupported in `0.1.26`.
|
|
357
|
+
|
|
358
|
+
## Object metadata
|
|
359
|
+
|
|
360
|
+
```ts
|
|
361
|
+
interface StorageObjectMetadata {
|
|
362
|
+
key: string;
|
|
363
|
+
size: number;
|
|
364
|
+
contentType: string;
|
|
365
|
+
lastModified: Date;
|
|
366
|
+
etag: string;
|
|
367
|
+
checksumSha256?: string;
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
`checksumSha256` is optional because not every cloud provider exposes a portable whole-object SHA-256 value for multipart objects.
|
|
372
|
+
|
|
373
|
+
Do not assume an S3 ETag is an MD5 hash.
|
|
374
|
+
|
|
375
|
+
## Storage errors
|
|
376
|
+
|
|
377
|
+
Current codes:
|
|
378
|
+
|
|
379
|
+
```text
|
|
380
|
+
INVALID_KEY
|
|
381
|
+
OBJECT_NOT_FOUND
|
|
382
|
+
OBJECT_EXISTS
|
|
383
|
+
RANGE_NOT_SATISFIABLE
|
|
384
|
+
STREAM_TOO_LARGE
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## Security boundary
|
|
388
|
+
|
|
389
|
+
Storage keys are not authorization. Authenticate and authorize requests before allowing access to user-selected keys.
|
|
390
|
+
|
|
391
|
+
MIME type and extension checks are metadata validation, not file-signature verification.
|
|
392
|
+
|
|
393
|
+
Streaming limits do not replace the framework/proxy request body limit. Configure `server.bodyLimit` / `BCP_BODY_LIMIT` large enough for intended uploads while keeping an explicit outer request boundary.
|
|
394
|
+
|
|
395
|
+
For S3/object storage:
|
|
396
|
+
|
|
397
|
+
- keep credentials server-only,
|
|
398
|
+
- prefer private buckets,
|
|
399
|
+
- scope IAM/provider permissions,
|
|
400
|
+
- use provider-side encryption/retention policies where required,
|
|
401
|
+
- validate provider CORS only when direct browser access is intentionally enabled.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chidchanun/bcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"description": "BCP Framework - a React full-stack framework with file-based routing, SSR, APIs, middleware, islands, caching and standalone production builds.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"./database": {
|
|
56
56
|
"types": "./packages/client/src/database.ts",
|
|
57
57
|
"browser": "./packages/client/src/server-only.browser.mjs",
|
|
58
|
-
"default": "./packages/client/src/database.
|
|
58
|
+
"default": "./packages/client/src/database.mjs"
|
|
59
59
|
},
|
|
60
60
|
"./auth": {
|
|
61
61
|
"types": "./packages/client/src/auth.ts",
|
|
@@ -79,9 +79,12 @@
|
|
|
79
79
|
"./package.json": "./package.json"
|
|
80
80
|
},
|
|
81
81
|
"dependencies": {
|
|
82
|
+
"@aws-sdk/client-s3": "^3.1119.0",
|
|
83
|
+
"@aws-sdk/lib-storage": "^3.1119.0",
|
|
82
84
|
"@babel/core": "^8.0.1",
|
|
83
85
|
"@babel/preset-react": "^8.0.1",
|
|
84
86
|
"@babel/preset-typescript": "^8.0.1",
|
|
87
|
+
"busboy": "^1.6.0",
|
|
85
88
|
"chokidar": "^5.0.0",
|
|
86
89
|
"esbuild": "^0.28.2",
|
|
87
90
|
"react-refresh": "^0.18.0",
|