Изначально вопрос выглядел так: "помогите пожалуйста настроить уведомления на 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() через определенные промежутки времени создадим триггер.
Периодичность выполнения - на ваш вкус.
Сохраняем триггер. Вопрос решен.








Комментариев нет:
Отправить комментарий
Комментарий будет опубликован после модерации