Страницы

вторник, 3 декабря 2013 г.

Как настроить уведомления об изменениях в Google Drive #2.

Реинкарнация скрипта, опубликованного в одном из предыдущих постов. Хранение данных в свойствах скрипта - не самая лучшая идея, о чем недвусмысленно сказано в разделе "ограничения" официальной документации, поэтому в текущем перерождении кода в качестве хранилища данных я решил использовать ScriptDB. Напомню, что первоначальный вариант кода был ответом на вопрос одного из сообществ Google+ и являлся скорее вектором, чем готовым решением. Насколько текущий вариант ближе к решению - судить вам.

Почему.

По причине ограничения в 9kB на размер свойства скрипта, предыдущий вариант кода имел возможность сохранить лишь ограниченное количество информации о файлах и каталогах. В ответ на это ограничение в сообществе разработчиков GAS появилось решение - сохранять данные о каждом объекте (файле, каталоге) в отдельно взятое свойство скрипта... что увеличивает лимит до 500kB.

Короче, ввиду интереса, проявленного к коду, встречайте... в красном углу ринга ... :)
var GDriveDog = (function () {
  function GDriveDog(db, itemId) { // конструктор
    try {
      this.db = db;
      this.dbSize = db.query({}).getSize(); // размер хранилища, заодно проверяем наличие объекта
    } catch(e) {
      throw new Error(e.message);
      return;
    }
    if (typeof itemId === 'string') {
      try{
        this.root = DriveApp.getFolderById(itemId);
      } catch(e) {
        throw new Error(e.message);
        return;
      }
      this.id = itemId;
    } else {
      this.root = DriveApp.getRootFolder();
      this.id = this.root.getId();
    }
    this.name = this.root.getName();
  }
  
  GDriveDog.prototype.getDb = function() { // получаем содержимое хранилища
    var ret = [], db = this.db, res = db.query({id: this.id}), item;
    while (res.hasNext()) {
      item = res.next();
      ret = ret.concat(item['data']);
    }
    return ret;
  }
  
  GDriveDog.prototype.clearDb = function() { // удаляем содержимое хранилища
    var db = this.db, res = db.query({id: this.id});
    while (res.hasNext()) {
       db.remove(res.next());
    }
    return true;
  }
  
  GDriveDog.prototype.updateDb = function(arr) { // обновляем содержимое хранилища
    this.clearDb(); // удаляем
    var db = this.db, data = arr || this.get(); // получаем
    var res = db.saveBatch([{id: this.id, data: data}], false); // обновляем
    if (db.allOk(res)) {
      return true;
    }
    return false; // не удалось сохранить все объекты
  }
    
  GDriveDog.prototype.get = function() { // получаем свойства каталогов и файлов
    var ret = [];    
    getPropsRec(this.root);
    return ret;
    
    function getPropsRec(folder) {
      var obj = getProps(folder, true); // каталог
      if (!match(obj)) ret.push(obj); // пропускаем одни и те же объекты
      var files = folder.getFiles();
      while (files.hasNext()) { // файлы
        var obj = getProps(files.next(), false);        
        if (!match(obj)) ret.push(obj);
      }    
      var folders = folder.getFolders();      
      while (folders.hasNext()) { // каталоги
        getPropsRec(folders.next()); // рекурсия        
      }      
    }      
    
    // ищем повторяющиеся данные - на случай если объекты находятся сразу в нескольких каталогах
    function match(obj) {
      for (var i=0; i<ret.length; i++) {
        if (ret[i].id == obj.id) return true;
      }
    }
    
    // получаем массив id 
    function getIdArr(iterator) {
      var ret = [];
      while (iterator.hasNext()) {
        ret.push(iterator.next().getId());
      }
      return ret;
    }  
    
    // получаем массив email
    function getEmailArr(arr) {
      var ret = [];
      for (var i=0; i<arr.length; i++) {
        ret.push(arr[i].getEmail());
      }
      return ret;
    }  
    
    // получаем свойства объектов
    function getProps(item, isFolder) {
      var ret = {
        'created': String(item.getDateCreated()), 
        'description': item.getDescription(), 
        'editors': getEmailArr(item.getEditors()),         
        'id': item.getId(), 
        'updated': String(item.getLastUpdated()),
        'name': item.getName(),          
        'owner': item.getOwner().getEmail(),
        'parents': getIdArr(item.getParents()),
        'sharingAccess': String(item.getSharingAccess()),
        'sharingPermission': String(item.getSharingPermission()),
        'size': item.getSize(),      
        'openUrl': item.getUrl(),
        'viewers': getEmailArr(item.getViewers()),
        'shareableByEditors': item.isShareableByEditors(), 
        'starred': item.isStarred(), 
        'trashed': item.isTrashed()
      }
      if (isFolder) {
        ret.files = getIdArr(item.getFiles());
        ret.folders = getIdArr(item.getFolders());
      } else {
        ret.downloadUrl = item.getDownloadUrl();
        ret.type = item.getMimeType();
      }
      return ret;
    }    
  }
  
  GDriveDog.prototype.compare = function() { // сравниваем свойства объектов с данными хранилища
    var ret = [], arr2 = this.get(), arr1 = this.getDb();
    
    for (var i=0; i<arr2.length; i++) { // получаем новые
      var gotIt = false;
      for (var j=0; j<arr1.length; j++) {        
        if (arr1[j].id == arr2[i].id) {
          gotIt = true;
          break;
        }
      }
      if (!gotIt) ret.push({'obj': arr2[i], 'prop': 'inserted'});      
    }
    for (var i=0; i<arr1.length; i++) { // получаем удаленные из корзины и сравниваем
      var gotIt = false;
      for (var j=0; j<arr2.length; j++) {
        if (arr1[i].id == arr2[j].id) {
          gotIt = true;          
          for (var prop in arr1[i]) { // сравниваем свойства
            var obj = {};
            if (prop == 'updated' || prop == 'created') { // к датам - особый подход              
              obj.prop1 = new Date(arr1[i][prop]).getTime();
              obj.prop2 = new Date(arr2[j][prop]).getTime();
            } else if (prop == 'files' || prop == 'folders' || prop == 'parents' || prop == 'editors' || prop == 'viewers') { // к массивам тоже              
              obj.prop1 = arr1[i][prop].length;
              obj.prop2 = arr2[j][prop].length;           
            } else {
              obj.prop1 = arr1[i][prop];
              obj.prop2 = arr2[j][prop];
            }
            if (obj.prop1 != obj.prop2) {
              ret.push({'obj': arr2[j], 'prop': prop});            
            }  
          }          
          break;
        } 
      }
      if (!gotIt) ret.push({'obj': arr1[i], 'prop': 'deleted'}); // удаленные из корзины
    }
    this.updateDb(arr2);
    return ret;  
  }
  
  GDriveDog.prototype.getMessage = function(arr) { // собираем сообщение
    var ret = [], updated = arr || this.compare();
    for (var i=0; i<updated.length; i++) {
      var arr = [];
      switch (updated[i].prop) {
        case 'inserted':
          arr.push('Новый объект:');         
          arr.push('- дата создания - ' + updated[i].obj.created);         
          break;
        case 'deleted':
          arr.push('Объект удален:');                  
          break;
        case 'created':
          arr.push('Изменилась дата создания:');         
          arr.push('- дата создания - ' + updated[i].obj.created);          
          break;
        case 'updated':
          arr.push('Изменилась дата редактирования:');          
          arr.push('- дата редактирования - ' + updated[i].obj.updated);          
          break;
        case 'description':
          arr.push('Изменилось описание:');          
          arr.push('- описание - ' + updated[i].obj.description);          
          break;
        case 'name':
          arr.push('Изменилось имя:');          
          break;
        case 'owner':
          arr.push('Изменился владелец:');          
          arr.push('- владелец - ' + updated[i].obj.owner);          
          break;
        case 'parents':
          arr.push('Изменилось количество родительских объектов:');          
          arr.push('- количество - ' + updated[i].obj.parents.length);          
          break;
        case 'size':
          arr.push('Изменился размер:');          
          arr.push('- размер - ' + updated[i].obj.size);          
          break;
        case 'editors':
          arr.push('Изменилось количество редакторов:');          
          arr.push('- количество - ' + updated[i].obj.editors.length);          
          break;
        case 'viewers':
          arr.push('Изменилось количество обозревателей:');         
          arr.push('- количество - ' + updated[i].obj.viewers.length);          
          break;
        case 'files':
          arr.push('Изменилось количество файлов:');         
          arr.push('- количество - ' + updated[i].obj.files.length);          
          break;
        case 'folders':
          arr.push('Изменилось количество каталогов:');  
          arr.push('- количество - ' + updated[i].obj.folders.length);          
          break;
        case 'starred':
          if (updated[i].obj.starred) {
            arr.push('Объект отмечен звездочкой:');
          } else {
            arr.push('Отметка объекта звездочкой снята:');
          }          
          break;
        case 'trashed':
          if (updated[i].obj.trashed) {
            arr.push('Объект перемещен в корзину');
          } else {
            arr.push('Объект восстановлен из корзины');
          }          
          break;
        default:
          arr.push('Свойства объекта изменились:');          
          arr.push('- свойство - ' + updated[i].prop);
          arr.push('- значение - ' + updated[i].obj[updated[i].prop]);                    
          break;
      }
      arr.push('- имя - ' + updated[i].obj.name);
      arr.push('- ссылка - ' + updated[i].obj.openUrl);
      ret.push(arr.join('\n'));
    }    
    return ret;
  }
  
  return GDriveDog;
})();

