quantum_upload 1.0.0 copy "quantum_upload: ^1.0.0" to clipboard
quantum_upload: ^1.0.0 copied to clipboard

A pure Dart package for reliable chunked file uploads with automatic resume, persistent session tracking, integrity verification, and seamless recovery from network interruptions.

example/main.dart

// ignore_for_file: avoid_print
import 'dart:async';
import 'dart:io';

import 'package:quantum_upload/quantum_upload.dart';

// ─────────────────────────────────────────────────────────────────────────────
// Demo entry-point — run each example in sequence
// ─────────────────────────────────────────────────────────────────────────────

Future<void> main() async {
  // Create a temporary 10 MB dummy file to upload
  final tempFile = await _createDummyFile(10 * 1024 * 1024);

  print('╔══════════════════════════════════════════════════════════╗');
  print('║         quantum_upload  — Live Demo                    ║');
  print('╚══════════════════════════════════════════════════════════╝\n');

  await _example1_simplestUpload(tempFile.path);
  await _example2_advancedWithCallbacks(tempFile.path);
  await _example3_pauseAndResume(tempFile.path);
  await _example4_resumeAfterInterruption(tempFile.path);
  await _example5_progressStream(tempFile.path);

  await tempFile.delete();
  print('\n✅  All examples completed.');
}

// ═════════════════════════════════════════════════════════════════════════════
// Example 1 — Simplest possible one-liner
// ═════════════════════════════════════════════════════════════════════════════

/// The absolute minimum code needed to upload a file.
///
/// `Uploader.upload()` handles everything: chunking, session management,
/// retries, and clean-up.
Future<void> _example1_simplestUpload(String filePath) async {
  _banner('Example 1', 'Simplest one-liner upload');

  try {
    final result = await Uploader.upload(
      filePath: filePath,
      url: 'https://httpbin.org/post', // public echo server for testing
      chunkSize: 2 * 1024 * 1024, // 2 MB chunks
      headers: {
        'Authorization': 'Bearer demo-token-12345',
        'X-App-Version': '1.0.0',
      },
      onProgress: (pct) => _printProgress(pct),
    );

    print('\n');
    _success('Upload complete!');
    _info('Session    : ${result.sessionId}');
    _info('Chunks     : ${result.totalChunks}');
    _info('Size       : ${result.sizeMiB}');
    _info('Duration   : ${result.duration.inSeconds}s');
    _info('Speed      : ${result.speedMbps}');
    _info('Fresh      : ${result.wasFreshUpload}');
  } on UploadException catch (e) {
    _error('Upload failed: $e');
  }
  print('');
}

// ═════════════════════════════════════════════════════════════════════════════
// Example 2 — Advanced config with all callbacks
// ═════════════════════════════════════════════════════════════════════════════

/// Shows every available configuration option and lifecycle callback.
Future<void> _example2_advancedWithCallbacks(String filePath) async {
  _banner('Example 2', 'Advanced configuration with all callbacks');

  final config = UploadConfig(
    filePath: filePath,
    url: Uri.parse('https://httpbin.org/post'),
    chunkSize: 3 * 1024 * 1024,
    maxRetries: 5,
    retryDelay: const Duration(seconds: 2),
    headers: {
      'Authorization': 'Bearer your-token-here',
      'X-Client-Platform': Platform.operatingSystem,
      'X-App-Version': '1.0.0',
    },
    onProgress: (pct) => _printProgress(pct),
    onChunkRetry: (chunkIdx, attempt) {
      _warn('  ↩ Retrying chunk $chunkIdx (attempt $attempt)');
    },
    onComplete: (result) {
      _success('  Callback: onComplete → ${result.speedMbps}');
    },
    onError: (err) {
      _error('  Callback: onError → $err');
    },
  );

  final uploader = Uploader(config);

  // Listen to state transitions
  uploader.stateStream.listen((state) {
    _info('  State → ${state.label}');
  });

  try {
    final result = await uploader.start(storage: InMemorySessionStorage());
    print('');
    _success('Done in ${result.duration.inSeconds}s');
  } on UploadException catch (e) {
    _error('Failed: $e');
  }
  print('');
}

// ═════════════════════════════════════════════════════════════════════════════
// Example 3 — Pause and resume mid-upload
// ═════════════════════════════════════════════════════════════════════════════

/// Demonstrates the pause / resume control API.
///
/// A real app would trigger pause/resume from a UI button tap.
Future<void> _example3_pauseAndResume(String filePath) async {
  _banner('Example 3', 'Pause and resume mid-upload');

  final config = UploadConfig(
    filePath: filePath,
    url: Uri.parse('https://httpbin.org/post'),
    chunkSize: 1 * 1024 * 1024,
    maxRetries: 2,
    retryDelay: const Duration(milliseconds: 500),
    onProgress: (pct) => _printProgress(pct),
  );

  final uploader = Uploader(config);

  // Simulate: pause after 2 seconds, resume after 1 more second
  Timer(const Duration(seconds: 2), () {
    _warn('\n  ⏸  Pausing upload…');
    uploader.pause();

    Timer(const Duration(seconds: 1), () {
      _info('  ▶  Resuming upload…\n');
      uploader.resume();
    });
  });

  try {
    await uploader.start(storage: InMemorySessionStorage());
    print('');
    _success('Completed after pause/resume cycle.');
  } on UploadException catch (e) {
    _error('Failed: $e');
  }
  print('');
}

// ═════════════════════════════════════════════════════════════════════════════
// Example 4 — Resume after interruption across sessions
// ═════════════════════════════════════════════════════════════════════════════

