NodeJs 第十一章 (fs/流/文件[夹]处理)

87 阅读13分钟

介绍

在 Node.js 的生态系统中,文件模块扮演着至关重要的角色,它为开发者提供了处理文件系统的强大能力,从文件的读取、写入到目录的操作,一应俱全。本文将深入探讨 Node.js 的文件模块,帮助你掌握其核心概念和实际应用。

文件模块基础

Node.js 中的文件模块是内置模块,无需额外安装即可使用。通过require('fs')就能引入,这里的fs就是文件系统(File System)的缩写。引入后,你就可以调用其提供的各种方法来操作文件和目录。

读取文件

fs.readFile
(path:String,(err:Error,data)=>{})
接收两个参数:一个是要读取的文件路径,另一个是一个回调函数,用于处理读取的结果。回调函数接收两个参数:一个是错误对象(如果发生错误),另一个是文件内容。

const fs = require('fs');
const path = require('path');

fs.readFile(path.resolve(__dirname,'i.txt'), 'utf8', (err, data) => {
  if (err) {
    console.error(err);
    return;
  }
  console.log(data);
  
})

image.png

写入文件

要向文件写入数据,可以使用 fs.writeFile() 方法。此方法同样接受两个主要参数:一个是要写入的文件路径,另一个是要写入的数据。此外,还可以指定编码选项。

const fs = require('fs');
const path = require('path');

fs.writeFile(path.join(__dirname, 'text.txt'), 'Hello Node.js', (err) => {
    if (err) {
        console.log(err);
        throw err
    }
    console.log('File has been created');
})

image.png 注意:文件存在也可以写入,会覆盖原来的文件

追加内容到文件

如果你想将一些文本追加到现有文件的末尾,而不是覆盖它,可以使用 fs.appendFile() 方法。

const fs = require('fs');
const path = require('path');
fs.appendFile(path.join(__dirname, 'text.txt'), '\nThis is an appended text', (err) => {
    if (err) {
        console.log(err);
        throw err
    }
    console.log('Appended to file');

})

image.png

删除文件

使用 fs.unlink() 方法可以删除一个文件。这个方法接受两个参数:一个是文件路径,另一个是一个回调函数,用于处理完成后的结果。

const fs = require('fs');
const path = require('path');

fs.unlink(path.join(__dirname, 'text.txt'), (err) => {
  if (err) {
    console.error(err);
    return;
  }
  console.log('File deleted successfully');
});

image.png

异步与同步操作

异步操作

大多数 fs 模块的方法都是异步的,这意味着它们不会阻塞其他操作的执行。这对于提高应用性能非常关键。

同步操作

如果你需要确保文件操作完成后才继续执行其他代码,可以使用同步版本的方法。这些方法以 sync 结尾, 例如 fs.readFileSync()

异步同步
readFilereadFileSync
writeFilewriteFileSync
appendFileappendFileSync
unlinkunlinkSync

用法

const fs = require('fs');

try {
  const data = fs.readFileSync('./example.txt', 'utf8');
  console.log(data);
} catch (err) {
  console.error(err);
}

流式文件处理是 Node.js 中一个非常重要的概念,它允许你高效地处理大量数据而不必一次性将所有数据加载到内存中。这种方法特别适合于处理大文件或者需要实时处理的数据流,如文件、网络传输等。Node.js 提供了四种类型的流:

  • 可读流(Readable):可以从流中读取数据。
  • 可写流(Writable):可以向流中写入数据。
  • 双工流(Duplex):既可以读取也可以写入数据。
  • 转换流(Transform):一种特殊的双工流,可以在读取数据时对其进行修改。

流事件

可读流主要涉及以下几种事件:

  • data:当有数据可用时触发。
  • end:当没有更多数据可读时触发。
  • error:当发生错误时触发。
  • close:当流关闭时触发。
const fs = require('fs');

const readStream = fs.createReadStream('./example.txt');

readStream.on('data', (chunk) => {
  console.log(`Received ${chunk.length} bytes of data.`);
});

