Skip to content
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

Move to the new Google APIs for auth and drive #465

Merged
merged 6 commits into from
Jan 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .idea/runConfigurations/Debug.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

286 changes: 131 additions & 155 deletions src/google-drive.js
Original file line number Diff line number Diff line change
@@ -1,187 +1,163 @@
"use strict";

import _ from "underscore";
import * as utils from "./utils.js";
import { discFor } from "./fdc.js";

export function GoogleDriveLoader() {
const self = this;
const MIME_TYPE = "application/vnd.jsbeeb.disc-image";
const CLIENT_ID = "356883185894-bhim19837nroivv18p0j25gecora60r5.apps.googleusercontent.com";
const SCOPES = "https://www.googleapis.com/auth/drive.file";
let gapi = null;

self.initialise = function () {
return new Promise(function (resolve) {
// https://github.com/google/google-api-javascript-client/issues/319
const gapiScript = document.createElement("script");
gapiScript.src = "https://apis.google.com/js/client.js?onload=__onGapiLoad__";
window.__onGapiLoad__ = function onGapiLoad() {
gapi = window.gapi;
gapi.client.load("drive", "v2", function () {
console.log("Google Drive: available");
resolve(true);
});
};
document.body.appendChild(gapiScript);
const MIME_TYPE = "application/vnd.jsbeeb.disc-image";
const CLIENT_ID = "356883185894-bhim19837nroivv18p0j25gecora60r5.apps.googleusercontent.com";
const SCOPES = "https://www.googleapis.com/auth/drive.file";
const DISCOVERY_DOC = "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest";
const FILE_FIELDS = "id,name,capabilities";
const PARENT_FOLDER_NAME = "jsbeeb disc images";

const boundary = "-------314159265358979323846";
const delimiter = `\r\n--${boundary}\r\n`;
const close_delim = `\r\n--${boundary}--`;

const FOLDER_MIME_TYPE = "application/vnd.google-apps.folder";

export class GoogleDriveLoader {
constructor() {
this.authorized = false;
this.parentFolderId = undefined;
this.driveClient = undefined;
}

async initialise() {
console.log("Creating GAPI");
const gapi = await this._loadScript("https://apis.google.com/js/api.js", () => window.gapi);
console.log("Got GAPI, creating token client");
this.tokenClient = await this._loadScript("https://accounts.google.com/gsi/client", () => {
return window.google.accounts.oauth2.initTokenClient({
client_id: CLIENT_ID,
scope: SCOPES,
error_callback: "", // defined later
callback: "", // defined later
});
});
};

self.authorize = function (immediate) {
return new Promise(function (resolve, reject) {
console.log("Authorizing", immediate);
gapi.auth.authorize(
{
client_id: CLIENT_ID,
scope: SCOPES,
immediate: immediate,
},
function (authResult) {
if (authResult && !authResult.error) {
console.log("Google Drive: authorized");
resolve(true);
} else if (authResult && authResult.error && !immediate) {
reject(new Error(authResult.error));
} else {
console.log("Google Drive: Need to auth");
resolve(false);
}
},
);
console.log("Token client created, loading client");

await gapi.load("client", async () => {
console.log("Client loaded; initialising GAPI");
await gapi.client.init({ discoveryDocs: [DISCOVERY_DOC] });
console.log("GAPI initialised");
this.driveClient = gapi.client.drive;
});
};

const boundary = "-------314159265358979323846";
const delimiter = "\r\n--" + boundary + "\r\n";
const close_delim = "\r\n--" + boundary + "--";

function listFiles() {
return new Promise(function (resolve) {
const retrievePageOfFiles = function (request, result) {
request.execute(function (resp) {
result = result.concat(resp.items);
const nextPageToken = resp.nextPageToken;
if (nextPageToken) {
request = gapi.client.drive.files.list({
pageToken: nextPageToken,
});
retrievePageOfFiles(request, result);
} else {
resolve(result);
}
});
console.log("Google Drive: available");
return true;
}

_loadScript(src, onload) {
// https://github.com/google/google-api-javascript-client/issues/319
return new Promise((resolve) => {
const script = document.createElement("script");
script.src = src;
script.onload = () => resolve(onload());
document.body.appendChild(script);
});
}

authorize(imm) {
if (this.authorized) return true;
if (imm) return false;
return new Promise((resolve, reject) => {
console.log("Authorizing...");
this.tokenClient.callback = (resp) => {
if (resp.error !== undefined) reject(resp);
console.log("Authorized OK");
this.authorized = true;
resolve(true);
};
this.tokenClient.error_callback = (resp) => {
console.log(`Token client failure: ${resp.type}; failed to authorize`);
reject(new Error(`Token client failure: ${resp.type}; failed to authorize`));
};
retrievePageOfFiles(
gapi.client.drive.files.list({
q: "mimeType = '" + MIME_TYPE + "'",
}),
[],
);
this.tokenClient.requestAccessToken({ select_account: false });
});
}

async listFiles() {
let response = await this.driveClient.files.list({ q: `mimeType = '${MIME_TYPE}' and trashed = false` });
let result = response.result.files;
while (response.result.nextPageToken) {
response = await this.driveClient.files.list({ pageToken: response.result.nextPageToken });
result = result.concat(response.result.files);
}
return result;
}

async _findOrCreateParentFolder() {
const list = await this.driveClient.files.list({
q: `name = '${PARENT_FOLDER_NAME}' and mimeType = '${FOLDER_MIME_TYPE}' and trashed = false`,
corpora: "user",
});
if (list.result.files.length === 1) {
console.log("Found existing parent folder");
return list.result.files[0].id;
}
console.log(`Creating parent folder ${PARENT_FOLDER_NAME}`);
const file = await this.driveClient.files.create({
resource: { name: PARENT_FOLDER_NAME, mimeType: FOLDER_MIME_TYPE },
fields: "id",
});
console.log("Folder Id:", file.result.id);
return file.result.id;
}

function saveFile(name, data, idOrNone) {
const metadata = {
title: name,
parents: ["jsbeeb disc images"], // TODO: parents doesn't work; also should probably prevent overwriting this on every save
mimeType: MIME_TYPE,
};
async saveFile(name, data, idOrNone) {
if (this.parentFolderId === undefined) {
this.parentFolderId = await this._findOrCreateParentFolder();
}
const metadata = { name, mimeType: MIME_TYPE };
if (!idOrNone) metadata.parents = [this.parentFolderId];

const str = utils.uint8ArrayToString(data);
const base64Data = btoa(str);
const base64Data = btoa(utils.uint8ArrayToString(data));
const multipartRequestBody =
delimiter +
"Content-Type: application/json\r\n\r\n" +
JSON.stringify(metadata) +
delimiter +
"Content-Type: " +
MIME_TYPE +
"\r\n" +
"Content-Transfer-Encoding: base64\r\n" +
"\r\n" +
base64Data +
close_delim;

return gapi.client.request({
path: "/upload/drive/v2/files" + (idOrNone ? "/" + idOrNone : ""),
method: idOrNone ? "PUT" : "POST",
params: { uploadType: "multipart", newRevision: false },
headers: {
"Content-Type": 'multipart/mixed; boundary="' + boundary + '"',
},
`${delimiter}Content-Type: application/json\r\n\r\n` +
`${JSON.stringify(metadata)}${delimiter}` +
`Content-Type: ${MIME_TYPE}\r\nContent-Transfer-Encoding: base64\r\n\r\n` +
`${base64Data}${close_delim}`;

return this.gapi.client.request({
path: `/upload/drive/v3/files${idOrNone ? `/${idOrNone}` : ""}`,
method: idOrNone ? "PATCH" : "POST",
params: { uploadType: "multipart", newRevision: false, fields: FILE_FIELDS },
headers: { "Content-Type": `multipart/mixed; boundary="${boundary}"` },
body: multipartRequestBody,
});
}

function loadMetadata(fileId) {
return gapi.client.drive.files.get({ fileId: fileId });
}

self.create = function (fdc, name) {
console.log("Google Drive: creating disc image: '" + name + "'");
async create(fdc, name) {
console.log(`Google Drive: creating disc image: '${name}'`);
const byteSize = utils.discImageSize(name).byteSize;
const data = new Uint8Array(byteSize);
utils.setDiscName(data, name);
return saveFile(name, data).then(function (response) {
const meta = response.result;
return { fileId: meta.id, disc: makeDisc(fdc, data, meta) };
});
};

function downloadFile(file) {
if (file.downloadUrl) {
return new Promise(function (resolve, reject) {
const accessToken = gapi.auth.getToken().access_token;
const xhr = new XMLHttpRequest();
xhr.open("GET", file.downloadUrl, true);
xhr.setRequestHeader("Authorization", "Bearer " + accessToken);
xhr.overrideMimeType("text/plain; charset=x-user-defined");

xhr.onload = function () {
if (xhr.status !== 200) {
reject(new Error("Unable to load '" + file.title + "', http code " + xhr.status));
} else if (typeof xhr.response !== "string") {
resolve(xhr.response);
} else {
resolve(utils.stringToUint8Array(xhr.response));
}
};
xhr.onerror = function () {
reject(new Error("Error sending request for " + file));
};
xhr.send();
});
} else {
return Promise.resolve(null);
}
const response = await this.saveFile(name, data);
const meta = response.result;
return { fileId: meta.id, disc: this.makeDisc(fdc, data, meta) };
}

function makeDisc(fdc, data, meta) {
makeDisc(fdc, data, meta) {
let flusher = null;
const name = meta.title;
if (meta.editable) {
const name = meta.name;
const id = meta.id;
if (meta.capabilities.canEdit) {
console.log("Making editable disc");
flusher = _.debounce(function () {
saveFile(this.name, this.data, meta.id).then(function () {
console.log("Saved ok");
});
flusher = _.debounce(async (changedData) => {
console.log("Data changed...");
await this.saveFile(name, changedData, id);
console.log("Saved ok");
}, 200);
} else {
console.log("Making read-only disc");
}
return discFor(fdc, name, data, flusher);
}

self.load = function (fdc, fileId) {
let meta = false;
return loadMetadata(fileId)
.then(function (response) {
meta = response.result;
return downloadFile(response.result);
})
.then(function (data) {
return makeDisc(fdc, data, meta);
});
};

self.cat = listFiles;
async load(fdc, fileId) {
const meta = (await this.driveClient.files.get({ fileId, fields: FILE_FIELDS })).result;
const data = (await this.driveClient.files.get({ fileId, alt: "media" })).body;
return this.makeDisc(fdc, data, meta);
}
}
Loading