Skip to content

Commit

Permalink
keep reference to OAuth2 handler
Browse files Browse the repository at this point in the history
  • Loading branch information
DrPaulBrewer committed Jan 27, 2019
1 parent 043c9cd commit e23867e
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 51 deletions.
89 changes: 46 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@

Initialize googleapi's Google Drive[tm] nodejs client, decorated with some useful 3rd party extensions.

## new in v5.0.0
* `drive.x.auth` contains a reference to the OAuth2 credentials object. For insecure applications, this should be deleted with `del drive.x.auth;`.

## new in v4.3.1
* tested against googleapis@36.0.0

## new in v4.3
* the `drive.x.hexid()` formula was changed. The new `crypto.createHmac` formula is more secure, and effectively case insensitive
as `.toLowerCase().trim()` is called on email strings before processing. But it does yield different hex values than v4.2.
as `.toLowerCase().trim()` is called on email strings before processing. But it does yield different hex values than v4.2.
* the internal formula is now available as `drive.x.hexIdFromEmail(email,secret)` and does not call any Google drive functions

## new in v4.2
Expand All @@ -19,10 +22,10 @@ as `.toLowerCase().trim()` is called on email strings before processing. But it
* (hopefully) now compatible with googleapis@30.0.0
* Initialization has changed slightly, because googleapis@30.0.0 uses named exports
* Now promise/async compatible at both clasic `drive` and extensions `drive.x`
* mostly the same API as v3, minimal changes. Still uses `request` for resumable upload. Will move to axios for `v5`.
* mostly the same API as v3, minimal changes. Still uses `request` for resumable upload. Will move to axios for `v5`.
* The `drive` functionality is vanilla GoogleApis and from their changes you may need to `.then((resp)=>(resp.data))`
* The `drive.x` functionality is mostly the same, except promise-yielding functions are now explicitly marked as async function


## Usage

Expand Down Expand Up @@ -58,7 +61,7 @@ The `tokens` are obtained when a user "Logs in with Google" in your app. There
access_token: "the-latest-access-token-your-app-received-the-most-recent-time-the-visitor-logged-in,
expiry_time: Date.now()+1000*60*59 // 59 minutes
};
const drive = driveX(google, request, keys, tokens, salt);
const drive = driveX(google, request, keys, tokens, salt);

Now:
* `drive` contains a googleapis.drive official client
Expand All @@ -77,14 +80,14 @@ This should work in cases where `drive` already exists and has credentials.
const request = require('request');
const driveX = require('decorated-google-drive');
const ddrive = driveX.decorate(drive, request);

Now the extensions are available in `ddrive.x` and `ddrive.x.appDataFolder`

### Verifying tokens

When you set up Google Sign-In, successful sign-ins are redirected to your website, which receives a token. But this could be faked.

How do you know a token is valid?
How do you know a token is valid?

One way to verify tokens is to get the profile of the current user.

Expand All @@ -95,40 +98,40 @@ whereas with Google Plus you can everything someone reveals on their public Goog
Here is code to fetch the logged in user's email address.

drive.x.aboutMe().then((info)=>(info.user.emailAddress)).then({...})

Once 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.
`access_token` expires, and usually has a time to live of 1 hour. It is refreshed by `googleapis` using the `refresh_token`.

An 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
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
avoid 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
Drive. 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`
is deleted when a user uninstalls or deletes your app.
is deleted when a user uninstalls or deletes your app.

### Store a string in the appDataFolder
### Store a string in the appDataFolder

Once initialized, this snippet will store a string in the file `myaccount` in the `appDataFolder`.

const str = require('string-to-stream');
const secrets = 'some-encrypted-string-of-secrets';

drive.x.appDataFolder.upload2({
folderPath: '',
name: 'myaccount',
stream: str(secrets),
mimeType: 'text/plain',
createPath: false,
drive.x.appDataFolder.upload2({
folderPath: '',
name: 'myaccount',
stream: str(secrets),
mimeType: 'text/plain',
createPath: false,
clobber: true
}).then((newFileMetadata)=>{...}).catch((e)=>{...})

upload2 uses a resumable upload.

A [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 5MB.

`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.

In `drive.x.appDataFolder.upload2` (shown here) these steps are included. Internally, they are used in a 2-step procedure
to first request an upload URL, and then do an upload. This 2-step procedure is invisible to the developer,
to first request an upload URL, and then do an upload. This 2-step procedure is invisible to the developer,
but can be seen in the source code.


Expand All @@ -155,7 +158,7 @@ Drive retains the corrupted upload.
createPath: true,
clobber: true
}).then((newFileMetaData)=>{...}).catch((e)=>{...});

We haven't tried disrupting the upload and then trying to resume it.