readStream.on('end', () => {
  console.log('There will be no more data.');
});

readStream.on('error', (err) => {
  console.error(`Error: ${err.message}`);
});

创建可读流

fs.createReadStream用于创建一个可读流,以流的方式读取文件,特别适合处理大文件,能有效节省内存资源。要熟练运用它,了解其参数至关重要:

  • path:这是createReadStream的第一个参数,必填项,它表示要读取的文件路径,可以是相对路径或绝对路径。例如fs.createReadStream('largeFile.txt')中的largeFile.txt就是相对路径,若文件在其他目录,需指定完整路径,如fs.createReadStream('/home/user/data/largeFile.txt') 。但当fd参数被指定时,ReadStream将忽略path,转而使用文件描述符进行操作。

  • options(可选参数对象)

    • encoding:用于指定读取文件时的编码格式。默认值为null,若不设置,读取的数据将以Buffer形式返回,适用于处理二进制文件或需要对字节数据进行精细控制的场景;设置为utf8,则会以 UTF - 8 编码格式将数据解析为字符串返回,方便直接处理文本内容,如fs.createReadStream('largeFile.txt', { encoding: 'utf8' }) 。

    • fd:文件描述符,若指定了该参数,ReadStream将忽略path,直接使用该文件描述符来操作文件。文件描述符是一个数字,通常在使用fs.open等底层文件操作方法获取,适用于需要复用已打开文件描述符的场景,如const fd = fs.openSync('example.txt', 'r'); const readStream = fs.createReadStream(null, { fd, encoding: 'utf8' }); 。

    • mode:设置文件的权限(和粘性位),仅在文件被创建时生效。默认值是0o666(八进制表示),对应十进制的 438 ,表示文件所有者、所属组和其他用户都有读和写的权限。可以根据需要修改,如fs.createWriteStream('output.txt', {mode: 0o644}) ,表示文件所有者有读和写权限,所属组和其他用户只有读权限。

    • autoClose:布尔值,默认值为true,表示在读取完成或发生错误时自动关闭文件描述符。这是一种常见的操作方式,能确保文件资源被正确释放。若设置为false,则需要手动关闭文件,如fs.createReadStream('largeFile.txt', { encoding: 'utf8', autoClose: false }) ,这种情况适用于需要对文件描述符进行更精细控制的场景,如在一个复杂的文件操作流程中,需要在特定时机关闭文件。

    • emitClose:布尔值,默认值为true,若设置为true,在流被销毁后,会触发一个'close'事件。这对于需要在文件关闭时执行特定清理操作的场景很有用,比如readStream.on('close', () => { console.log('File descriptor has been closed'); }); 。

    • startend:指定从文件中读取的字节范围,两个值都是包含性的,从 0 开始计数。例如fs.createReadStream('largeFile.txt', { start: 100, end: 200 }) ,表示从文件的第 100 个字节开始读取,直到第 200 个字节(包含第 200 个字节)。这在需要读取文件特定部分内容时非常实用,比如读取文件的头部元数据或者特定的数据块。

    • highWaterMark:控制内部缓冲区在停止从底层资源读取之前可以存储的最大字节数,默认值是 64KB(即 65536 字节)。比如设置为1024 * 10(即 10KB) ,意味着每次读取的数据块最大为 10KB,可根据实际需求调整,如fs.createReadStream('largeFile.txt', { encoding: 'utf8', highWaterMark: 1024 * 10 }) 。较小的数据块大小适用于内存有限或对数据处理频率要求较高的场景;较大的数据块大小则能提高读取效率,减少 I/O 操作次数,适用于追求整体读取速度的场景。

    • fs:允许覆盖open、read和close的fs实现。这在需要自定义文件操作逻辑,或者使用特定的文件系统抽象层时非常有用。例如,在一些测试场景中,可能需要模拟文件操作,就可以通过这个参数来替换默认的fs实现。

    • flags:指定文件打开的模式,默认值是'r',表示只读模式。这种模式下,文件只能被读取,无法进行写入操作。其他常见模式包括:

      • 'r+':可读可写模式,不会截断文件,即可以在文件原有内容的基础上进行读写操作。
      • 'w':写入模式,会截断文件,若文件存在,其内容会被清空后再写入;若文件不存在,则创建新文件。
      • 'a':追加模式,新数据会追加到文件末尾,若文件不存在,同样会创建新文件。
      • 'wx':类似'w',但如果文件已存在,创建操作会失败,返回错误。
      • 'ax':类似'a',但如果文件已存在,创建操作会失败,返回错误。

