I have hundreds of reports in my google drive. Each report is its own file (Spreadsheet, with one sheet).
I need a total of cell B10 from all those spreadsheets. It would be great if there was a function that took two parameters:
the name of the directory containing the Spreadsheet files
the specific cell you want totaled.
I tried to do script
function Suma(cell)
{
var files = DocsList.getFilesByType('spreadsheet', 0, 100);
for (var i = 0; i < files.length; i++)
{
var sheets = SpreadsheetApp.open(files[i]).getSheets();
var sum = 0;
for (var i = 0; i < sheets.length ; i++ )
{
var sheet = sheets[i];
var val = sheet.getRange(cell).getValue();
if (typeof(val) == 'number')
{
sum += val;
}
}
}
return sum;
}
need some help of course :)
THX
What you are doing is ok however u might want to list files on a folder and not the entire drive..
Also will fail if you have too many ss and script will run out of time. To solve that is possible but requires more complex code. Better do it on client side with jsapi or appengine.
This is going to be SLOW, and has a risk of timing out, due to the time it takes to open each spreadsheet.
This version of your function will take a folder name and a target cell, and produce a sum of the numeric values at that location from all spreadsheets in the folder.
The DriveApp relies on iterators rather than arrays for collections of folders and files, so you've got two examples of using .hasNext() and .next() for accessing objects in collections.
function Suma(folderName, cell)
{
var folders = DriveApp.getFolders();
var folderFound = false;
var folder;
while (folders.hasNext() && !folderFound) {
folder = folders.next();
folderFound = folder.getName() == folderName;
}
if (!folderFound) throw "No folder named " + folderName; // Error
var sheets = folder.getFilesByType(MimeType.GOOGLE_SHEETS);
var sum = 0;
while (sheets.hasNext()) {
var sheet = SpreadsheetApp.openById(sheets.next().getId());
var val = sheet.getActiveSheet().getRange(cell).getValue();
if (typeof(val) == 'number') {
sum += val;
}
}
return sum;
}
Related
I receive a new CSV file every hour in my Google Drive.
I need my spreadsheet updated with the data in the latest CSV file after it has been received in the Google Drive folder.
The files coming into the folder has a unique name for each new one according to date and time.
For example: FileName_date_24hourtime.csv
FileName_20190524_1800.csv then FileName_20190524_1900.csv etc.
Firstly I'm not sure what the best approach is:
simply with a formula (probably not possible with not knowing the exact filename?) like =IMPORTDATA
a google script to find latest .csv file and automatically import as soon as file was added to Google Drive folder
Any assistance will be great!
The .csv file:
.csv file contains 28 rows and data should be split by ;
.csv file looks like this:
NAME;-63.06;-58.08;50.62;-66.67;-80.00
NAME;-61.82;-56.83;-50.55;-77.78;-70.00
NAME;-57.77;-50.21;52.88;-77.78;-70.00
NAME1;-57.69;-61.48;-55.59;-55.56;-60.00
NAME2;-61.62;-53.79;50.34;-66.67;-70.00
NAME3;-54.62;-54.57;-52.22;55.56;-60.00
... with total of 28 rows
Data should go to "Import_Stats" sheet.
The best approach here would be a script with a trigger that runs a function that performs data import to a spreadsheet.
Create a time-based trigger with 1-hour offset:
function trigger() {
var trg = ScriptApp.newTrigger('checkFiles');
trg.timeBased().everyHours(1).create();
}
Create function that checks files in a folder (e.g. "checkFiles").
function checkFiles(alreadyWaited) {
//get spreadsheet and sheet;
var id = 'yourSpreadsheetId';
var ss = SpreadsheetApp.openById(id);
var sh = ss.getSheetByName('Import_Stats');
var folderId = 'yourIdHere'; //folder by id is the simplest way;
//get folder and files in it;
var folder = DriveApp.getFolderById(folderId);
var files = folder.getFilesByType('text/csv');
var filesImport = folder.getFilesByType('text/csv'); //fetch files again;
//try to fetch number of files;
var scriptProps = PropertiesService.getScriptProperties();
var numFiles = scriptProps.getProperty('numFiles');
//count current number of files;
var numCurr = 0;
while(files.hasNext()) {
var f = files.next();
numCurr++;
}
//if this is the first time, save current number;
if(numFiles===null) {
scriptProps.setProperty('numFiles',numCurr);
}else {
numFiles = parseInt(numFiles);
}
if(numFiles===null||numFiles===(numCurr-1)) {
//get today and reset everything except hours;
var today = new Date();
today.setMinutes(0);
today.setSeconds(0);
today.setMilliseconds(0);
//iterate over files;
while(files.hasNext()) {
var file = files.next();
//get file creation date and reset;
var created = file.getDateCreated();
created.setMinutes(0);
created.setSeconds(0);
created.setMilliseconds(0);
//calculate offset, equals 0 for each file created this hour;
var offset = today.valueOf()-created.valueOf();
if(offset===0) {
//perform data import here;
var data = file.getBlob().getDataAsString();
//ignore empty files;
if(data!=='') {
//split data in rows;
var arr = data.split('\r\n');
//resplit array if only one row;
if(arr.length===1) {
arr = data.split('\n');
}
//append rows with data to sheet;
arr.forEach(function(el){
el = el.split(';');
sh.appendRow(el);
});
}
}
}
}else {
//if never waited, set minute to wait, else add minute;
if(!alreadyWaited) {
alreadyWaited = 60000;
}else {
alreadyWaited += alreadyWaited;
}
//if waited for 10 minutes -> end recursion;
if(alreadyWaited===600000) {
Logger.log('Waited 10 minutes but recieved no files!');
return;
}
//wait a minute and recheck;
Utilities.sleep(60000);
return checkFiles(alreadyWaited);
}
}
And this is what should happen:
Because of Drive API Quotas, Services Quotas and limit of script execution time 6 min it's often critical to split Google Drive files manipulations on chunks.
We can use PropertiesService to store continuationToken for FolderIterator or FileIterator.
This way we can stop our script and on next run continue from the place we stop.
Working example (linear iterator)
// Logs the name of every file in the User's Drive
// this is useful as the script may take more that 5 minutes (max execution time)
var userProperties = PropertiesService.getUserProperties();
var continuationToken = userProperties.getProperty('CONTINUATION_TOKEN');
var start = new Date();
var end = new Date();
var maxTime = 1000*60*4.5; // Max safe time, 4.5 mins
if (continuationToken == null) {
// firt time execution, get all files from Drive
var files = DriveApp.getFiles();
} else {
// not the first time, pick up where we left off
var files = DriveApp.continueFileIterator(continuationToken);
}
while (files.hasNext() && end.getTime() - start.getTime() <= maxTime) {
var file = files.next();
Logger.log(file.getName());
end = new Date();
}
// Save your place by setting the token in your user properties
if(files.hasNext()){
var continuationToken = files.getContinuationToken();
userProperties.setProperty('CONTINUATION_TOKEN', continuationToken);
} else {
// Delete the token
PropertiesService.getUserProperties().deleteProperty('CONTINUATION_TOKEN');
}
Problem (recursive iterator)
For retrieve tree-like structure of folder and get it's files we have to use recursive function. Somethiong like this:
doFolders(DriveApp.getFolderById('root folder id'));
// recursive iteration
function doFolders(parentFolder) {
var childFolders = parentFolder.getFolders();
while(childFolders.hasNext()) {
var child = childFolders.next();
// do something with folder
// go subfolders
doFolders(child);
}
}
However, in this case I have no idea how to use continuationToken.
Question
How to use ContinuationToken with recursive folder iterator, when we need to go throw all folder structure?
Assumption
Is it make sense to construct many tokens with name based on the id of each parent folder?
If you're trying to recursively iterate on a folder and want to use continuation tokens (as is probably required for large folders), you'll need a data structure that can store multiple sets of continuation tokens. Both for files and folders, but also for each folder in the current hierarchy.
The simplest data structure would be an array of objects.
Here is a solution that gives you the template for creating a function that can recursively process files and store continuation tokens so it can resume if it times out.
Simply modify MAX_RUNNING_TIME_MS to your desired value (now it's set to 1 minute).
You don't want to set it more than ~4.9 minutes as the script could timeout before then and not store its current state.
Update the processFile method to do whatever you want on files.
Finally, call processRootFolder() and pass it a Folder. It'll be smart enough to know how to resume processing the folder.
Sure there is room for improvement (e.g. it simply checks the folder name to see if it's a resume vs. a restart) but this will most likely be sufficient for 95% of people that need to iterate recursively on a folder with continuation tokens.
function processRootFolder(rootFolder) {
var MAX_RUNNING_TIME_MS = 1 * 60 * 1000;
var RECURSIVE_ITERATOR_KEY = "RECURSIVE_ITERATOR_KEY";
var startTime = (new Date()).getTime();
// [{folderName: String, fileIteratorContinuationToken: String?, folderIteratorContinuationToken: String}]
var recursiveIterator = JSON.parse(PropertiesService.getDocumentProperties().getProperty(RECURSIVE_ITERATOR_KEY));
if (recursiveIterator !== null) {
// verify that it's actually for the same folder
if (rootFolder.getName() !== recursiveIterator[0].folderName) {
console.warn("Looks like this is a new folder. Clearing out the old iterator.");
recursiveIterator = null;
} else {
console.info("Resuming session.");
}
}
if (recursiveIterator === null) {
console.info("Starting new session.");
recursiveIterator = [];
recursiveIterator.push(makeIterationFromFolder(rootFolder));
}
while (recursiveIterator.length > 0) {
recursiveIterator = nextIteration(recursiveIterator, startTime);
var currTime = (new Date()).getTime();
var elapsedTimeInMS = currTime - startTime;
var timeLimitExceeded = elapsedTimeInMS >= MAX_RUNNING_TIME_MS;
if (timeLimitExceeded) {
PropertiesService.getDocumentProperties().setProperty(RECURSIVE_ITERATOR_KEY, JSON.stringify(recursiveIterator));
console.info("Stopping loop after '%d' milliseconds. Please continue running.", elapsedTimeInMS);
return;
}
}
console.info("Done running");
PropertiesService.getDocumentProperties().deleteProperty(RECURSIVE_ITERATOR_KEY);
}
// process the next file or folder
function nextIteration(recursiveIterator) {
var currentIteration = recursiveIterator[recursiveIterator.length-1];
if (currentIteration.fileIteratorContinuationToken !== null) {
var fileIterator = DriveApp.continueFileIterator(currentIteration.fileIteratorContinuationToken);
if (fileIterator.hasNext()) {
// process the next file
var path = recursiveIterator.map(function(iteration) { return iteration.folderName; }).join("/");
processFile(fileIterator.next(), path);
currentIteration.fileIteratorContinuationToken = fileIterator.getContinuationToken();
recursiveIterator[recursiveIterator.length-1] = currentIteration;
return recursiveIterator;
} else {
// done processing files
currentIteration.fileIteratorContinuationToken = null;
recursiveIterator[recursiveIterator.length-1] = currentIteration;
return recursiveIterator;
}
}
if (currentIteration.folderIteratorContinuationToken !== null) {
var folderIterator = DriveApp.continueFolderIterator(currentIteration.folderIteratorContinuationToken);
if (folderIterator.hasNext()) {
// process the next folder
var folder = folderIterator.next();
recursiveIterator[recursiveIterator.length-1].folderIteratorContinuationToken = folderIterator.getContinuationToken();
recursiveIterator.push(makeIterationFromFolder(folder));
return recursiveIterator;
} else {
// done processing subfolders
recursiveIterator.pop();
return recursiveIterator;
}
}
throw "should never get here";
}
function makeIterationFromFolder(folder) {
return {
folderName: folder.getName(),
fileIteratorContinuationToken: folder.getFiles().getContinuationToken(),
folderIteratorContinuationToken: folder.getFolders().getContinuationToken()
};
}
function processFile(file, path) {
console.log(path + "/" + file.getName());
}
I am trying to use the Google Apps Script ScriptApp getUserTriggers(spreadsheet) method to create a report that displays all of my user-installed triggers across all of my projects.
This code loops through all of the files in my Google Drive and makes an array of file IDs for all of the files that are Google Sheets files, and then attempts to get the triggers for each file:
var spreadsheetIds = [];
// get all spreadsheets in my google drive
var files = DriveApp.getFiles();
while (files.hasNext()) {
var file = files.next();
var type = file.getMimeType();
if (type == 'application/vnd.google-apps.spreadsheet') spreadsheetIds.push(file.getId());
}
var data = [];
try {
for (var i = 0; i < spreadsheetIds.length; i++) {
var ssOpen = SpreadsheetApp.openById(spreadsheetIds[i]);
Utilities.sleep(100);
var triggers = ScriptApp.getUserTriggers(ssOpen);
var spreadsheetName = ssOpen.getName();
Logger.log(spreadsheetName + ' triggers: ' + triggers.length);
for (var j = 0; j < triggers.length; j++) {
Logger.log(spreadsheetName + ' trigger ID: ' + triggers[j].getUniqueId());
data.push([spreadsheetName, triggers[j].getEventType(), triggers[j].getHandlerFunction(), triggers[j].getTriggerSource(), triggers[j].getTriggerSourceId(), triggers[j].getUniqueId()]);
}
}
} catch(e) {
Logger.log(e);
}
Many of my projects have user-installed triggers, but this just returns 0 for all of them (empty arrays are returned from ScriptApp.getUserTriggers()).
I am 100% sure that I am logged in to the same account when running this code as when I've set the triggers.
But just to be sure, when I add a test trigger to this project and run this code it correctly returns the number of triggers (which is 1):
function getCurrentProjectTriggersCount() {
Logger.log('Current project has ' + ScriptApp.getProjectTriggers().length + ' triggers.');
}
This has been previously reported as Issue 4562 - getProjectTriggers() always returns the triggers for the script it's contained in.
The usual advice is to visit and star the issue to increase its priority, and receive updates.
*,
Specs:
import data from a target sheet (A) to another (B) in a different Google Spreadsheet;
data on B sheet need to be filtered/sorted by user without affecting A sheet
when A data change, B data should update too (live or at least on refresh/push a button)
(optional) import B sheet notes into A sheet
Structure of A sheet (and then B sheet which is a mirror basically) is a list of items where every item has a column "ID".
Originally I tried IMPORTRANGE which works great with live updates, but unfortunately on B sheet user cannot use native filters to sort/filter data.
I wrote this custom function:
function importSingleItemData(idItem) {
//vars for debugging
//var idItem = 1;
// Id of spreadsheet where data are contained
var inKey = "xxxxx";
// Actual code
var outData;
var idItemColumn;
var ss = SpreadsheetApp.openById(inKey); // target sheet
// 1. Import idItemColumn
if (ss) {
idItemColumn = ss.getRange("sheet1!A1:A500").getValues();
// 2. find id_property row
for (var i = 0; i < idItemColumn.length; i++){
if(idItemColumn[i][0] == idItem){
var idFound = idItemColumn[i][0];
// 3. import property availability range
var row = i+1;
var RangeString = "sheet1!B"+row + ":AM"+row;
var range = ss.getRange(RangeString);
// copy formatting
// range.copyFormatToRange(range.getGridId(), 3, 4,5,7); !not working
outData = range.getValues();
break;
}
}
return outData;
}
}
Where I try to locate the Id of the item and import the interested data of that row. Then I apply it on B sheet using =importSingleItemData(A1) where A1 contains the id of item =1; A2 = 2, etc like
ID ItemData
1 =importSingleItemData(A1)
2 =importSingleItemData(A2)
...
This works great, the problem is that it does not update data on B sheet when A changes. I read a few posts on stackoverflow about this caching beaviour and tried a few things with no luck (like adding time to import, which is no longer supported), also tried setValue method which does not work with custom function.
I was now thing some combination of VLookup/Hlookup with IMPORTRANGE, not sure whether this will work.
Any tips how to sort this out guys?
Thanks in advance!!
If your working with alot of data between two different areas and matching up alot of info. I made script based vlookup. It maybe helpful in the future.
//-------------------------------------------------(Script Vlookup)------------------------------------------------//
/*
Benefit of this script is:
-That google sheets will not continually do lookups on data that is not changing with using this function
-Unlike Vlookup you can have it look at for reference data at any point in the row. Does not have to be in the first column for it to work like Vlookup.
Useage:
var LocNum = SpreadsheetApp.openById(SheetID).getSheetByName('Sheet1').getRange('J2:J').getValues();
FinderLookUpReturnArrayRange_(LocNum,0,'Data','A:G',[3],'test',1,1,'No');
-Loads all Locations numbers from J2:J into a variable
--looks for Location Numbers in Column 0 of Referance sheet and range eg "Data!A:G"
---Returns results to Column 3 of Target Sheet and range eg "test!A1" or "1,1"
*/
function FinderLookUpReturnArrayRange_(Search_Key,SearchKey_Ref_IndexOffSet,Ref_Sheet,Ref_Range,IndexOffSetForReturn,Set_Sheet,Set_PosRow,Set_PosCol,ReturnMultiResults)
{
var twoDimensionalArray = [];
var data = SpreadsheetApp.getActive().getSheetByName(Ref_Sheet).getRange(Ref_Range).getValues(); //Syncs sheet by name and range into var
for (var i = 0, Il=Search_Key.length; i<Il; i++) // i = number of rows to index and search
{
var Sending = []; //Making a Blank Array
var newArray = []; //Making a Blank Array
var Found ="";
for (nn=0,NNL=data.length;nn<NNL;nn++) //nn = will be the number of row that the data is found at
{
if(Found==1 && ReturnMultiResults=='No') //if statement for found if found = 1 it will to stop all other logic in nn loop from running
{
break; //Breaking nn loop once found
}
if (data[nn][SearchKey_Ref_IndexOffSet]==Search_Key[i]) //if statement is triggered when the search_key is found.
{
var newArray = [];
for (var cc=0,CCL=IndexOffSetForReturn.length;cc<CCL;cc++) //cc = numbers of columns to referance
{
var iosr = IndexOffSetForReturn[cc]; //Loading the value of current cc
var Sending = data[nn][iosr]; //Loading data of Level nn offset by value of cc
if(isEmpty_(Sending)==true) //if statement for if one of the returned Column level cells are blank
{
var Sending = "#N/A"; //Sets #N/A on all column levels that are blank
}
if (CCL>1) //if statement for multi-Column returns
{
newArray.push(Sending);
if(CCL-1 == cc) //if statement for pulling all columns into larger array
{
twoDimensionalArray.push(newArray);
Logger.log(twoDimensionalArray);
var Found = 1; //Modifying found to 1 if found to stop all other logic in nn loop
break; //Breaking cc loop once found
}
}
else if (CCL<=1) //if statement for single-Column returns
{
twoDimensionalArray.push(Sending);
var Found = 1; //Modifying found to 1 if found to stop all other logic in nn loop
break; //Breaking cc loop once found
}
}
}
if(NNL-1==nn && isEmpty_(Sending)==true) //following if statement is for if the current item in lookup array is not found. Nessessary for data structure.
{
for(var na=0,NAL=IndexOffSetForReturn.length;na<NAL;na++) //looping for the number of columns to place "#N/A" in to preserve data structure
{
if (NAL<=1) //checks to see if it's a single column return
{
var Sending = "#N/A";
twoDimensionalArray.push(Sending);
}
else if (NAL>1) //checks to see if it's a Multi column return
{
var Sending = "#N/A";
newArray.push(Sending);
}
}
if (NAL>1) //checks to see if it's a Multi column return
{
twoDimensionalArray.push(newArray);
}
}
}
}
if(typeof Set_PosRow != "number") //checks to see if what kinda of variable Set_PosRow is. if its anything other than a number it will goto next avaible row
{
var Set_PosRow = getFirstEmptyRowUsingArray_(Set_Sheet); //for usage in a database like entry without having to manually look for the next level.
}
for (var l = 0,lL=Search_Key.length; l<lL; l++) //Builds 2d Looping-Array to allow choosing of columns at a future point
{
if (CCL<=1) //checks to see if it's a single column return for running setValue
{
SpreadsheetApp.getActive().getSheetByName(Set_Sheet).getRange(Set_PosRow + l,Set_PosCol).setValue(twoDimensionalArray[l]);
}
}
if (CCL>1) //checks to see if it's a multi column return for running setValues
{
SpreadsheetApp.getActive().getSheetByName(Set_Sheet).getRange(Set_PosRow,Set_PosCol,twoDimensionalArray.length,twoDimensionalArray[0].length).setValues(twoDimensionalArray);
}
SpreadsheetApp.flush();
}
//*************************************************(Script Vlookup)*************************************************//
And some helper Functions
//;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
//Copy this block of fucnctions as they are used in the Vlookup Script
//;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
//-------------------------------------------------(Find Last Row on Database)------------------------------------------------//
function getFirstEmptyRowUsingArray_(sheetname)
{
var data = SpreadsheetApp.getActive().getSheetByName(sheetname).getDataRange().getValues();
for(var n = data.length ; n<0 ; n--)
{
if(isEmpty_(data[n][0])=false)
{
n++;
break;
}
}
n++
return (n);
}
//*************************************************(Find Last Row on Database)*************************************************//
//-------------------------------------------------(Blank Array Extractor/Rebuilder)------------------------------------------------//
function cleanArray_(actual)
{
var newArray = new Array();
for(var i = 0; i<actual.length; i++)
{
if (isEmpty_(actual[i]) == false)
{
newArray.push(actual[i]);
}
}
return newArray;
}
//*************************************************(Blank Array Extractor/Rebuilder)*************************************************//
//-------------------------------------------------(Even/Odd)------------------------------------------------//
function isEven_(value) {
if (value%2 == 0)
return true;
else
return false;
}
//*************************************************(Even/Odd)*************************************************//
//-------------------------------------------------(Array Col Sum Agent)------------------------------------------------//
function SumColArray_(sumagent)
{
var newArray = new Array();
for(var i = 0; i<sumagent.length; i++)
{
var totalsum = 0
var CleanForSum = cleanArray_(sumagent[i]);
for(var d = 0; d<CleanForSum.length; d++)
{
totalsum += CleanForSum[d];
}
newArray.push(Math.round(totalsum));
}
return newArray;
}
//*************************************************(Array Col Sum Agent)*************************************************//
//-------------------------------------------------(Empty String Check)------------------------------------------------//
function isEmpty_(string)
{
if(!string) return true;
if(string == '') return true;
if(string === false) return true;
if(string === null) return true;
if(string == undefined) return true;
string = string+' '; // check for a bunch of whitespace
if('' == (string.replace(/^\s\s*/, '').replace(/\s\s*$/, ''))) return true;
return false;
}
//*************************************************(Empty String Check)*************************************************//
Eventually I sorted out with native functions filtering on a single row by id
=IFERROR(FILTER(IMPORTRANGE("key";"sheet1!B1:AN300");IMPORTRANGE("key";"sheet1!A1:A300") = id))
Is it possible to merge 100 Google Docs documents into one?
I've tried copy-pasting, but it seems too long and it's not possible to copy comments.
This can be done with Google Apps Script. See this example. The most relevant parts (example assumes nothing but Google Docs in the folder):
function combine() {
var folder = DriveApp.getRootFolder();
if (folder == null) { Logger.log("Failed to get root folder"); return; }
var combinedTitle = "Combined Document Example";
var combo = DocumentApp.create(combinedTitle);
var comboBody = combo.getBody();
var hdr = combo.addHeader();
hdr.setText(combinedTitle)
var list = folder.getFiles();
while (list.hasNext()) {
var doc = list.next();
var src = DocumentApp.openById(doc.getId());
var srcBody = src.getBody();
var elems = srcBody.getNumChildren();
for (var i = 0; i < elems; i++ ) {
elem = srcBody.getChild(i).copy();
// fire the right method based on elem's type
switch (elem.getType()) {
case DocumentApp.ElementType.PARAGRAPH:
comboBody.appendParagraph(elem);
break;
case // something
}
}
}
}
Note that you don't copy the source document's contents in one lump; you have to loop through them as individual elements and fire the correct append* method to add them to the merged/destination file.
I expanded on #noltie's answer to support merging docs in a folder structure recursively, starting from an arbitrary folder (not necessarily the root folder of google docs) and guard agains script failures on too many unsaved changes.
function getDocsRec(rootFolder) {
var docs = [];
function iter(folder) {
var childFolders = folder.getFolders();
while (childFolders.hasNext()) {
iter(childFolders.next());
}
var childFiles = folder.getFiles();
while (childFiles.hasNext()) {
var item = childFiles.next();
var docName = item.getName();
var docId = item.getId();
var doc = {name: docName, id: docId};
docs.push(doc);
}
}
iter(rootFolder);
return docs;
}
function combineDocs() {
// This function assumes only Google Docs files are in the root folder
// Get the id from the URL of the folder.
var folder = DriveApp.getFolderById("<root folder id>");
if (folder == null) { Logger.log("Failed to get root folder"); return; }
var combinedTitle = "Combined Document Example";
var combo = DocumentApp.create(combinedTitle);
var comboBody = combo.getBody();
// merely get the files recursively, does not get them in alphabetical order.
var docArr = getDocsRec(folder);
// Log all the docs we got back. Click "Edit -> Logs" to see.
docArr.forEach(function(item) {
Logger.log(item.name)
});
// this sort will fail if you have files with identical names
// docArr.sort(function(a, b) { return a.name < b.name ? -1 : 1; });
// Now load the docs into the combo doc.
// We can't load a doc in one big lump though;
// we have to do it by looping through its elements and copying them
for (var j = 0; j < docArr.length; j++) {
// There is a limit somewhere between 50-100 unsaved changed where the script
// wont continue until a batch is commited.
if (j % 50 == 0) {
combo.saveAndClose();
combo = DocumentApp.openById(combo.getId());
comboBody = combo.getBody();
}
var entryId = docArr[j].id;
var entry = DocumentApp.openById(entryId);
var entryBody = entry.getBody();
var elems = entryBody.getNumChildren();
for (var i = 0; i < elems; i++) {
var elem = entryBody.getChild(i).copy();
switch (elem.getType()) {
case DocumentApp.ElementType.HORIZONTAL_RULE:
comboBody.appendHorizontalRule();
break;
case DocumentApp.ElementType.INLINE_IMAGE:
comboBody.appendImage(elem);
break;
case DocumentApp.ElementType.LIST_ITEM:
comboBody.appendListItem(elem);
break;
case DocumentApp.ElementType.PAGE_BREAK:
comboBody.appendPageBreak(elem);
break;
case DocumentApp.ElementType.PARAGRAPH:
comboBody.appendParagraph(elem);
break;
case DocumentApp.ElementType.TABLE:
comboBody.appendTable(elem);
break;
default:
var style = {};
style[DocumentApp.Attribute.BOLD] = true;
comboBody.appendParagraph("Element type '" + elem.getType() + "' could not be merged.").setAttributes(style);
}
}
// page break at the end of each entry.
comboBody.appendPageBreak();
}
}
You can create and run a script with the above code on https://script.google.com/home
Both the above fail for me with the script returning a red lozenge:
Service unavailable: Docs Dismiss
(the documents in the folder are found, as are the document id's, and the combined doc is created, but empty)
Fixed that - had a document in the list that wasn't owned by me or was created by conversion. Removed that and away we go.
Google Docs does not support any type of merge, yet.
You can select all 100 docs, download them and try to merge them offline.
Download all the files as Docx, then use Microsoft Word or Open Office to merge the documents using the "master document" feature. (Word also refers to this as "Outline.")