@statewalker/webrun-files-s3 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +70 -148
- package/dist/cjs/index.cjs +155 -408
- package/dist/esm/index.js +158 -410
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/s3-files-api.d.ts +22 -53
- package/dist/s3-files-api.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +1 -2
- package/src/s3-files-api.ts +243 -167
- package/dist/s3-file-handle.d.ts +0 -75
- package/dist/s3-file-handle.d.ts.map +0 -1
- package/src/s3-file-handle.ts +0 -491
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
# @statewalker/webrun-files-s3
|
|
2
2
|
|
|
3
|
-
S3
|
|
3
|
+
S3 implementation of the `FilesApi` interface from `@statewalker/webrun-files`.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
This package provides
|
|
7
|
+
This package provides a `FilesApi` implementation that stores files in Amazon S3 or S3-compatible object storage services (MinIO, DigitalOcean Spaces, Backblaze B2, Cloudflare R2, etc.). It maps filesystem-like operations to S3 API calls, providing:
|
|
8
8
|
|
|
9
9
|
- **Virtual directory structure** using key prefixes
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **Copy operations** using S3's server-side copy (no data transfer through client)
|
|
10
|
+
- **Range reads** via HTTP Range headers for efficient partial access
|
|
11
|
+
- **Server-side copy** for copy/move operations (no data transfer through client)
|
|
13
12
|
|
|
14
13
|
## Installation
|
|
15
14
|
|
|
@@ -22,40 +21,35 @@ npm install @statewalker/webrun-files-s3 @statewalker/webrun-files @aws-sdk/clie
|
|
|
22
21
|
### Basic Usage
|
|
23
22
|
|
|
24
23
|
```typescript
|
|
25
|
-
import { S3Client } from
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
24
|
+
import { S3Client } from '@aws-sdk/client-s3';
|
|
25
|
+
import { S3FilesApi } from '@statewalker/webrun-files-s3';
|
|
26
|
+
import { readText, writeText } from '@statewalker/webrun-files';
|
|
28
27
|
|
|
29
28
|
// Create S3 client
|
|
30
29
|
const s3Client = new S3Client({
|
|
31
|
-
region:
|
|
30
|
+
region: 'us-east-1',
|
|
32
31
|
credentials: {
|
|
33
|
-
accessKeyId:
|
|
34
|
-
secretAccessKey:
|
|
32
|
+
accessKeyId: 'YOUR_ACCESS_KEY',
|
|
33
|
+
secretAccessKey: 'YOUR_SECRET_KEY',
|
|
35
34
|
},
|
|
36
35
|
});
|
|
37
36
|
|
|
38
37
|
// Create S3-backed files API
|
|
39
|
-
const
|
|
38
|
+
const files = new S3FilesApi({
|
|
40
39
|
client: s3Client,
|
|
41
|
-
bucket:
|
|
42
|
-
prefix:
|
|
40
|
+
bucket: 'my-bucket',
|
|
41
|
+
prefix: 'my-app/data', // optional key prefix
|
|
43
42
|
});
|
|
44
43
|
|
|
45
|
-
// Wrap with FilesApi for convenience methods
|
|
46
|
-
const api = new FilesApi(s3FilesApi);
|
|
47
|
-
|
|
48
44
|
// Write a file
|
|
49
|
-
await
|
|
50
|
-
new TextEncoder().encode("Hello, S3!")
|
|
51
|
-
]);
|
|
45
|
+
await writeText(files, '/docs/hello.txt', 'Hello, S3!');
|
|
52
46
|
|
|
53
47
|
// Read a file
|
|
54
|
-
const content = await
|
|
55
|
-
console.log(
|
|
48
|
+
const content = await readText(files, '/docs/hello.txt');
|
|
49
|
+
console.log(content); // "Hello, S3!"
|
|
56
50
|
|
|
57
51
|
// List directory contents
|
|
58
|
-
for await (const entry of
|
|
52
|
+
for await (const entry of files.list('/docs')) {
|
|
59
53
|
console.log(entry.name, entry.kind, entry.size);
|
|
60
54
|
}
|
|
61
55
|
```
|
|
@@ -63,132 +57,69 @@ for await (const entry of api.list("/docs")) {
|
|
|
63
57
|
### With S3-Compatible Storage (MinIO)
|
|
64
58
|
|
|
65
59
|
```typescript
|
|
66
|
-
import { S3Client } from
|
|
67
|
-
import { S3FilesApi } from
|
|
68
|
-
import { FilesApi } from "@statewalker/webrun-files";
|
|
60
|
+
import { S3Client } from '@aws-sdk/client-s3';
|
|
61
|
+
import { S3FilesApi } from '@statewalker/webrun-files-s3';
|
|
69
62
|
|
|
70
63
|
const s3Client = new S3Client({
|
|
71
|
-
endpoint:
|
|
72
|
-
region:
|
|
64
|
+
endpoint: 'http://localhost:9000',
|
|
65
|
+
region: 'us-east-1',
|
|
73
66
|
credentials: {
|
|
74
|
-
accessKeyId:
|
|
75
|
-
secretAccessKey:
|
|
67
|
+
accessKeyId: 'minioadmin',
|
|
68
|
+
secretAccessKey: 'minioadmin',
|
|
76
69
|
},
|
|
77
70
|
forcePathStyle: true, // Required for MinIO
|
|
78
71
|
});
|
|
79
72
|
|
|
80
|
-
const
|
|
73
|
+
const files = new S3FilesApi({
|
|
81
74
|
client: s3Client,
|
|
82
|
-
bucket:
|
|
75
|
+
bucket: 'my-bucket',
|
|
83
76
|
});
|
|
84
|
-
|
|
85
|
-
const api = new FilesApi(s3FilesApi);
|
|
86
77
|
```
|
|
87
78
|
|
|
88
79
|
### With AWS IAM Roles (EC2, Lambda, ECS)
|
|
89
80
|
|
|
90
81
|
```typescript
|
|
91
|
-
import { S3Client } from
|
|
92
|
-
import { S3FilesApi } from
|
|
82
|
+
import { S3Client } from '@aws-sdk/client-s3';
|
|
83
|
+
import { S3FilesApi } from '@statewalker/webrun-files-s3';
|
|
93
84
|
|
|
94
85
|
// Credentials are automatically loaded from environment/IAM role
|
|
95
|
-
const s3Client = new S3Client({ region:
|
|
86
|
+
const s3Client = new S3Client({ region: 'us-east-1' });
|
|
96
87
|
|
|
97
|
-
const
|
|
88
|
+
const files = new S3FilesApi({
|
|
98
89
|
client: s3Client,
|
|
99
|
-
bucket:
|
|
90
|
+
bucket: 'my-bucket',
|
|
100
91
|
});
|
|
101
92
|
```
|
|
102
93
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
```typescript
|
|
106
|
-
const api = new FilesApi(s3FilesApi);
|
|
107
|
-
|
|
108
|
-
// Open a file for random access
|
|
109
|
-
const handle = await api.open("/large-file.bin");
|
|
110
|
-
|
|
111
|
-
console.log("File size:", handle.size);
|
|
112
|
-
|
|
113
|
-
// Read a specific range (uses HTTP Range header)
|
|
114
|
-
for await (const chunk of handle.createReadStream({ start: 1000, end: 2000 })) {
|
|
115
|
-
console.log("Chunk:", chunk.length);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Append data
|
|
119
|
-
await handle.appendFile([new TextEncoder().encode(" - appended")]);
|
|
120
|
-
|
|
121
|
-
await handle.close();
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
## Configuration
|
|
94
|
+
## API Reference
|
|
125
95
|
|
|
126
|
-
###
|
|
96
|
+
### S3FilesApi
|
|
127
97
|
|
|
128
98
|
```typescript
|
|
129
99
|
interface S3FilesApiOptions {
|
|
130
|
-
/**
|
|
131
|
-
* Pre-configured S3Client instance.
|
|
132
|
-
* Allows full control over credentials, region, endpoint.
|
|
133
|
-
*/
|
|
100
|
+
/** Pre-configured S3Client instance. */
|
|
134
101
|
client: S3Client;
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* S3 bucket name.
|
|
138
|
-
*/
|
|
102
|
+
/** S3 bucket name. */
|
|
139
103
|
bucket: string;
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Optional key prefix (acts as root directory).
|
|
143
|
-
* All paths will be relative to this prefix.
|
|
144
|
-
* @example "projects/my-app/data"
|
|
145
|
-
*/
|
|
104
|
+
/** Optional key prefix (acts as root directory). */
|
|
146
105
|
prefix?: string;
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Minimum part size for multipart uploads.
|
|
150
|
-
* S3 requires minimum 5MB for all parts except the last.
|
|
151
|
-
* @default 5 * 1024 * 1024 (5MB)
|
|
152
|
-
*/
|
|
106
|
+
/** Part size for multipart uploads (default: 5MB, S3 minimum). */
|
|
153
107
|
multipartPartSize?: number;
|
|
154
108
|
}
|
|
155
|
-
```
|
|
156
109
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
### S3FilesApi
|
|
160
|
-
|
|
161
|
-
Main class implementing `IFilesApi` for S3 storage.
|
|
162
|
-
|
|
163
|
-
```typescript
|
|
164
|
-
class S3FilesApi implements IFilesApi {
|
|
110
|
+
class S3FilesApi implements FilesApi {
|
|
165
111
|
constructor(options: S3FilesApiOptions);
|
|
166
112
|
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
move(source:
|
|
176
|
-
copy(source:
|
|
177
|
-
}
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
### S3FileHandle
|
|
181
|
-
|
|
182
|
-
File handle class for random access operations on S3 objects.
|
|
183
|
-
|
|
184
|
-
```typescript
|
|
185
|
-
class S3FileHandle implements FileHandle {
|
|
186
|
-
readonly size: number;
|
|
187
|
-
|
|
188
|
-
close(): Promise<void>;
|
|
189
|
-
appendFile(data: BinaryStream, options?: AppendOptions): Promise<number>;
|
|
190
|
-
createReadStream(options?: ReadStreamOptions): AsyncGenerator<Uint8Array>;
|
|
191
|
-
createWriteStream(data: BinaryStream, options?: WriteStreamOptions): Promise<number>;
|
|
113
|
+
// All FilesApi methods
|
|
114
|
+
read(path: string, options?: ReadOptions): AsyncIterable<Uint8Array>;
|
|
115
|
+
write(path: string, content: Iterable<Uint8Array> | AsyncIterable<Uint8Array>): Promise<void>;
|
|
116
|
+
mkdir(path: string): Promise<void>;
|
|
117
|
+
list(path: string, options?: ListOptions): AsyncIterable<FileInfo>;
|
|
118
|
+
stats(path: string): Promise<FileStats | undefined>;
|
|
119
|
+
exists(path: string): Promise<boolean>;
|
|
120
|
+
remove(path: string): Promise<boolean>;
|
|
121
|
+
move(source: string, target: string): Promise<boolean>;
|
|
122
|
+
copy(source: string, target: string): Promise<boolean>;
|
|
192
123
|
}
|
|
193
124
|
```
|
|
194
125
|
|
|
@@ -214,7 +145,7 @@ S3 doesn't have real directories, but this implementation simulates them using:
|
|
|
214
145
|
```typescript
|
|
215
146
|
// List /docs with prefix "my-app"
|
|
216
147
|
// S3 request: ListObjectsV2(Prefix="my-app/docs/", Delimiter="/")
|
|
217
|
-
for await (const entry of
|
|
148
|
+
for await (const entry of files.list('/docs')) {
|
|
218
149
|
// entry.kind is "file" or "directory"
|
|
219
150
|
}
|
|
220
151
|
```
|
|
@@ -224,31 +155,30 @@ for await (const entry of api.list("/docs")) {
|
|
|
224
155
|
Reads use `GetObject` with HTTP Range headers for efficient partial access:
|
|
225
156
|
|
|
226
157
|
```typescript
|
|
227
|
-
// Read bytes 1000-
|
|
228
|
-
const
|
|
229
|
-
for await (const chunk of handle.createReadStream({ start: 1000, end: 2000 })) {
|
|
158
|
+
// Read bytes 1000-1499 from a file
|
|
159
|
+
for await (const chunk of files.read('/large-file.bin', { start: 1000, length: 500 })) {
|
|
230
160
|
// Streams directly from S3, no full file download
|
|
231
161
|
}
|
|
232
162
|
```
|
|
233
163
|
|
|
234
164
|
### Writing Files
|
|
235
165
|
|
|
236
|
-
|
|
166
|
+
Files are uploaded using a streaming approach that minimizes memory usage:
|
|
237
167
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
3. **CompleteMultipartUpload** - Finalize
|
|
168
|
+
- **Small files** (< 5MB): Uses simple `PutObject` for efficiency
|
|
169
|
+
- **Large files** (>= 5MB): Uses streaming multipart upload, buffering only one part at a time
|
|
241
170
|
|
|
242
|
-
|
|
171
|
+
```typescript
|
|
172
|
+
// Small file - uses PutObject
|
|
173
|
+
await writeText(files, '/data/file.txt', 'small content');
|
|
243
174
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
175
|
+
// Large file - automatically uses multipart upload
|
|
176
|
+
const largeContent = generateLargeContent(); // AsyncIterable<Uint8Array>
|
|
177
|
+
await files.write('/data/large-file.bin', largeContent);
|
|
178
|
+
// Only one 5MB part is buffered at a time
|
|
179
|
+
```
|
|
247
180
|
|
|
248
|
-
|
|
249
|
-
- Buffers only one 5MB part at a time
|
|
250
|
-
- Uses server-side copy to preserve existing content
|
|
251
|
-
- Supports files of any size
|
|
181
|
+
The `multipartPartSize` option controls part size (default: 5MB, S3 minimum).
|
|
252
182
|
|
|
253
183
|
### Copy and Move
|
|
254
184
|
|
|
@@ -261,7 +191,7 @@ This approach:
|
|
|
261
191
|
S3 directories are implicit (they exist if files exist within them). The `mkdir()` method creates an empty directory marker object:
|
|
262
192
|
|
|
263
193
|
```typescript
|
|
264
|
-
await
|
|
194
|
+
await files.mkdir('/empty-dir');
|
|
265
195
|
// Creates object: "prefix/empty-dir/" with 0 bytes
|
|
266
196
|
```
|
|
267
197
|
|
|
@@ -278,28 +208,20 @@ This implementation works with any S3-compatible storage:
|
|
|
278
208
|
| **Cloudflare R2** | Use account-specific endpoint |
|
|
279
209
|
| **Wasabi** | Use region-specific endpoint |
|
|
280
210
|
|
|
281
|
-
## Performance Considerations
|
|
282
|
-
|
|
283
|
-
- **Multipart threshold**: Files larger than 5MB use multipart upload
|
|
284
|
-
- **Part size**: Default 5MB per part (configurable via `multipartPartSize`)
|
|
285
|
-
- **Range reads**: Always use `createReadStream({ start, end })` for partial reads
|
|
286
|
-
- **Server-side copy**: Copy/move operations don't transfer data through client
|
|
287
|
-
- **Pagination**: Directory listings handle pagination automatically
|
|
288
|
-
|
|
289
211
|
## Testing
|
|
290
212
|
|
|
291
213
|
Tests use [testcontainers](https://github.com/testcontainers/testcontainers-node) with MinIO:
|
|
292
214
|
|
|
293
215
|
```typescript
|
|
294
|
-
import { MinioContainer } from
|
|
295
|
-
import { S3Client, CreateBucketCommand } from
|
|
296
|
-
import { S3FilesApi } from
|
|
216
|
+
import { MinioContainer } from '@testcontainers/minio';
|
|
217
|
+
import { S3Client, CreateBucketCommand } from '@aws-sdk/client-s3';
|
|
218
|
+
import { S3FilesApi } from '@statewalker/webrun-files-s3';
|
|
297
219
|
|
|
298
220
|
const minioContainer = await new MinioContainer().start();
|
|
299
221
|
|
|
300
222
|
const s3Client = new S3Client({
|
|
301
223
|
endpoint: minioContainer.getConnectionUrl(),
|
|
302
|
-
region:
|
|
224
|
+
region: 'us-east-1',
|
|
303
225
|
credentials: {
|
|
304
226
|
accessKeyId: minioContainer.getUsername(),
|
|
305
227
|
secretAccessKey: minioContainer.getPassword(),
|
|
@@ -307,11 +229,11 @@ const s3Client = new S3Client({
|
|
|
307
229
|
forcePathStyle: true,
|
|
308
230
|
});
|
|
309
231
|
|
|
310
|
-
await s3Client.send(new CreateBucketCommand({ Bucket:
|
|
232
|
+
await s3Client.send(new CreateBucketCommand({ Bucket: 'test-bucket' }));
|
|
311
233
|
|
|
312
|
-
const
|
|
234
|
+
const files = new S3FilesApi({
|
|
313
235
|
client: s3Client,
|
|
314
|
-
bucket:
|
|
236
|
+
bucket: 'test-bucket',
|
|
315
237
|
});
|
|
316
238
|
```
|
|
317
239
|
|