Страницы

суббота, 29 июня 2013 г.

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

Изначально вопрос выглядел так: "помогите пожалуйста настроить уведомления на e-mail об изменениях в папках", и прозвучал в теме "Уголок взаимопомощи" одного из сообществ Google+. Вопрос интересный, для решения пишем веб-приложение, которое сможет следить за изменениями не только "в папках", но и на всем Диске Google, для чего используем Google Apps Script.

Логика приложения будет выглядеть примерно так:
- получить данные о каталогах и файлах
- сохранить данные
- спустя некоторое время получить данные еще раз
- сравнить полученные данные с предыдущими
- в случае наличия изменений сформировать сообщение
- отправить сообщение
- сохранить данные

Самые любопытные могут сразу забрать код на github не вникая в процесс его разработки.

Создаем тестовые данные.

Открываем в браузере Google Drive.
Создаем структуру каталогов. В корневом каталоге создаем скрипт. Я назвал его GDriveNotify.
Раскидаем по каталогам тестовые файлы, точнее создадим документ TestFile и добавим его в несколько каталогов.

Получаем данные.

В проекте GDriveNotify создаем скрипт (Файл - Создать - Скрипт), назовем его GetProp.
В созданном файле пишем код функций, которые будут получать свойства каталогов и файлов, и возвращать полученные данные в формате JSON.
function getProps(folder) {
  var ret = [];
  getPropsRec(folder);
  function getPropsRec(folder) {
    var obj = getFolderProp(folder); // каталог
    if (!isMatch(ret, obj)) ret.push(obj);    
    var files = folder.getFiles();
    while (files.hasNext()) { // файлы
      var obj = getFileProp(files.next());
      if (!isMatch(ret, obj)) ret.push(obj);
    }    
    var folders = folder.getFolders();
    while (folders.hasNext()) {
      getPropsRec(folders.next()); // рекурсия
    }
  }
  return ret;
}

// получаем свойства каталога
function getFolderProp(folder) {
  return {
    'id': folder.getId(), 
    'name': folder.getName(),          
    'created': folder.getDateCreated(), 
    'updated': folder.getLastUpdated(),     
    'openUrl': folder.getUrl(),     
    'description': folder.getDescription(), 
    'size': folder.getSize(), 
    'starred': folder.isStarred(), 
    'trashed': folder.isTrashed(), 
    'shareableByEditors': folder.isShareableByEditors(), 
    'files': getArr(folder.getFiles()), 
    'parents': getArr(folder.getParents())
  };
}

// получаем свойства файла
function getFileProp(file) {
  return {
    'id': file.getId(), 
    'name': file.getName(),          
    'created': file.getDateCreated(), 
    'updated': file.getLastUpdated(), 
    'downloadUrl': file.getDownloadUrl(),
    'openUrl': file.getUrl(), 
    'type': file.getMimeType(), 
    'description': file.getDescription(), 
    'size': file.getSize(), 
    'starred': file.isStarred(), 
    'trashed': file.isTrashed(), 
    'shareableByEditors': file.isShareableByEditors(), 
    'parents': getArr(file.getParents())
  }  
}

// ищем повторяющиеся данные
function isMatch(ret, obj) {
  for (var i=0; i<ret.length; i++) {
    if (ret[i].id == obj.id) return true;
  }
}

// получаем массив
function getArr(iterator) {
  var ret = [];
  while (iterator.hasNext()) {
      ret.push(iterator.next().getId())
  }
  return ret;
}

Пишем код функции, myFunction(), созданной по умолчанию.
function myFunction() {  
  var folderId = '0B0YcK5KeNe1tbkRTZjRPUlZ3S0k'; // ID каталога GDriveNotify
  ScriptProperties.setProperty('json', JSON.stringify(getProps(DriveApp.getFolderById(folderId))));
}

