How to upload large files in flutter web? - flutter

There is already a great thread about how to pick files:
Article
I end up with List<PlatformFile>? files if I use file_picker.
I passed withReadStream: true to the pickFiles method so I get a stream.
Here is my code so far:
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();
But if I run it an error occours every few seconds:
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)

A few weeks later I am able to post an answer: The way to go is to use a chunk uploader.
This means to manually send the file in little parts. I send 99MB per request for example.
There is already a basic implementation of this online:
https://pub.dev/packages/chunked_uploader
You have to get a stream, this is possible with the file_picker or the drop_zone library. I used the drop_zone library because it provides the file picker and the drop zone functionality. In my code the dynamic file objects come from the drop_zone library.
Maybe you have to adjust the chunk uploader functionality depending one your backend. I use a django backend where I wrote a simple view that saves the files. In case of small files it can receive multipart requests with multiple files, in case of large files it can receive chunks and continiues to write a file if a previous chunk was received.
Here some parts of my code:
Python backend:
#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 in my version:
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 code that decides whether to upload multiple files in one request or one file divided to many requests:
//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);
Hopefully this gives you a good overview how I solved the problem.

Related

How to send multiple(as a list) files in restApi in flutter with single key from form-data

I want to send multiple Files(Images) in post api from flutter with single key from form-data.
Just like in the picture for "images[]" key i have multiple images to send.
var uri = Uri.parse("${baseUrl}/user/addPost");
https.MultipartRequest request = new https.MultipartRequest('POST', uri);
request.fields['user_id'] = "54";
request.fields['title'] = postAdModel.title!;
request.fields['type'] = postAdModel.type!;
request.fields['category_id'] = postAdModel.category_id.toString();
////////////////////////// Thumbnail Image Adding /////////////////
final stream = https.ByteStream(postAdModel.thumbnail!.openRead());
stream.cast();
final length = await postAdModel.thumbnail!.length();
var multiport = https.MultipartFile(
'thumbnail', // key
stream,
length,
filename: postAdModel.thumbnail!.path,
);
request.files.add(multiport);
////////////////////////* Gallery Images Adding */////////////////////
List<https.MultipartFile> galleryImages = <https.MultipartFile>[];
for (int i = 0; i < postAdModel.images!.length; i++) {
File imageFile = postAdModel.images![i];
var stream = new https.ByteStream(imageFile.openRead());
stream.cast();
var length = await imageFile.length();
var multipartFile = https.MultipartFile(
"images[]",
stream,
length,
filename: imageFile.path);
galleryImages.add(multipartFile);
}
request.files.addAll(galleryImages);
var response = await request.send();

Dart is replacing "&" with "\u0026" in a URL

I am using flutter to process a link and download it to the device using dio package.
But the problem is dart is replacing all '&' with '\u0026' and making the link unusable. is there a way to avoid this problem? Thanks in advance.
Here's the code:
const uuid = Uuid();
final Dio dio = Dio();
// * create random id for naming downloaded file
final String randid = uuid.v4();
// * create a local instance of state all media
List<MediaModel> allMedia = state.allMedia;
// * create an instance of IGDownloader utility class from ~/lib/utilities
final IGDownloader igd = IGDownloader();
// * make a download link from provided link from the GetNewMedia event
final link = await igd.getPost(event.link);
link.replaceAll('\u0026', '&');
print(await link);
Output:
// expected : "http://www.example.com/example&examples/"
// result: "http://www.example.com/example\u0026example"
Pass your url to cleanContent function and don't forget to add imports
import 'package:html/parser.dart';
import 'package:html_unescape/html_unescape.dart';
static String cleanContent(String content, {bool decodeComponent = false}) {
if (content.contains("<p>")) {
content = content.replaceAll("<p>", "").trim();
}
if (content.contains("</p>")) {
content = content.replaceAll("</p>", "").trim();
}
var unescape = HtmlUnescape();
content = unescape.convert(content).toString();
if (content.contains("\\<.*?\\>")) {
content = content.replaceAll("\\<.*?\\>", "").trim();
}
content = parseHtmlString(content,decodeComponent: decodeComponent);
return content;
}
static String parseHtmlString(String htmlString,{bool decodeComponent = false}) {
final document = parse(htmlString);
String parsedString = parse(document.body!.text).documentElement!.text;
if(parsedString.contains("%3A")){
parsedString = parsedString.replaceAll("%3A", ":");
}
if(parsedString.contains("%2F")){
parsedString = parsedString.replaceAll("%2F", "/");
}
if(decodeComponent){
parsedString = Uri.decodeComponent(parsedString);
}
return parsedString;
}
replaceAll returns the modified string, but leaves original String untouched.
Try:
print(await link.replaceAll('\u0026', '&'));
or
newLink = link.replaceAll('\u0026', '&');
print(await newLink);

