Google Sheets - Convert comma to "#" before generate .CSV - google-apps-script

I have the following script in a Google Sheet:
/**
* Create CSV file of Sheet2
* Modified script written by Tanaike
* https://stackoverflow.com/users/7108653/tanaike
*
* Additional Script by AdamD.PE
* version 13.11.2022.1
* https://support.google.com/docs/thread/188230855
*/
/** Date extraction added by Tyrone */
const date = new Date();
/** Extract today's date */
let day = date.getDate();
let month = date.getMonth() + 1;
let year = date.getFullYear();
if (day < 10) {
day = '0' + day;
}
if (month < 10) {
month = `0${month}`;
}
/** Show today's date */
let currentDate = `${day}-${month}-${year}`;
/** Date extraction added by Tyrone */
function sheetToCsvModelo0101() {
var filename = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getSheetName() + "-01" + " - " + currentDate; // CSV file name
filename = filename + '.csv';
var ssid = SpreadsheetApp.getActiveSpreadsheet().getId();
var folders = DriveApp.getFileById(ssid).getParents();
var folder;
if (folders.hasNext()) {
folder = folders.next();
var user = Session.getEffectiveUser().getEmail();
if (!(folder.getOwner().getEmail() == user || folder.getEditors().some(e => e.getEmail() == user))) {
throw new Error("This user has no write permission for the folder.");
}
} else {
throw new Error("This user has no write permission for the folder.");
}
var SelectedRange = "A2:AB3";
var csv = "";
var v = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange(SelectedRange).getValues();
v.forEach(function (e) {
csv += e.join(",") + "\n";
});
var newDoc = folder.createFile(filename, csv, MimeType.CSV);
console.log(newDoc.getId()); // You can see the file ID.
}
This script basically creates a .CSV file in the same folder where the worksheet is, using the range defined in var SelectedRange.
This script is applied to a button on the worksheet.
The question is: how do I make every comma typed in this spreadsheet be converted into another sign, like # before generating the .CSV file in the folder?
I would also like to know if instead of generating 1 file in the folder it is possible to generate 2 files, each with a name.

Issue:
The question is: how do I make every comma typed in this spreadsheet be converted into another sign, like # before generating the .CSV file in the folder?
After you get the sheet values via getValues, replace all instances of , in the resulting 2D array with #, using map and replaceAll.
I think this is a better approach than TextFinder since sheet values are not modified.
Code snippet:
From your original sample, just add the following line:
// ...stuff...
var v = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange(SelectedRange).getValues();
v = v.map(r => r.map(c => c.replaceAll(",", "#"))); // Add this line
v.forEach(function (e) {
csv += e.join(",") + "\n";
});
// ...stuff...

If you are doing this to avoid conflicts between the comma in the cells and the csv delimiter then try doing the csv like this:
function sheetToCsv() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName("Sheet0")
const params = { "method": "GET", "headers": { "Authorization": "Bearer " + ScriptApp.getOAuthToken() } };
const url = "https://docs.google.com/spreadsheets/d/" + ss.getId() + "/export?gid=" + sh.getSheetId() + "&format=csv";
const r = UrlFetchApp.fetch(url, params);
const csv = r.getContentText();
return csv;
}
And then put it back in a spreadsheet like this:
function csvToSheet(csv) {
const vs = Utilities.parseCsv(csv,',');
const osh = ss.getSheetByName("Sheet1");
osh.getRange(1,1,vs.length,vs[0].length).setValues(vs);
}

In the meantime I've found a solution that almost works the way I'd like.
I created 2 functions, one to convert , to # and another to convert # to , again, then after the .csv file creation is complete the script switches back from # to , .
/**
* Create CSV file of Sheet2
* Modified script written by Tanaike
* https://stackoverflow.com/users/7108653/tanaike
*
* Additional Script by AdamD.PE
* version 13.11.2022.1
* https://support.google.com/docs/thread/188230855
*/
var SelectedRange = "A2:AB3";
function searchAndReplace_ToHash() {
SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange(SelectedRange).createTextFinder(',').replaceAllWith('#');
}
function searchAndReplace_ToComma() {
SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange(SelectedRange).createTextFinder('#').replaceAllWith(',');
}
function sheetToCsv_02() {
var filename = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getSheetName() + "-01" + " - " + currentDate; // CSV file name
filename = filename + '.csv';
var ssid = SpreadsheetApp.getActiveSpreadsheet().getId();
searchAndReplace_ToHash()
// I modified below script.
var folders = DriveApp.getFileById(ssid).getParents();
var folder;
if (folders.hasNext()) {
folder = folders.next();
var user = Session.getEffectiveUser().getEmail();
if (!(folder.getOwner().getEmail() == user || folder.getEditors().some(e => e.getEmail() == user))) {
throw new Error("This user has no write permission for the folder.");
}
} else {
throw new Error("This user has no write permission for the folder.");
}
var csv = "";
var v = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange(SelectedRange).getValues();
v.forEach(function (e) {
csv += e.join(",") + "\n";
});
var newDoc = folder.createFile(filename, csv, MimeType.CSV);
console.log(newDoc.getId()); // You can see the file ID.
searchAndReplace_ToComma()
}
It solves the problem, but it would be perfect if this change was not visible in the spreadsheet.
Is it possible to make this substitution without displaying it in the spreadsheet?
As for your script suggestion, I would like to change as little as possible in this script I'm using, it works exactly the way I need it to work, except for the fact that the commas of words conflict with the column divisions.
Anyway, thank you very much for all your attention and patience!