Схематично данные будут выглядеть примерно так:
[
 {
  "id": <id каталога или файла>,
  "name": <имя>,
  "created":<дата создания>,
  "updated":<дата изменения>,
  "downloadUrl": <url скачивания (только файлы)>
  "openUrl":<url открытия>,
  "type":<тип (только файлы)>,
  "description":<описание>,
  "size":<размер>,
  "starred":<помечено звездочкой>,
  "trashed":<в корзине>,
  "shareableByEditors":<может ли редактор расшаривать>,
  "files":[<id файла (только каталоги)>],
  "parents":[<id родительского каталога>]
 }
]

Выполняем функцию myFunction().

Авторизуем скрипт, после чего выполняем функцию еще раз.
Проверяем свойство json (Файл - Свойства проекта), копируем.

Вставляем код объекта в какой-нибудь блокнот (я использую Notepad++).
Приведем его в более менее "читабельный" вид.
[
 {
  "id":"0B0YcK5KeNe1tbkRTZjRPUlZ3S0k",
  "name":"GDriveNotify",
  "created":"2013-06-22T08:02:05.553Z",
  "updated":"2013-06-24T06:52:51.665Z",
  "openUrl":"https://docs.google.com/folder/d/0B0YcK5KeNe1tbkRTZjRPUlZ3S0k/edit?usp=drivesdk",
  "description":"",
  "size":0,
  "starred":false,
  "trashed":false,
  "shareableByEditors":true,
  "files":[
   "1dnvLQgq4PjzP85jOFbzO0f3COjvY8yZRIpLA3pXpSA7LN2gEpnQBFOxA",
   "1v7ppsEq-8b1CnIGFGzejKrMcjrdIQvFXkQNZ5z1N_90"
  ],
  "parents":[
   "0B0YcK5KeNe1tYUhsajN4bVpiZWs"
  ]
 },
 {
  "id":"1dnvLQgq4PjzP85jOFbzO0f3COjvY8yZRIpLA3pXpSA7LN2gEpnQBFOxA",
  "name":"GDriveNotify",
  "created":"2013-06-22T07:41:39.988Z",
  "updated":"2013-06-29T15:29:05.254Z",
  "downloadUrl":"https://script.google.com/feeds/download/export?id=1dnvLQgq4PjzP85jOFbzO0f3COjvY8yZRIpLA3pXpSA7LN2gEpnQBFOxA&format=json",
  "openUrl":"https://script.google.com/d/1dnvLQgq4PjzP85jOFbzO0f3COjvY8yZRIpLA3pXpSA7LN2gEpnQBFOxA/edit?usp=drivesdk",
  "type":"application/vnd.google-apps.script",
  "description":"",
  "size":0,
  "starred":false,
  "trashed":false,
  "shareableByEditors":true,
  "parents":[
   "0B0YcK5KeNe1tbkRTZjRPUlZ3S0k"
  ]
 },
 {
  "id":"1v7ppsEq-8b1CnIGFGzejKrMcjrdIQvFXkQNZ5z1N_90",
  "name":"TestFile",
  "created":"2013-06-22T12:27:46.928Z",
  "updated":"2013-06-26T11:08:34.499Z",
  "downloadUrl":null,
  "openUrl":"https://docs.google.com/document/d/1v7ppsEq-8b1CnIGFGzejKrMcjrdIQvFXkQNZ5z1N_90/edit?usp=drivesdk",
  "type":"application/vnd.google-apps.document",
  "description":"",
  "size":0,
  "starred":false,
  "trashed":false,
  "shareableByEditors":true,
  "parents":[
   "0B0YcK5KeNe1tbkRTZjRPUlZ3S0k",
   "0B0YcK5KeNe1tT0FrUlR6ckhxSXM",
   "0B0YcK5KeNe1tQ1NLS1BHdG9ucmc"
  ]
 },
 {
  "id":"0B0YcK5KeNe1tT0FrUlR6ckhxSXM",
  "name":"TestFolder11",
  "created":"2013-06-22T12:27:20.489Z",
  "updated":"2013-06-25T02:12:00.678Z",
  "openUrl":"https://docs.google.com/folder/d/0B0YcK5KeNe1tT0FrUlR6ckhxSXM/edit?usp=drivesdk",
  "description":"",
  "size":0,
  "starred":false,
  "trashed":false,
  "shareableByEditors":true,
  "files":[
   "1v7ppsEq-8b1CnIGFGzejKrMcjrdIQvFXkQNZ5z1N_90"
  ],
  "parents":[
   "0B0YcK5KeNe1tbkRTZjRPUlZ3S0k"
  ]
 },
 {
  "id":"0B0YcK5KeNe1tQ1NLS1BHdG9ucmc",
  "name":"TestFolder21",
  "created":"2013-06-22T15:45:56.535Z",
  "updated":"2013-06-24T06:50:59.994Z",
  "openUrl":"https://docs.google.com/folder/d/0B0YcK5KeNe1tQ1NLS1BHdG9ucmc/edit?usp=drivesdk",
  "description":"",
  "size":0,
  "starred":false,
  "trashed":false,
  "shareableByEditors":true,
  "files":[
   "1v7ppsEq-8b1CnIGFGzejKrMcjrdIQvFXkQNZ5z1N_90"
  ],
  "parents":[
   "0B0YcK5KeNe1tT0FrUlR6ckhxSXM"
  ]
 },
 {
  "id":"0B0YcK5KeNe1tREExMDJjOXVocmM",
  "name":"TestFolder12",
  "created":"2013-06-23T15:39:56.670Z",
  "updated":"2013-06-24T07:19:47.683Z",
  "openUrl":"https://docs.google.com/folder/d/0B0YcK5KeNe1tREExMDJjOXVocmM/edit?usp=drivesdk",
  "description":"",
  "size":0,
  "starred":false,
  "trashed":false,
  "shareableByEditors":true,
  "files":[],
  "parents":[
   "0B0YcK5KeNe1tbkRTZjRPUlZ3S0k"
  ]
 }
]

