chunkedUpload method

  1. @override
Future<Response> chunkedUpload({
  1. required String path,
  2. required Map<String, dynamic> params,
  3. required String paramName,
  4. required String idParamName,
  5. required Map<String, String> headers,
  6. dynamic onProgress(
    1. UploadProgress
    )?,
})
override

Upload a file in chunks.

Implementation

@override
Future<Response> chunkedUpload({
  required String path,
  required Map<String, dynamic> params,
  required String paramName,
  required String idParamName,
  required Map<String, String> headers,
  Function(UploadProgress)? onProgress,
}) async {
  InputFile file = params[paramName];
  if (file.path == null && file.bytes == null) {
    throw AppwriteException("File path or bytes must be provided");
  }

  int size = 0;
  if (file.bytes != null) {
    size = file.bytes!.length;
  }

  File? iofile;

  if (file.path != null) {
    iofile = File(file.path!);
    size = await iofile.length();
  }

  late Response res;
  if (size <= chunkSize) {
    if (file.path != null) {
      params[paramName] = await http.MultipartFile.fromPath(
        paramName,
        file.path!,
        filename: file.filename,
      );
    } else {
      params[paramName] = http.MultipartFile.fromBytes(
        paramName,
        file.bytes!,
        filename: file.filename,
      );
    }
    return call(
      HttpMethod.post,
      path: path,
      params: params,
      headers: headers,
    );
  }

  var offset = 0;
  String? uploadId;
  if (idParamName.isNotEmpty) {
    //make a request to check if a file already exists
    try {
      res = await call(
        HttpMethod.get,
        path: '$path/${params[idParamName]}',
        headers: headers,
      );
      final int chunksUploaded = res.data['chunksUploaded'] as int;
      offset = chunksUploaded * chunkSize;
      uploadId = res.data['\$id'] ?? params[idParamName]?.toString();
    } on AppwriteException catch (_) {}
  }

  if (offset >= size) {
    return res;
  }

  final totalChunks = (size / chunkSize).ceil();

  Future<Response> uploadChunk(int index, int start, int end, String? id,
      [RandomAccessFile? raf]) async {
    List<int> chunk = [];
    if (file.bytes != null) {
      chunk = file.bytes!.getRange(start, end).toList();
    } else {
      if (raf != null) {
        await raf.setPosition(start);
        chunk = await raf.read(end - start);
      } else {
        final chunkFile = await iofile!.open(mode: FileMode.read);
        try {
          await chunkFile.setPosition(start);
          chunk = await chunkFile.read(end - start);
        } finally {
          await chunkFile.close();
        }
      }
    }

    final chunkParams = Map<String, dynamic>.from(params);
    chunkParams[paramName] = http.MultipartFile.fromBytes(
      paramName,
      chunk,
      filename: file.filename,
    );
    final chunkHeaders = Map<String, String>.from(headers);
    if (id != null && id.isNotEmpty) {
      chunkHeaders['x-appwrite-id'] = id;
    }
    chunkHeaders['content-range'] = 'bytes $start-${end - 1}/$size';

    return call(
      HttpMethod.post,
      path: path,
      headers: chunkHeaders,
      params: chunkParams,
    );
  }

  final firstStart = offset;
  final firstEnd = min(firstStart + chunkSize, size);
  final firstIndex = firstStart ~/ chunkSize;
  res = await uploadChunk(firstIndex, firstStart, firstEnd, uploadId);
  uploadId = res.data['\$id'] ?? uploadId;

  var completedChunks = firstIndex + 1;
  var uploadedBytes = firstEnd;
  var lastResponse = res;
  Response? finalResponse;

  bool isUploadComplete(Response response) {
    final chunksUploaded = response.data['chunksUploaded'];
    final chunksTotal = response.data['chunksTotal'] ?? totalChunks;
    return chunksUploaded is num &&
        chunksTotal is num &&
        chunksUploaded >= chunksTotal;
  }

  final progress = UploadProgress(
    $id: uploadId ?? '',
    progress: min(uploadedBytes, size) / size * 100,
    sizeUploaded: min(uploadedBytes, size),
    chunksTotal: totalChunks,
    chunksUploaded: completedChunks,
  );
  onProgress?.call(progress);

  final chunks = <Map<String, int>>[];
  for (var start = firstEnd; start < size; start += chunkSize) {
    final end = min(start + chunkSize, size);
    chunks.add({
      'index': start ~/ chunkSize,
      'start': start,
      'end': end,
    });
  }

  var nextChunk = 0;
  Future<void> uploadNext() async {
    final raf =
        file.bytes == null ? await iofile!.open(mode: FileMode.read) : null;
    try {
      while (nextChunk < chunks.length) {
        final chunk = chunks[nextChunk++];
        final chunkResponse = await uploadChunk(
          chunk['index']!,
          chunk['start']!,
          chunk['end']!,
          uploadId,
          raf,
        );
        completedChunks++;
        uploadedBytes += chunk['end']! - chunk['start']!;
        lastResponse = chunkResponse;
        if (isUploadComplete(chunkResponse)) {
          finalResponse = chunkResponse;
        }

        final progress = UploadProgress(
          $id: uploadId ?? '',
          progress: min(uploadedBytes, size) / size * 100,
          sizeUploaded: min(uploadedBytes, size),
          chunksTotal: totalChunks,
          chunksUploaded: completedChunks,
        );
        onProgress?.call(progress);
      }
    } finally {
      await raf?.close();
    }
  }

  final concurrency = min(8, chunks.length);
  await Future.wait(List.generate(concurrency, (_) => uploadNext()));

  return finalResponse ?? lastResponse;
}