Download "private" files from Google Drive, and folders and files
INTRO
I have spent much time on methods for getting binary files up to Google Drive (private and public), but until now have not tackled the method for bringing private binary files stored on Google drive into App Inventor apps. This can be done by using a google apps script web app to convert the binary file to a base64 string, which can then be downloaded, as text, to the app, and converted back to a binary file. This relies on the files being owned by the google account running the web app. The same method, though not necessary, can be used for public files as well. I am relying on the excellent base64 extension provided by Juan Antonio, to handle the conversion at the app.
The methods presented below are for demonstration purposes of this specific activity. Developers will need to add more functionality.
The methods have been tested using the companion app and compiled version on Android 10 and Android 11, but are designed to work with any compatible Android version.
I have used image files for each of the methods, as these provide for better visibility of what is going on, but the approaches should work with most common binary filetypes (e.g. png/jpg/pdf/html/txt/zip...). I would recommend keeping file sizes to a minimum <5mb, running this with a large number of files on a data connection may be prohibitive.
This is not designed for use with Google Docs files (Sheets/Docs/Slides/Drawings).
I have purposely designed the methods to ask the user for permission to read/write to their device on first run.
The methods shown below do provide a level of security to ones private files, but this can be improved with additional security (e.g. password protection) at the web app.
I have used standalone google apps script projects, and assume that followers of this guide have the knowledge of how to create and publish google apps script web apps.
I provide three methods:
STAGE 1
A basic return of a single file from Google Drive to the app. This is the method in its simplest form to help with understanding and for development by others. The scripting in the web app here is used by all three methods to download the files.
STAGE 2
A return of all files in a single folder on Google Drive. The folder as well as the files are private. Probably the most required/usable method.
STAGE 3
A return of all files in all subfolders of a parent folder on Google Drive, recreating the directory structure on the android device. All folders and files are private. (This was partly inspired by seeing the struggles of a developer on the AI2 community attempting something similar with "public" files.)
STAGE 1
You require the script url for the web app, and a file ID of a private binary file stored on Google Drive
HOW DOES IT WORK?
On pressing Button1, a progressDialog is started to keep the user entertained, then you call the web app script with the web component, supplying the file ID. The web app receives the file ID, fetches the file, and then converts it to a base64 string, following which it returns the base64 string and the filename as a stringified JSON list to App Inventor.
When the web component receives the content, the stringified JSON is converted to a list, the two elements of this list are then used to convert the base64 string back into a binary file with the provided file name, and the file is stored in either the ASD or the root of the sdcard, depending on which Android version is in use.
Using the new when KIO4_Base641 .GotFile block, the progressDialog is closed, the file name is displayed, and the image is also displayed
BLOCKS
SCRIPT
function doGet(e) {
var myFile = DriveApp.getFileById(e.parameter.ID);
var b64String = Utilities.base64Encode(myFile.getBlob().getBytes());
return ContentService.createTextOutput(JSON.stringify([b64String,myFile.getName()]));
}
STAGE 2
You require the script url for the web app, and a folder ID of a private folder with private files stored on Google Drive
HOW DOES IT WORK?
When you press the Get Files in Folder button, and after giving read permission to the app (compiled), the following happens:
The app sends a web GET request using the web component to the web app script url with fn = folder, and supplying a folder ID.
The web app then generates a JSON stringified text containing all the files in the folder, with their filenames, fileIDs, and their containing folder name. This is returned to the app.
The app then creates the directory using the folder name, in either the ASD or root of the sdcard, depending on Android version.
Then using the filebyfile method (credits @ Taifun), the app then calls each file in turn, using the script in Stage 1, to return a base64 string and filename.
When this is returned, the app converts the base64 string to a binary file and names it with the filename, storing the file in the directory created on the device.
A delete folder and files button is provided to reset the app.
BLOCKS
SCRIPT
function doGet(e) {
//return filename and file as base64string from provided fileID
if (e.parameter.fn == 'file') {
var myFile = DriveApp.getFileById(e.parameter.fileID);
var b64String = Utilities.base64Encode(myFile.getBlob().getBytes());
return ContentService.createTextOutput(JSON.stringify([b64String,myFile.getName()]));
}
//return all filenames and fileIDs in provided folder
else if (e.parameter.fn == 'folder') {
var fileList=[];
var parentFolder = DriveApp.getFolderById(e.parameter.folderID);
var files = parentFolder.getFiles();
while (files.hasNext()) {
var file = files.next();
fileList.push([file.getId(),file.getName(),parentFolder.getName()]);
}
return ContentService.createTextOutput(JSON.stringify(fileList));
}
}
STAGE 3
You require the script url for the web app, and a parent folder ID of a private folder with private subfolders and private files, stored on Google Drive
HOW DOES IT WORK?
I have broken this down into three sections (plus a delete method) for demonstration purposes (they can all be run together).
Get the File Listing
On pressing the Get File Listing button, the web component is used to call the web app script url, providing the fn = filesAndFolders and the parentfolder ID. This activates the second of the two procedures in the web app, which generates a JSON stringified listing of:
The name of the parentFolder
A list of the subfolder names
A list of each file, its filename, its file ID, and its holding folder
The file listing contents are displayed in labels in the app from the response content returned to the web component.
Create the Directory Structure
On pressing the Create Directories button:
Using the parentFolder name, and the list of sub folder names, each of these folders is then created, either in the ASD or in the root of the sdcard, depending on the Android version.
Download Files
Using the time honoured "file by file" method (credits @ Taifun), the app then iterates over the list of files, supplying the file ID to the web component, and replicating the script call as seen in STAGE 1 above, but also providing the correct path to the correct folder for each file once the base64 string and filename have been returned.
As each file is downloaded, its filename is shown in the progressDialog, and its image is shown in the background.
Delete All Files and Folders
This is provided for a reset, and simply deletes everything that has just been created and downloaded. Note that subfolders have to be deleted before parentfolders can be deleted, but files within subfolders will be deleted when a subfolder is deleted
IMAGES (folders and files on Google Drive)
BLOCKS
SCRIPT
function doGet(e) {
//return filename and file as base64string from provided fileID
if (e.parameter.fn == 'file') {
var myFile = DriveApp.getFileById(e.parameter.fileID);
var b64String = Utilities.base64Encode(myFile.getBlob().getBytes());
return ContentService.createTextOutput(JSON.stringify([b64String,myFile.getName()]));
}
//return all filenames and fileIDs in provided parent folder
else if (e.parameter.fn == 'filesInFolders') {
var allList = [];
var parentFolder = DriveApp.getFolderById(e.parameter.folderID);
allList.push([parentFolder.getName()]);
var folderList = [];
var fileList = [];
var folders = parentFolder.getFolders();
while (folders.hasNext()) {
var folder = folders.next();
folderList.push(folder.getName());
var files = folder.getFiles();
while (files.hasNext()) {
var file = files.next();
fileList.push([file.getId(),file.getName(),folder.getName()]);
}
}
allList.push(folderList);
allList.push(fileList);
return ContentService.createTextOutput(JSON.stringify(allList));
}
}
VIDEO (STAGE 3)
CREDITS
My thanks again to Juan, without whose work on the base64 extension none of this would be possible
RESOURCES
For a more dynamic folder/file listing, see HERE for ideas