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
相关产品推荐
相关产品推荐

