|
1 | 1 | "use strict";
|
| 2 | + |
2 | 3 | import _ from "underscore";
|
3 | 4 | import * as utils from "./utils.js";
|
4 | 5 | import { discFor } from "./fdc.js";
|
5 | 6 |
|
6 |
| -export function GoogleDriveLoader() { |
7 |
| - const self = this; |
8 |
| - const MIME_TYPE = "application/vnd.jsbeeb.disc-image"; |
9 |
| - const CLIENT_ID = "356883185894-bhim19837nroivv18p0j25gecora60r5.apps.googleusercontent.com"; |
10 |
| - const SCOPES = "https://www.googleapis.com/auth/drive.file"; |
11 |
| - let gapi = null; |
| 7 | +const MIME_TYPE = "application/vnd.jsbeeb.disc-image"; |
| 8 | +const API_KEY = "AIzaSyAJOcuUV8x6qFL_ID3DmnH4dZ8VuAExTaU"; |
| 9 | +const CLIENT_ID = "356883185894-bhim19837nroivv18p0j25gecora60r5.apps.googleusercontent.com"; |
| 10 | +const SCOPES = "https://www.googleapis.com/auth/drive.file"; |
| 11 | +const DISCOVERY_DOC = "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest"; |
| 12 | +const FILE_FIELDS = "id,name,capabilities"; |
| 13 | + |
| 14 | +const boundary = "-------314159265358979323846"; |
| 15 | +const delimiter = `\r |
| 16 | +--${boundary}\r |
| 17 | +`; |
| 18 | +const close_delim = `\r |
| 19 | +--${boundary}--`; |
| 20 | + |
| 21 | +export class GoogleDriveLoader { |
| 22 | + constructor() { |
| 23 | + this.gapi = null; |
| 24 | + this.authorized = false; |
| 25 | + } |
12 | 26 |
|
13 |
| - self.initialise = function () { |
14 |
| - return new Promise(function (resolve) { |
| 27 | + async initialise() { |
| 28 | + console.log("Creating GAPI"); |
| 29 | + this.gapi = await new Promise((resolve) => { |
15 | 30 | // https://github.com/google/google-api-javascript-client/issues/319
|
16 | 31 | const gapiScript = document.createElement("script");
|
17 |
| - gapiScript.src = "https://apis.google.com/js/client.js?onload=__onGapiLoad__"; |
18 |
| - window.__onGapiLoad__ = function onGapiLoad() { |
19 |
| - gapi = window.gapi; |
20 |
| - gapi.client.load("drive", "v2", function () { |
21 |
| - console.log("Google Drive: available"); |
22 |
| - resolve(true); |
23 |
| - }); |
| 32 | + gapiScript.src = "https://apis.google.com/js/api.js"; |
| 33 | + gapiScript.onload = function onGapiLoad() { |
| 34 | + resolve(window.gapi); |
24 | 35 | };
|
25 | 36 | document.body.appendChild(gapiScript);
|
26 | 37 | });
|
27 |
| - }; |
28 |
| - |
29 |
| - self.authorize = function (immediate) { |
30 |
| - return new Promise(function (resolve, reject) { |
31 |
| - console.log("Authorizing", immediate); |
32 |
| - gapi.auth.authorize( |
33 |
| - { |
34 |
| - client_id: CLIENT_ID, |
35 |
| - scope: SCOPES, |
36 |
| - immediate: immediate, |
37 |
| - }, |
38 |
| - function (authResult) { |
39 |
| - if (authResult && !authResult.error) { |
40 |
| - console.log("Google Drive: authorized"); |
41 |
| - resolve(true); |
42 |
| - } else if (authResult && authResult.error && !immediate) { |
43 |
| - reject(new Error(authResult.error)); |
44 |
| - } else { |
45 |
| - console.log("Google Drive: Need to auth"); |
46 |
| - resolve(false); |
47 |
| - } |
48 |
| - }, |
49 |
| - ); |
| 38 | + console.log("Got GAPI, creating token client"); |
| 39 | + this.tokenClient = await new Promise((resolve) => { |
| 40 | + // https://github.com/google/google-api-javascript-client/issues/319 |
| 41 | + const gsiScript = document.createElement("script"); |
| 42 | + gsiScript.src = "https://accounts.google.com/gsi/client"; |
| 43 | + gsiScript.onload = function onGsiLoad() { |
| 44 | + resolve( |
| 45 | + window.google.accounts.oauth2.initTokenClient({ |
| 46 | + client_id: CLIENT_ID, |
| 47 | + scope: SCOPES, |
| 48 | + error_callback: "", // defined later |
| 49 | + callback: "", // defined later |
| 50 | + }), |
| 51 | + ); |
| 52 | + }; |
| 53 | + document.body.appendChild(gsiScript); |
50 | 54 | });
|
51 |
| - }; |
| 55 | + console.log("Token client created, loading client"); |
52 | 56 |
|
53 |
| - const boundary = "-------314159265358979323846"; |
54 |
| - const delimiter = "\r\n--" + boundary + "\r\n"; |
55 |
| - const close_delim = "\r\n--" + boundary + "--"; |
| 57 | + await this.gapi.load("client", async () => { |
| 58 | + console.log("Client loaded; initialising GAPI"); |
| 59 | + await this.gapi.client.init({ |
| 60 | + apiKey: API_KEY, |
| 61 | + discoveryDocs: [DISCOVERY_DOC], |
| 62 | + }); |
| 63 | + console.log("GAPI initialised"); |
| 64 | + }); |
| 65 | + console.log("Google Drive: available"); |
| 66 | + return true; |
| 67 | + } |
56 | 68 |
|
57 |
| - function listFiles() { |
58 |
| - return new Promise(function (resolve) { |
59 |
| - const retrievePageOfFiles = function (request, result) { |
60 |
| - request.execute(function (resp) { |
61 |
| - result = result.concat(resp.items); |
62 |
| - const nextPageToken = resp.nextPageToken; |
63 |
| - if (nextPageToken) { |
64 |
| - request = gapi.client.drive.files.list({ |
65 |
| - pageToken: nextPageToken, |
66 |
| - }); |
67 |
| - retrievePageOfFiles(request, result); |
68 |
| - } else { |
69 |
| - resolve(result); |
70 |
| - } |
71 |
| - }); |
| 69 | + authorize(imm) { |
| 70 | + if (this.authorized) return true; |
| 71 | + if (imm) return false; |
| 72 | + return new Promise((resolve, reject) => { |
| 73 | + console.log("Authorizing..."); |
| 74 | + this.tokenClient.callback = (resp) => { |
| 75 | + if (resp.error !== undefined) reject(resp); |
| 76 | + console.log("Authorized OK"); |
| 77 | + this.authorized = true; |
| 78 | + resolve(true); |
| 79 | + }; |
| 80 | + window.moo = this.tokenClient; // DO NOT CHECK IN |
| 81 | + this.tokenClient.error_callback = (resp) => { |
| 82 | + console.log(`Token client failure: ${resp.type}; failed to authorize`); |
| 83 | + reject(new Error(`Token client failure: ${resp.type}; failed to authorize`)); |
72 | 84 | };
|
73 |
| - retrievePageOfFiles( |
74 |
| - gapi.client.drive.files.list({ |
75 |
| - q: "mimeType = '" + MIME_TYPE + "'", |
76 |
| - }), |
77 |
| - [], |
78 |
| - ); |
| 85 | + this.tokenClient.requestAccessToken({ select_account: false }); |
79 | 86 | });
|
80 | 87 | }
|
81 | 88 |
|
82 |
| - function saveFile(name, data, idOrNone) { |
| 89 | + async listFiles() { |
| 90 | + let response = await this.gapi.client.drive.files.list({ |
| 91 | + q: `mimeType = '${MIME_TYPE}'`, |
| 92 | + }); |
| 93 | + let result = response.result.files; |
| 94 | + while (response.result.nextPageToken) { |
| 95 | + response = await this.gapi.client.drive.files.list({ |
| 96 | + pageToken: response.result.nextPageToken, |
| 97 | + }); |
| 98 | + result = result.concat(response.result.files); |
| 99 | + } |
| 100 | + return result; |
| 101 | + } |
| 102 | + |
| 103 | + saveFile(name, data, idOrNone) { |
83 | 104 | const metadata = {
|
84 |
| - title: name, |
85 |
| - parents: ["jsbeeb disc images"], // TODO: parents doesn't work; also should probably prevent overwriting this on every save |
| 105 | + name, |
| 106 | + // parents: ["jsbeeb disc images"], // TODO: parents doesn't work, need folder ID maybe? |
86 | 107 | mimeType: MIME_TYPE,
|
87 | 108 | };
|
88 | 109 |
|
89 | 110 | const str = utils.uint8ArrayToString(data);
|
90 | 111 | const base64Data = btoa(str);
|
91 |
| - const multipartRequestBody = |
92 |
| - delimiter + |
93 |
| - "Content-Type: application/json\r\n\r\n" + |
94 |
| - JSON.stringify(metadata) + |
95 |
| - delimiter + |
96 |
| - "Content-Type: " + |
97 |
| - MIME_TYPE + |
98 |
| - "\r\n" + |
99 |
| - "Content-Transfer-Encoding: base64\r\n" + |
100 |
| - "\r\n" + |
101 |
| - base64Data + |
102 |
| - close_delim; |
| 112 | + const multipartRequestBody = `${delimiter}Content-Type: application/json\r |
| 113 | +\r |
| 114 | +${JSON.stringify(metadata)}${delimiter}Content-Type: ${MIME_TYPE}\r |
| 115 | +Content-Transfer-Encoding: base64\r |
| 116 | +\r |
| 117 | +${base64Data}${close_delim}`; |
103 | 118 |
|
104 |
| - return gapi.client.request({ |
105 |
| - path: "/upload/drive/v2/files" + (idOrNone ? "/" + idOrNone : ""), |
106 |
| - method: idOrNone ? "PUT" : "POST", |
107 |
| - params: { uploadType: "multipart", newRevision: false }, |
| 119 | + return this.gapi.client.request({ |
| 120 | + path: `/upload/drive/v3/files${idOrNone ? `/${idOrNone}` : ""}`, |
| 121 | + method: idOrNone ? "PATCH" : "POST", |
| 122 | + params: { uploadType: "multipart", newRevision: false, fields: FILE_FIELDS }, |
108 | 123 | headers: {
|
109 |
| - "Content-Type": 'multipart/mixed; boundary="' + boundary + '"', |
| 124 | + "Content-Type": `multipart/mixed; boundary="${boundary}"`, |
110 | 125 | },
|
111 | 126 | body: multipartRequestBody,
|
112 | 127 | });
|
113 | 128 | }
|
114 | 129 |
|
115 |
| - function loadMetadata(fileId) { |
116 |
| - return gapi.client.drive.files.get({ fileId: fileId }); |
117 |
| - } |
118 |
| - |
119 |
| - self.create = function (fdc, name) { |
120 |
| - console.log("Google Drive: creating disc image: '" + name + "'"); |
| 130 | + async create(fdc, name) { |
| 131 | + console.log(`Google Drive: creating disc image: '${name}'`); |
121 | 132 | const byteSize = utils.discImageSize(name).byteSize;
|
122 | 133 | const data = new Uint8Array(byteSize);
|
123 | 134 | utils.setDiscName(data, name);
|
124 |
| - return saveFile(name, data).then(function (response) { |
125 |
| - const meta = response.result; |
126 |
| - return { fileId: meta.id, disc: makeDisc(fdc, data, meta) }; |
127 |
| - }); |
128 |
| - }; |
129 |
| - |
130 |
| - function downloadFile(file) { |
131 |
| - if (file.downloadUrl) { |
132 |
| - return new Promise(function (resolve, reject) { |
133 |
| - const accessToken = gapi.auth.getToken().access_token; |
134 |
| - const xhr = new XMLHttpRequest(); |
135 |
| - xhr.open("GET", file.downloadUrl, true); |
136 |
| - xhr.setRequestHeader("Authorization", "Bearer " + accessToken); |
137 |
| - xhr.overrideMimeType("text/plain; charset=x-user-defined"); |
138 |
| - |
139 |
| - xhr.onload = function () { |
140 |
| - if (xhr.status !== 200) { |
141 |
| - reject(new Error("Unable to load '" + file.title + "', http code " + xhr.status)); |
142 |
| - } else if (typeof xhr.response !== "string") { |
143 |
| - resolve(xhr.response); |
144 |
| - } else { |
145 |
| - resolve(utils.stringToUint8Array(xhr.response)); |
146 |
| - } |
147 |
| - }; |
148 |
| - xhr.onerror = function () { |
149 |
| - reject(new Error("Error sending request for " + file)); |
150 |
| - }; |
151 |
| - xhr.send(); |
152 |
| - }); |
153 |
| - } else { |
154 |
| - return Promise.resolve(null); |
155 |
| - } |
| 135 | + const response = await this.saveFile(name, data); |
| 136 | + const meta = response.result; |
| 137 | + return { fileId: meta.id, disc: this.makeDisc(fdc, data, meta) }; |
156 | 138 | }
|
157 | 139 |
|
158 |
| - function makeDisc(fdc, data, meta) { |
| 140 | + makeDisc(fdc, data, meta) { |
159 | 141 | let flusher = null;
|
160 |
| - const name = meta.title; |
161 |
| - if (meta.editable) { |
| 142 | + const name = meta.name; |
| 143 | + const id = meta.id; |
| 144 | + if (meta.capabilities.canEdit) { |
162 | 145 | console.log("Making editable disc");
|
163 |
| - flusher = _.debounce(function () { |
164 |
| - saveFile(this.name, this.data, meta.id).then(function () { |
165 |
| - console.log("Saved ok"); |
166 |
| - }); |
| 146 | + flusher = _.debounce(async (changedData) => { |
| 147 | + console.log("Data changed..."); |
| 148 | + await this.saveFile(name, changedData, id); |
| 149 | + console.log("Saved ok"); |
167 | 150 | }, 200);
|
168 | 151 | } else {
|
169 | 152 | console.log("Making read-only disc");
|
170 | 153 | }
|
171 | 154 | return discFor(fdc, name, data, flusher);
|
172 | 155 | }
|
173 | 156 |
|
174 |
| - self.load = function (fdc, fileId) { |
175 |
| - let meta = false; |
176 |
| - return loadMetadata(fileId) |
177 |
| - .then(function (response) { |
178 |
| - meta = response.result; |
179 |
| - return downloadFile(response.result); |
180 |
| - }) |
181 |
| - .then(function (data) { |
182 |
| - return makeDisc(fdc, data, meta); |
183 |
| - }); |
184 |
| - }; |
185 |
| - |
186 |
| - self.cat = listFiles; |
| 157 | + async load(fdc, fileId) { |
| 158 | + const meta = (await this.gapi.client.drive.files.get({ fileId: fileId, fields: FILE_FIELDS })).result; |
| 159 | + const data = (await this.gapi.client.drive.files.get({ fileId: fileId, alt: "media" })).body; |
| 160 | + return this.makeDisc(fdc, data, meta); |
| 161 | + } |
187 | 162 | }
|
0 commit comments