pichillilorenzo / flutter_inappwebview

A Flutter plugin that allows you to add an inline webview, to use a headless webview, and to open an in-app browser window.
https://inappwebview.dev
Apache License 2.0
3.22k stars 1.58k forks source link

[TUTO] How to download Blob files #2212

Open EArminjon opened 3 months ago

EArminjon commented 3 months ago

How to support BLOB download ?

Requirements

https://pub.dev/packages/mime https://pub.dev/packages/path https://pub.dev/packages/path_provider

1 - InAppWebView

Enable JavaScript and onDownloadStartRequest Use onDownloadStartRequest to listen BLOB download and execute the Javascript logic to get the BLOB base64 data content. Use onWebViewCreated to set up JS listeners.

@Override
Widget build(BuildContext context) {
  return InAppWebView(
    initialSettings: InAppWebViewSettings(
      javaScriptEnabled: true,
      useOnDownloadStart: true,
    ),
    onDownloadStartRequest: _onDownloadStartRequest,
    onWebViewCreated: _onWebViewCreated,
  );
}

2 - onDownloadStartRequest

Every blob opening / download will trigger this method. Url cannot be downloaded from flutter side, only the browser can do it.

Code bellow will fetch the BLOB by its URL and then read its content to get the base64 data. Once base64 received, the JS will send the result through a javaScript handler to the Flutter side. (You can use another strategy like postMessages)

void _onDownloadStartRequest(
    InAppWebViewController controller,
    DownloadStartRequest downloadStartRequest,
  ) {
  if (downloadURL.startsWith("blob")) {
    controller.evaluateJavascript(
      source: '''
            function flutterBlobToBase64(blob) {
                return new Promise((resolve, reject) => {
                    const reader = new FileReader();
                    reader.onloadend = () => {
                        const base64data = reader.result.split(',')[1];
                        resolve(base64data);
                    };
                    reader.onerror = reject;
                    reader.readAsDataURL(blob);
                });
            }

            async function flutterBlobDownload(filename, blobUrl) {
                const response = await fetch(blobUrl);
                const blob = await response.blob();

                flutterBlobToBase64(blob).then(base64 => {
                    window.flutter_inappwebview.callHandler(
                      'downloadBase64',
                      event.data.filename,
                      event.data.base64
                    );
                });
            };
           flutterBlobDownload("${downloadStartRequest.suggestedFilename ?? ''}", "$downloadURL");
      ''',
    );
  } else {
    // Handle other download types but it's out of scope :)
  }
}

3 - OnWebViewCreated

We define a addJavaScriptHandler to get the final base64 data and then download it to the device.

Future<void> _onWebViewCreated(
    InAppWebViewController controller,
  ) async {

  controller.addJavaScriptHandler(
    handlerName: "downloadBase64",
    callback: (List<dynamic> args) {
      if (args.length == 2 && args[0] is String && args[1] is String) {
        final String filename = args[0] as String;
        final String base64 = args[1] as String;

         // Here you can use want you want to download the file. 
        downloadToDevice(filename, base64);
      }
    },
  );
}

Future<void> downloadToDevice(String filename, String base64) async {
  final Uint8List data = await compute(base64Decode, base64);

  String finalFileName = filename.isNullOrEmpty ? randomNumber().toString() : filename;
  final String? mime = lookupMimeType(finalFileName, headerBytes: bytes);
  if (!finalFileName.contains('.') && mime != null) {
    final String extension = extensionFromMime(mime);
    finalFileName = '$finalFileName.$extension';
  }
  final Directory documents = await getApplicationDocumentsDirectory();
  final File file = await File('${documents.path}/$filename').renameIfExist;
  await file.create(recursive: true);
  await file.writeAsBytes(bytes);
}

extension FileExtension on File {
  /// Avoid override file if already exist, that will happen a number at the end of the filename
  /// Ex : file.txt -> file (1).txt
  Future<File> get renameIfExist async {
    File file = this;
    final int lastSeparator = path.lastIndexOf(Platform.pathSeparator);
    final String rawPath =
    lastSeparator == -1 ? '' : path.substring(0, lastSeparator + 1);

    final String fileName = basename(file.path);
    final String rawFileName = basenameWithoutExtension(file.path);
    final String ext = extension(file.path);

    int retry = 1;
    while (await file.exists()) {
      final String retryExtension = '($retry)';

      if (ext.isNotEmpty) {
        file = File('$rawPath$rawFileName $retryExtension$ext');
      } else {
        file = File('$rawPath$fileName $retryExtension');
      }
      ++retry;
    }
    return file;
  }
}
github-actions[bot] commented 3 months ago

👋 @EArminjon

NOTE: This comment is auto-generated.

Are you sure you have already searched for the same problem?

Some people open new issues but they didn't search for something similar or for the same issue. Please, search for it using the GitHub issue search box or on the official inappwebview.dev website, or, also, using Google, StackOverflow, etc. before posting a new one. You may already find an answer to your problem!

If this is really a new issue, then thank you for raising it. I will investigate it and get back to you as soon as possible. Please, make sure you have given me as much context as possible! Also, if you didn't already, post a code example that can replicate this issue.

In the meantime, you can already search for some possible solutions online! Because this plugin uses native WebView, you can search online for the same issue adding android WebView [MY ERROR HERE] or ios WKWebView [MY ERROR HERE] keywords.

Following these steps can save you, me, and other people a lot of time, thanks!

EArminjon commented 3 months ago

Can be added to the documentation i believe.

EArminjon commented 3 months ago

I detected that blob download from iframe didn't works (CORS) but this is maybe a normal behavior. I will post an update + comment if i found something.

Edit : It's a normal behavior and that can't be handled by webview.

If you have the hand over the iframe content, you can use postMessage and send the base64 of the blob inside it. If you didn't have the hand over it, it's over :'(.

adamk22 commented 2 months ago

Also ran into this issue with blob urls e.g. blob:https://myapp.com/6b37ec9a-d42e-4fa4-9738-e51001900318

Used your method to fetch the blob on the front-end app (the app that's loaded into the webview) but unfortunately ran into CSP issues:

Refused to connect to blob:https://myapp.com/6b37ec9a-d42e-4fa4-9738-e51001900318 because it does not appear in the connect-src directive of the Content Security Policy.

I assume the easiest method/workaround is just have the front-end app handle the blob > base64 transformation and send it over through postMessage like you said @EArminjon ? Have you been able to get it to work?