What can Google Drive do?
Google Drive is an online (cloud-based) storage service, with capacity and limits depending on your subscription. It can automatically sync the data stored on your computer and phone. You can also share content with others. It includes a web interface, so after authentication you can access your files from any major browser.
I think this much is clear even for everyday users.
However, don’t stop here—there is much more to discover!
1.) Not many people know that Drive does not operate using the classic, well-known file and folder model.
- every file and folder is identified by an ID, not by its path. This means that if you move or rename it, links still work—which will be a huge advantage from a programming perspective
- it allows multiple files with the same name and extension to exist in the same folder
- it can version uploaded files
- far fewer people know that a file or folder can exist in multiple folders at the same time. In other words, it can have multiple parents. This is much more valuable in a business environment than it first appears. How many times did you struggle deciding where a document belongs? Just place it in both places. Select the file or folder and press Shift+Z. Then choose the additional folder where it should appear.
So you no longer need to worry whether a presentation should go into the project folder or a global presentation folder. In a way, each folder can also be considered a tag you apply to files.
(Note: this is also why file paths cannot be used for identification—it depends on which folder you’re approaching it from.)
2.) Team Drive introduces another concept, which encourages thinking in terms of teams and projects. One of Drive’s sacred rules is that every uploaded item must have an owner. Although ownership can be transferred, removing an employee’s account when they leave the company—especially if there is no replacement—is not always simple. Team Drive solves this by shifting the ownership from individuals to the team itself, which is far more flexible for personnel changes.
This concept aligns well with Slack’s channel philosophy.
3.) Under G-Suite, Google Groups becomes truly meaningful. It’s not only useful as a mailing list—you can also assign Drive sharing permissions to groups. For example, you can create groups based on company roles such as Project Manager, Business Analyst, Sales, etc. Then assign access to folders based on the group. When employees join, leave, or change roles, you only need to update group membership. This reduces organizational overhead and saves time and money by lowering administration costs caused by staff turnover.
From here, it’s only one step to assign permissions to projects and teams in a flat structure—each Google Group represents a team and/or project.
Creating a Google Drive folder template
Now that we understand Drive’s behavior better, let’s see how we can orchestrate everything and support it with Google Apps Script.
Borrowing some proven methods from the Ponte workshop, let’s aim to achieve the following behavior:
Roles:
- Everyone – all employees of the company
- CEO – executive management
- PM – project managers
- BA – business analysts
- [Project] members – roles assigned per project
Folder structure:
Projects / [Client name] / [Project name] / ...
With the following structure inside:
⤷ Business Offer
⤷ Client Documents
⤷ Meeting Minutes
⤷ Specifications
⤷ Development
⤷ Design
⤷ Specifications
⤷ Documentation
*The two Specifications folders are actually one—we are using the multiple parent feature.
Permissions:
- generally, Everyone has read-only access, inherited by all subfolders
- CEO and PM have write access to all folders, inherited by subfolders
- only CEO and PM may read the Business Offer folder—meaning the inherited read permission of Everyone must be revoked here
- Project members have write access, but only within their specific project
Although this is just a minimal, not too complex concept, it is already clear that configuring all of this manually for each project would be repetitive and time-consuming. So let’s use Google Apps Script and build a web application with a frontend where providing a new project name creates everything automatically.
I. Backend functions
Let’s clarify the backend functions, which will be called asynchronously from the frontend. We aim for a fail-tolerant solution—if a client or project folder already exists, we won’t recreate it. Remember, Drive allows multiple folders with the same name.
Implementing key functions
Create a folder to store your projects. Take its ID, as we will use it to identify it in the code. You can find the ID in the browser’s URL bar when opening the folder.
Let’s define the constants first.
var PROJECTS_FOLDER_ID = '...'; // The ID of our Projects folder
Next, list all existing client folders:
function listExistsClients(){
var rootFolder = DriveApp.getFolderById(PROJECTS_FOLDER_ID);
var clientsFoldersIterator = rootFolder.getFolders();
var result = [];
while(clientsFoldersIterator.hasNext()){
var clientFolder = clientsFoldersIterator.next();
result.push({
name : removeAccent(clientFolder.getName().toLowerCase()), // normalized name for sort
folder : clientFolder
});
}
result.sort(function(a, b){
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
});
return result;
}
As you can see, we receive an iterator that allows us to traverse the subfolders. We collect information from them and normalize the names by removing accents for easier sorting.
The following code snippet removes accents in JavaScript:
var ACCENTS = {
a: 'àáâãäåæÀÁÂÃÄÅÆ',
c: 'çÇ',
e: 'ÈÉÊËÆ',
i: 'ìíîïÌÍÎÏ',
n: 'ñÑ',
o: 'òóôõöøÒÓÔÕÖØ',
s: 'ß',
u: 'ùúûüÙÚÛÜ',
y: 'ÿŸ'
};
function removeAccent(input) {
for(var c in ACCENTS)
if(ACCENTS.hasOwnProperty(c))
input = input.replace(new RegExp('[' + ACCENTS[c] + ']', 'g'), c.toString());
return input;
}
Next comes the function that checks whether a folder exists—if not, it creates it. This will come in handy multiple times.
function findOrCreateFolder(rootFolder, clientName){
// Check exists
var foldersIterator = rootFolder.getFoldersByName(clientName);
if(foldersIterator.hasNext())
return { state: 'exists', folder: foldersIterator.next() };
// Or create new one
return { state: 'new', folder: rootFolder.createFolder(clientName) };
}
Now we have the basics for listing and creating folders. Next, let’s focus on generating the structure itself. Since the structure may change in the future, let’s define it in JSON.
[
{
"name" : "Business Offer",
"description" : "Contracts, offers, purchase orders, deliverable confirmations",
"share" : {
"ceo@yourdomain.hu" : "organize",
"pm@yourdomain.hu" : "organize"
}
},
{
"@id" : "client-documents",
"name" : "Client Documents",
"description" : "Documents received from the client, minutes, deliverables, etc...",
"folders" : [
{
"name" : "Meeting Minutes",
"description" : "Memos, meeting notes"
}
]
},
{
"name" : "Developer Documentation",
"description" : "Development-related resources",
"folders" : [
{
"name" : "Specifications",
"description" : "Requirement or functional specifications",
"@parents" : [
"client-documents"
]
},
{
"name" : "Documentation"
},
{
"name" : "Design",
"description" : "Design, styling elements, guides, wireframes, fonts"
}
]
}
]
A brief explanation of the fields:
- name: folder name
- description: folder description
- folders: subfolders
- share: overrides inherited permissions
- @id: unique reference within the JSON
- @parents: multiple parent folders, referencing @id values
Now let’s look at the code that reads the structure and creates the folders:
var __IDS = {};
var __PARENTS = {};
function createStructureWithMultipleParents(rootFolder, structure){
__IDS = {};
__PARENTS = {};
Logger.log("Create structure by JSON in: " + rootFolder.getName());
createStructure(rootFolder, structure);
Logger.log("Structure successful created.");
Logger.log("Set multiple parent");
// Set multiple parent
for (var key in __PARENTS){
if (__PARENTS.hasOwnProperty(key)) {
var arr = __PARENTS[key];
for(var index in arr){
__IDS[key].addFolder(arr[index]);
}
}
}
Logger.log("Done");
}
function createStructure(rootFolder, structure){
if(!rootFolder || !structure) return;
if(isArray(structure)){
structure.forEach(function(config){
if(config && config.name){
// Create new
var newFolder = rootFolder.createFolder(config.name);
// Add description when necessary
if(config.description)
newFolder.setDescription(config.description);
// Collect id
if(config["@id"]){
__IDS[config["@id"]] = newFolder;
}
// Collect parents
if(config["@parents"] && isArray(config["@parents"])){
config["@parents"].forEach(function(parentId){
if(!__PARENTS[parentId])
__PARENTS[parentId] = [];
__PARENTS[parentId].push(newFolder);
});
}
// Override share
if(config.share){
// newFolder.setSharing(DriveApp.Access.PRIVATE, DriveApp.Permission.NONE);
}
// Create subfolders
if(config.folders){
createStructure(newFolder, config.folders);
}
}
});
}
}
You probably needed more time to review this code. The key points are: no matter how deeply nested a folder is, the creation method is the same, so recursion is used. Also, multiple parents can only be applied after all parent folders exist, which is why createStructure and createStructureWithMultipleParents are separate.
Permissions
Run one of the script’s functions and observe what happens. You will see an authorization dialog appear. Before running code, Apps Script analyzes which APIs and resources the script interacts with, and asks for permission accordingly. A script cannot modify a user's Drive without permission. This appears when your code includes previously unauthorized operations.
You must accept these permissions for successful execution.
Endpoints for frontend rendering
As mentioned in the previous article, Apps Script can return HTML content. To do this, we must implement one of the following methods:
- doGet()
- doPost()
We will implement doGet() to serve the frontend, allowing it to load nicely in the browser.
function doGet(e) {
return HtmlService
.createTemplateFromFile('index.html')
.evaluate()
.setTitle("Project Creator");
}
Let’s add one more method—the function that triggers the entire creation process:
function doCreateNewProject(clientName, projectName){
var rootFolder = DriveApp.getFolderById(PROJECTS_FOLDER_ID);
if(!rootFolder) return err('Root folder not found!');
var clientFolderResult = findOrCreateFolder(rootFolder, clientName);
if(!clientFolderResult.folder) return err('Client folder create failed: ' + clientName);
var projectFolderResult = findOrCreateFolder(clientFolderResult.folder, projectName);
if(!projectFolderResult.folder) return err('Project folder create failed: ' + projectName);
// Create structure
var structureJSONFile = DriveApp.getFileById(PROJECTS_STRUCTURES_JSON_FILE_ID);
var structureJSON = structureJSONFile.getBlob().getDataAsString();
var structure = JSON.parse(structureJSON);
createStructureWithMultipleParents(projectFolderResult.folder, structure);
return ok({
state: projectFolderResult.state,
url: projectFolderResult.folder.getUrl()
});
}
This returns two values: the state (whether the folder is new or already existed), and the URL so the frontend can redirect the user.
II. Frontend
Since Apps Script can deliver HTML responses, we can include CSS/CSS3 and client-side JavaScript as well. This even enables the use of frontend frameworks such as Bootstrap or Material Design.
I chose Material Design at the time because it integrates well with Google products. Several implementations exist, but I selected Materialize CSS, mainly because it provides a well-functioning Select component, which getmdl.io does not.
I won’t explain the entire frontend HTML here, but I will show how to include it in the script.
Add three HTML files after your .gs files:
- index.html
- javascript.html
- stylesheet.html
We saw earlier how doGet() serves index.html. Here is the relevant part showing how CSS and JS are included:
index.html
...
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<title>Client Project</title>
<?!= HtmlService.createHtmlOutputFromFile('stylesheet.html').getContent(); ?>
<?!= HtmlService.createHtmlOutputFromFile('javascript.html').getContent(); ?>
<?
var clients = listExistsClients();
?>
</head>
...
javascript.html – client-side JS
<script src="https://code.jquery.com/jquery-2.1.1.min.js" type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.98.2/js/materialize.min.js" type="text/javascript"></script>
<script>
function toast(message, callback){
if(callback)
Materialize.toast(message, 3000, '', callback);
else
Materialize.toast(message, 3000);
}
...
stylesheet.html
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:regular,bold,italic,thin,light,bolditalic,black,medium&lang=en" />
<!-- http://materializecss.com/getting-started.html -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.98.2/css/materialize.min.css" />
<style>
.company-logo{
max-width:144px;
max-height:48px;
vertical-align: middle;
}
...
We must also consider mobile devices. Modern frameworks support responsive layouts, but our page still looked off on mobile due to the container. The fix is to include the viewport meta tag:
function doGet(e) {
return HtmlService
.createTemplateFromFile('index.html')
.evaluate()
.addMetaTag('viewport', 'width=device-width, initial-scale=1')
.setTitle("Project Creator");
}
III. Ajax
We want to exchange data without reloading the page, i.e., asynchronously using AJAX. You might wonder how this can be done when both client and server use JavaScript.
The good news is: Apps Script supports this out of the box. From the client side, you can directly call server-side functions by name.
google.script.run.withSuccessHandler(function(r){
if(r.result == 'OK'){
// Redirect after message
if(r.content.state == 'exists'){
toast('Project folder already exists!', function(){
window.top.location.href = r.content.url; // Redirect
});
return;
}
// Redirect immediately
window.top.location.href = r.content.url;
} else {
toast(r.message);
}
formEnable($form, true);
}).withFailureHandler(function(e){
toast(e);
formEnable($form, true);
}).doCreateNewProject(client, project);
As shown above, you can call your backend function directly.
Launching the web application
Near the end, the remaining question is how to launch and test your application.
Publish > Deploy as web app
On the first run, you must create a version. After that, the “latest code” link will open your application.
Result
What’s next?
From here, it's just a matter of creativity. Extend the project creation feature to other tools you use. See what APIs they offer and call them. You may even add reporting or notification features.
If you're still motivated, the next chapter will show you how to build a much more complex application.