Another TUS Client

Pub Version
Platforms

A Dart client for resumable file uploads using the TUS protocol.
Forked from tus_client_dart.

tus is an HTTP‑based protocol for resumable file uploads.
It enables interruption (intentional or accidental) and later resumption of uploads without needing to restart from the beginning.


Table of Contents

Usage Examples

1. Creating a Client

import 'package:another_tus_client/another_tus_client.dart';
import 'package:cross_file/cross_file.dart';

final file = XFile("/path/to/my/pic.jpg");
final client = TusClient(
  file,  // Must be an XFile
  store: TusMemoryStore(), // Will not persist through device restarts. For persistent URL storage in memory see below
  maxChunkSize: 6 * 1024 * 1024, // 6MB chunks
  retries: 5,
  retryScale: RetryScale.exponential,
  retryInterval: 2,
  debug: true, // Will debug both the store and the client
);

2. Starting an Upload

await client.upload(
  uri: Uri.parse("https://your-tus-server.com/files/"),
  onStart: (TusClient client, Duration? estimate) {
    print("Upload started; estimated time: ${estimate?.inSeconds} seconds");
  },
  onProgress: (double progress, Duration estimate) {
    print("Progress: ${progress.toStringAsFixed(1)}%, estimated time: ${estimate.inSeconds} seconds");
  },
  onComplete: () {
    print("Upload complete!");
    print("File URL: ${client.uploadUrl}");
  },
  headers: {"Authorization": "Bearer your_token"},
  metadata: {"cacheControl": "3600"},
  measureUploadSpeed: true,
  preventDuplicates: true, // NEW: Prevents creating duplicate uploads of the same file
);

3. Pausing an Upload

Upload will pause after the current chunk finishes. For example:

print("Pausing upload...");
await client.pauseUpload();

4. Resuming an Upload

If the upload has been paused, you can resume using:

// Resume with the same callbacks as original upload
await client.resumeUpload();

// Resume with a new progress callback
await client.resumeUpload(
  onProgress: (progress, estimate) {
    print("New progress handler: $progress%");
  }
);

// Clear the progress callback while keeping others
await client.resumeUpload(
  clearProgressCallback: true
);

// Replace some callbacks and clear others
await client.resumeUpload(
  onComplete: () => print("New completion handler"),
  clearProgressCallback: true
);

// Clear all callbacks
await client.clearAllCallbacks();
await client.resumeUpload();

5. Canceling an Upload

Cancel the current upload and remove any saved state:

final result = await client.cancelUpload(); // Returns true when successful
if (result) {
  print("Upload canceled successfully.");
}

6. Using TusFileStore (Native Platforms)

On mobile/desktop, you can persist the upload progress to the file system.

import 'package:path_provider/path_provider.dart';
import 'dart:io';

final tempDir = await getTemporaryDirectory();
final tempDirectory = Directory('${tempDir.path}/${file.name}_uploads');
if (!tempDirectory.existsSync()) {
  tempDirectory.createSync(recursive: true);
}

final client = TusClient(
  file,
  store: TusFileStore(tempDirectory),
);
await client.upload(uri: Uri.parse("https://your-tus-server.com/files/"));

7. Using TusIndexedDBStore (Web)

For web applications, use IndexedDB for persistent upload state:

import 'package:flutter/foundation.dart' show kIsWeb;

final store = kIsWeb ? TusIndexedDBStore() : TusMemoryStore();

final client = TusClient(
  file,
  store: store,
);
await client.upload(uri: Uri.parse("https://your-tus-server.com/files/"));

8. File Selection on Web

For web applications, you have two main options for handling files:

Option 1: Using Any XFile with Loaded Bytes

import 'package:file_picker/file_picker.dart';

final result = await FilePicker.platform.pickFiles(
    withData: true, // Load bytes into memory. Works for small files
);

if (result == null) {
    return null;
}

final fileWithBytes = result.files.first.xFile; // This returns an XFile with bytes

