Google Drive como armazenamento para um aplicativo Web

Prefácio


Meu aplicativo da web está armazenando dados no localStorage . Isso foi conveniente até que eu quisesse que o usuário visse a mesma coisa ao acessar o site a partir de dispositivos diferentes. Ou seja, era necessário armazenamento remoto.

Mas o aplicativo está "hospedado" nas páginas do GitHub e não possui uma parte do servidor. Decidi não criar um servidor, mas armazenar os dados com terceiros. Isso oferece vantagens significativas:

  1. Não há necessidade de pagar pelo servidor, isso não prejudica sua estabilidade e disponibilidade.
  2. Menos código, menos erros.
  3. O usuário não precisa se registrar no meu aplicativo (isso é irritante para muitos).
  4. A privacidade é maior e o usuário sabe que seus dados são armazenados em um local em que ele provavelmente confia mais que eu.

Primeiro, a escolha caiu no remoteStorage.js . Eles oferecem um protocolo aberto de troca de dados, uma API bastante agradável, a capacidade de integrar-se ao Google Drive e Dropbox, além de seus servidores. Mas esse caminho acabou sendo um beco sem saída (por que - uma história separada).

No final, decidi usar o Google Drive diretamente e a Biblioteca do cliente da API do Google (doravante GAPI) como uma biblioteca para acessá-lo.

Infelizmente, a documentação do Google é decepcionante e a biblioteca GAPI parece inacabada, além disso, possui várias versões e nem sempre é claro qual delas está em questão. Portanto, a solução para meus problemas teve que ser coletada em partes da documentação, perguntas e respostas no StackOverflow e postagens aleatórias na Internet.

Espero que este artigo poupe tempo se você decidir usar o Google Drive em seu aplicativo.

Preparação


A seguir, é apresentada uma descrição de como obter chaves para trabalhar com a API do Google. Se você não estiver interessado, vá direto para a próxima parte.

Recebendo chaves
No Google Developer Console, crie um novo projeto, digite um nome.

No "Painel de controle", clique em "Ativar API e serviços" e ative o Google Drive.

Em seguida, vá para a seção API e serviços -> Credenciais, clique em "Criar credenciais". Há três coisas que você precisa fazer:

  1. Configure "Janela OAuth Access Request". Digite o nome do aplicativo, seu domínio na seção "Domínios autorizados" e um link para a página principal do aplicativo. Outros campos são opcionais.
  2. Na seção "Credenciais", clique em "Criar credenciais" -> "Identificador de cliente OAuth". Selecione o tipo "Aplicativo da Web". Na janela de configurações, adicione "Fontes de Javascript permitidas" e "URIs de redirecionamento permitido":
    • Seu domínio (obrigatório)
    • http://localhost:8000 (opcional para trabalhar localmente).


  3. Na seção "Credenciais", clique em "Criar credenciais" -> "Chave da API". Nas configurações de chave, especifique as restrições:
    • Tipo de aplicativo permitido -> referenciadores HTTP (sites)
    • Aceite solicitações http das seguintes fontes de referência (sites) -> seu domínio e host local (como no ponto 2).
    • APIs válidas -> API do Google Drive



A seção Credenciais deve se parecer com isso:



Aqui terminamos. Passamos para o código.

Inicialização e login


A maneira recomendada pelo Google de ativar o GAPI é colar o seguinte código no seu 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> 

Depois de carregar a biblioteca, a função initClient será chamada, a qual devemos escrever por nós mesmos. Sua aparência típica é a seguinte:

 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 armazenamento de dados, usaremos a chamada pasta Application Data . Suas vantagens sobre uma pasta comum:

  1. O usuário não o vê diretamente: os arquivos dele não obstruem seu espaço pessoal e não podem arruinar nossos dados.
  2. Outras aplicações não o veem e também não podem estragá-lo.
  3. O escopo, mencionado acima, concede ao aplicativo acesso a ele, mas não dá acesso ao restante dos arquivos do usuário. Ou seja, não assustaremos uma pessoa com pedidos de acesso aos seus dados pessoais.

Após a inicialização bem-sucedida da API do Google, a função faz o seguinte:

  1. Começa a capturar eventos de logon / logout - provavelmente, isso sempre deve ser feito.
  2. Inicializa o aplicativo. Isso pode ser feito antes de carregar e inicializar o GAPI - como você preferir. Meu procedimento de inicialização foi um pouco diferente se o Google não estiver disponível. Alguém pode dizer que isso não acontece :) Mas, em primeiro lugar, você pode ser inteligente com chaves e direitos de acesso no futuro. Em segundo lugar, por exemplo, na China, o Google é proibido.

