You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter导出xlsx时报FileSystemException只读文件系统errno=30错误

问题根因

报错和存储权限无关,是两个核心代码bug叠加导致:

  • 路径拼接错误:then回调中拿到的dir是Directory类型对象,直接用字符串模板'$dir'拼接路径时,调用的是Directory默认的toString()方法,返回值带Directory: 前缀,不是纯路径字符串,最终生成的文件路径完全非法。系统解析路径失败后会尝试往只读的系统根目录写入文件,触发只读文件系统错误,你日志里打印dir时输出的内容带Directory: 前缀就是直接证据。
  • Excel保存时机错误:你在往工作表写入任何内容之前就调用了excel.save(),后续追加的行数据根本不会被包含在导出的文件字节里,最终会导出空的xlsx文件。
  • 冗余逻辑问题:getApplicationDocumentsDirectory()返回的是应用私有文档目录,默认就存在,不需要手动调用create创建;同时读写这个目录完全不需要申请WRITE_EXTERNAL_STORAGE权限,该权限在Android 10+分区存储机制下无法用于访问私有目录,Android 13+更是直接废弃了这个权限,申请没有实际作用。
修复后代码
class ExportPasswords {
  static exportUserEntries(List entries) async {
    final _psd = PasswordDecrypter();
    final _secureStorage = FlutterSecureStorage();

    // 先创建excel、配置样式、填充所有数据
    var excel = Excel.createExcel();
    // 删除默认生成的空Sheet,避免多余文件
    excel.delete('Sheet1');
    Sheet sheetObject = excel['pssswd_export'];
    CellStyle cellStyle = CellStyle(
        backgroundColorHex: "#1AFF1A",
        fontFamily: getFontFamily(FontFamily.Calibri));
    cellStyle.underline = Underline.Single;

    for (var entry in entries) {
      final _username = entry['data']['username'];
      final _name = entry['data']['name'];
      final _url = entry['data']['url'];
      final _hashedPassword = entry['data']['password'];
      final _randForKeyToStore = entry['data']['randForKeyToStore'];
      final _randForIV = entry['data']['randForIV'];
      final _masterPassword = await _secureStorage.read(key: 'masterPassword');

      final _decryptedEntryPassword = await _psd.getDecryptedPassword(
          _hashedPassword, _randForKeyToStore, _randForIV, _masterPassword);

      sheetObject.appendRow([_name, _username, _decryptedEntryPassword, _url]);
    }

    // 所有内容填充完成后再生成文件字节
    var fileBytes = excel.save();
    if (fileBytes == null) return;

    // 直接取目录的path属性获取纯路径,拼接时加上.xlsx后缀
    Directory directory = await getApplicationDocumentsDirectory();
    String targetPath = join(directory.path, 'pssswd_export.xlsx');
    File exportFile = File(targetPath);
    await exportFile.writeAsBytes(fileBytes);
  }
}
额外说明
  • 如果需要把文件导出到手机公共存储(比如Download目录)让其他应用也能访问,不要依赖WRITE_EXTERNAL_STORAGE权限,Android 10+请使用系统存储访问框架(SAF),或者适配分区存储的公共目录写入规则,否则权限申请不会生效。
  • 所有路径拼接场景,不要直接把Directory、File对象转字符串拼接,必须取对应的path属性拿到纯路径字符串再操作,join方法的参数也应该传入纯路径字符串。

内容的提问来源于stack exchange,提问作者tr1via

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 17:12:28