Предыстория

Google изменяет политику хранения данных с 1 июня 2021 года. Вкратце: документы и фото теперь станут полновесными и будут учитываться в общей квоте 15Гб. К тому же, при неактивности аккаунта более двух лет, Google может удалить ваши данные.

Я часто работаю с Google документами, и при активном использовании дисковая квота закончится довольно быстро. Но есть и хорошая новость: документы, созданные до 1 июня 2021 года так и останутся невесомыми, поэтому вы не получите превышение квоты в одночасье.

У меня сразу возникла мысль сделать документов "в запас". Ниже я расскажу, как это можно осуществить, не тратя много времени и сил.

Пишем скрипт, создающий документы

Скрипт буду писать с помощью Google Apps Script.

Создаём новую таблицу Google, заходим в редактор скриптов (Инструменты - Редактор скриптов).

Создаём три файла

  • main.gs - основной код для создания файлов

  • menu.gs - код для создания пользовательского меню при открытии таблицы

  • index.html - шаблон страницы для отображения информации

Сначала в файле menu.gs создаём функцию onOpen(). Это простой триггер, который выполняется при открытии таблицы. Его задача - создать пользовательское меню, из которого можно запустить функцию.

function onOpen(e) { 												// Выполняется при открытии таблицы
  SpreadsheetApp.getUi() 										// Получаем интерфейс пользователя
      .createMenu('Меню')										// Создаём меню
      .addItem('Создать документы', 'main') // Создаём команду меню
      .addToUi();														// Добавляем меню в интерфейс
}

В файле main.gs создадим функцию main(), которая и будет запускаться с кнопки в меню.

function main() { // Меню - Создать документы
  // Создаём HTML документ из шаблона
  let template = HtmlService.createTemplateFromFile(`index`);
  // Показываем модальное окно с HTML 
  SpreadsheetApp.getUi()
    .showModelessDialog(template.evaluate(),`Создаю документы...`);
}

Пора разобраться с index.html. это обычный HTML файл, который отобразится в модальном окне. там можно использовать стили, скрипты и т.п.

index.html
<!doctype html>
<html lang="en">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">

    <script>
      let timer = function(){ // Обслуживает таймер
        // Получаем текущее время
        let now = (new Date()).getTime();
        
        // Задаём режим обновления таймера - 1 раз в секунду
        setInterval(function(){
          // Получаем прошедшее время. now будет доступно из-за замыкания.
          let time = (new Date()).getTime() - now;
          // Вычисляем минуты
          let minutes = Math.floor(time/60000);
          // И секунды
          let seconds = Math.floor(time%60000/1000);
          // Обновляем данные в поле lifeTime
          updateData("lifeTime", `${minutes<10?"0"+minutes:minutes}:${seconds<10?"0"+seconds:seconds}`);
        },1000);
      };
      
      updateData = function(id, value){ // Обновляет информацию в одном элементе по id
        let element = document.getElementById(id);
        if (element) {
          element.innerHTML = value;
        };
      };
      
      refreshData = function(){ // Обновляем все данные
        google.script.run.withSuccessHandler(function(data){
        updateData("createTables", data.createTables); 	// Таблиц создано:
        updateData("createDocs", data.createDocs);			// Документов создано:
        updateData("createForms", data.createForms);		// Форм создано
        updateData("createSlides", data.createSlides);	// Презентаций создано
        updateData("log", data.log);										// Лог
      }).getData(); // Получаем данные 
    };

      setTimeout(setInterval(refreshData, 2000),1000); 	// Данные обновляем раз в 2 секунды
      timer();																					// Запуск таймера
      google.script.run.doMagic();								// Запуск функции для создания документов
    </script>
  </head>
  <body>
    <div class=".container bg-dark text-white text-center">
      <div class="row">
        <div class="col">
          Таблиц
        </div>        
        <div class="col">
          Документов
        </div>        
        <div class="col">
          Форм
        </div>        
        <div class="col">
          Презентаций
        </div>
      </div>
      <div class="row">
        <div class="col" id="createTables">
          0
        </div>
        <div class="col" id="createDocs">
          0
        </div>        
        <div class="col" id="createForms">
          0
        </div>        
        <div class="col" id="createSlides">
          0
        </div>
      </div>
    </div>    
     <div class=".container bg-dark text-white">
      <div class="row">
        <div class="col text-right" id="label_lifeTime">
          Время работы:  
        </div>        
        <div class="col  text-left" id="lifeTime">
          00:00
        </div>        
      </div>
    </div>

    <div bg-dark text-white id="label_log">Лог: </div>
    <ul class="list-group" id="log">
    </ul>
  </body>
