Google Drive como almacenamiento para una aplicaci贸n web

Pr贸logo


Mi aplicaci贸n web est谩 almacenando datos en localStorage . Esto era conveniente hasta que quer铆a que el usuario viera lo mismo al acceder al sitio desde diferentes dispositivos. Es decir, se requer铆a almacenamiento remoto.

Pero la aplicaci贸n est谩 "alojada" en las p谩ginas de GitHub y no tiene una parte de servidor. Decid铆 no hacer un servidor, sino almacenar los datos con un tercero. Esto ofrece ventajas significativas:

  1. No es necesario pagar por el servidor, no perjudica a la cabeza por su estabilidad y disponibilidad.
  2. Menos c贸digo, menos errores.
  3. El usuario no necesita registrarse en mi aplicaci贸n (esto es molesto para muchos).
  4. La privacidad es mayor, y el usuario sabe que sus datos se almacenan en un lugar en el que probablemente conf铆a m谩s que yo.

Primero, la elecci贸n recay贸 en remoteStorage.js . Ofrecen un protocolo de intercambio de datos abierto, una API bastante agradable, la capacidad de integrarse con Google Drive y Dropbox, as铆 como con sus servidores. Pero este camino result贸 ser un callej贸n sin salida (por qu茅, una historia separada).

Al final, decid铆 usar Google Drive directamente y la Biblioteca de cliente API de Google (en adelante, GAPI) como biblioteca para acceder a ella.

Desafortunadamente, la documentaci贸n de Google es decepcionante, y la biblioteca GAPI parece inacabada, adem谩s, tiene varias versiones, y no siempre est谩 claro cu谩l est谩 en cuesti贸n. Por lo tanto, la soluci贸n a mis problemas tuvo que recopilarse en partes de la documentaci贸n, preguntas y respuestas en StackOverflow y publicaciones aleatorias en Internet.

Espero que este art铆culo le ahorre tiempo si decide usar Google Drive en su aplicaci贸n.

Preparaci贸n


La siguiente es una descripci贸n de c贸mo obtener claves para trabajar con la API de Google. Si no est谩 interesado, vaya directamente a la siguiente parte.

Recibiendo llaves
En Google Developer Console, cree un nuevo proyecto, ingrese un nombre.

En el "Panel de control", haga clic en "Habilitar API y servicios" y active Google Drive.

A continuaci贸n, vaya a la secci贸n API y Servicios -> Credenciales, haga clic en "Crear credenciales". Hay tres cosas que debes hacer:

  1. Configure la "Ventana de solicitud de acceso de OAuth". Ingrese el nombre de la aplicaci贸n, su dominio en la secci贸n "Dominios autorizados" y un enlace a la p谩gina principal de la aplicaci贸n. Otros campos son opcionales.
  2. En la secci贸n "Credenciales", haga clic en "Crear credenciales" -> "Identificador de cliente OAuth". Seleccione el tipo "Aplicaci贸n web". En la ventana de configuraci贸n, agregue "Fuentes de Javascript permitidas" y "URI de redireccionamiento permitidos":
    • Tu dominio (requerido)
    • http://localhost:8000 (opcional para trabajar localmente).


  3. En la secci贸n "Credenciales", haga clic en "Crear credenciales" -> "Clave API". En la configuraci贸n clave, especifique las restricciones:
    • Tipo de aplicaci贸n permitida -> referencias HTTP (sitios web)
    • Acepte solicitudes http de las siguientes fuentes de referencia (sitios) -> su dominio y localhost (como en el punto 2).
    • API v谩lidas -> API de Google Drive



La secci贸n de Credenciales deber铆a verse as铆:



Aqu铆 hemos terminado. Pasamos al c贸digo.

Inicializaci贸n e inicio de sesi贸n


La forma recomendada de Google para habilitar GAPI es pegar el siguiente c贸digo en su 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> 

Despu茅s de cargar la biblioteca, se initClient funci贸n initClient , que debemos escribir nosotros mismos. Su apariencia t铆pica es la siguiente:

 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'}) }) } 

Para el almacenamiento de datos, utilizaremos la llamada carpeta de datos de la aplicaci贸n . Sus ventajas sobre una carpeta normal:

  1. El usuario no lo ve directamente: los archivos que contiene no obstruyen su espacio personal y no puede arruinar nuestros datos.
  2. Otras aplicaciones no lo ven y tampoco pueden estropearlo.
  3. Scope, mencionado anteriormente, le da acceso a la aplicaci贸n, pero no le da acceso al resto de los archivos del usuario. Es decir, no asustaremos a una persona mediante solicitudes de acceso a sus datos personales.

Tras la inicializaci贸n exitosa de la API de Google, la funci贸n hace lo siguiente:

  1. Comienza a capturar eventos de inicio / cierre de sesi贸n; lo m谩s probable es que esto siempre se haga.
  2. Inicializa la aplicaci贸n. Esto se puede hacer antes de cargar e inicializar GAPI, como prefiera. Mi procedimiento de inicializaci贸n fue ligeramente diferente si Google no est谩 disponible. Alguien puede decir que esto no sucede :) Pero, en primer lugar, puede ser inteligente con las claves y los derechos de acceso en el futuro. En segundo lugar, por ejemplo, en China, Google est谩 prohibido.

