Save data in user Google Drive

Le but de cet article est d'expliquer comment enregistrer des données dans le stockage Google Drive de l'utilisateur.

Conditions préalables

Il est probablement préférable de savoir comment fonctionnent OAuth2 et HTTP.

  • OAuth2 est un framework d'autorisation. Il gère donc les clés pour accéder à une zone protégée.
  • La chose la plus importante à propos de HTTP dans notre situation est de savoir comment télécharger des données, en utilisant le formulaire POST et le multipart.

Scénario

  1. Configurez un projet Google Cloud. Ce projet détient les autorisations de la demande.
  2. Authentifiez-vous et obtenez les informations d'identification pour accéder à vos ressources (Google Drive).
  3. Obtenir l'autorisation d'utiliser une API
  4. Lisez/écrivez vos données à l'aide de l'API

Configuration du projet

L'éditeur de l'application doit configurer une clé d'application qui inclut l'autorisation (utiliser l'API Google Drive) dont il a besoin pour s'exécuter et les conditions d'utilisation des API (comme cors). Cela permet à l'éditeur et à l'utilisateur de s'assurer que l'application ou quelqu'un d'autre n'utilisera pas les API ou toute autre API au nom de l'utilisateur.

Accédez à votre compte de console Google : https://console.cloud.google.com/ et configurez ces informations d'identification.

NB : lors de la phase de test, n'oubliez pas d'ajouter des utilisateurs tests, sinon cela ne fonctionnera pas avec les erreurs 403.

Authentification

Le script Google GSI permet d'authentifier un utilisateur. Plusieurs options sont disponibles, notamment une sorte d'authentification automatique.

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

Autorisation

Une fois l'utilisateur authentifié vous pouvez demander l'autorisation de l'application pour utiliser l'API "au nom de l'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});

Parcourir le lecteur

gapi.client.drive.files est l'API client permettant de travailler avec le lecteur. Voici comment trouver ou créer un dossier.

 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}

Créer un fichier

Pour créer un fichier, selon la documentation, c'est très simple en utilisant l'API gapi.client.drive.files. C'est d'une certaine manière vrai. Vous pouvez créer un fichier avec toutes les métadonnées, mais pas le téléchargement du contenu. travail.

Le code suivant crée un descripteur de fichier, donc le fichier existe mais il est vide.

1files.create({
2	name: name,
3	parents: [folderName],
4	upload_protocol: 'raw',
5	uploadType: 'media',
6	fields: 'id',
7	mimeType: 'application/json',

Pour contourner cette situation, nous publierons un formulaire contenant le contenu du fichier et ses métadonnées. Le POST a également le jeton d'autorisation dans ses en-têtes.

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

Exemple complet de Vuejs

Cet exemple utilise des composants Element-plus.

  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

Traductions: