# 文件和目录操作

# 什么是 fs 文件系统模块

fs 模块是 Node.js 官方提供的、用来操作文件的模块。它提供了一系列的方法和属性,用来满足用户对文件的操作需求。具体 API 可以看这里 (opens new window)。所有文件系统操作都具有同步回调基于 promise的形式。

  • 回调的 APIPromise API,使用底层的 Node.js 线程池在事件循环线程之外执行文件系统操作。 这些操作不是同步的也不是线程安全的。 对同一文件执行多个并发修改时必须小心,否则可能会损坏数据。
  • 同步的 API同步地执行所有操作,阻塞事件循环(和进一步的 JavaScript 执行),直到操作完成或失败。
  • 当达到最佳性能时(无论是在执行时间还是在内存分配方面),fs 模块基于回调的 API 版本都比使用 promise API 更好(比同步也要快性能好)。但promise API可以解决回调地狱的问题。

基于 promise

import { unlink } from "fs/promises";

try {
  await unlink("/tmp/hello");
  console.log("successfully deleted /tmp/hello");
} catch (error) {
  console.error("there was an error:", error.message);
}
1
2
3
4
5
6
7
8

回调形式

import { unlink } from "fs";

unlink("/tmp/hello", (err) => {
  if (err) throw err;
  console.log("successfully deleted /tmp/hello");
});
1
2
3
4
5
6

同步形式

import { unlinkSync } from "fs";

try {
  unlinkSync("/tmp/hello");
  console.log("successfully deleted /tmp/hello");
} catch (err) {
  // 处理错误
}
1
2
3
4
5
6
7
8

# 文件操作

# fs.open()

fs.open(path, flags[, mode], callback)方法用于打开文件

  • path,必选参数,表示文件的路径。
  • flags,文件打开的行为。请参阅文件系统标志
  • mode,可选参数,设置文件模式(权限),文件创建默认权限为 0666(可读,可写)。请参阅文件的模式
  • callback,回调函数,带有两个参数如:callback(err, fd)

# fs.close()

fs.close(fd, callback)方法用于关闭文件

  • fd:通过fs.open()方法返回的文件描述符。
  • callback:回调函数。

# fs.read()

fs.read(fd, buffer, offset, length, position, callback)fd指定的文件中读取数据

  • fd:通过fs.open()方法返回的文件描述符。
  • buffer:数据将写入的缓冲区。 默认值: Buffer.alloc(16384)
  • offset:要写入数据的 buffer 中的位置。 默认值: 0
  • length:读取的字节数。 默认值: buffer.byteLength
  • position:指定从文件中开始读取的位置。 如果 position 为 null-1 ,则将从当前文件位置读取数据,并更新文件位置。 如果 position 是整数,则文件位置将保持不变。
  • callback:回调函数,有三个参数callback(err, bytesRead, buffer)。其中err为错误信息, bytesRead表示读取的字节数,buffer为缓冲区对象。
import { open, close } from "fs";

