Google Drive comme stockage pour une application Web

Préface


Mon application Web stocke des donnĂ©es dans localStorage . C'Ă©tait pratique jusqu'Ă  ce que je veuille que l'utilisateur voit la mĂȘme chose lorsqu'il accĂšde au site Ă  partir de diffĂ©rents appareils. Autrement dit, un stockage Ă  distance Ă©tait nĂ©cessaire.

Mais l'application est "hébergée" sur les pages GitHub et n'a pas de partie serveur. J'ai décidé de ne pas créer de serveur, mais de stocker les données avec un tiers. Cela donne des avantages importants:

  1. Pas besoin de payer pour le serveur, cela ne fait pas de mal Ă  la tĂȘte quant Ă  sa stabilitĂ© et sa disponibilitĂ©.
  2. Moins de code, moins d'erreurs.
  3. L'utilisateur n'a pas besoin de s'inscrire dans mon application (c'est gĂȘnant pour beaucoup).
  4. La confidentialité est plus élevée et l'utilisateur sait que ses données sont stockées dans un endroit auquel il fait probablement plus confiance que moi.

Tout d'abord, le choix s'est portĂ© sur remoteStorage.js . Ils offrent un protocole d'Ă©change de donnĂ©es ouvert, une trĂšs jolie API, la possibilitĂ© de s'intĂ©grer Ă  Google Drive et Dropbox, ainsi qu'Ă  leurs serveurs. Mais ce chemin s'est avĂ©rĂ© ĂȘtre une impasse (pourquoi - une histoire distincte).

Au final, j'ai décidé d'utiliser directement Google Drive et la bibliothÚque cliente Google API (ci-aprÚs GAPI) comme bibliothÚque pour y accéder.

Malheureusement, la documentation de Google est dĂ©cevante, et la bibliothĂšque GAPI semble inachevĂ©e, d'ailleurs, elle a plusieurs versions, et on ne sait pas toujours laquelle est en cause. Par consĂ©quent, la solution Ă  mes problĂšmes devait ĂȘtre collectĂ©e en morceaux Ă  partir de la documentation, des questions et des rĂ©ponses sur StackOverflow et des publications alĂ©atoires sur Internet.

J'espÚre que cet article vous fera gagner du temps si vous décidez d'utiliser Google Drive dans votre application.

La préparation


Voici une description de la façon d'obtenir des clĂ©s pour travailler avec l'API Google. Si vous n'ĂȘtes pas intĂ©ressĂ©, passez directement Ă  la partie suivante.

Réception des clés
Dans la console développeur de Google, créez un nouveau projet, entrez un nom.

Dans le "Panneau de configuration", cliquez sur "Activer l'API et les services" et activez Google Drive.

Ensuite, accédez à la section API et services -> Informations d'identification, cliquez sur "Créer des informations d'identification". Il y a trois choses que vous devez faire:

  1. Configurez la "fenĂȘtre de demande d'accĂšs OAuth". Entrez le nom de l'application, votre domaine dans la section "Domaines autorisĂ©s" et un lien vers la page principale de l'application. Les autres champs sont facultatifs.
  2. Dans la section "Informations d'identification", cliquez sur "CrĂ©er des informations d'identification" -> "Identifiant client OAuth". SĂ©lectionnez le type «Application Web». Dans la fenĂȘtre des paramĂštres, ajoutez "Sources Javascript autorisĂ©es" et "URI de redirection autorisĂ©es":
    • Votre domaine (obligatoire)
    • http://localhost:8000 (facultatif pour travailler localement).


  3. Dans la section "Informations d'identification", cliquez sur "Créer des informations d'identification" -> "Clé API". Dans les paramÚtres clés, spécifiez les restrictions:
    • Type d'application autorisĂ© -> rĂ©fĂ©rents HTTP (sites Web)
    • Acceptez les demandes http provenant des sources de rĂ©fĂ©rence (sites) suivantes -> votre domaine et votre hĂŽte local (comme au point 2).
    • API valides -> API Google Drive



La section des informations d'identification doit ressembler Ă  ceci:



Nous y voilĂ . Nous passons au code.