How can I get multiple messages from dart isolate?

How can I get multiple messages from dart isolate?
I'm trying to create an excel file and want to do some operation on that file in an isolate. Before doing an operation on that file, I want to return an message to main isolate, that excel file is created.
Here is function goes in isolate :
foo(String filePath){
// create excel file
var bytes = File(filePath).readAsBytesSync();
var excel = Excel.decodeBytes(bytes);
//HERE I WANT TO SEND THE MESSAGE THAT CREATING EXCEL FILE IS DONE
// some operatoin on excel file
var result = doSomeOperation(excel);
return result;
}
Main isolate code :
var result = await compute(foo, filePath);
What should I do to get creating file message before the actual result comes?
For excel, I'm using excel: ^2.0.0-null-safety-3 package.
Compute only returns one result. If you want to pass multiple 'events' back to the main isolate then you need to use the full Isolate logic (with sendPort and receivePort).
For example, the following code runs in an isolate, and downloads a file while emitting float values to represent progress, potentially a String to indicate log messages and then a bool to indicate success or failure upon completion.
Future<void> isolateDownload(
DownloadRequest request) async {
final sendPort = request.sendPort;
if (sendPort != null) {
var success = false;
var errorMessage = '';
var url = Uri.parse('a_url_based_on_request');
IOSink? out;
try {
http.StreamedResponse response =
await http.Client().send(http.Request('GET', url));
if (response.statusCode == 200) {
var filePath =
join(request.destinationDirPath, '${request.fileName}.ZIP');
var contentLength = response.contentLength;
var bytesLoadedUpdateInterval = (contentLength ?? 0) / 50;
var bytesLoaded = 0;
var bytesLoadedAtLastUpdate = 0;
out = File(filePath).openWrite();
await response.stream.forEach((chunk) {
out?.add(chunk);
bytesLoaded += chunk.length;
// update if enough bytes have passed since last update
if (contentLength != null &&
bytesLoaded - bytesLoadedAtLastUpdate >
bytesLoadedUpdateInterval) {
sendPort.send(bytesLoaded / contentLength);
bytesLoadedAtLastUpdate = bytesLoaded;
}
});
success = true;
if (contentLength != null) {
sendPort.send(1.0); // send 100% downloaded message
}
} else {
errorMessage =
'Download of ${request.fileName} '
'received response ${response.statusCode} - ${response.reasonPhrase}';
}
} catch (e) {
errorMessage = 'Download of ${request.chartType}:${request.chartName} '
'received error $e';
} finally {
await out?.flush();
await out?.close();
if (errorMessage.isNotEmpty) {
sendPort.send(errorMessage);
}
sendPort.send(success);
}
}
}
The code that spawns the isolate then simply checks for the type of the message passed to it to determine the action.
Future<bool> _downloadInBackground(
DownloadRequest request) async {
var receivePort = ReceivePort();
request.sendPort = receivePort.sendPort;
var isDone = Completer();
var success = false;
receivePort.listen((message) {
if (message is double) {
showUpdate(message);
}
if (message is String) {
log.fine(message); // log error messages
}
if (message is bool) {
success = message; // end with success or failure
receivePort.close();
}
}, onDone: () => isDone.complete()); // wraps up
await Isolate.spawn(isolateDownload, request);
await isDone.future;
return success;
}

Download a sub stream with youtube_explode_dart

