npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

expo-easy-fs

v0.2.3

Published

A file utility for Expo to access the system Downloads folder, Android-only.

Readme

expo-easy-fs

npm downloads license platform support

A file utility for Expo to access the system Downloads folder, Android-only.

Android 权限与兼容性 (Permissions & Compatibility)

从 vX.X.X 版本开始(包含此修改),expo-easy-fs 适配 Android 各版本存储策略:

| Android 版本 | 存储模型 | 需要的权限 / 配置 | 说明 | |--------------|----------|------------------|------| | API < 29 (Android 9 及以下) | Legacy 外部存储 | 必须在 AndroidManifest.xml 中声明 READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE,并在运行时申请(WRITE 为必需)。| 直接写入公共 Downloads 目录,需要用户授予权限。| | API 29 (Android 10) | Scoped Storage (过渡) | 无需 WRITE 权限;若要保持旧行为可加 requestLegacyExternalStorage=true(不推荐)。| 本库使用 MediaStore 写入 Downloads 集合,无需额外权限。| | API 30+ (Android 11 及以上) | Scoped Storage | 无需 WRITE_EXTERNAL_STORAGE;普通下载不需要 MANAGE_EXTERNAL_STORAGE。| 使用 MediaStore,系统自动管理。|

需要在 Expo / React Native 项目中配置的 Manifest 权限

如果你仍需要兼容 Android 9 及以下,请在 app.jsonapp.config.js 中添加:

"android": {
  "permissions": [
    "READ_EXTERNAL_STORAGE",
    "WRITE_EXTERNAL_STORAGE"
  ]
}

并在运行时(仅 API <29)调用:

import { PermissionsAndroid, Platform } from 'react-native';

async function ensureLegacyStoragePermission() {
  if (Platform.OS === 'android' && Platform.Version < 29) {
    const granted = await PermissionsAndroid.request(
      PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
      {
        title: '存储权限',
        message: '需要存储权限以便保存文件到下载目录',
        buttonPositive: '确定'
      }
    );
    if (granted !== PermissionsAndroid.RESULTS.GRANTED) {
      throw new Error('WRITE_EXTERNAL_STORAGE denied');
    }
  }
}

提示:Android 13 (API 33) 引入了细化的媒体权限(如 READ_MEDIA_IMAGES 等),由于本库写入的是通用文件(可能是任意类型),通常无需这些媒体读取权限;下载后若你再读取/处理特定媒体类型,按需自行申请。

使用注意 (Notes)

  1. 在 Android 10+ 上,本库通过 MediaStore 写入公共 Downloads,不需要手动申请写权限。
  2. 旧方法 copyFileToDownload 现在接受 content://file:// 或绝对路径,内部自动适配。
  3. 如果你使用的是 Expo Managed Workflow,请确保构建时所需的权限都在配置里列出,否则旧设备上会失败。
  4. 不要为仅保存普通下载文件去申请 MANAGE_EXTERNAL_STORAGE(All files access),那会触发应用上架审核风险。
  5. 若你的源文件来自 expo-file-system,传入前可用本库的 fixPath 处理前缀。

新增/更新行为 (Changelog 摘要)

  • 增强 copyFileToDownload:支持 Android 10+ Scoped Storage (MediaStore) 与旧版直写逻辑。
  • 自动判断输入是 content://file:// 还是普通路径。
  • 根据文件扩展名推断 MIME Type,提升在系统文件管理器中的可见性。
  • 对 API <29 若无写权限会直接抛出 ERR_PERMISSION_DENIED

最低示例(含权限处理)

import * as ExpoEasyFs from 'expo-easy-fs';
import * as FileSystem from 'expo-file-system';

async function demo() {
  await ensureLegacyStoragePermission(); // 仅旧设备需要
  const filename = 'demo.txt';
  const localPath = FileSystem.documentDirectory + filename;
  await FileSystem.writeAsStringAsync(localPath, 'Hello');
  const { downloads } = await ExpoEasyFs.getPaths();
  await ExpoEasyFs.copyFile(localPath, `${downloads}/demo-folder/${filename}`);
}

Installation

npm install expo-easy-fs

API

/**
 * Retrieves system paths such as the downloads directory.
 * @returns {Promise<{ downloads: string }>} A promise resolving to an object containing the downloads path.
 */
export function getPaths(): Promise<{
  downloads: string;
}> {
  return ExpoEasyFsModule.getPaths();
}

/**
 * Creates a new directory.
 * @param {string} dirPath - The path of the directory to create.
 * @returns {Promise<string>} A promise resolving to the created directory's path.
 */
export function mkdir(dirPath: string): Promise<string> {
  return ExpoEasyFsModule.mkdir(fixPath(dirPath));
}

/**
 * Copies a file from the source path to the destination path.
 * @param {string} sourcePath - The source file path.
 * @param {string} destinationPath - The destination file path.
 * @returns {Promise<string>} A promise resolving to the destination file's path.
 */
export function copyFile(sourcePath: string, destinationPath: string): Promise<string> {
  return ExpoEasyFsModule.copyFile(fixPath(sourcePath), fixPath(destinationPath));
}

/**
 * Deletes a file or directory.
 * @param {string} targetPath - The path of the file or directory to delete.
 * @returns {Promise<string>} A promise resolving to the deleted target's path.
 */
export function remove(targetPath: string): Promise<string> {
  return ExpoEasyFsModule.remove(fixPath(targetPath));
}

/**
 * Checks if a path exists and returns its type.
 * @param {string} targetPath - The path to check.
 * @returns {Promise<{ exists: boolean, type: 'file' | 'directory' | 'unknown', path: string }>} 
 * A promise resolving to an object indicating existence, type, and the path.
 */
export function exists(targetPath: string): Promise<{
  exists: boolean;
  type: 'file' | 'directory' | 'unknown';
  path: string;
}> {
  return ExpoEasyFsModule.exists(fixPath(targetPath));
}

Example

import * as ExpoEasyFs from "expo-easy-fs";
import * as fileSystem from "expo-file-system";
import { StyleSheet, Text, View, StatusBar, Button } from "react-native";
import { useCallback } from "react";

export default function App() {
  const func = useCallback(async () => {
    try {
      const filename = `expo-easy-fs-${Date.now()}.txt`;
      const originFilePath = fileSystem.documentDirectory + filename;

      const { downloads: systemDownloadsDir } = await ExpoEasyFs.getPaths();

      const destinationDirPath = systemDownloadsDir + `/expo-easy-fs/`;
      const destinationFilePath = destinationDirPath + filename;

      await fileSystem.writeAsStringAsync(originFilePath, "Test");
      await ExpoEasyFs.mkdir(destinationDirPath);
      await ExpoEasyFs.copyFile(originFilePath.replace(/^file:\//, ""), destinationFilePath);
      await ExpoEasyFs.remove(originFilePath);
      alert(`Success! The test file has been created and moved to: ${destinationFilePath}`);
    } catch (err) {
      console.error(err);
      alert("An error occurred while processing the file. Please try again.");
    }
  }, []);

  return (
    <View style={styles.container}>
      <StatusBar barStyle="dark-content" />
      <Text>Welcome to the Expo Easy FS Demo</Text>
      <Button
        title="Create a file and move it to the system's Downloads folder"
        onPress={func}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
});