Initialisation et connexion


La méthode recommandée par Google pour activer GAPI est de coller le code suivant dans votre code HTML:

 <script src="https://apis.google.com/js/api.js" onload="this.onload=function(){}; gapi.load('client:auth2', initClient)" onreadystatechange="if (this.readyState === 'complete') this.onload()"> </script> 

AprĂšs avoir chargĂ© la bibliothĂšque, la fonction initClient sera appelĂ©e, que nous devons Ă©crire nous-mĂȘmes. Son aspect typique est le suivant:

 function initClient() { gapi.client.init({ //   API apiKey: GOOGLE_API_KEY, //    clientId: GOOGLE_CLIENT_ID, // ,     Google Drive API v3 discoveryDocs: ['https://www.googleapis.com/discovery/v1/apis/drive/v3/rest'], //    application data folder (. ) scope: 'https://www.googleapis.com/auth/drive.appfolder' }).then(() => { //    / (. ) gapi.auth2.getAuthInstance().isSignedIn.listen(onSignIn) //   initApp() }, error => { console.log('Failed to init GAPI client', error) //    initApp({showAlert: 'google-init-failed-alert'}) }) } 

Pour le stockage des données, nous utiliserons le dossier des données d'application . Ses avantages par rapport à un dossier ordinaire:

  1. L'utilisateur ne le voit pas directement: les fichiers qu'il contient n'obstruent pas son espace personnel et il ne peut pas ruiner nos données.
  2. D'autres applications ne le voient pas et ne peuvent pas non plus le gĂącher.
  3. La portée, mentionnée ci-dessus, donne accÚs à l'application, mais ne donne pas accÚs au reste des fichiers de l'utilisateur. Autrement dit, nous n'effrayerons pas une personne par des demandes d'accÚs à ses données personnelles.

Une fois l'initialisation de l'API Google réussie, la fonction effectue les opérations suivantes:

  1. Commence Ă  attraper les Ă©vĂ©nements de connexion / dĂ©connexion - trĂšs probablement, cela devrait toujours ĂȘtre fait.
  2. Initialise l'application. Cela peut ĂȘtre fait avant de charger et d'initialiser GAPI - comme vous prĂ©fĂ©rez. Ma procĂ©dure d'initialisation Ă©tait lĂ©gĂšrement diffĂ©rente si Google n'Ă©tait pas disponible. Quelqu'un peut dire que cela ne se produit pas :) Mais, tout d'abord, vous pouvez ĂȘtre intelligent avec les clĂ©s et les droits d'accĂšs Ă  l'avenir. DeuxiĂšmement, par exemple, en Chine, Google est interdit.

La connexion et la déconnexion se font simplement:

 function isGapiLoaded() { return gapi && gapi.auth2 } function logIn() { if (isGapiLoaded()) { //    Google    gapi.auth2.getAuthInstance().signIn() } } function logOut() { if (isGapiLoaded()) { gapi.auth2.getAuthInstance().signOut() } } 

Vous recevrez les résultats de connexion dans le gestionnaire onSignIn :

 function isLoggedIn() { return isGapiLoaded() && gapi.auth2.getAuthInstance().isSignedIn.get() } function onSignIn() { if (isLoggedIn()) { //   } else { //   } //   .    "" } 

Malheureusement, travailler avec des fichiers n'est pas si évident.

Aide Ă  la promesse


GAPI ne renvoie pas de promesses normales. Au lieu de cela, sa propre interface Thennable est utilisée, ce qui est similaire aux promesses, mais pas tout à fait. Par conséquent, pour la commodité du travail (principalement pour utiliser async/await ), nous ferons un petit assistant:

 function prom(gapiCall, argObj) { return new Promise((resolve, reject) => { gapiCall(argObj).then(resp => { if (resp && (resp.status < 200 || resp.status > 299)) { console.log('GAPI call returned bad status', resp) reject(resp) } else { resolve(resp) } }, err => { console.log('GAPI call failed', err) reject(err) }) }) } 

Cette fonction prend la méthode GAPI et les paramÚtres comme premier argument et renvoie Promise. Ensuite, vous verrez comment l'utiliser.

Travailler avec des fichiers


Vous devez toujours vous rappeler que le nom de fichier sur Google Drive n'est pas unique . Vous pouvez crĂ©er un nombre illimitĂ© de fichiers et de dossiers portant le mĂȘme nom. Seul l'identifiant est unique.
Pour les tĂąches de base, vous n'avez pas besoin de travailler avec des dossiers, donc toutes les fonctions ci-dessous fonctionnent avec des fichiers Ă  la racine du dossier Application Data. Les commentaires indiquent ce qui doit ĂȘtre modifiĂ© pour fonctionner avec les dossiers. La documentation de Google est ici .

Créer un fichier vide


 async function createEmptyFile(name, mimeType) { const resp = await prom(gapi.client.drive.files.create, { resource: { name: name, //     // mimeType = 'application/vnd.google-apps.folder' mimeType: mimeType || 'text/plain', //  'appDataFolder'   ID  parents: ['appDataFolder'] }, fields: 'id' }) //    —    return resp.result.id } 

Cette fonction asynchrone crĂ©e un fichier vide et retourne son identifiant (chaĂźne). Si un tel fichier existe dĂ©jĂ , un nouveau fichier du mĂȘme nom sera créé et son ID sera retournĂ©. Si vous ne le souhaitez pas, vous devez d'abord vĂ©rifier qu'il n'y a pas de fichier du mĂȘme nom (voir ci-dessous).
Google Drive n'est pas une base de donnĂ©es complĂšte. Par exemple, si vous souhaitez que plusieurs utilisateurs travaillent simultanĂ©ment Ă  partir du mĂȘme compte Google Ă  partir de diffĂ©rents appareils, il peut y avoir des problĂšmes de rĂ©solution des conflits en raison du manque de transactions. Pour de telles tĂąches, il est prĂ©fĂ©rable de ne pas utiliser Google Drive.

Travailler avec le contenu des fichiers


GAPI (pour JavaScript basĂ© sur un navigateur) ne fournit pas de mĂ©thodes pour travailler avec le contenu des fichiers (trĂšs Ă©trange, n'est-ce pas?). Au lieu de cela, il existe une mĂ©thode de request gĂ©nĂ©rale (un wrapper fin sur une simple requĂȘte AJAX).

Par essais et erreurs, je suis arrivé aux implémentations suivantes:

 async function upload(fileId, content) { //    ,  ,     JSON return prom(gapi.client.request, { path: `/upload/drive/v3/files/${fileId}`, method: 'PATCH', params: {uploadType: 'media'}, body: typeof content === 'string' ? content : JSON.stringify(content) }) } async function download(fileId) { const resp = await prom(gapi.client.drive.files.get, { fileId: fileId, alt: 'media' }) // resp.body      // resp.result —    resp.body  JSON. //   ,  resp.result  false // ..    ,   return resp.result || resp.body } 

Recherche de fichiers


 async function find(query) { let ret = [] let token do { const resp = await prom(gapi.client.drive.files.list, { //  'appDataFolder'   ID  spaces: 'appDataFolder', fields: 'files(id, name), nextPageToken', pageSize: 100, pageToken: token, orderBy: 'createdTime', q: query }) ret = ret.concat(resp.result.files) token = resp.result.nextPageToken } while (token) // :    [{id: '...', name: '...'}], //     return ret } 

Cette fonction, si vous ne spécifiez pas de query , renvoie tous les fichiers du dossier d'application (un tableau d'objets avec des champs id et name ), triés par heure de création.

Si vous spĂ©cifiez la chaĂźne de query (la syntaxe est dĂ©crite ici ), elle renverra uniquement les fichiers correspondant Ă  la requĂȘte. Par exemple, pour vĂ©rifier si un fichier nommĂ© config.json , vous devez faire

  if ((await find('name = "config.json"')).length > 0) { // ()  } 

Suppression de fichiers


 async function deleteFile(fileId) { try { await prom(gapi.client.drive.files.delete, { fileId: fileId }) return true } catch (err) { if (err.status === 404) { return false } throw err } } 

Cette fonction supprime le fichier par ID et renvoie true s'il a été supprimé avec succÚs et false s'il n'y avait pas un tel fichier.

Sync


Il est conseillé que le programme fonctionne principalement avec localStorage , et Google Drive est utilisé uniquement pour synchroniser les données de localStorage .

Voici une stratégie de synchronisation de configuration simple:

  1. La nouvelle configuration est téléchargée à partir de Google Drive avec un identifiant, puis toutes les 3 minutes, en écrasant la copie locale;
  2. Les modifications locales sont versées sur Google Drive, écrasant ce qui s'y trouvait;
  3. le fileID configuration fileID est mis en cache dans localStorage pour accélérer le travail et réduire le nombre de demandes;
  4. Les situations correctement gérées (erronées) sont lorsque Google Drive possÚde plusieurs fichiers de configuration et que quelqu'un a supprimé notre fichier de configuration ou l'a ruiné.
  5. Les détails de la synchronisation n'affectent pas le reste du code d'application. Pour travailler avec la configuration, vous n'utilisez que deux fonctions: getConfig() et saveConfig(newConfig) .

Dans une application réelle, vous souhaiterez probablement implémenter une gestion des conflits plus flexible lors du chargement / déchargement d'une configuration.

Afficher le code
 //     const SYNC_PERIOD = 1000 * 60 * 3 // 3  //    const DEFAULT_CONFIG = { // ... } //  ID  ,      let configSyncTimeoutId async function getConfigFileId() { //  configFileId let configFileId = localStorage.getItem('configFileId') if (!configFileId) { //     Google Drive const configFiles = await find('name = "config.json"') if (configFiles.length > 0) { //   (  )  configFileId = configFiles[0].id } else { //   configFileId = await createEmptyFile('config.json') } //  ID localStorage.setItem('configFileId', configFileId) } return configFileId } async function onSignIn() { //   / (. ) if (isLoggedIn()) { //   //  (  -?)    scheduleConfigSync(0) } else { //   //          //   config file ID localStorage.removeItem('configFileId') //  localStorage   ,    } } function getConfig() { let ret try { ret = JSON.parse(localStorage.getItem('config')) } catch(e) {} //    ,    return ret || {...DEFAULT_CONFIG} } async function saveConfig(newConfig) { //    ,     localStorage.setItem('config', JSON.stringify(newConfig)) if (isLoggedIn()) { //  config file ID const configFileId = await getConfigFileId() //     Google Drive upload(configFileId, newConfig) } } async function syncConfig() { if (!isLoggedIn()) { return } //  config file ID const configFileId = await getConfigFileId() try { //   const remoteConfig = await download(configFileId) if (!remoteConfig || typeof remoteConfig !== 'object') { //    ,   upload(configFileId, getConfig()) } else { //  ,    localStorage.setItem('config', JSON.stringify(remoteConfig)) } //  ,  localStorage   } catch(e) { if (e.status === 404) { // -   ,   fileID     localStorage.removeItem('configFileId') syncConfig() } else { throw e } } } function scheduleConfigSync(delay) { //   ,    if (configSyncTimeoutId) { clearTimeout(configSyncTimeoutId) } configSyncTimeoutId = setTimeout(() => { //      syncConfig() .catch(e => console.log('Failed to synchronize config', e)) .finally(() => scheduleSourcesSync()) }, typeof delay === 'undefined' ? SYNC_PERIOD : delay) } function initApp() { //      scheduleConfigSync() } 


Conclusion


Il me semble que le magasin de donnĂ©es d'un site Web sur Google Drive est idĂ©al pour les petits projets et le prototypage. Il est non seulement facile Ă  mettre en Ɠuvre et Ă  prendre en charge, mais contribue Ă©galement Ă  rĂ©duire le nombre d'entitĂ©s inutiles dans l'univers. Et mon article, je l'espĂšre, vous aidera Ă  gagner du temps si vous choisissez ce chemin.

PS Le code du vrai projet se trouve sur GitHub , vous pouvez l' essayer ici .

Source: https://habr.com/ru/post/fr440844/


All Articles