s3kit 0.1.0
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 +398 -0
- package/dist/adapters/express.cjs +305 -0
- package/dist/adapters/express.cjs.map +1 -0
- package/dist/adapters/express.d.cts +10 -0
- package/dist/adapters/express.d.ts +10 -0
- package/dist/adapters/express.js +278 -0
- package/dist/adapters/express.js.map +1 -0
- package/dist/adapters/fetch.cjs +298 -0
- package/dist/adapters/fetch.cjs.map +1 -0
- package/dist/adapters/fetch.d.cts +9 -0
- package/dist/adapters/fetch.d.ts +9 -0
- package/dist/adapters/fetch.js +271 -0
- package/dist/adapters/fetch.js.map +1 -0
- package/dist/adapters/next.cjs +796 -0
- package/dist/adapters/next.cjs.map +1 -0
- package/dist/adapters/next.d.cts +28 -0
- package/dist/adapters/next.d.ts +28 -0
- package/dist/adapters/next.js +775 -0
- package/dist/adapters/next.js.map +1 -0
- package/dist/client/index.cjs +153 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.d.cts +59 -0
- package/dist/client/index.d.ts +59 -0
- package/dist/client/index.js +126 -0
- package/dist/client/index.js.map +1 -0
- package/dist/core/index.cjs +452 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +11 -0
- package/dist/core/index.d.ts +11 -0
- package/dist/core/index.js +430 -0
- package/dist/core/index.js.map +1 -0
- package/dist/http/index.cjs +270 -0
- package/dist/http/index.cjs.map +1 -0
- package/dist/http/index.d.cts +49 -0
- package/dist/http/index.d.ts +49 -0
- package/dist/http/index.js +243 -0
- package/dist/http/index.js.map +1 -0
- package/dist/index.cjs +808 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +784 -0
- package/dist/index.js.map +1 -0
- package/dist/manager-BbmXpgXN.d.ts +29 -0
- package/dist/manager-gIjo-t8h.d.cts +29 -0
- package/dist/react/index.cjs +4320 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.css +155 -0
- package/dist/react/index.css.map +1 -0
- package/dist/react/index.d.cts +79 -0
- package/dist/react/index.d.ts +79 -0
- package/dist/react/index.js +4315 -0
- package/dist/react/index.js.map +1 -0
- package/dist/types-g2IYvH3O.d.cts +123 -0
- package/dist/types-g2IYvH3O.d.ts +123 -0
- package/package.json +100 -0
package/README.md
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
# s3kit
|
|
2
|
+
|
|
3
|
+
A secure, server-driven, framework-agnostic S3 file manager with a React UI.
|
|
4
|
+
|
|
5
|
+
Package modules:
|
|
6
|
+
|
|
7
|
+
- `core`: S3 operations (virtual folders, pagination, presigned uploads, previews)
|
|
8
|
+
- `http`: thin HTTP handler (maps requests to core)
|
|
9
|
+
- `adapters/*`: framework adapters (Express, Next.js, Fetch/Remix)
|
|
10
|
+
- `client`: browser helper (typed API calls + multi-file upload orchestration)
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm i s3kit
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quickstart (local testing)
|
|
19
|
+
|
|
20
|
+
This repo includes a working Next.js example with a customizer UI and live preview.
|
|
21
|
+
|
|
22
|
+
### 1) Set up the Next.js example
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
cd examples/nextjs-app
|
|
26
|
+
|
|
27
|
+
# Copy environment template and configure
|
|
28
|
+
cp .env.example .env
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Edit `.env` with your S3 credentials:
|
|
32
|
+
|
|
33
|
+
```env
|
|
34
|
+
AWS_REGION=us-east-1
|
|
35
|
+
S3_BUCKET=your-bucket-name
|
|
36
|
+
S3_ROOT_PREFIX=dev # optional, keeps test files under dev/
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Then run:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install
|
|
43
|
+
npm run dev
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Open `http://localhost:3000`
|
|
47
|
+
|
|
48
|
+
### 2) Configure S3 CORS (required for browser uploads)
|
|
49
|
+
|
|
50
|
+
Uploads use presigned `PUT` URLs, which means the browser uploads directly to S3.
|
|
51
|
+
|
|
52
|
+
Your bucket must allow CORS from your UI origin (example: `http://localhost:3000`) with:
|
|
53
|
+
|
|
54
|
+
- `PUT` (uploads)
|
|
55
|
+
- `GET` (previews)
|
|
56
|
+
- `HEAD` (often used by browsers)
|
|
57
|
+
|
|
58
|
+
Example CORS configuration:
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
[
|
|
62
|
+
{
|
|
63
|
+
"AllowedOrigins": ["http://localhost:3000"],
|
|
64
|
+
"AllowedMethods": ["GET", "PUT", "HEAD", "OPTIONS"],
|
|
65
|
+
"AllowedHeaders": ["*"],
|
|
66
|
+
"ExposeHeaders": ["ETag"]
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
If uploads still fail with a CORS error:
|
|
72
|
+
|
|
73
|
+
- Confirm the UI origin matches exactly (scheme, host, and port).
|
|
74
|
+
- Ensure the bucket CORS rules are applied to the correct bucket.
|
|
75
|
+
- Include `OPTIONS` and `PUT` in `AllowedMethods` (preflight + upload).
|
|
76
|
+
- If you set custom headers in `prepareUploads`, include them in `AllowedHeaders`.
|
|
77
|
+
|
|
78
|
+
## Credentials and security
|
|
79
|
+
|
|
80
|
+
- S3 credentials must be configured on the server (Node).
|
|
81
|
+
- Do not put credentials in the browser app.
|
|
82
|
+
- Credentials are read from environment variables or AWS SDK defaults.
|
|
83
|
+
|
|
84
|
+
## Server-side (core)
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { S3Client } from '@aws-sdk/client-s3';
|
|
88
|
+
import { S3FileManager } from 's3kit/core';
|
|
89
|
+
|
|
90
|
+
const s3 = new S3Client({ region: process.env.AWS_REGION });
|
|
91
|
+
|
|
92
|
+
const manager = new S3FileManager(s3, {
|
|
93
|
+
bucket: process.env.S3_BUCKET!,
|
|
94
|
+
rootPrefix: 'uploads',
|
|
95
|
+
authorizationMode: 'deny-by-default',
|
|
96
|
+
hooks: {
|
|
97
|
+
authorize: ({ ctx }) => Boolean(ctx.userId),
|
|
98
|
+
allowAction: ({ action, path }) => {
|
|
99
|
+
if (action === 'file.delete') return false;
|
|
100
|
+
if (path && path.startsWith('private/')) return false;
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`authorize` returning `false` responds with a 401. `allowAction` returning `false` responds with a 403. `authorizationMode` defaults to `deny-by-default`.
|
|
108
|
+
|
|
109
|
+
### Authorization hooks (agnostic)
|
|
110
|
+
|
|
111
|
+
Use `authorize` for auth checks and `allowAction` for per-action rules. Both hooks are optional, but with the default `deny-by-default`, you must provide at least one.
|
|
112
|
+
|
|
113
|
+
Example: API key auth (framework-agnostic)
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const manager = new S3FileManager(s3, {
|
|
117
|
+
bucket: process.env.S3_BUCKET!,
|
|
118
|
+
authorizationMode: 'deny-by-default',
|
|
119
|
+
hooks: {
|
|
120
|
+
authorize: ({ ctx }) => ctx.apiKey === process.env.FILE_MANAGER_API_KEY
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### S3-compatible endpoints (MinIO, LocalStack, R2, Spaces, Wasabi, ...)
|
|
126
|
+
|
|
127
|
+
You can point the AWS SDK client at an S3-compatible endpoint:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
const s3 = new S3Client({
|
|
131
|
+
region: process.env.AWS_REGION,
|
|
132
|
+
endpoint: process.env.S3_ENDPOINT,
|
|
133
|
+
forcePathStyle: process.env.S3_FORCE_PATH_STYLE === '1'
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Virtual folder listing + pagination
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
const page1 = await manager.list({ path: '', limit: 100 }, { userId: '123' });
|
|
141
|
+
const page2 = await manager.list({ path: '', cursor: page1.nextCursor, limit: 100 }, { userId: '123' });
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## HTTP layer
|
|
145
|
+
|
|
146
|
+
The HTTP handler expects JSON `POST` requests.
|
|
147
|
+
|
|
148
|
+
Routes:
|
|
149
|
+
|
|
150
|
+
- `POST /list`
|
|
151
|
+
- `POST /search`
|
|
152
|
+
- `POST /folder/create`
|
|
153
|
+
- `POST /folder/delete`
|
|
154
|
+
- `POST /files/delete`
|
|
155
|
+
- `POST /files/copy`
|
|
156
|
+
- `POST /files/move`
|
|
157
|
+
- `POST /upload/prepare`
|
|
158
|
+
- `POST /preview`
|
|
159
|
+
|
|
160
|
+
Notes:
|
|
161
|
+
|
|
162
|
+
- `POST /search` returns file entries in stable, S3 listing order (lexicographic by key/path) so pagination via `cursor` is deterministic.
|
|
163
|
+
- `cursor` is the underlying S3 continuation token; pass `nextCursor` from the previous response to fetch the next page.
|
|
164
|
+
|
|
165
|
+
### Next.js (App Router) adapter
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
// app/api/s3/[...path]/route.ts
|
|
169
|
+
import { createNextRouteHandlerFromEnv } from 's3kit/adapters/next';
|
|
170
|
+
|
|
171
|
+
export const POST = createNextRouteHandlerFromEnv({
|
|
172
|
+
basePath: '/api/s3',
|
|
173
|
+
authorization: {
|
|
174
|
+
mode: 'allow-by-default'
|
|
175
|
+
},
|
|
176
|
+
env: {
|
|
177
|
+
region: 'AWS_REGION',
|
|
178
|
+
bucket: 'S3_BUCKET',
|
|
179
|
+
rootPrefix: 'S3_ROOT_PREFIX',
|
|
180
|
+
endpoint: 'S3_ENDPOINT',
|
|
181
|
+
forcePathStyle: 'S3_FORCE_PATH_STYLE',
|
|
182
|
+
requireUserId: 'REQUIRE_USER_ID'
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
This helper reads env values from the map above. At minimum, `region` and `bucket`
|
|
188
|
+
must point to defined env vars. For production, replace the example `allow-by-default`
|
|
189
|
+
with your own `authorize` / `allowAction` hooks.
|
|
190
|
+
|
|
191
|
+
### Express adapter
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
import express from 'express';
|
|
195
|
+
import { createExpressS3FileManagerHandler } from 's3kit/adapters/express';
|
|
196
|
+
|
|
197
|
+
const app = express();
|
|
198
|
+
app.use(express.json({ limit: '2mb' }));
|
|
199
|
+
|
|
200
|
+
app.use(
|
|
201
|
+
'/api/s3',
|
|
202
|
+
createExpressS3FileManagerHandler({
|
|
203
|
+
manager,
|
|
204
|
+
getContext: (req) => ({ userId: req.header('x-user-id') ?? undefined }),
|
|
205
|
+
api: { basePath: '/api/s3' }
|
|
206
|
+
})
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
app.listen(3001);
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Fetch/Remix adapter
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
import { createFetchHandler } from 's3kit/adapters/fetch';
|
|
216
|
+
|
|
217
|
+
export const handler = createFetchHandler({
|
|
218
|
+
manager,
|
|
219
|
+
getContext: async (req) => ({ userId: req.headers.get('x-user-id') ?? undefined }),
|
|
220
|
+
api: { basePath: '/api/s3' }
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Client helper
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import { S3FileManagerClient } from 's3kit/client';
|
|
228
|
+
|
|
229
|
+
const client = new S3FileManagerClient({
|
|
230
|
+
apiUrl: '/api/s3'
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const listing = await client.list({ path: '' });
|
|
234
|
+
|
|
235
|
+
const preview = await client.getPreviewUrl({ path: 'docs/readme.pdf', inline: true });
|
|
236
|
+
|
|
237
|
+
await client.uploadFiles({
|
|
238
|
+
files: [
|
|
239
|
+
{ file: someFile, path: `docs/${someFile.name}` },
|
|
240
|
+
{ file: otherFile, path: `docs/${otherFile.name}` }
|
|
241
|
+
],
|
|
242
|
+
hooks: {
|
|
243
|
+
onUploadProgress: ({ path, loaded, total }) => {
|
|
244
|
+
console.log(path, loaded, total);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Alternative client config
|
|
251
|
+
|
|
252
|
+
If you prefer splitting origin + mount path:
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
const client = new S3FileManagerClient({
|
|
256
|
+
baseUrl: 'http://localhost:3000',
|
|
257
|
+
basePath: '/api/s3'
|
|
258
|
+
});
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Multiple S3 configs (multiple API endpoints)
|
|
262
|
+
|
|
263
|
+
For multi-bucket / multi-environment setups, keep configs on the server and expose them as separate API endpoints.
|
|
264
|
+
|
|
265
|
+
- The client simply points to the right `apiUrl` (e.g. `/api/s3` vs `/api/s3-media`).
|
|
266
|
+
- The server routes each endpoint to its own `S3FileManager` instance.
|
|
267
|
+
|
|
268
|
+
Example (framework-agnostic `http` handler):
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
import { createS3FileManagerHttpHandler } from 's3kit/http';
|
|
272
|
+
|
|
273
|
+
export const s3Handler = createS3FileManagerHttpHandler({
|
|
274
|
+
getManager: () => managers.default,
|
|
275
|
+
api: { basePath: '/api/s3' }
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
export const mediaHandler = createS3FileManagerHttpHandler({
|
|
279
|
+
getManager: () => managers.media,
|
|
280
|
+
api: { basePath: '/api/s3-media' }
|
|
281
|
+
});
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## React embed examples
|
|
285
|
+
|
|
286
|
+
### Common UI props
|
|
287
|
+
|
|
288
|
+
- `theme`: `'light' | 'dark' | 'system'`
|
|
289
|
+
- `mode`: `'viewer' | 'picker' | 'manager'`
|
|
290
|
+
- `selection`: `'single' | 'multiple'`
|
|
291
|
+
- `toolbar`: `{ search, breadcrumbs, viewSwitcher, sort }`
|
|
292
|
+
- `labels`: text overrides for buttons and placeholders
|
|
293
|
+
- `viewMode`: `'grid' | 'list'`
|
|
294
|
+
|
|
295
|
+
The React UI is styled with CSS variables + CSS modules to keep styles scoped.
|
|
296
|
+
|
|
297
|
+
### 1) Viewer (read-only file browser)
|
|
298
|
+
|
|
299
|
+
```tsx
|
|
300
|
+
import { FileManager } from 's3kit/react';
|
|
301
|
+
|
|
302
|
+
export function FileViewer() {
|
|
303
|
+
return (
|
|
304
|
+
<FileManager
|
|
305
|
+
apiUrl="/api/s3"
|
|
306
|
+
mode="viewer"
|
|
307
|
+
allowActions={{ upload: false, createFolder: false, delete: false, rename: false, move: false, copy: false, restore: false }}
|
|
308
|
+
/>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### 2) Picker (select one or many files)
|
|
314
|
+
|
|
315
|
+
```tsx
|
|
316
|
+
import { FilePicker } from 's3kit/react';
|
|
317
|
+
|
|
318
|
+
export function FileField() {
|
|
319
|
+
return (
|
|
320
|
+
<FilePicker
|
|
321
|
+
apiUrl="/api/s3"
|
|
322
|
+
selection="single"
|
|
323
|
+
onConfirm={(entries) => {
|
|
324
|
+
const file = entries[0];
|
|
325
|
+
console.log('Selected:', file);
|
|
326
|
+
}}
|
|
327
|
+
onSelectionChange={(entries) => {
|
|
328
|
+
console.log('Current selection:', entries);
|
|
329
|
+
}}
|
|
330
|
+
confirmLabel="Use file"
|
|
331
|
+
allowActions={{ upload: true, createFolder: true }}
|
|
332
|
+
/>
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### 3) Manager (full CRUD)
|
|
338
|
+
|
|
339
|
+
```tsx
|
|
340
|
+
import { FileManager } from 's3kit/react';
|
|
341
|
+
|
|
342
|
+
export function FileManagerAdmin() {
|
|
343
|
+
return (
|
|
344
|
+
<FileManager
|
|
345
|
+
apiUrl="/api/s3"
|
|
346
|
+
mode="manager"
|
|
347
|
+
selection="multiple"
|
|
348
|
+
allowActions={{
|
|
349
|
+
upload: true,
|
|
350
|
+
createFolder: true,
|
|
351
|
+
delete: true,
|
|
352
|
+
rename: true,
|
|
353
|
+
move: true,
|
|
354
|
+
copy: true,
|
|
355
|
+
restore: true
|
|
356
|
+
}}
|
|
357
|
+
/>
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### UI customization (toolbar + labels)
|
|
363
|
+
|
|
364
|
+
```tsx
|
|
365
|
+
import { FileManager } from 's3kit/react';
|
|
366
|
+
|
|
367
|
+
export function FileManagerCustomized() {
|
|
368
|
+
return (
|
|
369
|
+
<FileManager
|
|
370
|
+
apiUrl="/api/s3"
|
|
371
|
+
toolbar={{ search: false, breadcrumbs: true, viewSwitcher: true, sort: false }}
|
|
372
|
+
labels={{
|
|
373
|
+
upload: 'Add files',
|
|
374
|
+
newFolder: 'Create folder',
|
|
375
|
+
delete: 'Remove',
|
|
376
|
+
deleteForever: 'Remove forever',
|
|
377
|
+
restore: 'Restore',
|
|
378
|
+
emptyTrash: 'Clear trash',
|
|
379
|
+
confirm: 'Select',
|
|
380
|
+
searchPlaceholder: 'Search...'
|
|
381
|
+
}}
|
|
382
|
+
viewMode="grid"
|
|
383
|
+
/>
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## Example
|
|
389
|
+
|
|
390
|
+
- `examples/nextjs-app`: Next.js App Router example with a customizer panel and live preview
|
|
391
|
+
|
|
392
|
+
## Security checklist
|
|
393
|
+
|
|
394
|
+
- Keep S3 credentials on the server only.
|
|
395
|
+
- Require auth on the API route (session/JWT/API key).
|
|
396
|
+
- Use `deny-by-default` and implement `authorize` / `allowAction`.
|
|
397
|
+
- Enforce least-privilege IAM policies for the bucket.
|
|
398
|
+
- Validate inputs (paths, allowed actions) and consider rate limiting.
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/adapters/express.ts
|
|
21
|
+
var express_exports = {};
|
|
22
|
+
__export(express_exports, {
|
|
23
|
+
createExpressS3FileManagerHandler: () => createExpressS3FileManagerHandler
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(express_exports);
|
|
26
|
+
|
|
27
|
+
// src/core/errors.ts
|
|
28
|
+
var S3FileManagerAuthorizationError = class extends Error {
|
|
29
|
+
status;
|
|
30
|
+
code;
|
|
31
|
+
constructor(message, status, code) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.status = status;
|
|
34
|
+
this.code = code;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// src/http/handler.ts
|
|
39
|
+
var S3FileManagerHttpError = class extends Error {
|
|
40
|
+
status;
|
|
41
|
+
code;
|
|
42
|
+
constructor(status, code, message) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.status = status;
|
|
45
|
+
this.code = code;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
function normalizeBasePath(basePath) {
|
|
49
|
+
if (!basePath) return "";
|
|
50
|
+
if (basePath === "/") return "";
|
|
51
|
+
return basePath.startsWith("/") ? basePath.replace(/\/+$/, "") : `/${basePath.replace(/\/+$/, "")}`;
|
|
52
|
+
}
|
|
53
|
+
function jsonError(status, code, message) {
|
|
54
|
+
return {
|
|
55
|
+
status,
|
|
56
|
+
headers: { "content-type": "application/json" },
|
|
57
|
+
body: {
|
|
58
|
+
error: {
|
|
59
|
+
code,
|
|
60
|
+
message
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function ensureObject(body) {
|
|
66
|
+
if (body && typeof body === "object" && !Array.isArray(body))
|
|
67
|
+
return body;
|
|
68
|
+
throw new S3FileManagerHttpError(400, "invalid_body", "Expected JSON object body");
|
|
69
|
+
}
|
|
70
|
+
function optionalString(value, key) {
|
|
71
|
+
if (value === void 0) return void 0;
|
|
72
|
+
if (typeof value === "string") return value;
|
|
73
|
+
throw new S3FileManagerHttpError(400, "invalid_body", `Expected '${key}' to be a string`);
|
|
74
|
+
}
|
|
75
|
+
function requiredString(value, key) {
|
|
76
|
+
if (typeof value === "string") return value;
|
|
77
|
+
throw new S3FileManagerHttpError(400, "invalid_body", `Expected '${key}' to be a string`);
|
|
78
|
+
}
|
|
79
|
+
function optionalNumber(value, key) {
|
|
80
|
+
if (value === void 0) return void 0;
|
|
81
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
82
|
+
throw new S3FileManagerHttpError(400, "invalid_body", `Expected '${key}' to be a finite number`);
|
|
83
|
+
}
|
|
84
|
+
function optionalBoolean(value, key) {
|
|
85
|
+
if (value === void 0) return void 0;
|
|
86
|
+
if (typeof value === "boolean") return value;
|
|
87
|
+
throw new S3FileManagerHttpError(400, "invalid_body", `Expected '${key}' to be a boolean`);
|
|
88
|
+
}
|
|
89
|
+
function requiredStringArray(value, key) {
|
|
90
|
+
if (!Array.isArray(value)) {
|
|
91
|
+
throw new S3FileManagerHttpError(
|
|
92
|
+
400,
|
|
93
|
+
"invalid_body",
|
|
94
|
+
`Expected '${key}' to be an array of strings`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
for (const item of value) {
|
|
98
|
+
if (typeof item !== "string") {
|
|
99
|
+
throw new S3FileManagerHttpError(
|
|
100
|
+
400,
|
|
101
|
+
"invalid_body",
|
|
102
|
+
`Expected '${key}' to be an array of strings`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
function optionalStringRecord(value, key) {
|
|
109
|
+
if (value === void 0) return void 0;
|
|
110
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
111
|
+
throw new S3FileManagerHttpError(
|
|
112
|
+
400,
|
|
113
|
+
"invalid_body",
|
|
114
|
+
`Expected '${key}' to be an object of strings`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const out = {};
|
|
118
|
+
for (const [k, v] of Object.entries(value)) {
|
|
119
|
+
if (typeof v !== "string") {
|
|
120
|
+
throw new S3FileManagerHttpError(400, "invalid_body", `Expected '${key}.${k}' to be a string`);
|
|
121
|
+
}
|
|
122
|
+
out[k] = v;
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
function parseListOptions(body) {
|
|
127
|
+
const obj = ensureObject(body);
|
|
128
|
+
return {
|
|
129
|
+
path: requiredString(obj.path, "path"),
|
|
130
|
+
cursor: optionalString(obj.cursor, "cursor"),
|
|
131
|
+
limit: optionalNumber(obj.limit, "limit")
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function parseSearchOptions(body) {
|
|
135
|
+
const obj = ensureObject(body);
|
|
136
|
+
return {
|
|
137
|
+
query: requiredString(obj.query, "query"),
|
|
138
|
+
path: optionalString(obj.path, "path"),
|
|
139
|
+
recursive: optionalBoolean(obj.recursive, "recursive"),
|
|
140
|
+
limit: optionalNumber(obj.limit, "limit"),
|
|
141
|
+
cursor: optionalString(obj.cursor, "cursor")
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function parseCreateFolderOptions(body) {
|
|
145
|
+
const obj = ensureObject(body);
|
|
146
|
+
return { path: requiredString(obj.path, "path") };
|
|
147
|
+
}
|
|
148
|
+
function parseDeleteFolderOptions(body) {
|
|
149
|
+
const obj = ensureObject(body);
|
|
150
|
+
return {
|
|
151
|
+
path: requiredString(obj.path, "path"),
|
|
152
|
+
recursive: optionalBoolean(obj.recursive, "recursive")
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function parseDeleteFilesOptions(body) {
|
|
156
|
+
const obj = ensureObject(body);
|
|
157
|
+
return { paths: requiredStringArray(obj.paths, "paths") };
|
|
158
|
+
}
|
|
159
|
+
function parseCopyMoveOptions(body) {
|
|
160
|
+
const obj = ensureObject(body);
|
|
161
|
+
return {
|
|
162
|
+
fromPath: requiredString(obj.fromPath, "fromPath"),
|
|
163
|
+
toPath: requiredString(obj.toPath, "toPath")
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function parsePrepareUploadsOptions(body) {
|
|
167
|
+
const obj = ensureObject(body);
|
|
168
|
+
const itemsValue = obj.items;
|
|
169
|
+
if (!Array.isArray(itemsValue)) {
|
|
170
|
+
throw new S3FileManagerHttpError(400, "invalid_body", "Expected 'items' to be an array");
|
|
171
|
+
}
|
|
172
|
+
const items = itemsValue.map((raw, idx) => {
|
|
173
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
174
|
+
throw new S3FileManagerHttpError(
|
|
175
|
+
400,
|
|
176
|
+
"invalid_body",
|
|
177
|
+
`Expected 'items[${idx}]' to be an object`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
const item = raw;
|
|
181
|
+
return {
|
|
182
|
+
path: requiredString(item.path, `items[${idx}].path`),
|
|
183
|
+
contentType: optionalString(item.contentType, `items[${idx}].contentType`),
|
|
184
|
+
cacheControl: optionalString(item.cacheControl, `items[${idx}].cacheControl`),
|
|
185
|
+
contentDisposition: optionalString(
|
|
186
|
+
item.contentDisposition,
|
|
187
|
+
`items[${idx}].contentDisposition`
|
|
188
|
+
),
|
|
189
|
+
metadata: optionalStringRecord(item.metadata, `items[${idx}].metadata`)
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
return {
|
|
193
|
+
items,
|
|
194
|
+
expiresInSeconds: optionalNumber(obj.expiresInSeconds, "expiresInSeconds")
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function parsePreviewOptions(body) {
|
|
198
|
+
const obj = ensureObject(body);
|
|
199
|
+
return {
|
|
200
|
+
path: requiredString(obj.path, "path"),
|
|
201
|
+
expiresInSeconds: optionalNumber(obj.expiresInSeconds, "expiresInSeconds"),
|
|
202
|
+
inline: optionalBoolean(obj.inline, "inline")
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function createS3FileManagerHttpHandler(options) {
|
|
206
|
+
const basePath = normalizeBasePath(options.api?.basePath);
|
|
207
|
+
if (!options.manager && !options.getManager) {
|
|
208
|
+
throw new Error("createS3FileManagerHttpHandler requires either manager or getManager");
|
|
209
|
+
}
|
|
210
|
+
return async (req) => {
|
|
211
|
+
try {
|
|
212
|
+
const ctx = await options.getContext?.(req) ?? {};
|
|
213
|
+
const manager = options.getManager ? await options.getManager(req, ctx) : options.manager;
|
|
214
|
+
const method = req.method.toUpperCase();
|
|
215
|
+
const path = req.path.startsWith(basePath) ? req.path.slice(basePath.length) || "/" : req.path;
|
|
216
|
+
if (method === "POST" && path === "/list") {
|
|
217
|
+
const out = await manager.list(parseListOptions(req.body), ctx);
|
|
218
|
+
return { status: 200, headers: { "content-type": "application/json" }, body: out };
|
|
219
|
+
}
|
|
220
|
+
if (method === "POST" && path === "/search") {
|
|
221
|
+
const out = await manager.search(parseSearchOptions(req.body), ctx);
|
|
222
|
+
return { status: 200, headers: { "content-type": "application/json" }, body: out };
|
|
223
|
+
}
|
|
224
|
+
if (method === "POST" && path === "/folder/create") {
|
|
225
|
+
await manager.createFolder(parseCreateFolderOptions(req.body), ctx);
|
|
226
|
+
return { status: 204 };
|
|
227
|
+
}
|
|
228
|
+
if (method === "POST" && path === "/folder/delete") {
|
|
229
|
+
await manager.deleteFolder(parseDeleteFolderOptions(req.body), ctx);
|
|
230
|
+
return { status: 204 };
|
|
231
|
+
}
|
|
232
|
+
if (method === "POST" && path === "/files/delete") {
|
|
233
|
+
await manager.deleteFiles(parseDeleteFilesOptions(req.body), ctx);
|
|
234
|
+
return { status: 204 };
|
|
235
|
+
}
|
|
236
|
+
if (method === "POST" && path === "/files/copy") {
|
|
237
|
+
await manager.copy(parseCopyMoveOptions(req.body), ctx);
|
|
238
|
+
return { status: 204 };
|
|
239
|
+
}
|
|
240
|
+
if (method === "POST" && path === "/files/move") {
|
|
241
|
+
await manager.move(parseCopyMoveOptions(req.body), ctx);
|
|
242
|
+
return { status: 204 };
|
|
243
|
+
}
|
|
244
|
+
if (method === "POST" && path === "/upload/prepare") {
|
|
245
|
+
const out = await manager.prepareUploads(parsePrepareUploadsOptions(req.body), ctx);
|
|
246
|
+
return { status: 200, headers: { "content-type": "application/json" }, body: out };
|
|
247
|
+
}
|
|
248
|
+
if (method === "POST" && path === "/preview") {
|
|
249
|
+
const out = await manager.getPreviewUrl(parsePreviewOptions(req.body), ctx);
|
|
250
|
+
return { status: 200, headers: { "content-type": "application/json" }, body: out };
|
|
251
|
+
}
|
|
252
|
+
return jsonError(404, "not_found", "Route not found");
|
|
253
|
+
} catch (err) {
|
|
254
|
+
if (err instanceof S3FileManagerHttpError) {
|
|
255
|
+
return jsonError(err.status, err.code, err.message);
|
|
256
|
+
}
|
|
257
|
+
if (err instanceof S3FileManagerAuthorizationError) {
|
|
258
|
+
return jsonError(err.status, err.code, err.message);
|
|
259
|
+
}
|
|
260
|
+
console.error("[S3FileManager Error]", err);
|
|
261
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
262
|
+
return jsonError(500, "internal_error", message);
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/adapters/express.ts
|
|
268
|
+
function toSingleHeaderValue(val) {
|
|
269
|
+
if (typeof val === "string") return val;
|
|
270
|
+
if (Array.isArray(val)) return val.join(",");
|
|
271
|
+
return void 0;
|
|
272
|
+
}
|
|
273
|
+
function createExpressS3FileManagerHandler(options) {
|
|
274
|
+
const handler = createS3FileManagerHttpHandler(options);
|
|
275
|
+
return async (req, res) => {
|
|
276
|
+
const httpReq = {
|
|
277
|
+
method: req.method,
|
|
278
|
+
path: `${req.baseUrl ?? ""}${req.path}`,
|
|
279
|
+
query: req.query,
|
|
280
|
+
headers: req.headers,
|
|
281
|
+
body: req.body
|
|
282
|
+
};
|
|
283
|
+
const out = await handler(httpReq);
|
|
284
|
+
if (out.headers) {
|
|
285
|
+
for (const [k, v] of Object.entries(out.headers)) {
|
|
286
|
+
res.setHeader(k, v);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (out.status === 204) {
|
|
290
|
+
res.status(204).end();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const ct = toSingleHeaderValue((out.headers ?? {})["content-type"]);
|
|
294
|
+
if (ct?.includes("application/json")) {
|
|
295
|
+
res.status(out.status).json(out.body ?? null);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
res.status(out.status).send(out.body ?? null);
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
302
|
+
0 && (module.exports = {
|
|
303
|
+
createExpressS3FileManagerHandler
|
|
304
|
+
});
|
|
305
|
+
//# sourceMappingURL=express.cjs.map
|