使用 React Native FS 将文件写入 Android 外部存储

Write files to the Android external storage using React Native FS

我的 React Native Android 应用程序需要在外部存储的特殊文件夹中保存一个 JSON 文件。我正在尝试以这种方式使用 RNFS (https://github.com/itinance/react-native-fs) 来做到这一点:

const saveData = async () => {
    var path = `${RNFS.ExternalStorageDirectoryPath}/MyApp`;
    RNFS.mkdir(path);
    path += '/data.json';
    RNFS.writeFile(path, JSON.stringify(getData()), 'utf8')
      .then((success) => {
        console.log('Success');
      })
      .catch((err) => {
        console.log(err.message);
      });
  }

它运行良好但在 Android Q 设备上失败。显示此错误:

Error: Directory could not be created

如果我尝试在不创建目录的情况下编写普通文件,则会抛出此错误:

ENOENT: open failed: ENOENT (No such file or directory), open '/storage/emulated/0/data.json'

但是,我已将此权限添加到我的 AndroidManifest.xml:

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

并在设置中授予外部存储权限。但是如果我将 RNFS.ExternalStorageDirectoryPath 更改为 RNFS.DocumentDirectoryPath 它可以正常工作而不会出现任何错误。但是我需要访问外部存储。有什么办法吗?

我发现 Android API 29 岁以上需要访问旧版外部存储。所以,我已经像这样编辑了我的 AndroidManifest.xml(位于 android/app/src/main/):

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.appName">

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

    <application
    ...
      android:requestLegacyExternalStorage="true"
    ...
    >
    </application>
</manifest>

一切都开始工作了。此外,我还添加了一个请求,用于向 saveData 函数授予权限:

const saveData = async () => {
    try {
      const granted = await PermissionsAndroid.requestMultiple([
        PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
        PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
      ]);
    } catch (err) {
      console.warn(err);
    }
    const readGranted = await PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE); 
    const writeGranted = await PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE);
    if(!readGranted || !writeGranted) {
      console.log('Read and write permissions have not been granted');
      return;
    }
    var path = `${RNFS.ExternalStorageDirectoryPath}/MyApp`;
    RNFS.mkdir(path);
    path += '/data.json';
    RNFS.writeFile(path, JSON.stringify(getData()), 'utf8')
      .then((success) => {
        console.log('Success');
      })
      .catch((err) => {
        console.log(err.message);
      });
  }

随着 Android 版本 10(API 级别 29)引入了范围存储的概念。 根据文档-

"To give users more control over their files and to limit file clutter, apps that target Android 10 (API level 29) and higher are given scoped access into external storage, or scoped storage, by default. Such apps have access only to the app-specific directory on external storage, as well as specific types of media that the app has created."

对于 Android 版本 10,您可以选择 opt-out 分区存储 添加 android:requestLegacyExternalStorage="true" 在 AndroidManifest.xml 这样子->

<application ...
 android:requestLegacyExternalStorage="true"
...>
....
</application>

这是一个临时解决方案,仅适用于 android 版本 10,不适用于更高版本。 还要确保您请求 run-time 对 read/write 外部存储的许可。

更新:使用分区存储

或者,我发现这个库 react-native-file-access 使用 Scoped Storage。就我而言,我将文件存储在 下载 目录中。

首先,对于 RNSF,我们需要下载文件并将目标设置为 RNFS.TemporaryDirectoryPath,然后使用 react-native-file-access 将下载的文件复制到特定文件夹(在我的例子中是下载)。

FileSystem.cpExternal(localFileUrl, `${filenameFormatted}`,'downloads')

也许你可以使用这个库通过一些调整将文件存储在你的特定目录中

使用权限管理外部存储(不推荐):

在Android11 及以上版本中,访问存储的权限已更改。您必须添加名为 MANAGE_EXTERNAL_STORAGE 的额外权限,请参阅这些 post :

https://developer.android.com/about/versions/11/privacy/storage#directory-access https://developer.android.com/training/data-storage/manage-all-files

将此添加到您的 AndroidManifest.xml

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

我基于 创建简单的权限请求,从 react native 调用这个本地函数。