O login e o logout são feitos simplesmente:

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

Você receberá resultados de login no manipulador onSignIn :

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

Infelizmente, trabalhar com arquivos não é tão óbvio.

Promise helper


O GAPI não retorna promessas normais. Em vez disso, é usada sua própria interface Thennable, que é semelhante às promessas, mas não exatamente. Portanto, para a conveniência do trabalho (principalmente para usar async/await ), faremos um pequeno ajudante:

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

Essa função leva o método GAPI e os parâmetros para ele como o primeiro argumento e retorna Promise. Então você verá como usá-lo.

Trabalhar com arquivos


Lembre-se sempre de que o nome do arquivo no Google Drive não é exclusivo . Você pode criar qualquer número de arquivos e pastas com o mesmo nome. Somente o identificador é único.
Para tarefas básicas, você não precisa trabalhar com pastas; portanto, todas as funções abaixo funcionam com arquivos na raiz da pasta Dados do Aplicativo. Os comentários indicam o que precisa ser alterado para funcionar com pastas. A documentação do Google está aqui .

Crie um arquivo vazio


 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 } 

Essa função assíncrona cria um arquivo vazio e retorna seu identificador (string). Se esse arquivo já existir, um novo arquivo com o mesmo nome será criado e seu ID será retornado. Se você não quiser isso, verifique primeiro se não há arquivos com o mesmo nome (veja abaixo).
O Google Drive não é um banco de dados completo. Por exemplo, se você deseja que vários usuários trabalhem da mesma conta do Google simultaneamente a partir de dispositivos diferentes, pode haver problemas com a resolução de conflitos devido à falta de transações. Para essas tarefas, é melhor não usar o Google Drive.

Trabalhar com o conteúdo do arquivo


O GAPI (para JavaScript baseado em navegador) não fornece métodos para trabalhar com o conteúdo dos arquivos (muito estranho, não é?). Em vez disso, existe um método de request geral (um invólucro fino sobre uma solicitação AJAX simples).

Por tentativa e erro, cheguei às seguintes implementações:

 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 } 

Pesquisa de arquivo


 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 } 

Essa função, se você não especificar a query , retornará todos os arquivos na pasta do aplicativo (uma matriz de objetos com campos de id e name ), classificados por hora de criação.

Se você especificar a string de query (a sintaxe é descrita aqui ), ela retornará apenas os arquivos que correspondem à consulta. Por exemplo, para verificar se config.json um arquivo chamado config.json , é necessário fazer

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

Excluindo arquivos


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

Essa função exclui o arquivo por ID e retorna true se ele foi excluído com êxito e false se não houver esse arquivo.

Sincronizar


É recomendável que o programa funcione principalmente com o localStorage , e o Google Drive seja usado apenas para sincronizar dados do localStorage .

A seguir, é apresentada uma estratégia simples de sincronização da configuração:

  1. A nova configuração é baixada do Google Drive com um login e, a cada 3 minutos, substituindo a cópia local;
  2. As alterações locais são lançadas no Google Drive, substituindo o que estava lá;
  3. o fileID configuração é armazenado em cache no localStorage para acelerar o trabalho e reduzir o número de solicitações;
  4. As situações tratadas corretamente (incorretamente) são quando o Google Drive possui vários arquivos de configuração e quando alguém exclui ou arruina nosso arquivo de configuração.
  5. Os detalhes da sincronização não afetam o restante do código do aplicativo. Para trabalhar com a configuração, você usa apenas duas funções: getConfig() e saveConfig(newConfig) .

Em um aplicativo real, você provavelmente deseja implementar uma manipulação de conflitos mais flexível ao carregar / descarregar uma configuração.

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


Conclusão


Parece-me que o armazenamento de dados de um site no Google Drive é ótimo para pequenos projetos e protótipos. Não é apenas fácil de implementar e apoiar, mas também ajuda a reduzir o número de entidades desnecessárias no universo. E espero que meu artigo o ajude a economizar tempo se você escolher esse caminho.

PS O código do projeto real está no GitHub , você pode experimentá-lo aqui .

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


All Articles