CORS toegang tot Google Drive API met de ajax bibliotheek
Home

CORS toegang tot Google Drive API met de ajax bibliotheek

CORS toegang tot Google Drive API met de ajax bibliotheek

Google APIs ondersteunen requests en responses die gebruik maken Cross-origin Resource Sharing (CORS). Je hoeft niet de volledige JavaScript client library te laden om CORS te gebruiken. Maar als je applicatie toegang wil hebben tot de persoonlijke informatie van een gebruiker moet je wel nog eerst het Google's OAuth 2.0 mechanisme gebruiken. Om dit mogelijk te maken beschikt Google over een aparte op zichzelf staande auth client — een subset van de JavaScript client. Op deze pagina leer je hoe je de standalone auth client en CORS gebruikt om toegang te verkrijgen tot de Google Drive APIs.Dat is mijn favoriete manier van werken omdat we die ook kunnen gebruiker voor andere providers bv. OneDrive van Microsoft.

Verantwoording

Als je een bestand naar Google Drive wilt uploaden kom je haast vanzelf uit bij de Google Drive API. In het begin van je zoektocht naar informatie over de Google API's kom je terecht bij hun SDK (client library). De SDK beschikt over gemakkelijke functies om de API aan te spreken en je vindt hopen voorbeelden in verschillende programmeertalen.

Het probleem bestaat erin dat je hoe meer de SDK gebruikt, hoe verwarder je wordt. De documentatie is vaak onduidelijk. Niet alle functies zijn geïmplementeerd op de zelfde manier in alle talen. Maar het feit dat het uploaden van een bestand naar Google Drive 5 keer de hoeveelheid geheugen van de tekst zelf vereist, deed me afhaken.

In tegenstelling tot de SDK is elke API-aanroep die je kan maken duidelijk en consequent gedocumenteerd. Het was tijd om direct naar de API te gaan. Tuurlijk is er nog steeds heel wat zoekwerk vereist om bepaalde functionaliteit te laten werken. Maar mijn ervaring met de API is een stuk beter. Het heeft me geholpen de REST structuur van de APIs te begrijpen waardoor nieuwe dingen uitzoeken en uitproberen veel sneller gaat.

De standalone auth client laden

De standalone Auth client kan geladen worden met de JavaScript client's load function. We roepen die Auth client op in een functie met de naam loadAuthClient. Deze functie staat in het bestand met de naam google-oauth-api.js die staat in de js folder:

function loadAuthClient() {
    // Load the API client and auth library
    gapi.load('client:auth2', initAuth);
}

Deze functie roepen we op op het moment dat we het api.js inladen. We geven de naam van de functie mee als de waarde voor de onload queryparameter. Dat doen we in de html pagina met de naam google-drive-api-ajax-library.html:

<script src="https://apis.google.com/js/api.js?onload=loadAuthClient"></script>

Geverifiëerde requests maken

Om een access token voor geverifëerde requests te bemachtigen, gebruik je dezelfde gapi.auth2 methoden van de standaard JavaScript Client of van de auth-only client. Meer info over hoe je een access token ophaalt, vond je op de Authentication page. Je hebt twee mogelijkheden om een geverifiëerde request met CORS te maken:

Om een access token op te halen, roep je de getAuthResponse() methode op of een GoogleUser object.

Het formaat van de OAuth 2.0 token wordt in detail beschreven in het Methods and classes document.

