@ooneex/storage 0.0.1
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/LICENSE +21 -0
- package/README.md +591 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +13 -0
- package/dist/ooneex-storage-0.0.1.tgz +0 -0
- package/package.json +36 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Ooneex
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
# @ooneex/storage
|
|
2
|
+
|
|
3
|
+
A comprehensive TypeScript/JavaScript storage library providing unified interfaces for file storage operations. This package supports both local filesystem and Cloudflare R2 storage with a consistent API, making it easy to switch between storage backends or use multiple storage providers in your applications.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
✅ **Unified Storage Interface** - Single API for multiple storage providers
|
|
12
|
+
|
|
13
|
+
✅ **Local Filesystem Storage** - Fast local file operations with automatic directory management
|
|
14
|
+
|
|
15
|
+
✅ **Cloudflare R2 Support** - Full integration with Cloudflare's S3-compatible object storage
|
|
16
|
+
|
|
17
|
+
✅ **Type-Safe** - Full TypeScript support with proper type definitions
|
|
18
|
+
|
|
19
|
+
✅ **Bucket Management** - Create, clear, and manage storage buckets/directories
|
|
20
|
+
|
|
21
|
+
✅ **Multiple Data Formats** - Support for strings, JSON, ArrayBuffers, Blobs, and streams
|
|
22
|
+
|
|
23
|
+
✅ **File Operations** - Put, get, delete, exists, and list operations
|
|
24
|
+
|
|
25
|
+
✅ **Stream Support** - Efficient streaming for large files
|
|
26
|
+
|
|
27
|
+
✅ **Exception Handling** - Comprehensive error handling with custom exceptions
|
|
28
|
+
|
|
29
|
+
✅ **Environment Configuration** - Support for environment variables and constructor options
|
|
30
|
+
|
|
31
|
+
✅ **Zero External Dependencies** - Uses only Bun's built-in APIs
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
### Bun
|
|
36
|
+
```bash
|
|
37
|
+
bun add @ooneex/storage
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### pnpm
|
|
41
|
+
```bash
|
|
42
|
+
pnpm add @ooneex/storage
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Yarn
|
|
46
|
+
```bash
|
|
47
|
+
yarn add @ooneex/storage
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### npm
|
|
51
|
+
```bash
|
|
52
|
+
npm install @ooneex/storage
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
### Filesystem Storage
|
|
58
|
+
|
|
59
|
+
#### Basic Setup
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import { FilesystemStorage } from '@ooneex/storage';
|
|
63
|
+
|
|
64
|
+
// Using constructor options
|
|
65
|
+
const storage = new FilesystemStorage({
|
|
66
|
+
storagePath: './my-storage'
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Or using environment variable FILESYSTEM_STORAGE_PATH
|
|
70
|
+
const storage = new FilesystemStorage();
|
|
71
|
+
|
|
72
|
+
// Set bucket/directory
|
|
73
|
+
storage.setBucket('my-bucket');
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
#### File Operations
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import { FilesystemStorage } from '@ooneex/storage';
|
|
80
|
+
|
|
81
|
+
const storage = new FilesystemStorage({ storagePath: './storage' })
|
|
82
|
+
.setBucket('documents');
|
|
83
|
+
|
|
84
|
+
// Store string content
|
|
85
|
+
await storage.put('hello.txt', 'Hello, World!');
|
|
86
|
+
|
|
87
|
+
// Store JSON data
|
|
88
|
+
await storage.put('config.json', JSON.stringify({ theme: 'dark', version: '1.0' }));
|
|
89
|
+
|
|
90
|
+
// Store from local file
|
|
91
|
+
await storage.putFile('backup.zip', '/path/to/local/file.zip');
|
|
92
|
+
|
|
93
|
+
// Check if file exists
|
|
94
|
+
const exists = await storage.exists('hello.txt'); // true
|
|
95
|
+
|
|
96
|
+
// Retrieve as string
|
|
97
|
+
const content = await storage.getAsArrayBuffer('hello.txt');
|
|
98
|
+
const text = new TextDecoder().decode(content);
|
|
99
|
+
|
|
100
|
+
// Retrieve as JSON
|
|
101
|
+
const config = await storage.getAsJson<{ theme: string; version: string }>('config.json');
|
|
102
|
+
|
|
103
|
+
// Get as stream for large files
|
|
104
|
+
const stream = storage.getAsStream('backup.zip');
|
|
105
|
+
|
|
106
|
+
// List all files
|
|
107
|
+
const files = await storage.list(); // ['hello.txt', 'config.json', 'backup.zip']
|
|
108
|
+
|
|
109
|
+
// Delete file
|
|
110
|
+
await storage.delete('hello.txt');
|
|
111
|
+
|
|
112
|
+
// Clear entire bucket
|
|
113
|
+
await storage.clearBucket();
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Cloudflare R2 Storage
|
|
117
|
+
|
|
118
|
+
#### Basic Setup
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { CloudflareStorageAdapter } from '@ooneex/storage';
|
|
122
|
+
|
|
123
|
+
// Using constructor options
|
|
124
|
+
const storage = new CloudflareStorageAdapter({
|
|
125
|
+
accessKey: 'your-access-key',
|
|
126
|
+
secretKey: 'your-secret-key',
|
|
127
|
+
endpoint: 'https://your-account.r2.cloudflarestorage.com',
|
|
128
|
+
region: 'EEUR' // EEUR, WEUR, APAC, NAM
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Or using environment variables:
|
|
132
|
+
// STORAGE_CLOUDFLARE_ACCESS_KEY, STORAGE_CLOUDFLARE_SECRET_KEY, STORAGE_CLOUDFLARE_ENDPOINT, STORAGE_CLOUDFLARE_REGION
|
|
133
|
+
const storage = new CloudflareStorageAdapter();
|
|
134
|
+
|
|
135
|
+
storage.setBucket('my-bucket');
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
#### Advanced Usage
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
import { CloudflareStorageAdapter } from '@ooneex/storage';
|
|
142
|
+
|
|
143
|
+
const storage = new CloudflareStorageAdapter({
|
|
144
|
+
accessKey: process.env.STORAGE_CLOUDFLARE_ACCESS_KEY!,
|
|
145
|
+
secretKey: process.env.STORAGE_CLOUDFLARE_SECRET_KEY!,
|
|
146
|
+
endpoint: process.env.STORAGE_CLOUDFLARE_ENDPOINT!,
|
|
147
|
+
region: 'EEUR'
|
|
148
|
+
}).setBucket('media-files');
|
|
149
|
+
|
|
150
|
+
// Store different types of content
|
|
151
|
+
await storage.put('image.png', new Blob([imageData], { type: 'image/png' }));
|
|
152
|
+
await storage.put('data.json', JSON.stringify({ users: [], posts: [] }));
|
|
153
|
+
await storage.put('binary-data', new ArrayBuffer(1024));
|
|
154
|
+
|
|
155
|
+
// Stream large files
|
|
156
|
+
const largeFileStream = storage.getAsStream('large-video.mp4');
|
|
157
|
+
const response = new Response(largeFileStream);
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Working with Different Data Types
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
import { FilesystemStorage } from '@ooneex/storage';
|
|
164
|
+
|
|
165
|
+
const storage = new FilesystemStorage({ storagePath: './data' })
|
|
166
|
+
.setBucket('examples');
|
|
167
|
+
|
|
168
|
+
// String content
|
|
169
|
+
await storage.put('text.txt', 'Plain text content');
|
|
170
|
+
|
|
171
|
+
// JSON objects
|
|
172
|
+
const userData = { name: 'John', age: 30, active: true };
|
|
173
|
+
await storage.put('user.json', JSON.stringify(userData));
|
|
174
|
+
|
|
175
|
+
// Binary data (ArrayBuffer)
|
|
176
|
+
const binaryData = new ArrayBuffer(256);
|
|
177
|
+
await storage.put('binary.dat', binaryData);
|
|
178
|
+
|
|
179
|
+
// Blobs
|
|
180
|
+
const blob = new Blob(['CSV data,here'], { type: 'text/csv' });
|
|
181
|
+
await storage.put('data.csv', blob);
|
|
182
|
+
|
|
183
|
+
// Files from disk
|
|
184
|
+
await storage.putFile('document.pdf', './local/document.pdf');
|
|
185
|
+
|
|
186
|
+
// Retrieve data in different formats
|
|
187
|
+
const textBuffer = await storage.getAsArrayBuffer('text.txt');
|
|
188
|
+
const text = new TextDecoder().decode(textBuffer);
|
|
189
|
+
|
|
190
|
+
const user = await storage.getAsJson<{ name: string; age: number; active: boolean }>('user.json');
|
|
191
|
+
|
|
192
|
+
const csvStream = storage.getAsStream('data.csv');
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Error Handling
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
import { FilesystemStorage, StorageException } from '@ooneex/storage';
|
|
199
|
+
|
|
200
|
+
const storage = new FilesystemStorage({ storagePath: './storage' });
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
await storage.getAsJson('nonexistent.json');
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (error instanceof StorageException) {
|
|
206
|
+
console.error('Storage error:', error.message);
|
|
207
|
+
console.error('Status:', error.getStatus()); // HTTP status code
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Using Abstract Interface
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
import { IStorage, FilesystemStorage, CloudflareStorageAdapter } from '@ooneex/storage';
|
|
216
|
+
|
|
217
|
+
function createStorage(type: 'filesystem' | 'cloudflare'): IStorage {
|
|
218
|
+
switch (type) {
|
|
219
|
+
case 'filesystem':
|
|
220
|
+
return new FilesystemStorage({ storagePath: './storage' });
|
|
221
|
+
case 'cloudflare':
|
|
222
|
+
return new CloudflareStorageAdapter();
|
|
223
|
+
default:
|
|
224
|
+
throw new Error('Unknown storage type');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Use the same interface regardless of storage type
|
|
229
|
+
const storage = createStorage('filesystem');
|
|
230
|
+
await storage.setBucket('shared-bucket');
|
|
231
|
+
await storage.put('shared-file.txt', 'This works with any storage provider');
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## API Reference
|
|
235
|
+
|
|
236
|
+
### `IStorage` Interface
|
|
237
|
+
|
|
238
|
+
The main interface that all storage adapters implement.
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
interface IStorage {
|
|
242
|
+
setBucket(name: string): this;
|
|
243
|
+
list(): Promise<string[]>;
|
|
244
|
+
clearBucket(): Promise<this>;
|
|
245
|
+
exists(key: string): Promise<boolean>;
|
|
246
|
+
delete(key: string): Promise<void>;
|
|
247
|
+
putFile(key: string, localPath: string): Promise<number>;
|
|
248
|
+
put(key: string, content: string | ArrayBufferView | ArrayBuffer | SharedArrayBuffer | Request | Response | BunFile | S3File | Blob): Promise<number>;
|
|
249
|
+
getAsJson<T = unknown>(key: string): Promise<T>;
|
|
250
|
+
getAsArrayBuffer(key: string): Promise<ArrayBuffer>;
|
|
251
|
+
getAsStream(key: string): ReadableStream;
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### `FilesystemStorage` Class
|
|
256
|
+
|
|
257
|
+
Local filesystem storage implementation.
|
|
258
|
+
|
|
259
|
+
#### Constructor
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
constructor(options?: {
|
|
263
|
+
storagePath?: string;
|
|
264
|
+
})
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Parameters:**
|
|
268
|
+
- `options.storagePath` - Base path for storage (optional, can use `FILESYSTEM_STORAGE_PATH` env var)
|
|
269
|
+
|
|
270
|
+
**Example:**
|
|
271
|
+
```typescript
|
|
272
|
+
const storage = new FilesystemStorage({
|
|
273
|
+
storagePath: '/var/app/storage'
|
|
274
|
+
});
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
#### Methods
|
|
278
|
+
|
|
279
|
+
##### `setBucket(name: string): this`
|
|
280
|
+
Sets the bucket (directory) name for operations.
|
|
281
|
+
|
|
282
|
+
**Parameters:**
|
|
283
|
+
- `name` - Bucket/directory name
|
|
284
|
+
|
|
285
|
+
**Returns:** `this` for method chaining
|
|
286
|
+
|
|
287
|
+
**Example:**
|
|
288
|
+
```typescript
|
|
289
|
+
storage.setBucket('uploads');
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
##### `put(key: string, content: ContentType): Promise<number>`
|
|
293
|
+
Stores content at the specified key.
|
|
294
|
+
|
|
295
|
+
**Parameters:**
|
|
296
|
+
- `key` - File key/path within the bucket
|
|
297
|
+
- `content` - Content to store (string, ArrayBuffer, Blob, etc.)
|
|
298
|
+
|
|
299
|
+
**Returns:** Number of bytes written
|
|
300
|
+
|
|
301
|
+
**Example:**
|
|
302
|
+
```typescript
|
|
303
|
+
const bytesWritten = await storage.put('file.txt', 'Hello World');
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
##### `putFile(key: string, localPath: string): Promise<number>`
|
|
307
|
+
Stores a local file at the specified key.
|
|
308
|
+
|
|
309
|
+
**Parameters:**
|
|
310
|
+
- `key` - Destination key/path within the bucket
|
|
311
|
+
- `localPath` - Local file system path
|
|
312
|
+
|
|
313
|
+
**Returns:** Number of bytes written
|
|
314
|
+
|
|
315
|
+
**Example:**
|
|
316
|
+
```typescript
|
|
317
|
+
await storage.putFile('backup.zip', './local-backup.zip');
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
##### `get*` Methods
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
// Get as JSON with type safety
|
|
324
|
+
getAsJson<T = unknown>(key: string): Promise<T>
|
|
325
|
+
|
|
326
|
+
// Get as ArrayBuffer
|
|
327
|
+
getAsArrayBuffer(key: string): Promise<ArrayBuffer>
|
|
328
|
+
|
|
329
|
+
// Get as ReadableStream
|
|
330
|
+
getAsStream(key: string): ReadableStream
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
##### `exists(key: string): Promise<boolean>`
|
|
334
|
+
Checks if a file exists at the specified key.
|
|
335
|
+
|
|
336
|
+
**Example:**
|
|
337
|
+
```typescript
|
|
338
|
+
const fileExists = await storage.exists('important.txt');
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
##### `delete(key: string): Promise<void>`
|
|
342
|
+
Deletes the file at the specified key.
|
|
343
|
+
|
|
344
|
+
**Example:**
|
|
345
|
+
```typescript
|
|
346
|
+
await storage.delete('old-file.txt');
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
##### `list(): Promise<string[]>`
|
|
350
|
+
Lists all files in the current bucket.
|
|
351
|
+
|
|
352
|
+
**Returns:** Array of file keys
|
|
353
|
+
|
|
354
|
+
**Example:**
|
|
355
|
+
```typescript
|
|
356
|
+
const files = await storage.list();
|
|
357
|
+
console.log('Files:', files); // ['file1.txt', 'folder/file2.json']
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
##### `clearBucket(): Promise<this>`
|
|
361
|
+
Removes all files from the current bucket.
|
|
362
|
+
|
|
363
|
+
**Returns:** `this` for method chaining
|
|
364
|
+
|
|
365
|
+
**Example:**
|
|
366
|
+
```typescript
|
|
367
|
+
await storage.clearBucket();
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### `CloudflareStorageAdapter` Class
|
|
371
|
+
|
|
372
|
+
Cloudflare R2 storage implementation.
|
|
373
|
+
|
|
374
|
+
#### Constructor
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
constructor(options?: {
|
|
378
|
+
accessKey?: string;
|
|
379
|
+
secretKey?: string;
|
|
380
|
+
endpoint?: string;
|
|
381
|
+
region?: "EEUR" | "WEUR" | "APAC" | "NAM";
|
|
382
|
+
})
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
**Parameters:**
|
|
386
|
+
- `options.accessKey` - Cloudflare R2 access key (or use `STORAGE_CLOUDFLARE_ACCESS_KEY` env var)
|
|
387
|
+
- `options.secretKey` - Cloudflare R2 secret key (or use `STORAGE_CLOUDFLARE_SECRET_KEY` env var)
|
|
388
|
+
- `options.endpoint` - Cloudflare R2 endpoint URL (or use `STORAGE_CLOUDFLARE_ENDPOINT` env var)
|
|
389
|
+
- `options.region` - Cloudflare R2 region (or use `STORAGE_CLOUDFLARE_REGION` env var)
|
|
390
|
+
|
|
391
|
+
**Example:**
|
|
392
|
+
```typescript
|
|
393
|
+
const storage = new CloudflareStorageAdapter({
|
|
394
|
+
accessKey: 'your-access-key',
|
|
395
|
+
secretKey: 'your-secret-key',
|
|
396
|
+
endpoint: 'https://account-id.r2.cloudflarestorage.com',
|
|
397
|
+
region: 'EEUR'
|
|
398
|
+
});
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
The `CloudflareStorageAdapter` inherits all methods from `AbstractStorage` and implements the same interface as `FilesystemStorage`.
|
|
402
|
+
|
|
403
|
+
### `StorageException` Class
|
|
404
|
+
|
|
405
|
+
Custom exception class for storage-related errors.
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
class StorageException extends Exception {
|
|
409
|
+
constructor(message: string, data?: T)
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
**Example:**
|
|
414
|
+
```typescript
|
|
415
|
+
try {
|
|
416
|
+
await storage.getAsJson('missing.json');
|
|
417
|
+
} catch (error) {
|
|
418
|
+
if (error instanceof StorageException) {
|
|
419
|
+
console.error('Storage operation failed:', error.message);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### `AbstractStorage` Class
|
|
425
|
+
|
|
426
|
+
Base class providing common functionality for storage adapters.
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
abstract class AbstractStorage implements IStorage {
|
|
430
|
+
protected abstract bucket: string;
|
|
431
|
+
public abstract getOptions(): S3Options;
|
|
432
|
+
|
|
433
|
+
// All IStorage methods implemented
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
## Environment Variables
|
|
438
|
+
|
|
439
|
+
### Filesystem Storage
|
|
440
|
+
|
|
441
|
+
- `FILESYSTEM_STORAGE_PATH` - Base path for filesystem storage
|
|
442
|
+
|
|
443
|
+
### Cloudflare R2 Storage
|
|
444
|
+
|
|
445
|
+
- `STORAGE_CLOUDFLARE_ACCESS_KEY` - R2 access key
|
|
446
|
+
- `STORAGE_CLOUDFLARE_SECRET_KEY` - R2 secret key
|
|
447
|
+
- `STORAGE_CLOUDFLARE_ENDPOINT` - R2 endpoint URL
|
|
448
|
+
- `STORAGE_CLOUDFLARE_REGION` - R2 region (EEUR, WEUR, APAC, NAM)
|
|
449
|
+
|
|
450
|
+
## Error Handling
|
|
451
|
+
|
|
452
|
+
The library uses custom `StorageException` for all storage-related errors:
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
import { StorageException } from '@ooneex/storage';
|
|
456
|
+
|
|
457
|
+
try {
|
|
458
|
+
const storage = new FilesystemStorage(); // No path provided
|
|
459
|
+
} catch (error) {
|
|
460
|
+
if (error instanceof StorageException) {
|
|
461
|
+
// Handle storage-specific errors
|
|
462
|
+
console.error('Storage setup failed:', error.message);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
Common error scenarios:
|
|
468
|
+
- Missing configuration (paths, credentials)
|
|
469
|
+
- File not found operations
|
|
470
|
+
- Permission errors
|
|
471
|
+
- Network errors (for cloud storage)
|
|
472
|
+
- Invalid JSON parsing
|
|
473
|
+
|
|
474
|
+
## Best Practices
|
|
475
|
+
|
|
476
|
+
### 1. Use Environment Variables for Configuration
|
|
477
|
+
|
|
478
|
+
```typescript
|
|
479
|
+
// .env file
|
|
480
|
+
FILESYSTEM_STORAGE_PATH=./storage
|
|
481
|
+
STORAGE_CLOUDFLARE_ACCESS_KEY=your-access-key
|
|
482
|
+
STORAGE_CLOUDFLARE_SECRET_KEY=your-secret-key
|
|
483
|
+
STORAGE_CLOUDFLARE_ENDPOINT=https://your-account.r2.cloudflarestorage.com
|
|
484
|
+
STORAGE_CLOUDFLARE_REGION=EEUR
|
|
485
|
+
|
|
486
|
+
// Application code
|
|
487
|
+
const storage = new FilesystemStorage(); // Uses env vars
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### 2. Implement Proper Error Handling
|
|
491
|
+
|
|
492
|
+
```typescript
|
|
493
|
+
async function safeStorageOperation() {
|
|
494
|
+
try {
|
|
495
|
+
const result = await storage.getAsJson('config.json');
|
|
496
|
+
return result;
|
|
497
|
+
} catch (error) {
|
|
498
|
+
if (error instanceof StorageException) {
|
|
499
|
+
// Log error and provide fallback
|
|
500
|
+
console.warn('Failed to load config:', error.message);
|
|
501
|
+
return getDefaultConfig();
|
|
502
|
+
}
|
|
503
|
+
throw error; // Re-throw unexpected errors
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### 3. Use Streams for Large Files
|
|
509
|
+
|
|
510
|
+
```typescript
|
|
511
|
+
// Good for large files
|
|
512
|
+
const stream = storage.getAsStream('large-video.mp4');
|
|
513
|
+
const response = new Response(stream, {
|
|
514
|
+
headers: { 'Content-Type': 'video/mp4' }
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// Avoid for large files - loads entire file into memory
|
|
518
|
+
const buffer = await storage.getAsArrayBuffer('large-video.mp4');
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### 4. Organize Files with Proper Keys
|
|
522
|
+
|
|
523
|
+
```typescript
|
|
524
|
+
// Good - organized structure
|
|
525
|
+
await storage.put('users/profile/123.json', userData);
|
|
526
|
+
await storage.put('uploads/images/2024/01/photo.jpg', imageData);
|
|
527
|
+
await storage.put('logs/2024-01-15.log', logData);
|
|
528
|
+
|
|
529
|
+
// Less organized
|
|
530
|
+
await storage.put('user123.json', userData);
|
|
531
|
+
await storage.put('photo.jpg', imageData);
|
|
532
|
+
await storage.put('log.txt', logData);
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### 5. Use Type Safety with JSON Operations
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
interface UserProfile {
|
|
539
|
+
id: number;
|
|
540
|
+
name: string;
|
|
541
|
+
email: string;
|
|
542
|
+
preferences: {
|
|
543
|
+
theme: 'light' | 'dark';
|
|
544
|
+
notifications: boolean;
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Type-safe JSON operations
|
|
549
|
+
const profile = await storage.getAsJson<UserProfile>('user/profile.json');
|
|
550
|
+
console.log(profile.preferences.theme); // TypeScript knows this exists
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
## License
|
|
554
|
+
|
|
555
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
556
|
+
|
|
557
|
+
## Contributing
|
|
558
|
+
|
|
559
|
+
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
|
|
560
|
+
|
|
561
|
+
### Development Setup
|
|
562
|
+
|
|
563
|
+
1. Clone the repository
|
|
564
|
+
2. Install dependencies: `bun install`
|
|
565
|
+
3. Run tests: `bun run test`
|
|
566
|
+
4. Build the project: `bun run build`
|
|
567
|
+
|
|
568
|
+
### Guidelines
|
|
569
|
+
|
|
570
|
+
- Write tests for new features
|
|
571
|
+
- Follow the existing code style
|
|
572
|
+
- Update documentation for API changes
|
|
573
|
+
- Ensure all tests pass before submitting PR
|
|
574
|
+
- Add TypeScript type definitions for new APIs
|
|
575
|
+
|
|
576
|
+
### Running Tests
|
|
577
|
+
|
|
578
|
+
```bash
|
|
579
|
+
# Run all tests
|
|
580
|
+
bun run test
|
|
581
|
+
|
|
582
|
+
# Run tests in watch mode
|
|
583
|
+
bun run test:watch
|
|
584
|
+
|
|
585
|
+
# Run specific test file
|
|
586
|
+
bun test tests/FilesystemStorage.spec.ts
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
---
|
|
590
|
+
|
|
591
|
+
Made with ❤️ by the Ooneex team
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { BunFile as BunFile2, S3File as S3File2, S3Options } from "bun";
|
|
2
|
+
import { BunFile, S3File } from "bun";
|
|
3
|
+
type StorageClassType = new (...args: any[]) => IStorage;
|
|
4
|
+
interface IStorage {
|
|
5
|
+
setBucket(name: string): IStorage;
|
|
6
|
+
list(): Promise<string[]>;
|
|
7
|
+
clearBucket(): Promise<this>;
|
|
8
|
+
exists(key: string): Promise<boolean>;
|
|
9
|
+
delete(key: string): Promise<void>;
|
|
10
|
+
putFile(key: string, localPath: string): Promise<number>;
|
|
11
|
+
put(key: string, content: string | ArrayBufferView | ArrayBuffer | SharedArrayBuffer | Request | Response | BunFile | S3File | Blob): Promise<number>;
|
|
12
|
+
getAsJson<T = unknown>(key: string): Promise<T>;
|
|
13
|
+
getAsArrayBuffer(key: string): Promise<ArrayBuffer>;
|
|
14
|
+
getAsStream(key: string): ReadableStream;
|
|
15
|
+
}
|
|
16
|
+
declare abstract class AbstractStorage implements IStorage {
|
|
17
|
+
protected client: Bun.S3Client | null;
|
|
18
|
+
abstract getOptions(): S3Options;
|
|
19
|
+
protected abstract bucket: string;
|
|
20
|
+
setBucket(name: string): this;
|
|
21
|
+
list(): Promise<string[]>;
|
|
22
|
+
clearBucket(): Promise<this>;
|
|
23
|
+
exists(key: string): Promise<boolean>;
|
|
24
|
+
delete(key: string): Promise<void>;
|
|
25
|
+
putFile(key: string, localPath: string): Promise<number>;
|
|
26
|
+
put(key: string, content: string | ArrayBuffer | SharedArrayBuffer | Request | Response | BunFile2 | S3File2 | Blob): Promise<number>;
|
|
27
|
+
getAsJson<T>(key: string): Promise<T>;
|
|
28
|
+
getAsArrayBuffer(key: string): Promise<ArrayBuffer>;
|
|
29
|
+
getAsStream(key: string): ReadableStream;
|
|
30
|
+
protected getClient(): Bun.S3Client;
|
|
31
|
+
protected getS3File(path: string): S3File2;
|
|
32
|
+
}
|
|
33
|
+
import { S3Options as S3Options2 } from "bun";
|
|
34
|
+
declare class CloudflareStorageAdapter extends AbstractStorage {
|
|
35
|
+
protected bucket: string;
|
|
36
|
+
private readonly accessKey;
|
|
37
|
+
private readonly secretKey;
|
|
38
|
+
private readonly endpoint;
|
|
39
|
+
private readonly region;
|
|
40
|
+
constructor(options?: {
|
|
41
|
+
accessKey?: string;
|
|
42
|
+
secretKey?: string;
|
|
43
|
+
endpoint?: string;
|
|
44
|
+
region?: "EEUR" | "WEUR" | "APAC" | "NAM";
|
|
45
|
+
});
|
|
46
|
+
getOptions(): S3Options2;
|
|
47
|
+
}
|
|
48
|
+
import { BunFile as BunFile3, S3File as S3File3, S3Options as S3Options3 } from "bun";
|
|
49
|
+
declare class FilesystemStorage extends AbstractStorage {
|
|
50
|
+
protected bucket: string;
|
|
51
|
+
private readonly storagePath;
|
|
52
|
+
constructor(options?: {
|
|
53
|
+
storagePath?: string;
|
|
54
|
+
});
|
|
55
|
+
getOptions(): S3Options3;
|
|
56
|
+
private getBucketPath;
|
|
57
|
+
private getFilePath;
|
|
58
|
+
setBucket(name: string): this;
|
|
59
|
+
list(): Promise<string[]>;
|
|
60
|
+
private listFilesRecursive;
|
|
61
|
+
clearBucket(): Promise<this>;
|
|
62
|
+
private removeDirectoryRecursive;
|
|
63
|
+
exists(key: string): Promise<boolean>;
|
|
64
|
+
delete(key: string): Promise<void>;
|
|
65
|
+
putFile(key: string, localPath: string): Promise<number>;
|
|
66
|
+
put(key: string, content: string | ArrayBuffer | SharedArrayBuffer | Request | Response | BunFile3 | S3File3 | Blob): Promise<number>;
|
|
67
|
+
getAsJson<T>(key: string): Promise<T>;
|
|
68
|
+
getAsArrayBuffer(key: string): Promise<ArrayBuffer>;
|
|
69
|
+
getAsStream(key: string): ReadableStream;
|
|
70
|
+
}
|
|
71
|
+
import { Exception } from "@ooneex/exception";
|
|
72
|
+
declare class StorageException extends Exception {
|
|
73
|
+
constructor(message: string, data?: Record<string, unknown>);
|
|
74
|
+
}
|
|
75
|
+
export { StorageException, StorageClassType, IStorage, FilesystemStorage, CloudflareStorageAdapter, AbstractStorage };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
class c{client=null;setBucket(t){return this.bucket=t,this.client=new Bun.S3Client(this.getOptions()),this}async list(){return(await this.getClient().list()).contents?.map((e)=>e.key)||[]}async clearBucket(){let t=this.getClient(),e=await this.list();for(let r of e)await t.delete(r);return this}async exists(t){return await this.getClient().exists(t)}async delete(t){await this.getClient().delete(t)}async putFile(t,e){let r=Bun.file(e);return await this.put(t,r)}async put(t,e){return await this.getS3File(t).write(e)}async getAsJson(t){return await this.getS3File(t).json()}async getAsArrayBuffer(t){return await this.getS3File(t).arrayBuffer()}getAsStream(t){return this.getS3File(t).stream()}getClient(){if(!this.client)this.client=new Bun.S3Client(this.getOptions());return this.client}getS3File(t){return this.getClient().file(t)}}import{Exception as d}from"@ooneex/exception";import{HttpStatus as m}from"@ooneex/http-status";class n extends d{constructor(t,e={}){super(t,{status:m.Code.InternalServerError,data:e});this.name="StorageException"}}class S extends c{bucket;accessKey;secretKey;endpoint;region;constructor(t){super();let e=t?.accessKey||Bun.env.STORAGE_CLOUDFLARE_ACCESS_KEY,r=t?.secretKey||Bun.env.STORAGE_CLOUDFLARE_SECRET_KEY,i=t?.endpoint||Bun.env.STORAGE_CLOUDFLARE_ENDPOINT;if(!e)throw new n("Cloudflare access key is required. Please provide an access key either through the constructor options or set the STORAGE_CLOUDFLARE_ACCESS_KEY environment variable.");if(!r)throw new n("Cloudflare secret key is required. Please provide a secret key either through the constructor options or set the STORAGE_CLOUDFLARE_SECRET_KEY environment variable.");if(!i)throw new n("Cloudflare endpoint is required. Please provide an endpoint either through the constructor options or set the STORAGE_CLOUDFLARE_ENDPOINT environment variable.");this.accessKey=e,this.secretKey=r,this.endpoint=i,this.region=t?.region||Bun.env.STORAGE_CLOUDFLARE_REGION||"EEUR"}getOptions(){return{accessKeyId:this.accessKey,secretAccessKey:this.secretKey,endpoint:this.endpoint,bucket:this.bucket,region:this.region}}}import{existsSync as o,mkdirSync as g}from"fs";import{mkdir as p,readdir as f,rmdir as y,stat as w}from"fs/promises";import{dirname as h,join as l}from"path";class b extends c{bucket;storagePath;constructor(t){super();let e=t?.storagePath||Bun.env.FILESYSTEM_STORAGE_PATH;if(!e)throw new n("Base path is required. Please provide a base path either through the constructor options or set the FILESYSTEM_STORAGE_PATH environment variable.");this.storagePath=e;try{if(!o(e))g(e,{recursive:!0})}catch(r){throw new n(`Failed to create base storage directory at ${e}: ${r instanceof Error?r.message:String(r)}`)}}getOptions(){return{accessKeyId:"filesystem",secretAccessKey:"filesystem",endpoint:this.storagePath,bucket:this.bucket,region:"local"}}getBucketPath(){if(!this.bucket)throw new n("Bucket name is required. Please call setBucket() first.");return l(this.storagePath,this.bucket)}getFilePath(t){return l(this.getBucketPath(),t)}setBucket(t){this.bucket=t;let e=this.getBucketPath();try{if(!o(e))g(e,{recursive:!0})}catch(r){throw new n(`Failed to create bucket directory at ${e}: ${r instanceof Error?r.message:String(r)}`)}return this}async list(){let t=this.getBucketPath();if(!o(t))return[];try{return await this.listFilesRecursive(t,t)}catch(e){throw new n(`Failed to list files in bucket: ${e instanceof Error?e.message:String(e)}`)}}async listFilesRecursive(t,e){let r=[],i=await f(t);for(let s of i){let a=l(t,s);if((await w(a)).isDirectory()){let u=await this.listFilesRecursive(a,e);r.push(...u)}else{let u=a.substring(e.length+1);r.push(u)}}return r}async clearBucket(){let t=this.getBucketPath();if(!o(t))return this;try{await this.removeDirectoryRecursive(t),await p(t,{recursive:!0})}catch(e){throw new n(`Failed to clear bucket: ${e instanceof Error?e.message:String(e)}`)}return this}async removeDirectoryRecursive(t){let e=await f(t);for(let r of e){let i=l(t,r);if((await w(i)).isDirectory())await this.removeDirectoryRecursive(i),await y(i);else await Bun.file(i).delete()}}async exists(t){let e=this.getFilePath(t);return await Bun.file(e).exists()}async delete(t){let e=this.getFilePath(t),r=Bun.file(e);if(!await r.exists())return;try{await r.delete();let i=h(e),s=this.getBucketPath();while(i!==s&&i!==this.storagePath)try{if((await f(i)).length===0)await y(i),i=h(i);else break}catch{break}}catch(i){throw new n(`Failed to delete file ${t}: ${i instanceof Error?i.message:String(i)}`)}}async putFile(t,e){let r=Bun.file(e);return await this.put(t,r)}async put(t,e){let r=this.getFilePath(t),i=h(r);try{if(!o(i))await p(i,{recursive:!0})}catch(s){throw new n(`Failed to create directory ${i}: ${s instanceof Error?s.message:String(s)}`)}try{let s;if(typeof e==="string")s=await Bun.write(r,e);else if(e instanceof ArrayBuffer)s=await Bun.write(r,e);else if(e instanceof SharedArrayBuffer){let a=new ArrayBuffer(e.byteLength);new Uint8Array(a).set(new Uint8Array(e)),s=await Bun.write(r,a)}else if(e instanceof Request){let a=await e.arrayBuffer();s=await Bun.write(r,a)}else if(e instanceof Response){let a=await e.arrayBuffer();s=await Bun.write(r,a)}else if(e instanceof Blob)s=await Bun.write(r,e);else{let a=await e.arrayBuffer();s=await Bun.write(r,a)}return s}catch(s){throw new n(`Failed to write file ${t}: ${s instanceof Error?s.message:String(s)}`)}}async getAsJson(t){let e=this.getFilePath(t),r=Bun.file(e);if(!await r.exists())throw new n(`File ${t} does not exist`);try{return await r.json()}catch(i){throw new n(`Failed to read file ${t} as JSON: ${i instanceof Error?i.message:String(i)}`)}}async getAsArrayBuffer(t){let e=this.getFilePath(t),r=Bun.file(e);if(!await r.exists())throw new n(`File ${t} does not exist`);try{return await r.arrayBuffer()}catch(i){throw new n(`Failed to read file ${t} as ArrayBuffer: ${i instanceof Error?i.message:String(i)}`)}}getAsStream(t){let e=this.getFilePath(t);if(!o(e))throw new n(`File ${t} does not exist`);let r=Bun.file(e);try{return r.stream()}catch(i){throw new n(`Failed to read file ${t} as stream: ${i instanceof Error?i.message:String(i)}`)}}}export{n as StorageException,b as FilesystemStorage,S as CloudflareStorageAdapter,c as AbstractStorage};
|
|
3
|
+
|
|
4
|
+
//# debugId=C81D99E08CC7CE6B64756E2164756E21
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["src/AbstractStorage.ts", "src/StorageException.ts", "src/CloudflareStorageAdapter.ts", "src/FilesystemStorage.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import type { BunFile, S3File, S3Options } from \"bun\";\nimport type { IStorage } from \"./types\";\n\nexport abstract class AbstractStorage implements IStorage {\n protected client: Bun.S3Client | null = null;\n public abstract getOptions(): S3Options;\n protected abstract bucket: string;\n\n public setBucket(name: string): this {\n this.bucket = name;\n this.client = new Bun.S3Client(this.getOptions());\n\n return this;\n }\n\n public async list(): Promise<string[]> {\n const client = this.getClient();\n\n return (await client.list()).contents?.map((content) => content.key) || [];\n }\n\n public async clearBucket(): Promise<this> {\n const client = this.getClient();\n const keys = await this.list();\n\n for (const key of keys) {\n await client.delete(key);\n }\n\n return this;\n }\n\n public async exists(key: string): Promise<boolean> {\n const client = this.getClient();\n\n return await client.exists(key);\n }\n\n public async delete(key: string): Promise<void> {\n const client = this.getClient();\n\n await client.delete(key);\n }\n\n public async putFile(key: string, localPath: string): Promise<number> {\n const file = Bun.file(localPath);\n\n return await this.put(key, file);\n }\n\n public async put(\n key: string,\n content: string | ArrayBuffer | SharedArrayBuffer | Request | Response | BunFile | S3File | Blob,\n ): Promise<number> {\n const s3file: S3File = this.getS3File(key);\n\n return await s3file.write(content);\n }\n\n public async getAsJson<T>(key: string): Promise<T> {\n const s3file: S3File = this.getS3File(key);\n\n return await s3file.json();\n }\n\n public async getAsArrayBuffer(key: string): Promise<ArrayBuffer> {\n const s3file: S3File = this.getS3File(key);\n\n return await s3file.arrayBuffer();\n }\n\n public getAsStream(key: string): ReadableStream {\n const s3file: S3File = this.getS3File(key);\n\n return s3file.stream();\n }\n\n protected getClient(): Bun.S3Client {\n if (!this.client) {\n this.client = new Bun.S3Client(this.getOptions());\n }\n\n return this.client;\n }\n\n protected getS3File(path: string): S3File {\n const client = this.getClient();\n\n return client.file(path);\n }\n}\n",
|
|
6
|
+
"import { Exception } from \"@ooneex/exception\";\nimport { HttpStatus } from \"@ooneex/http-status\";\n\nexport class StorageException extends Exception {\n constructor(message: string, data: Record<string, unknown> = {}) {\n super(message, {\n status: HttpStatus.Code.InternalServerError,\n data,\n });\n this.name = \"StorageException\";\n }\n}\n",
|
|
7
|
+
"import type { S3Options } from \"bun\";\nimport { AbstractStorage } from \"./AbstractStorage\";\nimport { StorageException } from \"./StorageException\";\n\nexport class CloudflareStorageAdapter extends AbstractStorage {\n protected bucket: string;\n private readonly accessKey: string;\n private readonly secretKey: string;\n private readonly endpoint: string;\n private readonly region: string;\n\n constructor(options?: {\n accessKey?: string;\n secretKey?: string;\n endpoint?: string;\n region?: \"EEUR\" | \"WEUR\" | \"APAC\" | \"NAM\";\n }) {\n super();\n\n const accessKey = options?.accessKey || Bun.env.STORAGE_CLOUDFLARE_ACCESS_KEY;\n const secretKey = options?.secretKey || Bun.env.STORAGE_CLOUDFLARE_SECRET_KEY;\n const endpoint = options?.endpoint || Bun.env.STORAGE_CLOUDFLARE_ENDPOINT;\n\n if (!accessKey) {\n throw new StorageException(\n \"Cloudflare access key is required. Please provide an access key either through the constructor options or set the STORAGE_CLOUDFLARE_ACCESS_KEY environment variable.\",\n );\n }\n if (!secretKey) {\n throw new StorageException(\n \"Cloudflare secret key is required. Please provide a secret key either through the constructor options or set the STORAGE_CLOUDFLARE_SECRET_KEY environment variable.\",\n );\n }\n if (!endpoint) {\n throw new StorageException(\n \"Cloudflare endpoint is required. Please provide an endpoint either through the constructor options or set the STORAGE_CLOUDFLARE_ENDPOINT environment variable.\",\n );\n }\n\n this.accessKey = accessKey;\n this.secretKey = secretKey;\n this.endpoint = endpoint;\n this.region = options?.region || Bun.env.STORAGE_CLOUDFLARE_REGION || \"EEUR\";\n }\n\n public getOptions(): S3Options {\n return {\n accessKeyId: this.accessKey,\n secretAccessKey: this.secretKey,\n endpoint: this.endpoint,\n bucket: this.bucket,\n region: this.region,\n };\n }\n}\n",
|
|
8
|
+
"import { existsSync, mkdirSync } from \"node:fs\";\nimport { mkdir, readdir, rmdir, stat } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport type { BunFile, S3File, S3Options } from \"bun\";\nimport { AbstractStorage } from \"./AbstractStorage\";\nimport { StorageException } from \"./StorageException\";\n\nexport class FilesystemStorage extends AbstractStorage {\n protected bucket: string;\n private readonly storagePath: string;\n\n constructor(options?: {\n storagePath?: string;\n }) {\n super();\n\n const basePath = options?.storagePath || Bun.env.FILESYSTEM_STORAGE_PATH;\n\n if (!basePath) {\n throw new StorageException(\n \"Base path is required. Please provide a base path either through the constructor options or set the FILESYSTEM_STORAGE_PATH environment variable.\",\n );\n }\n\n this.storagePath = basePath;\n\n try {\n if (!existsSync(basePath)) {\n mkdirSync(basePath, { recursive: true });\n }\n } catch (error) {\n throw new StorageException(\n `Failed to create base storage directory at ${basePath}: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n public getOptions(): S3Options {\n return {\n accessKeyId: \"filesystem\",\n secretAccessKey: \"filesystem\",\n endpoint: this.storagePath,\n bucket: this.bucket,\n region: \"local\",\n };\n }\n\n private getBucketPath(): string {\n if (!this.bucket) {\n throw new StorageException(\"Bucket name is required. Please call setBucket() first.\");\n }\n return join(this.storagePath, this.bucket);\n }\n\n private getFilePath(key: string): string {\n return join(this.getBucketPath(), key);\n }\n\n public override setBucket(name: string): this {\n this.bucket = name;\n\n const bucketPath = this.getBucketPath();\n try {\n if (!existsSync(bucketPath)) {\n mkdirSync(bucketPath, { recursive: true });\n }\n } catch (error) {\n throw new StorageException(\n `Failed to create bucket directory at ${bucketPath}: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n return this;\n }\n\n public override async list(): Promise<string[]> {\n const bucketPath = this.getBucketPath();\n\n if (!existsSync(bucketPath)) {\n return [];\n }\n\n try {\n const files = await this.listFilesRecursive(bucketPath, bucketPath);\n return files;\n } catch (error) {\n throw new StorageException(\n `Failed to list files in bucket: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n private async listFilesRecursive(dir: string, baseDir: string): Promise<string[]> {\n const files: string[] = [];\n const entries = await readdir(dir);\n\n for (const entry of entries) {\n const fullPath = join(dir, entry);\n const stats = await stat(fullPath);\n\n if (stats.isDirectory()) {\n const subFiles = await this.listFilesRecursive(fullPath, baseDir);\n files.push(...subFiles);\n } else {\n const relativePath = fullPath.substring(baseDir.length + 1);\n files.push(relativePath);\n }\n }\n\n return files;\n }\n\n public override async clearBucket(): Promise<this> {\n const bucketPath = this.getBucketPath();\n\n if (!existsSync(bucketPath)) {\n return this;\n }\n\n try {\n await this.removeDirectoryRecursive(bucketPath);\n await mkdir(bucketPath, { recursive: true });\n } catch (error) {\n throw new StorageException(`Failed to clear bucket: ${error instanceof Error ? error.message : String(error)}`);\n }\n\n return this;\n }\n\n private async removeDirectoryRecursive(dir: string): Promise<void> {\n const entries = await readdir(dir);\n\n for (const entry of entries) {\n const fullPath = join(dir, entry);\n const stats = await stat(fullPath);\n\n if (stats.isDirectory()) {\n await this.removeDirectoryRecursive(fullPath);\n await rmdir(fullPath);\n } else {\n const file = Bun.file(fullPath);\n await file.delete();\n }\n }\n }\n\n public override async exists(key: string): Promise<boolean> {\n const filePath = this.getFilePath(key);\n const file = Bun.file(filePath);\n return await file.exists();\n }\n\n public override async delete(key: string): Promise<void> {\n const filePath = this.getFilePath(key);\n const file = Bun.file(filePath);\n\n if (!(await file.exists())) {\n return;\n }\n\n try {\n await file.delete();\n\n let parentDir = dirname(filePath);\n const bucketPath = this.getBucketPath();\n\n while (parentDir !== bucketPath && parentDir !== this.storagePath) {\n try {\n const entries = await readdir(parentDir);\n if (entries.length === 0) {\n await rmdir(parentDir);\n parentDir = dirname(parentDir);\n } else {\n break;\n }\n } catch {\n break;\n }\n }\n } catch (error) {\n throw new StorageException(\n `Failed to delete file ${key}: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n public override async putFile(key: string, localPath: string): Promise<number> {\n const file = Bun.file(localPath);\n return await this.put(key, file);\n }\n\n public override async put(\n key: string,\n content: string | ArrayBuffer | SharedArrayBuffer | Request | Response | BunFile | S3File | Blob,\n ): Promise<number> {\n const filePath = this.getFilePath(key);\n const dir = dirname(filePath);\n\n try {\n if (!existsSync(dir)) {\n await mkdir(dir, { recursive: true });\n }\n } catch (error) {\n throw new StorageException(\n `Failed to create directory ${dir}: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n try {\n let bytesWritten: number;\n\n if (typeof content === \"string\") {\n bytesWritten = await Bun.write(filePath, content);\n } else if (content instanceof ArrayBuffer) {\n bytesWritten = await Bun.write(filePath, content);\n } else if (content instanceof SharedArrayBuffer) {\n const arrayBuffer = new ArrayBuffer(content.byteLength);\n new Uint8Array(arrayBuffer).set(new Uint8Array(content));\n bytesWritten = await Bun.write(filePath, arrayBuffer);\n } else if (content instanceof Request) {\n const arrayBuffer = await content.arrayBuffer();\n bytesWritten = await Bun.write(filePath, arrayBuffer);\n } else if (content instanceof Response) {\n const arrayBuffer = await content.arrayBuffer();\n bytesWritten = await Bun.write(filePath, arrayBuffer);\n } else if (content instanceof Blob) {\n bytesWritten = await Bun.write(filePath, content);\n } else {\n const arrayBuffer = await (content as BunFile | S3File).arrayBuffer();\n bytesWritten = await Bun.write(filePath, arrayBuffer);\n }\n\n return bytesWritten;\n } catch (error) {\n throw new StorageException(\n `Failed to write file ${key}: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n public override async getAsJson<T>(key: string): Promise<T> {\n const filePath = this.getFilePath(key);\n const file = Bun.file(filePath);\n\n if (!(await file.exists())) {\n throw new StorageException(`File ${key} does not exist`);\n }\n\n try {\n return await file.json();\n } catch (error) {\n throw new StorageException(\n `Failed to read file ${key} as JSON: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n public override async getAsArrayBuffer(key: string): Promise<ArrayBuffer> {\n const filePath = this.getFilePath(key);\n const file = Bun.file(filePath);\n\n if (!(await file.exists())) {\n throw new StorageException(`File ${key} does not exist`);\n }\n\n try {\n return await file.arrayBuffer();\n } catch (error) {\n throw new StorageException(\n `Failed to read file ${key} as ArrayBuffer: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n public override getAsStream(key: string): ReadableStream {\n const filePath = this.getFilePath(key);\n\n if (!existsSync(filePath)) {\n throw new StorageException(`File ${key} does not exist`);\n }\n\n const file = Bun.file(filePath);\n\n try {\n return file.stream();\n } catch (error) {\n throw new StorageException(\n `Failed to read file ${key} as stream: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n}\n"
|
|
9
|
+
],
|
|
10
|
+
"mappings": ";AAGO,MAAe,CAAoC,CAC9C,OAA8B,KAIjC,SAAS,CAAC,EAAoB,CAInC,OAHA,KAAK,OAAS,EACd,KAAK,OAAS,IAAI,IAAI,SAAS,KAAK,WAAW,CAAC,EAEzC,UAGI,KAAI,EAAsB,CAGrC,OAAQ,MAFO,KAAK,UAAU,EAET,KAAK,GAAG,UAAU,IAAI,CAAC,IAAY,EAAQ,GAAG,GAAK,CAAC,OAG9D,YAAW,EAAkB,CACxC,IAAM,EAAS,KAAK,UAAU,EACxB,EAAO,MAAM,KAAK,KAAK,EAE7B,QAAW,KAAO,EAChB,MAAM,EAAO,OAAO,CAAG,EAGzB,OAAO,UAGI,OAAM,CAAC,EAA+B,CAGjD,OAAO,MAFQ,KAAK,UAAU,EAEV,OAAO,CAAG,OAGnB,OAAM,CAAC,EAA4B,CAG9C,MAFe,KAAK,UAAU,EAEjB,OAAO,CAAG,OAGZ,QAAO,CAAC,EAAa,EAAoC,CACpE,IAAM,EAAO,IAAI,KAAK,CAAS,EAE/B,OAAO,MAAM,KAAK,IAAI,EAAK,CAAI,OAGpB,IAAG,CACd,EACA,EACiB,CAGjB,OAAO,MAFgB,KAAK,UAAU,CAAG,EAErB,MAAM,CAAO,OAGtB,UAAY,CAAC,EAAyB,CAGjD,OAAO,MAFgB,KAAK,UAAU,CAAG,EAErB,KAAK,OAGd,iBAAgB,CAAC,EAAmC,CAG/D,OAAO,MAFgB,KAAK,UAAU,CAAG,EAErB,YAAY,EAG3B,WAAW,CAAC,EAA6B,CAG9C,OAFuB,KAAK,UAAU,CAAG,EAE3B,OAAO,EAGb,SAAS,EAAiB,CAClC,GAAI,CAAC,KAAK,OACR,KAAK,OAAS,IAAI,IAAI,SAAS,KAAK,WAAW,CAAC,EAGlD,OAAO,KAAK,OAGJ,SAAS,CAAC,EAAsB,CAGxC,OAFe,KAAK,UAAU,EAEhB,KAAK,CAAI,EAE3B,CC1FA,oBAAS,0BACT,qBAAS,4BAEF,MAAM,UAAyB,CAAU,CAC9C,WAAW,CAAC,EAAiB,EAAgC,CAAC,EAAG,CAC/D,MAAM,EAAS,CACb,OAAQ,EAAW,KAAK,oBACxB,MACF,CAAC,EACD,KAAK,KAAO,mBAEhB,CCPO,MAAM,UAAiC,CAAgB,CAClD,OACO,UACA,UACA,SACA,OAEjB,WAAW,CAAC,EAKT,CACD,MAAM,EAEN,IAAM,EAAY,GAAS,WAAa,IAAI,IAAI,8BAC1C,EAAY,GAAS,WAAa,IAAI,IAAI,8BAC1C,EAAW,GAAS,UAAY,IAAI,IAAI,4BAE9C,GAAI,CAAC,EACH,MAAM,IAAI,EACR,uKACF,EAEF,GAAI,CAAC,EACH,MAAM,IAAI,EACR,sKACF,EAEF,GAAI,CAAC,EACH,MAAM,IAAI,EACR,iKACF,EAGF,KAAK,UAAY,EACjB,KAAK,UAAY,EACjB,KAAK,SAAW,EAChB,KAAK,OAAS,GAAS,QAAU,IAAI,IAAI,2BAA6B,OAGjE,UAAU,EAAc,CAC7B,MAAO,CACL,YAAa,KAAK,UAClB,gBAAiB,KAAK,UACtB,SAAU,KAAK,SACf,OAAQ,KAAK,OACb,OAAQ,KAAK,MACf,EAEJ,CCtDA,qBAAS,eAAY,WACrB,gBAAS,aAAO,WAAS,UAAO,oBAChC,kBAAS,UAAS,aAKX,MAAM,UAA0B,CAAgB,CAC3C,OACO,YAEjB,WAAW,CAAC,EAET,CACD,MAAM,EAEN,IAAM,EAAW,GAAS,aAAe,IAAI,IAAI,wBAEjD,GAAI,CAAC,EACH,MAAM,IAAI,EACR,mJACF,EAGF,KAAK,YAAc,EAEnB,GAAI,CACF,GAAI,CAAC,EAAW,CAAQ,EACtB,EAAU,EAAU,CAAE,UAAW,EAAK,CAAC,EAEzC,MAAO,EAAO,CACd,MAAM,IAAI,EACR,8CAA8C,MAAa,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,GAClH,GAIG,UAAU,EAAc,CAC7B,MAAO,CACL,YAAa,aACb,gBAAiB,aACjB,SAAU,KAAK,YACf,OAAQ,KAAK,OACb,OAAQ,OACV,EAGM,aAAa,EAAW,CAC9B,GAAI,CAAC,KAAK,OACR,MAAM,IAAI,EAAiB,yDAAyD,EAEtF,OAAO,EAAK,KAAK,YAAa,KAAK,MAAM,EAGnC,WAAW,CAAC,EAAqB,CACvC,OAAO,EAAK,KAAK,cAAc,EAAG,CAAG,EAGvB,SAAS,CAAC,EAAoB,CAC5C,KAAK,OAAS,EAEd,IAAM,EAAa,KAAK,cAAc,EACtC,GAAI,CACF,GAAI,CAAC,EAAW,CAAU,EACxB,EAAU,EAAY,CAAE,UAAW,EAAK,CAAC,EAE3C,MAAO,EAAO,CACd,MAAM,IAAI,EACR,wCAAwC,MAAe,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,GAC9G,EAGF,OAAO,UAGa,KAAI,EAAsB,CAC9C,IAAM,EAAa,KAAK,cAAc,EAEtC,GAAI,CAAC,EAAW,CAAU,EACxB,MAAO,CAAC,EAGV,GAAI,CAEF,OADc,MAAM,KAAK,mBAAmB,EAAY,CAAU,EAElE,MAAO,EAAO,CACd,MAAM,IAAI,EACR,mCAAmC,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,GAC1F,QAIU,mBAAkB,CAAC,EAAa,EAAoC,CAChF,IAAM,EAAkB,CAAC,EACnB,EAAU,MAAM,EAAQ,CAAG,EAEjC,QAAW,KAAS,EAAS,CAC3B,IAAM,EAAW,EAAK,EAAK,CAAK,EAGhC,IAFc,MAAM,EAAK,CAAQ,GAEvB,YAAY,EAAG,CACvB,IAAM,EAAW,MAAM,KAAK,mBAAmB,EAAU,CAAO,EAChE,EAAM,KAAK,GAAG,CAAQ,EACjB,KACL,IAAM,EAAe,EAAS,UAAU,EAAQ,OAAS,CAAC,EAC1D,EAAM,KAAK,CAAY,GAI3B,OAAO,OAGa,YAAW,EAAkB,CACjD,IAAM,EAAa,KAAK,cAAc,EAEtC,GAAI,CAAC,EAAW,CAAU,EACxB,OAAO,KAGT,GAAI,CACF,MAAM,KAAK,yBAAyB,CAAU,EAC9C,MAAM,EAAM,EAAY,CAAE,UAAW,EAAK,CAAC,EAC3C,MAAO,EAAO,CACd,MAAM,IAAI,EAAiB,2BAA2B,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,GAAG,EAGhH,OAAO,UAGK,yBAAwB,CAAC,EAA4B,CACjE,IAAM,EAAU,MAAM,EAAQ,CAAG,EAEjC,QAAW,KAAS,EAAS,CAC3B,IAAM,EAAW,EAAK,EAAK,CAAK,EAGhC,IAFc,MAAM,EAAK,CAAQ,GAEvB,YAAY,EACpB,MAAM,KAAK,yBAAyB,CAAQ,EAC5C,MAAM,EAAM,CAAQ,EAGpB,WADa,IAAI,KAAK,CAAQ,EACnB,OAAO,QAKF,OAAM,CAAC,EAA+B,CAC1D,IAAM,EAAW,KAAK,YAAY,CAAG,EAErC,OAAO,MADM,IAAI,KAAK,CAAQ,EACZ,OAAO,OAGL,OAAM,CAAC,EAA4B,CACvD,IAAM,EAAW,KAAK,YAAY,CAAG,EAC/B,EAAO,IAAI,KAAK,CAAQ,EAE9B,GAAI,CAAE,MAAM,EAAK,OAAO,EACtB,OAGF,GAAI,CACF,MAAM,EAAK,OAAO,EAElB,IAAI,EAAY,EAAQ,CAAQ,EAC1B,EAAa,KAAK,cAAc,EAEtC,MAAO,IAAc,GAAc,IAAc,KAAK,YACpD,GAAI,CAEF,IADgB,MAAM,EAAQ,CAAS,GAC3B,SAAW,EACrB,MAAM,EAAM,CAAS,EACrB,EAAY,EAAQ,CAAS,EAE7B,WAEF,KAAM,CACN,OAGJ,MAAO,EAAO,CACd,MAAM,IAAI,EACR,yBAAyB,MAAQ,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,GACxF,QAIkB,QAAO,CAAC,EAAa,EAAoC,CAC7E,IAAM,EAAO,IAAI,KAAK,CAAS,EAC/B,OAAO,MAAM,KAAK,IAAI,EAAK,CAAI,OAGX,IAAG,CACvB,EACA,EACiB,CACjB,IAAM,EAAW,KAAK,YAAY,CAAG,EAC/B,EAAM,EAAQ,CAAQ,EAE5B,GAAI,CACF,GAAI,CAAC,EAAW,CAAG,EACjB,MAAM,EAAM,EAAK,CAAE,UAAW,EAAK,CAAC,EAEtC,MAAO,EAAO,CACd,MAAM,IAAI,EACR,8BAA8B,MAAQ,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,GAC7F,EAGF,GAAI,CACF,IAAI,EAEJ,GAAI,OAAO,IAAY,SACrB,EAAe,MAAM,IAAI,MAAM,EAAU,CAAO,EAC3C,QAAI,aAAmB,YAC5B,EAAe,MAAM,IAAI,MAAM,EAAU,CAAO,EAC3C,QAAI,aAAmB,kBAAmB,CAC/C,IAAM,EAAc,IAAI,YAAY,EAAQ,UAAU,EACtD,IAAI,WAAW,CAAW,EAAE,IAAI,IAAI,WAAW,CAAO,CAAC,EACvD,EAAe,MAAM,IAAI,MAAM,EAAU,CAAW,EAC/C,QAAI,aAAmB,QAAS,CACrC,IAAM,EAAc,MAAM,EAAQ,YAAY,EAC9C,EAAe,MAAM,IAAI,MAAM,EAAU,CAAW,EAC/C,QAAI,aAAmB,SAAU,CACtC,IAAM,EAAc,MAAM,EAAQ,YAAY,EAC9C,EAAe,MAAM,IAAI,MAAM,EAAU,CAAW,EAC/C,QAAI,aAAmB,KAC5B,EAAe,MAAM,IAAI,MAAM,EAAU,CAAO,EAC3C,KACL,IAAM,EAAc,MAAO,EAA6B,YAAY,EACpE,EAAe,MAAM,IAAI,MAAM,EAAU,CAAW,EAGtD,OAAO,EACP,MAAO,EAAO,CACd,MAAM,IAAI,EACR,wBAAwB,MAAQ,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,GACvF,QAIkB,UAAY,CAAC,EAAyB,CAC1D,IAAM,EAAW,KAAK,YAAY,CAAG,EAC/B,EAAO,IAAI,KAAK,CAAQ,EAE9B,GAAI,CAAE,MAAM,EAAK,OAAO,EACtB,MAAM,IAAI,EAAiB,QAAQ,kBAAoB,EAGzD,GAAI,CACF,OAAO,MAAM,EAAK,KAAK,EACvB,MAAO,EAAO,CACd,MAAM,IAAI,EACR,uBAAuB,cAAgB,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,GAC9F,QAIkB,iBAAgB,CAAC,EAAmC,CACxE,IAAM,EAAW,KAAK,YAAY,CAAG,EAC/B,EAAO,IAAI,KAAK,CAAQ,EAE9B,GAAI,CAAE,MAAM,EAAK,OAAO,EACtB,MAAM,IAAI,EAAiB,QAAQ,kBAAoB,EAGzD,GAAI,CACF,OAAO,MAAM,EAAK,YAAY,EAC9B,MAAO,EAAO,CACd,MAAM,IAAI,EACR,uBAAuB,qBAAuB,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,GACrG,GAIY,WAAW,CAAC,EAA6B,CACvD,IAAM,EAAW,KAAK,YAAY,CAAG,EAErC,GAAI,CAAC,EAAW,CAAQ,EACtB,MAAM,IAAI,EAAiB,QAAQ,kBAAoB,EAGzD,IAAM,EAAO,IAAI,KAAK,CAAQ,EAE9B,GAAI,CACF,OAAO,EAAK,OAAO,EACnB,MAAO,EAAO,CACd,MAAM,IAAI,EACR,uBAAuB,gBAAkB,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,GAChG,GAGN",
|
|
11
|
+
"debugId": "C81D99E08CC7CE6B64756E2164756E21",
|
|
12
|
+
"names": []
|
|
13
|
+
}
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ooneex/storage",
|
|
3
|
+
"description": "",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"LICENSE",
|
|
9
|
+
"README.md",
|
|
10
|
+
"package.json"
|
|
11
|
+
],
|
|
12
|
+
"module": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"import": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "bun test tests",
|
|
26
|
+
"build": "bunup",
|
|
27
|
+
"lint": "tsgo --noEmit && bunx biome lint",
|
|
28
|
+
"publish:prod": "bun publish --tolerate-republish --access public",
|
|
29
|
+
"publish:pack": "bun pm pack --destination ./dist",
|
|
30
|
+
"publish:dry": "bun publish --dry-run"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@ooneex/exception": "0.0.1",
|
|
34
|
+
"@ooneex/http-status": "0.0.1"
|
|
35
|
+
}
|
|
36
|
+
}
|