如何在 flutter web 中上传大文件?

How to upload large files in flutter web?

已经有一篇关于如何选择文件的精彩讨论帖:

如果我使用 file_picker,我最终会得到 List<PlatformFile>? files。 我将 withReadStream: true 传递给 pickFiles 方法,所以我得到了一个流。 到目前为止,这是我的代码:

List<PlatformFile>? files = fileUploadView.result?.files;
for (PlatformFile file in files!) {


//-----add selected file with request
request.files.add(http.MultipartFile(
    "Your parameter name on server side",  file.readStream!,
    file.size,
    filename: file.name));
}

//-------Send request
var resp = await request.send();

但是如果我 运行 它每隔几秒就会发生错误:

RangeError: Array buffer allocation failed
    at new ArrayBuffer (<anonymous>)
    at new Uint8Array (<anonymous>)
    at Function._create1 (http://localhost:54372/dart_sdk.js:32192:14)
    at Function.new (http://localhost:54372/dart_sdk.js:32155:49)

几周后我可以post回答:方法是使用块上传器。 这意味着手动发送文件的小部分。例如,我为每个请求发送 99MB。 网上已经有这个的基本实现: https://pub.dev/packages/chunked_uploader

您必须获得流,这可以通过 file_picker 或 drop_zone 库实现。我使用了 drop_zone 库,因为它提供了文件选择器和拖放区功能。在我的代码中,dynamic file 对象来自 drop_zone 库。

也许您需要根据您的后端调整块上传器功能。我使用 Django 后端,我在其中编写了一个保存文件的简单视图。在小文件的情况下,它可以接收包含多个文件的多部分请求,在大文件的情况下,它可以接收块并在收到前一个块时继续写入文件。 这是我的部分代码:

Python 后端:

@api_view(["POST"])
def upload(request):
    basePath = config.get("BasePath")
    
    targetFolder = os.path.join(basePath, request.data["taskId"], "input")
    if not os.path.exists(targetFolder):
        os.makedirs(targetFolder)

    for count, file in enumerate(request.FILES.getlist("Your parameter name on server side")):
        path = os.path.join(targetFolder, file.name)
        print(path)
        with open(path, 'ab') as destination:
            for chunk in file.chunks():
                destination.write(chunk)

    return HttpResponse("File(s) uploaded!")

我的版本中的 flutter 块上传器:

import 'dart:async';
import 'dart:html';
import 'dart:math';
import 'package:dio/dio.dart';
import 'package:flutter_dropzone/flutter_dropzone.dart';
import 'package:http/http.dart' as http;

class UploadRequest {
  final Dio dio;
  final String url;
  final String method;
  final String fileKey;
  final Map<String, String>? bodyData;
  final Map<String, String>? headers;
  final CancelToken? cancelToken;
  final dynamic file;
  final Function(double)? onUploadProgress;
  late final int _maxChunkSize;
  int fileSize;
  String fileName;
  late DropzoneViewController controller;

  UploadRequest(
    this.dio, {
    required this.url,
    this.method = "POST",
    this.fileKey = "file",
    this.bodyData = const {},
    this.cancelToken,
    required this.file,
    this.onUploadProgress,
    int maxChunkSize = 1024 * 1024 * 99,
    required this.controller,
    required this.fileSize,
    required this.fileName,
    this.headers
  }) {
    _maxChunkSize = min(fileSize, maxChunkSize);
  }

  Future<Response?> upload() async {
    Response? finalResponse;
    for (int i = 0; i < _chunksCount; i++) {
      final start = _getChunkStart(i);
      print("start is $start");
      final end = _getChunkEnd(i);
      final chunkStream = _getChunkStream(start, end);
      
      
      var request = http.MultipartRequest(
        "POST",
        Uri.parse(url),
      );

      //request.headers.addAll(_getHeaders(start, end));
      request.headers.addAll(headers!);

      //-----add other fields if needed
      request.fields.addAll(bodyData!);

      request.files.add(http.MultipartFile(
        "Your parameter name on server side",
        chunkStream,
        fileSize,
        filename: fileName// + i.toString(),
        )
      );


      //-------Send request
      var resp = await request.send();

      //------Read response
      String result = await resp.stream.bytesToString();

      //-------Your response
      print(result);

      
    }
    return finalResponse;
  }

  Stream<List<int>> _getChunkStream(int start, int end) async* {
    print("reading from $start to $end");
    final reader = FileReader();
    final blob = file.slice(start, end);
    reader.readAsArrayBuffer(blob);
    await reader.onLoad.first;
    yield reader.result as List<int>;
  }


  // Updating total upload progress
  _updateProgress(int chunkIndex, int chunkCurrent, int chunkTotal) {
    int totalUploadedSize = (chunkIndex * _maxChunkSize) + chunkCurrent;
    double totalUploadProgress = totalUploadedSize / fileSize;
    this.onUploadProgress?.call(totalUploadProgress);
  }

  // Returning start byte offset of current chunk
  int _getChunkStart(int chunkIndex) => chunkIndex * _maxChunkSize;

  // Returning end byte offset of current chunk
  int _getChunkEnd(int chunkIndex) =>
      min((chunkIndex + 1) * _maxChunkSize, fileSize);

  // Returning a header map object containing Content-Range
  // https://tools.ietf.org/html/rfc7233#section-2
  Map<String, String> _getHeaders(int start, int end) {
    var header = {'Content-Range': 'bytes $start-${end - 1}/$fileSize'};
    if (headers != null) {
      header.addAll(headers!);
    }
    return header;
  }

  // Returning chunks count based on file size and maximum chunk size
  int get _chunksCount {
    var result = (fileSize / _maxChunkSize).ceil();
    return result;
  }
}

    

决定是在一个请求中上传多个文件还是一个文件分成多个请求的上传代码:

//upload the large files

Map<String, String> headers = {
  'Authorization': requester.loginToken!
};

fileUploadView.droppedFiles.sort((a, b) => b.size - a.size);

//calculate the sum of teh files:

double sumInMb = 0;
int divideBy = 1000000;

for (UploadableFile file in fileUploadView.droppedFiles) {
    sumInMb += file.size / divideBy;
}

var dio = Dio();

int uploadedAlready = 0;
for (UploadableFile file in fileUploadView.droppedFiles) {

  if (sumInMb < 99) {
    break;
  }

  var uploadRequest = UploadRequest(
    dio,
    url: requester.backendApi+ "/upload",
    file: file.file,
    controller: fileUploadView.controller!,
    fileSize: file.size,
    fileName: file.name,
    headers: headers,
    bodyData: {
      "taskId": taskId.toString(),
      "user": requester.username!,
    },
  );

  await uploadRequest.upload();

  uploadedAlready++;
  sumInMb -= file.size / divideBy;
}

if (uploadedAlready > 0) {
  fileUploadView.droppedFiles.removeRange(0, uploadedAlready);
}

print("large files uploaded");

// upload the small files

//---Create http package multipart request object
var request = http.MultipartRequest(
  "POST",
  Uri.parse(requester.backendApi+ "/upload"),
);


request.headers.addAll(headers);

//-----add other fields if needed
request.fields["taskId"] = taskId.toString();

print("adding files selected with drop zone");
for (UploadableFile file in fileUploadView.droppedFiles) {

  Stream<List<int>>? stream = fileUploadView.controller?.getFileStream(file.file);

  print("sending " + file.name);

  request.files.add(http.MultipartFile(
      "Your parameter name on server side",
      stream!,
      file.size,
      filename: file.name));
}


//-------Send request
var resp = await request.send();

//------Read response
String result = await resp.stream.bytesToString();

//-------Your response
print(result);

希望这能让您对我如何解决问题有一个很好的了解。