-
Notifications
You must be signed in to change notification settings - Fork 2k
GoogleDriveProvider #832
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: cloud-provider-refactor
Are you sure you want to change the base?
GoogleDriveProvider #832
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| /* global gapi */ | ||
| import GoogleProvider from './google-provider'; | ||
| import GoogleDriveIcon from '../components/icons/google-drive-icon'; | ||
|
|
||
| // Array of API discovery doc URLs for APIs | ||
| const DISCOVERY_DOCS = ["https://www.googleapis.com/discovery/v1/apis/drive/v3/rest"]; | ||
|
|
||
| const SCOPES = ['https://www.googleapis.com/auth/drive.file']; // Restrict to files from this app | ||
|
|
||
| // There is a selection of access scopes for Google Drive, e.g. | ||
| // - https://www.googleapis.com/auth/drive See, edit, create, and delete all of your Google Drive files | ||
| // - https://www.googleapis.com/auth/drive.appdata View and manage its own configuration data in your Google Drive | ||
| // - https://www.googleapis.com/auth/drive.file View and manage Google Drive files and folders that you have opened or created with this app | ||
| // See https://developers.google.com/identity/protocols/googlescopes#drivev3 | ||
|
|
||
| export default class GoogleDriveProvider extends GoogleProvider { | ||
| /** | ||
| * To access Google Drive, specify it in scopes | ||
| * param scopes = ['https://www.googleapis.com/auth/drive.file']; | ||
| */ | ||
| constructor(appName, clientId, appKey, scopes = SCOPES) { | ||
| super({ | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For default parameters I would create a constant and merge it with constructor’s postmasters |
||
| clientId, | ||
| appKey, | ||
| name: 'Google Drive', | ||
| appName, | ||
| icon: GoogleDriveIcon, | ||
| discoveryDocs: DISCOVERY_DOCS, | ||
| scopes | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Uploads a file to Google Drive | ||
| * @param blob to upload | ||
| * @param name if blob doesn't contain a file name, this field is used | ||
| * @param isPublic define whether the file will be available pubblicaly once uploaded | ||
| * @returns {Promise<void>} | ||
| */ | ||
| async uploadFile({blob, name, isPublic = true}) { | ||
| const mimeType = 'application/json'; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be defined outside the scope of this method. Actually it could be defined in a cloud provider constant file since other providers may take advantage of it |
||
| // gapi.client.drive doesn't seem to have good stubs for uploading, so we use the Google Drive REST API | ||
| const metadata = { | ||
| name, // Filename on Google Drive | ||
| mimeType, // mimeType on Google Drive | ||
| parents: ['root'] // Folder ID at Google Drive | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Constant |
||
| }; | ||
|
|
||
| // The body of the post request will be this form | ||
| const form = new FormData(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would encapsulate the logic to generate the body into an helper method |
||
| form.append('metadata', new Blob([JSON.stringify(metadata)], {type: mimeType})); | ||
| form.append('file', blob); | ||
| const accessToken = gapi.auth.getToken().access_token; // Here gapi is used for retrieving the access token. | ||
|
|
||
| return fetch('https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id', { | ||
| method: 'POST', | ||
| headers: new Headers({Authorization: `Bearer ${accessToken}`}), | ||
| body: form | ||
| }) | ||
| .then(res => res.json()) | ||
| .catch(error => console.error(val)); // eslint-disable-line | ||
|
|
||
| // TODO - implement sharing | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| /* global window, gapi */ | ||
| import {loadScript} from '../utils/load-script'; | ||
|
|
||
| // We load `gapi` at run-time from this URL | ||
| const GOOGLE_API_URL = 'https://apis.google.com/js/api.js'; | ||
|
|
||
| export default class GoogleProvider { | ||
| // Create a new Google Account provider | ||
| // To access Google Drive, specify it in scopes | ||
| // @param scopes = ['https://www.googleapis.com/auth/drive']; | ||
| constructor({clientId, appKey, name, appName, icon, discoveryDocs, scopes = []}) { | ||
| // All cloud-providers providers must implement the following properties | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I’m not clear what the hierarchy is here. My understanding is the following: But constructors don’t reflect such hierarchy, why google provider have an icon given it is an api layer? If my first assumptions is wrong, can you clarify in the PR description the role of GoolgeProvider and GoogleCloudProvider? I only see google provider in it |
||
| this.appName = appName; | ||
| this.name = name; | ||
| this.icon = icon; | ||
|
|
||
| this._gapiInitializationOptions = {clientId, appKey, discoveryDocs, scopes}; | ||
| this._clearUserData(); | ||
| } | ||
|
|
||
| /** | ||
| * This method will handle the oauth flow by performing the following steps: | ||
| * - Opening a popup window and letting the user select google account | ||
| * - If already signed in to the browser, the window will disappear immediately | ||
| * @returns {Promise<void>} | ||
| */ | ||
| async login(onCloudLoginSuccess) { | ||
| await this._initializeGoogleApi(); | ||
|
|
||
| // NOTE: We are not using async/await syntax here because: | ||
| // - babel's "heavy-handed" transpilation of `async/await` moves this code into a callback | ||
| // - but it needs to run directly in the `onClick` handler since it opens a popup | ||
| // - and popups that are not a direct result of the user's actions are typically blocked by browser settings. | ||
| return gapi.auth2.getAuthInstance().signIn().then(googleUser => { | ||
| this._getUserData(googleUser); | ||
| this._onLoginStatusChange(); | ||
| onCloudLoginSuccess(this.name); | ||
| }).catch (error => { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you call onCloudLoginFailure? |
||
| console.error('GOOGLE API LOGIN ERROR', error); // eslint-disable-line | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Provides the current auth token. | ||
| * @returns {any} | ||
| */ | ||
| getAccessToken() { | ||
| const token = this.accessToken; | ||
| return (token || '') !== '' ? token : null; | ||
| } | ||
|
|
||
| // FOR DERIVED CLASSES | ||
|
|
||
| _onLoginStatusChange(googleUser) { | ||
| // Redefine in subclass | ||
| } | ||
|
|
||
| // PRIVATE | ||
|
|
||
| // Initialize the Google API (gapi) by loading it dynamically and initializing with appropriate scope | ||
| // TODO | ||
| // gapi could potentially be initialized by other parts of kepler (in particular, by other cloud providers) | ||
| // To fully handle that case we would likely need to use the advanced gapi authentication API, | ||
| // as the recommended API we use here only supports one sign-in. | ||
| async _initializeGoogleApi() { | ||
| // Check if already initialized | ||
| if (window.gapi) { | ||
| return; | ||
| } | ||
|
|
||
| const {clientId, appKey, discoveryDocs, scopes} = this._gapiInitializationOptions; | ||
|
|
||
| await loadScript(GOOGLE_API_URL); | ||
|
|
||
| // gapi.load is supposed return a thenable object, but await does not seem to work on it so wrap in Promise instead | ||
| await new Promise(resolve => gapi.load('client', resolve)); | ||
|
|
||
| // Initialize the JavaScript client library. | ||
| // TODO - https://stackoverflow.com/questions/15657983/popup-blocking-the-gdrive-authorization-in-chrome | ||
|
|
||
| const gapiOptions = { | ||
| discoveryDocs, | ||
| scope: scopes.join(" "), | ||
| fetch_basic_profile: true, | ||
| immediate: false | ||
| }; | ||
| if (clientId) { | ||
| gapiOptions.client_id = clientId; | ||
| } | ||
| if (appKey) { | ||
| gapiOptions.appKey = appKey; | ||
| } | ||
| await gapi.client.init(gapiOptions); | ||
|
|
||
| // For APIKEY, no authinstance is provided | ||
| const authInstance = gapi.auth2.getAuthInstance(); | ||
| if (!authInstance) { | ||
| return; | ||
| } | ||
|
|
||
| // LISTEN FOR LOGIN | ||
| authInstance.isSignedIn.listen(isSignedIn => { | ||
| if (isSignedIn) { | ||
| // NOTE: This is only triggered on fresh login. I.e. signIn() does not trigger this if browser already logged in | ||
| console.log('GOOGLE API LOGIN DETECTED'); // eslint-disable-line | ||
| } else { | ||
| console.log('GOOGLE API LOGOUT DETECTED');// eslint-disable-line | ||
| this._clearUserData(); | ||
| this._onLoginChange(null); | ||
| } | ||
|
|
||
| // Update derived classes | ||
| this._onLoginStatusChange(); | ||
| // TODO - also needs a callback to update the app UI... | ||
| }); | ||
| } | ||
|
|
||
| // Extract a minimum of user data from the GoogleUser object | ||
| _getUserData(googleUser) { | ||
| const userProfile = googleUser.getBasicProfile(); | ||
| this.email = userProfile.getEmail(); | ||
| this.domain = googleUser.getHostedDomain(); | ||
| this.imageUrl = userProfile.getImageUrl(); | ||
|
|
||
| this.scopes = googleUser.getGrantedScopes().split(' '); | ||
|
|
||
| const authResponse = googleUser.getAuthResponse(true); | ||
| this.accessToken = authResponse.access_token; | ||
| this.idToken = authResponse.id_token; | ||
| } | ||
|
|
||
| // Clear user data | ||
| _clearUserData() { | ||
| this.signedIn = false; | ||
| this.email = null; | ||
| this.domain = null; | ||
| this.imageUrl = null; | ||
| this.scopes = []; | ||
| this.accessToken = null; | ||
| this.idToken = null; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should try to make to make this consistent across all cloud providers.
Right now Dropbox provider accepts an icon where here we have different parameters.
I think we can follow a more flexible pattern:
constructor(appName, clientId, options = {})
options can handle all different requirement for each cloud provider