pion_resumable_upload
Resumable, fault-tolerant chunked file uploads for Dart and Flutter that
speak the resumable.js wire protocol, so
they work against a Laravel
pion/laravel-chunk-upload
backend without any server changes.
If your web client already uploads with resumable.js to a Laravel chunk-upload endpoint, this package lets a Dart/Flutter client hit the same endpoint with a byte-for-byte compatible payload.
Features
- π§© Protocol parity β emits the exact
resumableChunkNumber,resumableTotalChunks,resumableIdentifier, β¦ fields theResumableJSUploadHandlerexpects. - πͺΆ Low memory β reads each chunk with a sliding window over
RandomAccessFile; a multi-gigabyte file never lands in RAM. - β‘ Concurrent β uploads several chunks at once (configurable).
- π Resumable β a
GETpre-flight skips chunks already on the server; chunks purged by the backend cleanup cron are re-uploaded automatically. - π‘οΈ Resilient β exponential backoff with jitter, with transient vs. permanent error classification.
- β―οΈ Controllable β pause, resume and cancel, with an interruptible backoff.
- πΎ Persistable β plug in any store (
Hive,sqflite, a file, β¦) to resume uploads after an app restart. - π Stateless auth β static headers and/or a per-request token provider so expiring JWTs can be refreshed mid-upload.
Supported platforms
Android, iOS, Windows, macOS and Linux. The package uses dart:io
(RandomAccessFile) for memory-efficient reads, so the web is not supported.
Installation
dependencies:
pion_resumable_upload: ^0.1.0
dart pub add pion_resumable_upload
Quick start
import 'dart:io';
import 'package:pion_resumable_upload/pion_resumable_upload.dart';
Future<void> main() async {
final client = UploadClient(
config: UploadConfig(
uploadUrl: Uri.parse('https://api.example.com/upload'),
),
);
final task = client.createTask(File('/path/to/large_video.mp4'));
task.progress.listen((p) {
print('${p.percentage.toStringAsFixed(1)}% '
'(${p.completedChunks}/${p.totalChunks} chunks) β ${p.state.name}');
});
try {
await task.start(); // completes when the whole file is uploaded
print('Done!');
} on ResumableUploadException catch (e) {
print('Upload failed: $e');
} finally {
await client.close();
}
}
Pause, resume and cancel
final task = client.createTask(file);
final done = task.start();
task.pause(); // stops scheduling, aborts in-flight chunks & backoff
await task.resume(); // continues from where it left off
task.cancel(); // aborts; `done` completes with UploadCancelledException
await done;
start()/resume() return a future that completes on success and completes
with an error on failure or cancellation. Pausing does not settle it β
resuming carries on toward the terminal state.
Configuration
| Option | Default | Description |
|---|---|---|
uploadUrl |
required | Endpoint for both the GET pre-flight and the chunk POST. |
chunkSize |
1048576 (1 MiB) |
Target chunk size in bytes. |
forceChunkSize |
false |
false: the last chunk absorbs the remainder (resumable.js behaviour). true: every chunk is exactly chunkSize. |
simultaneousUploads |
3 |
Max chunks uploaded concurrently. |
testChunks |
true |
Send a GET pre-flight to skip chunks already present. |
uploadMethod |
POST |
POST, PUT or PATCH. |
fileParameterName |
file |
Multipart field name for the binary chunk. |
maxChunkRetries |
5 |
Retry attempts per chunk. |
baseRetryDelay / maxRetryDelay / maxRetryJitter |
1s / 30s / 500ms |
Backoff parameters. |
headers / authHeaderProvider |
{} / null |
Static and per-request headers. |
extraParameters |
{} |
Extra fields added to every request. |
successStatuses |
[200, 201, 202] |
Status codes meaning "accepted / already present". |
permanentErrorStatuses |
[400, 404, 415, 500, 501] |
Non-retryable status codes. |
identifierGenerator |
MD5 of size-name |
Strategy for resumableIdentifier. |
filenameSanitizer |
strip + truncate β€100 | Strategy for resumableFilename. |
Authentication (JWT / bearer)
Because the backend is typically a stateless API, pass a token via headers. Use
authHeaderProvider so a token that expires during a long upload is refreshed
per request:
UploadConfig(
uploadUrl: Uri.parse('https://api.example.com/upload'),
authHeaderProvider: () async => {
'Authorization': 'Bearer ${await tokenStore.freshAccessToken()}',
},
);
Resuming across app restarts
The default store keeps progress in memory only. Implement UploadStateStore
(backed by Hive, sqflite, a file, β¦) and pass it to the client to survive a
full restart:
class MyStore implements UploadStateStore {
// load / save / markChunkComplete / removeChunk / delete
}
final client = UploadClient(config: config, store: MyStore());
Recreate the task for the same file after a restart and call start(); with
testChunks enabled, every chunk is re-verified, so already-uploaded chunks are
skipped and any purged ones are re-uploaded.
Custom transport
Swap the HTTP layer (e.g. to add Dio interceptors or logging) by implementing
UploadTransport, or pass a pre-configured Dio to DioUploadTransport:
final client = UploadClient(
config: config,
transport: DioUploadTransport(config: config, dio: myDio),
);
Laravel backend setup
Install the package and route a controller to a FileReceiver with the
ResumableJSUploadHandler. For mobile clients, also:
- Disable session locking (concurrent chunks otherwise serialize): use a
custom handler whose
canUseSession()returnsfalse, registered under theoverridekey inconfig/chunk-upload.php. - Keep
chunkSizeunderpost_max_size/upload_max_filesizeinphp.ini. The 1 MiB default is safe almost everywhere. - This package already truncates long file names to avoid the ext4 255-byte limit, but applying the same guard server-side is good defence in depth.
See the pion/laravel-chunk-upload wiki for full backend configuration.
How it works
- Chunk offset:
offset = (chunkNumber - 1) Γ chunkSize. - Chunk count:
ceil(size / chunkSize)whenforceChunkSizeistrue, otherwisemax(1, floor(size / chunkSize))(so the last chunk absorbs the remainder, matching resumable.js). - Backoff:
delay(n) = min(maxDelay, baseDelay Γ 2βΏ) + jitter.
Example
See example/ for a small Flutter app that picks a file and shows
upload progress with pause / resume / cancel controls.
License
Libraries
- pion_resumable_upload
- Resumable, fault-tolerant chunked file uploads for Dart.