Voorbereiding

  1. In de js map staan drie bestanden klaar:
    1. ajax.js met de Ajax library (Een Ajax bibliotheek);
    2. google-oauth-api.js met de authencticatie code (OAuth 2.0 voor client-side web applicaties);
    3. helpers.js (OAuth 2.0 voor client-side web applicaties);
  2. In de root hebben we een html bestand met de naam google-drive-api-ajax-library.html:
    <!doctype html>
    <html lang="nl">
    
    <head>
        <meta charset="UTF-8">
        <title>Ajax call to Google Drive API</title>
        <script type="text/javascript" src="js/google-oauth-api.js"></script>
        <script type="text/javascript" src="js/google-drive-api.js"></script>
        <script type="text/javascript" src="js/helpers.js"></script>
        <script type="text/javascript" src="js/ajax.js"></script>
        <script>
            var apiKey = 'AIzaSyBCQQ1MNcY1h5ptG-ADv7lGnyQ3gnOC9QM';
            var clientId = '420594077145-po45ldmitjnulihvt5309kmeoltb9pmq.apps.googleusercontent.com';
            var scopes = ['profile', 'https://www.googleapis.com/auth/drive.file'];
    
            /**
             * Handle the initial sign-in state.
             * This method is called by the OAuth initAuth method and is
             * called once the user is signed in succesfully
             */
            var handleInitialSignInStatus = function() {
                updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
                signinButton.addEventListener("click", signIn);
                signoutButton.addEventListener("click", signOut);
                createFolderButton.addEventListener("click", prepareCreateFolder);
                getFiles();
            }
        </script>
    </head>
    
    <body>
        <header id=user-profile>
            <h1>Myaa</h1>
            <!--Knoppen toevoegen om auth te initialiseren en af te melden -->
            <button id="signin-button">Aanmelden</button>
            <button id="signout-button">Afmelden</button>
        </header>
        <main>
            <div id="text-editor">
                <header class="command-panel"></header>
                <label style="display: block;" for="editor">Typ hier je tekst: </label>
                <textarea id="editor" style="width: 60em; height: 20em;, display: block;"></textarea>
                <button type="button" id="uploadText" onclick="prepareUploadText();">
                    Upload tekst naar Google Drive in de geselecteerde folder</button>
            </div>
            <aside id="explorer">
                <header class="command-panel">
                    <div>
                        <label for="currentFolderId">Geselecteerde folder id:</label>
                        <!-- if you don't want to show the id to the user set type to hidden -->
                        <input style="width: 20em;" type="text" id="currentFolderId" value="root" readonly/>
                        <label for="currentFolderName">Geselecteerde folder naam:</label>
                        <input type="text" id="currentFolderName" value="root" readonly/>
                    </div>
                    <div>
                        <label for="folder-name">Nieuwe foldernaam: </label>
                        <input type="text" placeholder="naam nieuwe map" id="folderName" />
                        <button type="button" id="createFolderButton">Folder maken</button>
                    </div>
                </header>
                <div id="list"></div>
            </aside>
        </main>
        <button type="button" onclick="uploadFileUitproberen();">Upload file uitproberen</button>
        <div id="feedback">
        </div>
        <script>
            var signinButton = document.getElementById('signin-button');
            var signoutButton = document.getElementById('signout-button');
        </script>
        <script src="https://apis.google.com/js/api.js?onload=loadAuthClient"></script>
    </body>
    
    </html>

Bestanden ophalen

De getFiles methode

Met deze methode halen we alle bestand op die op de Google drive van de gebruiker staan. Deze methode staat in google-drive-api.js.

/**
 * We weten niet van te voren wat we met de bestanden gaan doen,
 * vandaar dat je de callback methode als parameter kan meegeven.
 * Wordt er geen parameter meegegeven, gaan we ervan uit dat je
 * gewoon een lijst van de opgehaalde bestanden wilt tonen.
 * This is a simple listing with filters to the
 * not trashed (m.a.w. gedeleted door de gebruiker).
 *
 * @param {callbackFunction} the callback function for the ajax call
 */
var getFiles = function(callbackFunction = showFiles) {
    var accessToken = gapi.auth2.getAuthInstance().currentUser.get().getAuthResponse().access_token;
    // or this: gapi.auth.getToken().access_token;
    var ajax = new Ajax();
    // Google CORS
    var url = 'https://www.googleapis.com/drive/v3/files';
    // query
    url += '?q=';
    url += 'not+trashed';
    // fields
    url += '&';
    url += 'fields=files(iconLink%2Cname%2CmimeType%2Cid%2Cparents)'; // %2C is html code for a comma (,)
    ajax.getRequest(url, callbackFunction, 'text', accessToken);
    // onthoud de id en de naam van de geselecteerde folder
    document.getElementById('currentFolderId').value = 'root';
    document.getElementById('currentFolderName').value = 'root';
}

De showFiles callback methode

Deze methode staat in google-drive-api.js.

/**
 * Maak voor elk item in de lijst een li element met daarin de naam van het bestand.
 * Als het een folder is voegen we een knop toe waarop je kan klikken om de inhoud
 * van de folder te zien te krijgen
 *
 * @param {responseText} het antwoord van de server in tekstformaat
 */
