Google Apps Scripts: Insert additional text into a text file - google-apps-script

I'm trying to develop a method for adding Google Tasks to my running task list named todo.txt and is housed in Google Drive. I've found a way to grab the tasks. Of course, the data will nee to be manipulated as well (e.g., concatenate date formats to conform to todo.txt rules) but I'll deal with that next.
The question I have is how to insert them to the top of the todo.txt without overwriting the existing text contained within it. I have read in other circumstances, that I may need to read the current text out, add the new info and then write the whole thing back as an overwrite. Is this necessary? And, if so, how do I do it?
Below is the code I've written thus far, substituting logger.log for the destination file since I don't know how to do it. I;m not a programmer, so polease forgive any ignorance here. Thanks.
function listTasks(taskListId) {
var taskListId = '12345'; //this is the ID of my Google Tasks (the source data)
var name2="Text Journals"; //this is the folder in which my todo.text resides
var name="todo.txt"; //this is the name of my todo file (the destination)
var dir = DriveApp.getFoldersByName(name2).next()
var tasks = Tasks.Tasks.list(taskListId);
if (tasks.items) {
for (var i = 0; i < tasks.items.length; i++) {
var task = tasks.items[i];
Logger.log('Task with title "%s" and dute: "%s" and notes "%s" and status "%s" was found.',
task.title, task.due, task.notes, task.status);
}
} else {
Logger.log('No tasks found.');
}
}

Related

Google Apps Script trigger - run whenever a new file is added to a folder

I want to execute a google apps script whenever a new file is added to a specific folder.
Currently I'm using a run-every-x-minutes clock trigger, but I only need to run the script whenever I add a file to a folder. Is there a way to do this?
The same as this question - which is now almost 3 years old. The comment below the question states that:
There's not a trigger for that, if that's what you're hoping. How are
things getting into the folder, and do you have any control over that?
– Jesse Scherer Apr 8 '18 at 3:02
I wonder if this comment is still valid, and if it is, then if there's a workaround.
Issue:
Unfortunately, the comment you read is still true. Here is a list of all the available triggers and a trigger for a new file added to a folder is not one of them.
Workaround / Explanation:
I can offer you a workaround which is usually used by developers when they built their add-ons. You can take advantage of the PropertiesService class. The logic is quite simple.
You will store key-value pairs scoped to the script:
In your case, the key will be the folder id, and the value will be the number of files under this folder.
You will setup a time-driven trigger to execute mainFunction for example every one minute.
The script will count the current number of files within the selected folder. The function responsible for that is countFiles.
The checkProperty function is responsible for checking if the current number of files under this folder matches the old number of files. If there is a match, meaning no files were added, then checkProperty returns false, otherwise return true and update the property for the current folder ID, so when the script runs after 1 minute, it will compare with the fresh value.
If checkProperty returns true, then execute the desired code.
Code snippet:
Set up a time-driven trigger for mainFunction. Whatever code you put inside the brackets of the if(runCode) statement will be executed if the number of files under the folderID has changed.
function mainFunction(){
const folderID = 'folderID'; //provide here the ID of the folder
const newCounter = countFiles(folderID);
const runCode = checkProperty(folderID, newCounter);
if(runCode){
// here execute your main code
//
console.log("I am executed!");
//
}
}
And here are the helper functions which need to be in the same project (you can put them in the same script or different scripts but in the same "script editor").
function countFiles(folderID) {
const theFolder = DriveApp.getFolderById(folderID);
const files = theFolder.getFiles();
let count = 0;
while (files.hasNext()) {
let file = files.next();
count++;
};
return count;
}
function checkProperty(folderID, newC){
const scriptProperties = PropertiesService.getScriptProperties();
const oldCounter = scriptProperties.getProperty(folderID);
const newCounter = newC.toString();
if(oldCounter){
if(oldCounter==newCounter){
return false;
}
else{
scriptProperties.setProperty(folderID, newCounter);
return true;
}
}
else{
scriptProperties.setProperty(folderID, newCounter);
return true;
}
}

Return Collection of Google Drive Files Shared With Specific User