It seems to deal with 5GB binary .zip files ok.
Expand All @@ -171,11 +174,11 @@ These resumable upload URLs are good for quite a while and seem to be signed URL
If you have `folderMetadata` from, say, `drive.x.findPath`, then you can create a URL-generating function for uploads with

const getUploadUrlForFile = drive.x.uploadDirector(folderMetadata);
and then

and then

getUploadUrlForFile({name: 'hello.txt', mimeType: 'text/plain'})

will resolve to some Google uploader URL that you can post to with `npm:request`

### Download a file knowing only the /path/to/file
Expand All @@ -185,7 +188,7 @@ You can find a file and download it one step with:
drive.x.download('/path/to/myfile.zip', optional mimeType).then((zipdata)=>{...})

`mimeType` is only useful for Google Docs and Sheets that can be exported to various mimeTypes.

If the file does not exist, the promise will be rejected with Boom.notFound.

Internally, `drive.x.download` is a Promise chain with `drive.x.findPath` then `drive.x.contents`
Expand All @@ -197,14 +200,14 @@ Searching through the chain of folders involves multiple API calls and is slow w
Instead get the `file.id` and use drive.x.contents:

drive.x.contents(fileMetadata.id, optional mimeType).then((content)=>{...});

`mimeType` is only useful for Google Docs and Sheets that can be exported to various mimeTypes.

Internally, `drive.files.get` with the `media` download option is called. If the file is a doc or sheet or presentation,
this will throw an error with the string `Use Export`. `drive.x.contents` catches that error and calls `drive.files.export`
requesting the proper `mimeType`. If you know you need to fetch a Google doc/sheet/presentation, it will be quicker to
requesting the proper `mimeType`. If you know you need to fetch a Google doc/sheet/presentation, it will be quicker to
call `drive.files.export` directly.

### finding Paths with drive.x.findPath