// Create client with any XFile that has bytes loaded
final client = TusClient(
  fileWithBytes,  // Any XFile with bytes already loaded
  store: TusMemoryStore(), //This TusMemoryStore doesn't persist on reboots.
);

await client.upload(uri: Uri.parse("https://tus.example.com/files"));

Option 2: Using pickWebFilesForUpload()

This is a built-in method that will open a file picker on web and convert the files to a streamable XFile using Blob.

final result = await pickWebFilesForUpload(
    allowMultiple: true,
    acceptedFileTypes: ['*']  
)

if (result == null) {
    return null;
}

// Create client with any XFile that has bytes loaded
final client = TusClient(
  result.first,  // Streaming ready XFile
  store: TusMemoryStore(), 
);

await client.upload(uri: Uri.parse("https://tus.example.com/files"));

9. Using TusUploadManager

The TusUploadManager provides a convenient way to manage multiple uploads with features like automatic queuing, status tracking, and batch operations.

Creating the Upload Manager

import 'package:another_tus_client/another_tus_client.dart';
import 'package:flutter/foundation.dart' show kIsWeb;

// Create a persistent store for upload state
final store = kIsWeb ? TusIndexedDBStore() : TusFileStore(await getUploadDirectory());

// Initialize the manager
final uploadManager = TusUploadManager(
  serverUrl: Uri.parse("https://your-tus-server.com/files/"),
  store: store,
  maxConcurrentUploads: 3,  // Control how many uploads run simultaneously
  autoStart: true,           // Start uploads as soon as they're added (default)
  measureUploadSpeed: true,  // Estimate upload time
  retries: 3,                // Retry failed uploads 3 times
  retryScale: RetryScale.exponential,
  retryInterval: 2,          // Wait 2, 4, 8 seconds between retries
  preventDuplicates: true,   // Prevent creating duplicate uploads
  debug: true, // Will debug manager, client and store
);

Adding Files to Upload

// Add a single file. Returns a custom ID for this upload
final uploadId1 = await uploadManager.addUpload(
  file1,
  metadata: {
    "bucketName": "user_files",
    "cacheControl": "3600",
    "contentType": file1.mimeType ?? "application/octet-stream"
  },
  headers: {
    "x-custom-header": "value"  // Add upload-specific headers
  }
);

// Add another file with different settings
final uploadId2 = await uploadManager.addUpload(
  file2,
  metadata: {"bucketName": "images"}
);

Listening to Upload Events

// Listen to upload status changes and progress updates
uploadManager.uploadEvents.listen((UploadEvent event) {
  final upload = event.upload;
  final type = event.eventType;
  print("Upload ID: ${upload.id}");
  print("Event: ${type}"); // start, resume, pause, progress, complete, error, cancel, add
  print("Status: ${upload.status}");  // ready, uploading, paused, completed, failed, cancelled
  print("Progress: ${upload.progress}%");
  print("Estimated time: ${upload.estimate.inSeconds} seconds");
  
  if (upload.status == UploadStatus.completed) {
    print("Upload URL: ${upload.client.uploadUrl}");
  } else if (upload.status == UploadStatus.failed) {
    print("Error: ${upload.error}");
  }
});

Controlling Individual Uploads

// Pause a specific upload
await uploadManager.pauseUpload(uploadId1);

// Resume a paused upload
await uploadManager.resumeUpload(uploadId1);

// Cancel an upload
await uploadManager.cancelUpload(uploadId2);

// Check for specific upload
final upload = uploadManager.getUpload(uploadId1);
if (upload != null && upload.status == UploadStatus.paused) {
  // Check if it's resumable
  final isResumable = await upload.client.isResumable();
  print("Can resume: $isResumable");
}

Batch Operations

// Pause all active uploads
await uploadManager.pauseAll();

// Resume all paused uploads
await uploadManager.resumeAll();

// Cancel all uploads
await uploadManager.cancelAll();

// Get all uploads
final allUploads = uploadManager.getAllUploads();
print("Total uploads: ${allUploads.length}");

Cleanup

// Clean up resources when done
@override
void dispose() {
  uploadManager.dispose();
  super.dispose();
}

Maintainers