Работа с Google Drive

Скрипт, который каждый месяц сам выгружает отчёт в PDF, кладёт его в папку «Отчёты/2026/07», открывает доступ руководителю и присылает ссылку.

DriveApp — служба Apps Script для работы с Google Диском: создание и поиск файлов и папок, копирование, перемещение, выдача прав доступа.

Зачем это нужно

Ежемесячный отчёт — классика офисной рутины. Открыть таблицу, «Файл → Скачать → PDF», выбрать нужный лист, поправить ориентацию, сохранить, залить на Диск, найти прошлогоднюю папку, создать новую, переименовать файл по стандарту, расшарить, скопировать ссылку, вставить в письмо. Десять минут в месяц, два часа в год, и каждый раз кто-нибудь ошибается в названии.

Всё это описывается тридцатью строками кода — и дальше работает само по триггеру первого числа. Заодно вы получаете то, чего не даёт ручная работа: единообразие. Файлы всегда названы одинаково, всегда лежат там, где ожидаешь, и всегда доступны тем, кому нужно.

Файлы и папки: главное, что нужно понять

На Google Диске нет путей в привычном смысле. Нет метода getFolderByPath('Отчёты/2026/07'). Есть файлы, у которых указан родитель, и подниматься по этой цепочке приходится вручную, уровень за уровнем.

Второе, ещё более важное: имена на Диске не уникальны. В одной папке могут спокойно лежать три файла «Отчёт.pdf» и две папки «2026». Поэтому все методы поиска возвращают не объект, а итератор — ленивую последовательность, которую нужно обходить через hasNext() и next().

function getOrCreateFolder(parent, name) {
  const folders = parent.getFoldersByName(name);
  return folders.hasNext() ? folders.next() : parent.createFolder(name);
}

Эта крошечная функция — рабочая лошадка любого скрипта архивации. Без неё каждый запуск честно вызовет createFolder('Отчёты') и создаст ещё одну папку с тем же именем: через полгода на Диске будет шесть «Отчётов», и в каждой по одному файлу. Google не ругнётся — с его точки зрения всё легально.

Поиск файлов

Для более сложных выборок есть searchFiles с языком запросов Drive:

function findOldReports() {
  const query = 'title contains "Отчёт" and modifiedDate < "2026-01-01" and trashed = false';
  const files = DriveApp.searchFiles(query);
  const result = [];

  while (files.hasNext()) {
    const file = files.next();
    result.push([file.getName(), file.getUrl(), file.getLastUpdated()]);
  }
  Logger.log('Найдено файлов: ' + result.length);
  return result;
}

Обратите внимание на кавычки: внутри строки запроса значения обёрнуты в двойные кавычки, а сама строка — в одинарные. Условие trashed = false добавляйте всегда: по умолчанию поиск найдёт и то, что лежит в корзине.

Экспорт листа в PDF

Здесь новичков ждёт сюрприз: у SpreadsheetApp нет метода «сохрани лист как PDF». Есть два обходных пути.

Быстрый: ss.getAs('application/pdf') — но он выгрузит всю книгу целиком, со всеми служебными листами, в вертикальной ориентации и с сеткой. Годится для черновиков.

Точный: обратиться к тому же URL экспорта, который дёргает кнопка «Скачать → PDF» в интерфейсе. Он принимает параметры: конкретный лист, ориентацию, поля, сетку.

function exportSheetToPdf(sheet) {
  const ss = sheet.getParent();
  const params = [
    'format=pdf',
    'gid=' + sheet.getSheetId(),   // какой именно лист
    'portrait=false',              // альбомная ориентация
    'fitw=true',                   // вписать по ширине страницы
    'gridlines=false',             // без сетки
    'printtitle=false',            // без названия книги в шапке
    'sheetnames=false',
    'top_margin=0.5', 'bottom_margin=0.5',
    'left_margin=0.5', 'right_margin=0.5'
  ].join('&');

  const url = 'https://docs.google.com/spreadsheets/d/' + ss.getId() + '/export?' + params;

  const response = UrlFetchApp.fetch(url, {
    headers: { Authorization: 'Bearer ' + ScriptApp.getOAuthToken() },
    muteHttpExceptions: true
  });

  if (response.getResponseCode() !== 200) {
    throw new Error('Экспорт не удался, код ' + response.getResponseCode());
  }
  return response.getBlob().setName(sheet.getName() + '.pdf');
}

Ключевая строка — ScriptApp.getOAuthToken(). Она отдаёт токен доступа текущего пользователя, и мы подставляем его в заголовок Authorization. Без него Google ответит страницей входа: с его точки зрения к приватному документу постучался анонимный клиент. Именно так скрипт «притворяется» вашим браузером и получает ровно тот PDF, который вы увидели бы, нажав кнопку скачивания.

Сценарий целиком: архивация отчёта

