flutter 振颤腹板:如何上传大文件?

hs1ihplo  于 2023-04-13  发布在  Flutter
关注(0)|答案(2)|浏览(257)

有办法把大文件上传到服务器吗?
我正在使用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);
    });

对于30MB左右的文件是可以的,但是对于超过30MB的文件,我得到的是Error code: Out of Memory .

我做错什么了吗?我在某个地方看到
MultipartFile.fromBytes会给予你一些大文件的问题,因为浏览器会限制你的内存消耗。
我认为他的解决方案是:
有一个fromStream构造函数。通常,对于较大的文件,我只使用HttpRequest,并将File对象放在FormData示例中。
我使用MultipartFileMultipartFile.fromString,两次(150 MB文件)都再次发生。我如何使用此解决方案?或者对于超过500 MB的文件,是否有更好的方法?

更新

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

ar7v8xwq

ar7v8xwq1#

更新

here提供了一个项目,用于展示如何使用此解决方案和进度指示器完成此任务。
目前,我使用这种方法解决了这个问题:
导入:

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

Flutter部位:

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);
}
6qqygrtg

6qqygrtg2#

我只用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!")

flutter chunk uploader在我的版本:

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

希望这能给你一个很好的概述我是如何解决这个问题的。

相关问题