Сравниваем данные. 

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

Создание:
- в случае создания каталога или файла в массив добавляется новый элемент, увеличивается длина массива

Удаление:
- в случае удаления файла или каталога изменяется свойство соответствующего объекта "trashed" с "false" на "true", из массива объектов удаляется элемент, уменьшается длина массива

Копирование, перемещение:
- в случае копирования или перемещения файла или каталога изменяется свойство соответствующего объекта "parents",
которое является масивом, содержащим id родительских каталогов - добавляются либо удаляются элементы массива, изменяется длина массива
- в случае копирования или перемещения файла изменяется свойство каталога "files",
которое является массивом, содержащим id файлов - добавляются либо удаляются элементы массива, изменяется длина массива

Редактирование:
- в случае редактирования файла или каталога изменяется свойство соответствующего объекта "updated", которое представляет из себя дату редактирования

Изменение свойств:
- в случае изменения свойств файла или каталога изменяются свойства соответствующего объекта

Задача нашего приложения - уведомление пользователя Диска Google о перечисленных событиях.

В проекте GDriveNotify создаем скрипт (Файл - Создать - Скрипт), назовем его CompareProp.
В созданном файле пишем код функций, которые будут возвращать результат сравнения текущих свойств каталогов и файлов, и сохраненных в свойстве скрипта, в виде массива измененных объектов.
function compareObj(arr1, arr2) {  
  return {
    'inserted': getInserted(arr1, arr2), 
    'deleted': getDeleted(arr1, arr2), 
    'modified': getModified(arr1, arr2)
  };  
}

// получаем новые
function getInserted(arr1, arr2) {
  var ret = [];
  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(arr2[i]);      
  }
  return ret;
}

// получаем удаленные из корзины
function getDeleted(arr1, arr2) {
  var ret = [];
  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;
        break;
      } 
    }
    if (!gotIt) ret.push(arr1[i]);
  }
  return ret;
}

