Google云端硬盘作为网络应用程序的存储

前言


我的Web应用程序将数据存储在localStorage 。 这很方便,直到我希望用户从不同设备访问该站点时看到相同的东西。 即,需要远程存储。

但是该应用程序“托管”在GitHub Pages上,并且没有服务器部分。 我决定不制造服务器,而是与第三方存储数据。 这具有明显的优势:

  1. 无需为服务器付费,它不会伤及服务器的稳定性和可用性。
  2. 更少的代码,更少的错误。
  3. 用户不需要在我的应用程序中注册(这很烦人)。
  4. 隐私性更高,并且用户知道他的数据存储在一个他最可能信任我的地方。

首先,选择权落在remoteStorage.js上 。 他们提供了一个开放的数据交换协议,一个非常不错的API,与Google Drive和Dropbox以及它们的服务器集成的能力。 但是,这条道路最终是死路一条(为什么-一个单独的故事)。

最后,我决定直接使用Google云端硬盘,并将Google API客户端库 (以下称为GAPI)用作访问它的库。

不幸的是,Google文档令人失望,并且GAPI库看起来还没有完成,而且,它有多个版本,而且不清楚哪个是有问题的。 因此,必须从文档,StackOverflow上的问题和答案以及Internet上的随机帖子中分几部分收集解决问题的方法。

如果您决定在应用程序中使用Google云端硬盘,希望本文能为您节省时间。

准备工作


以下是有关如何获取用于Google API的密钥的说明。 如果您不感兴趣,请直接进入下一部分。

接收钥匙
在Google Developer Console中, 创建一个新项目,输入名称。

在“控制面板”中,单击“启用API和服务”,然后打开Goog​​le云端硬盘。

接下来,转到“ API和服务->凭据”部分,单击“创建凭据”。 您需要做三件事:

  1. 配置“ OAuth访问请求窗口”。 输入应用程序的名称,“授权域”部分中的域以及指向应用程序主页的链接。 其他字段是可选的。
  2. 在“凭据”部分中,单击“创建凭据”->“ OAuth客户端标识符”。 选择类型“ Web应用程序”。 在设置窗口中,添加“允许的Javascript源”和“允许的重定向URI”:
    • 您的域(必填)
    • http://localhost:8000 (可选,可在本地工作)。


  3. 在“凭据”部分中,单击“创建凭据”->“ API密钥”。 在关键设置中,指定限制:
    • 允许的应用程序类型-> HTTP引荐来源网址(网站)
    • 接受来自以下引荐来源(站点)的HTTP请求->您的域和本地主机(如第2点所示)。
    • 有效的API-> Google Drive API



凭据部分应如下所示:



至此我们完成。 我们传递给代码。

初始化和登录


Google建议的启用GAPI的方法是将以下代码粘贴到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> 

加载库后,将调用initClient函数,我们必须编写自己的函数。 其典型外观如下:

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

对于数据存储,我们将使用所谓的Application Data文件夹 。 与常规文件夹相比,它的优点是:

  1. 用户不会直接看到它:来自其中的文件不会阻塞他的个人空间,并且他也不会破坏我们的数据。
  2. 其他应用程序看不到它,也不能破坏它。
  3. 上面提到的范围使应用程序可以访问它,但不能访问用户的其余文件。 也就是说,我们不会通过请求访问一个人的个人数据来吓scar一个人。

成功初始化Google API后,该函数将执行以下操作:

  1. 开始捕获登录/注销事件-最有可能,应该始终这样做。
  2. 初始化应用程序。 您可以根据需要在加载和初始化GAPI之前完成此操作。 如果Google不可用,我的初始化过程会稍有不同。 可能有人会说这种情况不会发生:)但是,首先,您将来可以拥有密钥和访问权限。 其次,例如在中国,谷歌被禁止。

登录和注销很简单:

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

您将在onSignIn处理程序中收到登录结果:

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

不幸的是,使用文件并不是很明显。

无极助手


GAPI不返回正常的承诺。 取而代之的是,使用了它自己的Thennable接口,该接口与Promise类似,但不完全相同。 因此,为了方便工作(主要是使用async/await ),我们将提供一个小助手:

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

此函数将GAPI方法和参数作为第一个参数,并返回Promise。 然后,您将看到如何使用它。

处理文件


您应该永远记住, Google云端硬盘上的文件名不是唯一的 。 您可以创建任意数量的具有相同名称的文件和文件夹。 只有标识符是唯一的。
对于基本任务,您不需要使用文件夹,因此下面的所有功能都可以使用Application Data文件夹根目录中的文件。 注释指示需要更改以使用文件夹的内容。 Google的文档在这里

创建一个空文件


 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 } 

此异步函数创建一个空文件并返回其标识符(字符串)。 如果已经存在这样的文件,则将创建一个具有相同名称的新文件,并返回其ID。 如果不想这样做,则必须首先检查是否没有相同名称的文件(请参见下文)。
Google云端硬盘不是完整的数据库。 例如,如果您希望多个用户同时在不同设备上使用同一Google帐户工作,则可能由于缺少交易而导致解决冲突的问题。 对于此类任务,最好不要使用Google云端硬盘。

处理文件内容


GAPI(用于基于浏览器的JavaScript)没有提供处理文件内容的方法(很奇怪,不是吗?)。 相反,有一个通用的request方法(一个简单的AJAX请求的瘦包装器)。

通过反复试验,我得出以下实现:

 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 } 

档案搜寻


 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 } 

如果未指定query ,则此函数返回应用程序文件夹中的所有文件(具有idname字段的对象数组),并按创建时间排序。

如果您指定query字符串(语法在此处描述),它将仅返回与查询匹配的文件。 例如,要检查是否config.json名为config.json的文件,您需要执行

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

删除文件


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

此函数按ID删除文件,如果删除成功,则返回true否则返回false

同步处理


建议该程序主要与localStorage ,并且Google Drive仅用于同步localStorage数据。

以下是一个简单的配置同步策略:

  1. 通过登录从Google云端硬盘下载新配置,然后每3分钟覆盖本地副本;
  2. 本地更改已注入Google云端硬盘,并覆盖其中的内容;
  3. 配置文件fileID缓存在localStorage以加快工作速度并减少请求数量;
  4. 正确处理(错误)的情况是Google云端硬盘具有多个配置文件,并且有人删除了我们的配置文件或将其破坏了。
  5. 同步详细信息不会影响其余的应用程序代码。 要使用配置,您仅使用两个函数: getConfig()saveConfig(newConfig)

在实际的应用程序中,您可能希望在加载/卸载配置时实现更灵活的冲突处理。

查看代码
 //     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() } 


结论


在我看来,Google云端硬盘上网站的数据存储非常适合小型项目和原型制作。 它不仅易于实现和支持,而且还有助于减少Universe中不必要实体的数量。 我希望,如果您选择此路径,我的文章将帮助您节省时间。

PS实际项目的代码位于GitHub上您可以在此处尝试。

Source: https://habr.com/ru/post/zh-CN440844/


All Articles