As 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.
Expand All @@ -214,7 +217,7 @@ and continuing down the chain. In the library, I wrote functional wrappers on `
on 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:

drive.x.findPath('/work/projectA/2012/Oct/customers/JoeSmith.txt').then((fileMetaData)=>{...})

where `{...}` is your code that needs `fileMetaData`. The resolved data looks like this:

{
Expand Down Expand Up @@ -244,47 +247,47 @@ To find all the files in the Drive that you can access, that are not in the tras

const findAll = drive.x.searcher({}); // or { trashed: false }
findAll().then(({files})=>{...});

Here `files` is an array of objects with properties `.id`, `.name`, `.parents`, `.mimeType` and at least the properties you were searching over.

To find the files you can access that are in the trash:

const findTrash = data.x.searcher({trashed: true});
findTrash().then(({files})=>{...});

Note that as of 3.0.0 there is no way to return all the files independent of trash status.

You can set which fields are returned by setting `fields` explicitly like this `drive.x.searcher({fields: 'id,name,mimeType,md5Checksum'})`

Notice that `drive.x.searcher` returns a `function`. That function takes two parameters, a `parent` which is a folder file id and a `name`.

To find the top level files in the root of the Drive that you can access:

const findAll = drive.x.searcher({});
findAll('root').then(({files})=>{...});

To find zero, one or more files named `kittens.png` in the root of the Drive:

findAll('root', 'kittens'png').then(({files})=>{...});

To find zero, one, or more trashed file named `severedhead.png` in the Drive:

const findTrash = data.x.searcher({trashed: true});
findTrash(null, 'severedHead.png').then(({files})=>{...});

You can restrict mimeType or require a unique (single) file in the searcher parameters:

const findTrashedPng = drive.x.searcher({trashed:true, mimeType: 'image/png', unique: true };
( findTrashedPng(null, 'severedHead.png')
.then(drive.x.checkSearch)
.then(({ files })=>{...})
)

`unique:true` sets `limit:2` so is not in fact unique but instead returns 2 files quickly. You can enforce uniqueness, thowing Boom errors, by calling
`drive.x.checkSearch` on the search results. Successful searches are passed to the next `then()` and searches with missing files or duplicates
throw errors. (see `drive.x.findPath` above for a descrption of these Boom errors and how to catch them).

`drive.x.searcher` tests all returned files/folders mimeTypes against the Google Drive Folder mimeType 'application/vnd.google-apps.folder' and sets
`drive.x.searcher` tests all returned files/folders mimeTypes against the Google Drive Folder mimeType 'application/vnd.google-apps.folder' and sets
`.isFolder` to `true` or `false` for each file/folder in `files` appropriately.

You 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.
Expand All @@ -300,21 +303,21 @@ Finds the folder "/crime/sprees/murder" and looks for any files in this folder t
.then( ({files})=>{ if (files.length===0) return notGuilty(); return guilty(); } )
.catch( (e)=>{ if (e.isBoom && e.typeof===Boom.notFound) return notGuilty(); throw e; })
)

### update file metadata

`drive.x.updateMetadata(fileId, metadata)` is a Promise-based alias for `drive.files.update({fileId, resource:metadata})`
`drive.x.updateMetadata(fileId, metadata)` is a Promise-based alias for `drive.files.update({fileId, resource:metadata})`

`drive.x.updateMetadata(fileId, {properties: {role: 'instructions'}, description: 'read this first'})` would set public file properties to `{role: 'instructions'}` and
`drive.x.updateMetadata(fileId, {properties: {role: 'instructions'}, description: 'read this first'})` would set public file properties to `{role: 'instructions'}` and
set the file's `description` field to "read this first".

The Promise resolves to the new file object, with properties `.id`,`.name`,`.mimeType`,`.parents`, and at least any fields set in metadata.

### delete the files you found

`drive.x.janitor` returns a function that calls something like `Promise.all(files.map(delete))`.

The function returned by `drive.x.janitor` is intended to be placed in a `then` and picks out the data it needs and
The function returned by `drive.x.janitor` is intended to be placed in a `then` and picks out the data it needs and
opionally sets a flag if the deletions are successful. The Janitor will not throw an error on an empty search, and
`drive.x.checkSearch` is not called in the upcoming snippet. However, irregardless, delete could throw an error on some file
and so a `.catch` is needed to catch the failed cases.
Expand All @@ -337,7 +340,7 @@ I'm going to try to stay sane and not post a set of encrypted API keys and token

Instead, look in [testResults.txt](./testResults.txt), or set up your own testing.

Current tests demonstrate some basic functionality.
Current tests demonstrate some basic functionality.

To confirm access tokens are being refreshed automatically, set up and run the tests once. Wait until the access token
expires (usually an hour) and run the tests again.
Expand All @@ -352,7 +355,7 @@ The above copyright notice and this permission notice shall be included in all c

THE 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.

### No relationship to Google, Inc.
### No relationship to Google, Inc.

This is third party software, not a product of Google Inc.

Expand Down
17 changes: 9 additions & 8 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ function extensions(drive, request, rootFolderId, spaces, salt) {
});
return allfields;
}

function getdata(resp){ return resp.data; }

async function driveAboutMe(_fields) {
Expand All @@ -46,7 +46,7 @@ function extensions(drive, request, rootFolderId, spaces, salt) {
}

x.aboutMe = driveAboutMe;

function hexIdFromEmail(email, secret){
if (!secret) throw Boom.badImplementation("missing secret");
if (!crypto) throw Boom.badImplementation("missing crypto");
Expand All @@ -58,16 +58,16 @@ function extensions(drive, request, rootFolderId, spaces, salt) {
.digest('hex')
);
}

x.hexIdFromEmail = hexIdFromEmail;

async function driveHexid(){
if (!salt) throw Boom.badImplementation("missing salt");
const info = await driveAboutMe();
const email = info.user.emailAddress;
return hexIdFromEmail(email, salt);
}

x.hexid = driveHexid;

function driveSearcher(options) {
Expand Down Expand Up @@ -278,7 +278,7 @@ function extensions(drive, request, rootFolderId, spaces, salt) {
x.nameFrom = nameFrom;

// for url override see end of http://google.github.io/google-api-nodejs-client/22.2.0/index.html

// legacy url: "https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable"

function driveUploadDirector(parentFolderOrId) {
Expand Down Expand Up @@ -400,10 +400,11 @@ function extensions(drive, request, rootFolderId, spaces, salt) {
return x;
}

function decorate(drive, request, salt) {
function decorate(drive, auth, request, salt) {
// drive is delivered from googleapis frozen, so we'll refreeze after adding extensions
const extras = {};
extras.x = extensions(drive, request, 'root', 'drive', salt);
if (auth) extras.x.auth = auth;
extras.x.appDataFolder = extensions(drive, request, 'appDataFolder', 'appDataFolder', salt);
return Object.freeze(Object.assign({}, drive, extras));
}
Expand All @@ -428,7 +429,7 @@ function decoratedGoogleDrive(googleapis, request, keys, tokens, salt) {
const drive = googleapis.drive({ version: 'v3', auth });
if (typeof(drive) !== 'object')
throw Boom.badImplementation("drive is not an object, got: " + typeof(drive));
return decorate(drive, request, salt);
return decorate(drive, auth, request, salt);
}

decoratedGoogleDrive.decorate = decorate;
Expand Down

0 comments on commit e23867e

Please sign in to comment.