Iniciar y cerrar sesi贸n se realiza simplemente:

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

Recibir谩 resultados de inicio de sesi贸n en el controlador onSignIn :

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

Desafortunadamente, trabajar con archivos no es tan obvio.

Promesa de ayuda


GAPI no devuelve las promesas normales. En cambio, se utiliza su propia interfaz Thennable, que es similar a las promesas, pero no del todo. Por lo tanto, para la comodidad del trabajo (principalmente para usar async/await ), haremos un peque帽o ayudante:

 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) }) }) } 

Esta funci贸n toma el m茅todo GAPI y los par谩metros como el primer argumento y devuelve Promise. Entonces ver谩s c贸mo usarlo.

Trabajar con archivos


Siempre debe recordar que el nombre del archivo en Google Drive no es 煤nico . Puede crear cualquier cantidad de archivos y carpetas con el mismo nombre. Solo el identificador es 煤nico.
Para tareas b谩sicas, no necesita trabajar con carpetas, por lo que todas las funciones a continuaci贸n funcionan con archivos en la ra铆z de la carpeta Datos de la aplicaci贸n. Los comentarios indican qu茅 se debe cambiar para trabajar con carpetas. La documentaci贸n de Google est谩 aqu铆 .

Crea un archivo vac铆o


 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 } 

Esta funci贸n asincr贸nica crea un archivo vac铆o y devuelve su identificador (cadena). Si dicho archivo ya existe, se crear谩 un nuevo archivo con el mismo nombre y se devolver谩 su ID. Si no desea esto, primero debe verificar que no haya ning煤n archivo con el mismo nombre (ver m谩s abajo).
Google Drive no es una base de datos completa. Por ejemplo, si desea que varios usuarios trabajen desde la misma cuenta de Google simult谩neamente desde diferentes dispositivos, puede haber problemas para resolver conflictos debido a la falta de transacciones. Para tales tareas, es mejor no usar Google Drive.

Trabajar con contenido de archivo


GAPI (para JavaScript basado en navegador) no proporciona m茅todos para trabajar con el contenido de los archivos (muy extra帽o, 驴no?). En cambio, hay un m茅todo de request general (un envoltorio delgado sobre una solicitud AJAX simple).

A trav茅s de prueba y error, llegu茅 a las siguientes implementaciones:

 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 } 

B煤squeda de archivos


 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 } 

Esta funci贸n, si no especifica la query , devuelve todos los archivos en la carpeta de la aplicaci贸n (una matriz de objetos con campos de id y name ), ordenados por hora de creaci贸n.

Si especifica la cadena de query (la sintaxis se describe aqu铆 ), solo devolver谩 archivos que coincidan con la consulta. Por ejemplo, para verificar si config.json un archivo llamado config.json , debe hacer

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

Eliminar archivos


 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 } } 

Esta funci贸n elimina el archivo por ID y devuelve true si se elimin贸 con 茅xito, y false si no exist铆a dicho archivo.

Sincronizaci贸n


Es aconsejable que el programa trabaje principalmente con localStorage , y Google Drive se usa solo para sincronizar datos de localStorage .

La siguiente es una estrategia de sincronizaci贸n de configuraci贸n simple:

  1. La nueva configuraci贸n se descarga de Google Drive con un inicio de sesi贸n, y luego cada 3 minutos, sobrescribiendo la copia local;
  2. Los cambios locales se vierten en Google Drive, sobrescribiendo lo que estaba all铆;
  3. el fileID configuraci贸n se almacena en cach茅 en localStorage para acelerar el trabajo y reducir el n煤mero de solicitudes;
  4. Las situaciones manejadas correctamente (err贸neas) son cuando Google Drive tiene varios archivos de configuraci贸n y cuando alguien elimin贸 nuestro archivo de configuraci贸n o lo arruin贸.
  5. Los detalles de sincronizaci贸n no afectan el resto del c贸digo de la aplicaci贸n. Para trabajar con la configuraci贸n, utiliza solo dos funciones: getConfig() y saveConfig(newConfig) .

En una aplicaci贸n real, probablemente desee implementar un manejo de conflictos m谩s flexible al cargar / descargar una configuraci贸n.

Ver c贸digo
 //     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() } 


Conclusi贸n


Me parece que el almac茅n de datos para un sitio web en Google Drive es ideal para peque帽os proyectos y creaci贸n de prototipos. No solo es f谩cil de implementar y soportar, sino que tambi茅n ayuda a reducir la cantidad de entidades innecesarias en el universo. Y espero que mi art铆culo lo ayude a ahorrar tiempo si elige este camino.

PD: El c贸digo del proyecto real se encuentra en GitHub , puedes probarlo aqu铆 .

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


All Articles