</html>

В ней подключаем стили, создаём скрипт, в котором три функции:

  • updateData(id, value) - ищет на странице элемент по id и обновляет содержимое

  • refreshData() - обновляет все данные на странице, кроме таймера

  • timer() - обслуживает таймер

Для создания документов в файле main.gs создаём универсальную функцию.

function create()
const FILES_TO_CREATE = 50;

function create(filesToCreate = FILES_TO_CREATE, folderId, prefix="file_", app, key) {
  // Получаем словарь ключ-значение для текущего скрипта.
  let props = PropertiesService.getScriptProperties();
  // Получаем значение для ключа data. Преобразуем в объект
  let data = JSON.parse(props.getProperty(`data`));

  try{
    // Получаем директорию по id
    let folder = DriveApp.getFolderById(folderId);
    for(var i=0; i<filesToCreate; i++){ // Создаём filesToCreate документов
      // Создаём документ, получаем его id
      let ssId = app.create(`${prefix}${(new Date()).getTime()}`).getId();
      // Получаем файл по его id
      let ss = DriveApp.getFileById(ssId);
      // Копируем файл в папку
      folder.addFile(ss);
      // Удаляем файл из корневой папки
      DriveApp.getRootFolder().removeFile(ss);

      // Увеличиваем счётчик созданных файлов
      data[key]=1+data[key];
      // Сохраняем новые данные
      props.setProperty(`data`, JSON.stringify(data));
    };
  }catch(err){
    // В случае ошибки - пишем её в лог
    logToHtml(`Error: ${err}`, LOG_TYPES.danger);
  };
  // Возвращаем из функции количество созданных файлов
  return +i;
};

И конкретные реализации - для таблиц, документов, форм и презентаций.

Обёртки для функции create()
function createSheets(key) {
  // Получаем id папки для таблиц
  let folderId =  PropertiesService.getScriptProperties().getProperty(`sheetsFolder`);
  // Создаём нужное количество таблиц
  let count = create(FILES_TO_CREATE, folderId, `sheet_`, SpreadsheetApp, key);
  // Сохраняем в лог
  logToHtml(`${count} sheets were created`, LOG_TYPES.success);
}

function createDocs(key) {
  let folderId =  PropertiesService.getScriptProperties().getProperty(`docsFolder`);
  let count = create(FILES_TO_CREATE, folderId, `doc_`, DocumentApp, key);
  logToHtml(`${count} docs were created`, LOG_TYPES.success);
}

function createForms(key) {
  let folderId =  PropertiesService.getScriptProperties().getProperty(`formsFolder`);
  let count = create(FILES_TO_CREATE, folderId, `form_`, FormApp, key);
  logToHtml(`${count} forms were created`, LOG_TYPES.success);
}

function createSlides(key) {
  let folderId =  PropertiesService.getScriptProperties().getProperty(`slidesFolder`);
  let count = create(FILES_TO_CREATE, folderId, `slide_`, SlidesApp, key);
  logToHtml(`${count} slides were created`, LOG_TYPES.success);
}

Далее делаем функцию для создания папок.

