Save data in user Google Drive
The goal of this post is to explain how to save data in user's google drive storage.
Prerequisites
It's probably better to know how OAuth2 and HTTP works.
- OAuth2 is an Authorization framework. So it manages the keys to access a protected area.
- The most important thing about HTTP in our situation is how to upload data, using POST form and multipart.
Scenario
- Setup a Google Cloud project. This projects holds the authorizations of the application.
- Authenticate and obtain credentials to access your resources (Google drive).
- Obtain the authorization to use an API
- Read/Write your data using the API
Project setup
The application publisher need to setup an application key which includes the permission (use google drive API) it needs to run, and the conditions to use the APIs (like cors) This allows publisher and user to make sure the app or someone else won't use the APIs or any other API on behalf of the user.
Go to your Google console account: https://console.cloud.google.com/ and setup these credentials.
NB: during the test phase, don't forget to add test users, otherwise it won't work with 403 errors.
Authentication
Google GSI script allows to authenticate a user. Several option are available, including some kind of automatic authentication.
1this.$loadScript("https://accounts.google.com/gsi/client?hl=fr").then(args => {
2 google.accounts.id.initialize({
3 client_id: CLIENT_ID,
4 callback: this.handleCredentialResponse
5 });
6 this.tokenClient = google.accounts.oauth2.initTokenClient({
7 client_id: CLIENT_ID,
8 scope: SCOPES,
9 callback: () => {
10 this.googleInitialized = true
11 },
12 });
13 // load authorization here, looks to me more logical but you can load authorization in parallel
14});
Authorization
Once the user is authenticated you can request the application authorization to use the API "on behalf of the application"
1this.$loadScript("https://apis.google.com/js/api.js").then(args => {
2 gapi.load('client', () => {
3 gapi.client.init({
4 apiKey: API_KEY,
5 discoveryDocs: [DISCOVERY_DOC]
6 }).then(response => console.log(response));
7 if (gapi.client.getToken() === null) {
8 // Prompt the user to select a Google Account and ask for consent to share their data
9 // when establishing a new session.
10 this.tokenClient.requestAccessToken({prompt: 'consent'});
11 } else {
12 // Skip display of account chooser and consent dialog for an existing session.
13 this.tokenClient.requestAccessToken({prompt: ''});
14 }
15 });
16});
Browse the drive
The gapi.client.drive.files
is the client api to work with the drive. Here is how to find or create a folder.
1function findOrCreateFolder(folderName) {
2 if (this.folderId) {
3 return new Promise((resolve, reject) => {
4 resolve(this.folderId);
5 });
6 }
7 // Rechercher le répertoire par nom
8 return new Promise((resolve, reject) => {
9 gapi.client.drive.files.list({
10 q: `mimeType='application/vnd.google-apps.folder' and name='${folderName}'`,
11 fields: 'files(id)',
12 }).then(searchResults => {
13 const folders = searchResults.result.files;
14 if (folders.length > 0) {
15 // Le répertoire existe, renvoyer son ID
16 this.folderId = folders[0].id;
17 } else {
18 // Le répertoire n'existe pas, le créer
19 const folderMetadata = {
20 name: folderName,
21 mimeType: 'application/vnd.google-apps.folder',
22 };
23 gapi.client.drive.files.create({
24 resource: folderMetadata,
25 fields: 'id',
26 }).then(response => {
27 this.folderId = response.result.id;
28 });
29 }
30 resolve(this.folderId);
31 }).catch(err => {
32 reject(err)
33 });
34 });
35}
Create a file
To create a file, according to the documentation, it's very easy using The gapi.client.drive.files
API. This is
in a way true. You can create a file with all the metadata, but the upload of the content does not
work.
The following code creates a file descriptor, so the file exists but it's empty.
1files.create({
2 name: name,
3 parents: [folderName],
4 upload_protocol: 'raw',
5 uploadType: 'media',
6 fields: 'id',
7 mimeType: 'application/json',
To work around this situation, we'll POST a form containing the file content and it's metadatas. The POST also have the authorization token in its headers.
1const form = new FormData();
2form.append('metadata', new Blob([JSON.stringify(metadata)], {type: 'application/json'}));
3form.append('file', new Blob([JSON.stringify(data)], {type: 'application/json'}));
4fetch("https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id", {
5 method: 'POST',
6 headers: new Headers({
7 'Authorization': 'Bearer ' + gapi.auth.getToken().access_token
8 }),
9 body: form
10}).then(res => console.log(res));
Full Vuejs example
This example uses Element-plus components.
1
2<template>
3 <div class="g_id_signin" data-type="standard" data-shape="rectangular" data-theme="outline" data-text="signin_with" data-size="large" data-logo_alignment="left"></div>
4 {{ user.given_name }}
5 <el-button @click="login" v-if="!googleInitialized">Login</el-button>
6 <el-button @click="logout" v-if="googleInitialized">Logout</el-button>
7 <el-button @click="listFiles" :disabled="!googleInitialized">Refresh</el-button>
8 <el-button @click="saveJson" :disabled="!googleInitialized">Create</el-button>
9 <el-table :data="files">
10 <el-table-column fixed prop="name" label="Name" width="150"/>
11 <el-table-column fixed prop="modifiedTime" label="Date" width="150"/>
12 <el-table-column fixed="right" label="Operations" width="120">
13 <template #default="scope">
14 <el-button link type="primary" size="small" @click.prevent="open(scope.row)">
15 Open
16 </el-button>
17 </template>
18 </el-table-column>
19 </el-table>
20</template>
21<script>
22 import VueJwtDecode from 'vue-jwt-decode';
23
24 const CLIENT_ID = '....apps.googleusercontent.com';
25 const API_KEY = '...';
26 const DISCOVERY_DOC = 'https://www.googleapis.com/discovery/v1/apis/drive/v3/rest';
27 const REDIRECT_URI = 'https://developers.google.com/oauthplayground';
28 const SCOPES = 'https://www.googleapis.com/auth/drive';
29 export default {
30 name: "UserDrive",
31 props: {
32 folder: String,
33 },
34 components: {},
35 data() {
36 return {
37 user: {},
38 googleInitialized: false,
39 tokenClient: null,
40 files: [],
41 folderId: null,
42 }
43 },
44 mounted() {
45 },
46 methods: {
47 open(file) {
48 console.log(file);
49 this.$emit("open", file);
50 },
51 logout() {
52 gapi.client.setToken(null);
53 },
54 login() {
55 if (!this.googleInitialized) {
56 this.$loadScript("https://accounts.google.com/gsi/client?hl=fr").then(args => {
57 google.accounts.id.initialize({
58 client_id: CLIENT_ID,
59 callback: this.handleCredentialResponse
60 });
61 /*google.accounts.id.prompt((resp) => {
62 console.log(resp);
63 });*/
64 this.tokenClient = google.accounts.oauth2.initTokenClient({
65 client_id: CLIENT_ID,
66 scope: SCOPES,
67 callback: () => {
68 this.googleInitialized = true
69 }, // defined later
70 });
71 this.$loadScript("https://apis.google.com/js/api.js").then(args => {
72 gapi.load('client', () => {
73 gapi.client.init({
74 apiKey: API_KEY,
75 discoveryDocs: [DISCOVERY_DOC]
76 }).then(response => console.log(response));
77 if (gapi.client.getToken() === null) {
78 // Prompt the user to select a Google Account and ask for consent to share their data
79 // when establishing a new session.
80 this.tokenClient.requestAccessToken({prompt: 'consent'});
81 } else {
82 // Skip display of account chooser and consent dialog for an existing session.
83 this.tokenClient.requestAccessToken({prompt: ''});
84 }
85 });
86 });
87 });
88 }
89 },
90 handleCredentialResponse(response) {
91 this.user = VueJwtDecode.decode(response.credential);
92 },
93 findOrCreateFolder(folderName) {
94 if (this.folderId) {
95 return new Promise((resolve, reject) => {
96 resolve(this.folderId);
97 });
98 }
99 // Rechercher le répertoire par nom
100 return new Promise((resolve, reject) => {
101 gapi.client.drive.files.list({
102 q: `mimeType='application/vnd.google-apps.folder' and name='${folderName}'`,
103 fields: 'files(id)',
104 }).then(searchResults => {
105 const folders = searchResults.result.files;
106 if (folders.length > 0) {
107 // Le répertoire existe, renvoyer son ID
108 this.folderId = folders[0].id;
109 } else {
110 // Le répertoire n'existe pas, le créer
111 const folderMetadata = {
112 name: folderName,
113 mimeType: 'application/vnd.google-apps.folder',
114 };
115 gapi.client.drive.files.create({
116 resource: folderMetadata,
117 fields: 'id',
118 }).then(response => {
119 this.folderId = response.result.id;
120 });
121 }
122 resolve(this.folderId);
123 }).catch(err => {
124 reject(err)
125 });
126 });
127 },
128 saveJson() {
129 this._saveJson({name: 'stephane'}, "stephane.json")
130 },
131 _saveJson(data, name) {
132 this.findOrCreateFolder(this.folderName)
133 .then(folder => {
134 let metadata = {
135 name: name,
136 parents: [folder],
137 };
138 let files = gapi.client.drive.files;
139 let media = {
140 mimeType: 'application/json',
141 body: JSON.stringify(data)
142 };
143 const form = new FormData();
144 form.append('metadata', new Blob([JSON.stringify(metadata)], {type: 'application/json'}));
145 form.append('file', new Blob([JSON.stringify(data)], {type: 'application/json'}));
146 fetch("https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id", {
147 method: 'POST',
148 headers: new Headers({
149 'Authorization': 'Bearer ' + gapi.auth.getToken().access_token
150 }),
151 body: form
152 }).then(res => console.log(res));
153 this.listFiles();
154 }).catch(err => console.log(err))
155 },
156 listFiles() {
157 this.findOrCreateFolder(this.folderName)
158 .then((folder) => gapi.client.drive.files.list({
159 'q': `'${folder}' in parents`,
160 'pageSize': 10,
161 'fields': 'files(id, name,modifiedTime)',
162 }).then((response) => {
163 this.files.splice(0, this.files.length);
164 response.result.files.forEach(file => {
165 this.files.push(file);
166 })
167 }).catch(err => console.log(err)));
168 }
169 }
170 }
171</script>
172<style></style>
173