// получаем измененные
function getModified(arr1, arr2) {
  var ret = [];
  for (var i=0; i<arr1.length; i++) {
    for (var j=0; j<arr2.length; j++) {
      if (arr1[i].id == arr2[j].id) { // нашли объект        
        for (var prop in arr1[i]) { // сравниваем свойства
          var obj = {};
          if (prop == 'updated' || prop == 'created') { // к датам - особый подход              
            obj.prop1 = new Date(arr1[i][prop]).getTime();
            obj.prop2 = arr2[j][prop].getTime();
          }else if (prop == 'files' || prop == 'parents') { // к массивам тоже              
            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;
      }
    }
  }
  return ret;
}

Изменим код функции myFunction().
function myFunction() {  
  var folderId = '0B0YcK5KeNe1tbkRTZjRPUlZ3S0k'; // ID каталога GDriveNotify  
  Logger.log(compareObj(
    JSON.parse(ScriptProperties.getProperty('json')), 
    getProps(DriveApp.getFolderById(folderId))
  )); 
}

Выполним функцию, после чего проверим Logger (Ctrl+Enter).
[13-06-29 19:50:31:315 GST] 
{
 inserted=[], 
 deleted=[], 
 modified=[
  {
   prop=updated, 
   obj={
    parents=[0B0YcK5KeNe1tbkRTZjRPUlZ3S0k], 
    openUrl=https://script.google.com/d/1dnvLQgq4PjzP85jOFbzO0f3COjvY8yZRIpLA3pXpSA7LN2gEpnQBFOxA/edit?usp=drivesdk, 
    downloadUrl=https://script.google.com/feeds/download/export?id=1dnvLQgq4PjzP85jOFbzO0f3COjvY8yZRIpLA3pXpSA7LN2gEpnQBFOxA&format=json, 
    type=application/vnd.google-apps.script, 
    size=0, 
    id=1dnvLQgq4PjzP85jOFbzO0f3COjvY8yZRIpLA3pXpSA7LN2gEpnQBFOxA, 
    trashed=false, 
    created=Sat Jun 22 11:41:39 GMT+04:00 2013, 
    updated=Sat Jun 29 19:47:49 GMT+04:00 2013, 
    starred=false, 
    description=, 
    name=GDriveNotify, 
    shareableByEditors=true
   }
  }
 ]
}

Рефакторинг.

Усовершенствуем наш код. Переходим от функционального подхода к объектам.
В проекте GDriveNotify создаем еще один скрипт. Я назвал его Class.gs.
Пишем код класса GDriveNotify.
function GDriveNotify(folderId, propName) {
  this.root = DriveApp.getFolderById(folderId) || DriveApp.getRootFolder();
  this.propName = propName || 'json';
}
// возвращает массив объектов - свойства каталогов и файлов
GDriveNotify.prototype.get = function() {
  var ret = [];
  getPropsRec(this.root);
  
  function getPropsRec(folder) {
    var obj = getFolderProp(folder); // каталог
    if (!isMatch(ret, obj)) ret.push(obj);    
    var files = folder.getFiles();
    while (files.hasNext()) { // файлы
      var obj = getFileProp(files.next());
      if (!isMatch(ret, obj)) ret.push(obj);
    }    
    var folders = folder.getFolders();
    while (folders.hasNext()) {
      getPropsRec(folders.next()); // рекурсия
    }
  }
  
  return ret;
  
  // получаем свойства каталога
  function getFolderProp(folder) {
    return {
      'id': folder.getId(), 
      'name': folder.getName(),          
      'created': folder.getDateCreated(), 
      'updated': folder.getLastUpdated(),     
      'openUrl': folder.getUrl(),     
      'description': folder.getDescription(), 
      'size': folder.getSize(), 
      'starred': folder.isStarred(), 
      'trashed': folder.isTrashed(), 
      'shareableByEditors': folder.isShareableByEditors(), 
      'files': getArr(folder.getFiles()), 
      'parents': getArr(folder.getParents())
    };
  }
  
  // получаем свойства файла
  function getFileProp(file) {
    return {
      'id': file.getId(), 
      'name': file.getName(),          
      'created': file.getDateCreated(), 
      'updated': file.getLastUpdated(), 
      'downloadUrl': file.getDownloadUrl(),
      'openUrl': file.getUrl(), 
      'type': file.getMimeType(), 
      'description': file.getDescription(), 
      'size': file.getSize(), 
      'starred': file.isStarred(), 
      'trashed': file.isTrashed(), 
      'shareableByEditors': file.isShareableByEditors(), 
      'parents': getArr(file.getParents())
    }
  }
  
  // ищем повторяющиеся данные
  function isMatch(ret, obj) {
    for (var i=0; i<ret.length; i++) {
      if (ret[i].id == obj.id) return true;
    }
  }
  
  // получаем массив
  function getArr(iterator) {
    var ret = [];
    while (iterator.hasNext()) {
      ret.push(iterator.next().getId())
    }
    return ret;
  }  
}

// возвращает массив объектов - свойства измененных каталогов и файлов
GDriveNotify.prototype.compare = function() {
  var arr1 = JSON.parse(ScriptProperties.getProperty(this.propName)) || []; // получаем свойство скрипта
  var arr2 = this.get(); // получаем свойства каталогов и файлов
  var ret = [];  
  
  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 = arr2[j][prop].getTime();
          }else if (prop == 'files' || prop == 'parents') { // к массивам тоже              
            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'}); // удаленные из корзины
  }
  
  return ret;
}

Отправляем сообщение.

Возвращаемся к файлу скрипта, созданному по умолчанию - Код.gs.
Пишем код функции формирования сообщения.
function getMessage(updated) {
  var ret = [];
  for (var i=0; i<updated.length; i++) {
    var arr = [];
    if (updated[i].prop == 'inserted') {        
      arr.push('Новый объект:');      
      arr.push('- имя - ' + updated[i].obj.name);
      arr.push('- дата создания - ' + updated[i].obj.created);
      arr.push('- ссылка - ' + updated[i].obj.openUrl);    
      ret.push(arr.join('\n'));
    } else if (updated[i].prop == 'deleted') {        
      arr.push('Объект удален:');      
      arr.push('- имя - ' + updated[i].obj.name);     
      ret.push(arr.join('\n'));
    } else {        
      arr.push('Свойства объекта изменились:');      
      arr.push('- имя - ' + updated[i].obj.name);
      arr.push('- свойство - ' + updated[i].prop);
      arr.push('- значение - ' + updated[i].obj[updated[i].prop]);
      arr.push('- ссылка - ' + updated[i].obj.openUrl);
      ret.push(arr.join('\n'));
    }
  }  
  return ret;
}

Для теста создадим еще один тестовый документ в каталоге TestFolder12, назовем его TestFile2.

Изменим код функции myFunction().
function myFunction() {  
  var folderId = '0B0YcK5KeNe1tbkRTZjRPUlZ3S0k'; // ID каталога GDriveNotify   
  var gdn = new GDriveNotify(folderId);
  var msg = getMessage(gdn.compare());
  if (msg.length > 0) {
    msg.unshift('Количество объектов: ' + msg.length);    
    GmailApp.sendEmail(
      Session.getActiveUser().getEmail(), 
      'Обнаружены изменения на Диске Google', 
      msg.join('\n')
    );
  }  
}

Выполним функцию MyFunction().

Авторизуем скрипт, выполним функцию еще раз.
Проверяем почту.

Сохраняем данные. 

В завершение подрихтуем код функции MyFunction() с тем, чтобы она не забывала сохранять обновленные данные в свойство скрипта по имени "json".
function myFunction() {  
  var folderId = '0B0YcK5KeNe1tbkRTZjRPUlZ3S0k'; // ID каталога GDriveNotify 
  var gdn = new GDriveNotify(folderId);
  var msg = getMessage(gdn.compare());
  if (msg.length > 0) {
    ScriptProperties.setProperty('json', JSON.stringify(gdn.get()));
    msg.unshift('Количество объектов: ' + msg.length);    
    GmailApp.sendEmail(
      Session.getActiveUser().getEmail(), 
      'Обнаружены изменения на Диске Google', 
      msg.join('\n')
    );
  }  
}

Выполним функцию.
Отправим TestFile2 в корзину и выполним функцию еще раз.
Проверяем почту.

Создание триггера.

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

Сохраняем триггер. Вопрос решен.