quantum_upload πŸ“¦

pub version Dart SDK License: MIT Test coverage

Upload large files over unreliable networks β€” reliably.
Splits. Persists. Resumes. Works with any HTTP server.


The Problem

You're uploading a 2 GB video. The network drops at 99%.
You start again from zero. You cry.

The Solution

quantum_upload splits the file into small pieces, confirms each one with the server, and saves progress locally. When the network drops, it picks up from the last confirmed piece β€” not from the beginning.


Features

Feature Detail
βœ‚οΈ Smart chunking Configurable chunk size (default 5 MiB); last chunk gets the remainder
πŸ’Ύ Persistent sessions Survives app kill + device restart via SharedPreferences
πŸ”„ True resume Skips already-confirmed chunks β€” zero redundant uploads
πŸ” Exponential back-off delay_n = baseDelay Γ— 2^(nβˆ’1), capped at 30 s
πŸ” MD5 integrity check Every chunk is hashed before sending
πŸ“Š Rich progress Percent, EMA speed, ETA, uploaded/total formatted
⏸️ Pause / resume / cancel Full lifecycle control between chunks
🌐 Server-agnostic Standard HTTP multipart POST β€” no special protocol needed
πŸ”Œ Pluggable storage Replace SharedPreferences with any SessionStorage
πŸ§ͺ 100 % unit-tested Every layer tested in isolation with a mock HTTP client

Installation

dependencies:
  quantum_upload: ^1.0.0
dart pub get

Quick Start

import 'package:quantum_upload/quantum_upload.dart';

final result = await Uploader.upload(
  filePath: '/path/to/video.mp4',
  url: 'https://api.example.com/upload',
  headers: {'Authorization': 'Bearer $token'},
  onProgress: (pct) => print('${pct.toStringAsFixed(1)} %'),
);

print('Done in ${result.duration.inSeconds}s @ ${result.speedMbps}');

Advanced Usage

Full configuration

final config = UploadConfig(
  filePath   : '/path/to/video.mp4',
  url        : Uri.parse('https://api.example.com/upload'),
  chunkSize  : 10 * 1024 * 1024,          // 10 MiB chunks
  maxRetries : 5,
  retryDelay : const Duration(seconds: 2), // 2 s β†’ 4 s β†’ 8 s β†’ …
  headers    : {'Authorization': 'Bearer $token'},
  sessionId  : savedSessionId,             // null = fresh upload
  onProgress   : (pct)          => setState(() => _progress = pct),
  onChunkRetry : (chunk, attempt) => print('Retrying chunk $chunk ($attempt)'),
  onComplete   : (result)        => print('Done! ${result.speedMbps}'),
  onError      : (err)           => showErrorDialog(err.message),
);

final uploader = Uploader(config);
await uploader.start();

Pause and resume

final uploader = Uploader(config);
unawaited(uploader.start());   // fire and forget in background

// From UI buttons:
uploader.pause();              // stops after current chunk finishes
uploader.resume();             // continues immediately
await uploader.cancel();       // permanent β€” cleans up session

Cross-session resume (app restart)

// ── Run 1 β€” save the session ID ─────────────────────────────────
final uploader = Uploader(config);
await prefs.setString('uploadSession', uploader.sessionId);
await uploader.start();

// ── Run 2 β€” resume automatically ────────────────────────────────
await Uploader.upload(
  filePath  : '/path/to/video.mp4',
  url       : 'https://api.example.com/upload',
  sessionId : prefs.getString('uploadSession'),  // ← magic
);

Rich progress stream

uploader.progressStream.listen((snap) {
  print(
    '[${snap.percent.toStringAsFixed(1).padLeft(5)}%]  '
    '${snap.uploadedFormatted} / ${snap.totalFormatted}  '
    '@ ${snap.speedMbps}  ETA ${snap.etaFormatted}',
  );
});

Sample output:

[ 12.5%]    12.50 MiB /  100.00 MiB  @  8.32 MB/s  ETA 10:38
[ 25.0%]    25.00 MiB /  100.00 MiB  @  9.01 MB/s  ETA 08:21

How It Works

