在创建之前重命名文件

时间:2019-10-06 10:45:23

标签: javascript node.js node-modules fs

我刚刚开始学习Node.js,并立即使用下面的代码进行学习:

const fs = require(`fs`);

fs.appendFile(`mynewfile1.txt`, `Hello content!`, err => {
  if (err) throw err;
  console.log(`Saved!`);
});

fs.rename(`mynewfile1.txt`, `myrenamedfile.txt`, err => {
  if (err) throw err;
  console.log(`File Renamed!`);
});

它位于app.js中,并且在运行之前此目录中没有其他文件

当我运行它(使用node app.js bash命令)时,它稳定输出:

> File Renamed!
> Saved!

我知道这两种操作都是异步运行的,并且我认为重命名文件必须比创建新文件快,但是如果我们尝试重命名的文件不存在,那么fs.rename应该会抛出,那么如果不存在第一条语句,该怎么办。否则,它会假装一切正常。

我认为发生这种情况是因为在执行昂贵的I / O节点之前以某种方式评估了操作,就我而言,不是立即创建新文件并重命名,而是立即创建名为myrenamedfile.txt的文件。 / p>

我的问题是:

  1. 我是对的吗?如果是这样,请您指出与此有关的一些文档,否则,请您解释为什么会发生这种情况吗?
  2. 在这种情况下,您还可以指出规范fs.rename行为的规则吗?我的意思是,因为它显然并没有实际重命名现有文件(因为它当时尚未创建),所以它应该做些不同的事情。那么,“不同之处”的文档在哪里? The main documentation未指定。

UPD:

非常感谢所有迄今为止做出回应的好人,我知道promisify模块中的utils或实验性require('fs').promises都可以用来链接操作,但是问题是关于为什么代码有效并且不会引发错误,而不是如何将操作链接在一起。

4 个答案:

答案 0 :(得分:1)

如果“文件重命名!”首先执行“已保存!”部分将创建一个名为mynewfile1.txt

的新文件

如果要保留订单,请在异步函数中使用

const fs = require(`fs`);

await fs.appendFile(`mynewfile1.txt`, `Hello content!`);
console.log(`Saved!`);

await fs.rename(`mynewfile1.txt`, `myrenamedfile.txt`);
console.log(`File Renamed!`);

答案 1 :(得分:1)

是的,您对异步流的假设是正确的。由于文件最初不存在,并且无论 appendFile 是否完成,都将触发重命名,因此它将抛出错误no such file or directory

控制台显示:File Renamed!,因为您只是无条件打印了它。

要处理这种情况,请分别使用以下 sync方法

const fs = require(`fs`);

fs.appendFileSync(`mynewfile1.txt`, `Hello content!`);
console.log(`Saved!`);

fs.renameSync(`mynewfile1.txt`, `myrenamedfile.txt`);
console.log(`File Renamed!`);

或使用异步/等待

const fs = require(`fs`);
const util = require('util');

const appendAndRename = async () => {
    try {
        await util.promisify(fs.appendFile)(`mynewfile1.txt`, `Hello content!`);
        console.log(`Saved!`);

        await util.promisify(fs.rename)(`mynewfile1.txt`, `myrenamedfile.txt`);
        console.log(`File Renamed!`);
    } catch(err) {
        console.log(err);
        throw err;
    }
}

appendAndRename();

确保您的 await 语句始终包裹在 try / catch 块中,并使用 async 关键字声明直接父函数。

希望这会有所帮助:)

答案 2 :(得分:1)

在下面找到有效的(经过测试的)代码段。使用内置的“ util”模块来实现fs功能。

    const fs = require(`fs`);
    const util = require('util');

    async function doTheOpAsync() {
        const appendFile = util.promisify(fs.appendFile);
        const rename = util.promisify(fs.rename);
        try{
            await appendFile(`mynewfile1.txt`, `Hello content!`);
            console.log(`Saved!`);
            await rename(`mynewfile1.txt`, `myrenamedfile.txt`);
            console.log(`File Renamed!`);
        }catch(ex){
            console.log(ex);
        }
    }
    doTheOpAsync();

答案 3 :(得分:0)

好吧,我终于找到了一些时间来研究这个话题,而我的发现似乎可以很好地解释正在发生的事情。这是最可能的答案:

  1. 不。

Node既不预编译代码也不“分批更新/操作”(例如,React这样做)。

fs.rename,是在编译器到达其被调用的位置并且直到这里都没有黑魔法发生时才执行的。

  1. 当然可以,但是答案却很隐蔽。

Node documentation引用a Linux manual page暗示fs.rename(我使用的at least on Unix-based operational systems)的行为与Linux rename完全相同。

因此,我们进入了操作系统级别的命令。

现在,在基于Unix的操作系统上的操作大部分是atomic operations,这(在非常基本的水平上)意味着以无法区分任何部分的方式执行操作。因此,不能将操作分成较小的部分。

对于我们的特定情况,很重要的一点是要知道,这个概念意味着在执行某个操作时,只有该操作本身才能感知到它正在执行的任务的任何进度。仅当操作100%完成时,系统的其余部分才会开始查看操作结果。当有99%的工作是从手术外部完成时,无法感知状态。否则,该操作将不是原子操作。

现在,有几个引用(a comment on PHP Docsan answer on askubuntuan answer on serverfault)表示在重命名操作之前开始的任何写/读操作将在原子操作世界中和平地继续。 / p>

考虑到fs.appendFile只是fs.openfs.write组合的包装,我们可以说fs.appendFile不是原子的

因此,很可能是在基于Unix的系统上运行上述脚本时发生的情况:

  1. fs.appendFile已到达。 It's code被执行。
  2. fs.appendFile本身是同步的,因此将完全执行,直到调用fs.writeFile
  3. 在我们的例子中,第一个异步操作是对fs.open的调用。因此,fs.writeFile一直执行到调用fs.open为止,然后将异步操作添加到队列中。 JS编译器现在可以返回到最初执行的文件(我们的源文件)。
  4. 看到fs.rename并开始执行。
  5. 同时fs.open已解决,因为创建空文件的速度非常快。但是,appendFile尚未完成运行,因为尚未开始写入操作。编译器将对writeAll的调用添加到异步队列中。
  6. 因此,现在fs.rename正在执行,而我们已经有了该文件,但它仍为空。但是,这并不重要,因为所需的文件是存在的,因此,重命名成功并且File Renamed!被打印到控制台。
  7. 写操作不受重命名的影响,因为它使用内部ID而不是名称来标识文件,因此,它成功完成了写入操作,并且Saved!已打印到控制台。

不幸的是,我无法通过测试来证实自己的猜测,因为显然不可能在fs.rename回调中执行任何有意义的事情(例如读取文件内容以确认其为空),因为操作系统操作比JS代码。另外,也无法暂停/推迟/阻止它们。

如果有任何大师会读过这篇文章,请告诉我我的想法是否正确。


最后,我找到了一种检验我的猜测的方法:

如果我写入大量数据(例如'Hello content!'.repeat(9e6)),则在写操作完成之前,我有足够的时间在fs.statSync回调中调用fs.rename。它报告该文件存在,并且大小为0。

如果我随后在appendFile回调中调用相同的文件,它将报告所有相同的文件,但文件的size现在为126000000

而且,最重要的是,如果我添加以下行:

console.log(execSync('lsof myrenamedfile.txt', {encoding: 'utf8'}));

rename回调中,我可以看到仍在写入文件的节点进程,如下所示:

COMMAND   PID      USER   FD   TYPE DEVICE     SIZE/OFF           NODE NAME
node    50837  username   20w   REG    1,4   0 15997608   myrenamedfile.txt