Use Netlify Functions to upload files to Google Drive

I recently worked on a project where the client wanted to upload images to Google Drive from their Wordpress site. They wanted a custom built image uploader that could allow the users to drag and drop their photos to order them and then upload to a Google Drive. I’m going to talk about how I used Netlify Functions to solve this problem and how you can use Netlify Functions for almost any type of backend logic that you need.

I’ve had great success working with Netlify both for hosting frontend projects and for hosting Lambdas with their Functions product. The best part about Netlify is that almost every time I work with it, it just works. There are very few CI/CD tools that provide that same reliability.

Add the busboy package:

Terminal window
yarn add busboy

Let’s start with a basic handler and the structure that we want:

functions/uploadImage.js
export const handler = async (event) => {
  try {
    if (!event.body || !event.isBase64Encoded) {
      return {
        statusCode: 400,
        body: JSON.stringify({
          message: "no data",
        }),
      };
    }

    // Attempt to process file and fields sent up in the request using busboy

    // Upload to Google Drive

    // return the file ID and URL for viewing on the client

    return {
      statusCode: 200,
      body: JSON.stringify({
        fileId: ''
        fileUrl: '',
      }),
    };
  } catch (error) {
    return {
      statusCode: 400,
      body: JSON.stringify({ error }),
    };
  }
};

Now let’s add our code to pull the image data and any fields out from the event:

functions/helpers/processImageUpload.js
export const processImageUpload = async (event) => {
  return new Promise((resolve, reject) => {
    const busboy = new Busboy({
      headers: {
        ...event.headers,
        "content-type": event.headers["Content-Type"] ?? event.headers["content-type"],
      },
    });

    const result = {
      fields: {},
      files: [],
    };

    busboy.on("file", (_fieldname, file, fileName, encoding, contentType) => {
      console.log(`Processed file ${fileName}`);

      file.on("data", (data) => {
        result.files.push({
          file: data,
          fileName,
          encoding,
          contentType,
        });
      });
    });

    busboy.on("field", (fieldName, value) => {
      console.log(`Processed field ${fieldName}: ${value}`);
      result.fields[fieldName] = value;
    });

    busboy.on("finish", () => resolve(result));
    busboy.on("error", (error) => reject(`Parse error: ${error}`));

    // pushes the event data into busboy to start the processing and using the event.isBase64Encoded property to tell which kind of data
    // we're working with
    busboy.write(event.body, event.isBase64Encoded ? "base64" : "binary");

    busboy.end();
  });
};
functions/uploadImage.js
import { processImageUpload } from './helpers/processImageUpload';

export const handler = async (event) => {
  try {
    if (!event.body || !event.isBase64Encoded) {
      return {
        statusCode: 400,
        body: JSON.stringify({
          message: "no data",
        }),
      };
    }

    // Attempt to process file and fields sent up in the request using busboy
   const { files, fields } = await processImageUpload(event);

    // Upload to Google Drive

    // return the file ID and URL for viewing on the client

    return {
      statusCode: 200,
      body: JSON.stringify({
        fileId: ''
        fileUrl: '',
      }),
    };
  } catch (error) {
    return {
      statusCode: 400,
      body: JSON.stringify({ error }),
    };
  }
};

Now we need to build the code to upload to Google Drive. You’ll want to make sure you’ve created a new project in the Google Cloud Console and have credentials that allow you Google Drive access. After downloading the credentials file from Google, you can set all these values as environment variables in Netlify for your project.

functions/helpers/googleDrive.js
function getCredentials() {
  const credentials = {
    type: "service_account",
    project_id: process.env.GOOGLE_SERVICE_ACCOUNT_PROJECT_ID,
    private_key_id: process.env.GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY_ID,
    private_key: process.env.GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY.replace(/\\n/gm, "\n"),
    client_email: process.env.GOOGLE_SERVICE_ACCOUNT_CLIENT_EMAIL,
    client_id: process.env.GOOGLE_SERVICE_ACCOUNT_CLIENT_ID,
    auth_uri: "https://accounts.google.com/o/oauth2/auth",
    token_uri: "https://oauth2.googleapis.com/token",
    auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs",
    client_x509_cert_url: process.env.GOOGLE_SERVICE_ACCOUNT_CLIENT_CERT_URL,
  };

  let errorMessage = "";

  for (const key of Object.keys(credentials)) {
    if (!credentials[key]) {
      errorMessage += `${key} must be defined, but was not.`;
    }
  }

  if (errorMessage.length) {
    throw new Error(errorMessage);
  }

  return credentials;
}