Как это можно применить.

Для начала предлагаю проверить скорость работы Drive Service, для чего пробежимся по всем файлам и каталогам.
var t = Date.now(), i = 0, j = 0;
  var files = DriveApp.getFiles(), folders = DriveApp.getFolders();
  while (files.hasNext()) {
    i++;
    files.next();
  } 
  while (folders.hasNext()) {
    j++;
    folders.next();
  }
  Logger.log('Файлов: ' + i + '\nКаталогов: ' + j + '\nВремя: ' + (Date.now() - t) + ' ms');

Выполняем код, проверяем журнал (Ctrl + Enter), у меня следующие показатели:

Что же будет если попытаться получить свойства всех этих объектов, да еще и рекурсивно (подразумевая, что рекурсия тоже кушает ресурсы путем увеличения стека контекстов)?
// первый аргумент - ScriptDb
var gdd = new GDriveDog(ScriptDb.getMyDb());
gdd.get();

Выполним код. Я ждал, пока хватало сил...

Откроем отчет о выполнении (Вид - Отчет о выполнении):

Похоже лимит выполнения скрипта - 6 минут. Прокрутим отчет вверх:

Каждый раз получая свойства файла или каталога в коде у нас происходил запрос на сервер, а плюс еще итераторы... не удивительно, что времени не хватило.

