3

Is there a simple way to make node.js increment the filename of a file (i.e., append a number, so that it doesn't overwrite previous files) when saving it?

Below is my attempt:

// can i make this a one-liner?:)
async function incrementIfExists(dirPath, fileName, data, increment=1) {
    const fs = require('fs'),
        path = require('path'),
        errorFunc = (err) => console.error(err);
        
    // Get the last file saved with same fileName (i.e., the one that has the greatest increment number), if there is one   
    let lastFile = await fs.promises.readdir(dirPath) 
        .then(files => {
            let result = '';
            
            for (const name of files) {
                if (!name.startsWith(fileName)) continue;
                if ((name.length < result.length) || (name.length === result.length && name < result)) continue;
                result = name;
            }
            
            return result;
        })
        
        .catch(errorFunc);
        
    if (lastFile) {
        const lastIncrementNr = Number(lastFile.slice((fileName + '_').length));
        if (increment <= lastIncrementNr) increment = lastIncrementNr + 1;
    }
    
    fileName = path.join(dirPath, fileName);
    
    while (true) {
        let breakLoop = await fs.promises.writeFile(lastFile ? fileName + '_' + increment : fileName, data, {encoding: 'utf8', flag: 'wx'})
            
            .then(fd => true)
            
            .catch(err => {
                if (err.code === 'EEXIST') {console.log(err);
                    return false;
                }
                throw err;
            });
        
        if (breakLoop) break;
        increment++;
    }
}

incrementIfExists('.', fileName, data);

Related:
How to not overwrite file in node.js
Creating a file only if it doesn't exist in Node.js

flen
  • 1,905
  • 1
  • 21
  • 44

1 Answers1

4

I use something similar to version uploaded image files on disk. I decided to use the "EEXIST" error to increment the number rather than explicitly iterating the files in the directory.

const writeFile = async(filename, data, increment = 0) => {
  const name = `${path.basename(filename, path.extname(filename))}${increment || ""}${path.extname(filename)}`
  return await fs.writeFile(name, data, { encoding: 'utf8', flag: 'wx' }).catch(async ex => {
    if (ex.code === "EEXIST") return await writeFile(filename, data, increment += 1)
    throw ex
  }) || name
}
const unversionedFile = await writeFile("./file.txt", "hello world")
const version1File = await writeFile("./file.txt", "hello world")
const version2File = await writeFile("./file.txt", "hello world")
BlueWater86
  • 1,773
  • 12
  • 22
  • Great idea! And I should also have checked first for file extensions. At first, I thought I should get the last filename to avoid async calls if there are thousands of files with the same base name. But now I saw your code I realized this isn't plausible and I'm actually wasting time by searching all files in the folder, besides your code being much shorter. Thanks for sharing this! – flen Dec 22 '20 at 08:36
  • Though I think you can make this even shorter (I'm not sure you need async here): ```const writeFile = (filename, data, increment = 0) => { const name = `${path.basename(filename, path.extname(filename))}${increment || ""}${path.extname(filename)}` return fs.promises.writeFile(name, data, { encoding: 'utf8', flag: 'wx' }).catch(ex => { if (ex.code === "EEXIST") return writeFile(filename, data, ++increment) throw ex }) }``` – flen Dec 26 '20 at 22:37
  • If you worked for me I would review your code and reject it. By removing async, you have removed all sign to a caller that the method is asynchronous. Succinct code is good but code that lacks meaning is bad. – BlueWater86 Dec 27 '20 at 10:14
  • 1
    Sorry for this, I'm still learning it, but what's the point of it being async? It will return a promise, but my code already returns a promise anyway. What's the gain of having an `async` function if you're not using `await` anymore? – flen Dec 28 '20 at 14:07
  • I actually don't have a good enough reason in this context to say what I did. Sorry about the tone that came out in that previous comment. – BlueWater86 Dec 30 '20 at 04:41
  • In the context of where the method came from (an ES6 class) the lack of async on any method that returns a promise can cause a lot of confusion and trigger unnecessary future refactors where calling code assumes that it is synchronous. – BlueWater86 Dec 30 '20 at 04:44
  • 2
    No worries! I still don't see a difference, but if naming is a problem you could also just name it `const writeFileAsync`. But you know your code better than me, so I'll just leave it at that. And I think now I see your point, having `async` always there can make it easier to spot it still, especially for automated jobs – flen Dec 30 '20 at 09:52