const getDrive = () => {
  const auth = new google.auth.GoogleAuth({
    credentials: getCredentials(),
    scopes: ["https://www.googleapis.com/auth/drive"],
  });

  return google.drive({
    auth,
    version: "v3",
  });
};

export const uploadFile = async ({ name, parents, fileContent, mimeType, originalFileName }) => {
  const drive = getDrive();

  // upload file
  const file = await drive.files.create({
    requestBody: {
      name,
      mimeType,
      parents,
    },
    media: {
      mimeType,
      body: Buffer.isBuffer(fileContent) ? Readable.from(fileContent) : fileContent,
    },
  });

  // set permissions
  await drive.permissions.create({
    fileId: file.data.id,
    requestBody: {
      type: "anyone",
      role: "reader",
    },
  });

  return file;
};
functions/uploadImage.js
import { processImageUpload } from "./helpers/processImageUpload";
import { uploadImage } from "./helpers/googleDrive";

export const handler = async (event) => {
  try {
    if (!event.body || !event.isBase64Encoded) {
      return {
        statusCode: 400,
        body: JSON.stringify({
          message: "no data",
        }),
      };
    }

    // Attempt to process file and fields sent up in the request using busboy
    const { files, fields } = await processImageUpload(event);

    // Upload to Google Drive
    const file = files[0];

    if (!file) {
      return {
        statusCode: 400,
        body: JSON.stringify({
          message: "no file uploaded",
        }),
      };
    }

    const uploadedFile = await uploadFile(file.fileName, {
      fileContent: file.file,
      mimeType: file.contentType,
      originalFileName: file.fileName,
      parents: [fields.folderId],
    });

    if (uploadedFile.status !== 200) {
      return {
        statusCode: uploadedFile.status,
        body: JSON.stringify({
          statusText: uploadedFile.statusText,
        }),
      };
    }

    // return the file ID and URL for viewing on the client

    return {
      statusCode: 200,
      body: JSON.stringify({
        fileId: uploadedFile.id,
        fileUrl: "",
      }),
    };
  } catch (error) {
    return {
      statusCode: 400,
      body: JSON.stringify({ error }),
    };
  }
};

Let’s add a quick little function to our googleDrive.js file to get the public URL so the client can view the file.

functions/helpers/googleDrive.js
export const getPublicUrl = async (fileId) => {
  const drive = getDrive();

  const file = await drive.files.get({
    fileId,
    fields: "id,webContentLink",
  });

  return file.data.webContentLink;
};
functions/uploadImage.js
import { processImageUpload } from "./helpers/processImageUpload";
import { uploadImage, getPublicUrl } from "./helpers/googleDrive";

export const handler = async (event) => {
  try {
    if (!event.body || !event.isBase64Encoded) {
      return {
        statusCode: 400,
        body: JSON.stringify({
          message: "no data",
        }),
      };
    }

    // Attempt to process file and fields sent up in the request using busboy
    const { files, fields } = await processImageUpload(event);

    // Upload to Google Drive
    const file = files[0];

    if (!file) {
      return {
        statusCode: 400,
        body: JSON.stringify({
          message: "no file uploaded",
        }),
      };
    }

    const uploadedFile = await uploadFile(file.fileName, {
      fileContent: file.file,
      mimeType: file.contentType,
      originalFileName: file.fileName,
      parents: [fields.folderId],
    });

    if (uploadedFile.status !== 200) {
      return {
        statusCode: uploadedFile.status,
        body: JSON.stringify({
          statusText: uploadedFile.statusText,
        }),
      };
    }

    // return the file ID and URL for viewing on the client
    const fileUrl = await getPublicUrl(uploadedFile.id);

    return {
      statusCode: 200,
      body: JSON.stringify({
        fileId: uploadedFile.id,
        fileUrl,
      }),
    };
  } catch (error) {
    return {
      statusCode: 400,
      body: JSON.stringify({ error }),
    };
  }
};

That concludes the code needed to upload one file to GoogleDrive using Netlify Functions. Let me know if you have any questions. The full code is below if you want to copy and paste to use in your own projects.