例如fs.createReadStream('largeFile.txt', { encoding: 'utf8', flags: 'r+' }) ,设置为'r+'模式,以便对文件进行读写操作。

const fs = require('fs');
const path = require('path');


const readStream = fs.createReadStream(path.join(__dirname, 'i.txt'), { encoding: 'utf8' });
let buffer = '';

readStream.on('data', (chunk) => {
  console.log(`Received ${chunk.length} bytes of data.`);
  buffer += chunk;
});
readStream.on('end', () => {
  console.log('Read completed.');
  console.log(buffer);
  console.log(buffer.toString());

});

readStream.on('error', (err) => {
  console.error(err);
});

image.png

创建可写流

fs.createWriteStream用于创建一个可写流,实现文件的流式写入,适用于写入大文件或者需要持续写入数据的场景。其参数如下:

  • path:可以是字符串、Buffer或URL,用于指定文件路径,是必填参数。例如fs.createWriteStream('output.txt') 中的output.txt是相对路径;若为绝对路径,像fs.createWriteStream('/user/data/output.txt')也可行;若传入Buffer或URL,则需确保其正确指向目标文件路径 。当fd参数被指定时,WriteStream将忽略path,转而使用文件描述符进行操作。
  • options(可选参数对象)
    • flags:指定文件系统标志,默认值是'w',表示写入模式,会截断文件。若文件存在,其内容会被清空后再写入;若文件不存在,则创建新文件。其他常见模式有:
      • 'a':追加模式,新数据会追加到文件末尾,若文件不存在,同样会创建新文件,如fs.createWriteStream('output.txt', {flags: 'a'}) 。
      • 'r+':可读可写模式,不会截断文件,即可以在文件原有内容的基础上进行读写操作 。
    • encoding:指定编码格式,默认值是'utf8'。设置为'utf8'时,写入的数据会以 UTF - 8 编码格式写入文件;若不设置或设置为null,写入的数据需是Buffer类型,比如fs.createWriteStream('output.txt', {encoding: 'utf8'}) 。
    • fd:文件描述符,若指定了该参数,WriteStream将忽略path,直接使用该文件描述符来操作文件。文件描述符通常在使用fs.open等底层文件操作方法获取,适用于需要复用已打开文件描述符的场景,例如const fd = fs.openSync('example.txt', 'w'); const writeStream = fs.createWriteStream(null, { fd, encoding: 'utf8' }); 。
    • mode:设置文件的权限(和粘性位),仅在文件被创建时生效,默认值是0o666(八进制表示),对应十进制的 438 ,表示文件所有者、所属组和其他用户都有读和写的权限。可以根据需要修改,如fs.createWriteStream('output.txt', {mode: 0o644}) ,表示文件所有者有读和写权限,所属组和其他用户只有读权限。
    • autoClose:布尔值,默认值为true,表示在发生'error'或'finish'事件时,文件描述符会自动关闭。这是一种常见的操作方式,能确保文件资源被正确释放。若设置为false,则需要手动关闭文件,适用于需要对文件描述符进行更精细控制的场景,如fs.createWriteStream('output.txt', {autoClose: false}) 。
    • emitClose:布尔值,默认值为true,若设置为true,在流被销毁后,会触发一个'close'事件。这对于需要在文件关闭时执行特定清理操作的场景很有用,比如writeStream.on('close', () => { console.log('File descriptor has been closed'); }); 。
    • start:允许从文件起始位置之后的某个位置开始写入数据。例如fs.createWriteStream('output.txt', {start: 100}) ,表示从文件的第 100 个字节位置开始写入新数据,这在需要更新文件特定部分内容时很实用。
    • highWaterMark:控制内部缓冲区在停止向底层资源写入之前可以存储的最大字节数,默认值是 16384 字节(即 16KB)。当写入的数据量超过这个值时,写入操作会暂停,直到缓冲区有空间。例如fs.createWriteStream('output.txt', {highWaterMark: 1024 * 5})设置缓冲区大小为 5KB ,较小的缓冲区适合对写入频率要求高、数据量较小的场景;较大的缓冲区能提高写入效率,减少 I/O 操作次数,适用于写入大数据量的场景。
    • flush:布尔值,默认值为false,若设置为true,在关闭底层文件描述符之前,会先将其刷新,确保数据都已写入文件,例如fs.createWriteStream('output.txt', {flush: true}) 。