function archiveMonthlyReport() {
  const ss = SpreadsheetApp.getActive();
  const sheet = ss.getSheetByName('Отчёт');
  const tz = ss.getSpreadsheetTimeZone();
  const now = new Date();

  const year = Utilities.formatDate(now, tz, 'yyyy');
  const month = Utilities.formatDate(now, tz, 'MM');
  const stamp = Utilities.formatDate(now, tz, 'yyyy-MM-dd');

  const root = getOrCreateFolder(DriveApp.getRootFolder(), 'Отчёты');
  const folder = getOrCreateFolder(getOrCreateFolder(root, year), month);

  const blob = exportSheetToPdf(sheet).setName('Отчёт ' + stamp + '.pdf');
  const file = folder.createFile(blob);

  file.addViewer('[email protected]');

  MailApp.sendEmail({
    to: '[email protected]',
    subject: 'Отчёт за ' + stamp,
    htmlBody: '<p>Отчёт готов: <a href="' + file.getUrl() + '">открыть</a></p>'
  });

  Logger.log('Сохранено: ' + file.getUrl());
}

Двадцать строк — и вся ручная процедура из вступления автоматизирована. Utilities.formatDate с часовым поясом таблицы обязателен: без него скрипт, запущенный триггером в 00:30, легко положит июльский отчёт в папку за июнь, потому что серверное время — UTC.

Раздача доступа

МетодЧто делает
file.addViewer(email)даёт человеку право просмотра, Google шлёт ему уведомление
file.addEditor(email)право редактирования
file.addCommenter(email)право комментировать
file.removeViewer(email)отзывает доступ
file.getViewers()список тех, у кого есть доступ — удобно для аудита
file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW)доступ «по ссылке» всем подряд

С последней строкой обращайтесь осторожно. Ссылка на финансовый отчёт, однажды попавшая в рабочий чат, живёт там вечно и пересылается дальше без вашего ведома. Точечный addViewer почти всегда правильнее. В корпоративных доменах политика безопасности может вообще запрещать публичные ссылки — тогда вызов упадёт с ошибкой прав.

Как это работает

DriveApp — это обёртка над сетевым API Диска. Каждый вызов getFilesByName, createFile, addViewer — это отдельный запрос к серверу Google, который занимает десятки, а то и сотни миллисекунд. Локально это незаметно, но в цикле по тысяче строк превращается в катастрофу: скрипт упрётся в лимит выполнения (6 минут) задолго до конца.

Отсюда правило: обращения к Диску выносим за пределы циклов. Нужную папку получаем один раз до цикла, а не на каждой итерации.

Второй важный момент — чей это Диск. Файлы создаются на Диске того аккаунта, от имени которого работает скрипт, и занимают его квоту хранилища. Если скрипт написал стажёр и повесил триггер, а потом уволился и его аккаунт отключили, архив отчётов пропадёт вместе с ним. Для важных процессов используйте общий диск или сервисный аккаунт организации.

И третье: file.setTrashed(true) отправляет файл в корзину, откуда его можно восстановить. Полного удаления в DriveApp нет намеренно — это отличная страховка. Не пишите скрипты, которые «чистят» Диск безвозвратно; перемещайте в архивную папку или в корзину.

Частые ошибки

  • Считать, что getFoldersByName вернёт папку. Он возвращает итератор. Обращение к .getName() напрямую даст ошибку, а .lengthundefined.
  • Создавать папку без проверки. Каждый запуск плодит дубликат с тем же именем. Всегда используйте паттерн «найди или создай».
  • Забыть trashed = false в поиске. Скрипт «находит» файлы, которые пользователь давно удалил.
  • Экспортировать книгу вместо листа. ss.getAs('application/pdf') отдаёт все листы, включая служебные, где лежат пароли и черновики расчётов.
  • Не ставить muteHttpExceptions: true при экспорте. Вместо внятного «код 401» вы получите обрезанное исключение и будете гадать, что сломалось.
  • Не учитывать часовой пояс. Ночной триггер раскладывает файлы по неправильным месяцам.
  • Раздавать ANYONE_WITH_LINK «чтобы не возиться с правами». Это самая частая причина утечки внутренних данных через Диск.

Итоги

  • На Диске нет путей и нет уникальных имён: спускайтесь по папкам вручную и всегда работайте через итераторы.
  • getOrCreateFolder — обязательный помощник; без него архив зарастает дублями папок.
  • Точный экспорт листа в PDF делается через URL /export с параметрами и токеном ScriptApp.getOAuthToken().
  • Даты в именах файлов и папок форматируйте через Utilities.formatDate с часовым поясом таблицы.
  • Доступ выдавайте точечно (addViewer), а не ссылкой для всех.
  • Каждый вызов DriveApp — сетевой запрос: не держите их внутри длинных циклов.
Проверьте себя
1. Что возвращает DriveApp.getRootFolder().getFoldersByName('Отчёты')?
AМассив папок, у которого можно спросить length
BИтератор FolderIterator, который нужно обходить через hasNext() и next()
CПервую найденную папку или null, если папок нет
DОшибку, если папок с таким именем несколько
2. Зачем в exportSheetToPdf передаётся заголовок Authorization с ScriptApp.getOAuthToken()?
AЧтобы PDF получился в альбомной ориентации
BЧтобы Google понял, что запрос идёт от авторизованного пользователя, имеющего доступ к таблице — иначе вернётся страница входа
CЧтобы не тратить квоту UrlFetchApp
DТокен нужен только для публичных таблиц