var showFiles = function(responseText) {
    var ul = document.querySelector("#explorer #list");
    ul.innerHTML = '';
    var li = document.createElement("li");
    li.innerHTML = '<button type="button" onclick="getFiles();">Terug naar root</button>';
    ul.appendChild(li);

    var response = JSON.parse(responseText);
    for (var i = 0; i < response.files.length; i++) {
        var item = response.files[i];
        var li = document.createElement("li");
        var html = "<img src='" + item.iconLink + "'> ";
        if (item.mimeType == 'application/vnd.google-apps.folder') {
            html += '<button type="button" onclick="getFilesInFolder(\'' +
                item.id + '\', \'' + item.name + '\');">' + item.name + '</button>';
        }
        else {
            html += item.name;
            // only html
            if (item.name.indexOf('.html') > -1) {
                html += ' <button type="button" onclick="downloadText(' + '\'' +
                    item.id + '\', \'' + item.name + '\');">open</button>'
            }
        }
        html += ' <button type="button" onclick="deleteFile(' + '\'' +
            item.id + '\', \'' + item.name + '\');">delete</button>';
        li.innerHTML = html;
        ul.appendChild(li);
    }
    showFeedback(responseText);
}

De getFilesInFolder methode

Deze methode staat in google-drive-api.js.

Een folder in Google Drive is een bestand met een tag waarmee aangegeven wordt dat het bestand een folder is. Als we alle bestanden in een folder willen oplijsten volstaat het om alle bestanden met een specifieke paren tag op te halen. Dat doen we in de q querystring parameter.

/**
 * We weten niet van te voren wat we met de bestanden gaan doen,
 * vandaar dat je de callback methode als parameter kan meegeven.
 * Wordt er geen parameters meegegeven, gaan we ervan uit dat je
 * gewoon een lijst van de opgehaalde bestanden wilt tonen.
 * This is a simple listing with filters to the parent directory
 * and not trashed (m.a.w. gedeleted door de gebruiker).
 *
 * @param {id} de id van de te tonen folder
 * @param {name} de naam van de te tonen folder
 * @param {callbackFunction} the callback function for the ajax call
 */
function getFilesInFolder(id, name, callback = showFiles) {
    var accessToken = gapi.auth2.getAuthInstance().currentUser.get().getAuthResponse().access_token;
    // or this: gapi.auth.getToken().access_token;
    var ajax = new Ajax();
    // google CORS
    var url = 'https://www.googleapis.com/drive/v3/files';
    // query
    url += '?q=';
    url += 'not+trashed';
    url += '+and+';
    url += '\'' + id + '\'+in+parents';
    // fields
    url += '&';
    url += 'fields=files(iconLink%2Cname%2CmimeType%2Cid%2Cparents)';
    //alert(url);
    ajax.getRequest(url, showFiles, 'text', accessToken);
    // onthoud de id en de naam van de geselecteerde folder
    document.getElementById('currentFolderId').value = id;
    document.getElementById('currentFolderName').value = name;
}

Een nieuwe folder maken

Folders werken niet zoals op Windows of Linux. Ze zijn niet hiërarchisch opgebouwd. Een folder is een niet meer dan een tag die aan een bestand of een andere folder gegeven wordt.

De createFolder methode

Deze methode staat in google-drive-api.js.

Deze methode creëert een folder in de 'root' map van de Google drive. Dat wil zeggen dat die folder geen parent folder heeft. De enige paramater is dan ook de naam van de te creëren folder. Het mime type is application/vnd.google-apps.folder waarmee we aangegeven dat we een bestand van het type folder willen maken. Creëren wil zeggen dat we een POST doen naar de server.

Vervolgens maken we een instantie van het Ajax object. We geven in de body van het request een JSON string mee waarin de naam en het mime type staan.We geven op dat het antwoord van de server text is en we voegen een header toe waarin we aan de server meedelen dat de gegevens in de body van het gegevenstype JSON zijn.

/**
 * Drive API Helper function createFolder
 * Create a folder with the name given in the title parameter
 *
 * @param {string} title the name of the folder to be created
 */
