Google Drive drive.file + Picker: picking a folder grants the folder, not its contents
A web app lets the user pick a Google Drive folder in the Google Picker, then lists that folder from the server with the Drive API v3. The folder was full of files. The listing came back like this, every time:
{"files":[]}
The status was 200, with no 403 and nothing in the response that mentioned permissions.
If you request the drive.file scope and let users pick a folder, this is expected: the grant covers the folder itself and none of the files inside it.
Setup #
The browser gets an OAuth access token with the drive.file scope and opens the Picker on a folder view:
const view = new google.picker.DocsView(google.picker.ViewId.FOLDERS)
.setMimeTypes('application/vnd.google-apps.folder')
.setSelectFolderEnabled(true)
.setIncludeFolders(true);
const picker = new google.picker.PickerBuilder()
.addView(view)
.setOAuthToken(token)
.setDeveloperKey(API_KEY)
.setCallback(onPicked)
.build();
The callback sends the picked folder ID to the server, which walks the folder with files.list:
const url = new URL('https://www.googleapis.com/drive/v3/files');
url.searchParams.set('q', `'${folderId}' in parents and trashed = false`);
url.searchParams.set('fields', 'nextPageToken, files(id, name, mimeType, size)');
url.searchParams.set('supportsAllDrives', 'true');
url.searchParams.set('includeItemsFromAllDrives', 'true');
drive.file looks like the obvious scope here. The Picker guide
tells you to use it, and Google’s scope list
marks it non-sensitive, so a public app needs only basic verification. The app only ever reads folders the user picked, which sounds like exactly what the scope is for.
The dead end: setAppId #
The first suspect was PickerBuilder.setAppId(). The setup above doesn’t call it, and the Picker guide says to “set the Drive App ID using the Cloud project number to allow the app to access the user’s files”. The theory was that without an app ID, the grant belongs to the access token that was live at pick time, so the server’s later call with a refreshed token sees nothing.
.setAppId(CLOUD_PROJECT_NUMBER) // the numeric project number, not the project ID
If you stay on drive.file, set it, because the guide asks for it. It didn’t change the result here. The listing was still {"files":[]}.
What’s actually going on #
To see where the files went, the server logged the raw body of every files.list response. A truly empty folder and a folder the app can’t see into give the same answer: status 200 and "files": []. The app only had access to the selected folder. Its children were never shared, so Drive left them out of the result and didn’t report an error.
The scope’s description says this once you read it with folders in mind. drive.file covers files “that you open with an app or that the user shares with an app” — picking a folder shares one item: the folder. The docs never say the grant extends to its contents, and it doesn’t.
Fix #
Pick the scope that matches what the server does. A server that reads everything under the picked folder can’t do that with drive.file, so the app switched to drive.readonly:
const DRIVE_SCOPE = 'https://www.googleapis.com/auth/drive.readonly';
It kept the Picker for choosing the folder and removed the setAppId call, which drive.readonly doesn’t need.
drive.readonly is a restricted scope
, so a public app needs Google’s restricted-scope verification to use it. The app in this story was internal to one Google Workspace domain, so verification didn’t apply.
Update the consent text in your own UI as well. “The app only sees folders you pick” was true under drive.file and false after the switch. The new text says the app has read-only access to the whole Drive and downloads only from the folders the user picks.
Two other ways out may fit your constraints better. If users can pick files instead of a folder, enable multi-select in the Picker: every picked file is shared with the app, and drive.file covers it. Or use a service account and have users share the folder with its address, which moves the access decision into Drive’s own sharing dialog. We ruled that out here for organizational reasons.
Takeaway #
Under drive.file, the Picker shares exactly the items the user selects. A folder is one item, and its children aren’t part of the grant, so listing it returns an empty files array with status 200.
If your app requests drive.file and calls setSelectFolderEnabled(true), list a folder you know isn’t empty from the server and log the raw response before you ship.