0

The goal: given a file path and an array of file paths in input, I need to check if there is a file in the array (even with a different name) that is equal to the input file. I need to read the files asynchronously and stop if an equal file is found (in that case I don't want to read all the files in the array).

I would like to use only ecma6 features without additional libraries. I am using node.js, thus to compare the files is sufficient to use the compare buffers function:

const fs = require("fs");

let buffer1 = fs.readFileSync(filePath1);
let buffer2 = fs.readFileSync(filePath2);
if (buffer1.compare(buffer2) === 0) {
    console.log("Files are equal");  
} else {
    console.log("Files are different");  
}

Basically for each of the file path in the array I want to check equality sequentially (but with asynchronous read) with a function like this:

function isFileEqual (fileInputBuffer, path) {
    return new Promise((resolve, reject) => {
        fs.readFile(path, (err, data) => {
            if (err) {
                resolve(false);
            }

            if (data.compare(fileInputBuffer) === 0) {                  
                resolve(true);
            } else {
                resolve(false);
            }   
        });     
    });
}

I came up with a solution taking advantage of the "reject fast" property of Promises.all, which cause to reject immediately in case any of the input promises reject without wait for the other promises to complete:

const fs = require("fs");

let paths = ["file2", "file3", "file1_copy", "file4", "file5"];


checkIfFileAlreadyExistsAsync("file1", paths).then(path => {
    console.log("\n", "File equal found at: ", path, "\n");
}).catch(err => {
    console.log(err);
});



function checkIfFileAlreadyExistsAsync(filePath, paths) {

    return new Promise( (rootResolve, rootReject) => {

        fs.readFile(filePath, (err, inputBuffer) => {

            if (err) {
                rootReject(err);
                return;
            }


            function isFileEqual(path) {
                return new Promise((resolve, reject) => {
                    fs.readFile(path, (err, data) => {

                        console.log("[isFileEqual]", path);

                        if (err) {
                            resolve();
                        }                                       
                        else if (data.compare(inputBuffer) === 0) {                     
                            reject(path);   // file equal found, reject fast!
                        } else {
                            resolve();
                        }
                    });
                });     
            }



            let promises = [];

            // fill promises array
            paths.forEach(path => {
                promises.push(isFileEqual(path));
            })



            Promise.all(promises).then(values => {

                rootReject(false);

            })
            .catch((path) => {

                // use reject fast to resolve without wait the other promises to complete   
                rootResolve(path);

            });

        });

    });
}

Output of the above script:

[isFileEqual] file2
[isFileEqual] file1_copy

 File equal found at:  file1_copy

[isFileEqual] file4
[isFileEqual] file3
[isFileEqual] file5

The above solution works but as you can see there is a problem: all the files are always read regardless if an equal file has already been found.

I didn't know before, but a Promise is executed as soon as it is created (I though the opposite, why was it implemented in this way?), thus I was thinking to use a promise factory like the following:

function promiseFactory(path) {
    return function () {
        return new Promise((resolve, reject) => {
            fs.readFile(path, (err, data) => {

                console.log("[isFileEqual]", path);

                if (err) {
                    reject();
                }                                       
                else if (data.compare(inputBuffer) === 0) {                     
                    resolve(true);  
                } else {
                    resolve(false);
                }
            });
        });
    };
}

and try to run the promises in sequence. But how can I do that? Or are there alternative ways?

revy
  • 3,945
  • 7
  • 40
  • 85
  • A promise isn't "executed". A promise is just an object that represents an asynchronous result, and one creates them when calling (executing) the asynchronous function (like `fs.readFile` in your case) – Bergi May 05 '17 at 00:50
  • @Bergi Yes I meant that the function wrapped by the Promise is executed as soon as the Promise is created. Thanks for the clarification – revy May 05 '17 at 06:37

2 Answers2

1

I found a solution using recursion. Basically I create an array of factories (each factory returns a function which returns a Promise) and then use that array as input for a recursive function. If an equal file is found, the recursive function resolve the main Promise (the one returned by the main function), otherwise it call itself recursively with in input the array of factories and the index of the next promise to create.

const fs = require("fs");
let paths = ["file2", "file3", "file1_copy", "file4", "file5"];


checkIfFileAlreadyExistsAsync("file1", paths).then(result => {
    console.log("SUCCESS", result);
}).catch(err => {
    console.log("FAIL", err);
});


function checkIfFileAlreadyExistsAsync(filePath, paths) {

    return new Promise((rootResolve, rootReject) => {   

        fs.readFile(filePath, (err, inputBuffer) => {

            if (err) {
                rootReject(err);
            }



            function compareFilePromiseFactory(path) {
                return function() {
                    return new Promise((resolve, reject) => {
                        fs.readFile(path, (err, data) => {

                            console.log("Compare file: ", path, "\n");  

                            if (err) {
                                resolve(false);
                            }
                            else if(data.compare(inputBuffer) === 0) {
                                resolve(true);
                            }
                            else {
                                resolve(false);
                            }
                        });
                    });
                }
            }



            let factories = [];
            paths.forEach(path => {
                factories.push(compareFilePromiseFactory(path));
            });




            function findFile(factories, index) {               

                if (index == factories.length) {
                    rootReject(false);      
                    return;         
                }

                factories[index]().then(result => {                 

                    if (result) {
                        rootResolve(true);                      
                    }
                    else {                      
                        findFile(factories, index + 1);
                    }
                }); 
            }


            findFile(factories, 0);


        }); 
    });     
}

The above script gives the following output:

Compare file:  file2

Compare file:  file3

Compare file:  file1_copy

SUCCESS true

Each promise is created in sequence but it is all async. It stops to generate promises as soon as an equal file is found. I don't know if it is also possible an iterative solution, what do you think?

revy
  • 3,945
  • 7
  • 40
  • 85
  • This is nearly the correct solution. You should wrap the outer `new Promise` only around the `fs.readFile`, and put the `findFile(factories, 0);` in a `then` callback so that you can use promise chaining – Bergi May 05 '17 at 00:54
  • @Bergi Do you intend something like: return new Promise((resolve, reject) => { fs.readFile(.. => { if (err) reject(err); else resolve(inputBuffer); }); }).then(inputBuffer => { return new Promise((rootResolve, rootReject) => { // [...] findFile(factories, 0); }); }); Is there any advantage to do this? I am already returning a Promise thus I can do chaining: checkIfFileAlreadyExistsAsync("file1", paths).then( ... ) – revy May 05 '17 at 08:58
  • Yes, like that, but [omit the `new Promise` wrapping in the `then`](http://stackoverflow.com/q/23803743/1048572?What-is-the-promise-construction-antipattern-and-how-to-avoid-it) - `findFile(factories, 0)` already creates a promise that you can directly `return`. – Bergi May 05 '17 at 12:19
0

Promise.race

Promise.race is an interesting function -- instead of waiting for all promises to be resolved or rejected, Promise.race triggers as soon as any promise in the array is resolved or rejected:

var req1 = new Promise(function(resolve, reject) { 
    // A mock async action using setTimeout
    setTimeout(function() { resolve('First!'); }, 8000);
});
var req2 = new Promise(function(resolve, reject) { 
    // A mock async action using setTimeout
    setTimeout(function() { resolve('Second!'); }, 3000);
});
Promise.race([req1, req2]).then(function(one) {
    console.log('Then: ', one);
}).catch(function(one, two) {
    console.log('Catch: ', one);
});

// From the console: // Then: Second!

Check this one, I believe it will help you

Devaraj C
  • 317
  • 3
  • 11
  • Hi, thanks for the answer. Unfortunately Promise.race works similar to Promise.all, it needs an array of promises. As soon as a promise is created, it is executed, you can't stop it. – revy May 04 '17 at 23:18