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 explain the independent service, API, OAuth, and resource-access gates.

Chapter navigation

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:

  1. Create a folder named Advanced Drive inspection practice in My Drive, keeping 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 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

  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 (configured by enabledAdvancedServices in the manifest; do not add a duplicate entry via Services > Add a service). 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 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, 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:

  1. Script service configuration: enabledAdvancedServices in appsscript.json exposes Drive v3 as Drive.

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

  3. OAuth authorization: the executing user grants https://www.googleapis.com/auth/drive.metadata.readonly to the script.

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

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

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), 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 than appDataFolder.

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

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

Source
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, 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.

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

  1. Confirm the practice folder contains the three Google Docs, the intended Google account is active in the editor, and 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 halts the first run, rerun inspectAdvancedDriveFolder after granting the scope.

  3. Inspect the logged JSON report in Execution log and compare its fields against your fixture:

Report fieldMeaning on the initial run
folder.id, folder.nameThe verified folder ID and its current Drive name
request.maximumPages1 (the files.list page budget, excluding the initial files.get call)
request.maximumItems2 (maximumPages * pageSize, an upper bound on accepted items)
pagesArray of per-page summaries recording pageNumber, itemCount, and hasNextPageToken
itemsUp to two child records containing name, mimeType, modifiedTime, and sizeBytes
nextPageAvailabletrue 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 changePrediction for this three-document fixtureWhat to observe
Set includeSecondPage: true, keeping pageSize: 2Up to two list calls and four items; 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; 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

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 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, re-save the manifest or Services entry.

  • 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 a user lacks read access to an ID).

  • Bad request (400) or invalid fields / q parameter: fix the misspelled field selector or parameter in code before rerunning.

  • Forbidden (403) or rate limit (429): inspect the error reason (userRateLimitExceeded or 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
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"
  ]
}