const fs = require('fs');
const path = require('path');

const writeStream = fs.createWriteStream(path.join(__dirname, 'i2.txt'));

writeStream.write('This is a large file content.', 'utf8');

writeStream.end();

writeStream.on('finish', () => {
  console.log('Writing finished.');
});

writeStream.on('error', (err) => {
  console.error(err);
});

自定义可读流

除了使用内置流之外,还可以通过继承 stream.Readable 类来创建自定义的可读流。

const { Readable } = require('stream');

class CustomReadable extends Readable {
  constructor(options) {
    super(options);
    this.data = ['Hello', 'World'];
    this.index = 0;
  }

  _read() {
    if (this.index === this.data.length) {
      this.push(null);
      return;
    }
    const chunk = this.data[this.index++];
    this.push(chunk + '\n');
  }
}

const customStream = new CustomReadable();
customStream.pipe(process.stdout);

管道流

在 Node.js 中,管道流是一种基于流的机制,它允许将一个可读流(如fs.createReadStream创建的流)和一个可写流(如fs.createWriteStream创建的流)连接起来,实现数据从可读流到可写流的自动传输 。其原理是通过pipe方法,在可读流读取到数据后,自动将数据写入到可写流中,无需开发者手动处理数据的读取和写入逻辑。

优势

  • 高效性:避免了一次性将大量数据加载到内存中,对于大文件处理尤其有效。以文件复制为例,使用管道流时,数据是逐块从源文件读取并写入目标文件,不会占用过多内存。
  • 简洁性:大大简化了数据传输的代码。如上述文件复制示例,只需一行readStream.pipe(writeStream)就能完成数据从源文件到目标文件的传输,而无需复杂的循环读取和写入操作。
const fs = require('fs');
const path = require('path');


const readStream = fs.createReadStream(path.join(__dirname, 'i.txt'));
const writeStream = fs.createWriteStream(path.join(__dirname, 'i2.txt'));

readStream.pipe(writeStream);

image.png

多流连接

Node.js 的管道流不仅支持单个可读流和单个可写流的连接,还可以实现多个流的连接。例如,可以将多个可读流的数据依次传输到一个可写流中:

const fs = require('fs');

const readStream1 = fs.createReadStream('file1.txt');

const readStream2 = fs.createReadStream('file2.txt');

const writeStream = fs.createWriteStream('combinedFile.txt');

readStream1.pipe(writeStream, { end: false });

readStream1.on('end', () => {
    readStream2.pipe(writeStream);
});

readStream1.on('error', (err) => {
    console.error('Error reading file1:', err);
});

readStream2.on('error', (err) => {
    console.error('Error reading file2:', err);
});

writeStream.on('error', (err) => {
    console.error('Error writing to combinedFile:', err);
});

双工流和转换流

双工流同时支持读取和写入操作,而转换流则是在读取的同时对数据进行某种形式的处理。

双工流

