Skip to content

Commit bebdf0a

Browse files
committed
Implement progressiveUrlSaving for parallel uploads
1 parent 8d18ffd commit bebdf0a

File tree

4 files changed

+99
-4
lines changed

4 files changed

+99
-4
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,22 @@ input.addEventListener('change', function (e) {
5555
})
5656
```
5757

58+
### Parallel Uploads with Progressive URL Saving
59+
60+
For better fault tolerance with parallel uploads, you can enable progressive URL saving:
61+
62+
```js
63+
var upload = new tus.Upload(file, {
64+
endpoint: 'http://localhost:1080/files/',
65+
parallelUploads: 4,
66+
progressiveUrlSaving: true, // Save each partial upload URL immediately
67+
urlStorage: myThreadSafeStorage, // Your storage implementation
68+
// ... other options
69+
})
70+
```
71+
72+
When enabled, partial upload URLs are saved immediately as each completes, rather than waiting for all to finish. This improves resumability if failures occur during parallel uploads. See the [API documentation](docs/api.md#progressiveurlsaving) for implementation details.
73+
5874
## Documentation
5975

6076
- [Installation & Requirements](/docs/installation.md)

docs/api.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,47 @@ _Default value:_ `false`
248248

249249
A boolean indicating if the fingerprint in the URL storage will be removed once the upload is successfully completed. When this feature is enabled and the same file is uploaded again, it will create an entirely new upload instead of reusing the previous one. Furthermore, this option will only change behavior if `urlStorage` is not `null`.
250250

251+
#### progressiveUrlSaving
252+
253+
_Default value:_ `false`
254+
255+
A boolean indicating whether partial upload URLs should be saved progressively during parallel uploads. When `false` (default), all partial upload URLs must be successfully created before any are saved to storage. When `true`, each partial upload URL is saved immediately after its POST request succeeds.
256+
257+
This option only has an effect when `parallelUploads` is greater than 1. Enabling this provides better fault tolerance for parallel uploads:
258+
- If a browser crash or network failure occurs, successfully created partial uploads can still be resumed
259+
- Earlier persistence reduces the window of data loss
260+
- More granular progress tracking across sessions
261+
262+
When using this option, your `urlStorage` implementation should handle concurrent updates safely, especially if using a database backend. Consider using a mutex or other synchronization mechanism to prevent race conditions when multiple parallel uploads save their URLs simultaneously.
263+
264+
Example usage with a thread-safe storage implementation:
265+
```js
266+
import { Mutex } from 'async-mutex'
267+
268+
class ThreadSafeUrlStorage {
269+
constructor() {
270+
this.mutex = new Mutex()
271+
}
272+
273+
async addUpload(fingerprint, upload) {
274+
const release = await this.mutex.acquire()
275+
try {
276+
// Merge parallelUploadUrls arrays safely
277+
// Your database operations here
278+
} finally {
279+
release()
280+
}
281+
}
282+
}
283+
284+
const upload = new tus.Upload(file, {
285+
parallelUploads: 4,
286+
progressiveUrlSaving: true,
287+
urlStorage: new ThreadSafeUrlStorage(),
288+
// ... other options
289+
})
290+
```
291+
251292
#### uploadLengthDeferred
252293

253294
_Default value:_ `false`

lib/options.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export interface UploadOptions {
8585
parallelUploadBoundaries?: { start: number; end: number }[]
8686
storeFingerprintForResuming: boolean
8787
removeFingerprintOnSuccess: boolean
88+
progressiveUrlSaving: boolean
8889
uploadLengthDeferred: boolean
8990
uploadDataDuringCreation: boolean
9091

lib/upload.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export const defaultOptions = {
4747
parallelUploadBoundaries: undefined,
4848
storeFingerprintForResuming: true,
4949
removeFingerprintOnSuccess: false,
50+
progressiveUrlSaving: false,
5051
uploadLengthDeferred: false,
5152
uploadDataDuringCreation: false,
5253

@@ -361,10 +362,17 @@ export class BaseUpload {
361362
onUploadUrlAvailable: async () => {
362363
// @ts-expect-error We know that _parallelUploadUrls is defined
363364
this._parallelUploadUrls[index] = upload.url
364-
// Test if all uploads have received an URL
365-
// @ts-expect-error We know that _parallelUploadUrls is defined
366-
if (this._parallelUploadUrls.filter((u) => Boolean(u)).length === parts.length) {
367-
await this._saveUploadInUrlStorage()
365+
366+
// Progressive saving: save immediately when each URL becomes available
367+
// This allows for better fault tolerance and earlier persistence
368+
if (this.options.progressiveUrlSaving && upload.url) {
369+
await this._savePartialUploadUrl(index, upload.url)
370+
} else {
371+
// Legacy behavior: wait for all URLs before saving
372+
// @ts-expect-error We know that _parallelUploadUrls is defined
373+
if (this._parallelUploadUrls.filter((u) => Boolean(u)).length === parts.length) {
374+
await this._saveUploadInUrlStorage()
375+
}
368376
}
369377
},
370378
}
@@ -1010,6 +1018,35 @@ export class BaseUpload {
10101018
this._urlStorageKey = undefined
10111019
}
10121020

1021+
/**
1022+
* Save a single partial upload URL at the specified index.
1023+
* This is used for progressive URL saving during parallel uploads.
1024+
*
1025+
* @api private
1026+
*/
1027+
private async _savePartialUploadUrl(index: number, url: string): Promise<void> {
1028+
if (
1029+
!this.options.storeFingerprintForResuming ||
1030+
!this._fingerprint
1031+
) {
1032+
return
1033+
}
1034+
1035+
const storedUpload: PreviousUpload = {
1036+
size: this._size,
1037+
metadata: this.options.metadata,
1038+
creationTime: new Date().toString(),
1039+
urlStorageKey: this._fingerprint,
1040+
parallelUploadUrls: this._parallelUploadUrls,
1041+
}
1042+
1043+
// Save/update the upload with the current state of parallel URLs,
1044+
// The UrlStorage implementation must handle concurrent updates
1045+
// safely when using this method.
1046+
const urlStorageKey = await this.options.urlStorage.addUpload(this._fingerprint, storedUpload)
1047+
this._urlStorageKey = urlStorageKey
1048+
}
1049+
10131050
/**
10141051
* Add the upload URL to the URL storage, if possible.
10151052
*

0 commit comments

Comments
 (0)