I'm developing a Flutter app, using the package youtube_explode_dart.
Currently I download all the audio stream, then crop it using ffmpeg. Quickly this looks like :
var id = "aboZctrHfK8";
var file = File("mySong.mp4");
var start = "2000ms";
var end = "5000ms";
// Downloading audio only
var manifest = await yt.videos.streamsClient.getManifest(id);
var audio = manifest.audioOnly.first;
var audioStream = yt.videos.streamsClient.get(audio);
var output = file.openWrite(mode: FileMode.writeOnlyAppend);
for (final data in audioStream) {
output.add(data);
}
// Croping audio
FlutterFFmpeg _flutterFFmpeg = new FlutterFFmpeg();
await _flutterFFmpeg.executeWithArguments([
"-v", "error",
"-ss", start,
"-to", end,
"-i", file.path,
"-acodec", "copy", "myCroppedSong.mp4"]);
Now I'm facing another issue: Some videos are really heavy and take a while to download. This is not acceptable for my end users, especially since I only want part of the original audio.
Is there a way to download only a subset of the audio stream?
Something like:
for (final data in audioStream.subset(start, end)) {
output.add(data);
}
It would be anwsome!
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
// Initialize the YoutubeExplode instance.
final yt = YoutubeExplode();
Future<void> ExplodeDown() async {
stdout.writeln('Type the video id or url: ');
var url = stdin.readLineSync()!.trim();
// Save the video to the download directory.
Directory('downloads').createSync();
// Download the video.
await download(url);
yt.close();
exit(0);
}
Future<void> download(String id) async {
// Get video metadata.
var video = await yt.videos.get(id);
// Get the video manifest.
var manifest = await yt.videos.streamsClient.getManifest(id);
var streams = manifest.videoOnly;
// Get the audio track with the highest bitrate.
var audio = streams.first;
var audioStream = yt.videos.streamsClient.get(audio);
// Compose the file name removing the unallowed characters in windows.
var fileName = '${video.title}.${audio.container.name}'
.replaceAll(r'\', '')
.replaceAll('/', '')
.replaceAll('*', '')
.replaceAll('?', '')
.replaceAll('"', '')
.replaceAll('<', '')
.replaceAll('>', '')
.replaceAll('|', '');
var file = File('downloads/$fileName');
// Delete the file if exists.
if (file.existsSync()) {
file.deleteSync();
}
// Open the file in writeAppend.
var output = file.openWrite(mode: FileMode.writeOnlyAppend);
// Track the file download status.
var len = audio.size.totalBytes;
var count = 0;
// Create the message and set the cursor position.
var msg = 'Downloading ${video.title}.${audio.container.name}';
stdout.writeln(msg);
// Listen for data received.
// var progressBar = ProgressBar();
await for (final data in audioStream) {
// Keep track of the current downloaded data.
count += data.length;
// Calculate the current progress.
var progress = ((count / len) * 100).ceil();
print (progress);
// Update the progressbar.
// progressBar.update(progress);
// Write to file.
output.add(data);
}
await output.close();
}
**How to download load youtube video & audio & play stream audio **
String youTubeLink = "https://www.youtube.com/watch?v=Ja-85lFDSEM";
Future<void> _downloadVideo(youTubeLink) async{
final yt = YoutubeExplode();
final video = await yt.videos.get(youTubeLink);
// Get the video manifest.
final manifest = await yt.videos.streamsClient.getManifest(youTubeLink);
final streams = manifest.muxed;
final audio = streams.first;
final audioStream = yt.videos.streamsClient.get(audio);
final fileName = '${video.title}.${audio.container.name.toString()}'
.replaceAll(r'\', '')
.replaceAll('/', '')
.replaceAll('*', '')
.replaceAll('?', '')
.replaceAll('"', '')
.replaceAll('<', '')
.replaceAll('>', '')
.replaceAll('|', '');
final dir = await getApplicationDocumentsDirectory();
final path = dir.path;
final directory = Directory('$path/video/');
await directory.create(recursive: true);
final file = File('$path/video/$fileName');
final output = file.openWrite(mode: FileMode.writeOnlyAppend);
var len = audio.size.totalBytes;
var count = 0;
var msg = 'Downloading ${video.title}.${audio.container.name}';
stdout.writeln(msg);
await for (final data in audioStream){
count += data.length;
var progress = ((count / len) * 100).ceil();
print(progress);
output.add(data);
}
await output.flush();
await output.close();
}

Flutter:Dart unzip & save response from http

i need help for this:
Using Flutter 1.12.13+hotfix.5 & Dart 2.7.0
i receive a zip response.bodyBytes from an http.get
how can i zipdecode and save directly to disk ?
This an extract of my code to show what i'm searching:
import 'dart:async';
import 'dart:io';
...
var basicAuth = 'my auth';
Map<String, String> headers = new HashMap();
headers.putIfAbsent('authorization', () => basicAuth);
headers.putIfAbsent('Content-Type', () => 'application/json');
var url = 'http://myhost';
http.Response response = await http.get(
url,
headers: headers,
);
var dir = await getApplicationDocumentsDirectory();
String path = dir.path + '/';
String fn = 'filename.xxx';
new File(path + fn).writeAsBytes(response.bodyBytes); // This cmd store zip file to disk
// In my case i've a zip file with only one file inside, below a code to unzip all files inside zip
// Extract the contents of the Zip archive to disk.
for (ArchiveFile file in archive) {
String filename = file.name;
if (file.isFile) {
List<int> data = file.content;
File('out/' + filename)
..createSync(recursive: true)
..writeAsBytesSync(data);
} else {
Directory('out/' + filename)
..create(recursive: true);
}
}
// Instead i'd like to unzip the only one file there's inside
IF your data is in gzip format you can try
GZipCodec().decode(response.bodyBytes);