Реинкарнация скрипта, опубликованного в одном из предыдущих постов. Хранение данных в свойствах скрипта - не самая лучшая идея, о чем недвусмысленно сказано в разделе "ограничения" официальной документации, поэтому в текущем перерождении кода в качестве хранилища данных я решил использовать ScriptDB. Напомню, что первоначальный вариант кода был ответом на вопрос одного из сообществ Google+ и являлся скорее вектором, чем готовым решением. Насколько текущий вариант ближе к решению - судить вам.
Почему.
По причине ограничения в 9kB на размер свойства скрипта, предыдущий вариант кода имел возможность сохранить лишь ограниченное количество информации о файлах и каталогах. В ответ на это ограничение в сообществе разработчиков GAS появилось решение - сохранять данные о каждом объекте (файле, каталоге) в отдельно взятое свойство скрипта... что увеличивает лимит до 500kB.
Короче, ввиду интереса, проявленного к коду, встречайте... в красном углу ринга ... :)
Как это можно применить.
Для начала предлагаю проверить скорость работы Drive Service, для чего пробежимся по всем файлам и каталогам.
Выполняем код, проверяем журнал (Ctrl + Enter), у меня следующие показатели:
Что же будет если попытаться получить свойства всех этих объектов, да еще и рекурсивно (подразумевая, что рекурсия тоже кушает ресурсы путем увеличения стека контекстов)?
Выполним код. Я ждал, пока хватало сил...
Откроем отчет о выполнении (Вид - Отчет о выполнении):
Похоже лимит выполнения скрипта - 6 минут. Прокрутим отчет вверх:
Каждый раз получая свойства файла или каталога в коде у нас происходил запрос на сервер, а плюс еще итераторы... не удивительно, что времени не хватило.
ОК, замахнемся на каталог поменьше.
Выполним код, откроем отчет о выполнении.
Я уложился в 100 секунд. Это прорыв :).
Выполним обновление хранилища - заполним ScriptDB документами JSON, содержащими информацию о свойствах объектов (файлов, каталогов).
Откроем журнал.
Проверим отчет о выполнении.
Посчитаем количество документов в хранилище - то же, что и количество объектов (файлов, каталогов) в целевом каталоге (каталоге, который мы планируем мониторить на предмет изменений свойств дочерних объектов).
Прочие свойства и методы объекта по имени GDriveDog достаточно подробно описаны в коде, разобраться не составит труда.
Заключительный пример - функция, которая будет выполнять всю работу по мониторингу изменений в целевом каталоге и отправке уведомлений на Email - на самом деле единственный код, который необходимо добавить к коду объекта.
Выполняем, открываем отчет о выполнении.
Проверяем почту.
Еще один тест: один файл удалим (но у него в целевом каталоге этажом ниже осталась копия), еще один отмечаем звездочкой, выполняем код, проверяем почту.
Поясню, по порядку:
- файл отмечен звездочкой
- ссылка на файл присутствовала в двух каталогах, из одного мы его удалили, поэтому количество родительских объектов уменьшилось с 2 до 1
- одновременно с удалением ссылки на файл уменьшилось количество файлов в каталоге с 3 до 2
Для запуска функции myFunction() через определенные промежутки времени не забываем создать триггер (по этому и другим вопросам обращаемся к предыдущему посту).
Почему.
По причине ограничения в 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() через определенные промежутки времени не забываем создать триггер (по этому и другим вопросам обращаемся к предыдущему посту).
Комментариев нет:
Отправить комментарий
Комментарий будет опубликован после модерации