functions/helpers/processImageUpload.js
export const processImageUpload = async (event) => {
  return new Promise((resolve, reject) => {
    const busboy = new Busboy({
      headers: {
        ...event.headers,
        "content-type": event.headers["Content-Type"] ?? event.headers["content-type"],
      },
    });

    const result: Result = {
      fields: {},
      files: [],
    };

    busboy.on("file", (_fieldname, file, fileName, encoding, contentType) => {
      console.log(`Processed file ${fileName}`);

      file.on("data", (data) => {
        result.files.push({
          file: data,
          fileName,
          encoding,
          contentType,
        });
      });
    });

    busboy.on("field", (fieldName, value) => {
      console.log(`Processed field ${fieldName}: ${value}`);
      result.fields[fieldName] = value;
    });

    busboy.on("finish", () => resolve(result));
    busboy.on("error", (error) => reject(`Parse error: ${error}`));

    busboy.write(event.body, event.isBase64Encoded ? "base64" : "binary");

    busboy.end();
  });
};
functions/helpers/googleDrive.js
function getCredentials(): Credentials {
  const credentials = {
    type: "service_account",
    project_id: process.env.GOOGLE_SERVICE_ACCOUNT_PROJECT_ID,
    private_key_id: process.env.GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY_ID,
    private_key: process.env.GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY.replace(/\\n/gm, "\n"),
    client_email: process.env.GOOGLE_SERVICE_ACCOUNT_CLIENT_EMAIL,
    client_id: process.env.GOOGLE_SERVICE_ACCOUNT_CLIENT_ID,
    auth_uri: "https://accounts.google.com/o/oauth2/auth",
    token_uri: "https://oauth2.googleapis.com/token",
    auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs",
    client_x509_cert_url: process.env.GOOGLE_SERVICE_ACCOUNT_CLIENT_CERT_URL,
  };

  let errorMessage = "";

  for (const key of Object.keys(credentials)) {
    if (!credentials[key]) {
      errorMessage += `${key} must be defined, but was not.`;
    }
  }

  if (errorMessage.length) {
    throw new Error(errorMessage);
  }

  return credentials;
}

const getDrive = () => {
  const auth = new google.auth.GoogleAuth({
    credentials: getCredentials(),
    scopes: ["https://www.googleapis.com/auth/drive"],
  });

  return google.drive({
    auth,
    version: "v3",
  });
};

export const uploadFile = async (
  name: string,
  {
    parents,
    fileContent,
    mimeType,
    originalFileName,
  }: {
    parents: string[],
    fileContent: Buffer | string,
    mimeType: string,
    originalFileName: string,
  }
) => {
  const drive = getDrive();

  // upload file
  const file = await drive.files.create({
    requestBody: {
      name,
      mimeType,
      parents,
    },
    media: {
      mimeType,
      body: Buffer.isBuffer(fileContent) ? Readable.from(fileContent) : fileContent,
    },
  });

  // set permissions
  await drive.permissions.create({
    fileId: file.data.id,
    requestBody: {
      type: "anyone",
      role: "reader",
    },
  });

  return file;
};

export const getPublicUrl = async (fileId) => {
  const drive = getDrive();

  const file = await drive.files.get({
    fileId,
    fields: "id,webContentLink",
  });

  return file.data.webContentLink;
};
functions/uploadImage.js
import { processImageUpload } from "./helpers/processImageUpload";
import { uploadImage, getPublicUrl } from "./helpers/googleDrive";

export const handler = async (event) => {
  try {
    if (!event.body || !event.isBase64Encoded) {
      return {
        statusCode: 400,
        body: JSON.stringify({
          message: "no data",
        }),
      };
    }

    // Attempt to process file and fields sent up in the request using busboy
    const { files, fields } = await processImageUpload(event);

    // Upload to Google Drive
    const file = files[0];

    if (!file) {
      return {
        statusCode: 400,
        body: JSON.stringify({
          message: "no file uploaded",
        }),
      };
    }

    const uploadedFile = await uploadFile(file.fileName, {
      fileContent: file.file,
      mimeType: file.contentType,
      originalFileName: file.fileName,
      parents: [fields.folderId],
    });

    if (uploadedFile.status !== 200) {
      return {
        statusCode: uploadedFile.status,
        body: JSON.stringify({
          statusText: uploadedFile.statusText,
        }),
      };
    }

    // return the file ID and URL for viewing on the client
    const fileUrl = await getPublicUrl(uploadedFile.id);

    return {
      statusCode: 200,
      body: JSON.stringify({
        fileId: uploadedFile.id,
        fileUrl,
      }),
    };
  } catch (error) {
    return {
      statusCode: 400,
      body: JSON.stringify({ error }),
    };
  }
};