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 explain the independent service, API, OAuth, and resource-access gates.
Inspect one private folder without changing it
Inspect 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 a single page of up to two items; you can opt into a second page, with hard caps of two pages and five items per page. Every call is read-only.
This lesson uses Advanced Drive v3 rather than DriveApp because the Drive REST API exposes an optional-parameter object, a partial-response fields mask, a FileList response envelope, and an opaque nextPageToken in one self-contained read operation. As the Advanced Drive guide notes, built-in DriveApp is simpler for everyday file and folder work, while Advanced Drive exposes REST-specific features such as field masks, custom properties, and revisions.
An advanced service is a thin Apps Script wrapper generated from a public Google API's discovery document. Apps Script manages the OAuth flow and provides editor autocomplete, while method parameters, response objects, quotas, and error codes follow the underlying REST API. See the advanced-services guide.
Unlike the raw UrlFetchApp requests in connecting external services, an advanced service needs no manual 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), or three when includeSecondPage is true.
Create a small private fixture
Set up a private test folder in My Drive:
Create a folder named
Advanced Drive inspection practicein My Drive, keeping 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 fixture and run the script. Make sure you copy the folder's ID rather than a document ID, shortcut ID, Apps Script project ID, or Google Cloud project number.
Install the project and enabled service
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(configured byenabledAdvancedServicesin the manifest; do not add a duplicate entry via Services > Add a service). Under Overview > Project OAuth Scopes, confirm the project requestshttps://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 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, and changing version changes the underlying API contract.
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: when you declare an advanced service in the manifest or enable it under Services, Apps Script enables the corresponding Google API automatically.
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 Google Cloud projects guide. Keep the default Cloud project for this exercise; switching a script from its default Cloud project to a standard project is a one-way administrative change.
Default Cloud projects also provide an automatically configured OAuth consent screen for your account, whereas a standard Cloud project uses its own configured consent screen and audience settings. In both cases, Apps Script manages the user OAuth token for advanced-service calls without API keys or downloaded service-account credentials.
Four gates control one call
Every Advanced Drive call passes through four independent gates:
Script service configuration:
enabledAdvancedServicesinappsscript.jsonexposes Drive v3 asDrive.Cloud API enablement: the Google Drive API is enabled in the backing Cloud project (automatic for default Cloud projects, manual in Cloud Console for standard projects).
OAuth authorization: the executing user grants
https://www.googleapis.com/auth/drive.metadata.readonlyto the script.Resource rights: the executing user has Drive permission to inspect the target folder and its children.
Each gate checks a different layer: enabling the API in Cloud Console does not grant OAuth consent, granting OAuth consent does not share a folder with the user, and 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 allowing read-only metadata access across all Drive files the user can view. Our q parameter narrows which files Drive.Files.list returns, but query filtering in code does not narrow the OAuth grant itself. (In user-facing production apps, drive.file is a narrower non-sensitive scope for files the app creates or the user selects through Google Picker, whereas a pasted folder ID in a standalone script requires a metadata or read scope.)
Inside inspectAdvancedDriveFolder, ScriptApp.requireScopes(ScriptApp.AuthMode.FULL, [ADVANCED_DRIVE_SCOPE]) verifies that the required scope was granted during granular OAuth consent before calling Drive.Files. See authorization scopes and the ScriptApp reference. Note that Apps Script may prompt for initial authorization before entering the function body, even if folderId is still set to 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:
| Public Drive API contract | Advanced service call | Arguments in this project |
|---|---|---|
GET /drive/v3/files/{fileId} | Drive.Files.get(fileId, optionalParameters) | Required folderId, then { fields } |
GET /drive/v3/files | Drive.Files.list(optionalParameters) | One object containing q, spaces, corpora, orderBy, pageSize, fields, and optionally pageToken |
Advanced-service method signatures place the request body first (when a method accepts one), followed by required path parameters, media blobs, and finally an optionalParameters object for query and system parameters. Because files.get has a required fileId path parameter and no request body, its signature is Drive.Files.get(fileId, optionalParameters). Because files.list has neither a request body nor path parameters, optionalParameters is its sole argument.
Each call executes synchronously and returns the parsed JSON response object directly (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, not DriveApp.File instances with getName() methods. Where a REST method name is a reserved JavaScript keyword 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, validateConfiguredFolderId_ rejects non-strings, the placeholder string, full URLs, and characters outside [A-Za-z0-9_-] so a pasted value cannot alter the q filter string. After Drive.Files.get returns, the script checks the live resource metadata to catch valid-looking IDs that point to a document, a trashed folder, or a shared drive.
Because the call omits alt: 'media' and never calls Drive.Files.export, it reads metadata only and never downloads file contents.
Use q to select and fields to shape
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), and trashed = false excludes trashed items that files.list would otherwise include by default. Direct children can be documents, subfolders, or shortcuts; a subfolder or shortcut appears as a single item record without recursion or target resolution.
Four additional parameters constrain the list call:
spaces: 'drive'queries standard Drive storage rather thanappDataFolder.corpora: 'user'searches files accessible in the user corpus rather than a domain or shared-drive corpus.orderBy: 'name_natural'sorts numbered names in natural order (1,2,12).pageSizecaps the maximum number of items returned in a single page.
While q filters which files match, fields is a partial-response mask that selects which properties the server serializes in the response:
kind,nextPageToken,incompleteSearch,files(id,name,mimeType,modifiedTime,size,trashed,parents) Top-level selectors (kind, nextPageToken, incompleteSearch) apply to the FileList envelope, and files(...) selects properties on each element of the files array. Omitting nextPageToken from fields would hide continuation tokens from the response, and misspelling a field name causes Drive to reject 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 kind === 'drive#fileList', rejects incompleteSearch: true, treats an omitted files property as an empty array (while rejecting null or non-array values), and verifies files.length <= pageSize. 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.
Editor files and folders often omit size or report metadata sizes that differ from exported file sizes; when size is absent, the report records sizeBytes: null rather than 0. The script uses each item's id to detect duplicates across pages and parents to confirm folder membership, then strips both id and parents from the logged child records.
Bound pagination to one optional continuation
In files.list, pageSize is an upper bound, not a guaranteed page length: Drive can return fewer items than pageSize (or even an empty files array) while still supplying a nextPageToken. Always check whether nextPageToken is present rather than testing 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 opaque token unchanged as pageToken on a second call with identical 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 when the page budget (one or two pages) is exhausted, the script sets nextPageAvailable: true and stops. Note the difference between nextPageAvailable: true (more results exist beyond our page budget) and incompleteSearch: true (the server could not complete the search across the requested corpus, which the validator treats as an error).
Because Drive pagination is not a snapshot isolation transaction, renaming, adding, or moving files between page requests can shift page boundaries or repeat an item across pages; the script rejects duplicate item IDs if a concurrent edit causes an item to appear twice.
Shared-drive flags are intentionally absent
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; in many cases, passing a shared-drive folder ID without supportsAllDrives: true causes Drive.Files.get itself to fail with File not found.
To support shared drives in a workflow designed for them, requests must pass supportsAllDrives: true, and Drive.Files.list calls must also set includeItemsFromAllDrives: true (plus corpora: 'drive' and driveId when scoping a search to a single shared drive). Avoid the deprecated v2/early-v3 names supportsTeamDrives, includeTeamDriveItems, and teamDriveId. See Implement shared drive support.
Run and interpret the bounded inspection
Confirm the practice folder contains the three Google Docs, the intended Google account is active in the editor, and
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 halts the first run, reruninspectAdvancedDriveFolderafter granting the scope.Inspect the logged JSON report in Execution log and compare its fields against your fixture:
| Report field | Meaning on the initial run |
|---|---|
folder.id, folder.name | The verified folder ID and its current Drive name |
request.maximumPages | 1 (the files.list page budget, excluding the initial files.get call) |
request.maximumItems | 2 (maximumPages * pageSize, an upper bound on accepted items) |
pages | Array of per-page summaries recording pageNumber, itemCount, and hasNextPageToken |
items | Up to two child records containing name, mimeType, modifiedTime, and sizeBytes |
nextPageAvailable | true when the last fetched page returned an unconsumed nextPageToken |
With the three-document fixture and orderBy: 'name_natural', the first run normally returns 01 Seed list and 02 Route notes with 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 fixture:
| Configuration or fixture change | Prediction for this three-document fixture | What to observe |
|---|---|---|
Set includeSecondPage: true, keeping pageSize: 2 | Up to two list calls and four items; 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; 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 |
The first two changes adjust pagination parameters without changing the q filter, while renaming the document changes its position under name_natural without changing 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. Because the script stores no state and performs only reads, 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. Advanced-service errors throw JavaScript Error exceptions rather than returning HTTPResponse objects; inspect 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, re-save the manifest or Services entry.
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 a user lacks read access to an ID).Bad request (400) or invalid
fields/qparameter: fix the misspelled field selector or parameter in code before rerunning.Forbidden (403) or rate limit (429): inspect the error reason (
userRateLimitExceededor quota errors require backing off rather than changing Drive permissions).Transient server error (500, 502, 503, 504): wait briefly and rerun manually 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.
If you want to remove the advanced service from the practice project, delete the Drive entry under Services (or from enabledAdvancedServices in appsscript.json) and remove drive.metadata.readonly from oauthScopes if you also remove the inspection code. 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 as described in the authorization guide.
Next, add custom menus, dialogs, and sidebars to Google Sheets.
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"
]
}