I'm trying to get a collection of files where user (let's use billyTheUser#gmail.com) is an editor.
I know this can be accomplished almost instantly on the front-end of google drive by doing a search for to:billyTheUser#gmail.com in the drive search bar.
I presume this is something that can be done in Google App Scripts, but maybe I'm wrong. I figured DriveApp.searchFiles would work, but I'm having trouble structuring the proper string syntax. I've looked at the Google SDK Documentation and am guessing I am doing something wrong with the usage of the in matched to the user string search? Below is the approaches I've taken, however if there's a different method to accomplishing the collection of files by user, I'd be happy to change my approach.
var files = DriveApp.searchFiles(
//I would expect this to work, but this doesn't return values
'writers in "billyTheUser#gmail.com"');
//Tried these just experimenting. None return values
'writers in "to:billyTheUser#gmail.com"');
'writers in "to:billyTheUser#gmail.com"');
'to:billyTheUser#gmail.com');
// this is just a test to confirm that some string searches successfully work
'modifiedDate > "2013-02-28" and title contains "untitled"');
Try flipping the operands within the in clause to read as:
var files = DriveApp.searchFiles('"billyTheUser#gmail.com" in writers');
Thanks #theAddonDepot! To illustrate specifically how the accepted answer is useful, I used it to assist in building a spreadsheet to help control files shared with various users. The source code for the full procedure is at the bottom of this post. It can be used directly within this this google sheet if you copy it.
The final result works rather nicely for listing out files by rows and properties in columns (i.e. last modified, security, descriptions... etc.).
The ultimate purpose is to be able to update large number of files without impacting other users. (use case scenario for sudden need to immediately revoke security... layoffs, acquisition, divorce, etc).
//code for looking up files by security
//Posted on stackoverlow here: https://stackoverflow.com/questions/62940196/return-collection-of-google-drive-files-shared-with-specific-user
//sample google File here: https://docs.google.com/spreadsheets/d/1jSl_ZxRVAIh9ULQLy-2e1FdnQpT6207JjFoDq60kj6Q/edit?usp=sharing
const ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("FileList");
const clearRange = true;
//const clearRange = SpreadsheetApp.getActiveSpreadsheet().getRangeByName("ClearRange").getValue();
//if you have the named range setup.
function runReport() {
//var theEmail= SpreadsheetApp.getActiveSpreadsheet().getRangeByName("emailFromExcel").getValue();
//or
var theEmail = 'billyTheUser#gmail.com';
findFilesByUser(theEmail);
}
function findFilesByUser(theUserEmail) {
if(clearRange){
ss.getDataRange().offset(1,0).deleteCells(SpreadsheetApp.Dimension.ROWS)
}
var someFiles = DriveApp.searchFiles('"' + theUserEmail + '" in writers');
var aListOfFiles = []
while(someFiles.hasNext()){
var aFile = someFiles.next();
aListOfFiles.push([aFile.getId()
,aFile.getName()
,aFile.getDescription()
,aFile.getSharingAccess()
,aFile.getSharingPermission()
,listEmails(aFile.getEditors())
,listEmails(aFile.getViewers())
,aFile.getMimeType().replace('application/','').replace('vnd.google-apps.','')
,aFile.getDateCreated()
,aFile.getLastUpdated()
,aFile.getSize()
,aFile.getUrl()
,aFile.getDownloadUrl()
])
}
if(aListOfFiles.length==0){
aListOfFiles.push("no files for " + theUserEmail);
}
ss.getRange(ss.getDataRange().getLastRow()+1,1, aListOfFiles.length, aListOfFiles[0].length).setValues(aListOfFiles);
}
function listEmails(thePeople){
var aList = thePeople;
for (var i = 0; i < aList.length;i++){
aList[i] = aList[i].getEmail();
}
return aList.toString();
}

DriveApp.getRootFolder() is returning null to webapp

