useStorageFiles function

StorageFilesHandler<PlatformFile, String> useStorageFiles({
  1. required Reference reference,
  2. required FileType fileType,
  3. List<String>? allowedExtensions,
  4. bool generateUniqueFileName = false,
  5. SettableMetadata? settableMetadata,
})

Returns a StorageFileResult that can be used to first select a file and then upload that file to Firebase Storage.

This delegates work to FilePicker and can be managed by calling methods on StorageFileResult.fileResult.

StorageFileResult.fileResult.selectFile()

Once a file has been selected, to commit the file to Firebase Storage, you simply need to commit that file which will upload the file to Firebase.

When true, generateUniqueFileName will append a 5 character to the file name to ensure it is unique in the database. This may not be desirable however if you'd like the previous file to be deleted or overwritten.

Example:

'my coolPicture.jpg' -> '6a9h4_my_coolPicture.jpg'

The returned map contains platformFile keys with the addresses of the file in Firebase Storage as values.

Implementation

StorageFilesHandler<PlatformFile, String> useStorageFiles({
  required Reference reference,
  required FileType fileType,
  List<String>? allowedExtensions,

  /// If `true`, the file will have a 5 character hash appended to its name to
  /// ensure it is unique. This may not be desirable however if you'd like the
  /// previous file to be deleted or overwritten.
  ///
  /// Example:
  ///
  /// 'my coolPicture.jpg' -> '6a9h4_my_coolPicture.jpg'
  bool generateUniqueFileName = false,
  SettableMetadata? settableMetadata,
}) {
  final progresses = useState<Map<String, TaskSnapshot?>?>(null);
  final error = useState<Object?>(null);
  final stackTrace = useState<StackTrace?>(null);
  // `useRef` does not trigger a rebuild when the value changes.
  final streamSubscriptions = useRef<List<StreamSubscription>?>(null);
  final isMounted = useIsMounted();
  final fileHandler = useFiles(
    fileType: fileType,
    allowedExtensions: allowedExtensions,
  );
  // The path of the file in FirebaseStorage.
  final returnMap = useState<Map<PlatformFile, String>?>(null);
  final _error = error.value;
  final _stackTrace = stackTrace.value;

  void clearError() {
    error.value = null;
    stackTrace.value = null;
  }

  // TODO: have this return a revert function.
  Future<CommitsHandler<String>> commit() async {
    final completer = Completer<CommitsHandler<String>>();
    if (streamSubscriptions.value != null) {
      completer.completeError('Stream upload already in progress');
    }

    clearError();

    final platformFiles = fileHandler.snapshot.data;

    if (platformFiles != null) {
      final bucketName = reference.bucket;
      progresses.value = Map.fromEntries(
        platformFiles.map(
          (file) {
            return MapEntry(
              file.name,
              null,
            );
          },
        ),
      );

      try {
        final uploadTasks = <UploadTask>[];

        for (var i = 0; i < platformFiles.length; i++) {
          final platformFile = platformFiles[i];
          final rawFileName = platformFile.name;
          final uniqueKey = UniqueKey();
          var fileName =
              path.basename(rawFileName).replaceAll(RegExp(r'\s+'), '_');

          if (generateUniqueFileName) {
            fileName = '${shortHash(uniqueKey)}_$fileName';
          }

          final childReference = reference.child(fileName);
          final fileLocation = 'gs://$bucketName/${childReference.fullPath}';

          // Web must work off bytes, no filePath
          if (kIsWeb) {
            uploadTasks.add(
              childReference.putData(
                platformFile.bytes!,
                settableMetadata,
              ),
            );
          } else {
            final filePath = platformFile.path;

            uploadTasks.add(
              childReference.putFile(
                File(filePath!),
                settableMetadata,
              ),
            );
          }

          final uploadTask = uploadTasks[i];
          final _streamSubscriptions = streamSubscriptions.value;

          if (_streamSubscriptions != null) {
            _streamSubscriptions.add(
              uploadTask.snapshotEvents.listen(
                (TaskSnapshot snapshot) {
                  progresses.value = {
                    ...progresses.value!
                      ..update(
                        rawFileName,
                        (taskSnapshot) => taskSnapshot,
                        ifAbsent: () {
                          throw StateError('No file $rawFileName');
                        },
                      ),
                  };
                },
                onError: (Object _error, StackTrace _stackTrace) {
                  completer.completeError(_error, _stackTrace);
                  error.value = _error;
                  stackTrace.value = _stackTrace;
                },
                onDone: () {
                  returnMap.value ??= returnMap.value = Map.fromEntries(
                    platformFiles.map(
                      (platformFile) => MapEntry(platformFile, ''),
                    ),
                  );

                  returnMap.value = {
                    ...returnMap.value!
                      ..update(
                        platformFile,
                        (key) => fileLocation,
                        ifAbsent: () {
                          throw StateError('No file $platformFile');
                        },
                      ),
                  };
                },
                cancelOnError: true,
              ),
            );
          }

          await Future.wait(uploadTasks);
          completer.complete(
            CommitsHandler(
              values: returnMap.value!.values.toList(),
              revert: () async {
                await Future.wait(
                  returnMap.value!.values.map(
                    (remoteFile) async {
                      final childReference = reference.child(fileName);

                      await childReference.delete();
                    },
                  ),
                );
              },
              retryFailed: () async {
                throw UnimplementedError();
              },
            ),
          );
        }
      } catch (_error, _stackTrace) {
        error.value = _error;
        stackTrace.value = _stackTrace;
      } finally {
        streamSubscriptions.value?.map((subscription) {
          subscription.cancel();
        });
        streamSubscriptions.value = null;

        if (isMounted()) {
          progresses.value = null;
        }
      }
    } else {
      completer.completeError(
        StateError(
          'Cannot commit a file that has not been selected yet.',
        ),
      );
    }

    return completer.future;
  }

  final _returnMap = returnMap.value;
  final hasAllFiles =
      _returnMap != null && _returnMap.values.every((value) => value != '');
  final _progresses = progresses.value;
  final AsyncSnapshotWithProgress<Map<PlatformFile, String>?,
      Map<String, TaskSnapshot?>> snapshot;

  if (_error != null && _stackTrace != null) {
    snapshot = AsyncSnapshotWithProgress.withError(
      _returnMap != null ? ConnectionState.done : ConnectionState.active,
      _error,
      _stackTrace,
    );
  } else if (hasAllFiles) {
    snapshot = AsyncSnapshotWithProgress.withData(
      ConnectionState.done,
      _returnMap,
    );
  } else if (_progresses != null) {
    snapshot = AsyncSnapshotWithProgress.waiting(
      _progresses,
    );
  } else {
    assert(_error == null && _stackTrace == null);
    snapshot = const AsyncSnapshotWithProgress.nothing();
  }

  return StorageFilesHandler(
    commit: commit,
    filesHandler: fileHandler,
    snapshot: snapshot,
  );
}