双工流在许多场景下都非常有用,尤其是在需要进行双向通信的情况下。以下是一些常见的使用场景:

  • TCP/IP 通信:在网络通信中,客户端和服务器之间的数据交换通常需要双向通信,这时可以使用双工流。
  • 文件传输:在传输文件的过程中,可能需要同时读取和写入文件,这时也可以使用双工流。
  • 管道处理:在处理数据流时,可能需要对数据进行过滤或转换,这时可以将多个读取和写入操作组合在一起形成一个双工流。 双工流既可以读取数据,也可以写入数据。
创建双工流
const { Duplex } = require('stream');

const duplexStream = new Duplex({
  write(chunk, encoding, callback) {
    console.log(chunk.toString());
    callback();
  },
  read(size) {
    this.push('Hello, World!\n');
    this.push(null);
  }
});

duplexStream.pipe(process.stdout);
双工流事件

双工流同时继承了可读流和可写流的事件。

暂停和恢复

  • pause():暂停数据流动。
  • resume():恢复数据流动。
const { Duplex } = require('stream');

class MyDuplexStream extends Duplex {
    constructor(options) {
        super(options);
    }

    _read(size) {
        // 实现读取逻辑
        this.push(null); // 结束读取
    }

    _write(chunk, encoding, callback) {
        // 实现写入逻辑
        console.log('写入数据:', chunk.toString());
        callback();
    }
}

// 使用自定义的双工流
const myStream = new MyDuplexStream();

myStream.pause(); // 暂停数据流动
setTimeout(() => {
    myStream.resume(); // 恢复数据流动
}, 5000);

创建转换流

转换流是一种特殊的双工流,用于数据转化 Node.js 提供了内置的转换流,如 stream.Transform 类,可以用来实现更复杂的流式处理逻辑。

const stream = require('stream');
const util = require('util');

// 定义一个转换流类
function MyTransform(options) {
    if (!(this instanceof MyTransform)) return new MyTransform(options);
    stream.Transform.call(this, options);
}

// 继承 Transform 类
util.inherits(MyTransform, stream.Transform);

// 实现 _transform 方法来定义数据处理逻辑
MyTransform.prototype._transform = function(chunk, encoding, callback) {
    this.push(chunk.toString().toUpperCase());
    callback();
};

// 使用转换流
const myTransform = new MyTransform();

const readStream = fs.createReadStream('./input.txt');
const writeStream = fs.createWriteStream('./output.txt');

readStream.pipe(myTransform).pipe(writeStream);

在这个例子中,我们创建了一个自定义的转换流 MyTransform,它会将读取到的所有文本转换为大写形式。然后我们通过管道将这个转换流连接到一个读取流和一个写入流之间,从而实现了文件内容的大写转换。

优雅地关闭流

当发生错误时,最好关闭流以防止进一步的数据处理。这可以通过调用流的 .destroy() 方法来完成,该方法会立即关闭流,并触发 close 事件。

const fs = require('fs');

// 创建可写流
const writableStream = fs.createWriteStream('output.txt');

// 监听 error 事件
writableStream.on('error', (err) => {
  console.error('Error occurred:', err.message);
  // 关闭流
  writableStream.destroy();
});

// 写入数据到不存在的文件,这将触发 error 事件
writableStream.write('Hello, World!');

目录操作

创建目录

异步创建

const fs = require('fs');

const newDir = 'newDirectory';

fs.mkdir(newDir, err => {

if (err) {
    console.error(err);
    return;
}
console.log(`Directory ${newDir} created`);

});
```JavaScript
### 同步创建

```JavaScript
try {
  fs.mkdirSync('newFolder');
  console.log('Directory created successfully!');
} catch (err) {
  console.error(err);
}

递归创建目录

为了递归地创建目录,可以使用 fs.mkdir() 方法的 recursive 选项或使用 fs.promises.mkdir() 方法:

// 使用 options 参数
fs.mkdir('path/to/newFolder', { recursive: true }, (err) => {
  if (err) throw err;
  console.log('Directory created successfully!');
});

