node.jsgoogle-drive-apiasynchronous-javascript

Async with recursive method


I'm trying to make a method that copies a folder tree in google drive using the drive API. I'm coming across some issues with asynchronous methods not working, if anybody can spot the flaw in the code, or point me towards some useful articles it would be very helpful.

I need the line that is causing the error to be awaited because the next recursive call requires the ID of the folder that is being required with the 'copy' function. It returns a promise, so it seems like it should be working.

In general, I am not very experienced with asynchronous programming so I could be overlooking something simple, but I have been stuck on this for a while now. Thanks!!

code:

function authorize() {
    const auth = new google.auth.GoogleAuth({
        keyFile: 'creds.json',
        scopes: SCOPES
    })
    return google.drive({version: 'v3', auth})
}

function search(driveService, parentFolder) {
    return new Promise((resolve, reject) => {
        driveService.files.list({
            pageSize: 20,
            q: `'${parentFolder}' in parents and trashed = false`, 
            fields: 'files(id, name, mimeType)'
        }).then(({data}) => resolve(data))
        .catch(err => reject(err))
    })
}

function createRoot(driveService, CLIENT_NAME, NEW_FOLDER_LOCATION) {
    let fileMetadata = {
        'name': CLIENT_NAME,
        'mimeType': 'application/vnd.google-apps.folder',
        'parents': [NEW_FOLDER_LOCATION]
    }
    return new Promise((resolve, reject) => {
        const file = driveService.files.create({
            resource: fileMetadata,
            fields: 'id'
        }, function(err, file) {
            if(err) {
                reject(err);
            } else {
                resolve(file.data.id);
            }
        })
    })
}

function copy(driveService, copyContentId, contentNewName, root){
    
    let fileMetadata = {
        'name': contentNewName,
        'parents': [root]
    };
    return new Promise((resolve, reject) => {
        const file = driveService.files.copy({
            'fileId': copyContentId,
            'resource': fileMetadata
        }, function(err, file) {
            if(err) {
                reject(err);
            } else {
                resolve(file.data.id);
            }
        })
    })
}

async function recursive(driveService, startSearch, rootFolder) {
    let children = await search(driveService, startSearch);
    if(children.files.length > 0) {
        children.files.forEach((element) => {
            if(element.mimeType === 'application/vnd.google-apps.folder') {
                let name = element.name.replace('Last, First', CLIENT_NAME);
                let folderID = await copy(driveService, element.id, name, rootFolder);
                recursive(driveService, element.id, folderID);
            } else {
                let name = element.name.replace('Last, First', CLIENT_NAME);
                let fileID = await copy(driveService, element.id, name, rootFolder);
            }
        })
    } else {
        console.log('end of branch');
    }
}

async function run() {
    try{

        const google = await authorize();
        const root = await createRoot(google, CLIENT_NAME, NEW_FOLDER_LOCATION);
        await recursive(google, START_SEARCH, root);

    } catch(err) {
        console.log('error', err);
    }
}

run();

output:

app.js:75
                let folderID = await copy(driveService, element.id, name, rootFolder);
                               ^^^^^

SyntaxError: await is only valid in async functions and the top level bodies of modules
    at Object.compileFunction (node:vm:352:18)
    at wrapSafe (node:internal/modules/cjs/loader:1031:15)
    at Module._compile (node:internal/modules/cjs/loader:1065:27)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
    at Module.load (node:internal/modules/cjs/loader:981:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
    at node:internal/main/run_main_module:17:47

Solution

  • Switch from

    children.files.forEach(element => { ... })
    

    to:

    for (let element of children.files) { ...}
    

    And, change this:

    recursive(driveService, element.id, folderID);
    

    to this:

    await recursive(driveService, element.id, folderID);
    

    You want to switch the .forEach() so you aren't creating a new function scope so you can use await and so that await will pause the parent function. That new function in the callback to .forEach() is not declared aysnc which is why you get the error (you can't use await in that callback function). And, declaring that callback as async will make the error go away, but will not do what you want because it will not pause the parent function.

    .forEach() is NOT async/await aware. Do not use it in this context.

    I would go so far as to say .forEach() is obsolete now. With for/of and let/const being block-scoped and .forEach() not supporting async/await there's lots of reasons to avoid .forEach() and no reason to use it.


    You need the await recursive(...) so that new promise chain is tied into the current one rather than creating a whole new independent promise chain that the caller will have no idea when it's done.