app/src/main/<your_package>

中创建 PermissionFileModule.java
public class PermissionFileModule extends ReactContextBaseJavaModule implements ActivityEventListener {

    public PermissionFileModule(@Nullable ReactApplicationContext reactContext) {
        super(reactContext);
        reactContext.addActivityEventListener(this);
    }

    @NonNull
    @Override
    public String getName() {
        return "PermissionFile";
    }

    @ReactMethod
    public void checkAndGrantPermission(Callback errorCallback, Callback successCallback) {
        try {
            if (!checkPermission()) {
                requestPermission();
                successCallback.invoke(false);
            } else {
                successCallback.invoke(true);
            }
        } catch (IllegalViewOperationException e) {
            errorCallback.invoke(e.getMessage());
        }
    }

    private boolean checkPermission() {
        if (SDK_INT >= Build.VERSION_CODES.R) {
            return Environment.isExternalStorageManager();
        } else {
            int result = ContextCompat.checkSelfPermission(getReactApplicationContext(), READ_EXTERNAL_STORAGE);
            int result1 = ContextCompat.checkSelfPermission(getReactApplicationContext(), WRITE_EXTERNAL_STORAGE);
            return result == PackageManager.PERMISSION_GRANTED && result1 == PackageManager.PERMISSION_GRANTED;
        }
    }

    private void requestPermission() {
        if (SDK_INT >= Build.VERSION_CODES.R) {
            try {
                Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
                intent.addCategory("android.intent.category.DEFAULT");
                intent.setData(Uri.parse(String.format("package:%s",getReactApplicationContext().getPackageName())));
                getCurrentActivity().startActivityForResult(intent, 2296);
            } catch (Exception e) {
                Intent intent = new Intent();
                intent.setAction(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
                getCurrentActivity().startActivityForResult(intent, 2296);
            }
        } else {
            //below android 11
            ActivityCompat.requestPermissions(getCurrentActivity(), new String[]{WRITE_EXTERNAL_STORAGE}, 100);
        }
    }

    @Override
    public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
        if (requestCode == 2296) {
            if (SDK_INT >= Build.VERSION_CODES.R) {
                if (Environment.isExternalStorageManager()) {
                    Toast.makeText(getReactApplicationContext(), "Access granted", Toast.LENGTH_SHORT).show();
                } else {
                    Toast.makeText(getReactApplicationContext(), "Access not granted", Toast.LENGTH_SHORT).show();
                }
            }
        }
    }

    @Override
    public void onNewIntent(Intent intent) {
        // do nothing
    }
}

app/src/main/<your_package>

中创建 PermissionFilePackage.java
public class PermissionFilePackage implements ReactPackage {
    @NonNull
    @Override
    public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
        List<NativeModule> modules = new ArrayList<>();

        modules.add(new PermissionFileModule(reactContext));
        return modules;
    }

    @NonNull
    @Override
    public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }
}

MainApplication.java中,添加PermissionFilePackage.java作为附加包

...
@Override
protected List<ReactPackage> getPackages() {
@SuppressWarnings("UnnecessaryLocalVariable")
    List<ReactPackage> packages = new PackageList(this).getPackages();
    // Packages that cannot be autolinked yet can be added manually here, for example:
    // packages.add(new MyReactNativePackage());
    packages.add(new PermissionFilePackage());
    return packages;
}
...

在你的 RN 组件中,像这样调用权限文件

...
import {NativeModules} from 'react-native';
var PermissionFile = NativeModules.PermissionFile;
...

if (Platform.Version >= 30) {
        PermissionFile.checkAndGrantPermission(
          (err) => {
            DeviceUtils.showAlert(
              'Sorry',
              'Access not granted',
            );
          },
          (res) => {
            if (res) {
              checkDirectoryAndDownload(url, name, ext);
            }
          },
        );
      } else {
        DeviceUtils.grantPermissionSingleAndroid(
          PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE,
          (isAllow) => {
            if (isAllow) {
              checkDirectoryAndDownload(url, name, ext);
            } else {
              DeviceUtils.showAlert(
                'Sorry',
                'Access not granted',
              );
            }
          },
        );
      }