As part of a larger Google App Script webapp, I want to create a rudimentary file system with files/folders in the user's Google Drive. I'm doing this through a element where each would be a different folder (prefixed with a '*') or file.
I have setup the webapp HTML to include the element, but within this element I call a script that will populate the via a call to google.script.run.withSuccessHandler. It appears that this code runs as I'd expect, but the result of DriveApp.getRootFolder() is null, thereby making me unable to access the file structure.
// In the HTML file.
...
<head>
<script>
...
// Populate options in the file/folder list based on the provided folder.
function setFiles(folder)
{
alert(folder);
return;
/* // Get the select item.
var e = document.getElementById("file-select");
// First list all the folders at the top.
//#TODO Adding an asterick on folders to identify them for now, maybe have a different method later?
var folderI = folder.getFolders();
var i = 0;
while(folderI.hasNext())
{
var fldr = folderI.next();
e.innerHTML += "<option id='f_'" + i + "'>*" + fldr.getName() + "</option>";
i++;
}
// Now list all the files in the current directory.
i = 0;
var fileI = folder.getFiles();
while(fileI.hasNext())
{
var fle = fileI.next();
e.inner.HTML += "<option id='f_'" + i + "'>*" + fle.getName() + "</option>";
i++
}
*/
....
</script>
</head>
<body>
...
<div id="select-files">
<select id="file-select" size="10">
<script>
// Populate the initial file/folder list.
google.script.run.withSuccessHandler(setFiles).getRootFolder();
</script>
</select>
</div>
...
// In code.gs
/**
* Returns the root folder for the user.
* #return The root folder of the user.
*/
function getRootFolder()
{
return DriveApp.getRootFolder();
}
This is the code as I'm testing it now, hence my commenting out most of setFiles(). alert() results in 'null', but I'd expect it to be an 'Object [Object]' type that I could iterate through.
Interestingly, when I've added Logger.log() lines in the code.gs file, no log output is produced (I can't figure out why, because if I change the return value of getRootFolder() to a string, that string is displayed in the alert, so I know the code is entering that function correctly.
I'm wondering if this is a misunderstanding such that Google Drive (or maybe, generally, Google App Script specific objects) cannot be passed to an HTML file, though I couldn't find any clear documentation that this is the case.
As Cooper said in the comments, the Folder type is not legal to send to the client. If you look at what a Folder contains, it is purely functions, which are not allowed to be sent over.
All that client-side you commented out in setFiles cannot function in the user's browser. Even if you were able to pass the Folder code into the client, what would folder.getFolders() mean to the user's browser? It would start looking for the rest of the code from DriveApp, which doesn't exist in the browser, and still fail.
I'm wondering if this is a misunderstanding such that Google Drive (or maybe, generally, Google App Script specific objects) cannot be passed to an HTML file
What you get passed to the HTML file is documented here. Pay special attention to how google.script.run works.
No, you cannot pass the entire environment of your server-side code to the client (e.g. pass all of DriveApp and its dependences over to the client).
What you can do on both sides is construct your own version of Folder which exports the strings on the server side and reconstructs them on the client side. Note that arrays of strings are OK, so I would put things like the child, parent folder names and IDs in arrays. Just to be safe, I use JSON stringify/parse to strip functions out. This example works without the JSON part, but on more complicated objects it can be nice to clean them up.
client-side code
// just to log that it works
google.script.run.withSuccessHandler(response => {
response = JSON.parse(response);
console.log({response})
}).getFolder();
Code.gs
// client-code calls this to get folder info
function getFolder(id) {
return JSON.stringify(new Folder_(id ? DriveApp.getFolderById(id) : DriveApp.getRootFolder()));
}
// constructor for a `folder` suitable to send to the client
function Folder_(folder) {
this.id = folder.getId();
this.name = folder.getName();
this.foldersIds = [];
this.foldersNames = [];
this.parentsIds = [];
this.parentsNames = [];
this._extractFolders(folder, "folders");
this._extractFolders(folder, "parents");
}
// one function for both "getFolders" and "getParents"
Folder_.prototype._extractFolders = function(folder, type) {
var folders = folder["get" + type.replace(/^./, function(str){return str.toUpperCase()})]();
while (folders.hasNext()) {
var folder = folders.next();
this[type + "Ids"].push(folder.getId());
this[type + "Names"].push(folder.getName());
}
};

Google apps script to delete old files permanently and keep last 100 files

I created a folder in my root google Drive that contains video files (.avi). I need to write a google apps script to delete the old video files permanently when the total numbers of the files are more than 100 files? i.e deleting all video files except last (newer) 100 files.
The name for each file is related to the time that this file were created example: 2013-02-25__20-29-45-01.avi
2013-02-25__20-24-49-09.avi
2013-02-25__18-26-24-08.avi
......
So I think the script should first list these files alphabetical starting with the newer and ended with the old one, then keep first 100 files and delete all others permanently.
I know how to do that in bash script, but not in google drive which I think they use javascript (.gs).
As I said in the comments, the script you referred to was not very far from what you want... but I admit your situation is a bit more complex so let's say this will be another exception to sto politics ;-)
That said, I didn't test this code thoroughly so it will probably need some tuning. I left a couple of commented logs throughout the script to test intermediate results, don't hesitate to use them. Also, think about updating the mail adress and don't forget that setTrashed can be manually reversed ;-) (better so when trying new code)
EDIT : I took some time this morning to test the script, it had a couple of "approximations";-)
here is a "clean" version that works nicely
function DeleteMyOldAvi() {
var pageSize = 200;
var files = null;
var token = null;
var i = null;
var totalFiles = []
var toDelete = []
Logger.clear()
do {
var result = DocsList.getAllFilesForPaging(pageSize, token);
var files = result.getFiles()
var token = result.getToken();
for(n=0;n<files.length;++n){
if(files[n].getName().toLowerCase().match('.avi')=='.avi'){
totalFiles.push([files[n].getName(),files[n].getDateCreated().getTime(),files[n].getId()]);// store name, Date created in mSec, ID in a subarray
// Logger.log(files[n].getName()+' created on '+Utilities.formatDate(files[n].getDateCreated(), 'GMT','MMM-dd-yyyy'))
}
}
} while (files.length == pageSize);// continue until job is done
totalFiles.sort(function(x,y){ // sort array on milliseconds date created (numeric/descending)
var xp = x[1];
var yp = y[1];
return yp-xp ;
});
// Logger.log(totalFiles.length)
if(totalFiles.length>100){
for(nn=totalFiles.length-1;nn>=100;nn--){
toDelete.push(totalFiles[nn]) ;// store the files to delete
}
// Logger.log(toDelete)
for(n=0;n<toDelete.length;++n){
var file = toDelete[n]
DocsList.getFileById(file[2]).setTrashed(true);// move to trash each file that is in the toDelete array
Logger.log(file[0]+' was deleted');// log the file name to create mail message
}
MailApp.sendEmail('myMail#gmail.com', 'Script AUTODELETE report', Logger.getLog());// send yourself a mail
}else{
MailApp.sendEmail('myMail#gmail.com', 'Script AUTODELETE report', 'No file deleted');// send yourself a mail
}
}

