@statewalker/webrun-files-s3 0.5.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 +21 -0
- package/README.md +320 -0
- package/dist/cjs/index.cjs +579 -0
- package/dist/esm/index.js +578 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/s3-file-handle.d.ts +75 -0
- package/dist/s3-file-handle.d.ts.map +1 -0
- package/dist/s3-files-api.d.ts +84 -0
- package/dist/s3-files-api.d.ts.map +1 -0
- package/package.json +57 -0
- package/src/index.ts +6 -0
- package/src/s3-file-handle.ts +491 -0
- package/src/s3-files-api.ts +455 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 statewalker
|
|
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,320 @@
|
|
|
1
|
+
# @statewalker/webrun-files-s3
|
|
2
|
+
|
|
3
|
+
S3-backed implementation of the `IFilesApi` interface from `@statewalker/webrun-files`.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This package provides an `IFilesApi` implementation that stores files in Amazon S3 or S3-compatible object storage services (MinIO, DigitalOcean Spaces, Backblaze B2, etc.). It maps filesystem-like operations to S3 API calls, providing:
|
|
8
|
+
|
|
9
|
+
- **Virtual directory structure** using key prefixes
|
|
10
|
+
- **Streaming reads** with HTTP Range headers for efficient partial access
|
|
11
|
+
- **Streaming multipart uploads** for large files without buffering entirely in memory
|
|
12
|
+
- **Copy operations** using S3's server-side copy (no data transfer through client)
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @statewalker/webrun-files-s3 @statewalker/webrun-files @aws-sdk/client-s3
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### Basic Usage
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { S3Client } from "@aws-sdk/client-s3";
|
|
26
|
+
import { FilesApi } from "@statewalker/webrun-files";
|
|
27
|
+
import { S3FilesApi } from "@statewalker/webrun-files-s3";
|
|
28
|
+
|
|
29
|
+
// Create S3 client
|
|
30
|
+
const s3Client = new S3Client({
|
|
31
|
+
region: "us-east-1",
|
|
32
|
+
credentials: {
|
|
33
|
+
accessKeyId: "YOUR_ACCESS_KEY",
|
|
34
|
+
secretAccessKey: "YOUR_SECRET_KEY",
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Create S3-backed files API
|
|
39
|
+
const s3FilesApi = new S3FilesApi({
|
|
40
|
+
client: s3Client,
|
|
41
|
+
bucket: "my-bucket",
|
|
42
|
+
prefix: "my-app/data", // optional key prefix
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Wrap with FilesApi for convenience methods
|
|
46
|
+
const api = new FilesApi(s3FilesApi);
|
|
47
|
+
|
|
48
|
+
// Write a file
|
|
49
|
+
await api.write("/docs/hello.txt", [
|
|
50
|
+
new TextEncoder().encode("Hello, S3!")
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
// Read a file
|
|
54
|
+
const content = await api.readFile("/docs/hello.txt");
|
|
55
|
+
console.log(new TextDecoder().decode(content)); // "Hello, S3!"
|
|
56
|
+
|
|
57
|
+
// List directory contents
|
|
58
|
+
for await (const entry of api.list("/docs")) {
|
|
59
|
+
console.log(entry.name, entry.kind, entry.size);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### With S3-Compatible Storage (MinIO)
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { S3Client } from "@aws-sdk/client-s3";
|
|
67
|
+
import { S3FilesApi } from "@statewalker/webrun-files-s3";
|
|
68
|
+
import { FilesApi } from "@statewalker/webrun-files";
|
|
69
|
+
|
|
70
|
+
const s3Client = new S3Client({
|
|
71
|
+
endpoint: "http://localhost:9000",
|
|
72
|
+
region: "us-east-1",
|
|
73
|
+
credentials: {
|
|
74
|
+
accessKeyId: "minioadmin",
|
|
75
|
+
secretAccessKey: "minioadmin",
|
|
76
|
+
},
|
|
77
|
+
forcePathStyle: true, // Required for MinIO
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const s3FilesApi = new S3FilesApi({
|
|
81
|
+
client: s3Client,
|
|
82
|
+
bucket: "my-bucket",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const api = new FilesApi(s3FilesApi);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### With AWS IAM Roles (EC2, Lambda, ECS)
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { S3Client } from "@aws-sdk/client-s3";
|
|
92
|
+
import { S3FilesApi } from "@statewalker/webrun-files-s3";
|
|
93
|
+
|
|
94
|
+
// Credentials are automatically loaded from environment/IAM role
|
|
95
|
+
const s3Client = new S3Client({ region: "us-east-1" });
|
|
96
|
+
|
|
97
|
+
const s3FilesApi = new S3FilesApi({
|
|
98
|
+
client: s3Client,
|
|
99
|
+
bucket: "my-bucket",
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Random Access with FileHandle
|
|
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
|
|
125
|
+
|
|
126
|
+
### S3FilesApiOptions
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
interface S3FilesApiOptions {
|
|
130
|
+
/**
|
|
131
|
+
* Pre-configured S3Client instance.
|
|
132
|
+
* Allows full control over credentials, region, endpoint.
|
|
133
|
+
*/
|
|
134
|
+
client: S3Client;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* S3 bucket name.
|
|
138
|
+
*/
|
|
139
|
+
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
|
+
*/
|
|
146
|
+
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
|
+
*/
|
|
153
|
+
multipartPartSize?: number;
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## API Reference
|
|
158
|
+
|
|
159
|
+
### S3FilesApi
|
|
160
|
+
|
|
161
|
+
Main class implementing `IFilesApi` for S3 storage.
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
class S3FilesApi implements IFilesApi {
|
|
165
|
+
constructor(options: S3FilesApiOptions);
|
|
166
|
+
|
|
167
|
+
// Core IFilesApi methods
|
|
168
|
+
list(file: FileRef, options?: ListOptions): AsyncGenerator<FileInfo>;
|
|
169
|
+
stats(file: FileRef): Promise<FileInfo | undefined>;
|
|
170
|
+
remove(file: FileRef): Promise<boolean>;
|
|
171
|
+
open(file: FileRef): Promise<FileHandle>;
|
|
172
|
+
|
|
173
|
+
// Optional methods (all implemented)
|
|
174
|
+
mkdir(file: FileRef): Promise<void>;
|
|
175
|
+
move(source: FileRef, target: FileRef): Promise<boolean>;
|
|
176
|
+
copy(source: FileRef, target: FileRef, options?: CopyOptions): Promise<boolean>;
|
|
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>;
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## How It Works
|
|
196
|
+
|
|
197
|
+
### Path to Key Mapping
|
|
198
|
+
|
|
199
|
+
Virtual paths are mapped to S3 keys by combining the optional prefix with the path:
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
prefix: "my-app/data"
|
|
203
|
+
path: "/docs/file.txt"
|
|
204
|
+
key: "my-app/data/docs/file.txt"
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Directory Listing
|
|
208
|
+
|
|
209
|
+
S3 doesn't have real directories, but this implementation simulates them using:
|
|
210
|
+
|
|
211
|
+
- **ListObjectsV2** with `Delimiter="/"` to get "subdirectories" via `CommonPrefixes`
|
|
212
|
+
- Files are returned from `Contents`
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
// List /docs with prefix "my-app"
|
|
216
|
+
// S3 request: ListObjectsV2(Prefix="my-app/docs/", Delimiter="/")
|
|
217
|
+
for await (const entry of api.list("/docs")) {
|
|
218
|
+
// entry.kind is "file" or "directory"
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Reading Files
|
|
223
|
+
|
|
224
|
+
Reads use `GetObject` with HTTP Range headers for efficient partial access:
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
// Read bytes 1000-1999 from a file
|
|
228
|
+
const handle = await api.open("/large-file.bin");
|
|
229
|
+
for await (const chunk of handle.createReadStream({ start: 1000, end: 2000 })) {
|
|
230
|
+
// Streams directly from S3, no full file download
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Writing Files
|
|
235
|
+
|
|
236
|
+
Since S3 objects are immutable, writes use streaming multipart upload:
|
|
237
|
+
|
|
238
|
+
1. **CreateMultipartUpload** - Start the upload
|
|
239
|
+
2. **UploadPart** - Upload each 5MB+ chunk as it arrives
|
|
240
|
+
3. **CompleteMultipartUpload** - Finalize
|
|
241
|
+
|
|
242
|
+
For partial writes (preserving content before a position):
|
|
243
|
+
|
|
244
|
+
1. **UploadPartCopy** - Copy existing data as parts (no download needed)
|
|
245
|
+
2. **UploadPart** - Upload new data
|
|
246
|
+
3. **CompleteMultipartUpload** - Finalize
|
|
247
|
+
|
|
248
|
+
This approach:
|
|
249
|
+
- Buffers only one 5MB part at a time
|
|
250
|
+
- Uses server-side copy to preserve existing content
|
|
251
|
+
- Supports files of any size
|
|
252
|
+
|
|
253
|
+
### Copy and Move
|
|
254
|
+
|
|
255
|
+
- **Copy** uses `CopyObject` for single files or multiple `CopyObject` calls for directories
|
|
256
|
+
- **Move** is implemented as copy + delete
|
|
257
|
+
- Both operations happen server-side without transferring data through the client
|
|
258
|
+
|
|
259
|
+
### Directory Creation
|
|
260
|
+
|
|
261
|
+
S3 directories are implicit (they exist if files exist within them). The `mkdir()` method creates an empty directory marker object:
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
await api.mkdir("/empty-dir");
|
|
265
|
+
// Creates object: "prefix/empty-dir/" with 0 bytes
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## S3-Compatible Storage
|
|
269
|
+
|
|
270
|
+
This implementation works with any S3-compatible storage:
|
|
271
|
+
|
|
272
|
+
| Service | Configuration Notes |
|
|
273
|
+
|---------|---------------------|
|
|
274
|
+
| **AWS S3** | Standard configuration |
|
|
275
|
+
| **MinIO** | Set `forcePathStyle: true` |
|
|
276
|
+
| **DigitalOcean Spaces** | Use `endpoint: "https://<region>.digitaloceanspaces.com"` |
|
|
277
|
+
| **Backblaze B2** | Use S3-compatible endpoint |
|
|
278
|
+
| **Cloudflare R2** | Use account-specific endpoint |
|
|
279
|
+
| **Wasabi** | Use region-specific endpoint |
|
|
280
|
+
|
|
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
|
+
## Testing
|
|
290
|
+
|
|
291
|
+
Tests use [testcontainers](https://github.com/testcontainers/testcontainers-node) with MinIO:
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
import { MinioContainer } from "@testcontainers/minio";
|
|
295
|
+
import { S3Client, CreateBucketCommand } from "@aws-sdk/client-s3";
|
|
296
|
+
import { S3FilesApi } from "@statewalker/webrun-files-s3";
|
|
297
|
+
|
|
298
|
+
const minioContainer = await new MinioContainer().start();
|
|
299
|
+
|
|
300
|
+
const s3Client = new S3Client({
|
|
301
|
+
endpoint: minioContainer.getConnectionUrl(),
|
|
302
|
+
region: "us-east-1",
|
|
303
|
+
credentials: {
|
|
304
|
+
accessKeyId: minioContainer.getUsername(),
|
|
305
|
+
secretAccessKey: minioContainer.getPassword(),
|
|
306
|
+
},
|
|
307
|
+
forcePathStyle: true,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
await s3Client.send(new CreateBucketCommand({ Bucket: "test-bucket" }));
|
|
311
|
+
|
|
312
|
+
const s3FilesApi = new S3FilesApi({
|
|
313
|
+
client: s3Client,
|
|
314
|
+
bucket: "test-bucket",
|
|
315
|
+
});
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## License
|
|
319
|
+
|
|
320
|
+
MIT
|