{"id":22123372,"url":"https://github.com/drpaulbrewer/decorated-google-drive","last_synced_at":"2025-03-24T07:28:55.236Z","repository":{"id":147350586,"uuid":"106368652","full_name":"DrPaulBrewer/decorated-google-drive","owner":"DrPaulBrewer","description":"decorate googleapi's Google Drive[tm] node.js client with some useful extensions for path-management and resumable upload","archived":false,"fork":false,"pushed_at":"2020-08-01T09:39:24.000Z","size":126,"stargazers_count":1,"open_issues_count":2,"forks_count":0,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-03-02T21:16:40.293Z","etag":null,"topics":["file-path","google-drive-api","google-drive-wrapper","googleapis","nodejs-modules","resumable-upload"],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/DrPaulBrewer.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-10-10T04:32:23.000Z","updated_at":"2020-08-01T09:39:26.000Z","dependencies_parsed_at":null,"dependency_job_id":"491101ae-a935-4ae7-9c2f-e08afc476e6e","html_url":"https://github.com/DrPaulBrewer/decorated-google-drive","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DrPaulBrewer%2Fdecorated-google-drive","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DrPaulBrewer%2Fdecorated-google-drive/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DrPaulBrewer%2Fdecorated-google-drive/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DrPaulBrewer%2Fdecorated-google-drive/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/DrPaulBrewer","download_url":"https://codeload.github.com/DrPaulBrewer/decorated-google-drive/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245226948,"owners_count":20580775,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["file-path","google-drive-api","google-drive-wrapper","googleapis","nodejs-modules","resumable-upload"],"created_at":"2024-12-01T15:32:41.104Z","updated_at":"2025-03-24T07:28:55.205Z","avatar_url":"https://github.com/DrPaulBrewer.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# decorated-google-drive\n\nInitialize googleapi's Google Drive[tm] nodejs client, decorated with some useful 3rd party extensions.\n\n## new in v6.0.0 -- BREAKING CHANGES\n* initialization has changed\n* provided methods and testing is (mostly) the same\n* initialization uses object parameters\n* replaced `request` peer dependency with `axios`\n* tested against googleapis@58.0.0\n\n## new in v5.4.0\n* path traversal now uses search option 'recent' and will find the most recent file\n* it is better behaved in the case of multiple files with the same name\n* issues can arise if a user creates a file with the same name as an existing app folder\n\n## new in v5.3.0\n* tested against googleapis@47.0.0\n\n## new in v5.0.0\n* `drive.x.auth` contains a reference to the OAuth2 credentials object.  For insecure applications, this should be deleted with `del drive.x.auth;`.\n\n## new in v4.3.1\n* tested against googleapis@36.0.0\n\n## new in v4.3\n* the `drive.x.hexid()` formula was changed.  The new  `crypto.createHmac` formula is more secure, and effectively case insensitive\nas `.toLowerCase().trim()` is called on email strings before processing.  But it does yield different hex values than v4.2.\n* the internal formula is now available as `drive.x.hexIdFromEmail(email,secret)` and does not call any Google drive functions\n\n## new in v4.2\n* `drive.x.hexid()` returns a Promise resolving to a consistent 64 char hex id that is an anonymous pseudonym of the drive owner's email address.\n* you can enable `drive.x.hexid` by setting any string as the salt for the hexid sha256 hash when `driveX` is called to initialize.\n\n## new in v4.0\n\n* (hopefully) now compatible with googleapis@30.0.0\n* Initialization has changed slightly, because googleapis@30.0.0 uses named exports\n* Now promise/async compatible at both clasic `drive` and extensions `drive.x`\n* mostly the same API as v3, minimal changes.  Still uses `request` for resumable upload.  Will move to axios for `v5`.\n   * The `drive` functionality is vanilla GoogleApis and from their changes you may need to `.then((resp)=\u003e(resp.data))`\n   * The `drive.x` functionality is mostly the same, except  promise-yielding functions are now explicitly marked as async function\n\n\n## Usage\n\n### Install\n\nPre-requisites are `googleapis@58.0.0` and `axios`\n\n\n    npm i googleapis@58.0.0 -S\n    npm i axios -S\n    npm i decorated-google-drive -S\n\n### Initialize\n\nUpdated for v6.0\n\nPass the google object from initializing googleapis and pass the axios module, your keys and tokens. The `keys` are obtained from the Google API credentials console.\n\nThe `tokens` are obtained when a user \"Logs in with Google\" in your app.  There is various middleware for \"Log in with Google\", such as\n`passport` for `express`, `grant` and `bell` for `hapi`, and even a client-Javascript side library you can get from Google.  \n\n    const {google} = require('googleapis'); // works with googleapis-58.0.0\n    const axios = require('axios'); // worked with axios-0.19.2\n    const driveX = require('decorated-google-drive');\n    const salt = \"100% Organic Sea Salt, or some other string for salting the email addresses when making hexids\";\n    const keys = {\n  \t\tkey:  \"your-drive-api-key-goes-here\",\n\t  \tsecret: \"your-drive-api-secret-goes-here\",\n\t\t  redirect: \"https://yourhost.com/your/apps/google/redirect/url\"\n\t  };\n\t  // refresh_token is optional, but if present googleapis should automatically refresh access_token for you\n\t  const tokens = {\n\t\t  refresh_token: \"the-refresh-token-your-app-received-the-first-time-a-new-visitor-approved-your-app\",\n\t    access_token: \"the-latest-access-token-your-app-received-the-most-recent-time-the-visitor-logged-in,\n\t\t  expiry_time: Date.now()+1000*60*59 // 59 minutes\n    };\n\t  const drive = driveX({google, axios, keys, tokens, salt});\n\nNow:\n* `drive` contains a googleapis.drive official client\n* `drive.x` contains 3rd part extension methods for accessing Google Drive, providing path resolution, search, testing search result existence/uniqueness, and resumable upload.\n* `drive.x.appDataFolder` contains the same extension methods as `drive.x`, but set up to access the hidden appDataFolder\n\nAll extensions are written in terms of calls to `googleapis.drive`, it is simply that some of the techniques are tedious or less than obvious,\nand so it is useful to repackage these as extensions.\n\nBoth the original drive client in `drive` and the `drive.x` extensions are async functions and return Promises.\n\n### Decorate an existing vanilla googleapis.drive instance\n\nThis should work in cases where `drive` already exists and has credentials.\n\n\t const axios = require('axios');\n\t const driveX = require('decorated-google-drive');\n   const salt = 'saltIsGoodForYourHexids';\n\t const ddrive = driveX.decorate({drive, axios, salt});\n\nNow the extensions are available in `ddrive.x` and `ddrive.x.appDataFolder`\n\n### Verifying tokens\n\nWhen you set up Google Sign-In, successful sign-ins are redirected to your website, which receives a token.  But this could be faked.\n\nHow do you know a token is valid?\n\nOne way to verify tokens is to get the profile of the current user.  \n\nThe Google Drive REST API `/about` will tell you the user's email address, picture thumbnail, and the capacity and usage of their drive.\n\nHere is code to fetch the logged in user's email address.  \n\n\tdrive.x.aboutMe().then((info)=\u003e(info.user.emailAddress)).then({...})\n\nOnce you have verified that a set of tokens work, you should encrypt them and store them someplace safe, where your app can get them when a user takes an action.\n`access_token` expires, and usually has a time to live of 1 hour.  It is refreshed by `googleapis` using the `refresh_token`.\n\nAn obvious place is an encrypted browser cookie.  Of these, the `refresh_token` is only delivered once, the first time a user logs into google and approves your app, and is *not delivered on subsequent logins*. If you encrypt it and store it in a database, then your database, along with the keys, becomes a treasure-trove.  You can\navoid doing that by either throwing away the `refresh_token` and living with the 1 hour timeouts, or by storing an encrypted copy of the `refresh_token` in the users\nDrive.  The `appDataFolder` is useful for this.  It is a special folder that is stored in the user's Drive for each app, and hidden from the user. The entire `appDataFolder`\nis deleted when a user uninstalls or deletes your app.\n\n### Store a string in the appDataFolder\n\nOnce initialized, this snippet will store a string in the file `myaccount` in the `appDataFolder`.\n\n\tconst str = require('string-to-stream');\n\tconst secrets = 'some-encrypted-string-of-secrets';\n\n    drive.x.appDataFolder.upload2({\n\t   folderPath: '',\n\t   name: 'myaccount',\n\t   stream: str(secrets),\n\t   mimeType: 'text/plain',\n\t   createPath: false,\n\t   clobber: true\n\t   }).then((newFileMetadata)=\u003e{...}).catch((e)=\u003e{...})\n\nupload2 uses a resumable upload.  \n\nA [media upload](https://developers.google.com/drive/v3/web/manage-uploads) using `drive.files.create` directly from the unextended drive googleapi might be quicker for short files up to a few MB.\n\n`drive.files.create` media upload (not shown above) requires having the `folder.Id` of the `parent` folder for the new file, here it is simply `appDataFolder`.  Also setting `spaces` to `appDataFolder` is required.\n\nIn `drive.x.appDataFolder.upload2` (shown here) these steps are included. Internally, they are used in a 2-step procedure\nto first request an upload URL, and then do an upload.  This 2-step procedure is invisible to the developer,\nbut can be seen in the source code.\n\n\n### upload a file to the user's Drive via resumable upload\n\nTo upload a local file, a stream is required, so call node's `fs.createReadStream('/path/to/local/files')`.\n\nTo create missing intermediate folders, set `createPath:true`, otherwise it may throw a `Boom.notFound`, which you can catch.\n\nTo replace an existing file, set `clobber:true`, otherwise it may throw a `Boom.conflict`, which you can catch.\n\nPost-upload checksums reported by Google Drive API are used to guarantee fidelity for **binary** file uploads.\n\nA binary file\nis any non-text file.  The md5 checksum computed from the file stream is reported as `ourMD5` in the `newFileMetaData`\nand the md5 checksum computed by Google is reported as `md5Checksum` in the `newFileMetaData`.  When there is a mismatch\non a binary file the code will throw `Boom.badImplementation`, which you can catch, and any recovery should check if Google\nDrive retains the corrupted upload.\n\n\n    drive.x.upload2({\n       folderPath: '/destination/path/on/drive',\n       name: 'mydata.csv',\n       stream: fs.createReadStream('/path/to/local/files/mydata.csv'),\n       mimeType: 'text/csv',\n       createPath: true,\n       clobber: true\n       }).then((newFileMetaData)=\u003e{...}).catch((e)=\u003e{...});\n\nWe haven't tried disrupting the upload and then trying to resume it.  \n\nIt seems to deal with 5GB binary .zip files ok.\n\nAs of `decorated-google-drive:2.1.0` It is also possible to set `folderId` to a Drive `folder.id` string instead of setting `folderPath` to a path string.\n\n### getting a URL for resumable upload later\n\nIf you want to manage the resumable uploads, this creates a 0 byte file and retrieves a resumable upload URL for later use.  \n\nThese resumable upload URLs are good for quite a while and seem to be signed URL's that don't require tokens.  [See Drive API Docs:resumable-upload](https://developers.google.com/drive/v3/web/resumable-upload)\n\nIf you have `folderMetadata` from, say, `drive.x.findPath`, then you can create a URL-generating function for uploads with\n\n    const getUploadUrlForFile = drive.x.uploadDirector(folderMetadata);\n\nand then\n\n    getUploadUrlForFile({name: 'hello.txt', mimeType: 'text/plain'})\n\nwill resolve to some Google uploader URL that you can post to with `npm:axios`\n\n### Download a file knowing only the /path/to/file\n\nYou can find a file and download it one step with:\n\n    drive.x.download('/path/to/myfile.zip', optional mimeType).then((zipdata)=\u003e{...})\n\n`mimeType` is only useful for Google Docs and Sheets that can be exported to various mimeTypes.\n\nIf the file does not exist, the promise will be rejected with Boom.notFound.\n\nInternally, `drive.x.download` is a Promise chain with `drive.x.findPath` then `drive.x.contents`\n\n### Download a file when you have the fileMetadata\n\nSearching through the chain of folders involves multiple API calls and is slow when you already have the fileMetadata.\n\nInstead get the `file.id` and use drive.x.contents:\n\n     drive.x.contents(fileMetadata.id, optional mimeType).then((content)=\u003e{...});\n\n`mimeType` is only useful for Google Docs and Sheets that can be exported to various mimeTypes.\n\nInternally, `drive.files.get` with the `media` download option is called.  If the file is a doc or sheet or presentation,\nthis will throw an error with the string `Use Export`.  `drive.x.contents` catches that error and calls `drive.files.export`\nrequesting the proper `mimeType`.  If you know you need to fetch a Google doc/sheet/presentation, it will be quicker to\ncall `drive.files.export` directly.\n\n### finding Paths with drive.x.findPath\n\nAs of Oct 2017, the Google Drive REST API and googleapis.drive nodeJS libraries do not let you directly search for `/work/projectA/2012/Oct/customers/JoeSmith.txt`.  Therefore we provide an extension to do this search.\n\nThe search can be done, by either searching for any file named JoeSmith.txt and possibly looking at duplicates, or by searching the root folder for `/work` then searching `/work` for `projectA`\nand continuing down the chain.  In the library, I wrote functional wrappers on `googleapis.drive` so that `findPath` becomes a functional Promise `p-reduce` of an appropriate folder search\non an array of path components. Now you can simply search for a path by a simple call to `drive.x.findPath` or `drive.x.appDataFolder.findPath` as follows:\n\n    drive.x.findPath('/work/projectA/2012/Oct/customers/JoeSmith.txt').then((fileMetaData)=\u003e{...})\n\nwhere `{...}` is your code that needs `fileMetaData`.  The resolved data looks like this:\n\n\t{\n\t   id:  'dfakf20301241024klaflkafm', // Drive File Id\n\t   name: 'JoeSmith.txt',\n\t   mimeType: 'text/plain',\n\t   modifiedTime: 1507846447000, //  ms since Epoch\n\t   size: 21398 // size in Drive, may not equal number of bytes in file\n\t}\n\nAdditionally, `findPath` can fail with a rejected Promise.  \n`npm:boom` is used for errors our code throws.  \nYou can also get errors thrown by the googleapis code.\n\nTo catch file not found:\n\n    .catch( (e)=\u003e{  if (e.isBoom \u0026\u0026 e.typeof===Boom.notFound) return your_file_not_found_handler(e); throw e; } )\n\n\n### searching folders with drive.x.searcher\n\nIn all cases below, `...` should be replaced by your JavaScript code acting on the returned information.\n\nTo find all the files in the Drive that you can access, that are not in the trash:\n\n    const findAll = drive.x.searcher({}); // or { trashed: false }\n\tfindAll().then(({files})=\u003e{...});\n\nHere `files` is an array of objects with properties `.id`, `.name`, `.parents`, `.mimeType` and at least the properties you were searching over.\n\nTo find the files you can access that are in the trash:\n\n\tconst findTrash = data.x.searcher({trashed: true});\n\tfindTrash().then(({files})=\u003e{...});\n\nNote that as of 3.0.0 there is no way to return all the files independent of trash status.\n\nYou can set which fields are returned by setting `fields` explicitly like this `drive.x.searcher({fields: 'id,name,mimeType,md5Checksum'})`\n\nNotice that `drive.x.searcher` returns a `function`.  That function takes two parameters, a `parent` which is a folder file id and a `name`.\n\nTo find the top level files in the root of the Drive that you can access:\n\n    const findAll = drive.x.searcher({});\n\tfindAll('root').then(({files})=\u003e{...});\n\nTo find zero, one or more files named `kittens.png` in the root of the Drive:\n\n    findAll('root', 'kittens'png').then(({files})=\u003e{...});\n\nTo find zero, one, or more trashed file named `severedhead.png` in the Drive:\n\n\tconst findTrash = data.x.searcher({trashed: true});\n    findTrash(null, 'severedHead.png').then(({files})=\u003e{...});\n\nYou can restrict mimeType or require a unique (single) file in the searcher parameters:\n\n    const findTrashedPng = drive.x.searcher({trashed:true, mimeType: 'image/png', unique: true };\n\t( findTrashedPng(null, 'severedHead.png')\n\t    .then(drive.x.checkSearch)\n\t\t.then(({ files })=\u003e{...})\n\t\t)\n\n`recent:true` sets `limit:1` and `orderby:'modifiedTime desc'` so that the most\nrecently created/modified file will be returned.\n\n`unique:true` sets `limit:2` so is not in fact unique but instead returns 2 files quickly.  \n\nYou can enforce uniqueness, thowing Boom errors, by calling\n`drive.x.checkSearch` on the search results.  Successful searches are passed to the next `then()` and searches with missing files or duplicates\nthrow errors.  (see `drive.x.findPath` above for a descrption of these Boom errors and how to catch them).\n\n`drive.x.searcher` tests all returned files/folders  mimeTypes against the Google Drive Folder mimeType 'application/vnd.google-apps.folder' and sets\n`.isFolder` to `true` or `false` for each file/folder in `files` appropriately.\n\nYou can also use `isFolder:true` or `isFolder:false` as a search term to limit what is returned.  If `isFolder` is unspecified, a search can return a mix of files and folders.\n\nThe parent folder can be specified from an earlier promise, such as `drive.x.findPath` like this:\n\nFinds the folder \"/crime/sprees/murder\" and looks for any files in this folder that are .png files, then calls imaginary functions\n`notGuilty()` or `guilty()`.  Here `files` is an array so `files.length` is the number of files found.\n\n    const findAll = drive.x.searcher({ mimeType: 'image/png' });\n    ( drive.x.findPath('/crime/sprees/murder')\n\t    .then((folder)=\u003e(findAll(folder.id)))\n\t\t.then( ({files})=\u003e{ if (files.length===0) return notGuilty(); return guilty(); } )\n\t\t.catch( (e)=\u003e{ if (e.isBoom \u0026\u0026 e.typeof===Boom.notFound) return notGuilty(); throw e; })\n\t\t)\n\n### update file metadata\n\n`drive.x.updateMetadata(fileId, metadata)` is a Promise-based alias for `drive.files.update({fileId, resource:metadata})`\n\n`drive.x.updateMetadata(fileId, {properties: {role: 'instructions'}, description: 'read this first'})` would set public file properties to `{role: 'instructions'}` and\nset the file's `description` field to \"read this first\".\n\nThe Promise resolves to the new file object, with properties `.id`,`.name`,`.mimeType`,`.parents`, and at least any fields set in metadata.\n\n### delete the files you found\n\n`drive.x.janitor` returns a function that calls something like `Promise.all(files.map(delete))`.  \n\nThe function returned by `drive.x.janitor` is intended to be placed in a `then` and picks out the data it needs and\nopionally sets a flag if the deletions are successful. The Janitor will not throw an error on an empty search, and\n`drive.x.checkSearch` is not called in the upcoming snippet. However, irregardless, delete could throw an error on some file\nand so a `.catch` is needed to catch the failed cases.  \n\nThis could delete all the accessible files with mimeType audio/mpeg\n\n    const mp3search = drive.x.searcher({mimeType:'audio/mpeg'});\n    const Jim = drive.x.janitor('files','deleted');\n    mp3search().then(Jim).catch((e)=\u003e{}); // we're trusting Jim the Janitor to clean up a lot here, he might hit an API limit\n\n## Additional properties in resolved file objects\n\n`.isNew` is always set to `true` by `drive.x.upload2` and `drive.x.folderCreator`  always and set to `true` conditionally by `drive.x.createPath`, `drive.x.folderFactory` if a new folder is created, and is not set (undefined/falsey)  when the requested folder already exists.\n\n`.isFolder` is set to `true` on searches and folder creation when mimeType in the returned metadata indicates the Google Drive folder mimeType.\n\n## Tests\n\nI'm going to try to stay sane and not post a set of encrypted API keys and tokens to get a green \"build passing\" travis badge.\n\nInstead, look in [testResults.txt](./testResults.txt), or set up your own testing.  \n\nCurrent tests demonstrate some basic functionality.\n\nTo confirm access tokens are being refreshed automatically, set up and run the tests once.  Wait until the access token\nexpires (usually an hour) and run the tests again.  \n\n## License: MIT\n\nCopyright 2017 Paul Brewer, Economic and Financial Technology Consulting LLC \u003cdrpaulbrewer@eaftc.com\u003e\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n### No relationship to Google, Inc.\n\nThis is third party software, not a product of Google Inc.\n\nGoogle Drive[tm] is a trademark of Google, Inc.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdrpaulbrewer%2Fdecorated-google-drive","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdrpaulbrewer%2Fdecorated-google-drive","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdrpaulbrewer%2Fdecorated-google-drive/lists"}