Uploader.start()
β”‚
β”œβ”€ ChunkManager.initialize()
β”‚   └─ compute chunk boundaries: [(0, 5M), (5M, 10M), …]
β”‚
β”œβ”€ UploadSession.restore(sessionId) or create fresh
β”‚   └─ persisted in SharedPreferences as JSON
β”‚
└─ for each pending chunk:
    β”œβ”€ readChunk(i)           β€” RandomAccessFile.setPosition + read
    β”œβ”€ computeChecksum(data)  β€” MD5 hex
    β”œβ”€ RetryPolicy.executeChunk(() => UploadRequest.send(…))
    β”‚   └─ on success: markChunkCompleted(i) + session.save()
    β”‚   └─ on fail:    exponential back-off β†’ retry
    └─ progressTracker.addBytes(chunkSize)

β†’ UploadResult  (session deleted from storage)

HTTP Wire Format

Every chunk is a standard multipart/form-data POST:

POST /upload
Content-Type: multipart/form-data; boundary=…

X-Session-Id:     c3d4e5f6-…
X-Chunk-Index:    3
X-Total-Chunks:   40
X-Chunk-Checksum: a3f4b2c1d5e6…   (MD5 hex)
X-File-Size:      209715200
Content-Range:    bytes 15728640-20971519/209715200
Authorization:    Bearer <your-token>   (from config.headers)

--boundary
Content-Disposition: form-data; name="chunk"; filename="chunk_3"

<raw 5 MiB bytes>
--boundary--

See example/server_example/server.js for a Node.js reference implementation.


Package Architecture

lib/
β”œβ”€β”€ quantum_upload.dart       ← barrel (one import for everything)
└── src/
    β”œβ”€β”€ uploader.dart           ← main orchestrator β€” start/pause/resume/cancel
    β”œβ”€β”€ chunk_manager.dart      ← file splitting + chunk reading + MD5
    β”œβ”€β”€ upload_session.dart     ← persistent per-chunk state
    β”œβ”€β”€ upload_request.dart     ← HTTP multipart builder + sender
    β”œβ”€β”€ retry_policy.dart       ← exponential back-off executor
    β”œβ”€β”€ progress_tracker.dart   ← EMA speed, ETA, stream emitter
    β”œβ”€β”€ models/
    β”‚   β”œβ”€β”€ upload_config.dart  ← immutable configuration value object
    β”‚   β”œβ”€β”€ upload_state.dart   ← enum: idle|uploading|paused|…|completed
    β”‚   β”œβ”€β”€ chunk_info.dart     ← byte-range + state + checksum per chunk
    β”‚   └── upload_result.dart  ← final summary (speed, chunks, duration)
    β”œβ”€β”€ storage/
    β”‚   β”œβ”€β”€ session_storage.dart        ← abstract interface
    β”‚   └── shared_prefs_storage.dart   ← default implementation
    └── exceptions/
        β”œβ”€β”€ upload_exception.dart   ← base class
        β”œβ”€β”€ chunk_exception.dart    ← per-chunk failure (HTTP status, body)
        └── session_exception.dart  ← storage failure

Comparison

Feature quantum_upload flutter_upchunk tus_client_dart
Pure Dart (no Flutter dep.) βœ… βœ… βœ…
Resume after app restart βœ… ❌ βœ…
Works with any HTTP server βœ… βœ… ❌ requires tus server
MD5 checksum per chunk βœ… ❌ ❌
Exponential back-off βœ… ❌ ❌
Pause / resume βœ… βœ… βœ…
Progress stream (speed + ETA) βœ… ❌ ❌
Pluggable session storage βœ… ❌ ❌
4xx vs 5xx retry distinction βœ… ❌ ❌
Unit test coverage βœ… 100 % partial partial

Tests

dart test
dart test --coverage=coverage
dart pub global run coverage:format_coverage --lcov \
    --in=coverage --out=lcov.info --report-on=lib

Test suite covers:

  • chunk_manager_test.dart β€” boundary computation, byte reading, checksums
  • upload_session_test.dart β€” persistence, restore, mutation
  • retry_policy_test.dart β€” should-retry logic, delay formula, executors
  • progress_tracker_test.dart β€” EMA speed, ETA, stream emissions
  • uploader_test.dart β€” end-to-end with MockHttpClient

Contributing

  1. Fork the repo and create a feature branch.
  2. Write tests for every new behaviour.
  3. Run dart analyze && dart test β€” both must pass.
  4. Submit a pull request with a clear description.

License

MIT Β© 2026 β€” see LICENSE.

Libraries

quantum_upload
A pure Dart library for resumable, chunked file uploads over HTTP.