Изначально вопрос выглядел так: "помогите пожалуйста настроить уведомления на e-mail об изменениях в папках", и прозвучал в теме "Уголок взаимопомощи" одного из сообществ Google+. Вопрос интересный, для решения пишем веб-приложение, которое сможет следить за изменениями не только "в папках", но и на всем Диске Google, для чего используем Google Apps Script.
Логика приложения будет выглядеть примерно так:
- получить данные о каталогах и файлах
- сохранить данные
- спустя некоторое время получить данные еще раз
- сравнить полученные данные с предыдущими
- в случае наличия изменений сформировать сообщение
- отправить сообщение
- сохранить данные
Самые любопытные могут сразу забрать код на github не вникая в процесс его разработки.
Создаем тестовые данные.
Открываем в браузере Google Drive.
Создаем структуру каталогов. В корневом каталоге создаем скрипт. Я назвал его GDriveNotify.
Раскидаем по каталогам тестовые файлы, точнее создадим документ TestFile и добавим его в несколько каталогов.
Получаем данные.
В проекте GDriveNotify создаем скрипт (Файл - Создать - Скрипт), назовем его GetProp.
В созданном файле пишем код функций, которые будут получать свойства каталогов и файлов, и возвращать полученные данные в формате JSON.
Пишем код функции, myFunction(), созданной по умолчанию.
Схематично данные будут выглядеть примерно так:
Выполняем функцию myFunction().
Авторизуем скрипт, после чего выполняем функцию еще раз.
Проверяем свойство json (Файл - Свойства проекта), копируем.
Вставляем код объекта в какой-нибудь блокнот (я использую Notepad++).
Приведем его в более менее "читабельный" вид.
Сравниваем данные.
Разберемся со следующим вопросом: какие изменения могут происходить с каталогами и файлами, а также каким образом эти изменения отображаются на сохраненные данные.
Создание:
- в случае создания каталога или файла в массив добавляется новый элемент, увеличивается длина массива
Удаление:
- в случае удаления файла или каталога изменяется свойство соответствующего объекта "trashed" с "false" на "true", из массива объектов удаляется элемент, уменьшается длина массива
Копирование, перемещение:
- в случае копирования или перемещения файла или каталога изменяется свойство соответствующего объекта "parents",
которое является масивом, содержащим id родительских каталогов - добавляются либо удаляются элементы массива, изменяется длина массива
- в случае копирования или перемещения файла изменяется свойство каталога "files",
которое является массивом, содержащим id файлов - добавляются либо удаляются элементы массива, изменяется длина массива
Редактирование:
- в случае редактирования файла или каталога изменяется свойство соответствующего объекта "updated", которое представляет из себя дату редактирования
Изменение свойств:
- в случае изменения свойств файла или каталога изменяются свойства соответствующего объекта
Задача нашего приложения - уведомление пользователя Диска Google о перечисленных событиях.
В проекте GDriveNotify создаем скрипт (Файл - Создать - Скрипт), назовем его CompareProp.
В созданном файле пишем код функций, которые будут возвращать результат сравнения текущих свойств каталогов и файлов, и сохраненных в свойстве скрипта, в виде массива измененных объектов.
Изменим код функции myFunction().
Выполним функцию, после чего проверим Logger (Ctrl+Enter).
Усовершенствуем наш код. Переходим от функционального подхода к объектам.
В проекте GDriveNotify создаем еще один скрипт. Я назвал его Class.gs.
Пишем код класса GDriveNotify.
Отправляем сообщение.
Возвращаемся к файлу скрипта, созданному по умолчанию - Код.gs.
Пишем код функции формирования сообщения.
Для теста создадим еще один тестовый документ в каталоге TestFolder12, назовем его TestFile2.
Изменим код функции myFunction().
Выполним функцию MyFunction().
Авторизуем скрипт, выполним функцию еще раз.
Проверяем почту.
Сохраняем данные.
В завершение подрихтуем код функции MyFunction() с тем, чтобы она не забывала сохранять обновленные данные в свойство скрипта по имени "json".
Выполним функцию.
Отправим TestFile2 в корзину и выполним функцию еще раз.
Проверяем почту.
Создание триггера.
Для запуска функции myFunction() через определенные промежутки времени создадим триггер.
Периодичность выполнения - на ваш вкус.
Сохраняем триггер. Вопрос решен.
Логика приложения будет выглядеть примерно так:
- получить данные о каталогах и файлах
- сохранить данные
- спустя некоторое время получить данные еще раз
- сравнить полученные данные с предыдущими
- в случае наличия изменений сформировать сообщение
- отправить сообщение
- сохранить данные
Самые любопытные могут сразу забрать код на 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() через определенные промежутки времени создадим триггер.
Периодичность выполнения - на ваш вкус.
Сохраняем триггер. Вопрос решен.
Комментариев нет:
Отправить комментарий
Комментарий будет опубликован после модерации