Inspect a folder with Advanced Drive v3
Enable the Advanced Drive service in Google Apps Script and call Drive.Files.list with pageToken to inspect metadata from a selected Drive folder.
Configure Advanced Drive v3, read a partial file-list response, and name the four independent settings that must all allow each call.
Inspect one private folder without changing it
List the names, file types, and modification times of the direct children of one private My Drive folder without reading file contents. By default the script reads one page of up to two items. You can opt into a second page. The script reads at most two pages and at most five items per page.
An advanced service lets Apps Script call a public Google API. Google generates the wrapper from the API's discovery document, its machine-readable description. Apps Script manages the OAuth flow and provides editor autocomplete. Method parameters, response objects, quotas, and error codes follow the underlying REST API. See the advanced-services guide.
The Advanced Drive v3 example introduces an object of optional query parameters, a fields mask that picks which properties Drive returns, the FileList response object, and an opaque nextPageToken for the next page. As the Advanced Drive guide notes, built-in DriveApp is simpler for everyday file and folder work. Advanced Drive exposes REST-specific features such as field masks, custom properties, and revisions.
Compared with the raw UrlFetchApp requests in Fetch data from web APIs, an advanced service needs no URL assembly, Authorization header, or JSON.parse call. Each run makes at most two Drive API calls by default (one Drive.Files.get call to verify the folder and one Drive.Files.list call). When includeSecondPage is true, a run makes at most three calls.
Create a small private test folder
Set up a private test folder in My Drive:
Create a folder named
Advanced Drive inspection practicein My Drive. Keep General access set to Restricted with no added collaborators (see Share files from Google Drive).Directly inside that folder, create three Google Docs named
01 Seed list,02 Route notes, and03 Review checklist.Open the folder in Drive and copy the folder ID from
https://drive.google.com/drive/folders/FOLDER_ID(the segment after/folders/, before any?query string).
Use the same Google account to create the test folder and run the script. Copy the folder's own ID. A document ID, shortcut ID, Apps Script project ID, or Google Cloud project number will not work.
Create the project and enable Drive
Create a standalone Apps Script project named
Advanced Drive inspection practice.Replace
Code.gswith the complete source below.In Project Settings, enable Show "appsscript.json" manifest file in editor, return to Editor, replace
appsscript.jsonwith the supplied manifest, and save both files.In
ADVANCED_DRIVE_CONFIG, replacePASTE_PRIVATE_MY_DRIVE_FOLDER_ID_HEREwith your folder ID. LeavepageSize: 2andincludeSecondPage: falsefor the first run.In Editor, verify that Services lists Drive v3 with the identifier
Drive. The manifest'senabledAdvancedServicesentry adds it, so do not add a second entry through Services > Add a service.Under Overview > Project OAuth Scopes, confirm the project requests
https://www.googleapis.com/auth/drive.metadata.readonly.
const ADVANCED_DRIVE_SCOPE = 'https://www.googleapis.com/auth/drive.metadata.readonly';
const ADVANCED_DRIVE_FOLDER_MIME = 'application/vnd.google-apps.folder';
const ADVANCED_DRIVE_MAX_PAGE_SIZE = 5;
const ADVANCED_DRIVE_MAX_PAGES = 2;
const ADVANCED_DRIVE_CONFIG = Object.freeze({
folderId: 'PASTE_PRIVATE_MY_DRIVE_FOLDER_ID_HERE',
pageSize: 2,
includeSecondPage: false,
});
/**
* @typedef {{id?: unknown, name?: unknown, mimeType?: unknown, modifiedTime?: unknown, size?: unknown, trashed?: unknown, parents?: unknown}} AdvancedDriveFileResource_
*/
/**
* @typedef {{kind?: unknown, incompleteSearch?: unknown, files?: unknown, nextPageToken?: unknown}} AdvancedDriveListResponse_
*/
/**
* @typedef {{id: string, name: string, mimeType: string, modifiedTime: string, sizeBytes: string | null}} AdvancedDriveItem_
*/
/**
* @typedef {{name: string, mimeType: string, modifiedTime: string, sizeBytes: string | null}} AdvancedDriveReportItem_
*/
/**
* @typedef {{items: AdvancedDriveItem_[], nextPageToken: string | null}} AdvancedDrivePage_
*/
/**
* Reads selected metadata for one or two bounded pages of a My Drive folder.
* This function does not download content or change Drive resources.
*/
function inspectAdvancedDriveFolder() {
const config = requireAdvancedDriveConfig_();
// Apps Script can ask for initial consent before this function starts.
ScriptApp.requireScopes(ScriptApp.AuthMode.FULL, [ADVANCED_DRIVE_SCOPE]);
const folder = getAdvancedDriveFolder_(config.folderId);
const folderId = folder.id;
if (typeof folderId !== 'string' || folderId.length === 0) {
throw new Error('Drive returned folder metadata without a usable ID.');
}
/** @type {{pageNumber: number, itemCount: number}[]} */
const pages = [];
/** @type {AdvancedDriveReportItem_[]} */
const items = [];
const seenIds = new Set();
const pageLimit = config.includeSecondPage ? ADVANCED_DRIVE_MAX_PAGES : 1;
let pageToken;
for (let pageNumber = 1; pageNumber <= pageLimit; pageNumber += 1) {
const response = listAdvancedDrivePage_(
folderId,
config.pageSize,
pageToken,
);
const page = requireAdvancedDrivePage_(response, folderId, config.pageSize);
page.items.forEach((item) => {
if (seenIds.has(item.id)) {
throw new Error('Drive returned the same file ID on more than one inspected page. Restart after folder activity stops.');
}
seenIds.add(item.id);
items.push({
name: item.name,
mimeType: item.mimeType,
modifiedTime: item.modifiedTime,
sizeBytes: item.sizeBytes,
});
});
pages.push({
pageNumber: pageNumber,
itemCount: page.items.length,
});
// A short or empty page is not the end if Drive supplies a continuation.
pageToken = page.nextPageToken;
if (!pageToken) break;
}
const report = {
folder: {
id: folderId,
name: folder.name,
},
request: {
pageSize: config.pageSize,
maximumPages: pageLimit,
maximumItems: config.pageSize * pageLimit,
directChildrenOnly: true,
nonTrashedOnly: true,
},
pages: pages,
items: items,
nextPageAvailable: Boolean(pageToken),
};
console.log(JSON.stringify(report, null, 2));
return report;
}
/**
* @param {string} folderId
*/
function getAdvancedDriveFolder_(folderId) {
const folder = Drive.Files.get(folderId, {
fields: 'kind,id,name,mimeType,trashed,driveId,capabilities/canListChildren',
});
if (!folder || folder.kind !== 'drive#file' || typeof folder.id !== 'string' || folder.id !== folderId) {
throw new Error('Drive returned an unexpected folder metadata response.');
}
if (folder.mimeType !== ADVANCED_DRIVE_FOLDER_MIME || folder.trashed !== false) {
throw new Error('folderId must identify a nontrashed folder.');
}
if (folder.driveId) {
throw new Error('This exercise accepts a My Drive folder, not a shared drive folder.');
}
if (!folder.capabilities || folder.capabilities.canListChildren !== true) {
throw new Error('The executing user cannot list children of the selected folder.');
}
if (typeof folder.name !== 'string' || folder.name.length === 0) {
throw new Error('Drive returned folder metadata without a name.');
}
return folder;
}
/**
* @param {string} folderId
* @param {number} pageSize
* @param {string | null | undefined} pageToken
*/
function listAdvancedDrivePage_(folderId, pageSize, pageToken) {
/** @type {{q: string, spaces: string, corpora: string, orderBy: string, pageSize: number, fields: string, pageToken?: string}} */
const optionalParameters = {
q: "'" + folderId + "' in parents and trashed = false",
spaces: 'drive',
corpora: 'user',
orderBy: 'name_natural',
pageSize: pageSize,
fields: 'kind,nextPageToken,incompleteSearch,files(id,name,mimeType,modifiedTime,size,trashed,parents)',
};
if (pageToken) optionalParameters.pageToken = pageToken;
return Drive.Files.list(optionalParameters);
}
/**
* @param {AdvancedDriveListResponse_} response
* @param {string} folderId
* @param {number} pageSize
* @returns {AdvancedDrivePage_}
*/
function requireAdvancedDrivePage_(response, folderId, pageSize) {
if (!response || response.kind !== 'drive#fileList') {
throw new Error('Drive returned an unexpected file-list response.');
}
if (response.incompleteSearch === true) {
throw new Error('Drive reported an incomplete search. No partial report was accepted.');
}
if (response.incompleteSearch !== undefined && typeof response.incompleteSearch !== 'boolean') {
throw new Error('Drive returned an invalid incompleteSearch value.');
}
const files = response.files === undefined ? [] : response.files;
if (!Array.isArray(files)) {
throw new Error('Drive returned files in an unexpected shape.');
}
if (files.length > pageSize) {
throw new Error('Drive returned more files than the requested pageSize. No oversized report was accepted.');
}
const items = files.map((file) => requireAdvancedDriveItem_(file, folderId));
return {
items: items,
nextPageToken: requireAdvancedDrivePageToken_(response.nextPageToken),
};
}
/**
* @param {AdvancedDriveFileResource_} file
* @param {string} folderId
* @returns {AdvancedDriveItem_}
*/
function requireAdvancedDriveItem_(file, folderId) {
if (!file || typeof file !== 'object') {
throw new Error('Drive returned an invalid file resource.');
}
if (typeof file.id !== 'string' || file.id.length === 0) {
throw new Error('Drive returned a file without an ID.');
}
if (typeof file.name !== 'string' || file.name.length === 0) {
throw new Error('Drive returned a file without a name.');
}
if (typeof file.mimeType !== 'string' || file.mimeType.length === 0) {
throw new Error('Drive returned a file without a MIME type.');
}
if (typeof file.modifiedTime !== 'string' || Number.isNaN(Date.parse(file.modifiedTime))) {
throw new Error('Drive returned a file without a valid modifiedTime.');
}
if (file.trashed !== false) {
throw new Error('Drive returned a trashed file despite the request filter.');
}
if (!Array.isArray(file.parents) || !file.parents.includes(folderId)) {
throw new Error('Drive returned a file outside the selected parent folder.');
}
let sizeBytes = null;
if (file.size !== undefined) {
if (typeof file.size !== 'string' || !/^\d+$/.test(file.size)) {
throw new Error('Drive returned size outside its decimal-string contract.');
}
// Do not convert an int64 string through Number and lose integer precision.
sizeBytes = file.size;
}
return {
id: file.id,
name: file.name,
mimeType: file.mimeType,
modifiedTime: file.modifiedTime,
sizeBytes: sizeBytes,
};
}
/**
* @param {unknown} pageToken
* @returns {string | null}
*/
function requireAdvancedDrivePageToken_(pageToken) {
if (pageToken === undefined || pageToken === null || pageToken === '') return null;
if (typeof pageToken !== 'string') {
throw new Error('Drive returned an invalid nextPageToken.');
}
return pageToken;
}
function requireAdvancedDriveConfig_() {
if (typeof ADVANCED_DRIVE_CONFIG.folderId !== 'string') {
throw new Error('folderId must be the quoted ID copied from your practice folder URL.');
}
const folderId = ADVANCED_DRIVE_CONFIG.folderId.trim();
if (folderId === 'PASTE_PRIVATE_MY_DRIVE_FOLDER_ID_HERE') {
throw new Error('Set ADVANCED_DRIVE_CONFIG.folderId before running the inspection.');
}
if (folderId.includes('/') || !/^[A-Za-z0-9_-]{10,}$/.test(folderId)) {
throw new Error('folderId must be an ID copied from a Drive folder URL, not the complete URL.');
}
const pageSize = ADVANCED_DRIVE_CONFIG.pageSize;
if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > ADVANCED_DRIVE_MAX_PAGE_SIZE) {
throw new Error('pageSize must be an integer from 1 through ' + ADVANCED_DRIVE_MAX_PAGE_SIZE + '.');
}
if (typeof ADVANCED_DRIVE_CONFIG.includeSecondPage !== 'boolean') {
throw new Error('includeSecondPage must be true or false.');
}
return {
folderId: folderId,
pageSize: pageSize,
includeSecondPage: ADVANCED_DRIVE_CONFIG.includeSecondPage,
};
}
The manifest declares the advanced-service dependency and its single OAuth scope:
{
"timeZone": "Etc/UTC",
"dependencies": {
"enabledAdvancedServices": [
{
"userSymbol": "Drive",
"serviceId": "drive",
"version": "v3"
}
]
},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"oauthScopes": [
"https://www.googleapis.com/auth/drive.metadata.readonly"
]
}
userSymbol: "Drive"defines the global identifier used byDrive.Files.getandDrive.Files.list.serviceId: "drive"matches the API discovery document's service identifier.version: "v3"selects Drive API v3.
See the manifest dependency reference. Renaming userSymbol changes the global object name in code. Changing version changes which Drive API version your code uses, and with it the method and field names.
Default and standard Cloud projects differ
Every Apps Script project is backed by a Google Cloud project. A new standalone script uses a default Cloud project. In a default Cloud project, Apps Script enables the corresponding Google API automatically when you declare an advanced service in the manifest or enable it under Services.
If a script is linked to a standard Cloud project, an administrator of that Cloud project must also enable Google Drive API under APIs & Services in the Google Cloud console. See the advanced-services guide and the Google Cloud projects guide. Keep the default Cloud project for this exercise. Switching a script from its default Cloud project to a standard project cannot be undone.
A default Cloud project also comes with an OAuth consent screen configured for your account. A standard Cloud project uses its own consent screen and audience settings. In both cases, Apps Script manages the user's OAuth token for advanced-service calls, so you need no API key or downloaded service-account credentials.
Check the API, service, consent, and file access
An Advanced Drive call requires all four conditions:
enabledAdvancedServicesinappsscript.jsonexposes Drive v3 asDrive.The Google Drive API is enabled in the backing Cloud project. This is automatic for default Cloud projects. For standard projects, you enable it manually in Cloud Console.
The executing user has granted
https://www.googleapis.com/auth/drive.metadata.readonlyto the script.The executing user has Drive permission to inspect the target folder and its children.
The four conditions are independent. Enabling the API in Cloud Console does not grant OAuth consent, and granting OAuth consent does not share a folder with the user. Likewise, sharing a folder in Drive does not enable a disabled API.
The manifest requests https://www.googleapis.com/auth/drive.metadata.readonly, which the Drive scopes guide classifies as a restricted scope. It allows read-only metadata access to every Drive file the user can view. The script's q parameter limits which files Drive.Files.list returns, but the OAuth grant still covers every file the user can view.
In a user-facing production app, drive.file is a narrower non-sensitive scope for files the app creates or the user selects through Google Picker. A standalone script that uses a pasted folder ID needs a metadata or read scope.
Inside inspectAdvancedDriveFolder, ScriptApp.requireScopes(ScriptApp.AuthMode.FULL, [ADVANCED_DRIVE_SCOPE]) verifies that the user granted the required scope during granular OAuth consent before the script calls Drive.Files. See authorization scopes and the ScriptApp reference. Apps Script may prompt for authorization before it enters the function body, even while folderId is still the placeholder string.
Translate REST methods into Apps Script calls
The Drive v3 REST methods files.get and files.list map to Drive.Files.get and Drive.Files.list with capitalized resource namespaces:
| Drive REST method | Advanced service call | Arguments in this project |
|---|---|---|
GET /drive/v3/files/{fileId} | Drive.Files.get(fileId, optionalParameters) | Required fileId, then { fields } |
GET /drive/v3/files | Drive.Files.list(optionalParameters) | One object containing q, spaces, corpora, orderBy, pageSize, fields, and optionally pageToken |
An advanced-service method takes its arguments in a fixed order: the request body (when the method accepts one), required path parameters, media blobs, and an optionalParameters object for query and system parameters. files.get has a required fileId path parameter and no request body, so its signature is Drive.Files.get(fileId, optionalParameters). files.list has neither, so optionalParameters is its only argument.
Each call runs synchronously and returns the parsed JSON response object directly, with no .execute() call or .data wrapper. The returned File and FileList values are plain JavaScript objects with REST property names such as file.name and file.mimeType. They are not DriveApp.File instances with getName() methods. Where a REST method name is a reserved JavaScript word such as delete, the wrapper renames it to remove. See the advanced-services guide.
Verify the selected folder before listing
The script first verifies the target folder with Drive.Files.get:
Drive.Files.get(folderId, {
fields: 'kind,id,name,mimeType,trashed,driveId,capabilities/canListChildren',
}); See the files.get reference. The fields mask requests only the properties needed to validate the target before listing:
kindandidconfirm the resource type and exact file ID.nameprovides the human-readable folder title for the report.mimeTypemust equalapplication/vnd.google-apps.folder.trashedmust befalse.driveIdis present on shared-drive resources, which this My Drive script rejects.capabilities/canListChildrenconfirms that the executing user can list the folder's children.
Before calling the API, requireAdvancedDriveConfig_ rejects a folderId that is not a string, is still the placeholder, contains a /, is shorter than 10 characters, or contains characters outside [A-Za-z0-9_-]. That check keeps a pasted value from altering the q filter string. After Drive.Files.get returns, the script checks the live metadata to catch a valid-looking ID that points to a document, a trashed folder, or a shared drive.
The script omits alt: 'media' and never calls Drive.Files.export, so it reads metadata only and never downloads file contents.
Filter with q, choose properties with fields
Next, Drive.Files.list filters direct children with q:
'FOLDER_ID' in parents and trashed = false In the Drive search query grammar, 'FOLDER_ID' in parents matches only direct children of that folder, not nested descendants. trashed = false excludes trashed items, which files.list otherwise includes by default. A direct child can be a document, a subfolder, or a shortcut. A subfolder or shortcut appears as one item record, and the script does not descend into it or resolve its target.
Four more parameters constrain the list call:
spaces: 'drive'queries standard Drive storage. It excludes the hiddenappDataFolder.corpora: 'user'searches files the user can access. It does not search a domain or shared-drive corpus.orderBy: 'name_natural'sorts numbered names in natural order (1,2,12).pageSizecaps the number of items in one page.
q filters which files match. fields is a partial-response mask that selects which properties Drive includes in the response:
kind,nextPageToken,incompleteSearch,files(id,name,mimeType,modifiedTime,size,trashed,parents) The top-level selectors (kind, nextPageToken, incompleteSearch) apply to the FileList response object, and files(...) selects properties on each element of the files array. If you omit nextPageToken from fields, the response never includes a continuation token. If you misspell a field name, Drive rejects the request with an invalid-parameter error. See Return specific Drive fields.
Validate a partial FileList
Drive.Files.list returns a FileList object whose files property holds the current page's array, alongside optional nextPageToken and incompleteSearch fields. See the files.list response reference.
The validator checks the response as a whole:
kindmust be'drive#fileList'.incompleteSearch: trueis rejected.An omitted
filesproperty counts as an empty array, butnullor a non-array value is rejected.files.lengthmust be at mostpageSize.
For each item in files, it requires:
A nonempty
id,name, andmimeType.A
modifiedTimestring (RFC 3339 timestamp from the File resource) thatDate.parseaccepts.trashed: false.The configured
folderIdinsideparents.An optional base-10 integer string for
size, kept as a string because Drive v3 represents 64-bit byte counts (int64) as JSON strings.
Google Docs, Sheets, and Slides files and folders often omit size, or report a metadata size that differs from the exported file size. When size is absent, the report records sizeBytes: null, not 0. The script uses each item's id to detect duplicates across pages and parents to confirm folder membership, then leaves both out of the logged child records.
Read at most two pages
In files.list, pageSize is a maximum, not a guaranteed page length. Drive can return fewer items than pageSize, or even an empty files array, and still supply a nextPageToken. To find out whether more results exist, check whether nextPageToken is present. Do not test files.length < pageSize.
The first Drive.Files.list call omits pageToken. When includeSecondPage is true and the first response includes a nonempty nextPageToken string, the script passes that token unchanged as pageToken on a second call with the same query parameters. With pageSize capped at 5 and at most two pages, a run issues at most two list requests and accepts at most ten items.
If a nextPageToken is still present after the last allowed page, the script sets nextPageAvailable: true and stops. nextPageAvailable: true means more results exist beyond the page limit. incompleteSearch: true means the server could not complete the search across the requested corpus, and the validator treats it as an error.
Drive does not freeze the folder's contents between page requests. Renaming, adding, or moving files between requests can shift page boundaries or repeat an item across pages, so the script throws if the same item ID appears on two pages.
This project does not support shared drives
This project targets a private My Drive folder and omits shared-drive parameters. If Drive.Files.get returns a driveId, the validator rejects the target. Often Drive.Files.get itself fails first with File not found, because the call passes a shared-drive folder ID without supportsAllDrives: true.
A workflow that supports shared drives must pass supportsAllDrives: true on every request, and its Drive.Files.list calls must also set includeItemsFromAllDrives: true. To search one shared drive, add corpora: 'drive' and driveId. Avoid the deprecated v2 and early v3 names supportsTeamDrives, includeTeamDriveItems, and teamDriveId. See Implement shared drive support.
Run the inspection and read the report
Confirm that the practice folder contains the three Google Docs, that the intended Google account is active in the Apps Script editor, and that
ADVANCED_DRIVE_CONFIGhas yourfolderId,pageSize: 2, andincludeSecondPage: false.Select
inspectAdvancedDriveFolderand click Run. Review and approve the OAuth consent prompt if it appears. If granular consent stops the first run, reruninspectAdvancedDriveFolderafter granting the scope.Read the logged JSON report in Execution log and compare its fields against your test folder:
| Report field | Meaning on the first run |
|---|---|
folder.id, folder.name | The verified folder ID and its current Drive name |
request.maximumPages | 1: the files.list page limit, not counting the files.get call |
request.maximumItems | 2: maximumPages * pageSize, an upper bound on accepted items |
pages | One entry per fetched page with its pageNumber and itemCount |
items | Up to two child records with name, mimeType, modifiedTime, and sizeBytes |
nextPageAvailable | true when the last fetched page returned a nextPageToken the script did not use |
With the three-document test folder and orderBy: 'name_natural', the first run normally returns 01 Seed list and 02 Route notes. The report shows nextPageAvailable: true and mimeType: "application/vnd.google-apps.document" (see Google Workspace MIME types).
Predict, change, observe, restore
Test how includeSecondPage, pageSize, and orderBy: 'name_natural' interact on the three-document test folder:
| Configuration or folder change | Prediction for this three-document folder | What to observe |
|---|---|---|
Set includeSecondPage: true, keeping pageSize: 2 | Allows up to two list calls and four items, and returns all three docs | Two entries in pages, three entries in items, and nextPageAvailable: false |
Keep includeSecondPage: true and set pageSize: 1 | Two list calls of one item each (01 Seed list, 02 Route notes) | Two entries in pages, two items total, and nextPageAvailable: true |
Restore pageSize: 2 and includeSecondPage: false, then rename 03 Review checklist to 00 Review checklist in Drive | 00 Review checklist sorts before 01 Seed list on page one | items[0].name is 00 Review checklist while folder.id is unchanged |
Renaming the document changes its position under name_natural but not its folder membership.
Afterward, rename the third document back to 03 Review checklist, restore pageSize: 2 and includeSecondPage: false, and run inspectAdvancedDriveFolder once more to confirm the original output. The script stores no state and only reads, so every run queries live Drive metadata from page one.
Classify failures before rerunning
If any check or API call fails, inspectAdvancedDriveFolder throws an error and logs no partial report. A failed advanced-service call throws a JavaScript Error. It does not return an HTTPResponse object the way UrlFetchApp can. Read the message and stack trace in Executions alongside the Drive error guide and Apps Script quotas:
Drive is not definedor missing autocomplete: verifyenabledAdvancedServicesinappsscript.json(withuserSymbol: "Drive",serviceId: "drive", andversion: "v3") and save the manifest.API disabled in Cloud project: on a standard Cloud project, enable Google Drive API in the linked project's Cloud Console. On a default Cloud project, save the manifest or the Services entry again.
Authorization required or scope denied: run
inspectAdvancedDriveFolderfrom the editor toolbar under the target account and grantdrive.metadata.readonly.Not found (404) or insufficient permission: check that
folderIdhas no URL prefix or query string and that the executing account owns or can access the folder. Drive returns 404 when the user lacks read access to an ID.Bad request (400) or invalid
fieldsorqparameter: fix the misspelled field selector or parameter in code before rerunning.Forbidden (403) or rate limit (429): read the error reason.
userRateLimitExceededand quota errors need a pause before retrying. Changing Drive permissions does not fix them.Transient server error (500, 502, 503, 504): wait briefly and rerun from the editor.
Remove configuration without deleting data
When you finish the exercise, restore pageSize: 2, includeSecondPage: false, and the placeholder folderId in Code.gs.
To remove the advanced service from the practice project, delete the Drive entry under Services or from enabledAdvancedServices in appsscript.json. If you also remove the inspection code, remove drive.metadata.readonly from oauthScopes.
To revoke the project's OAuth grant from your Google Account, open Google Account > Security > Your connections to third-party apps & services and remove the practice project connection. See the authorization guide.
Next, read Menus, sidebars, and modals.
Project files
Complete source files for this chapter's examples.
Inspect a private folder with Advanced Drive v3
Code.gs
Download fileconst ADVANCED_DRIVE_SCOPE = 'https://www.googleapis.com/auth/drive.metadata.readonly';
const ADVANCED_DRIVE_FOLDER_MIME = 'application/vnd.google-apps.folder';
const ADVANCED_DRIVE_MAX_PAGE_SIZE = 5;
const ADVANCED_DRIVE_MAX_PAGES = 2;
const ADVANCED_DRIVE_CONFIG = Object.freeze({
folderId: 'PASTE_PRIVATE_MY_DRIVE_FOLDER_ID_HERE',
pageSize: 2,
includeSecondPage: false,
});
/**
* @typedef {{id?: unknown, name?: unknown, mimeType?: unknown, modifiedTime?: unknown, size?: unknown, trashed?: unknown, parents?: unknown}} AdvancedDriveFileResource_
*/
/**
* @typedef {{kind?: unknown, incompleteSearch?: unknown, files?: unknown, nextPageToken?: unknown}} AdvancedDriveListResponse_
*/
/**
* @typedef {{id: string, name: string, mimeType: string, modifiedTime: string, sizeBytes: string | null}} AdvancedDriveItem_
*/
/**
* @typedef {{name: string, mimeType: string, modifiedTime: string, sizeBytes: string | null}} AdvancedDriveReportItem_
*/
/**
* @typedef {{items: AdvancedDriveItem_[], nextPageToken: string | null}} AdvancedDrivePage_
*/
/**
* Reads selected metadata for one or two bounded pages of a My Drive folder.
* This function does not download content or change Drive resources.
*/
function inspectAdvancedDriveFolder() {
const config = requireAdvancedDriveConfig_();
// Apps Script can ask for initial consent before this function starts.
ScriptApp.requireScopes(ScriptApp.AuthMode.FULL, [ADVANCED_DRIVE_SCOPE]);
const folder = getAdvancedDriveFolder_(config.folderId);
const folderId = folder.id;
if (typeof folderId !== 'string' || folderId.length === 0) {
throw new Error('Drive returned folder metadata without a usable ID.');
}
/** @type {{pageNumber: number, itemCount: number}[]} */
const pages = [];
/** @type {AdvancedDriveReportItem_[]} */
const items = [];
const seenIds = new Set();
const pageLimit = config.includeSecondPage ? ADVANCED_DRIVE_MAX_PAGES : 1;
let pageToken;
for (let pageNumber = 1; pageNumber <= pageLimit; pageNumber += 1) {
const response = listAdvancedDrivePage_(
folderId,
config.pageSize,
pageToken,
);
const page = requireAdvancedDrivePage_(response, folderId, config.pageSize);
page.items.forEach((item) => {
if (seenIds.has(item.id)) {
throw new Error('Drive returned the same file ID on more than one inspected page. Restart after folder activity stops.');
}
seenIds.add(item.id);
items.push({
name: item.name,
mimeType: item.mimeType,
modifiedTime: item.modifiedTime,
sizeBytes: item.sizeBytes,
});
});
pages.push({
pageNumber: pageNumber,
itemCount: page.items.length,
});
// A short or empty page is not the end if Drive supplies a continuation.
pageToken = page.nextPageToken;
if (!pageToken) break;
}
const report = {
folder: {
id: folderId,
name: folder.name,
},
request: {
pageSize: config.pageSize,
maximumPages: pageLimit,
maximumItems: config.pageSize * pageLimit,
directChildrenOnly: true,
nonTrashedOnly: true,
},
pages: pages,
items: items,
nextPageAvailable: Boolean(pageToken),
};
console.log(JSON.stringify(report, null, 2));
return report;
}
/**
* @param {string} folderId
*/
function getAdvancedDriveFolder_(folderId) {
const folder = Drive.Files.get(folderId, {
fields: 'kind,id,name,mimeType,trashed,driveId,capabilities/canListChildren',
});
if (!folder || folder.kind !== 'drive#file' || typeof folder.id !== 'string' || folder.id !== folderId) {
throw new Error('Drive returned an unexpected folder metadata response.');
}
if (folder.mimeType !== ADVANCED_DRIVE_FOLDER_MIME || folder.trashed !== false) {
throw new Error('folderId must identify a nontrashed folder.');
}
if (folder.driveId) {
throw new Error('This exercise accepts a My Drive folder, not a shared drive folder.');
}
if (!folder.capabilities || folder.capabilities.canListChildren !== true) {
throw new Error('The executing user cannot list children of the selected folder.');
}
if (typeof folder.name !== 'string' || folder.name.length === 0) {
throw new Error('Drive returned folder metadata without a name.');
}
return folder;
}
/**
* @param {string} folderId
* @param {number} pageSize
* @param {string | null | undefined} pageToken
*/
function listAdvancedDrivePage_(folderId, pageSize, pageToken) {
/** @type {{q: string, spaces: string, corpora: string, orderBy: string, pageSize: number, fields: string, pageToken?: string}} */
const optionalParameters = {
q: "'" + folderId + "' in parents and trashed = false",
spaces: 'drive',
corpora: 'user',
orderBy: 'name_natural',
pageSize: pageSize,
fields: 'kind,nextPageToken,incompleteSearch,files(id,name,mimeType,modifiedTime,size,trashed,parents)',
};
if (pageToken) optionalParameters.pageToken = pageToken;
return Drive.Files.list(optionalParameters);
}
/**
* @param {AdvancedDriveListResponse_} response
* @param {string} folderId
* @param {number} pageSize
* @returns {AdvancedDrivePage_}
*/
function requireAdvancedDrivePage_(response, folderId, pageSize) {
if (!response || response.kind !== 'drive#fileList') {
throw new Error('Drive returned an unexpected file-list response.');
}
if (response.incompleteSearch === true) {
throw new Error('Drive reported an incomplete search. No partial report was accepted.');
}
if (response.incompleteSearch !== undefined && typeof response.incompleteSearch !== 'boolean') {
throw new Error('Drive returned an invalid incompleteSearch value.');
}
const files = response.files === undefined ? [] : response.files;
if (!Array.isArray(files)) {
throw new Error('Drive returned files in an unexpected shape.');
}
if (files.length > pageSize) {
throw new Error('Drive returned more files than the requested pageSize. No oversized report was accepted.');
}
const items = files.map((file) => requireAdvancedDriveItem_(file, folderId));
return {
items: items,
nextPageToken: requireAdvancedDrivePageToken_(response.nextPageToken),
};
}
/**
* @param {AdvancedDriveFileResource_} file
* @param {string} folderId
* @returns {AdvancedDriveItem_}
*/
function requireAdvancedDriveItem_(file, folderId) {
if (!file || typeof file !== 'object') {
throw new Error('Drive returned an invalid file resource.');
}
if (typeof file.id !== 'string' || file.id.length === 0) {
throw new Error('Drive returned a file without an ID.');
}
if (typeof file.name !== 'string' || file.name.length === 0) {
throw new Error('Drive returned a file without a name.');
}
if (typeof file.mimeType !== 'string' || file.mimeType.length === 0) {
throw new Error('Drive returned a file without a MIME type.');
}
if (typeof file.modifiedTime !== 'string' || Number.isNaN(Date.parse(file.modifiedTime))) {
throw new Error('Drive returned a file without a valid modifiedTime.');
}
if (file.trashed !== false) {
throw new Error('Drive returned a trashed file despite the request filter.');
}
if (!Array.isArray(file.parents) || !file.parents.includes(folderId)) {
throw new Error('Drive returned a file outside the selected parent folder.');
}
let sizeBytes = null;
if (file.size !== undefined) {
if (typeof file.size !== 'string' || !/^\d+$/.test(file.size)) {
throw new Error('Drive returned size outside its decimal-string contract.');
}
// Do not convert an int64 string through Number and lose integer precision.
sizeBytes = file.size;
}
return {
id: file.id,
name: file.name,
mimeType: file.mimeType,
modifiedTime: file.modifiedTime,
sizeBytes: sizeBytes,
};
}
/**
* @param {unknown} pageToken
* @returns {string | null}
*/
function requireAdvancedDrivePageToken_(pageToken) {
if (pageToken === undefined || pageToken === null || pageToken === '') return null;
if (typeof pageToken !== 'string') {
throw new Error('Drive returned an invalid nextPageToken.');
}
return pageToken;
}
function requireAdvancedDriveConfig_() {
if (typeof ADVANCED_DRIVE_CONFIG.folderId !== 'string') {
throw new Error('folderId must be the quoted ID copied from your practice folder URL.');
}
const folderId = ADVANCED_DRIVE_CONFIG.folderId.trim();
if (folderId === 'PASTE_PRIVATE_MY_DRIVE_FOLDER_ID_HERE') {
throw new Error('Set ADVANCED_DRIVE_CONFIG.folderId before running the inspection.');
}
if (folderId.includes('/') || !/^[A-Za-z0-9_-]{10,}$/.test(folderId)) {
throw new Error('folderId must be an ID copied from a Drive folder URL, not the complete URL.');
}
const pageSize = ADVANCED_DRIVE_CONFIG.pageSize;
if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > ADVANCED_DRIVE_MAX_PAGE_SIZE) {
throw new Error('pageSize must be an integer from 1 through ' + ADVANCED_DRIVE_MAX_PAGE_SIZE + '.');
}
if (typeof ADVANCED_DRIVE_CONFIG.includeSecondPage !== 'boolean') {
throw new Error('includeSecondPage must be true or false.');
}
return {
folderId: folderId,
pageSize: pageSize,
includeSecondPage: ADVANCED_DRIVE_CONFIG.includeSecondPage,
};
}
appsscript.json
Download file{
"timeZone": "Etc/UTC",
"dependencies": {
"enabledAdvancedServices": [
{
"userSymbol": "Drive",
"serviceId": "drive",
"version": "v3"
}
]
},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"oauthScopes": [
"https://www.googleapis.com/auth/drive.metadata.readonly"
]
}