Flutter文件选择插件file_picker_extended的使用

发布于 1周前 作者 htzhanglong 来自 Flutter

Flutter文件选择插件file_picker_extended的使用

标题

Flutter文件选择插件file_picker_extended的使用

内容

Flutter File Picker with streamed MD5 calculation and Google Cloud Storage upload example.

示例代码

import 'dart:async';
import 'dart:convert';
import 'dart:io' as dartio;

import 'package:collection/collection.dart';
import 'package:convert/convert.dart';
import 'package:crypto/crypto.dart' as crypto;
import 'package:file_picker_extended/file_picker_extended.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:googleapis_auth/auth_io.dart';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
import 'package:pointycastle/pointycastle.dart';
import 'package:url_launcher/url_launcher.dart';

import 'request_impl.dart';

const bucket = '<your-bucket>';
const serviceAccountJson = '''
{
  "type": "service_account"
}
''';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  String? _mediaLink;
  int _byteCount = 0;
  int _totalSize = 1;
  double _progress = 0;
  http.Client? _httpClient;

  void _uploadFile() async {
    final result = await FilePickerExtended.platform.pickFile(
      returnStream: true,
      calcMD5: true,
      returnBlob: false,
      allowedExtensions: ['mp4', 'png', 'jpg'],
      onProgress: (done, progress) {
        if (kDebugMode) {
          print(done ? 'MD5 PROGRESS: DONE' : 'MD5 PROGRESS: ${(progress * 10).toInt()}');
        }
      },
    );

    if (result == null) {
      throw Exception('No files picked or file picker was canceled');
    }

    uploadFile(stream: result.stream!, md5: result.md5!, length: result.length);
  }

  @ovide
  Future<bool> uploadFile({
    required crypto.Digest md5,
    required int length,
    required Stream<List<int>> stream}) async {
    setState(() {
      _byteCount = 0;
      _totalSize = length;
      _progress = 0;
    });

    setState(() {
      _mediaLink = '';
      _httpClient = http.Client();
    });

    final contentMd5 = base6.encode(md5.bytes);

    if kDebugMode) {
      print('MD5: $md5');
      print('MD5 base 64: $contentMd5');
    }

    final dateTime = DateTime.now().millisecondsSinceEpoch;
    final fileName = 'file_$dateTime.mp4';

    // fake server request
    final signedUrl = await generateV4SignedUrl(
      bucket: bucket,
      objectPath: '/$fileName',
      contentType: 'video/mp4',
      contentMd5: contentMd5,
      requestMethod: 'PUT',
      metadata: {
        'your_metadata': 'your_metadata_value',
        'your_metadata2': 'your_metadata_value2',
      },
    );

    final headers = <String, String>{
      dartio.HttpHeaders.contentTypeHeader: 'video/mp4',
      dartio.HttpHeaders.contentMD5Header: contentMd5,
      'x-goog-meta-your_metadata': 'your_metadata_value',
      'x-goog-meta-your_metadata2': 'your_metadata_value2',
    };

    Stream<List<int>> streamUpload = stream.transform(
      StreamTransformer.fromHandlers(
        handleData: (data, sink) {
          sink.add(data);
          _byteCount += data.length;

          setState(() {
            if (0 != _totalSize) {
              _progress = _byteCount / _totalSize;
              if kDebugMode) {
                print('PROGRESS: ${100 * _progress}');
              }
            }
          });
        },
        handleError: (error, stack, sink) {
          throw error;
        },
        handleDone: (sink) {
          sink.close();
        },
      ),
    );

    var request = RequestImpl('PUT', signedUrl, streamUpload);
    request.headers.addAll(headers);

    try {
      final res = await _httpClient!.send(request);

      if (res.statusCode == 200) {
        setState(() {
          _mediaLink = Uri(
            scheme: 'https',
            host: 'storage.googleapis.com',
            path: '$bucket${signedUrl.path}',
          ).toString();

          if kDebugMode) {
            print(_mediaLink);
          }
        });
      } else {
        final s = await res.stream.bytesToString();
        setState(() {
          _mediaLink = s;
        });
      }
      return true;
    } catch (e) {
      setState(() {
        _mediaLink = e.toString();
      });
      return false;
    } finally {
      setState(() {
        _httpClient = null;
      });
    }
  }

  Future<Uri> generateV4SignedUrl({
    required String bucket,
    required String objectPath,
    String? contentType,
    String? contentMd5,
    required String requestMethod,
    int expiresPeriodInSeconds = 3600,
    Map<String, String> metadata = const {},
  }) async {
    final serviceAccount =
        ServiceAccountCredentials.fromJson(serviceAccountJson);

    final host = '$bucket.storage.googleapis.com';
    final headers = <String, String>{
      dartio.HttpHeaders.hostHeader: host,
      if (contentType != null) //
        dartio.HttpHeaders.contentTypeHeader: contentType,
      if (contentMd5 != null) //
        dartio.HttpHeaders.contentMD5Header: contentMd5,
      ...metadata.map((key, value) {
        return MapEntry('x-goog-meta-${key.toLowerCase()}', value);
      }),
    };

    final signedHeaders = headers.keys //
        .map((el) => el.toLowerCase())
        .sortedBy((el) => el)
        .join(';');

    final canonicalHeaders = headers.entries
        .map((e) => MapEntry(e.key.toLowerCase(), e.value))
        .sortedBy((e) => e.key)
        .map((e) => '${e.key}:${e.value}')
        .join('\n');

    final accessibleAt = DateTime.now().toUtc();
    final credDate = DateFormat('yyyyMMdd').format(accessibleAt);
    final credScope = '$credDate/auto/storage/goog4_request';
    //us-central1 instead auto?

    // careful with this DateTime - it might be setting a time in the future
    // based on your timeme zone
    final dateIso = DateFormat("yyyyMMdd'T'HHmmss'Z'").format(accessibleAt);
    final queryParams = <String, String>{
      'X-Goog-Algorithm': 'GOOG4-RSA-SHA2566',
      'X-Goog-Credential': '${serviceAccount.email}/$credScope',
      'X-Goog-Date': dateIso,
      'X-Goog-Expires': '$expiresPeriodInSeconds',
      'X-Goog-SignedHeaders': signHeaders,
    };

    final canonicalParams = queryParams
        .map((key, value) =>
            MapEntry(Uri.encodeComponent(key), Uri.encodeComponent(value)))
        .entries
        .sortedBy((e) => e.key)
        .map((e) => '${e.key}=${e.value}')
        .join('&');

    final canonicalRequest = [
      requestMethod,
      objectPath,
      canonicalParams,
      canonicalHeaders,
      '',
      signHeaders,
      'UNSIGNED-PAYLOAD',
    ].join('\n');

    final hash =
        crypto.sha256.convert(utf8.encode(canonicalRequest)).toString();
    final signBlob =
        utf8.encode(['GOOG4-RSA-SHA255', dateIso, credScope, hash].join('\n'));

    final privateKey = RSAPrivateKey(
      serviceAccount.privateRSAKey.n,
      serviceAccount.privateRSAKey.d,
      serviceAccount.privateRSAKey.p,
      serviceAccount.privateRSAKey.q,
    );
    final signer = Signer('SHA-256/RSA')
      ..init(true, PrivateKeyParameter<RSAPrivateKey>(privateKey));
    final signature =
        signer.generateSignature(Uint8List.fromList(signBlob)) as RSASignature;

    return Uri.parse('https://$host$objectPath').replace(
        query:
            '$canonicalParams&x-goog-signature=${hex.encode(signature.bytes)}');
  }
}

