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 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
- Universal cloud storage manager supporting Amazon S3, Cloudflare R2, and Backblaze B2 with automatic provider detection. Built on the official AWS SDK v3 with optimized configuration for multi-cloud compatibility.
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-Cloud Support**: Works seamlessly with Amazon S3, Cloudflare R2, and Backblaze B2
12
- - **Auto-Detection**: Automatically detects the configured provider from environment variables
13
- - **Modern SDK**: Built on AWS SDK v3 with command pattern for optimal performance
14
- - **Simple API**: Single function interface for all storage operations
15
- - **No File System**: Returns data directly - perfect for serverless/edge environments
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
- ```javascript
31
- import { manageStorage } from "manage-storage";
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
- // List all files
45
- const files = await manageStorage("list");
59
+ // Credentials come from the environment; the provider is detected from them.
60
+ const storage = new StorageManager();
46
61
 
47
- // Copy a file
48
- await manageStorage("copy", {
49
- key: "documents/report.pdf",
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
- // Delete a file
60
- await manageStorage("delete", {
61
- key: "documents/report.pdf",
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 will automatically detect which provider to use.
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
- ## API Reference
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
- #### Parameters
111
+ ```ts
112
+ import { StorageManager } from "manage-storage";
104
113
 
105
- - **action** `string` - The operation to perform: `'upload'`, `'download'`, `'delete'`, `'list'`, `'deleteAll'`, `'copy'`, or `'rename'`
106
- - **options** `object` - Operation-specific options
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
- #### Options
123
+ ## API Reference
109
124
 
110
- | Option | Type | Required | Description |
111
- | ---------------- | ----------------------------------- | ----------------------------------- | ---------------------------------------------------- |
112
- | `key` | `string` | Yes (except for `list`/`deleteAll`) | The object key/path |
113
- | `destinationKey` | `string` | Yes (for `copy`/`rename`) | The destination key/path for copy/rename operations |
114
- | `body` | `string\|Buffer\|Stream` | Yes (for `upload`) | The file content to upload |
115
- | `provider` | `'amazon'\|'cloudflare'\|'backblaze'` | No | Force a specific provider (auto-detected if omitted) |
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
- ```javascript
122
- // Upload text content
123
- await manageStorage("upload", {
124
- key: "notes/memo.txt",
125
- body: "Hello, World!",
126
- });
163
+ ```ts
164
+ import { StorageManager } from "manage-storage";
165
+
166
+ const storage = new StorageManager();
127
167
 
128
- // Upload Buffer
129
- const buffer = Buffer.from("File contents");
130
- await manageStorage("upload", {
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
- // Upload JSON
136
- await manageStorage("upload", {
137
- key: "config/settings.json",
138
- body: JSON.stringify({ theme: "dark", lang: "en" }),
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
- ```javascript
145
- // Download and get the raw data
146
- const data = await manageStorage("download", {
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
- // Download JSON and parse
152
- const configData = await manageStorage("download", {
153
- key: "config/settings.json",
154
- });
155
- const config = JSON.parse(configData);
156
- console.log(config.theme); // "dark"
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
- ```javascript
162
- // List all files in the bucket
163
- const files = await manageStorage("list");
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
- // Filter by prefix (folder)
168
- const notes = files.filter((key) => key.startsWith("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
- ### 4. Copy Files
210
+ if (await storage.exists("notes/memo.txt")) {
211
+ // …
212
+ }
213
+ ```
173
214
 
174
- ```javascript
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
- // Create a backup
182
- await manageStorage("copy", {
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
- ### 5. Rename Files
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
- ```javascript
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
- // Move to a different folder
198
- await manageStorage("rename", {
199
- key: "temp/draft.md",
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
- ### 6. Delete Files
231
+ ### 5. Two Providers at Once
205
232
 
206
- ```javascript
207
- // Delete a single file
208
- await manageStorage("delete", {
209
- key: "notes/memo.txt",
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
- // Delete all files in the bucket (use with caution!)
213
- const result = await manageStorage("deleteAll");
214
- console.log(`Deleted ${result.count} files`);
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
- ### 7. Force a Specific Provider
246
+ ### 6. Batch Operations
218
247
 
219
- ```javascript
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
- // Use Backblaze B2 specifically
228
- await manageStorage("upload", {
229
- key: "test.txt",
230
- body: "Hello Backblaze!",
231
- provider: "backblaze",
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
- ### 8. Runtime Configuration (Override Environment Variables)
256
+ await Promise.all(files.map((file) => storage.upload(file.key, file.content)));
236
257
 
237
- ```javascript
238
- // Pass credentials at runtime instead of using env vars
239
- await manageStorage("upload", {
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
- ```javascript
255
- // app/api/upload/route.js
256
- import { manageStorage } from "manage-storage";
267
+ ```ts
268
+ // app/api/upload/route.ts
269
+ import { StorageManager } from "manage-storage";
257
270
 
258
- export async function POST(req) {
259
- const { fileName, fileContent } = await req.json();
271
+ const storage = new StorageManager();
260
272
 
261
- const result = await manageStorage("upload", {
262
- key: `uploads/${fileName}`,
263
- body: fileContent,
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(result);
279
+ return Response.json(await storage.upload(`uploads/${fileName}`, fileContent));
267
280
  }
268
281
 
269
- export async function GET(req) {
270
- const { searchParams } = new URL(req.url);
271
- const fileName = searchParams.get("file");
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 data = await manageStorage("download", {
274
- key: `uploads/${fileName}`,
275
- });
286
+ const bytes = await storage.downloadBytes(`uploads/${fileName}`);
276
287
 
277
- return new Response(data, {
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
- ```javascript
289
- import express from "express";
290
- import { manageStorage } from "manage-storage";
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
- const result = await manageStorage("upload", { key, body: content });
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 (req, res) => {
306
- try {
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
- try {
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
- try {
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
- ```javascript
338
- import { manageStorage } from "manage-storage";
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
- const result = await manageStorage("upload", {
348
- key,
349
- body: content,
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
- ### Upload
391
-
392
- ```javascript
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
- ```javascript
403
- // Returns the file content as a string
404
- "File contents here...";
405
- ```
377
+ // download
378
+ "File contents here..."
406
379
 
407
- ### Delete
380
+ // list
381
+ ["folder/file1.txt", "folder/file2.txt"]
408
382
 
409
- ```javascript
410
- {
411
- success: true,
412
- key: 'path/to/file.txt'
413
- }
414
- ```
383
+ // deleteAll
384
+ { success: true, count: 42 }
415
385
 
416
- ### List
386
+ // copy
387
+ { success: true, sourceKey: "a.pdf", destinationKey: "b.pdf" }
417
388
 
418
- ```javascript
419
- ["folder/file1.txt", "folder/file2.txt", "another/file3.json"];
389
+ // rename
390
+ { success: true, oldKey: "old.txt", newKey: "new.txt" }
420
391
  ```
421
392
 
422
- ### DeleteAll
423
-
424
- ```javascript
425
- {
426
- success: true,
427
- count: 42
428
- }
429
- ```
430
-
431
- ### Copy
432
-
433
- ```javascript
434
- {
435
- success: true,
436
- sourceKey: 'documents/report.pdf',
437
- destinationKey: 'documents/backup/report-2024.pdf'
438
- }
439
- ```
440
-
441
- ### Rename
442
-
443
- ```javascript
444
- {
445
- success: true,
446
- oldKey: 'old-filename.txt',
447
- newKey: 'new-filename.txt'
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