Use Netlify Functions to upload files to Google Drive
June 5th, 2021
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:
yarn add busboy
Let’s start with a basic handler and the structure that we want:
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:
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 ();
});
};
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.
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 ;
};
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.
export const getPublicUrl = async ( fileId ) => {
const drive = getDrive ();
const file = await drive . files . get ({
fileId ,
fields: "id,webContentLink" ,
});
return file . data . webContentLink ;
};
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.
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 ();
});
};
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 ;
};
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 }),
};
}
};