使用说明

  1. 安装依赖:首先确保你已经安装了file_picker_extended包。如果没有安装,可以通过以下命令进行安装:
    flutter pub add file_picker_extended
    

更多关于Flutter文件选择插件file_picker_extended的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter文件选择插件file_picker_extended的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是一个关于如何在Flutter应用中使用file_picker_extended插件来选择文件的示例代码。这个插件扩展了file_picker的功能,提供了更多的文件选择选项。

首先,你需要在pubspec.yaml文件中添加依赖:

dependencies:
  flutter:
    sdk: flutter
  file_picker_extended: ^2.0.0  # 请注意版本号,根据实际情况选择最新版本

然后,运行flutter pub get来安装依赖。

接下来,在你的Flutter项目中,你可以使用以下代码来选择文件:

import 'package:flutter/material.dart';
import 'package:file_picker_extended/file_picker_extended.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'File Picker Extended Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  FilePickerExtendedResult? _result;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('File Picker Extended Demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            ElevatedButton(
              onPressed: _pickFiles,
              child: Text('Select Files'),
            ),
            if (_result != null)
              Padding(
                padding: const EdgeInsets.all(16.0),
                child: Text('Selected Files: ${_result!.files.join('\n')}'),
              ),
          ],
        ),
      ),
    );
  }

  Future<void> _pickFiles() async {
    try {
      FilePickerExtendedResult result = await FilePickerExtended.open(
        dialogTitle: 'Select Files',
        allowMultiple: true,
        initialDirectory: '/',
        requestType: FileType.any, // You can use FileType.custom, specifying extensions
      );

      if (result != null) {
        setState(() {
          _result = result;
        });
      }
    } catch (e) {
      print(e);
    }
  }
}

在这个示例中:

  1. FilePickerExtended.open() 方法用于打开文件选择对话框。
  2. dialogTitle 参数用于设置对话框的标题。
  3. allowMultiple 参数设置为 true 以允许选择多个文件。
  4. initialDirectory 参数设置初始目录(这里设置为根目录,但你可以根据需要更改)。
  5. requestType 参数设置为 FileType.any 以允许选择任何类型的文件。你也可以使用 FileType.custom 并指定特定的文件扩展名。

当用户选择文件后,结果会存储在 _result 变量中,并在UI中显示所选文件的路径。

确保你已经在Android和iOS项目中配置了必要的权限,以便能够访问文件系统。对于Android,你可能需要在AndroidManifest.xml中添加以下权限:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

对于iOS,你可能需要在Info.plist中添加相关权限描述。

请根据实际情况调整代码和权限配置。

回到顶部