@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 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