Allowing others to add Google tasks

I am looking for a way for employees to send me an email or add information to a spreadsheet that will then add tasks to my task list. Ideally, the script would capture the task list, task, due date, and any notes.
I have already successfully implemented five scripts (five task lists) that allow my employees to add tasks to specific tasklists, following this script shown below. This works OK but does not have the capacity to add due dates or notes:
Automated email to task list API
I recently came across references to scripts that monitors task lists, and then posts them to a spread sheet, including task, due dates, notes, etc. It strikes me that a spreadsheet might be a better way to do this though it does not have the convenience of email:
Task list to spreadsheet API
I wonder if the REVERSE can be done. I envision a spreadsheet that I could give my employees access to, with two worksheets (NEW and PROCESSED) with columns:
TASKLIST TASK DUE DATE NOTES
and the script would run through this every hour or two. Anything in NEW would be processed and added to my task list, then moved to the end of PROCESSED.
Does anyone know of something like that out there? Alternatively, perhaps there are ways to change the email script so that it moves anything in the body of the email into the NOTES section of the task. I am a raw newbie at this BTW. Thanks.
you should replace
var newTask = Tasks.newTask().setTitle(title);
by
var newTask = Tasks.newTask().setTitle(title).setDue(date).setNotes(notes);
I'm also stuck in the way
I can from a spreadsheet :
- Create a new tasklist
- Create a new task in a dedicated tasklist (with due date and notes)
I can from the Gtasks :
- Check if the task is completed and mark it as completed in the spreadsheet
- Check if the task still exists in the spreadsheet and remove it if necessary
I'm still looking for a way to make a task completed in GTasks when it's closed in spreadsheet
All the functionality exists for you to accomplish this, but I don't know if there is a pre-built script out there that does what you want. You may want to look into use a Google Form that saves data to the spreadsheet, and then create a trigger for form submit that scoops up the data and creates a new task using it.
Is this [part] of what you're looking for?
https://developers.google.com/apps-script/articles/google_apis_reading_list
It syncs a Spreadsheet based task list with a your regular Task List, and if you mark the task done in gmail, it records that back in the spreadsheet.
// Fetch the list of URLs to keep synchronized
var articleUrls = SpreadsheetApp.getActiveSheet().getRange("A2:A");
for (var rowNum = 0; rowNum < articleUrls.getNumRows(); rowNum++) {
// Limit our range to a single cell containing a URL
var oneUrlCell = articleUrls.offset(rowNum, 0, 1, 1);
if (oneUrlCell.getComment() === "") {
// This is a new URL that needs to be shortened/inserted
var urlText = oneUrlCell.getValue();
if (urlText !== "") {
// Shorten the URL
Logger.log("Adding task for url: " + urlText);
var toShorten = UrlShortener.newUrl().setLongUrl(urlText);
var shortened = UrlShortener.Url.insert(toShorten);
// Insert the shortened URL into our reading list
var taskToInsert = Tasks.newTask().setTitle(shortened.getId());
taskToInsert.setNotes(urlText);
var newTask = Tasks.Tasks.insert(taskToInsert, readingListId);
// Save the new ID as our comment.
oneUrlCell.setComment(newTask.getId());
}
} else {
// This URL has already been inserted, update the status
var existingTask = Tasks.Tasks.get(readingListId, oneUrlCell.getComment());
if (existingTask.getStatus() === "completed") {
var absRowNum = oneUrlCell.getRow();
var completedCell = sheet.getRange(absRowNum, 2);
completedCell.setValue("Yes");
}
}
Should be part of the solution, no?
I'm looking to make something a bit bigger myself.