function createFolder(name) {
    var accessToken = gapi.auth2.getAuthInstance().currentUser.get().getAuthResponse().access_token;
    // or this: gapi.auth.getToken().access_token;
    var ajax = new Ajax();
    var data = {
        name: name,
        mimeType: "application/vnd.google-apps.folder"
    }
    ajax.postRequest('https://www.googleapis.com/drive/v3/files',
        JSON.stringify(data),
        function(responseText) {
            alert(responseText);
        },
        'text',
        'application/json',
        accessToken);
}

De createFolderInParent methode

Deze functie doet hetzelfde als de createFolder methode maar heeft een extra parameter die de id van de folder bevat waarin de nieuwe folder gemaakt moet worden.

/**
 * Drive API Helper function createFolderInParent
 * Create a folder with the name given in the title parameter
 * in the root folder (myap)
 *
 * @param {string} title the name of the folder to be created
 * @param {parentId} the id of the parent folder
 */
function createFolderInParent(name, parentId) {
    var data = {
        'name': name,
        'mimeType': "application/vnd.google-apps.folder",
        'parents': [parentId]
    };

    var accessToken = gapi.auth2.getAuthInstance().currentUser.get().getAuthResponse().access_token;
    // or this: gapi.auth.getToken().access_token;
    var ajax = new Ajax();
    ajax.postRequest('https://www.googleapis.com/drive/v3/files',
        JSON.stringify(data),
        function(responseText) {
            alert(responseText);
        },
        'text',
        'application/json',
        accessToken);
}

De prepareCreateFolder methode

Om een folder in een andere folder te kunnen maken moeten we eerst wat voorbereidend werk doen. In de getFilesInFolder methode hebben we een manier gevonden om te weten welke folder de gebruiker geselecteerd heeft door de id en de naam op te slaan in input elementen.

In de prepareCreateFolder halen we de id op van de getoonde folder evenals de naam van de nieuwe folder die gemaakt moet worden. We valideren de naam. De naam mag niet leeg zijn en mag alleen alfanumerieke tekens, een punt, een spatie of een underscore bevatten. Verder roepen we de methode doesFolderExist op om na te gaan als de foldernaam niet reeds bestaat. Als de 'root' geselecteerd is halen we alle bestanden op want dan mogen er geen twee mappen met dezelfde naam op de gehele Google Drive staan. Heeft de gebruiker een map geselecteerd halen we alleen de mappen in die geselecteerde submap op want dan kijken we alleen als er geen map met dezelfde naam in de submap bestaat. Het moet immers mogelijk zijn om in twee verschillende mappen een map met dezelfde naam te hebben.

/**
 * Prepare the creation of a new folder
 * Validate the foldername
 * Check if new foldername does already exist
 */
function prepareCreateFolder() {
    var folderName = document.getElementById('folderName').value;
    // haal de Id van de parent folder op
    var parentFolderId = document.getElementById('currentFolderId').value;
    var parentFolderName = document.getElementById('currentFolderName').value;

    if (folderName.length > 0) {
        var regExpresion = new RegExp("[^a-zA-Z0-9_. ]+");
        if (regExpresion.test(folderName)) {
            alert('Ongeldige foldernaam: ' + folderName)
        }
        else {
            // haal alle bestanden van de google drive op en verifiëer
            // als de folder al bestaat, daarvoor geven we de callback
            // functie doesFolderExist mee
            if (parentFolderId == 'root') {
                getFiles(function(responseText) {
                    doesFolderExist(responseText,
                        folderName, parentFolderId);
                });
            }
            else {
                // als de gebruiker een map geselecteerd heeft halen
                // we alleen de bestanden in de geselecteerde map op
                getFilesInFolder(parentFolderId,
                    parentFolderName,
                    function(responseText) {
                        doesFolderExist(responseText,
                            folderName, parentFolderId);
                    });
            }
        }
    }
    else {
        alert('Typ eerst een naam voor de folder in.');
    }
}

De doesFolderExist methode

Alvorens de map aan te maken verifiëert deze methode als de map al bestaat. Deze methode heeft drie parameters:

  1. responseText: JSON array met de naam van de bestanden in de root of in de geselecteerde folder;
  2. folderName: de naam van de te maken folder;
  3. parentFolderId: de id van de folder waarin de map gemaakt moet worden;
