@visulima/storage 1.0.0-alpha.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/CHANGELOG.md +12 -0
- package/LICENSE.md +1189 -0
- package/README.md +561 -0
- package/package.json +286 -0
package/README.md
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h3>Visulima upload</h3>
|
|
3
|
+
<p>Visulima upload copies files to a web-accessible location and provides a consistent way to get the URLs that correspond to those files.</p>
|
|
4
|
+
<p>Visulima upload can also resize, crop and autorotate uploaded images.</p>
|
|
5
|
+
<p>Visulima upload includes S3-based, Azure-based, GCS-based and local filesystem-based backends and you may supply others.</p>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<br />
|
|
9
|
+
|
|
10
|
+
<div align="center">
|
|
11
|
+
|
|
12
|
+
[![typescript-image]][typescript-url] [![npm-image]][npm-url] [![license-image]][license-url]
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div align="center">
|
|
17
|
+
<sub>Built with ❤︎ by <a href="https://twitter.com/_prisis_">Daniel Bannert</a></sub>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- **Multiple Upload Handlers**: Multipart (form-based), REST (direct binary), and TUS (resumable) uploads
|
|
23
|
+
- **Chunked Uploads**: REST handler supports client-side chunked uploads for large files
|
|
24
|
+
- **Automatic Retry**: Built-in retry mechanism with exponential backoff for all storage backends (S3, Azure, GCS, Vercel Blob, Netlify Blob)
|
|
25
|
+
- Parent directories are created automatically as needed (like S3 and Azure)
|
|
26
|
+
- Content types are inferred from file extensions (like the filesystem)
|
|
27
|
+
- Files are by default marked as readable via the web (like a filesystem + web server)
|
|
28
|
+
- Images can be automatically scaled to multiple sizes
|
|
29
|
+
- Images can be cropped
|
|
30
|
+
- Images are automatically rotated if necessary for proper display on the web (i.e. iPhone photos with rotation hints are right side up)
|
|
31
|
+
- Image width, image height and correct file extension are made available to the developer
|
|
32
|
+
- Non-image files are also supported
|
|
33
|
+
- Web access to files can be disabled and reenabled
|
|
34
|
+
- GIF is supported, including animation, with full support for scaling and cropping
|
|
35
|
+
- Batch operations: Delete multiple files in a single request
|
|
36
|
+
- On fire about minimizing file sizes for your resized images? You can plug in `imagemin` and compatible tools using the `postprocessors` option.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
npm install @visulima/upload
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
yarn add @visulima/upload
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
pnpm add @visulima/upload
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Install requirements peer storage
|
|
53
|
+
|
|
54
|
+
### AWS S3
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
npm install @aws-sdk/client-s3 @aws-sdk/credential-providers @aws-sdk/s3-request-presigner @aws-sdk/signature-v4-crt aws-crt @aws-sdk/types
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
yarn add @aws-sdk/client-s3 @aws-sdk/credential-providers @aws-sdk/s3-request-presigner @aws-sdk/signature-v4-crt aws-crt @aws-sdk/types
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
pnpm add @aws-sdk/client-s3 @aws-sdk/credential-providers @aws-sdk/s3-request-presigner @aws-sdk/signature-v4-crt aws-crt @aws-sdk/types
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Azure Blob Storage
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
npm install @azure/storage-blob
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
yarn add @azure/storage-blob
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
pnpm add @azure/storage-blob
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
> Note: If you hit this error: "TypeError: Expected signal to be an instanceof AbortSignal" [#784](https://github.com/node-fetch/node-fetch/issues/784) you need to install node-fetch 4.0.0-beta or higher.
|
|
83
|
+
|
|
84
|
+
### Google Cloud Storage
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
npm install @google-cloud/storage node-fetch gaxios
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
yarn add @google-cloud/storage node-fetch gaxios
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
pnpm add @google-cloud/storage node-fetch gaxios
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Caching
|
|
99
|
+
|
|
100
|
+
The storage package supports caching to improve performance and reduce API calls. You provide your own cache implementation that follows the simple `Cache` interface.
|
|
101
|
+
|
|
102
|
+
### LRU Cache
|
|
103
|
+
|
|
104
|
+
Use the built-in LRU cache for simple in-memory caching:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import { LRUCache } from "lru-cache";
|
|
108
|
+
import { DiskStorage } from "@visulima/storage";
|
|
109
|
+
|
|
110
|
+
const cache = new LRUCache({
|
|
111
|
+
max: 1000, // Maximum number of items
|
|
112
|
+
ttl: 3600000, // 1 hour in milliseconds
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const storage = new DiskStorage({
|
|
116
|
+
directory: "/uploads",
|
|
117
|
+
cache,
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Custom Cache Implementation
|
|
122
|
+
|
|
123
|
+
Implement the `Cache` interface for any cache provider:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
import { DiskStorage, type Cache } from "@visulima/storage";
|
|
127
|
+
import { Redis } from "ioredis";
|
|
128
|
+
|
|
129
|
+
// Custom Redis cache implementation
|
|
130
|
+
class RedisCache implements Cache<string, any> {
|
|
131
|
+
constructor(private redis: Redis) {}
|
|
132
|
+
|
|
133
|
+
async get(key: string): Promise<any | undefined> {
|
|
134
|
+
const value = await this.redis.get(key);
|
|
135
|
+
return value ? JSON.parse(value) : undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async set(key: string, value: any): Promise<boolean> {
|
|
139
|
+
await this.redis.set(key, JSON.stringify(value));
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async delete(key: string): Promise<boolean> {
|
|
144
|
+
await this.redis.del(key);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async clear(): Promise<void> {
|
|
149
|
+
await this.redis.flushall();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async has(key: string): Promise<boolean> {
|
|
153
|
+
const exists = await this.redis.exists(key);
|
|
154
|
+
return exists === 1;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const redis = new Redis();
|
|
159
|
+
const cache = new RedisCache(redis);
|
|
160
|
+
|
|
161
|
+
const storage = new DiskStorage({
|
|
162
|
+
directory: "/uploads",
|
|
163
|
+
cache,
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### BentoCache Integration
|
|
168
|
+
|
|
169
|
+
For advanced multi-tier caching, use BentoCache with the adapter:
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
import { BentoCache, bentostore } from "bentocache";
|
|
173
|
+
import { memoryDriver } from "bentocache/drivers/memory";
|
|
174
|
+
import { redisDriver } from "bentocache/drivers/redis";
|
|
175
|
+
import { BentoCacheAdapter } from "@visulima/storage/utils/cache";
|
|
176
|
+
|
|
177
|
+
const bento = new BentoCache({
|
|
178
|
+
default: "storage",
|
|
179
|
+
stores: {
|
|
180
|
+
storage: bentostore()
|
|
181
|
+
.useL1Layer(memoryDriver({ maxSize: "10mb" }))
|
|
182
|
+
.useL2Layer(
|
|
183
|
+
redisDriver({
|
|
184
|
+
connection: { host: "127.0.0.1", port: 6379 },
|
|
185
|
+
}),
|
|
186
|
+
),
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const cache = new BentoCacheAdapter({
|
|
191
|
+
bento,
|
|
192
|
+
namespace: "storage",
|
|
193
|
+
defaultTtl: 3600000, // 1 hour
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const storage = new DiskStorage({
|
|
197
|
+
directory: "/uploads",
|
|
198
|
+
cache,
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Transformer Caching
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
import { MediaTransformer } from "@visulima/upload/transformer";
|
|
206
|
+
import ImageTransformer from "@visulima/upload/transformer/image";
|
|
207
|
+
import VideoTransformer from "@visulima/upload/transformer/video";
|
|
208
|
+
import { LRUCache } from "lru-cache";
|
|
209
|
+
|
|
210
|
+
const transformer = new MediaTransformer(storage, {
|
|
211
|
+
cache: new LRUCache({ max: 100, ttl: 3600000 }),
|
|
212
|
+
ImageTransformer: ImageTransformer,
|
|
213
|
+
VideoTransformer: VideoTransformer,
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Cache Interface
|
|
218
|
+
|
|
219
|
+
Any cache implementation must implement this interface:
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
interface Cache<K = string, V = any> {
|
|
223
|
+
get(key: K): V | undefined | Promise<V | undefined>;
|
|
224
|
+
set(key: K, value: V, options?: { ttl?: number }): boolean | Promise<boolean>;
|
|
225
|
+
delete(key: K): boolean | Promise<boolean>;
|
|
226
|
+
clear(): void | Promise<void>;
|
|
227
|
+
has(key: K): boolean | Promise<boolean>;
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
For more information, see the [BentoCache documentation](https://bentocache.dev/docs/introduction).
|
|
232
|
+
|
|
233
|
+
## Retry Mechanism
|
|
234
|
+
|
|
235
|
+
All storage backends (AWS S3, Azure, GCS, Vercel Blob, Netlify Blob) include automatic retry logic for transient failures. This ensures your uploads and file operations are resilient to network issues, rate limits, and temporary service unavailability.
|
|
236
|
+
|
|
237
|
+
### Default Behavior
|
|
238
|
+
|
|
239
|
+
By default, storage operations will retry up to 3 times with exponential backoff:
|
|
240
|
+
|
|
241
|
+
- Initial delay: 1 second
|
|
242
|
+
- Backoff multiplier: 2x (delays double each retry: 1s → 2s → 4s)
|
|
243
|
+
- Maximum delay: 30 seconds
|
|
244
|
+
- Retryable status codes: 408, 429, 500, 502, 503, 504
|
|
245
|
+
|
|
246
|
+
### Basic Configuration
|
|
247
|
+
|
|
248
|
+
Configure retry behavior when creating your storage instance:
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
import { S3Storage } from "@visulima/storage";
|
|
252
|
+
|
|
253
|
+
const storage = new S3Storage({
|
|
254
|
+
bucket: "my-bucket",
|
|
255
|
+
region: "us-east-1",
|
|
256
|
+
retryConfig: {
|
|
257
|
+
maxRetries: 5,
|
|
258
|
+
initialDelay: 2000,
|
|
259
|
+
backoffMultiplier: 1.5,
|
|
260
|
+
maxDelay: 60_000,
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Advanced Configuration
|
|
266
|
+
|
|
267
|
+
Customize retry logic with backend-specific error detection:
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
import { AzureStorage } from "@visulima/storage";
|
|
271
|
+
|
|
272
|
+
const storage = new AzureStorage({
|
|
273
|
+
containerName: "uploads",
|
|
274
|
+
accountName: "myaccount",
|
|
275
|
+
accountKey: "mykey",
|
|
276
|
+
retryConfig: {
|
|
277
|
+
maxRetries: 3,
|
|
278
|
+
initialDelay: 1000,
|
|
279
|
+
backoffMultiplier: 2,
|
|
280
|
+
maxDelay: 30_000,
|
|
281
|
+
retryableStatusCodes: [408, 429, 500, 502, 503, 504],
|
|
282
|
+
shouldRetry: (error: unknown) => {
|
|
283
|
+
// Custom retry logic
|
|
284
|
+
if (error instanceof Error) {
|
|
285
|
+
const errorCode = (error as any).code;
|
|
286
|
+
|
|
287
|
+
// Retry on network errors
|
|
288
|
+
if (errorCode === "ECONNRESET" || errorCode === "ETIMEDOUT") {
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Retry on specific HTTP status codes
|
|
294
|
+
if ((error as any).statusCode && [429, 503].includes((error as any).statusCode)) {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return false;
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Retry Configuration Options
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
interface RetryConfig {
|
|
308
|
+
/** Maximum number of retry attempts (default: 3) */
|
|
309
|
+
maxRetries?: number;
|
|
310
|
+
|
|
311
|
+
/** Initial delay in milliseconds before first retry (default: 1000) */
|
|
312
|
+
initialDelay?: number;
|
|
313
|
+
|
|
314
|
+
/** Multiplier for exponential backoff (default: 2) */
|
|
315
|
+
backoffMultiplier?: number;
|
|
316
|
+
|
|
317
|
+
/** Maximum delay in milliseconds between retries (default: 30000) */
|
|
318
|
+
maxDelay?: number;
|
|
319
|
+
|
|
320
|
+
/** HTTP status codes that should trigger a retry (default: [408, 429, 500, 502, 503, 504]) */
|
|
321
|
+
retryableStatusCodes?: number[];
|
|
322
|
+
|
|
323
|
+
/** Custom function to determine if an error should be retried */
|
|
324
|
+
shouldRetry?: (error: unknown) => boolean;
|
|
325
|
+
|
|
326
|
+
/** Custom function to calculate delay for a specific retry attempt */
|
|
327
|
+
calculateDelay?: (attempt: number, error: unknown) => number | undefined;
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Using Retry Utilities Directly
|
|
332
|
+
|
|
333
|
+
You can also use the retry utilities for custom operations:
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
import { retry, createRetryWrapper, isRetryableError } from "@visulima/storage";
|
|
337
|
+
|
|
338
|
+
// One-off retry
|
|
339
|
+
const result = await retry(
|
|
340
|
+
async () => {
|
|
341
|
+
// Your operation here
|
|
342
|
+
return await someOperation();
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
maxRetries: 3,
|
|
346
|
+
initialDelay: 1000,
|
|
347
|
+
},
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
// Create a reusable retry wrapper
|
|
351
|
+
const retryWrapper = createRetryWrapper({
|
|
352
|
+
maxRetries: 5,
|
|
353
|
+
initialDelay: 2000,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const result = await retryWrapper(async () => {
|
|
357
|
+
return await someOperation();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// Check if an error is retryable
|
|
361
|
+
if (isRetryableError(error)) {
|
|
362
|
+
// Handle retryable error
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Supported Error Types
|
|
367
|
+
|
|
368
|
+
The retry mechanism automatically handles:
|
|
369
|
+
|
|
370
|
+
- **Network errors**: `ECONNRESET`, `ETIMEDOUT`, `ENOTFOUND`, `ECONNREFUSED`, `EAI_AGAIN`
|
|
371
|
+
- **AWS SDK errors**: Server faults, retryable status codes, SDK v2/v3 error formats
|
|
372
|
+
- **Azure Storage errors**: HTTP status codes, network connection issues
|
|
373
|
+
- **HTTP errors**: 408 (Request Timeout), 429 (Too Many Requests), 5xx (Server Errors)
|
|
374
|
+
|
|
375
|
+
### Examples by Storage Backend
|
|
376
|
+
|
|
377
|
+
#### AWS S3
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
import { S3Storage } from "@visulima/storage";
|
|
381
|
+
|
|
382
|
+
const storage = new S3Storage({
|
|
383
|
+
bucket: "my-bucket",
|
|
384
|
+
region: "us-east-1",
|
|
385
|
+
retryConfig: {
|
|
386
|
+
maxRetries: 3,
|
|
387
|
+
// AWS SDK errors are automatically detected
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
#### Azure Blob Storage
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
import { AzureStorage } from "@visulima/storage";
|
|
396
|
+
|
|
397
|
+
const storage = new AzureStorage({
|
|
398
|
+
containerName: "uploads",
|
|
399
|
+
accountName: "myaccount",
|
|
400
|
+
accountKey: "mykey",
|
|
401
|
+
retryConfig: {
|
|
402
|
+
maxRetries: 5,
|
|
403
|
+
initialDelay: 2000,
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
#### Google Cloud Storage
|
|
409
|
+
|
|
410
|
+
GCS already has built-in retry support via `gaxios`. The retry mechanism works alongside GCS's native retry logic:
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
import { GCStorage } from "@visulima/storage";
|
|
414
|
+
|
|
415
|
+
const storage = new GCStorage({
|
|
416
|
+
bucket: "my-bucket",
|
|
417
|
+
projectId: "my-project",
|
|
418
|
+
retryConfig: {
|
|
419
|
+
maxRetries: 3,
|
|
420
|
+
// Works with GCS's existing retryOptions
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
#### Vercel Blob
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
import { VercelBlobStorage } from "@visulima/storage";
|
|
429
|
+
|
|
430
|
+
const storage = new VercelBlobStorage({
|
|
431
|
+
token: process.env.BLOB_READ_WRITE_TOKEN,
|
|
432
|
+
retryConfig: {
|
|
433
|
+
maxRetries: 3,
|
|
434
|
+
retryableStatusCodes: [408, 429, 500, 502, 503, 504],
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
#### Netlify Blob
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
import { NetlifyBlobStorage } from "@visulima/storage";
|
|
443
|
+
|
|
444
|
+
const storage = new NetlifyBlobStorage({
|
|
445
|
+
storeName: "uploads",
|
|
446
|
+
retryConfig: {
|
|
447
|
+
maxRetries: 3,
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
## Chunked Uploads
|
|
453
|
+
|
|
454
|
+
The REST handler supports client-side chunked uploads for large files. This allows you to upload files in smaller pieces, reducing memory usage and enabling resumable uploads.
|
|
455
|
+
|
|
456
|
+
### Initializing a Chunked Upload
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
import { Rest } from "@visulima/upload/handler/rest";
|
|
460
|
+
|
|
461
|
+
const rest = new Rest({ storage });
|
|
462
|
+
|
|
463
|
+
// Initialize chunked upload
|
|
464
|
+
const initResponse = await fetch("/files", {
|
|
465
|
+
method: "POST",
|
|
466
|
+
headers: {
|
|
467
|
+
"X-Chunked-Upload": "true",
|
|
468
|
+
"X-Total-Size": "10485760", // Total file size in bytes
|
|
469
|
+
"Content-Length": "0",
|
|
470
|
+
"Content-Type": "application/octet-stream",
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const { id } = await initResponse.json();
|
|
475
|
+
// id is the upload session ID
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### Uploading Chunks
|
|
479
|
+
|
|
480
|
+
```typescript
|
|
481
|
+
// Upload chunk 1 (bytes 0-524288)
|
|
482
|
+
await fetch(`/files/${id}`, {
|
|
483
|
+
method: "PATCH",
|
|
484
|
+
headers: {
|
|
485
|
+
"X-Chunk-Offset": "0",
|
|
486
|
+
"Content-Length": "524288",
|
|
487
|
+
"Content-Type": "application/octet-stream",
|
|
488
|
+
},
|
|
489
|
+
body: chunk1,
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// Upload chunk 2 (bytes 524288-1048576) - can be out of order
|
|
493
|
+
await fetch(`/files/${id}`, {
|
|
494
|
+
method: "PATCH",
|
|
495
|
+
headers: {
|
|
496
|
+
"X-Chunk-Offset": "524288",
|
|
497
|
+
"Content-Length": "524288",
|
|
498
|
+
"Content-Type": "application/octet-stream",
|
|
499
|
+
},
|
|
500
|
+
body: chunk2,
|
|
501
|
+
});
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
### Checking Upload Progress
|
|
505
|
+
|
|
506
|
+
```typescript
|
|
507
|
+
// Check upload status
|
|
508
|
+
const statusResponse = await fetch(`/files/${id}`, {
|
|
509
|
+
method: "HEAD",
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const offset = statusResponse.headers.get("X-Upload-Offset");
|
|
513
|
+
const complete = statusResponse.headers.get("X-Upload-Complete");
|
|
514
|
+
const chunks = JSON.parse(statusResponse.headers.get("X-Received-Chunks") || "[]");
|
|
515
|
+
|
|
516
|
+
console.log(`Uploaded: ${offset} bytes, Complete: ${complete}`);
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### Features
|
|
520
|
+
|
|
521
|
+
- **Out-of-Order Chunks**: Chunks can be uploaded in any order
|
|
522
|
+
- **Idempotency**: Duplicate chunks are safely ignored
|
|
523
|
+
- **Resumable**: Check progress and resume from last uploaded chunk
|
|
524
|
+
- **Progress Tracking**: Real-time upload progress via HEAD requests
|
|
525
|
+
- **Chunk Size Limits**: Maximum 100MB per chunk (configurable)
|
|
526
|
+
|
|
527
|
+
### Response Headers
|
|
528
|
+
|
|
529
|
+
- `X-Upload-ID`: Upload session ID (returned on initialization)
|
|
530
|
+
- `X-Chunked-Upload`: Indicates chunked upload mode
|
|
531
|
+
- `X-Upload-Offset`: Current upload offset in bytes
|
|
532
|
+
- `X-Upload-Complete`: "true" when upload is complete, "false" otherwise
|
|
533
|
+
- `X-Received-Chunks`: JSON array of received chunks `[{ offset, length }]`
|
|
534
|
+
|
|
535
|
+
## Supported Node.js Versions
|
|
536
|
+
|
|
537
|
+
Libraries in this ecosystem make the best effort to track
|
|
538
|
+
[Node.js’ release schedule](https://github.com/nodejs/release#release-schedule). Here’s [a
|
|
539
|
+
post on why we think this is important](https://medium.com/the-node-js-collection/maintainers-should-consider-following-node-js-release-schedule-ab08ed4de71a).
|
|
540
|
+
|
|
541
|
+
## Contributing
|
|
542
|
+
|
|
543
|
+
If you would like to help take a look at the [list of issues](https://github.com/visulima/visulima/issues) and check our [Contributing](.github/CONTRIBUTING.md) guild.
|
|
544
|
+
|
|
545
|
+
> **Note:** please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms.
|
|
546
|
+
|
|
547
|
+
## Credits
|
|
548
|
+
|
|
549
|
+
- [Daniel Bannert](https://github.com/prisis)
|
|
550
|
+
- [All Contributors](https://github.com/visulima/visulima/graphs/contributors)
|
|
551
|
+
|
|
552
|
+
## License
|
|
553
|
+
|
|
554
|
+
The visulima uploads is open-sourced software licensed under the [MIT][license-url]
|
|
555
|
+
|
|
556
|
+
[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript
|
|
557
|
+
[typescript-url]: "typescript"
|
|
558
|
+
[license-image]: https://img.shields.io/npm/l/@visulima/upload?color=blueviolet&style=for-the-badge
|
|
559
|
+
[license-url]: LICENSE.md "license"
|
|
560
|
+
[npm-image]: https://img.shields.io/npm/v/@visulima/upload/latest.svg?style=for-the-badge&logo=npm
|
|
561
|
+
[npm-url]: https://www.npmjs.com/package/@visulima/upload/v/latest "npm"
|