Skip to content
Skip to chapter

Chapter 20

Workspace services

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.

By the end

Configure Advanced Drive v3, read a partial file-list response, and name the four independent settings that must all allow each call.

Chapter navigation

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:

  1. Create a folder named Advanced Drive inspection practice in My Drive. Keep General access set to Restricted with no added collaborators (see Share files from Google Drive).

  2. Directly inside that folder, create three Google Docs named 01 Seed list, 02 Route notes, and 03 Review checklist.

  3. 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

  1. Create a standalone Apps Script project named Advanced Drive inspection practice.

  2. Replace Code.gs with the complete source below.

  3. In Project Settings, enable Show "appsscript.json" manifest file in editor, return to Editor, replace appsscript.json with the supplied manifest, and save both files.

  4. In ADVANCED_DRIVE_CONFIG, replace PASTE_PRIVATE_MY_DRIVE_FOLDER_ID_HERE with your folder ID. Leave pageSize: 2 and includeSecondPage: false for the first run.

  5. In Editor, verify that Services lists Drive v3 with the identifier Drive. The manifest's enabledAdvancedServices entry adds it, so do not add a second entry through Services > Add a service.

  6. Under Overview > Project OAuth Scopes, confirm the project requests https://www.googleapis.com/auth/drive.metadata.readonly.

Code.gs
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,
  };
}

Download Code.gs

The manifest declares the advanced-service dependency and its single OAuth scope:

appsscript.json
{
  "timeZone": "Etc/UTC",
  "dependencies": {
    "enabledAdvancedServices": [
      {
        "userSymbol": "Drive",
        "serviceId": "drive",
        "version": "v3"
      }
    ]
  },
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "oauthScopes": [
    "https://www.googleapis.com/auth/drive.metadata.readonly"
  ]
}

Download appsscript.json
  • userSymbol: "Drive" defines the global identifier used by Drive.Files.get and Drive.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.

An Advanced Drive call requires all four conditions:

  1. enabledAdvancedServices in appsscript.json exposes Drive v3 as Drive.

  2. 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.

  3. The executing user has granted https://www.googleapis.com/auth/drive.metadata.readonly to the script.

  4. 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 methodAdvanced service callArguments in this project
GET /drive/v3/files/{fileId}Drive.Files.get(fileId, optionalParameters)Required fileId, then { fields }
GET /drive/v3/filesDrive.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:

Source
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:

  • kind and id confirm the resource type and exact file ID.

  • name provides the human-readable folder title for the report.

  • mimeType must equal application/vnd.google-apps.folder.

  • trashed must be false.

  • driveId is present on shared-drive resources, which this My Drive script rejects.

  • capabilities/canListChildren confirms 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:

Source
'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 hidden appDataFolder.

  • 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).

  • pageSize caps 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:

Source
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:

  • kind must be 'drive#fileList'.

  • incompleteSearch: true is rejected.

  • An omitted files property counts as an empty array, but null or a non-array value is rejected.

  • files.length must be at most pageSize.

For each item in files, it requires:

  • A nonempty id, name, and mimeType.

  • A modifiedTime string (RFC 3339 timestamp from the File resource) that Date.parse accepts.

  • trashed: false.

  • The configured folderId inside parents.

  • 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

  1. 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_CONFIG has your folderId, pageSize: 2, and includeSecondPage: false.

  2. Select inspectAdvancedDriveFolder and click Run. Review and approve the OAuth consent prompt if it appears. If granular consent stops the first run, rerun inspectAdvancedDriveFolder after granting the scope.

  3. Read the logged JSON report in Execution log and compare its fields against your test folder:

Report fieldMeaning on the first run
folder.id, folder.nameThe verified folder ID and its current Drive name
request.maximumPages1: the files.list page limit, not counting the files.get call
request.maximumItems2: maximumPages * pageSize, an upper bound on accepted items
pagesOne entry per fetched page with its pageNumber and itemCount
itemsUp to two child records with name, mimeType, modifiedTime, and sizeBytes
nextPageAvailabletrue 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 changePrediction for this three-document folderWhat to observe
Set includeSecondPage: true, keeping pageSize: 2Allows up to two list calls and four items, and returns all three docsTwo entries in pages, three entries in items, and nextPageAvailable: false
Keep includeSecondPage: true and set pageSize: 1Two 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 Drive00 Review checklist sorts before 01 Seed list on page oneitems[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 defined or missing autocomplete: verify enabledAdvancedServices in appsscript.json (with userSymbol: "Drive", serviceId: "drive", and version: "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 inspectAdvancedDriveFolder from the editor toolbar under the target account and grant drive.metadata.readonly.

  • Not found (404) or insufficient permission: check that folderId has 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 fields or q parameter: fix the misspelled field selector or parameter in code before rerunning.

  • Forbidden (403) or rate limit (429): read the error reason. userRateLimitExceeded and 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
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,
  };
}

appsscript.json

Download file
appsscript.json
{
  "timeZone": "Etc/UTC",
  "dependencies": {
    "enabledAdvancedServices": [
      {
        "userSymbol": "Drive",
        "serviceId": "drive",
        "version": "v3"
      }
    ]
  },
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "oauthScopes": [
    "https://www.googleapis.com/auth/drive.metadata.readonly"
  ]
}