/**
 * Ga na als de folder al bestaat of niet.
 * Als de folder al bestaat stuur een boodschap dat de folder bestaat.
 * Als de folder in de root staat roepen we CreatFolder op
 * anders CreateFolderInParent
 *
 * @param {responseText} het antwoord van de server in tekstformaat
 * @param {folderName} de naam van de te maken folder
 * @param {parentFolderId} de id folder waarin de nieuwe map gemaakt moet worden
 */
var doesFolderExist = function(responseText, folderName, parentFolderId) {
    var response = JSON.parse(responseText);
    // check is the folder already exists
    // names not case sensitive
    if (response.files.some(function(item) {
            return item.name.toLowerCase() === folderName.toLowerCase()
        })) {
        alert('Folder met de naam ' + folderName + ' bestaat al!')
    }
    else {
        if (folderName == 'root') {
            createFolder(folderName);
        }
        else {
            createFolderInParent(folderName, parentFolderId);
        }
    }
}

Een bestand deleten

De deleteFile methode

Met deze methode kan je een bestand of folder op de Google Drive deleten. De REST API is zeer eenvoudig. Je geeft het werkwoord DELETE mee door de methode deleteRequest van de Ajax library op te roepen en de id van het bestand of folder die gedeleted moet worden. Deze methode wordt opgeroepen door de delete knop die naast elk bestand of folder in de html staat. Deze methode heeft twee parameters:

  1. id: de id van de te deleten folder of bestand;
  2. name: de naam van de te deleten folder of bestand; de naam hebben we nodig in de feedback aan de gebruiker;
/**
 * Drive API Helper function deleteFile
 * Delete a folder or a file based in it's Id
 * Permanently deletes a file owned by the user without moving it to the trash.
 * If the target is a folder, all descendants owned by the user are also deleted
 * https://developers.google.com/drive/v3/reference/files/delete
 *
 * @param id {string} the id of the item to be deleted
 * @param name {string} the name of the item to be deleted
 */
function deleteFile(id, name) {
    var ajax = new Ajax();
    // Google CORS
    var url = 'https://www.googleapis.com/drive/v3/files';
    // id
    url += '/' + id;
    var accessToken = gapi.auth2.getAuthInstance().currentUser.get().getAuthResponse().access_token;
    // or this: gapi.auth.getToken().access_token;
    ajax.deleteRequest(url,
        function(responseText) {
            alert(name + ' is gedeleted!');
        },
        accessToken);
}

De ingetypte tekst uploaden

Onze app moet in staat zijn om de tekst, die in de editor staat, up te loaden.

De prepareUploadText methode

Net zoals voor het maken van een nieuwe folder hebben we hier ook voorbereidend werk te doen:

/**
 * Drive API Helper function prepareUploadText
 * Get text out from editor
 * Validate file name and call uploadText method if file name is valid
 *
 */
function prepareUploadText() {
    // get the text from the editor
    var text = document.getElementById('editor').value;
    if (text.length > 0) {
        // get the file name
        // Eerst gedacht met parser to doen, maar dit is te streng
        // https://developer.mozilla.org/en-US/docs/Web/API/DOMParser
        // var parser = new DOMParser();
        // var doc = parser.parseFromString(text, "application/xml");
        // if (doc.getElementsByTagName('h1').length == 0) {
        var name = text.match(/<h1>(.*?)<\/h1>/g);
        if (!name) {
            alert('Je moet een h1 element toevoegen waarin de naam van het bestand staat!');
        }
        else {
            // we need the first match
            var fileName = name[0];
            // remove tags, dat kan waarschijnlijk eleganter...
            // maar ik heb hier al genoeg over moeten nadenken
            fileName = fileName.replace('<h1>', '');
            fileName = fileName.replace('</h1>', '');
            fileName = fileName.replace(/[^a-zA-Z0-9_. ]+/, '');
            // haal de Id van de parent folder op
            var parentFolderId = document.getElementById('currentFolderId').value;
            uploadText(fileName, text, parentFolderId);
        }
    }
    else {
        alert('Editor is leeg, kan geen leeg bestand uploaden...');
    }
}

De uploadText methode

Deze methode heeft me de meeste moeite gekost. We moeten eerst door hebben dat we twee soorten gegevens moeten doorsturen:

  1. metadata: de naam, mime type van het bestand;
  2. tekst: de inhoud van het bestand, de tekst die in de editor is ingetypt:

Om dit te kunnen doen moet je een multipart upload doen. Dat wil zeggen dat je de body van de request moet opdelen in twee delen met elk zijn eigen content type. De metadata is van het application/json type en de inhoud van de tekst is van het application/text type.

De body van de request moet heel precies de volgende indeling volgen (met lege lijnen inbegrepen). Uit het voorbeeld van Google:

--foo_bar_baz
Content-Type: application/json; charset=UTF-8

{ "name": "My File" }

--foo_bar_baz 
Content-Type: image/jpeg

JPEG data
--foo_bar_baz--

In ons voorbeeld genereert de volgende JavaScript exact hetzelfde patroon. Let erop dat je identiek hetzelfde patroon volgt:

var metaData = {
        'name': name + '.html',
        'parents': [parentFolderId]
    };

    var data = '--next_section\r\n' +
        'Content-Type: application/json; charset=UTF-8\r\n\r\n' +
        JSON.stringify(metaData) +
        '\r\n\r\n--next_section\r\n' +
        'Content-Type: application/text\r\n\r\n' +
        text +
        '\r\n--next_section--';

Verder moet je de Content-Type header in je XMLHttpRequest instellen op multipart/related; boundary=next_section zodat de server weet in welke indeling de body van de request is opgesteld. De naam next_section heb ik zelf gekozen, dat is de naam die zelf kiest om het begin en einde van elk onderdeel in de body van de request aan te duiden. Meer info op The Multipart Content-Type pagina van w3.org. Tenslotte voeg je een uploadType parameter toe op de querystring waarvan je de waarde instelt op multipart zoat de Google API weet dat je body zowel metadata als inhoud bevat.

/**
 * Drive API Helper function uploadText
 * https://developers.google.com/drive/v3/web/manage-uploads#multipart
 * Simple upload: uploadType=media.
 * For quick transfer of smaller files, for example, 5 MB or less.
 *
 * @param name {string} the file name
 * @param text {string} the html content of the file
 * @param parentFolderId {string} the id of the parent folder of file
 */
function uploadText(name, text, parentFolderId) {

    var metaData = {
        'name': name + '.html',
        'parents': [parentFolderId]
    };

    var data = '--next_section\r\n' +
        'Content-Type: application/json; charset=UTF-8\r\n\r\n' +
        JSON.stringify(metaData) +
        '\r\n\r\n--next_section\r\n' +
        'Content-Type: application/text\r\n\r\n' +
        text +
        '\r\n--next_section--';

    var accessToken = gapi.auth2.getAuthInstance().currentUser.get().getAuthResponse().access_token;
    // or this: gapi.auth.getToken().access_token;
    var ajax = new Ajax();
    ajax.postRequest('https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart',
        data,
        function(responseText) {
            alert(responseText);
        },
        'text',
        'multipart/related; boundary=next_section',
        accessToken);
}

Een html bestand vanop de Google Drive openen in de editor

Deze methode is eenvoudig. We maken een GET request en geven de id van het te openen bestand op de url mee. In de callback stoppen we de geretourneerde tekst in het HTML element met id="editor".

/**
 * Drive API Helper function uploadText
 * https://developers.google.com/drive/v3/web/manage-downloads
 * Simple upload: uploadType=media.
 * For quick transfer of smaller files, for example, 5 MB or less.
 *
 * @param id {string} the file id
 * @param name {string} the name of the file
 */
function downloadText(id, name) {
    var ajax = new Ajax();
    // Google CORS
    var url = 'https://www.googleapis.com/drive/v3/files';
    // id
    url += '/' + id;
    // download
    url += '?alt=media';
    var accessToken = gapi.auth2.getAuthInstance().currentUser.get().getAuthResponse().access_token;
    // or this: gapi.auth.getToken().access_token;
    ajax.getRequest(url,
        function(responseText) {
            document.getElementById('editor').innerHTML = responseText;
        },
        'text',
        accessToken);
}

Filmpje

  1. CORS toegang tot Google Drive API met de ajax bibliotheek 1
  2. CORS toegang tot Google Drive API met de ajax bibliotheek 2

Bron

How to use CORS to access Google APIs

JI
2016-11-10 22:30:31