Related

Script only works when uploading a single file

When an employee submits the google form, this script renames the file they have uploaded based off of information said employee fills out in the form. (At the moment it is pulling their Name, Job ID, and Address of the current job location, the file is always pictures of what they completed on the jobsite.)
However the script only pulls this information if a single file/picture is uploaded the form instead of all files.
What modification would allow this to rename all uploaded files in the form.
while (files.hasNext()) {
for (var i = 0; i < formResponses.length; i++) {
var formResponse = formResponses[i];
var itemResponses = formResponse.getItemResponses();
var itemResponseFname = itemResponses[0];
var itemResponseLname = itemResponses[10];
var itemResponseID = itemResponses[11];
var itemResponsePhoto = itemResponses[13];
var photoID = itemResponsePhoto.getResponse();
var newName = itemResponseFname.getResponse() + " " + itemResponseLname.getResponse() + " - " + itemResponseID.getResponse();
var url = baseString + photoID + endString;
var urlCheck = file.getUrl();
if ( url == urlCheck) {
var modName = newName + ".jpg";
file.setName(modName);
The easiest way of doing such thing is the following:
Install a submit trigger
Get the list of IDs of submitted files
Change its name to whatever you like
In practice this will look something like this:
function submit(e) {
const itemResponses = e.response.getItemResponses()
// Read values
const fname = itemResponses[0].getResponse()
const lname = itemResponses[10].getResponse()
const uid = itemResponses[11].getResponse()
const imageIds = itemResponses[13].getResponse()
// Iterate images
for (let imgId of imageIds){
const image = DriveApp.getFileById(imgId)
// Create new filename (preserves extension)
const filename = `${fname} ${lname} - ${uid}${getExtension(image)}`
// Set file name
image.setName(filename)
}
}
/**
* Returns the extension of a file
*
* For example: A file with name 'example.json' will return '.json'.
*
* #param file {DriveApp.File} File to extract the extension from
* #returns {string} The extension with the dot.
*/
function getExtension(file) {
// Gets the last dot and the characters that follow
const r = /(\.\w+)$/.exec(file.getName())
// If it has no extension return an empty string, otherwise return the captured group
return r === null ? '' : r[1]
}
Obviously you need to add all your other code that you seem to have and install the trigger if you haven't, already.

Can you create spreadsheet in gas & get ID & getActive?

Multiple versions of the spreadsheet with the subset of data exist on my drive and I have a way to get the most recent but I thought that if I could capture the ID when I created it I would not have to run that extra code. If I just create a spreadsheet the return is a 'spreadsheet' which I can .getActiveSheet().getRange(. . . ).setValues(. . .)
If I create and immediately getId then get use DriveApp.getFileById the return is a 'file'. It has a mime type of spreadsheet but cannot be used with .getActiveSheet() etc. Why? Is there a way to get the ID when creating a spreadsheet and still work with that new sheet?
function createSubset(fileName) {
//
// var mimeType = 'application/vnd.google-apps.spreadsheet';
baseSs = SpreadsheetApp.getActiveSpreadsheet(); // "StudentRosterForPractice"
baseSheet = baseSs.getSheetByName("Roster");
// there are formulas save below actual data but no in column B
var totDataRows = cntDataRows (baseSheet.getRange('B1:B').getValues()) - 1;
dataSubset = baseSheet.getRange(3, 2, totDataRows, 2).getValues();
// ---- this does NOT work ----
// tempSsId = SpreadsheetApp.create(fileName).getId();
// tempSs = DriveApp.getFileById(tempSsId); // returns file
// Logger.log("tempSs.getMimeType(): " + tempSs.getMimeType() );
//// tempSs.getMimeType(): application/vnd.google-apps.spreadsheet
// tempSs.getActiveSheet().getRange(1, 1, totDataRows, 2).setValues(dataSubset);
//// gets TypeError: tempSs.getActiveSheet is not a function
// return tempSsId;
// ---- this does work ----
tempSs = SpreadsheetApp.create(fileName); // return spreadhsheet
tempSs.getActiveSheet().getRange(1, 1, totDataRows, 2).setValues(dataSubset);
return null;
}
/**
* Number rows with actual data. There is a row of forumulas.
*/
function cntDataRows (colB) {
var count = 0 ;
colB.forEach(
function(item) {
if ( item != "" ) { // no formula in last name column
count = count + 1;
}
}
) ;
Logger.log("totDataRows: " + count);
return count
}
function testCreateSubset() {
var ssName = 'tempSs'; //subset name
tempSsId = createSubset(ssName);
}
Thank you Marios.
My subset spreadsheet only has one sheet so that was not a problem. I am surprised that I can getActiveSheet on the subset even though the script is bound to the base spreadsheet.
There is still a problem however. When I create my subset, return the ID and create the PDF I get these messages
totDataRows: 41
Nov 8, 2020, 8:54:51 AM Info spreadsheet ID: 1t16QOEqT2OP8vVmdTueT2hqQjTGdlMQLAc7XnJNWScM
Nov 8, 2020, 8:54:51 AM Info In makeBlobPdf spreadsheet ID: 1t16QOEqT2OP8vVmdTueT2hqQjTGdlMQLAc7XnJNWScM
Nov 8, 2020, 8:54:51 AM Info URL to be fetched: https://docs.google.com/spreadsheets/d/1t16QOEqT2OP8vVmdTueT2hqQjTGdlMQLAc7XnJNWScM/export?exportFormat=pdf&format=pdf&size=A4&portrait=false&fitw=true&sheetnames=false&printtitle=false&printnotes=false&pagenumbers=false&pagenum=CENTER&gridlines=false&fzr=true&top_margin=0.15&left_margin=0.15&right_margin=0.15&bottom_margin=0.15
but the PDF is empty. If I open the subset and get the ID, hardcode it in the script and create the PDF the messages are:
In makeBlobPdf spreadsheet ID: 1t16QOEqT2OP8vVmdTueT2hqQjTGdlMQLAc7XnJNWScM
Nov 8, 2020, 8:57:28 AM Info URL to be fetched: https://docs.google.com/spreadsheets/d/1t16QOEqT2OP8vVmdTueT2hqQjTGdlMQLAc7XnJNWScM/export?exportFormat=pdf&format=pdf&size=A4&portrait=false&fitw=true&sheetnames=false&printtitle=false&printnotes=false&pagenumbers=false&pagenum=CENTER&gridlines=false&fzr=true&top_margin=0.15&left_margin=0.15&right_margin=0.15&bottom_margin=0.15
The ID received in the make PDF code is the same and the URL generated is the same but the PDF where the ID was hardcoded has data and the PDF where it was passed from the create subset is empty. The create PDF code is unchanged.
Works:
var ssName = 'tempSs'; //subset name
// var spreadsheetId = createSubset(ssName);
// Logger.log("spreadsheet ID: " + spreadsheetId );
var spreadsheetId = "1t16QOEqT2OP8vVmdTueT2hqQjTGdlMQLAc7XnJNWScM";
var blobPdf = makeBlobPdf(spreadsheetId, ssName);
Returns empty PDF:
var ssName = 'tempSs'; //subset name
var spreadsheetId = createSubset(ssName);
Logger.log("spreadsheet ID: " + spreadsheetId );
// var spreadsheetId = "1t16QOEqT2OP8vVmdTueT2hqQjTGdlMQLAc7XnJNWScM";
var blobPdf = makeBlobPdf(spreadsheetId, ssName);
Corrected createSubset
function createSubset(fileName) {;
baseSs = SpreadsheetApp.getActiveSpreadsheet(); // "StudentRosterForPractice"
baseSheet = baseSs.getSheetByName("Sheet1");
// there are formulas saved below actual data but no in column B
var totDataRows = cntDataRows (baseSheet.getRange('B1:B').getValues()) - 1;
dataSubset = baseSheet.getRange(3, 2, totDataRows, 2).getValues();
tempSsId = SpreadsheetApp.create(fileName).getId();
tempSs = SpreadsheetApp.openById(tempSsId);
tempSs.getActiveSheet().getRange(1, 1, totDataRows, 2).setValues(dataSubset);
return tempSsId;
}
Code that calls createSubset and makeBlobPdf
function createAndMailPdf () {
var ssName = 'tempSs'; //subset name
var spreadsheetId = createSubset(ssName);
Logger.log("spreadsheet ID: " + spreadsheetId );
// var spreadsheetId = "1t16QOEqT2OP8vVmdTueT2hqQjTGdlMQLAc7XnJNWScM";
var blobPdf = makeBlobPdf(spreadsheetId, ssName);
if (MailApp.getRemainingDailyQuota() > 0) {
var emailAddress = "blah blah # blah";
var subject = "With useless PDF file";
var htmlBody = HtmlService.createHtmlOutputFromFile('letterBody').getContent();
var templ = HtmlService.createTemplateFromFile('letterBody');
var message = templ.evaluate().getContent();
MailApp.sendEmail(
{ to: emailAddress,
subject: subject,
htmlBody: message,
attachments: blobPdf}
);
} else {
Logger.log("Mail quota exceeded but the PDF has been saved");
}
}
The makeBlobPdf code:
function makeBlobPdf (spreadsheetId, ssName) {
Logger.log("In makeBlobPdf spreadsheet ID: " + spreadsheetId );
var marginStringValue = '0.15';
var margin = '_margin=' + marginStringValue;
var margins = '&top' + margin + '&left' + margin
+ '&right' + margin + '&bottom' + margin;
var url = 'https://docs.google.com/spreadsheets/d/'
+ spreadsheetId + '/export?'
+ 'exportFormat=pdf&format=pdf'
+ '&size=A4'
+ '&portrait=false'
+ '&fitw=true' // Fit to width
+ '&sheetnames=false'
+ '&printtitle=false'
+ '&printnotes=false'
+ '&pagenumbers=false'
+ '&pagenum=CENTER'
+ '&gridlines=false'
+ '&fzr=true' // Repeat frozen rows
+ margins;
var token = ScriptApp.getOAuthToken();
Logger.log("URL to be fetched: " + url );
var result = UrlFetchApp.fetch(url, {
headers: {
Authorization: 'Bearer ' + token
}
});
var attachName = ssName + '.pdf';
var pdfBlob = result.getBlob().setName(attachName);
return pdfBlob;
};
Solution:
The issue in your code is that there is no method getActiveSheet()
for a file. It makes sense since a file can be anything in your
google drive. It wouldn't make sense to have a spreadsheet method like getActiveSheet() being applied to an image or a folder for example. As a result tempSs.getActiveSheet() is invalid since tempSs is a type of file in the deleted part of your code.
Here is the proper way to do that.
If you want to create the file and get its ID directly. And then you can use openById to get the spreadsheet.
function myFunction() {
var fileName = 'testname';
var file = Drive.Files.insert({title: fileName, mimeType: MimeType.GOOGLE_SHEETS}); // file
var newFile = SpreadsheetApp.openById(file.id); // Spreadsheet file
newFile.getActiveSheet().getRange(1, 1, totDataRows, 2).setValues(dataSubset);
}
But keep in mind newFile.getActiveSheet() gives the first sheet in your file. You might want to use newFile.getSheetByName('Sheet1') instead.
To use this code please enable Drive API.
From the question
Can you create spreadsheet in gas & get ID & getActive?
Yes. Bear in mind that running getActiveSheet() for the created spreadsheet will return the firts sheet.
Regarding DriveApp.getFileById(id) it returns a File object not a Spreadsheet object.
The Class File hasn't a getActiveSheet() method. If you wan to use this method one opction is to use SpreadsheetApp.openById(id) or SpreadsheetApp.open(file)
Resources
https://developers.google.com/apps-script/reference/drive/drive-app
https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet-app
The main problem you are encountering is that your sheet is being created but is taking some time to set the values in the function createSubset(). Therefore, while the creation of the Spreadsheet is complete, the values of the sheet have not been set yet which is done in the background.
This makes that createSubset() returns the Spreadsheet ID to the other functions and the PDF file is immediately created (but the cell values have not been set yet). This leads to your current behaviour where you are sending an empty Spreadsheets PDF because the cell values had not been set yet.
To force the cells to update with the right values and avoid sending the PDF before the Spreadsheet is all ready you can use the function flush which applies all the pending changes to the Spreadsheet. Use it as follows:
function createSubset(fileName) {;
baseSs = SpreadsheetApp.getActiveSpreadsheet(); // "StudentRosterForPractice"
baseSheet = baseSs.getSheetByName("Sheet1");
// there are formulas saved below actual data but no in column B
var totDataRows = cntDataRows (baseSheet.getRange('B1:B').getValues()) - 1;
dataSubset = baseSheet.getRange(3, 2, totDataRows, 2).getValues();
tempSsId = SpreadsheetApp.create(fileName).getId();
tempSs = SpreadsheetApp.openById(tempSsId);
tempSs.getActiveSheet().getRange(1, 1, totDataRows, 2).setValues(dataSubset);
// FORCE ALL UPDATES IN THE SPREADSHEET BEFORE RETURNING THE SPREADHSEET ID
SpreadsheetApp.flush();
return tempSsId;
}

Automatically Delete Files from Google Drive Older than n days - "getFolderById" issue

I'm trying to delete files and folders older than 7 days in a specific Team Drive (not in the whole Google Drive).
To accomplish this I tried to merge what I read here:
Apps Script - Automatically Delete Files from Google Drive Older than 3 Days - Get List of Files
I don't have enough rep to add a comment there so that's why I'm opening a new thread. Here you can find my ripoff from user1588938:
function getOldFileIDs() {
var fileIDs = [];
// Old date is 30 days
var oldDate = new Date().getTime() - 3600*1000*24*30;
var cutOffDate = Utilities.formatDate(new Date(oldDate), "GMT", "yyyy-MM-dd");
// Get folderID using the URL on google drive
var folder = DriveApp.getFolderById('XXXXXXX');
var files = folder.searchFiles('modifiedDate < "' + cutOffDate + '"');
while (files.hasNext()) {
var file = files.next();
fileIDs.push(file.getId());
Logger.log('ID: ' + file.getId() + ', Name: ' + file.getName());
}
return fileIDs;
};
function deleteFiles() {
var fileIDs = getOldFileIDs();
fileIDs.forEach(function(fileID) {
DriveApp.getFileById(fileID).setTrashed(true);
});
};
I'm stuck with 'getFolderById' function because I suppose it doesn't apply to a Team Drive root but only works for folders inside of it.
Indeed, when I look at the logs I can see that the output for:
var folder = DriveApp.getFolderById('this-is-my-team-drive-id');
is a generic: [18-07-30 06:34:49:146 PDT] Team Drive and not the name of the Team Drive I chose.
I can't go any further with the script because of this.
Any hint on how to list every file and subfolder in a Team Drive using searchFiles?
This solution might apply but it works for a folder inside a Team Drive and not on the root of the Team Drive:
browse files in google team drive
Thanks!
If someone out there is trying to achieve the same result, this is how you do it.
function deleteOldFiles() {
var Folders = new Array(
'YOUR-TEAM-DRIVE-ID' //you can find this in the team drive url
);
var DaysRetentionNumber = 15; //how many days old your files and folders must be before getting deleted?
var RetentionPeriod = DaysRetentionNumber * 24 * 60 * 60 * 1000;
Logger.clear();
for each (var FolderID in Folders) {
folder = DriveApp.getFolderById(FolderID);
processFolder(folder);
}
function processFolder(folder){
Logger.log('Folder: ' + folder.getName());
var files = folder.getFiles();
while (files.hasNext()) {
var file = files.next();
Logger.log('File: ' + file.getName());
if (new Date() - file.getLastUpdated() > RetentionPeriod) {
//file.setTrashed(true); //uncomment this line to put them in the trash
//Drive.Files.remove(file.getId()); //uncomment this line to delete them immediately; CAREFUL!
Logger.log('File '+ file.getName() + ' trashed');
}
}
var subfolders = folder.getFolders();
while (subfolders.hasNext()) {
subfolder = subfolders.next();
processFolder(subfolder);
}
checkEmptyFolder(folder);
}
function checkEmptyFolder(folder){
if(!folder.getFiles().hasNext() && !folder.getFolders().hasNext()){
Logger.log('Empty folder: '+ folder.getName());
folder.setTrashed(true); // put them in the trash
}
}
if(Logger.getLog() != '')
MailApp.sendEmail('youremailaddresshere', 'Team Drive weekly cleanup report', Logger.getLog()); //get a log in your email so that you can see what will be deleted; try this before uncommenting the trash/delete lines!
}
#Daniele INeDiA's script is broken due to various reasons:
The script fails withReferenceError: "Drive" is not defined. (line 24, file "Code"), which refers to //uncomment this line to delete them immediately;
The script also fails because of Access denied: DriveApp. (line 39, file "Code"), in case the root folder is empty. It's because it doesn't differentiate between the root folder and its sub folders.
I've fixed #1 by simply leaving it commented out, but here's a fixed version for #2.
While at it, I've also added a feature to always delete (always_delete on/off). That way you can schedule it to run every night to make sure you start the day with an empty folder.
Also while at it, I've added an on/off switch to send a log by e-mail.
P.S.
I did see tehhowch's comment that "for each (... in ...) is deprecated", so feel free to add an alternative and I'll use it instead.
function deleteOldFiles() {
var sendlog = false, Folders = new Array(
'YOUR-TEAM-DRIVE-ID' //you can find this in the team drive url
);
var DaysRetentionNumber = 15; //how many days old your files and folders must be before getting deleted?
var RetentionPeriod = DaysRetentionNumber * 24 * 60 * 60 * 1000;
var always_delete = true;
Logger.clear();
for each (var FolderID in Folders) {
folder = DriveApp.getFolderById(FolderID);
processFolder(folder, FolderID);
}
function processFolder(folder, FolderID){
Logger.log('Folder: ' + folder.getName());
var files = folder.getFiles();
while (files.hasNext()) {
var file = files.next();
if (!always_delete)
Logger.log('File: ' + file.getName());
if (always_delete || new Date() - file.getLastUpdated() > RetentionPeriod) {
file.setTrashed(true); //uncomment this line to put them in the trash
// Don't uncomment the following because it breaks the script!
//Drive.Files.remove(file.getId()); //uncomment this line to delete them immediately; CAREFUL!
Logger.log('File '+ file.getName() + ' trashed');
}
}
var subfolders = folder.getFolders();
while (subfolders.hasNext()) {
subfolder = subfolders.next();
processFolder(subfolder);
}
if(Folders.indexOf(FolderID) == -1)
checkEmptyFolder(folder);
}
function checkEmptyFolder(folder){
if(!folder.getFiles().hasNext() && !folder.getFolders().hasNext()){
Logger.log('Empty folder: '+ folder.getName());
folder.setTrashed(true); // put them in the trash
}
}
if(sendlog && Logger.getLog() != '')
MailApp.sendEmail('youremailaddresshere', 'Team Drive cleanup report', Logger.getLog()); //get a log in your email so that you can see what will be deleted; try this before uncommenting the trash/delete lines!
}
In my case I needed variability of how long to keep files based on what top folder they are in. Some folders have thousands of generated imgs that need to be cleaned up often and other folders have reports that should be kept around for longer. There are a lot of nested folders as well. This script has to be triggered a lot to keep up. Any tips on optimization would be great!
I have not been able to find an example quite like this, so I hope it can be useful to someone searching as I had searched.
/**
* Clean up Google Drive folders sending all files
* last modified older than daysToKeep to the trash
*/
function cleanUpFolders() {
let folders = [
{ id: "folderId1", daysToKeep: 14 },
{ id: "folderId2", daysToKeep: 30 },
{ id: "folderId3", daysToKeep: 90 }
];
for (let f = 0; f < folders.length; f++) {
let folder = DriveApp.getFolderByIdAndResourceKey(folders[f].id, "");
let daysAgo = getDaysAgo(folders[f].daysToKeep);
cleanUpFolder(folder, daysAgo);
}
}
/**
* Cleans up a folder sending all files last modified more than filterDate to the trash
*
* #param {DriveApp.Folder} folder to proccess
* #param {String} filterDate date to search files older than in format YYYY-MM-DD
*/
function cleanUpFolder(folder, filterDate) {
console.log("searching in " + folder.getName());
let trashCount = 0;
// Send files last modified before filterDate to the trash
let query = 'modifiedDate < "' + filterDate + '"';
let files = folder.searchFiles(query);
while (files.hasNext()) {
let file = files.next();
file.setTrashed(true);
// console.log(file.getName() + " modified " + formatDate(file.getLastUpdated()));
trashCount++;
}
// Recursively clean up all sub folders
let folders = folder.getFolders();
while (folders.hasNext()) {
trashCount += cleanUpFolder(folders.next(), filterDate);
}
// Check if the folder is now empty and trash it as well
if (isEmptyFolder(folder)) {
folder.setTrashed(true);
console.log(folder.getName() + " is empty and has been trashed");
}
console.log(trashCount + " files found last modified before " + filterDate + " in " + folder.getName() + " have been trashed");
return trashCount;
}
/**
* Check if a folder has any files or folders
*
* #param {DriveApp.Folder} folder to check
*/
function isEmptyFolder(folder) {
return !folder.getFiles().hasNext() && !folder.getFolders().hasNext();
}
/**
* get date string "YYYY-MM-DD" days ago from now
*
* #param {number} days ago from now
*/
function getDaysAgo(days) {
let date = new Date(Date.now());
let nowString = formatDate(date);
date.setDate(date.getDate() - days);
let daysAgoString = formatDate(date);
console.log("now: " + nowString + ", " + days + " days ago: " + daysAgoString);
return daysAgoString;
}
/**
* Format date as "YYYY-MM-DD"
*
* #param {Date} date to format
*/
function formatDate(date) {
return date.getFullYear() + '-' +
(date.getMonth()+1).toString().padStart(2,'0') + '-' +
date.getDate().toString().padStart(2, '0');
}

How to insert in a cell the name and link of a folder that has just been created in Google Drive?

I'd like to create some folders in Google Drive through a script in Google Sheets and then to get the URLs of the new folders to put them in a cell.
I successfully created the folders based on the column "Employee ID" and replaced the values of the column "Employee ID" with the folder hyperlinks.
So I get the 3 folders created in Google Drive: 1,2 and 3, respectively. The problem is that my code repeats the same name and URL in every row in the Google Sheets setting the name and URL of the last folder that was created (the folder 3 in this case).
I would appreciate it if you could give me some help with this. This is my code:
function onEdit(e) {
if ([1, 2,].indexOf(e.range.columnStart) != 1) return;
createEmployeeFolder();
}
function createEmployeeFolder() {
var parent = DriveApp.getFolderById("1H0i69rE9WO0IAoxhnrFY2YKT_tD50fuX")
SpreadsheetApp.getActive().getSheetByName('Database').getRange('B3:B').getValues()
.forEach(function (r) {
if(r[0]) checkIfFolderExistElseCreate(parent, r[0]);
})
}
function checkIfFolderExistElseCreate(parent, folderName) {
var folder;
var idfolder;
var link;
try {
folder = parent.getFoldersByName(folderName).next();
} catch (e) {
folder = parent.createFolder(folderName);
idfolder = folder.getId();
link = folder.getUrl();
formula = '=hyperlink("' + link + '",' + folder + ')';
SpreadsheetApp.getActive().getSheetByName('Database').getRange('B3:B').setFormula(formula);
}
}
Ok I changed my code for this. I can create the folders but the problem is with the SetFormula I can't make it work in order to read every row to replace the ID with the URL. It just take the last ID and then it repeats the same ID in all the rows from the range. Please some help! :P
function createEmployeeFolder() {
var parent = DriveApp.getFolderById("1H0i69rE9WO0IAoxhnrFY2YKT_tD50fuX")
SpreadsheetApp.getActive().getSheetByName('Database').getRange('B3:B').getValues()
.forEach(function (r) {
if(r[0]) createFolder(r[0]);
})
}
function createFolder(folderName) {
var parent = DriveApp.getFolderById("1H0i69rE9WO0IAoxhnrFY2YKT_tD50fuX");
var projectFolder;
if (parent.getFoldersByName(folderName).hasNext()) {
// folder already exists
Folder = parent.getFoldersByName(folderName).next();
} else {
Folder = parent.createFolder(folderName);
}
var id = Folder.getId();
var link = Folder.getUrl();
var formula = '=hyperlink("' + link + '",' + Folder + ')';
SpreadsheetApp.getActive().getSheetByName('Database').getRange('B3:B').setFormula(formula);
return formula;
}
I have a function that does this for me. It takes a userName and returns a formula for a link to the folder that either existed or was created. This link can be put into a cell using setFormula():
function createUserFolder(userName) {
var parent = DriveApp.getFolderById("1H0i69rE9WO0IAoxhnrFY2YKT_tD50fuX");
if (parent.getFoldersByName(userName).hasNext()) {
// folder already exists
userFolder = parent.getFoldersByName(userName).next();
} else {
userFolder = parent.createFolder(userName);
}
var id = userFolder.getId();
var formula = '=HYPERLINK("https://drive.google.com/open?id=' + id + '","Files")'
return formula;
}
Please note however that you should rethink your code quite dramatically. onEdit is called for every single change in the spreadsheet which is very wasteful. Iterating through every single cell in column B is very wasteful. Using the catch clause in a try to do your critical code is a bad idea.
I would add a dialog or other ui to the sheet for triggering the creation of the folder rather than monitoring the sheet continually.
Solution:
The following script creates a folder in drive and a link with the name of the folder within a cell automatically.
function createAndHyperlink() {
var ss, sh, parent, parent2, r, v, thisCell, folder
ss = SpreadsheetApp.getActive()
sh = ss.getSheetByName('INSERTTHENAMEOFYOURSHEETHERE')
parent = DriveApp.getFileById(ss.getId()).getParents().next();
parent2 = DriveApp.getFolderById("INSERT-YOURGOOGLEDRIVEFOLDERIDEHERE")
r = sh.getRange('B3:B')
v = r.getValues()
for (var i = 0, l = v.length; i < l; i++) {
thisCell = sh.getRange(i + 3, 2)
if (v[i][0] && !thisCell.getFormula()) {
folder = parent2.createFolder(v[i][0]);
thisCell.setFormula('=HYPERLINK("' + folder.getUrl() + '"; "' + v[i][0] + '")');
}
}
}
Your issue stems from incomplete refactoring - you split the task of getting the folder id into a separate method, but also split the task of setting the hyperlink into that Range-unaware code.
The solution to repeating the last value across all other rows is to fix your refactoring - setting the formulas should be done where the range to write to is known.
function createEmployeeFolder() {
const parent = DriveApp.getFolderById("some id");
const sheet = SpreadsheetApp.getActive().getSheetByName('Database');
const range = sheet.getRange(3, 2, sheet.getLastRow() - 2);
range.getValues().forEach(function (row, index) {
// Consider checking if this r & c has a formula in the equal size array from getFormulas()
if(row[0]) {
var newLink = getLinkForFolderName_(parent, row[0]);
// Use the current array index to write this formula in only the correct cell.
range.offset(index, 0, 1).setFormula(
"=hyperlink(\"" + newLink + "\", \"" + row[0] + "\")";
);
}
});
}
function getLinkForFolderName_(root, name) {
var folder;
const search = root.getFoldersByName(name);
if (search.hasNext()) {
folder = search.next();
if (search.hasNext())
console.warn("Multiple folders named '" + name + "' in root folder '" + root.getName() + "'");
}
else
folder = root.createFolder(name);
return folder.getUrl();
}
You should do away with the onEdit simple trigger binding since simple triggers cannot create folders (you need authorization for that) - just change the name and install the on edit trigger for the new name. Another option is to use a menu option that calls this function. However, one (possible) benefit of using an installed trigger is that all folders will be owned by the same account.

DocumentApp.openById() fails with “Service unavailable”

I am trying to read the contents of a spreadsheet which contains some folderId, fileName and targetFile and then based on the data entered in the spreadsheet.
I am finding the latest fileId in the drive for the same fileName as multiple files with the same name are getting added into the folder daily (this is done by the function mostRecentFiIeInFolder) and then I am trying to copy the contents of the latest file with ID dociIdSource into a different file with ID docIdTarget (which is done by the function docCopy).
But when I tried Implementing this using DocumentApp, I am getting a weird error which says
Service unavailable: Docs
for the code var baseDoc = DocumentApp.openById(docID);.
May I know where I am going wrong?
// Test function to call applyDocCopytoList.
function test(){
applyDocCopytoList();
}
// Read the values from the spreadsheet.
function applyDocCopytoList(){
var originalSpreadsheet = SpreadsheetApp.openById('sheet Id goes here').getSheetByName("sheet name goes here");
var getRange = originalSpreadsheet.getDataRange();
var data = originalSpreadsheet.getDataRange().getValues();
for (var i = 1; i < data.length; i++) {
var folderId = data[i][1];
var fileName = data[i][2];
var targetFile = data[i][3];
Logger.log('****************Record No: ' + i);
Logger.log('folderId: ' + data[i][1]);
Logger.log('fileName: ' + data[i][2]);
Logger.log('targetFile: ' + data[i][3]);
var latestFileId = mostRecentFiIeInFolder(folderId, fileName);
if(latestFileId!= undefined){
docCopy(latestFileId, targetFile);
}
}
}
// Log the id of the latest file with a particular name in the folder.
function mostRecentFiIeInFolder(folderId, fileName) {
var folder = DriveApp.getFolderById(folderId);
Logger.log(folder);
var files = DriveApp.getFilesByName(fileName);
Logger.log(fileName);
var result = [];
// Checks whether the given file is in the folder or not
if(!files.hasNext()){
Logger.log('No such file in the folder with the given name');
}
else{
while (files.hasNext()) {
var file = files.next();
result.push([file.getDateCreated(), file.getId()]);
}
Logger.log('************All the file ids with the same file name and their dates created************');
Logger.log(result);
result.sort(function (x, y){
var xp = x[0];// get first element in inner array
var yp = y[0];
return xp == yp ? 0 : xp > yp ? -1 : 1;// choose the sort order, here its in descending order of created date
});
var id = result[0][1];
Logger.log(id);
return id;
}
}
// Copy the contents of the latest file in the target file.
function docCopy(dociIdSource, docIdTarget){
Logger.log('The file with id: ' + dociIdSource + ' will be copied to the target id: ' + docIdTarget);
var docID = docIdTarget;
var baseDoc = DocumentApp.openById(docID); //Service unavailable: Docs error is thrown for this line of code
var body = baseDoc.getBody();
var otherBody = DocumentApp.openById(dociIdSource).getBody();
var totalElements = otherBody.getNumChildren();
for( var j = 0; j < totalElements; ++j ) {
var element = otherBody.getChild(j).copy();
var type = element.getType();
if( type == DocumentApp.ElementType.PARAGRAPH )
body.appendParagraph(element);
else if( type == DocumentApp.ElementType.TABLE )
body.appendTable(element);
else if( type == DocumentApp.ElementType.LIST_ITEM )
body.appendListItem(element);
else if( type == DocumentApp.ElementType.INLINE_IMAGE )
body.appendImage(element);
else if( type == DocumentApp.ElementType.TEXT )
body.setText(element);
else
throw new Error("According to the doc this type couldn't appear in the body: " + type);
}
}
Note that your mostRecentFiIeInFolder function never actually uses the folder, and doesn't ever check that the files are of the correct type - i.e., are actually Google Docs files. Thus if your searched name should have found nothing (i.e. there is no recent file with that name in your target folder), but you had some alternate file elsewhere in your Drive, one that is not a Google Docs file, your script will find it and treat it as something it is not.
The solution is to restrict your search to your desired folder, and again by actual Google Docs mimetype:
function testIt() {
var folderIds = [
"", // Search all of Drive
"123428349djf8234", // Search that specific folder
];
// Log (in Stackdriver) the file id of the most recently created Google Docs file with the name "some name" in the various folders:
folderIds.forEach(function (folderId) {
console.log(getMostRecentFileIdWithName("some name", folderId, MimeType.GOOGLE_DOCS));
});
}
function getMostRecentFileIdWithName(fileName, folderId = "", mimeType = "") {
// If a folder was given, restrict the search to that folder. Otherwise, search with
// DriveApp. (Throws an error if the folderId is invalid.)
var parent = folderId ? DriveApp.getFolderById(folderId) : DriveApp;
// I assume your fileName variable does not contain any unescaped single-quotes.
var params = "name='" + fileName + "'";
// If a mimetype was given, search by that type only, otherwise search any type.
if (mimeType)
params += " and mimeType='" + mimeType + "'";
var matches = parent.searchFiles(params),
results = [];
// Collect and report results.
while (matches.hasNext())
results.push(matches.next());
if (!results.length)
throw new Error("Bad search query \"" + params + "\" for folder id = '" + folderId + "'.");
// Sort descending by the creation date (a native Date object).
// (a - b sorts ascending by first column).
results.sort(function (a, b) { return b.getDateCreated() - a.getDateCreated(); });
return results[0].getId();
}
You can read more about the acceptable search parameters in the Drive REST API documentation, and more about the Apps Script native implementation in the DriveApp documentation.