// 使用 promises
async function createRecursiveDir() {
  try {
    await fs.promises.mkdir('path/to/newFolder', { recursive: true });
    console.log('Directory created successfully!');
  } catch (err) {
    console.error(err);
  }
}

createRecursiveDir();

读取目录

异步读取

const fs = require('fs');

const dir = '.';

fs.readdir(dir, (err, files) => {

if (err) {
    console.error(err);
    return;
}

console.log(files);

});

同步读取目录

try {
  const files = fs.readdirSync('targetFolder');
  for (const file of files) {
    console.log(file);
  }
} catch (err) {
  console.error(err);
}

删除目录

要删除一个空目录,可以使用 fs.rmdir() 方法。若要删除非空目录,需要先递归删除其下的所有内容。

删除空目录

fs.rmdir('emptyFolder', (err) => {
  if (err) throw err;
  console.log('Directory removed successfully!');
});

删除非空目录

对于非空目录,我们通常需要先列出并删除目录内的所有内容,然后再删除目录本身。这里提供一个简单的示例函数来实现这一点:

async function deleteDirectory(path) {
  const files = await fs.promises.readdir(path);

  for (const file of files) {
    const curPath = path + '/' + file;
    const stats = await fs.promises.stat(curPath);

    if (stats.isDirectory()) {
      // Recursively delete subdirectories
      await deleteDirectory(curPath);
    } else {
      // Delete files
      await fs.promises.unlink(curPath);
    }
  }

  // Remove the directory itself
  await fs.promises.rmdir(path);
}

deleteDirectory('nonEmptyFolder')
  .then(() => console.log('Directory deleted successfully!'))
  .catch((err) => console.error(err));

修改目录属性

Node.js 的 fs 模块提供了多种方法来修改文件系统的属性,包括目录。例如,我们可以改变目录的权限:

更改目录权限

    fs.chmod('directoryPath', '0777', (err) => {
      if (err) throw err;
      console.log('Permissions changed successfully!');
    });

硬链接

概念

硬链接是文件的另一个名字,它与原文件共享同一个 inode(索引节点)。inode 是文件系统中用于存储文件元数据(如文件权限、所有者、大小、创建时间等)以及指向文件数据块的指针的数据结构。当创建一个硬链接时,实际上是在文件系统中增加了一个指向相同数据块的目录项,因此硬链接和原文件在本质上是完全相同的文件,只是名字不同。这意味着对其中一个文件的修改会立即反映到另一个文件上,因为它们共享底层的数据。

使用场景

  • 数据冗余与备份:在需要避免数据冗余的情况下,硬链接非常有用。例如,对于一些占用大量磁盘空间的文件,如大型日志文件或数据库文件,如果需要在不同位置访问,创建硬链接可以节省磁盘空间,因为实际数据只存储一份。
  • 文件版本管理:在版本控制系统中,硬链接可以用于管理文件的不同版本。通过创建硬链接,可以在不复制文件内容的情况下,对文件进行不同版本的管理,提高存储效率。

Node.js 中的实现

在 Node.js 中,使用fs.link方法来创建硬链接。其语法如下:


    const fs = require('fs');

    const target = 'originalFile.txt';
    const link = 'hardLink.txt';

    fs.link(target, link, err => {
        if (err) {
            console.error('Error creating hard link:', err);
            return;
        }
        console.log('Hard link created successfully');
    });

在上述代码中,fs.link方法接收两个参数,第一个参数target是原文件的路径,第二个参数link是要创建的硬链接的路径。如果创建过程中出现错误,err会包含错误信息;如果成功,会输出 “Hard link created successfully”。

注意事项

  • 硬链接限制:硬链接不能跨越文件系统。因为每个文件系统都有自己独立的 inode 表,所以无法在不同的文件系统之间创建硬链接。
  • 文件删除:当删除原文件时,硬链接文件仍然存在,并且可以正常访问。只有当所有指向该 inode 的硬链接都被删除时,文件的数据块才会被真正删除,磁盘空间才会被释放。

软链接(符号链接)

概念

