Node.js Google Drive Client - Error downloading file: response.data.on is not a function

Viewed 276

I am using the Node.js Google Drive client trying to download certain files from a gdrive. When using the example provided in their GitHub I get a Uncaught (in promise) TypeError: res.data.on is not a function error. The file is still created locally, but it's just an empty file from createWriteStream().

When I log the res variable I get: ReadableStream {locked: false}.

I'm pretty new to streams so this is quite a bit over my head.

Here is my code. You'll notice it's almost exactly what the example they provide looks like.

syncFileFromDrive(fileId, filePath) {
        filePath.replace(userDataPath, '');
        filePath = `${userDataPath}/${filePath}`;
        filePath.replaceAll('//', '/');

        logger.info(`Sync file from drive: Syncing file to path: ${filePath}`);
        logger.info(`Sync file from drive: File id: ${fileId}`)

        const dest = fs.createWriteStream(filePath);
        let progress = 0;

        this.drive.files.get({fileId, alt: 'media'}, {responseType: 'stream'}).then(res => {
            console.log(res)
            console.log(res.data)
            res.data
                .on('end', () => {
                    console.log('Done downloading file.');
                    folderStructure.buildFileMenu()
                    resolve(dest)
                })
                .on('error', err => {
                    console.error('Error downloading file.');
                    reject(err);
                })
                .on('data', d => {
                    progress += d.length;
                    if (process.stdout.isTTY) {
                        process.stdout.clearLine();
                        process.stdout.cursorTo(0);
                        process.stdout.write(`Downloaded ${progress} bytes`);
                    }
                })
                .pipe(dest);
            });
    }

Edit: I should add that this is for an Electron application. So while Node is supported, I'm not sure if that may affect the way I can use streams.

3 Answers

This feels like it's a bit of a work around, and I am open to any suggestions, but this was able to solve the issue I was having.

syncFileFromDrive(fileId, filePath) {
        filePath.replace(userDataPath, '');
        filePath = `${userDataPath}/${filePath}`;
        filePath.replaceAll('//', '/');

        logger.info(`Sync file from drive: Syncing file to path: ${filePath}`);
        logger.info(`Sync file from drive: File id: ${fileId}`)

        this.drive.files
            .get({ fileId, alt: "media"}, {responseType: 'stream'})
            .then((res) => {
                const dest = fs.createWriteStream(filePath);

                const decoder = new TextDecoder("utf-8");
                const reader = res.data.getReader()
                reader.read().then(function processText({ done, value }) {
                    if (done) {
                        console.log("Stream complete");
                        return;
                    }
                    dest.write(decoder.decode(value))

                    // Read some more, and call this function again
                    return reader.read().then(processText);
                });
            })
    }

Please take a look at my implementation, which I used to downloading the file

import { google } from 'googleapis';


const getOauth2Client = () => new google.auth.OAuth2(
  process.env.GOOGLE_DRIVE_CLIENT_ID,
  process.env.GOOGLE_DRIVE_CLIENT_SECRET,
  process.env.GOOGLE_DRIVE_REDIRECT_URL
);

const downloadFile = ({ id, access_token, path }) => {
  return new Promise((resolve, reject) => {
    const dest = fs.createWriteStream(path);
    const oauth2Client = getOauth2Client();
    oauth2Client.setCredentials({ access_token });
    const drive = google.drive({
      version: 'v3',
      auth: oauth2Client
    });
    drive.files.get(
      { fileId: id, alt: 'media' }, { responseType: 'stream' },
      (err, res) => {
        if (err) reject(err);
        res.data
          .on('end', () => {
            console.log('Done');
          })
          .on('error', _e => {
            console.log('Error', _e);
            if (_e) reject(_e);
          })
          .pipe(dest);
        dest.on('finish', () => {
          console.log('Download finished');
          resolve(true);
        });
      }
    );
  });
};

This is because in the renderer process, Google's gaxios modules uses the fetch API instead of Node's http. Fetch API returns a ReadableStream unlike http which returns a Node.js Readable. Currently there's no way to change the default adapter. You can use this quick workaround the convert it.

// Transforms a web ReadableStream to Node.js Readable
function toNodeReadable(webStream) {
    const reader = webStream.getReader();
    const rs = new Readable();

    rs._read = async () => {
        const result = await reader.read();

        if (!result.done) {
            rs.push(Buffer.from(result.value));
        } else {
            rs.push(null);
        }
    };
    return rs;
}

Usage with your code:

syncFileFromDrive(fileId, filePath) {
        filePath.replace(userDataPath, '');
        filePath = `${userDataPath}/${filePath}`;
        filePath.replaceAll('//', '/');

        logger.info(`Sync file from drive: Syncing file to path: ${filePath}`);
        logger.info(`Sync file from drive: File id: ${fileId}`)

        const dest = fs.createWriteStream(filePath);
        let progress = 0;

        this.drive.files.get({fileId, alt: 'media'}, {responseType: 'stream'}).then(res => {
            console.log(res)
            console.log(res.data)

            toNodeReadable(res.data)
                .on('end', () => {
                    console.log('Done downloading file.');
                    folderStructure.buildFileMenu()
                    resolve(dest)
                })
                .on('error', err => {
                    console.error('Error downloading file.');
                    reject(err);
                })
                .on('data', d => {
                    progress += d.length;
                    if (process.stdout.isTTY) {
                        process.stdout.clearLine();
                        process.stdout.cursorTo(0);
                        process.stdout.write(`Downloaded ${progress} bytes`);
                    }
                })
                .pipe(dest);
            });
    }

Related