Flutter web:如何上传大文件?

Posted

技术标签:

【中文标题】Flutter web:如何上传大文件?【英文标题】:Flutter web: How to Upload a Large File? 【发布时间】:2020-12-17 22:48:34 【问题描述】:

有没有办法将大文件上传到服务器?

我正在使用 MultipartRequestMultipartFile 之类的:

  List<int> fileBytes) async 
  var request = new http.MultipartRequest("POST", Uri.parse(url));
  request.files.add(http.MultipartFile.fromBytes(
    'file',
    fileBytes,
    contentType: MediaType('application', 'octet-stream'),
    filename: fileName));
  request.headers.addAll(headers);
  var streamedResponse = await request.send();
  return await http.Response.fromStream(streamedResponse);

并像这样读取文件:

    html.InputElement uploadInput = html.FileUploadInputElement();
    uploadInput.multiple = false;
    uploadInput.draggable = true;
    uploadInput.click();

    uploadInput.onChange.listen((e) 
      final files = uploadInput.files;
      final file = files[0];

      final reader = new html.FileReader();

      reader.onLoadEnd.listen((e) 
        setState(() 
          _bytesData =
              Base64Decoder().convert(reader.result.toString().split(",").last);
          _selectedFile = _bytesData;
        );
      );

      reader.readAsDataUrl(file);
    );

30 MB 左右的文件可以,但不止于此,我收到了Error code: Out of Memory

我做错了吗?我在某个地方看到了

MultipartFile.fromBytes 会给你一些关于更大文件的问题,因为浏览器会限制你的内存消耗。

我认为他的解决方案是:

有一个 fromStream 构造函数。通常,对于较大的文件,我只使用 HttpRequest,并将 File 对象放在 FormData 实例中。

我使用了MultipartFileMultipartFile.fromString,而且两次(对于 150 MB 文件)都再次发生。 我该如何使用这个解决方案?或者对于超过 500 MB 的文件有更好的方法吗?

更新

使用Worker 添加了答案。这不是一个很好的解决方案,但我认为这可能会对某人有所帮助。

【问题讨论】:

确实,不要使用 fromBytes 命名构造函数,因为它需要 500 MB 字节缓冲区 - 而是使用其他 constructors 我使用了 MultipartFileMultipartFile.fromString 并且两次(对于 150 MB 文件)再次发生。这就是为什么我认为我在这里做错了什么。对于fromString,我可以使用reader.result.toString() 对吗? 然后试试pub.dev/documentation/http/latest/http/MultipartFile/… 不适用于网络。 您找到解决方案了吗? 【参考方案1】:

目前,我使用这种方法解决了这个问题:

进口:

import 'package:universal_html/html.dart' as html;

颤振部分:

class Upload extends StatefulWidget 
  @override
  _UploadState createState() => _UploadState();


class _UploadState extends State<Upload> 
  html.Worker myWorker;
  html.File file;

  _uploadFile() async 
    String _uri = "/upload";

    myWorker.postMessage("file": file, "uri": _uri);
  

  _selectFile() 
    html.InputElement uploadInput = html.FileUploadInputElement();
    uploadInput.multiple = false;
    uploadInput.click();

    uploadInput.onChange.listen((e) 
      file = uploadInput.files.first;
    );
  

  @override
  void initState() 
    myWorker = new html.Worker('upload_worker.js');
    myWorker.onMessage.listen((e) 
      setState(() 
        //progressbar,...
      );
    );

    super.initState();
  

  @override
  Widget build(BuildContext context) 
    return Column(
      children: [
        RaisedButton(
          onPressed: _selectFile(),
          child: Text("Select File"),
        ),
        RaisedButton(
          onPressed: _uploadFile(),
          child: Text("Upload"),
        ),
      ],
    );
  

javascript 部分:

在 web 文件夹中(index.html 旁边),创建文件 'upload_worker.js' 。

self.addEventListener('message', async (event) => 
    var file = event.data.file;
    var url = event.data.uri;
    uploadFile(file, url);
);

function uploadFile(file, url) 
    var xhr = new XMLHttpRequest();
    var formdata = new FormData();
    var uploadPercent;

    formdata.append('file', file);

    xhr.upload.addEventListener('progress', function (e) 
        //Use this if you want to have a progress bar
        if (e.lengthComputable) 
            uploadPercent = Math.floor((e.loaded / e.total) * 100);
            postMessage(uploadPercent);
        
    , false);
    xhr.onreadystatechange = function () 
        if (xhr.readyState == XMLHttpRequest.DONE) 
            postMessage("done");
        
    
    xhr.onerror = function () 
        // only triggers if the request couldn't be made at all
        postMessage("Request failed");
    ;

    xhr.open('POST', url, true);

    xhr.send(formdata);

【讨论】:

我很高兴它有帮助。【参考方案2】:

我只使用 Dart 代码解决了这个问题:要走的路是使用块上传器。 这意味着手动发送文件的小部分。例如,我每个请求发送 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!")

我的版本中的颤振块上传器:

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);

希望这能让您很好地了解我是如何解决问题的。

【讨论】:

以上是关于Flutter web:如何上传大文件?的主要内容,如果未能解决你的问题,请参考以下文章

asp.net(c#)如何上传大文件?

JAVA WEB项目大文件上传下载组件

asp.net(c#)如何上传大文件?

java web 大文件上传下载

求php怎么实现web端上传超大文件

Web大文件上传断点续传解决方案