list activities on google drive - google-apps-script

I would like to be able to upload my file activities with API from google drive activity or other ideas. Each of my files are stored in a tree structure that contains the name of my client. And in the following put it in an excel file in order to be able to sort it correctly for example if a file has been modified more than 2 times in one day, it is validated
Currently, I used the code provided by google but I can't find the directory of these modified files
I thank you in advance, I really block, other ideas or API are welcome.
Sincerly
/**
* Lists 10 activity for a Drive user.
* #see https://developers.google.com/drive/activity/v2/reference/rest/v2/activity/query
*/
function listDriveActivity() {
const request = {
"ancestor_name": "items/root",
"filter": "time >= \"2022-06-01T00:00:00Z\" time < \"2022-06-30T00:00:00Z\" detail.action_detail_case:EDIT",
"consolidation_strategy": { "legacy": {} },
"page_size": 10,
};
try {
// Activity.query method is used Query past activity in Google Drive.
const response = DriveActivity.Activity.query(request);
const activities = response.activities;
if (!activities || activities.length === 0) {
console.log('No activity.');
return;
}
console.log('Recent activity:');
for (const activity of activities) {
// get time information of activity.
const time = getTimeInfo(activity);
// get the action details/information
const action = getActionInfo(activity.primaryActionDetail);
// get the actor's details of activity
const actors = activity.actors.map(getActorInfo);
// get target information of activity.
const targets = activity.targets.map(getTargetInfo);
// print the time,actor,action and targets of drive activity.
console.log('%s: %s, %s, %s', time, actors, action, targets);
}
} catch (err) {
// TODO (developer) - Handle error from drive activity API
console.log('Failed with an error %s', err.message);
}
}
/**
* #param {object} object
* #return {string} Returns the name of a set property in an object, or else "unknown".
*/
function getOneOf(object) {
for (const key in object) {
return key;
}
return 'unknown';
}
/**
* #param {object} activity Activity object.
* #return {string} Returns a time associated with an activity.
*/
function getTimeInfo(activity) {
if ('timestamp' in activity) {
return activity.timestamp;
}
if ('timeRange' in activity) {
return activity.timeRange.endTime;
}
return 'unknown';
}
/**
* #param {object} actionDetail The primary action details of the activity.
* #return {string} Returns the type of action.
*/
function getActionInfo(actionDetail) {
return getOneOf(actionDetail);
}
/**
* #param {object} user The User object.
* #return {string} Returns user information, or the type of user if not a known user.
*/
function getUserInfo(user) {
if ('knownUser' in user) {
const knownUser = user.knownUser;
const isMe = knownUser.isCurrentUser || false;
return isMe ? 'people/me' : knownUser.personName;
}
return getOneOf(user);
}
/**
* #param {object} actor The Actor object.
* #return {string} Returns actor information, or the type of actor if not a user.
*/
function getActorInfo(actor) {
if ('user' in actor) {
return getUserInfo(actor.user);
}
return getOneOf(actor);
}
/**
* #param {object} target The Target object.
* #return {string} Returns the type of a target and an associated title.
*/
function getTargetInfo(target) {
if ('driveItem' in target) {
const title = target.driveItem.title || 'unknown';
return 'driveItem:"' + title + '"';
}
if ('drive' in target) {
const title = target.drive.title || 'unknown';
return 'drive:"' + title + '"';
}
if ('fileComment' in target) {
const parent = target.fileComment.parent || {};
const title = parent.title || 'unknown';
return 'fileComment:"' + title + '"';
}
return getOneOf(target) + ':unknown';
}
Final goal: to get the amount of days I work for all my clients.
That a idea
After some search and code (some help with GTP so sorry for begin code)
function listDriveActivity(sheet,timezone) {
let pageToken = null;
do {
try {
// Activity.query method is used Query past activity in Google Drive.
const response = DriveActivity.Activity.query({ "ancestor_name": "items/13oHhdSDQqnM4ppO48FPJq7HAmVjR27H5", //"items/root",
"filter": "time >= \"2022-01-01T00:00:00Z\" time < \"2022-01-31T00:00:00Z\" detail.action_detail_case:EDIT",
"consolidation_strategy": { "legacy": {} },"pageSize": 10,pageToken: pageToken});
//Logger.log(response);
const activities = response.activities;
if (!activities || activities.length === 0) {
console.log('No activity.');
return;
}
//console.log('Recent activity:');
for (const activity of activities) {
// get time information of activity.
const time = getTimeInfo(activity);
// get the action details/information
const action = getActionInfo(activity.primaryActionDetail);
// get the actor's details of activity
//const actors = activity.actors.map(getActorInfo);
// get target information of activity.
const targets = activity.targets.map(getTargetInfo);
// print the time,actor,action and targets of drive activity.
// const folderName = activity.targets.map(getFileArborescenceByName);
const folderName = activity.targets.map(getFileArborescenceByID);
var NomDossier = JSON.stringify(folderName).replace(/\[\"|\"\]/g,'');
const ClientName = getClient(NomDossier);
//console.log('%s: %s, %s, %s, %s', time, action, targets, folderName, ClientName);
const timeAsDate = new Date(time);
const lastModified = Utilities.formatDate(timeAsDate, "UTC", "dd-MM-yyyy HH:mm");
var Nomfichier = JSON.stringify(targets).replace(/\[\"|\"\]/g,'');
sheet.appendRow([lastModified, action, Nomfichier, NomDossier, ClientName]);
}
pageToken = response.nextPageToken;
}
catch (err) {
// TODO (developer) - Handle error from drive activity API
console.log('Failed with an error %s', err.message);
}
} while (pageToken);
return sheet;
}
function getFileArborescenceByID(activity) {
if ('driveItem' in activity) {
try {
const fileName = activity.driveItem.name;
var modif = getLastPart (fileName);
var file = DriveApp.getFileById(modif);
var folders = [];
var parent = file.getParents();
while (parent.hasNext()) {
parent = parent.next();
folders.push(parent.getName());
parent = parent.getParents();
}
if (folders.length) {
// Display the full folder path
var folderName = (folders.reverse().join("/"));
}
//var ClientName = getClient(folderName);
}
catch (err) {
// TODO (developer) - Handle error from drive activity API
console.log('Failed with an error %s', err.message);
}
}
//return 'folderName:"' + folderName + '", Client :"' + ClientName + '"';
return folderName;

Related

Exception: Cannot retrieve the next object: iterator has reached the end

im trying to get files in different folders and get some cell values from them and put it in a sheet.
but i get below error
Exception: Cannot retrieve the next object: iterator has reached the end
im using below code and when i run it. it returns error on line 14
it seems code returns some values from first and second folders but for third one it returns error
after running macro it gives log below:
5:24:43 PM Notice Execution started
5:24:45 PM Info [[]]
5:24:45 PM Info [[]]
5:24:46 PM Error
Exception: Cannot retrieve the next object: iterator has reached the end.
list_files_in_folders # macros.gs:14
my code : line 14 error
function list_files_in_folders(){
var sh = SpreadsheetApp.getActiveSheet();
var mainfolder = DriveApp.getFolderById('id-here'); // I change the folder ID here
var mfolders = mainfolder.getFolders();
var data = [];
data.push(['Person Name','File Name','Value 1','Value 2']);
while (mfolders.hasNext){
var mfolder = mfolders.next();
var personfolder = DriveApp.getFolderById(mfolder.getId());
var pfiles = personfolder.getFiles();
data.push([personfolder.getName,,,]);
while(pfiles.hasNext){
var pfile = pfiles.next(); //error here
var personfile = SpreadsheetApp.openById(pfile.getId());
var value1 = personfile.getSheetValues(2,9,1,1);
//var value2 = personfile.getSheetValues();
Logger.log(value1);
data.push(["",pfile.getName,value1,""]);
}
}
sh.getRange(1,1,data.length,data[0].length).setValues(data);
}
Folder or File hasNext() is a method, not a property.
Replace both
mfolders.hasNext
pfiles.hasNext
With
mfolders.hasNext()
pfiles.hasNext()
Reference
FolderIterator.hasNext()
FileIterator.hasNext()
You can make your list_files_in_folders() function easier to adapt by doing report building in one function and the recursive subfolder iteration in another, using closures to process the files.
More lines of code are required, but making changes becomes much easier, and you can apply these functions in other use cases more smoothly — just modify the _fileAction and _folderAction closures as required.
This pattern also lets you handle errors and timeouts more gracefully.
/** #NotOnlyCurrentDoc */
'use strict';
function list_files_in_folders() {
const mainFolder = DriveApp.getFolderById('...put folder ID here...');
const data = generateReport_(mainFolder);
SpreadsheetApp.getActiveSheet().getRange('A1')
.offset(0, 0, data.length, data[0].length)
.setValues(data);
}
/**
* Generates a report based on spreadsheets organized by subfolder.
*
* #param {DriveApp.Folder} folder A folder with files and subfolders.
* #return {String[][]} A 2D array that contains a report of files organized by subfolder.
*/
function generateReport_(folder) {
const data = [];
const _errorHandler = (error) => console.log(error.message);
const _prefix = (folderName, nestingLevel) => ' '.repeat(nestingLevel) + folderName;
const _folderAction = (folder, nestingLevel) => {
data.push([_prefix(folder.getName(), nestingLevel), '', '', '']);
};
const _fileAction = (file) => {
const ss = getSpreadsheetFromFile_(file, _errorHandler);
if (ss) {
data.push(['', ss.getName(), ss.getSheetValues(2, 9, 1, 1), '']);
} else {
data.push(['', file.getName(), '(not a Google Sheet)', '']);
}
};
const timelimit = new Date().getTime() + 4 * 60 * 1000; // stop when 4 minutes have passed
const error = processFilesInFolderRecursively_(folder, _folderAction, _fileAction, _errorHandler, 1, timelimit);
if (error) {
data.push([error.message, '', '', '']);
_errorHandler(error);
}
return [['Person Name', 'File Name', 'Value 1', 'Value 2']].concat(
data.length ? data : [['(Could not find any files. Check folder ID.)', '', '', '']]
);
}
/**
* Iterates files in a folder and its subfolders, executing
* _folderAction with each folder and _fileAction with each file.
*
* #param {DriveApp.Folder} folder The folder to process.
* #param {Function} _folderAction The function to run with each folder.
* #param {Function} _fileAction The function to run with each file.
* #param {Function} _errorHandler The function to call when an error occurs.
* #param {String} nestingLevel Optional. A number that indicates the current folder nesting nevel.
* #param {Number} timelimit Optional. The moment in milliseconds that indicates when to stop processing.
* #return {Error} An error when the function ran out of time, otherwise undefined.
* #license https://www.gnu.org/licenses/gpl-3.0.html
*/
function processFilesInFolderRecursively_(folder, _folderAction, _fileAction, _errorHandler, nestingLevel, timelimit) {
// version 1.2, written by --Hyde, 1 December 2022
nestingLevel = nestingLevel || 0;
const outOfTime = new Error('Ran out of time.');
if (new Date().getTime() > timelimit) {
return outOfTime;
}
const files = folder.getFiles();
while (files.hasNext()) {
if (new Date().getTime() > timelimit) {
return outOfTime;
}
try {
_fileAction(files.next());
} catch (error) {
_errorHandler(error);
}
}
const subfolders = folder.getFolders();
while (subfolders.hasNext()) {
const folder = subfolders.next();
_folderAction(folder, nestingLevel);
const error = processFilesInFolderRecursively_(folder, _folderAction, _fileAction, _errorHandler, nestingLevel + 1, timelimit);
if (error) {
return error; // outOfTime
};
}
}
/**
* Gets a spreadsheet object from a file object.
*
* #param {DriveApp.File} file The file that contains a spreadsheet.
* #param {Function} _errorHandler The function to call when an error occurs.
* #return {SpreadsheetApp.Spreadsheet} The spreadsheet object, or null if file is not a spreadsheet or cannot be accessed.
* #license https://www.gnu.org/licenses/gpl-3.0.html
*/
function getSpreadsheetFromFile_(file, _errorHandler) {
// version 1.0, written by --Hyde, 9 October 2022
if (file.getMimeType() !== 'application/vnd.google-apps.spreadsheet') {
return null;
}
let ss;
try {
ss = SpreadsheetApp.open(file);
} catch (error) {
_errorHandler(error);
return null;
}
return ss;
}

Google Calendar API App Script - "Error: Not Found: Skipping"

I'm using this App Script Populate a team vacation calendar
I'm receiving the following error for 1 person in a Google Group of 50 users.
'Error retriving events for email#email.com, vacation:
GoogleJsonResponseException: API call to calendar.events.list failed
with error: Not Found; skipping'
When setting a breakpoint, it fails on this import event.
function importEvent(username, event) {
event.summary = '[' + username + '] ' + event.summary;
event.organizer = {
id: TEAM_CALENDAR_ID,
};
event.attendees = [];
console.log('Importing: %s', event.summary);
try {
Calendar.Events.import(event, TEAM_CALENDAR_ID);
} catch (e) {
console.error('Error attempting to import event: %s. Skipping.',
e.toString());
}
}
do {
params.pageToken = pageToken;
let response;
try {
response = Calendar.Events.list(user.getEmail(), params);
} catch (e) {
console.error('Error retriving events for %s, %s: %s; skipping',
user, keyword, e.toString());
continue;
}
events = events.concat(response.items.filter(function(item) {
return shoudImportEvent(user, keyword, item);
}));
pageToken = response.nextPageToken;
} while (pageToken);
return events;
}
This is the stack:
"GoogleJsonResponseException: API call to calendar.events.list failed
with error: Not Found at findEvents (Code:111:34) at Code:48:20 at
Array.forEach () at Code:47:14 a…"
This is the full code
// Set the ID of the team calendar to add events to. You can find the calendar's
// ID on the settings page.
let TEAM_CALENDAR_ID = 'CALENDAR ID';
// Set the email address of the Google Group that contains everyone in the team.
// Ensure the group has less than 500 members to avoid timeouts.
let GROUP_EMAIL = 'GROUP EMAIL';
let KEYWORDS = ['vacation', 'ooo', 'out of office', 'offline'];
let MONTHS_IN_ADVANCE = 3;
/**
* Sets up the script to run automatically every hour.
*/
function setup() {
let triggers = ScriptApp.getProjectTriggers();
if (triggers.length > 0) {
throw new Error('Triggers are already setup.');
}
ScriptApp.newTrigger('sync').timeBased().everyHours(1).create();
// Runs the first sync immediately.
sync();
}
/**
* Looks through the group members' public calendars and adds any
* 'vacation' or 'out of office' events to the team calendar.
*/
function sync() {
// Defines the calendar event date range to search.
let today = new Date();
let maxDate = new Date();
maxDate.setMonth(maxDate.getMonth() + MONTHS_IN_ADVANCE);
// Determines the time the the script was last run.
let lastRun = PropertiesService.getScriptProperties().getProperty('lastRun');
lastRun = lastRun ? new Date(lastRun) : null;
// Gets the list of users in the Google Group.
let users = GroupsApp.getGroupByEmail(GROUP_EMAIL).getUsers();
// For each user, finds events having one or more of the keywords in the event
// summary in the specified date range. Imports each of those to the team
// calendar.
let count = 0;
users.forEach(function(user) {
let username = user.getEmail().split('#')[0];
KEYWORDS.forEach(function(keyword) {
let events = findEvents(user, keyword, today, maxDate, lastRun);
events.forEach(function(event) {
importEvent(username, event);
count++;
}); // End foreach event.
}); // End foreach keyword.
}); // End foreach user.
PropertiesService.getScriptProperties().setProperty('lastRun', today);
console.log('Imported ' + count + ' events');
}
/**
* Imports the given event from the user's calendar into the shared team
* calendar.
* #param {string} username The team member that is attending the event.
* #param {Calendar.Event} event The event to import.
*/
function importEvent(username, event) {
event.summary = '[' + username + '] ' + event.summary;
event.organizer = {
id: TEAM_CALENDAR_ID,
};
event.attendees = [];
console.log('Importing: %s', event.summary);
try {
Calendar.Events.import(event, TEAM_CALENDAR_ID);
} catch (e) {
console.error('Error attempting to import event: %s. Skipping.',
e.toString());
}
}
/**
* In a given user's calendar, looks for occurrences of the given keyword
* in events within the specified date range and returns any such events
* found.
* #param {Session.User} user The user to retrieve events for.
* #param {string} keyword The keyword to look for.
* #param {Date} start The starting date of the range to examine.
* #param {Date} end The ending date of the range to examine.
* #param {Date} optSince A date indicating the last time this script was run.
* #return {Calendar.Event[]} An array of calendar events.
*/
function findEvents(user, keyword, start, end, optSince) {
let params = {
q: keyword,
timeMin: formatDateAsRFC3339(start),
timeMax: formatDateAsRFC3339(end),
showDeleted: true,
};
if (optSince) {
// This prevents the script from examining events that have not been
// modified since the specified date (that is, the last time the
// script was run).
params.updatedMin = formatDateAsRFC3339(optSince);
}
let pageToken = null;
let events = [];
do {
params.pageToken = pageToken;
let response;
try {
response = Calendar.Events.list(user.getEmail(), params);
} catch (e) {
console.error('Error retriving events for %s, %s: %s; skipping',
user, keyword, e.toString());
continue;
}
events = events.concat(response.items.filter(function(item) {
return shoudImportEvent(user, keyword, item);
}));
pageToken = response.nextPageToken;
} while (pageToken);
return events;
}
/**
* Determines if the given event should be imported into the shared team
* calendar.
* #param {Session.User} user The user that is attending the event.
* #param {string} keyword The keyword being searched for.
* #param {Calendar.Event} event The event being considered.
* #return {boolean} True if the event should be imported.
*/
function shoudImportEvent(user, keyword, event) {
// Filters out events where the keyword did not appear in the summary
// (that is, the keyword appeared in a different field, and are thus
// is not likely to be relevant).
if (event.summary.toLowerCase().indexOf(keyword.toLowerCase) < 0) {
return false;
}
if (!event.organizer || event.organizer.email == user.getEmail()) {
// If the user is the creator of the event, always imports it.
return true;
}
// Only imports events the user has accepted.
if (!event.attendees) return false;
let matching = event.attendees.filter(function(attendee) {
return attendee.self;
});
return matching.length > 0 && matching[0].responseStatus == 'accepted';
}
/**
* Returns an RFC3339 formated date String corresponding to the given
* Date object.
* #param {Date} date a Date.
* #return {string} a formatted date string.
*/
function formatDateAsRFC3339(date) {
return Utilities.formatDate(date, 'UTC', 'yyyy-MM-dd\'T\'HH:mm:ssZ');
}

LockService lock does not persist after a prompt is shown

I have a script in Google Sheets, which runs a function when a user clicks on an image. The function modifies content in cells and in order to avoid simultaneous modifications I need to use lock for this function.
I cannot get, why this doesn't work (I still can invoke same function several times from different clients):
function placeBidMP1() {
var lock = LockService.getScriptLock();
lock.waitLock(10000)
placeBid('MP1', 'I21:J25');
lock.releaseLock();
}
placeBid() function is below:
function placeBid(lotName, range) {
var firstPrompt = ui.prompt(lotName + '-lot', 'Please enter your name:', ui.ButtonSet.OK);
var firstPromptSelection = firstPrompt.getSelectedButton();
var userName = firstPrompt.getResponseText();
if (firstPromptSelection == ui.Button.OK) {
do {
var secondPrompt = ui.prompt('Increase by', 'Amount (greater than 0): ', ui.ButtonSet.OK_CANCEL);
var secondPromptSelection = secondPrompt.getSelectedButton();
var increaseAmount = parseInt(secondPrompt.getResponseText());
} while (!(secondPromptSelection == ui.Button.CANCEL) && !(/^[0-9]+$/.test(increaseAmount)) && !(secondPromptSelection == ui.Button.CLOSE));
if (secondPromptSelection != ui.Button.CANCEL & secondPromptSelection != ui.Button.CLOSE) {
var finalPrompt = ui.alert("Price for lot will be increased by " + increaseAmount + " CZK. Are you sure?", ui.ButtonSet.YES_NO);
if (finalPrompt == ui.Button.YES) {
var cell = SpreadsheetApp.getActiveSheet().getRange(range);
var currentCellValue = Number(cell.getValue());
cell.setValue(currentCellValue + Number(increaseAmount));
bidsHistorySheet.appendRow([userName, lotName, cell.getValue()]);
SpreadsheetApp.flush();
showPriceIsIncreased();
} else {showCancelled();}
} else {showCancelled();}
} else {showCancelled();}
}
I have several placeBidMP() functions for different elements on the Sheet and need to lock only separate function from being invoked multiple times.
I've tried as well next way:
if (lock.waitLock(10000)) {
placeBidMP1(...);
}
else {
showCancelled();
}
and in this case, it shows cancellation pop-up straight away.
I still can invoke the same function several times from different clients
The documentation is clear on that part: prompt() method won't persist LockService locks as it suspends script execution awaiting user interaction:
The script resumes after the user dismisses the dialog, but Jdbc connections and LockService locks don't persist across the suspension
and in this case, it shows cancellation pop-up straight away
Nothing strange here as well - if statement evaluates what's inside the condition and coerces the result to Boolean. Take a look at the waitLock() method signature - it returns void, which is a falsy value. You essentially created this: if(false) and this is why showCancelled() fires straight away.
Workaround
You could work around that limitation by emulating what Lock class does. Be aware that this approach is not meant to replace the service, and there are limitations too, specifically:
PropertiesService has quota on reads / writes. A generous one, but you might want to set toSleep interval to higher values to avoid burning through your quota at the expense of precision.
Do not replace the Lock class with this custom implementation - V8 does not put your code in a special context, so the services are directly exposed and can be overridden.
function PropertyLock() {
const toSleep = 10;
let timeoutIn = 0, gotLock = false;
const store = PropertiesService.getScriptProperties();
/**
* #returns {boolean}
*/
this.hasLock = function () {
return gotLock;
};
/**
* #param {number} timeoutInMillis
* #returns {boolean}
*/
this.tryLock = function (timeoutInMillis) {
//emulates "no effect if the lock has already been acquired"
if (this.gotLock) {
return true;
}
timeoutIn === 0 && (timeoutIn = timeoutInMillis);
const stored = store.getProperty("locked");
const isLocked = stored ? JSON.parse(stored) : false;
const canWait = timeoutIn > 0;
if (isLocked && canWait) {
Utilities.sleep(toSleep);
timeoutIn -= toSleep;
return timeoutIn > 0 ?
this.tryLock(timeoutInMillis) :
false;
}
if (!canWait) {
return false;
}
store.setProperty("locked", true);
gotLock = true;
return true;
};
/**
* #returns {void}
*/
this.releaseLock = function () {
store.setProperty("locked", false);
gotLock = false;
};
/**
* #param {number} timeoutInMillis
* #returns {boolean}
*
* #throws {Error}
*/
this.waitLock = function (timeoutInMillis) {
const hasLock = this.tryLock(timeoutInMillis);
if (!hasLock) {
throw new Error("Could not obtain lock");
}
return hasLock;
};
}
Version 2
What follows below is closer to the original and solves one important issue with using PropertiesService as a workaround: if there is an unhandled exception during the execution of the function that acquires the lock, the version above will get the lock stuck indefinitely (can be solved by removing the corresponding script property).
The version below (or as a gist) uses a self-removing time-based trigger set to fire after the current maximum execution time of a script is exceeded (30 minutes) and can be configured to a lower value should one wish to clean up earlier:
var PropertyLock = (() => {
let locked = false;
let timeout = 0;
const store = PropertiesService.getScriptProperties();
const propertyName = "locked";
const triggerName = "PropertyLock.releaseLock";
const toSleep = 10;
const currentGSuiteRuntimeLimit = 30 * 60 * 1e3;
const lock = function () { };
/**
* #returns {boolean}
*/
lock.hasLock = function () {
return locked;
};
/**
* #param {number} timeoutInMillis
* #returns {boolean}
*/
lock.tryLock = function (timeoutInMillis) {
//emulates "no effect if the lock has already been acquired"
if (locked) {
return true;
}
timeout === 0 && (timeout = timeoutInMillis);
const stored = store.getProperty(propertyName);
const isLocked = stored ? JSON.parse(stored) : false;
const canWait = timeout > 0;
if (isLocked && canWait) {
Utilities.sleep(toSleep);
timeout -= toSleep;
return timeout > 0 ?
PropertyLock.tryLock(timeoutInMillis) :
false;
}
if (!canWait) {
return false;
}
try {
store.setProperty(propertyName, true);
ScriptApp.newTrigger(triggerName).timeBased()
.after(currentGSuiteRuntimeLimit).create();
console.log("created trigger");
locked = true;
return locked;
}
catch (error) {
console.error(error);
return false;
}
};
/**
* #returns {void}
*/
lock.releaseLock = function () {
try {
locked = false;
store.setProperty(propertyName, locked);
const trigger = ScriptApp
.getProjectTriggers()
.find(n => n.getHandlerFunction() === triggerName);
console.log({ trigger });
trigger && ScriptApp.deleteTrigger(trigger);
}
catch (error) {
console.error(error);
}
};
/**
* #param {number} timeoutInMillis
* #returns {boolean}
*
* #throws {Error}
*/
lock.waitLock = function (timeoutInMillis) {
const hasLock = PropertyLock.tryLock(timeoutInMillis);
if (!hasLock) {
throw new Error("Could not obtain lock");
}
return hasLock;
};
return lock;
})();
var PropertyLockService = (() => {
const init = function () { };
/**
* #returns {PropertyLock}
*/
init.getScriptLock = function () {
return PropertyLock;
};
return init;
})();
Note that the second version uses static methods and, just as LockService, should not be instantiated (you could go for a class and static methods to enforce this).
References
waitLock() method reference
prompt() method reference
Falsiness concept in JavaScript

Getting past permissions for a file through the API/Apps Script

I'm trying to create a list of files stored in my Google Drive and also a list of their current and previous permissions. Specifically, I want to create a list of files in my Google Drive which at any point in the past have had the 'Anyone with a link can view/edit (etc)' permission set.
I have created a Google Apps Script to do this and I can iterate through all the files OK and I can get files which currently have that permission set, but I can't see a way to get the history of the file's permissions.
I have found and activated the revisions list API: https://developers.google.com/drive/api/v2/reference/revisions/list
This gets revisions but I can't see anywhere that it lists the sharing history of a revision.
Is what I'm attempting to do possible?
It's definitely possible using the Drive Activity API. You can use the Quickstart for Google Apps Script to view all the activity of an item (file or folder) or done by a User. In this case I modified the Quickstart to show the Permissions changes of a given Drive Id.
function listDriveActivity() {
var request = {
itemName: "items/1bFQvSJ8pMdss4jInrrg7bxdae3dKgu-tJqC1A2TktMs", //Id of the file
pageSize: 10};
var response = DriveActivity.Activity.query(request);
var activities = response.activities;
if (activities && activities.length > 0) {
Logger.log('Recent activity:');
for (var i = 0; i < activities.length; i++) {
var activity = activities[i];
var time = getTimeInfo(activity);
var action = getActionInfo(activity.primaryActionDetail);
var actors = activity.actors.map(getActorInfo);
var targets = activity.targets.map(getTargetInfo);
if (action == "permissionChange"){ //Only show permissionChange activity
Logger.log(
'%s: %s, %s, %s', time, truncated(actors), action,
truncated(targets));
}
}
} else {
Logger.log('No activity.');
}
}
/** Returns a string representation of the first elements in a list. */
function truncated(array, opt_limit) {
var limit = opt_limit || 2;
var contents = array.slice(0, limit).join(', ');
var more = array.length > limit ? ', ...' : '';
return '[' + contents + more + ']';
}
/** Returns the name of a set property in an object, or else "unknown". */
function getOneOf(object) {
for (var key in object) {
return key;
}
return 'unknown';
}
/** Returns a time associated with an activity. */
function getTimeInfo(activity) {
if ('timestamp' in activity) {
return activity.timestamp;
}
if ('timeRange' in activity) {
return activity.timeRange.endTime;
}
return 'unknown';
}
/** Returns the type of action. */
function getActionInfo(actionDetail) {
return getOneOf(actionDetail);
}
/** Returns user information, or the type of user if not a known user. */
function getUserInfo(user) {
if ('knownUser' in user) {
var knownUser = user.knownUser;
var isMe = knownUser.isCurrentUser || false;
return isMe ? 'people/me' : knownUser.personName;
}
return getOneOf(user);
}
/** Returns actor information, or the type of actor if not a user. */
function getActorInfo(actor) {
if ('user' in actor) {
return getUserInfo(actor.user)
}
return getOneOf(actor);
}
/** Returns the type of a target and an associated title. */
function getTargetInfo(target) {
if ('driveItem' in target) {
var title = target.driveItem.title || 'unknown';
return 'driveItem:"' + title + '"';
}
if ('drive' in target) {
var title = target.drive.title || 'unknown';
return 'drive:"' + title + '"';
}
if ('fileComment' in target) {
var parent = target.fileComment.parent || {};
var title = parent.title || 'unknown';
return 'fileComment:"' + title + '"';
}
return getOneOf(target) + ':unknown';
}
Remember to enable the Drive Activity API in Resources > Advanced Google Services
In my example this returns the logs:
You can also look deeper into the Permissions by using the permissionChange Parameters in the query.
If you have a business/enterprise/edu account the admin audit logs will tell you this for 6 months of data. Or it will at least tell you when a permission was changed from x to y.
Can't think of a method for personal.

How can I use Google Script method UrlFetchApp.fetch within an ArrayFormula?

function getContentForAPI(path)
{
var result = {};
result.url = "https://" + HOST + "/rest/api/2/" + path;
result.response = UrlFetchApp.fetch(result.url, API_HEADERS);
result.text = result.response.getContentText();
try { result.data = JSON.parse(result.text); } catch(error) {}
return result;
}
I have a custom formula like such:
/**
* Import data.
*
* #param {string} the ID that belongs to the issue.
* #return The summary.
* #customfunction
*/
function IMPORTDATA(issueID)
{
var details = [];
if (issueID instanceof Array) {
var issueIDs = issueID;
for (i in issueIDs){
for (j in issueIDs[i]){
issueID = issueIDs[i][j];
var content = getContentForAPI("issue/"+issueID);
if (content == undefined || content.data == undefined) {
details.push(["G","H"]);
} else {
details.push(["E", "F"]);
}
}
}
} else {
var content = getContentForAPI("issue/"+issueID);
if (content.data != undefined) {
details.push([content.data.fields.project.name, content.data.fields.summary]);
}
}
return details;
}
This works for a single formula =IMPORTDATA(A2) however when I put it within an ArrayFormula =ARRAYFORMULA(IMPORTDATA(A2:A)) the result is #ERROR however when I comment out UrlFetchApp.fetch() it works so the error seems to be coming from that method. Does anyone know why?