Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | 1x 10x 10x 10x 3x 4x 4x 1x 1x 7x 3x 3x 1x 1x 2x 2x 1x 1x 2x 2x 1x 1x 1x 1x 1x 2x 2x 1x 1x 1x 1x 1x 1x | import * as disc from "../fdc.js";
import { GoogleDriveLoader } from "./google-drive.js";
import { toast } from "./toast.js";
import { errorText } from "./reporting.js";
import { replaceOrAddExtension } from "../archive.js";
import { describeDriveFile } from "./media-catalogue.js";
const isUnauthorised = (error) => error?.status === 401 || error?.result?.error?.code === 401;
/**
* Google Drive as a media source: signing in, listing the user's discs,
* loading one, and creating a new one there.
*/
export class GoogleDriveSource {
constructor({ media, loader = new GoogleDriveLoader() }) {
this.googleDrive = loader;
media.addSource("drive", (cat, layout) => this.load(cat, layout));
media.addLister("gdrive", async () =>
this.connected ? (await this.withToken(() => this.googleDrive.listFiles())).map(describeDriveFile) : [],
);
}
/** Runs a Drive call; a 401 means the token has lapsed, so the account is connected no longer. */
async withToken(call) {
try {
return await call();
} catch (error) {
Eif (isUnauthorised(error)) this.googleDrive.authorized = false;
// gapi rejects with a response object; its message is a level down.
throw error instanceof Error ? error : new Error(error?.result?.error?.message ?? errorText(error));
}
}
get connected() {
return !!this.googleDrive.authorized;
}
/** Loads the Google client and signs in; false when either is refused. Needs a user gesture. */
async connect() {
try {
await this.googleDrive.initialise();
} catch (error) {
toast(`Google Drive is unavailable: ${errorText(error)}`, { title: "Google Drive" });
return false;
}
try {
return await this.googleDrive.authorize(false);
} catch (error) {
toast(`There was an error accessing your Google Drive account: ${errorText(error)}`, {
title: "Google Drive",
});
return false;
}
}
/**
* A disc from the Drive, once signed in. Signing in needs a click, so a
* load from elsewhere (the URL at startup, say) can only ask for one.
*/
async load(cat, layout) {
Iif (!(await this.googleDrive.initialise())) throw new Error("Google Drive is not available");
if (!(await this.googleDrive.authorize(true)))
throw new Error("Google Drive is not connected; use Connect Google Drive in the media window first");
const loaded = await this.withToken(() => this.googleDrive.load(cat.id, layout));
Eif (!loaded.savesChanges) {
toast(`${cat.name} is read only on Google Drive, so changes to it are not written back.`, {
title: "Google Drive",
quietKey: "quietDriveReadOnly",
});
}
return loaded;
}
/**
* Makes a blank, formatted disc on the Drive.
*
* @returns {Promise<{ref: string, disc: import("../disc.js").Disc}>} the disc, and how the URL names it
*/
async createBlank(name, layout) {
// TODO(#1070) blank HFE images have no fixed byteSize, so only SSD and DSD blanks can be made here.
const discType = disc.guessDiscTypeFromName(name);
if (!discType.byteSize) throw new Error(`blank ${discType.extension} discs have no known size`);
const data = new Uint8Array(discType.byteSize);
Eif (discType.supportsCatalogue) discType.setDiscName(data, name);
return this.createFrom(name, data, layout);
}
/** Puts an image on the Drive under `name`, as a disc that saves its changes there. */
async createFrom(name, data, layout) {
const fullName = replaceOrAddExtension(name, disc.guessDiscTypeFromName(name).extension);
const result = await this.withToken(() => this.googleDrive.create(fullName, data, layout));
return { ref: `gd:${result.fileId}/${fullName}`, disc: result.disc };
}
}
|