Create a File on Google Drive using Promises

Here's how to create a file on Google Drive using Promises in TypeScript.

Upload a File

My goal is to upload a file to Google Drive using the JavaScript lib googleapis. I'm on Next.js 14, in TypeScript.

Client

On the client, we've created a file drop zone. On file drop, we're capturing the file and passing it to uploadContract. There, we set up the FormData to submit to our API as a multipart/form request.

import { Result } from '@/common/data/types'

export async function uploadContract(file: File): Promise<Result<string>> {
  try {
    let formData = new FormData()
    formData.set('file', file)

    const res = await fetch('/rpc/upload-contract', {
      method: 'POST',
      body: formData,
    })

    const json = await res.json()
    return res.ok
      ? { ok: true, value: json.fileId }
      : { ok: false, error: new Error(json.error) }
  } catch (error) {
    return {
      ok: false,
      error: error instanceof Error ? error : new Error('Failed to upload file'),
    }
  }
}

Note that this snippet also uses the Result type for handling potential failures.

Server Multi-part Form Parse

On the server, there are several pieces. We won't include all the code, but we'll get the essential points. First, parse the form data out of the request in /src/app/rpc/upload-contract/route.ts:

export async function POST(request: Request) {
  const formData = await request.formData()
  const file = formData.get('file') as File
  const authClient = await initAuthClient()
  const uploadResult = await uploadFile(authClient, file)
  return uploadResult.ok
    ? Response.json({ fileId: uploadResult.value})
    : new Response(JSON.stringify({ error: uploadResult.error }), { status: 500 })
}

Authenticate with Google Drive

To upload the file, we'll need an authenticated Google Drive client. You'll need to set up a service account and a new key file in the Google Cloud Console. Then, you can authenticate with it (ie, pkey.json):

import { google } from 'googleapis'
import { Oauth2Client } from 'googleapis-common'

const pkey = require('./pkey.json')

async function initAuthClient(): Promise<OAuth2Client> {
  const SCOPES = ['https://www.googleapis.com/auth/drive.file']
  const jwtClient = new google.auth.JWT(pkey.client_email, undefined, pkey.private_key, SCOPES)
  await jwtClient.authorize()
  return jwtClient
}

Upload to Google Drive

For the main event, we can upload the file to Google Drive:

import { Result } from '@/common/data/types'
import { google, drive_v3 } from 'googleapis'
import { Oauth2Client } from 'googleapis-common'

async function uploadFile(authClient: OAuth2Client, file: File): Promise<Result<drive_v3.Schema$File>> {
  const drive = google.drive({ version: 'v3', auth: authClient })
  try {
    const res = await drive.files.create({
      requestBody: {
        name: file.name,
      },
      media: {
        body: Readable.from(file.stream() as unknown as Iterable<any>),
      },
    }, {})
    return { ok: true, value: res.data }
  } catch (error) {
    return { ok: false, error: error instanceof Error ? error : new Error('Failed to create file') }
  }
}

Yay, that should be it. Of course, there were a few bumps along the way. You may or may not run into these.

TypeScript complains "await has no effect on drive.files.create"

I kept getting a TypeScript warning:

"'await' has no effect on the type of this expression"

On this line:

But the docs said that the create function:

@returns A promise if used with async/await, or void if used with a callback.

And the typedef showed 6 overloads:

create(params: Params$Resource$Files$Create, options: StreamMethodOptions): GaxiosPromise<Readable>;
create(params?: Params$Resource$Files$Create, options?: MethodOptions): GaxiosPromise<Schema$File>;
create(params: Params$Resource$Files$Create, options: StreamMethodOptions | BodyResponseCallback<Readable>, callback: BodyResponseCallback<Readable>): void;
create(params: Params$Resource$Files$Create, options: MethodOptions | BodyResponseCallback<Schema$File>, callback: BodyResponseCallback<Schema$File>): void;
create(params: Params$Resource$Files$Create, callback: BodyResponseCallback<Schema$File>): void;
create(callback: BodyResponseCallback<Schema$File>): void;

Looking more closely, the problem is that if there's one argument passed to the function, TypeScript thinks it's a callback. At runtime it'll work, because internally it's checked as a params object and not a function.

But the types were confused until I passed a second param for options as an empty object ({}). Then we get the second function overload, which returns a Promise (though an obnoxious GaxiosPromise), and we can unwrap the Schema$File.

Stream errors with "part.body.pipe is not a function"

When calling drive.files.create, I got this runtime error:

 "part.body.pipe is not a function"

This was apparently related to the value passed to media.body. I was using file.stream().

The File object does have a .stream() method. But that's not apparently good enough. Others have had this problem too.

The version I wrote to make the stream properly available was this:

media: {
  body: Readable.from(file.stream() as unknown as Iterable<any>),
}

The types are kinda clunky, but it works. The file is uploaded, and the file ID is returned to the client.

Any other challenges you've had when uploading to Google Drive? Any different approaches you've taken?