tus_background_upload 0.1.0 copy "tus_background_upload: ^0.1.0" to clipboard
tus_background_upload: ^0.1.0 copied to clipboard

TUS resumable uploads that keep running while the app is backgrounded or the device is locked, over native transports (iOS background URLSession, Android WorkManager via background_downloader).

example/lib/main.dart

import 'dart:async';
import 'dart:io';

import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:tus_background_upload/tus_background_upload.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // One-time setup for long-running transfers: Android runs uploads as a
  // foreground service (lifts the ~9-minute WorkManager cap) behind this
  // notification; iOS gets an 8h URLSession resource timeout.
  await configureBackgroundUploads(
    notificationTitle: 'Uploading',
    notificationBody: 'Your file keeps uploading in the background',
  );

  runApp(const TusExampleApp());
}

class TusExampleApp extends StatelessWidget {
  const TusExampleApp({super.key});

  @override
  Widget build(BuildContext context) => MaterialApp(
    title: 'tus_background_upload example',
    theme: ThemeData(colorSchemeSeed: Colors.teal),
    home: const UploadScreen(),
  );
}

class UploadScreen extends StatefulWidget {
  const UploadScreen({super.key});

  @override
  State<UploadScreen> createState() => _UploadScreenState();
}

class _UploadScreenState extends State<UploadScreen> {
  // Public tusd demo server — fine for testing; uploads are public and
  // deleted after a day.
  final TextEditingController _endpointController = TextEditingController(
    text: 'https://tusd.tusdemo.net/files/',
  );

  File? _file;
  double _progress = 0;
  String _statusText = 'Pick a file to start';
  TusUploader? _tusUploader;
  Uri? _uploadUrl;

  bool get _isPaused => _tusUploader?.status == TusUploadStatus.paused;

  bool get _isUploading => _tusUploader?.status == TusUploadStatus.uploading;

  @override
  void dispose() {
    _endpointController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(title: const Text('tus_background_upload')),
    body: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: <Widget>[
          TextField(
            controller: _endpointController,
            decoration: const InputDecoration(
              labelText: 'TUS creation endpoint',
              border: OutlineInputBorder(),
            ),
          ),
          const SizedBox(height: 16),
          FilledButton.tonal(
            onPressed: _isUploading || _isPaused ? null : () => unawaited(_pickFile()),
            child: Text(_file == null ? 'Pick file' : _file!.uri.pathSegments.last),
          ),
          const SizedBox(height: 16),
          LinearProgressIndicator(value: _progress),
          const SizedBox(height: 8),
          Text(_statusText, textAlign: TextAlign.center),
          const SizedBox(height: 16),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: <Widget>[
              FilledButton(
                onPressed: _file == null || _isUploading || _isPaused ? null : () => unawaited(_upload()),
                child: const Text('Upload'),
              ),
              OutlinedButton(
                onPressed: _isUploading
                    ? () => unawaited(_pause())
                    : _isPaused
                    ? () => unawaited(_resume())
                    : null,
                child: Text(_isPaused ? 'Resume' : 'Pause'),
              ),
              OutlinedButton(
                onPressed: _isUploading || _isPaused ? () => unawaited(_cancel()) : null,
                child: const Text('Cancel'),
              ),
            ],
          ),
          const SizedBox(height: 24),
          const Text(
            'Tip: start a big upload, background the app or lock the device for '
            '10+ minutes — the transfer keeps running (watch the notification) '
            'and progress catches up when you return.',
            style: TextStyle(fontSize: 12, color: Colors.grey),
            textAlign: TextAlign.center,
          ),
        ],
      ),
    ),
  );

  Future<void> _cancel() async {
    await _tusUploader?.cancel();
    setState(() {});
  }

  Future<void> _pause() async {
    await _tusUploader?.pause();
    setState(() => _statusText = 'Paused — the server keeps the bytes it received');
  }

  Future<void> _pickFile() async {
    final FilePickerResult? filePickerResult = await FilePicker.platform.pickFiles();
    final String? filePath = filePickerResult?.files.single.path;

    if (filePath == null) {
      return;
    }

    setState(() {
      _file = File(filePath);
      _progress = 0;
      _statusText = 'Ready to upload';
    });
  }

  Future<void> _resume() async {
    final File? file = _file;
    final Uri? uploadUrl = _uploadUrl;

    if (file == null || uploadUrl == null) {
      return;
    }

    setState(() => _statusText = 'Resuming from the server offset…');
    await _tusUploader?.resume(
      endpoint: uploadUrl,
      headers: const <String, String>{},
      filePath: file.path,
      fileLength: await file.length(),
      onProgress: _onProgress,
    );
    setState(() {});
  }

  Future<void> _upload() async {
    final File? file = _file;

    if (file == null) {
      return;
    }

    try {
      // Android 13+: the upload notification (and the foreground-service
      // promotion that lifts the ~9-minute cap) needs this granted.
      await requestBackgroundUploadNotificationPermission();

      setState(() => _statusText = 'Creating upload…');
      final int fileLength = await file.length();

      // Step 1: TUS creation — returns the upload URL. Skip this when your
      // backend hands out ready-made upload URLs itself.
      _uploadUrl = await createTusUpload(
        creationEndpoint: Uri.parse(_endpointController.text),
        fileLength: fileLength,
        metadata: <String, String>{'filename': file.uri.pathSegments.last},
      );

      // Step 2: the transfer, on a native background-capable transport.
      _tusUploader = TusUploader(transport: BackgroundDownloaderTusTransport());
      setState(() => _statusText = 'Uploading…');

      await _tusUploader!.upload(
        endpoint: _uploadUrl!,
        headers: const <String, String>{},
        filePath: file.path,
        fileLength: fileLength,
        onProgress: _onProgress,
      );

      setState(() => _statusText = 'Done! Uploaded to $_uploadUrl');
    } on TusUploadCancelledException {
      setState(() {
        _progress = 0;
        _statusText = 'Cancelled';
      });
    } catch (error) {
      setState(() => _statusText = 'Failed: $error');
    }
  }

  void _onProgress(double progress) {
    if (!mounted) {
      return;
    }

    setState(() {
      _progress = progress;
      _statusText = 'Uploading… ${(progress * 100).toStringAsFixed(0)}%';
    });
  }
}
0
likes
150
points
0
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

TUS resumable uploads that keep running while the app is backgrounded or the device is locked, over native transports (iOS background URLSession, Android WorkManager via background_downloader).

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

background_downloader, flutter, http, path

More

Packages that depend on tus_background_upload