open("myfile", "wx", (err, fd) => {
  if (err) {
    if (err.code === "EEXIST") {
      console.error("myfile already exists");
      return;
    }

    throw err;
  }
  const buf = Buffer.alloc(6);
  console.log(buf);

  fs.read(fd, buf, 0, 6, 24, (err, bytesRead, buffer) => {
    if (err) {
      console.error(err);
    } else {
      console.log("读取到的字节数:", bytesRead);
      console.log("读取到的数据是:", buffer.toString());
      console.log(buf);
    }
    close(fd, (err) => {
      if (err) throw err;
    });
  });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# fs.readFile()

fs.readFile(path[, options], callback)方法,可以读取指定文件中的内容

  • path,必选参数,表示文件的路径
  • options,可选参数,表示以什么编码格式来读取文件。如果不指定这个参数,默认会将读取到的数据放到 Buffer 中
  • callback,必选参数,文件读取完成后,通过回调函数拿到读取的结果
const fs = require("fs");

// err为报错信息,data为文件内容
fs.readFile("/Users/joe/test.txt", "utf8", (err, data) => {
  if (err) {
    console.error(`文件读取失败:${err.message}`);
    return;
  }
  console.log(`文件读取成功:${data}`);
});
1
2
3
4
5
6
7
8
9
10

fs.readFile()方法每次一块地异步读取文件内容到内存中,允许事件循环在每个块之间转换。 这允许读取操作对可能使用底层 libuv 线程池的其他活动的影响较小,但意味着将完整文件读入内存需要更长的时间。对于需要尽可能快地读取文件内容的应用程序,最好直接使用fs.read()并让应用程序代码管理读取文件本身的全部内容。当然也可以使用fs.createReadStream()以流的形式读取文件。

# fs.write()

fs.write(fd, buffer[, offset[, length[, position]]], callback)将 buffer 缓冲区里的数据写入 fd 指定的文件。

  • fd:通过fs.open()方法返回的文件描述符。
  • buffer:数据源,是一个缓冲区。
  • offset:确定要写入的缓冲区部分
  • length:是整数,指定要写入的字节数。
  • position:指从文件开头数据应被写入的偏移量。 如果 typeof position !== 'number',则数据将写入当前位置。
  • callback:回调函数,有三个参数callback(err, bytesWritten, buffer)。其中err为错误信息, bytesWritten表示写入的字节数,buffer为缓冲区对象。
import { open, close } from "fs";

open("myfile", "wx", (err, fd) => {
  if (err) {
    if (err.code === "EEXIST") {
      console.error("myfile already exists");
      return;
    }

    throw err;
  }
  const buf = Buffer.from("hello");
  console.log(buf);

  fs.write(fd, buf, 0, 8, 0, (err, bytesWritten, buffer) => {
    if (err) {
      console.error(err);
    } else {
      console.log(bytesWritten);
      console.log(buffer.toString());
      console.log(buf);
    }
    close(fd, (err) => {
      if (err) throw err;
    });
  });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

fs.write(fd, string[, position[, encoding]], callback)将 string 写入 fd 指定的文件。

  • fd:通过fs.open()方法返回的文件描述符。
  • string:数据源,是一个字符串。
  • position指从文件开头数据应被写入的偏移量。 如果 typeof position !== 'number',则数据将写入当前位置。
  • encoding是预期的字符串编码。
  • callback:回调函数,有三个参数callback(err, written, string)。其中err为错误信息, written表示写入的字节数。

fs.write()在同一个文件上多次使用而不等待回调是不安全的。对于这种情况,推荐使用fs.createWriteStream()(对于性能敏感的代码也推荐使用它)。

# fs.writeFile()

fs.writeFile(file, data[, options], callback)方法,可以向指定的文件中写入内容。

  • file,必选参数,需要指定一个文件路径的字符串,表示文件的存放路径
  • data,必选参数,表示要写入的内容
  • options,该参数是一个对象,包含{encoding, mode, flag}。默认 encoding 为'utf8', mode 为0666flag'w'
  • callback,必选参数,文件写入完成后的回调参数
const fs = require("fs");

fs.writeFile("message.txt", "Hello Node.js", "utf8", (err) => {
  if (err) {
    console.error(`文件写入失败:${err.message}`);
    return;
  }
  console.log(`文件写入成功`);
});
1
2
3
4
5
6
7
8
9

fs.writeFile()在同一个文件上多次使用而不等待回调是不安全的。对于这种情况,推荐使用fs.createWriteStream()(对于性能敏感的代码也推荐使用它)。

# fs.stat()

fs.stat(path[, options], callback)查看文件状态

const fs = require("fs");

fs.stat(__filename, function (err, stats) {
  if (err) {
    console.error(err);
    return;
  }
  // birthtime文件创建时间。mtime文件内容发生变化,文件的修改时间
  console.log(stats);
  if (stats.isFile()) {
    console.log("当前路径对应的是一个文件");
  } else if (stats.isDirectory()) {
    console.log("当前路径对应的是一个文件夹");
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

不推荐在调用fs.open()fs.readFile()fs.writeFile()之前使用fs.stat()检查文件是否存在。 而是,用户代码应该直接打开/读取/写入文件,并在文件不可用时处理引发的错误

stats 类中的方法有:

方法 描述
stats.isFile() 如果是文件返回 true,否则返回 false。
stats.isDirectory() 如果是目录返回 true,否则返回 false。
stats.isBlockDevice() 如果是块设备返回 true,否则返回 false。
stats.isCharacterDevice() 如果是字符设备返回 true,否则返回 false。
stats.isSymbolicLink() 如果是软链接返回 true,否则返回 false。
stats.isFIFO() 如果是 FIFO,返回 true,否则返回 false。FIFO 是 UNIX 中的一种特殊类型的命令管道。
stats.isSocket() 如果是 Socket 返回 true,否则返回 false。

# fs.access()

fs.access(path[, mode], callback)检查文件是否存在而不对其进行操作。mode 参数是可选的整数,指定要执行的可访问性检查,可查阅文件访问的常量

import { access, constants } from "fs";

const file = "package.json";

// 检查当前目录中是否存在该文件。
access(file, constants.F_OK, (err) => {
  console.log(`${file} ${err ? "does not exist" : "exists"}`);
});

// 检查文件是否可读。
access(file, constants.R_OK, (err) => {
  console.log(`${file} ${err ? "is not readable" : "is readable"}`);
});

// 检查文件是否可写。
access(file, constants.W_OK, (err) => {
  console.log(`${file} ${err ? "is not writable" : "is writable"}`);
});

// 检查当前目录中是否存在文件,是否可写。
access(file, constants.F_OK | constants.W_OK, (err) => {
  if (err) {
    console.error(`${file} ${err.code === "ENOENT" ? "does not exist" : "is read-only"}`);
  } else {
    console.log(`${file} exists, and it is writable`);
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

在调用fs.open()fs.readFile()fs.writeFile()之前,不要使用fs.access()检查文件的可访问性。 这样做会引入竞争条件,因为其他进程可能会在两次调用之间更改文件的状态。 而是,用户代码应直接打开/读取/写入文件,并处理无法访问文件时引发的错误。

“文件写入”的一种写法:

import { open, close } from "fs";

open("myfile", "wx", (err, fd) => {
  if (err) {
    if (err.code === "EEXIST") {
      console.error("myfile already exists");
      return;
    }

    throw err;
  }

  try {
    writeMyData(fd);
  } finally {
    close(fd, (err) => {
      if (err) throw err;
    });
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

“文件读取”的一种写法:

import { open, close } from "fs";

open("myfile", "r", (err, fd) => {
  if (err) {
    if (err.code === "ENOENT") {
      console.error("myfile does not exist");
      return;
    }

    throw err;
  }

  try {
    readMyData(fd);
  } finally {
    close(fd, (err) => {
      if (err) throw err;
    });
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# fs.appendFile()

fs.appendFile(path, data[, options], callback)将数据追加到文件,如果该文件尚不存在,则创建该文件。 data 可以是字符串或 Buffer。

import { appendFile } from "fs";

appendFile("message.txt", "data to append", (err) => {
  if (err) throw err;
  console.log('The "data to append" was appended to file!');
});
1
2
3
4
5
6
import { open, close, appendFile } from "fs";

function closeFd(fd) {
  close(fd, (err) => {
    if (err) throw err;
  });
}

open("message.txt", "a", (err, fd) => {
  if (err) throw err;

  try {
    appendFile(fd, "data to append", "utf8", (err) => {
      closeFd(fd);
      if (err) throw err;
    });
  } catch (err) {
    closeFd(fd);
    throw err;
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# fs.createReadStream()

fs.createReadStream(path[, options])

  • options
    • flags:默认值'r'。请参阅文件系统标志
    • encoding:默认值null
    • fd:默认值null
    • mode:默认值0o666
    • autoClose:默认值true
    • emitClose: 默认值true
    • start:开始读取的位置
    • end:默认值Infinity
    • highWaterMark:默认值 64 * 1024
    • fs:默认值 null

# fs.createWriteStream()

# 文件的模式

mode 参数是使用以下常量的逻辑或创建的数字位掩码:

常量 八进制 描述
fs.constants.S_IRUSR 0o400 所有者可读取
fs.constants.S_IWUSR 0o200 所有者可写入
fs.constants.S_IXUSR 0o100 所有者可执行/搜索
fs.constants.S_IRGRP 0o40 群组可读取
fs.constants.S_IWGRP 0o20 群组可写入
fs.constants.S_IXGRP 0o10 群组可执行/搜索
fs.constants.S_IROTH 0o4 其他人可读取
fs.constants.S_IWOTH 0o2 其他人可写入
fs.constants.S_IXOTH 0o1 其他人可执行/搜索

构建 mode 的一种更简单的方法是使用三个八进制数字的序列(例如 765)。 最左边的数字(示例中的 7)指定文件所有者的权限。 中间的数字(示例中的 6)指定群组的权限。 最右边的数字(示例中的 5)指定其他人的权限。

数值 描述
7 可读、可写和可执行
6 可读和可写
5 可读和可执行
4 只读
3 可写和可执行
2 只写
1 只可执行
0 无权限

例如,八进制值 0o765 表示:

  • 所有者可以读取、写入和执行文件。
  • 群组可以读取和写入文件。
  • 其他人可以读取和执行文件。

在需要文件模式的地方使用原始数字时,任何大于 0o777 的值都可能导致特定于平台的行为不支持一致工作。 因此,像 S_ISVTX、S_ISGID 或 S_ISUID 这样的常量不会在 fs.constants 中暴露。

# 文件系统标志

以下标志在 flag 选项接受字符串的任何地方可用。

  • 'a': 打开文件进行追加。 如果文件不存在,则创建该文件。
  • 'ax': 类似于'a'但如果路径存在则失败。
  • 'a+': 打开文件进行读取和追加。 如果文件不存在,则创建该文件。
  • 'ax+': 类似于'a+'但如果路径存在则失败。
  • 'as': 以同步模式打开文件进行追加。 如果文件不存在,则创建该文件。
  • 'as+': 以同步模式打开文件进行读取和追加。 如果文件不存在,则创建该文件。
  • 'r': 打开文件进行读取。 如果文件不存在,则会发生异常。
  • 'r+': 打开文件进行读写。 如果文件不存在,则会发生异常。
  • 'rs+': 以同步模式打开文件进行读写。 指示操作系统绕过本地文件系统缓存。这主要用于在 NFS 挂载上打开文件,因为它允许跳过可能过时的本地缓存。 它对 I/O 性能有非常实际的影响,因此除非需要,否则不建议使用此标志。这不会将fs.open()fsPromises.open()变成同步阻塞调用。 如果需要同步操作,应该使用类似fs.openSync()的东西。
  • 'w': 打开文件进行写入。 创建(如果它不存在)或截断(如果它存在)该文件。
  • 'wx': 类似于'w'但如果路径存在则失败。
  • 'w+': 打开文件进行读写。 创建(如果它不存在)或截断(如果它存在)该文件。
  • 'wx+': 类似于'w+'但如果路径存在则失败。

# FS 常量

以下常量由 fs.constants 导出。并非每个常量都适用于每个操作系统。要使用多个常量,请使用按位或|运算符。

import { open, constants } from "fs";

const { O_RDWR, O_CREAT, O_EXCL } = constants;

open("/path/to/my/file", O_RDWR | O_CREAT | O_EXCL, (err, fd) => {
  // ...
});
1
2
3
4
5
6
7

# 文件访问的常量

以下常量旨在与fs.access()一起使用。

常量 描述
F_OK 指示文件对调用进程可见的标志。 这对于确定文件是否存在很有用,但没有说明 rwx 权限。 未指定模式时的默认值。
R_OK 指示文件可以被调用进程读取的标志。
W_OK 指示文件可以被调用进程写入的标志。
X_OK 指示文件可以被调用进程执行的标志。 这在 Windows 上不起作用(行为类似于 fs.constants.F_OK)。

# 文件复制的常量

以下常量旨在与fs.copyFile()一起使用。

常量 描述
COPYFILE_EXCL 如果存在,如果目标路径已经存在,复制操作将失败并显示错误。
COPYFILE_FICLONE 如果存在,复制操作将尝试创建写时复制引用链接。 如果底层平台不支持写时复制,则使用回退复制机制。
COPYFILE_FICLONE_FORCE 如果存在,复制操作将尝试创建写时复制引用链接。 如果底层平台不支持写时复制,则操作将失败并显示错误。

# 文件打开的常量

以下常量旨在与fs.open()一起使用。

常量 描述
O_RDONLY 指示打开文件以进行只读访问的标志。
O_WRONLY 指示打开文件以进行只写访问的标志。
O_RDWR 指示打开文件以进行读写访问的标志。
O_CREAT 如果文件不存在则指示创建文件的标志。
O_EXCL 如果设置了 O_CREAT 标志并且文件已经存在,则指示打开文件应该失败的标志。
O_NOCTTY 标志表示如果路径标识一个终端设备,打开路径不应导致该终端成为进程的控制终端(如果进程还没有一个)。
O_TRUNC 标志表示如果文件存在并且是一个普通文件,并且该文件被成功打开以进行写访问,则其长度应被截断为零。
O_APPEND 指示数据将追加到文件末尾的标志。
O_DIRECTORY 如果路径不是目录,则表示打开应该失败的标志。
O_NOATIME 指示对文件系统的读取访问的标志将不再导致与文件关联的 atime 信息的更新。 此标志仅在 Linux 操作系统上可用。
O_NOFOLLOW 如果路径是符号链接,则表示打开应该失败的标志。
O_SYNC 指示文件为同步 I/O 打开的标志,写操作等待文件完整性。
O_DSYNC 指示文件为同步 I/O 打开的标志,写操作等待数据完整性。
O_SYMLINK 指示打开符号链接本身而不是它指向的资源的标志。
O_DIRECT 设置后,将尝试最小化文件 I/O 的缓存影响。
O_NONBLOCK 指示在可能的情况下以非阻塞模式打开文件的标志。
UV_FS_O_FILEMAP 设置后,将使用内存文件映射来访问文件。 此标志仅在 Windows 操作系统上可用。 在其他操作系统上,此标志被忽略。

# 文件类型的常量

以下常量旨在与fs.Stats对象的 mode 属性一起使用,以确定文件的类型。

常量 描述
S_IFMT 用于提取文件类型代码的位掩码。
S_IFREG 常规文件的文件类型常量。
S_IFDIR 目录的文件类型常量。
S_IFCHR 面向字符的设备文件的文件类型常量。
S_IFBLK 面向块的设备文件的文件类型常量。
S_IFIFO FIFO/管道的文件类型常量。
S_IFLNK 符号链接的文件类型常量。
S_IFSOCK 套接字的文件类型常量。

# 文件模式的常量

以下常量旨在与fs.Stats对象的 mode 属性一起使用,以确定文件的访问权限。

常量 描述
S_IRWXU 文件模式指示所有者可读、可写和可执行。
S_IRUSR 文件模式指示所有者可读。
S_IWUSR 文件模式指示所有者可写。
S_IXUSR 文件模式指示所有者可执行。
S_IRWXG 文件模式指示群组可读、可写和可执行。
S_IRGRP 文件模式指示群组可读。
S_IWGRP 文件模式指示群组可写。
S_IXGRP 文件模式指示群组可执行。
S_IRWXO 文件模式指示其他人可读、可写和可执行。
S_IROTH 文件模式指示其他人可读。
S_IWOTH 文件模式指示其他人可写。
S_IXOTH 文件模式指示其他人可执行。