软链接,也称为符号链接,是一种特殊的文件,它包含了一个指向另一个文件或目录的路径。软链接并不直接指向文件的数据块,而是指向原文件的路径。因此,软链接可以看作是一个指向原文件的指针,通过它可以访问到原文件。与硬链接不同,软链接有自己独立的 inode,并且它的文件类型是符号链接类型。

使用场景

  • 方便快捷访问:在大型项目中,可能存在一些常用文件位于较深的目录结构中,通过创建软链接,可以将这些文件链接到更方便访问的位置,提高开发效率。
  • 系统配置与部署:在系统配置和部署过程中,软链接常用于将配置文件链接到不同的运行环境中,实现配置的灵活管理。例如,将一个通用的配置文件链接到不同的应用程序目录下,方便统一管理和修改。

Node.js 中的实现

在 Node.js 中,使用fs.symlink方法来创建软链接。其语法如下:

    const fs = require('fs');

    const target = 'originalFile.txt';
    const link ='softLink.txt';

    fs.symlink(target, link, 'file', err => {
        if (err) {
            console.error('Error creating soft link:', err);
            return;
        }
        console.log('Soft link created successfully');
    });

在这段代码中,fs.symlink方法接收三个参数,第一个参数target是原文件的路径,第二个参数link是要创建的软链接的路径,第三个参数'file'表示要创建的是文件链接(如果是目录链接,则使用'dir')。同样,如果创建过程中出现错误,err会包含错误信息;如果成功,会输出 “Soft link created successfully”。

注意事项

  • 链接失效:如果原文件被移动或删除,软链接将失效,因为它指向的路径不再有效。在使用软链接时,需要确保原文件的稳定性。
  • 跨文件系统支持:软链接可以跨越文件系统,这是它与硬链接的一个重要区别。因此,软链接在跨文件系统的文件管理中具有更大的灵活性。

实战

文件加密与解密

让我们来看一个更实际的例子,演示如何使用转换流来加密和解密文件。我们将使用 Node.js 的 crypto 模块来处理加密和解密逻辑。

加密文件

// www.javascriptcn.com code example
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');

class EncryptTransform extends Transform {
  constructor(key, options) {
    super(options);
    this.key = key;
    this.cipher = crypto.createCipheriv('aes-256-cbc', key, Buffer.alloc(16));
  }

  _transform(chunk, encoding, callback) {
    const encryptedChunk = this.cipher.update(chunk);
    this.push(encryptedChunk);
    callback();
  }

  _flush(callback) {
    const finalChunk = this.cipher.final();
    this.push(finalChunk);
    callback();
  }
}

const key = crypto.randomBytes(32);
const encryptStream = new EncryptTransform(key);

fs.createReadStream(path.join(__dirname, 'plaintext.txt'))
  .pipe(encryptStream)
  .pipe(fs.createWriteStream(path.join(__dirname, 'ciphertext.bin')));

在这个例子中,我们定义了一个 EncryptTransform 类,它使用 AES-256-CBC 算法对数据进行加密。然后我们使用这个转换流来加密一个文本文件。

解密文件

// www.javascriptcn.com code example
class DecryptTransform extends Transform {
  constructor(key, options) {
    super(options);
    this.key = key;
    this.decipher = crypto.createDecipheriv('aes-256-cbc', key, Buffer.alloc(16));
  }

  _transform(chunk, encoding, callback) {
    const decryptedChunk = this.decipher.update(chunk);
    this.push(decryptedChunk);
    callback();
  }

  _flush(callback) {
    const finalChunk = this.decipher.final();
    this.push(finalChunk);
    callback();
  }
}

const decryptStream = new DecryptTransform(key);

fs.createReadStream(path.join(__dirname, 'ciphertext.bin'))
  .pipe(decryptStream)
  .pipe(fs.createWriteStream(path.join(__dirname, 'decrypted.txt')));

这个例子展示了如何使用 DecryptTransform 类来解密之前加密的文件。通过这种方式,我们可以轻松地在文件之间传输加密数据,并在目标位置安全地解密数据。