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