manage-storage 0.0.62 → 0.0.64
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 +258 -288
- package/dist/index.browser-VtSbPvUr.js +1 -0
- package/dist/{manage-storage-CGRNU6uH.js → manage-storage-De0Cb_cq.js} +1 -1
- package/dist/manage-storage.d.ts +261 -67
- package/dist/manage-storage.js +1 -1
- package/package.json +1 -1
- package/dist/index.browser-CZ9CQ86G.js +0 -1
package/README.md
CHANGED
|
@@ -2,18 +2,44 @@
|
|
|
2
2
|
<img width="350px" src="https://i.imgur.com/qEdTwly.png" />
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
+
<!-- template-git-repo:badges:start -->
|
|
6
|
+
<p align="center">
|
|
7
|
+
<a href="https://starterdocs.vtempest.workers.dev/docs/packages/manage-storage"><img src="https://img.shields.io/badge/Docs-blue?logo=ReadTheDocs&logoColor=white" alt="Documentation" /></a>
|
|
8
|
+
<a href="https://stackblitz.com/github/OpenSourceAGI/dev-tools-starter-agent/tree/master/packages/manage-storage"><img height="20px" src="https://developer.stackblitz.com/img/open_in_stackblitz.svg" alt="Open in StackBlitz" /></a>
|
|
9
|
+
<br />
|
|
10
|
+
<a href="https://www.npmjs.com/package/manage-storage"><img src="https://img.shields.io/npm/dm/manage-storage.svg" alt="NPM Monthly Downloads" /></a>
|
|
11
|
+
<a href="https://www.npmjs.com/package/manage-storage"><img src="https://img.shields.io/npm/v/manage-storage.svg" alt="npm version" /></a>
|
|
12
|
+
<a href="https://www.npmjs.com/package/manage-storage"><img src="https://img.shields.io/npm/dt/manage-storage.svg" alt="NPM Total Downloads" /></a>
|
|
13
|
+
<a href="https://www.npmjs.com/package/manage-storage"><img src="https://img.shields.io/npm/types/manage-storage" alt="TypeScript types" /></a>
|
|
14
|
+
<a href="https://packagephobia.com/result?p=manage-storage"><img src="https://packagephobia.com/badge?p=manage-storage" alt="Install size" /></a>
|
|
15
|
+
<a href="https://app.codecov.io/gh/OpenSourceAGI/dev-tools-starter-agent/flags"><img src="https://img.shields.io/codecov/c/github/OpenSourceAGI/dev-tools-starter-agent?flag=manage-storage&label=manage-storage%20coverage&logo=codecov&logoColor=white" alt="Coverage" /></a>
|
|
16
|
+
</p>
|
|
17
|
+
<!-- template-git-repo:badges:end -->
|
|
18
|
+
|
|
19
|
+
<!-- skills:install:start -->
|
|
20
|
+
**🤖 Agent skill** — `npx skills@latest add https://github.com/OpenSourceAGI/dev-tools-starter-agent --skill manage-storage` ([what it covers](../../skills/manage-storage/SKILL.md))
|
|
21
|
+
<!-- skills:install:end -->
|
|
22
|
+
|
|
5
23
|
# Cloud Storage Manager
|
|
6
24
|
|
|
7
|
-
|
|
25
|
+
One class, `StorageManager`, for Amazon S3, Cloudflare R2 and Backblaze B2. It
|
|
26
|
+
resolves credentials once, from the environment or from what you pass it, then
|
|
27
|
+
exposes the bucket as methods: `.upload()`, `.download()`, `.list()`, `.copy()`,
|
|
28
|
+
`.rename()`, `.delete()`, `.deleteAll()`, `.exists()`. Built on the official AWS
|
|
29
|
+
SDK v3, written in TypeScript, and shipped with its declarations, so the key and
|
|
30
|
+
body an operation needs are in its signature rather than in a comment.
|
|
31
|
+
|
|
32
|
+
[**▶ Open the runnable example in StackBlitz**](https://stackblitz.com/github/OpenSourceAGI/dev-tools-starter-agent/tree/master/packages/manage-storage)
|
|
33
|
+
— it boots this package with its dependencies installed; add your bucket's four
|
|
34
|
+
env vars in the StackBlitz shell and the snippets below run as written.
|
|
8
35
|
|
|
9
36
|
## Features
|
|
10
37
|
|
|
11
|
-
- **Multi-
|
|
12
|
-
- **Auto-
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
16
|
-
- **Minified**: Terser minification for smaller bundle sizes
|
|
38
|
+
- **Multi-cloud**: the same calls against S3, R2 and B2 — only the endpoint differs
|
|
39
|
+
- **Auto-detection**: the provider is read from whichever credential prefix your environment has
|
|
40
|
+
- **Typed end to end**: `.upload(key, body)` and `.list(prefix)` are checked at the call site, not documented in prose
|
|
41
|
+
- **Paginated**: `list()` follows the continuation token and `deleteAll()` batches, so neither stops silently at 1000 keys
|
|
42
|
+
- **Edge-ready**: pass credentials in the constructor where there is no `process.env`; nothing touches the file system
|
|
17
43
|
|
|
18
44
|
## Installation
|
|
19
45
|
|
|
@@ -27,44 +53,26 @@ bun i manage-storage
|
|
|
27
53
|
|
|
28
54
|
## Quick Start
|
|
29
55
|
|
|
30
|
-
```
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
// Upload a file
|
|
34
|
-
await manageStorage("upload", {
|
|
35
|
-
key: "documents/report.pdf",
|
|
36
|
-
body: fileContent,
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
// Download a file
|
|
40
|
-
const data = await manageStorage("download", {
|
|
41
|
-
key: "documents/report.pdf",
|
|
42
|
-
});
|
|
56
|
+
```ts
|
|
57
|
+
import { StorageManager } from "manage-storage";
|
|
43
58
|
|
|
44
|
-
//
|
|
45
|
-
const
|
|
59
|
+
// Credentials come from the environment; the provider is detected from them.
|
|
60
|
+
const storage = new StorageManager();
|
|
46
61
|
|
|
47
|
-
|
|
48
|
-
await
|
|
49
|
-
|
|
50
|
-
destinationKey: "documents/report-backup.pdf",
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// Rename a file (copy + delete)
|
|
54
|
-
await manageStorage("rename", {
|
|
55
|
-
key: "documents/old-name.pdf",
|
|
56
|
-
destinationKey: "documents/new-name.pdf",
|
|
57
|
-
});
|
|
62
|
+
await storage.upload("documents/report.pdf", fileContent);
|
|
63
|
+
const data: string = await storage.download("documents/report.pdf");
|
|
64
|
+
const keys: string[] = await storage.list();
|
|
58
65
|
|
|
59
|
-
|
|
60
|
-
await
|
|
61
|
-
|
|
62
|
-
});
|
|
66
|
+
await storage.copy("documents/report.pdf", "documents/report-backup.pdf");
|
|
67
|
+
await storage.rename("documents/old-name.pdf", "documents/new-name.pdf");
|
|
68
|
+
await storage.delete("documents/report.pdf");
|
|
63
69
|
```
|
|
64
70
|
|
|
65
71
|
## Configuration
|
|
66
72
|
|
|
67
|
-
Set environment variables for your preferred provider. The library
|
|
73
|
+
Set environment variables for your preferred provider. The library detects which
|
|
74
|
+
provider to use from the first prefix whose four variables are all present:
|
|
75
|
+
Cloudflare, then Backblaze, then Amazon.
|
|
68
76
|
|
|
69
77
|
### Cloudflare R2
|
|
70
78
|
|
|
@@ -84,6 +92,9 @@ BACKBLAZE_SECRET_ACCESS_KEY=your-application-key
|
|
|
84
92
|
BACKBLAZE_BUCKET_URL=https://s3.us-west-004.backblazeb2.com
|
|
85
93
|
```
|
|
86
94
|
|
|
95
|
+
B2's console calls these two the *application key id* and *application key*;
|
|
96
|
+
`BACKBLAZE_APPLICATION_KEY_ID` and `BACKBLAZE_APPLICATION_KEY` are read too.
|
|
97
|
+
|
|
87
98
|
### Amazon S3
|
|
88
99
|
|
|
89
100
|
```env
|
|
@@ -94,187 +105,187 @@ AMAZON_BUCKET_URL=https://s3.amazonaws.com
|
|
|
94
105
|
AMAZON_REGION=us-east-1
|
|
95
106
|
```
|
|
96
107
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
### `manageStorage(action, options)`
|
|
100
|
-
|
|
101
|
-
Performs storage operations on your configured cloud provider.
|
|
108
|
+
Or pass any of it to the constructor, which wins over the environment — the only
|
|
109
|
+
option on an edge runtime, where there is no `process.env` to read:
|
|
102
110
|
|
|
103
|
-
|
|
111
|
+
```ts
|
|
112
|
+
import { StorageManager } from "manage-storage";
|
|
104
113
|
|
|
105
|
-
|
|
106
|
-
|
|
114
|
+
const storage = new StorageManager({
|
|
115
|
+
provider: "cloudflare",
|
|
116
|
+
bucket: "my-bucket",
|
|
117
|
+
accessKeyId: "runtime-key-id",
|
|
118
|
+
secretAccessKey: "runtime-secret",
|
|
119
|
+
endpoint: "https://account-id.r2.cloudflarestorage.com",
|
|
120
|
+
});
|
|
121
|
+
```
|
|
107
122
|
|
|
108
|
-
|
|
123
|
+
## API Reference
|
|
109
124
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
|
113
|
-
|
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
125
|
+
### `new StorageManager(config?)`
|
|
126
|
+
|
|
127
|
+
| Option | Type | Falls back to |
|
|
128
|
+
| ----------------- | ---------------------------------------- | ---------------------------------------------------------- |
|
|
129
|
+
| `provider` | `"amazon" \| "cloudflare" \| "backblaze"` | Auto-detected from the environment |
|
|
130
|
+
| `bucket` | `string` | `<PROVIDER>_BUCKET_NAME` |
|
|
131
|
+
| `accessKeyId` | `string` | `<PROVIDER>_ACCESS_KEY_ID` |
|
|
132
|
+
| `secretAccessKey` | `string` | `<PROVIDER>_SECRET_ACCESS_KEY` |
|
|
133
|
+
| `endpoint` | `string` | `<PROVIDER>_BUCKET_URL` |
|
|
134
|
+
| `region` | `string` | `AMAZON_REGION`, else `us-east-1`; R2 and B2 are `auto` |
|
|
135
|
+
| `client` | `S3Client` | A client built from the above — pass one to supply your own |
|
|
136
|
+
|
|
137
|
+
Credentials are checked on the first operation, not in the constructor, so a
|
|
138
|
+
manager can be created at module scope before `dotenv` has run. `storage.provider`
|
|
139
|
+
and `storage.bucket` report what it resolved to.
|
|
140
|
+
|
|
141
|
+
### Methods
|
|
142
|
+
|
|
143
|
+
| Method | Returns | Notes |
|
|
144
|
+
| ----------------------------------------- | ------------------- | ---------------------------------------------------------------- |
|
|
145
|
+
| `.upload(key, body, options?)` | `UploadResult` | `body` is a string, `Buffer`, `Uint8Array` or stream; `options.contentType` sets the stored MIME type |
|
|
146
|
+
| `.download(key)` | `string` | UTF-8 text. Throws if the key does not exist |
|
|
147
|
+
| `.downloadBytes(key)` | `Uint8Array` | For anything that is not text |
|
|
148
|
+
| `.list(prefix?)` | `string[]` | Every key, paginated to the end |
|
|
149
|
+
| `.exists(key)` | `boolean` | Exact key match, no download |
|
|
150
|
+
| `.copy(key, destinationKey)` | `CopyResult` | |
|
|
151
|
+
| `.rename(key, destinationKey)` | `RenameResult` | Copy then delete; not atomic |
|
|
152
|
+
| `.delete(key)` | `DeleteResult` | Deleting a missing key succeeds, as it does in S3 |
|
|
153
|
+
| `.deleteAll(prefix?)` | `DeleteAllResult` | Irreversible. Batches of 1000 |
|
|
154
|
+
|
|
155
|
+
Also exported: `detectProvider()`, `resolveConfig(config)`, and the types
|
|
156
|
+
`Provider`, `StorageBody`, `StorageConfig`, `ResolvedConfig`, `UploadResult`,
|
|
157
|
+
`DeleteResult`, `DeleteAllResult`, `CopyResult`, `RenameResult`.
|
|
116
158
|
|
|
117
159
|
## Usage Examples
|
|
118
160
|
|
|
119
161
|
### 1. Upload Files
|
|
120
162
|
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
body: "Hello, World!",
|
|
126
|
-
});
|
|
163
|
+
```ts
|
|
164
|
+
import { StorageManager } from "manage-storage";
|
|
165
|
+
|
|
166
|
+
const storage = new StorageManager();
|
|
127
167
|
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
key: "data/file.bin",
|
|
132
|
-
body: buffer,
|
|
168
|
+
// Text
|
|
169
|
+
await storage.upload("notes/memo.txt", "Hello, World!", {
|
|
170
|
+
contentType: "text/plain",
|
|
133
171
|
});
|
|
134
172
|
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
173
|
+
// Buffer
|
|
174
|
+
const buffer: Buffer = Buffer.from("File contents");
|
|
175
|
+
await storage.upload("data/file.bin", buffer);
|
|
176
|
+
|
|
177
|
+
// JSON
|
|
178
|
+
await storage.upload("config/settings.json", JSON.stringify({ theme: "dark" }), {
|
|
179
|
+
contentType: "application/json",
|
|
139
180
|
});
|
|
140
181
|
```
|
|
141
182
|
|
|
142
183
|
### 2. Download Files
|
|
143
184
|
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
key: "notes/memo.txt",
|
|
148
|
-
});
|
|
149
|
-
console.log(data); // "Hello, World!"
|
|
185
|
+
```ts
|
|
186
|
+
const text: string = await storage.download("notes/memo.txt");
|
|
187
|
+
console.log(text); // "Hello, World!"
|
|
150
188
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
189
|
+
interface Settings {
|
|
190
|
+
theme: "dark" | "light";
|
|
191
|
+
lang: string;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const settings: Settings = JSON.parse(await storage.download("config/settings.json"));
|
|
195
|
+
console.log(settings.theme); // "dark"
|
|
196
|
+
|
|
197
|
+
// Binary stays binary.
|
|
198
|
+
const png: Uint8Array = await storage.downloadBytes("images/logo.png");
|
|
157
199
|
```
|
|
158
200
|
|
|
159
201
|
### 3. List Files
|
|
160
202
|
|
|
161
|
-
```
|
|
162
|
-
//
|
|
163
|
-
const
|
|
164
|
-
console.log(files);
|
|
165
|
-
// Output: ['notes/memo.txt', 'data/file.bin', 'config/settings.json']
|
|
203
|
+
```ts
|
|
204
|
+
// Every key in the bucket, across as many pages as it takes.
|
|
205
|
+
const keys: string[] = await storage.list();
|
|
166
206
|
|
|
167
|
-
//
|
|
168
|
-
const notes =
|
|
169
|
-
console.log(notes); // ['notes/memo.txt']
|
|
170
|
-
```
|
|
207
|
+
// Keys are flat strings — "notes/" is a prefix, not a folder — so filter server-side.
|
|
208
|
+
const notes: string[] = await storage.list("notes/");
|
|
171
209
|
|
|
172
|
-
|
|
210
|
+
if (await storage.exists("notes/memo.txt")) {
|
|
211
|
+
// …
|
|
212
|
+
}
|
|
213
|
+
```
|
|
173
214
|
|
|
174
|
-
|
|
175
|
-
// Copy a file to a new location
|
|
176
|
-
await manageStorage("copy", {
|
|
177
|
-
key: "documents/report.pdf",
|
|
178
|
-
destinationKey: "documents/backup/report-2024.pdf",
|
|
179
|
-
});
|
|
215
|
+
### 4. Copy, Rename and Delete
|
|
180
216
|
|
|
181
|
-
|
|
182
|
-
await
|
|
183
|
-
key: "config/settings.json",
|
|
184
|
-
destinationKey: "config/settings.backup.json",
|
|
185
|
-
});
|
|
186
|
-
```
|
|
217
|
+
```ts
|
|
218
|
+
await storage.copy("documents/report.pdf", "documents/backup/report-2024.pdf");
|
|
187
219
|
|
|
188
|
-
|
|
220
|
+
// Rename is a copy followed by a delete, in that order: a failed copy leaves
|
|
221
|
+
// the original alone.
|
|
222
|
+
await storage.rename("temp/draft.md", "published/article.md");
|
|
189
223
|
|
|
190
|
-
|
|
191
|
-
// Rename a file (performs copy + delete)
|
|
192
|
-
await manageStorage("rename", {
|
|
193
|
-
key: "old-filename.txt",
|
|
194
|
-
destinationKey: "new-filename.txt",
|
|
195
|
-
});
|
|
224
|
+
await storage.delete("notes/memo.txt");
|
|
196
225
|
|
|
197
|
-
//
|
|
198
|
-
await
|
|
199
|
-
|
|
200
|
-
destinationKey: "published/article.md",
|
|
201
|
-
});
|
|
226
|
+
// Everything under a prefix. Irreversible.
|
|
227
|
+
const { count } = await storage.deleteAll("temp/");
|
|
228
|
+
console.log(`Deleted ${count} files`);
|
|
202
229
|
```
|
|
203
230
|
|
|
204
|
-
###
|
|
231
|
+
### 5. Two Providers at Once
|
|
205
232
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
233
|
+
Each manager holds its own bucket, so mirroring is two objects rather than a
|
|
234
|
+
flag on every call:
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
import { StorageManager } from "manage-storage";
|
|
211
238
|
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
|
|
239
|
+
const r2 = new StorageManager({ provider: "cloudflare" });
|
|
240
|
+
const b2 = new StorageManager({ provider: "backblaze" });
|
|
241
|
+
|
|
242
|
+
const key = "documents/report.pdf";
|
|
243
|
+
await b2.upload(key, await r2.downloadBytes(key));
|
|
215
244
|
```
|
|
216
245
|
|
|
217
|
-
###
|
|
246
|
+
### 6. Batch Operations
|
|
218
247
|
|
|
219
|
-
|
|
220
|
-
// Use Cloudflare R2 even if other providers are configured
|
|
221
|
-
await manageStorage("upload", {
|
|
222
|
-
key: "test.txt",
|
|
223
|
-
body: "Hello Cloudflare!",
|
|
224
|
-
provider: "cloudflare",
|
|
225
|
-
});
|
|
248
|
+
The methods are ordinary promises and nothing is rate-limited internally:
|
|
226
249
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
key: "
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
});
|
|
233
|
-
```
|
|
250
|
+
```ts
|
|
251
|
+
const files = [
|
|
252
|
+
{ key: "docs/file1.txt", content: "Content 1" },
|
|
253
|
+
{ key: "docs/file2.txt", content: "Content 2" },
|
|
254
|
+
];
|
|
234
255
|
|
|
235
|
-
|
|
256
|
+
await Promise.all(files.map((file) => storage.upload(file.key, file.content)));
|
|
236
257
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
key: "secure/data.json",
|
|
241
|
-
body: JSON.stringify({ secret: "value" }),
|
|
242
|
-
provider: "cloudflare",
|
|
243
|
-
BUCKET_NAME: "my-custom-bucket",
|
|
244
|
-
ACCESS_KEY_ID: "runtime-key-id",
|
|
245
|
-
SECRET_ACCESS_KEY: "runtime-secret",
|
|
246
|
-
BUCKET_URL: "https://custom-account.r2.cloudflarestorage.com",
|
|
247
|
-
});
|
|
258
|
+
const contents: string[] = await Promise.all(
|
|
259
|
+
files.map((file) => storage.download(file.key)),
|
|
260
|
+
);
|
|
248
261
|
```
|
|
249
262
|
|
|
250
263
|
## Advanced Examples
|
|
251
264
|
|
|
252
265
|
### Next.js API Route
|
|
253
266
|
|
|
254
|
-
```
|
|
255
|
-
// app/api/upload/route.
|
|
256
|
-
import {
|
|
267
|
+
```ts
|
|
268
|
+
// app/api/upload/route.ts
|
|
269
|
+
import { StorageManager } from "manage-storage";
|
|
257
270
|
|
|
258
|
-
|
|
259
|
-
const { fileName, fileContent } = await req.json();
|
|
271
|
+
const storage = new StorageManager();
|
|
260
272
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
273
|
+
export async function POST(req: Request): Promise<Response> {
|
|
274
|
+
const { fileName, fileContent } = (await req.json()) as {
|
|
275
|
+
fileName: string;
|
|
276
|
+
fileContent: string;
|
|
277
|
+
};
|
|
265
278
|
|
|
266
|
-
return Response.json(
|
|
279
|
+
return Response.json(await storage.upload(`uploads/${fileName}`, fileContent));
|
|
267
280
|
}
|
|
268
281
|
|
|
269
|
-
export async function GET(req) {
|
|
270
|
-
const
|
|
271
|
-
|
|
282
|
+
export async function GET(req: Request): Promise<Response> {
|
|
283
|
+
const fileName = new URL(req.url).searchParams.get("file");
|
|
284
|
+
if (!fileName) return new Response("file is required", { status: 400 });
|
|
272
285
|
|
|
273
|
-
const
|
|
274
|
-
key: `uploads/${fileName}`,
|
|
275
|
-
});
|
|
286
|
+
const bytes = await storage.downloadBytes(`uploads/${fileName}`);
|
|
276
287
|
|
|
277
|
-
return new Response(
|
|
288
|
+
return new Response(bytes, {
|
|
278
289
|
headers: {
|
|
279
290
|
"Content-Type": "application/octet-stream",
|
|
280
291
|
"Content-Disposition": `attachment; filename="${fileName}"`,
|
|
@@ -285,48 +296,33 @@ export async function GET(req) {
|
|
|
285
296
|
|
|
286
297
|
### Express.js Endpoint
|
|
287
298
|
|
|
288
|
-
```
|
|
289
|
-
import express from "express";
|
|
290
|
-
import {
|
|
299
|
+
```ts
|
|
300
|
+
import express, { type Request, type Response } from "express";
|
|
301
|
+
import { StorageManager } from "manage-storage";
|
|
291
302
|
|
|
292
303
|
const app = express();
|
|
304
|
+
const storage = new StorageManager();
|
|
293
305
|
app.use(express.json());
|
|
294
306
|
|
|
295
|
-
app.post("/api/files", async (req, res) => {
|
|
307
|
+
app.post("/api/files", async (req: Request, res: Response) => {
|
|
296
308
|
try {
|
|
297
|
-
const { key, content } = req.body;
|
|
298
|
-
|
|
299
|
-
res.json(result);
|
|
309
|
+
const { key, content } = req.body as { key: string; content: string };
|
|
310
|
+
res.json(await storage.upload(key, content));
|
|
300
311
|
} catch (error) {
|
|
301
|
-
res.status(500).json({ error: error.message });
|
|
312
|
+
res.status(500).json({ error: (error as Error).message });
|
|
302
313
|
}
|
|
303
314
|
});
|
|
304
315
|
|
|
305
|
-
app.get("/api/files", async (
|
|
306
|
-
|
|
307
|
-
const files = await manageStorage("list");
|
|
308
|
-
res.json({ files });
|
|
309
|
-
} catch (error) {
|
|
310
|
-
res.status(500).json({ error: error.message });
|
|
311
|
-
}
|
|
316
|
+
app.get("/api/files", async (_req: Request, res: Response) => {
|
|
317
|
+
res.json({ files: await storage.list() });
|
|
312
318
|
});
|
|
313
319
|
|
|
314
|
-
app.get("/api/files/:key", async (req, res) => {
|
|
315
|
-
|
|
316
|
-
const data = await manageStorage("download", { key: req.params.key });
|
|
317
|
-
res.send(data);
|
|
318
|
-
} catch (error) {
|
|
319
|
-
res.status(500).json({ error: error.message });
|
|
320
|
-
}
|
|
320
|
+
app.get("/api/files/:key", async (req: Request, res: Response) => {
|
|
321
|
+
res.send(await storage.download(req.params.key));
|
|
321
322
|
});
|
|
322
323
|
|
|
323
|
-
app.delete("/api/files/:key", async (req, res) => {
|
|
324
|
-
|
|
325
|
-
const result = await manageStorage("delete", { key: req.params.key });
|
|
326
|
-
res.json(result);
|
|
327
|
-
} catch (error) {
|
|
328
|
-
res.status(500).json({ error: error.message });
|
|
329
|
-
}
|
|
324
|
+
app.delete("/api/files/:key", async (req: Request, res: Response) => {
|
|
325
|
+
res.json(await storage.delete(req.params.key));
|
|
330
326
|
});
|
|
331
327
|
|
|
332
328
|
app.listen(3000, () => console.log("Server running on port 3000"));
|
|
@@ -334,27 +330,37 @@ app.listen(3000, () => console.log("Server running on port 3000"));
|
|
|
334
330
|
|
|
335
331
|
### Cloudflare Workers
|
|
336
332
|
|
|
337
|
-
|
|
338
|
-
|
|
333
|
+
A Worker has no `process.env`, so the bindings go to the constructor. Build the
|
|
334
|
+
manager per request — `env` is only available there:
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
import { StorageManager } from "manage-storage";
|
|
338
|
+
|
|
339
|
+
interface Env {
|
|
340
|
+
CLOUDFLARE_BUCKET_NAME: string;
|
|
341
|
+
CLOUDFLARE_ACCESS_KEY_ID: string;
|
|
342
|
+
CLOUDFLARE_SECRET_ACCESS_KEY: string;
|
|
343
|
+
CLOUDFLARE_BUCKET_URL: string;
|
|
344
|
+
}
|
|
339
345
|
|
|
340
346
|
export default {
|
|
341
|
-
async fetch(request, env) {
|
|
347
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
348
|
+
const storage = new StorageManager({
|
|
349
|
+
provider: "cloudflare",
|
|
350
|
+
bucket: env.CLOUDFLARE_BUCKET_NAME,
|
|
351
|
+
accessKeyId: env.CLOUDFLARE_ACCESS_KEY_ID,
|
|
352
|
+
secretAccessKey: env.CLOUDFLARE_SECRET_ACCESS_KEY,
|
|
353
|
+
endpoint: env.CLOUDFLARE_BUCKET_URL,
|
|
354
|
+
});
|
|
355
|
+
|
|
342
356
|
const url = new URL(request.url);
|
|
343
357
|
|
|
344
358
|
if (request.method === "POST" && url.pathname === "/upload") {
|
|
345
|
-
const { key, content } = await request.json()
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
provider: "cloudflare",
|
|
351
|
-
BUCKET_NAME: env.CLOUDFLARE_BUCKET_NAME,
|
|
352
|
-
ACCESS_KEY_ID: env.CLOUDFLARE_ACCESS_KEY_ID,
|
|
353
|
-
SECRET_ACCESS_KEY: env.CLOUDFLARE_SECRET_ACCESS_KEY,
|
|
354
|
-
BUCKET_URL: env.CLOUDFLARE_BUCKET_URL,
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
return Response.json(result);
|
|
359
|
+
const { key, content } = (await request.json()) as {
|
|
360
|
+
key: string;
|
|
361
|
+
content: string;
|
|
362
|
+
};
|
|
363
|
+
return Response.json(await storage.upload(key, content));
|
|
358
364
|
}
|
|
359
365
|
|
|
360
366
|
return new Response("Not found", { status: 404 });
|
|
@@ -362,91 +368,55 @@ export default {
|
|
|
362
368
|
};
|
|
363
369
|
```
|
|
364
370
|
|
|
365
|
-
### Batch Operations
|
|
366
|
-
|
|
367
|
-
```javascript
|
|
368
|
-
// Upload multiple files
|
|
369
|
-
const files = [
|
|
370
|
-
{ key: "docs/file1.txt", content: "Content 1" },
|
|
371
|
-
{ key: "docs/file2.txt", content: "Content 2" },
|
|
372
|
-
{ key: "docs/file3.txt", content: "Content 3" },
|
|
373
|
-
];
|
|
374
|
-
|
|
375
|
-
await Promise.all(
|
|
376
|
-
files.map((file) =>
|
|
377
|
-
manageStorage("upload", { key: file.key, body: file.content })
|
|
378
|
-
)
|
|
379
|
-
);
|
|
380
|
-
|
|
381
|
-
// Download multiple files
|
|
382
|
-
const keys = ["docs/file1.txt", "docs/file2.txt", "docs/file3.txt"];
|
|
383
|
-
const contents = await Promise.all(
|
|
384
|
-
keys.map((key) => manageStorage("download", { key }))
|
|
385
|
-
);
|
|
386
|
-
```
|
|
387
|
-
|
|
388
371
|
## Return Values
|
|
389
372
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
{
|
|
394
|
-
success: true,
|
|
395
|
-
key: 'path/to/file.txt',
|
|
396
|
-
// ... additional provider-specific metadata
|
|
397
|
-
}
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
### Download
|
|
373
|
+
```ts
|
|
374
|
+
// upload / delete
|
|
375
|
+
{ success: true, key: "path/to/file.txt" } // plus provider metadata (ETag, …)
|
|
401
376
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
"File contents here...";
|
|
405
|
-
```
|
|
377
|
+
// download
|
|
378
|
+
"File contents here..."
|
|
406
379
|
|
|
407
|
-
|
|
380
|
+
// list
|
|
381
|
+
["folder/file1.txt", "folder/file2.txt"]
|
|
408
382
|
|
|
409
|
-
|
|
410
|
-
{
|
|
411
|
-
success: true,
|
|
412
|
-
key: 'path/to/file.txt'
|
|
413
|
-
}
|
|
414
|
-
```
|
|
383
|
+
// deleteAll
|
|
384
|
+
{ success: true, count: 42 }
|
|
415
385
|
|
|
416
|
-
|
|
386
|
+
// copy
|
|
387
|
+
{ success: true, sourceKey: "a.pdf", destinationKey: "b.pdf" }
|
|
417
388
|
|
|
418
|
-
|
|
419
|
-
|
|
389
|
+
// rename
|
|
390
|
+
{ success: true, oldKey: "old.txt", newKey: "new.txt" }
|
|
420
391
|
```
|
|
421
392
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
{
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
success
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
```
|
|
393
|
+
## Migrating from `manageStorage()`
|
|
394
|
+
|
|
395
|
+
The action-string function still ships and still works, so nothing breaks on
|
|
396
|
+
upgrade — it is deprecated, and delegates to `StorageManager`. The one breaking
|
|
397
|
+
change is the **default export**, which is now the class: `import manageStorage
|
|
398
|
+
from "manage-storage"` becomes `import { manageStorage } from "manage-storage"`.
|
|
399
|
+
|
|
400
|
+
| Before | After |
|
|
401
|
+
| ------------------------------------------------------------ | ------------------------------------------- |
|
|
402
|
+
| `manageStorage("upload", { key, body })` | `storage.upload(key, body)` |
|
|
403
|
+
| `manageStorage("download", { key })` | `storage.download(key)` |
|
|
404
|
+
| `manageStorage("list")` | `storage.list()` |
|
|
405
|
+
| `manageStorage("copy", { key, destinationKey })` | `storage.copy(key, destinationKey)` |
|
|
406
|
+
| `manageStorage("rename", { key, destinationKey })` | `storage.rename(key, destinationKey)` |
|
|
407
|
+
| `manageStorage("delete", { key })` | `storage.delete(key)` |
|
|
408
|
+
| `manageStorage("deleteAll")` | `storage.deleteAll()` |
|
|
409
|
+
| `{ provider, BUCKET_NAME, ACCESS_KEY_ID, … }` on every call | `new StorageManager({ provider, bucket, accessKeyId, … })` once |
|
|
410
|
+
|
|
411
|
+
What the class fixes rather than renames:
|
|
412
|
+
|
|
413
|
+
- **Credentials are resolved once.** The old function rebuilt a client on every
|
|
414
|
+
call, which is why every call had to repeat the credentials.
|
|
415
|
+
- **`list()` and `deleteAll()` finish.** The old versions issued one request and
|
|
416
|
+
returned the first 1000 keys, which looked like success on a larger bucket.
|
|
417
|
+
- **Required arguments are in the signature.** `upload` cannot be called without
|
|
418
|
+
a body, and `copy` cannot be called without a destination, at compile time
|
|
419
|
+
rather than as a runtime throw.
|
|
450
420
|
|
|
451
421
|
## Why AWS SDK v3?
|
|
452
422
|
|