function createFolders(){
  // Получаем словарь ключ-значение для текущего скрипта
  let props = PropertiesService.getScriptProperties();

  // Задаём структуру папок
  let folders = [
    {key:`rootFolder`,   name:`Прозапас`                         },
    {key:`sheetsFolder`, name:`Sheets`, parentFolder:`rootFolder`},
    {key:`docsFolder`,   name:`Docs`,   parentFolder:`rootFolder`},
    {key:`formsFolder`,  name:`Forms`,  parentFolder:`rootFolder`},
    {key:`slidesFolder`, name:`Slides`, parentFolder:`rootFolder`},
    ];

  // Проходим по структуре и создаём папки
    folders.forEach(folder=>{
      if (!props.getProperty(folder.key)){ // Если папка ещё не создана
        // Если есть параметр rootFolder, то используем его, иначе выбираем корневую папку
        let parentFolder = folder.parentFolder?DriveApp.getFolderById(props.getProperty(folder.parentFolder)):DriveApp.getRootFolder();
        // Создаём папку
        let folderId = parentFolder.createFolder(folder.name).getId();
        // Сохраняем информацию о ней
        props.setProperty(folder.key, folderId);
      };
    });
}

И функцию для создания всех типов документов:

Основная функция, которая запускает создание папок и документов
function doMagic(){
  let props = PropertiesService.getScriptProperties();

  // Структура данных
  let data = {
    createTables:0, 
    createDocs:0, 
    createForms:0, 
    createSlides:0, 
    startTime:new Date(),
    log:``,
  };

  // Сохраняем данные в словарь скрипта
  props.setProperty(`data`, JSON.stringify(data));
  
  try{
    createFolders(); 							// Создаём папки
    createSheets(`createTables`);	// Создаём таблицы
    createDocs(`createDocs`);			// Создаём документы
    createForms(`createForms`);		// Создаём формы
    createSlides(`createSlides`);	// Создаём презентации
    
    // Сообщаем, что всё готово
    SpreadsheetApp.getUi().alert(`Готово!`);
  }catch(err){
    // При ошибке сообщаем об этом
    SpreadsheetApp.getUi().alert(`Ошибка! ${err}`);
  };
};

Остаётся создать функцию для получения данных - так модальное окно может получить актуальные данные для отображения.

function getData(){ // Получает данные из словаря
  // Получаем словарь ключ-значение
  let props = PropertiesService.getScriptProperties();
  // Получаем нужные данные и преобразуем в объект
  let data = JSON.parse(props.getProperty(`data`));
  return data; //Возвращаем данные
};

И функция для записи строки в лог:

const LOG_TYPES = { // Типы сообщений. Взято из bootstrap
  primary:   "primary",
  secondary: "secondary",
  success:   "success",
  danger:    "danger",
  warning:   "warning",
  info:      "info",  
};

function logToHtml(log, type = LOG_TYPES.primary){
  // Получаем данные
  let data = getData();
  // Добавляем li тег (лог отображается в виде списка) с данными
  data.log+=`<li class="list-group-item text-${type}">${log}</li>\n`;
  // Сохраняем данные
  let props = PropertiesService.getScriptProperties();
  props.setProperty(`data`, JSON.stringify(data));
};

Что получилось

Остаётся только запустить скрипт из меню. При первом запуске Google запросит права - это нормально. После этого откроется окно, в котором можно наблюдать за прогрессом.

Модальное окно для визуализации прогресса
Модальное окно для визуализации прогресса

Ограничения скрипта

  • Время жизни скрипта ограничено 6 минутами. За это время он успеет создать несколько сотен документов. Можно обойти это ограничение, закинув все функции непосредственно в HTML код модального окна и обращаться к диску по API, но об этом в следующий раз

  • Есть ограничение на количество ежедневно созданных документов. Рано или поздно будет появляться ошибка Exception: Служба была вызвана слишком много раз за день: docs create. Тогда скрипт можно запустить на следующий день


TL;DR

Всё вышеописанное я собрал в таблицу, которую можно скопировать себе(Файл - Создать копию), запустить(Меню - Создать файлы) и получить к себе на диск несколько сотен файлов. При необходимости процедуру повторить.

Спасибо за внимание. Буду рад получить фидбэк по коду. Удачи!