ОК, замахнемся на каталог поменьше.
// второй аргумент - id каталога
var gdd = new GDriveDog(ScriptDb.getMyDb(), '0B0YcK5KeNe1tYUhsajN4bVpiZWs');
gdd.get();

Выполним код, откроем отчет о выполнении.

Я уложился в 100 секунд. Это прорыв :).

Выполним обновление хранилища - заполним ScriptDB документами JSON, содержащими информацию о свойствах объектов (файлов, каталогов).
var gdd = new GDriveDog(ScriptDb.getMyDb(), '0B0YcK5KeNe1tYUhsajN4bVpiZWs');
Logger.log('Обновление хранилища: ' + gdd.updateDb());

Откроем журнал.

Проверим отчет о выполнении.

Посчитаем количество документов в хранилище - то же, что и количество объектов (файлов, каталогов) в целевом каталоге (каталоге, который мы планируем мониторить на предмет изменений свойств дочерних объектов).
var gdd = new GDriveDog(ScriptDb.getMyDb(), '0B0YcK5KeNe1tYUhsajN4bVpiZWs');
Logger.log('Количество документов: ' + gdd.dbSize);

Выполним код, проверим журнал.

Прочие свойства и методы объекта по имени GDriveDog достаточно подробно описаны в коде, разобраться не составит труда.

Заключительный пример - функция, которая будет выполнять всю работу по мониторингу изменений в целевом каталоге и отправке уведомлений на Email - на самом деле единственный код, который необходимо добавить к коду объекта.
function myFunction() {  
  var gdd = new GDriveDog(ScriptDb.getMyDb(), '0B0YcK5KeNe1tYUhsajN4bVpiZWs');  
  var msg = gdd.getMessage();
  if (msg.length > 0) {    
    msg.unshift('Количество объектов: ' + msg.length);    
    GmailApp.sendEmail(
      Session.getActiveUser().getEmail(), 
      'Обнаружены изменения на Диске Google, каталог - ' + gdd.name,  
      msg.join('\n')
    );
  }  
}

Выполняем, открываем отчет о выполнении.

Проверяем почту.

Еще один тест: один файл удалим (но у него в целевом каталоге этажом ниже осталась копия), еще один отмечаем звездочкой, выполняем код, проверяем почту.

Поясню, по порядку:
- файл отмечен звездочкой
- ссылка на файл присутствовала в двух каталогах, из одного мы его удалили, поэтому количество родительских объектов уменьшилось с 2 до 1
- одновременно с удалением ссылки на файл уменьшилось количество файлов в каталоге с 3 до 2

Для запуска функции myFunction() через определенные промежутки времени не забываем создать триггер (по этому и другим вопросам обращаемся к предыдущему посту).