@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.
- package/README.md +169 -263
- package/docs/README.md +148 -210
- package/docs/file-upload.md +194 -32
- package/docs/releases/0.1.26.md +281 -0
- package/docs/s3-storage.md +276 -0
- package/docs/storage.md +278 -162
- package/package.json +5 -2
- package/packages/client/src/database.mjs +236 -0
- package/packages/client/src/server.ts +22 -0
- package/packages/server/src/file-delivery.ts +8 -24
- 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,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.
|