/// Simulates saving the session ID after a partial upload, then resuming
/// it from where it stopped — even if the app had been closed.
Future<void> _example4_resumeAfterInterruption(String filePath) async {
  _banner('Example 4', 'Resume across sessions (app restart simulation)');

  // Use an in-memory store so we can inspect it
  final store = InMemorySessionStorage();
  const sessionId = 'my-persistent-session-id-v1';

  // ── First run: simulate interruption after a few chunks ──────────────────
  _info('  First run: upload until interrupted…');
  try {
    await Uploader.upload(
      filePath: filePath,
      url: 'https://httpbin.org/post',
      sessionId: sessionId,
      chunkSize: 2 * 1024 * 1024,
      storage: store,
      retryDelay: const Duration(milliseconds: 100),
      // Inject a custom HTTP client that fails after chunk 2 to simulate
      // a network drop.  In a real scenario, the app would simply be killed.
    );
  } on UploadException {
    // Ignore — we intentionally interrupted it above
  }

  _info('  Session "$sessionId" was saved. Pretend app restarted.\n');

  // ── Second run: resume ───────────────────────────────────────────────────
  _info('  Second run: resuming from saved session…');
  try {
    final result = await Uploader.upload(
      filePath: filePath,
      url: 'https://httpbin.org/post',
      sessionId: sessionId, // ← magic: pass the saved ID
      chunkSize: 2 * 1024 * 1024,
      storage: store,
      retryDelay: const Duration(milliseconds: 100),
      onProgress: (pct) => _printProgress(pct),
    );

    print('');
    _success('Resumed and completed!');
    _info('  Chunks uploaded this session : ${result.uploadedThisSession}');
    _info('  Total chunks                 : ${result.totalChunks}');
    _info('  Fraction resumed from prior  : '
        '${(result.resumedFraction * 100).toStringAsFixed(1)}%');
  } on UploadException catch (e) {
    _error('Resume failed: $e');
  }
  print('');
}

// ═════════════════════════════════════════════════════════════════════════════
// Example 5 — Progress stream with rich snapshots
// ═════════════════════════════════════════════════════════════════════════════

/// Uses the [Uploader.progressStream] to build a rich live display showing
/// upload speed, ETA, and uploaded/total sizes.
Future<void> _example5_progressStream(String filePath) async {
  _banner('Example 5', 'Rich progress via progressStream');

  final config = UploadConfig(
    filePath: filePath,
    url: Uri.parse('https://httpbin.org/post'),
    chunkSize: 1 * 1024 * 1024,
    maxRetries: 2,
    retryDelay: const Duration(milliseconds: 500),
  );

  final uploader = Uploader(config);

  // Subscribe to the rich snapshot stream
  final sub = uploader.progressStream.listen((snap) {
    stdout.write(
      '\r  ${_bar(snap.percent, width: 25)} '
      '${snap.percent.toStringAsFixed(1).padLeft(5)}%  '
      '${snap.uploadedFormatted.padLeft(10)} / ${snap.totalFormatted}  '
      '@ ${snap.speedMbps.padLeft(12)}  '
      'ETA ${snap.etaFormatted}  ',
    );
  });

  try {
    await uploader.start(storage: InMemorySessionStorage());
    await sub.cancel();
    stdout.writeln('\n');
    _success('Completed with rich stream!');
  } on UploadException catch (e) {
    await sub.cancel();
    _error('\nFailed: $e');
  }
  print('');
}

// ─────────────────────────────────────────────────────────────────────────────
// Terminal helpers
// ─────────────────────────────────────────────────────────────────────────────

void _banner(String id, String title) {
  print('─' * 60);
  print('  $id — $title');
  print('─' * 60);
}

void _success(String msg) => print('\x1B[32m✓  $msg\x1B[0m');
void _info(String msg)    => print('\x1B[36m   $msg\x1B[0m');
void _warn(String msg)    => print('\x1B[33m$msg\x1B[0m');
void _error(String msg)   => print('\x1B[31m✗  $msg\x1B[0m');

void _printProgress(double pct) {
  stdout.write('\r  ${_bar(pct)} ${pct.toStringAsFixed(1).padLeft(5)}%  ');
}

String _bar(double pct, {int width = 20}) {
  final filled = (pct / 100 * width).round().clamp(0, width);
  return '[' + '█' * filled + '░' * (width - filled) + ']';
}

// ─────────────────────────────────────────────────────────────────────────────
// Dummy file generator
// ─────────────────────────────────────────────────────────────────────────────

Future<File> _createDummyFile(int sizeBytes) async {
  final tmp = await Directory.systemTemp.createTemp('chunked_demo_');
  final file = File('${tmp.path}/dummy.bin');
  final sink = file.openWrite();
  const chunkSize = 64 * 1024;
  final chunk = List.filled(chunkSize, 0x42); // 'B' bytes

  int written = 0;
  while (written < sizeBytes) {
    final toWrite = (sizeBytes - written).clamp(0, chunkSize);
    sink.add(chunk.sublist(0, toWrite));
    written += toWrite;
  }
  await sink.flush();
  await sink.close();

  _info('Created dummy file: ${file.path} (${sizeBytes ~/ (1024 * 1024)} MiB)');
  return file;
}
0
likes
150
points
6
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A pure Dart package for reliable chunked file uploads with automatic resume, persistent session tracking, integrity verification, and seamless recovery from network interruptions.

Repository (GitHub)
View/report issues

Topics

#upload #file #chunked #resumable #network

License

MIT (license)

Dependencies

crypto, http, path, shared_preferences, uuid

More

Packages that depend on quantum_upload