| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386 |
- /*
- * Copyright 2010-2020 Gildas Lormeau
- * contact : gildas.lormeau <at> gmail.com
- *
- * This file is part of SingleFile.
- *
- * The code in this file is free software: you can redistribute it and/or
- * modify it under the terms of the GNU Affero General Public License
- * (GNU AGPL) as published by the Free Software Foundation, either version 3
- * of the License, or (at your option) any later version.
- *
- * The code in this file is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
- * General Public License for more details.
- *
- * As additional permission under GNU AGPL version 3 section 7, you may
- * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU
- * AGPL normally required by section 4, provided you include this license
- * notice and a URL through which recipients can access the Corresponding
- * Source.
- */
- /* global browser, fetch, setInterval */
- const TOKEN_URL = "https://oauth2.googleapis.com/token";
- const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
- const REVOKE_ACCESS_URL = "https://accounts.google.com/o/oauth2/revoke";
- const GDRIVE_URL = "https://www.googleapis.com/drive/v3/files";
- const GDRIVE_UPLOAD_URL = "https://www.googleapis.com/upload/drive/v3/files";
- let requestPermissionIdentityNeeded = true;
- class GDrive {
- constructor(clientId, scopes) {
- this.clientId = clientId;
- this.scopes = scopes;
- this.folderIds = new Map();
- setInterval(() => this.folderIds.clear(), 60 * 1000);
- }
- async auth(options = { interactive: true, auto: true }) {
- if (options.requestPermissionIdentity && requestPermissionIdentityNeeded) {
- try {
- await browser.permissions.request({ permissions: ["identity"] });
- requestPermissionIdentityNeeded = false;
- }
- catch (error) {
- // ignored;
- }
- }
- if (nativeAuth(options)) {
- this.accessToken = await browser.identity.getAuthToken({ interactive: options.interactive });
- return { revokableAccessToken: this.accessToken };
- } else {
- getAuthURL(this, options);
- return options.code ? authFromCode(this, options) : initAuth(this, options);
- }
- }
- setAuthInfo(authInfo, options) {
- if (!nativeAuth(options)) {
- if (authInfo) {
- this.accessToken = authInfo.accessToken;
- this.refreshToken = authInfo.refreshToken;
- this.expirationDate = authInfo.expirationDate;
- } else {
- delete this.accessToken;
- delete this.refreshToken;
- delete this.expirationDate;
- }
- }
- }
- async refreshAuthToken() {
- if (this.refreshToken) {
- const httpResponse = await fetch(TOKEN_URL, {
- method: "POST",
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
- body: "client_id=" + this.clientId +
- "&refresh_token=" + this.refreshToken +
- "&grant_type=refresh_token"
- });
- if (httpResponse.status == 400) {
- throw new Error("unknown_token");
- }
- const response = await getJSON(httpResponse);
- this.accessToken = response.access_token;
- if (response.refresh_token) {
- this.refreshToken = response.refresh_token;
- }
- if (response.expires_in) {
- this.expirationDate = Date.now() + (response.expires_in * 1000);
- }
- return { accessToken: this.accessToken, refreshToken: this.refreshToken, expirationDate: this.expirationDate };
- }
- }
- async revokeAuthToken(accessToken) {
- if (accessToken) {
- if (browser.identity && browser.identity.removeCachedAuthToken) {
- try {
- await browser.identity.removeCachedAuthToken({ token: accessToken });
- } catch (error) {
- // ignored
- }
- }
- const httpResponse = await fetch(REVOKE_ACCESS_URL, {
- method: "POST",
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
- body: "token=" + accessToken
- });
- try {
- await getJSON(httpResponse);
- }
- catch (error) {
- if (error.message != "invalid_token") {
- throw error;
- }
- }
- finally {
- delete this.accessToken;
- delete this.refreshToken;
- delete this.expirationDate;
- }
- }
- }
- async upload(fullFilename, blob, options, retry = true) {
- const parentFolderId = await getParentFolderId(this, fullFilename);
- const fileParts = fullFilename.split("/");
- const filename = fileParts.pop();
- const uploader = new MediaUploader({
- token: this.accessToken,
- file: blob,
- parents: [parentFolderId],
- filename,
- onProgress: options.onProgress
- });
- try {
- return {
- cancelUpload: () => uploader.cancelled = true,
- uploadPromise: uploader.upload()
- };
- }
- catch (error) {
- if (error.message == "path_not_found" && retry) {
- this.folderIds.clear();
- return this.upload(fullFilename, blob, options, false);
- } else {
- throw error;
- }
- }
- }
- }
- class MediaUploader {
- constructor(options) {
- this.file = options.file;
- this.onProgress = options.onProgress;
- this.contentType = this.file.type || "application/octet-stream";
- this.metadata = {
- name: options.filename,
- mimeType: this.contentType,
- parents: options.parents || ["root"]
- };
- this.token = options.token;
- this.offset = 0;
- this.chunkSize = options.chunkSize || 512 * 1024;
- }
- async upload() {
- const httpResponse = getResponse(await fetch(GDRIVE_UPLOAD_URL + "?uploadType=resumable", {
- method: "POST",
- headers: {
- "Authorization": "Bearer " + this.token,
- "Content-Type": "application/json",
- "X-Upload-Content-Length": this.file.size,
- "X-Upload-Content-Type": this.contentType
- },
- body: JSON.stringify(this.metadata)
- }));
- const location = httpResponse.headers.get("Location");
- this.url = location;
- if (!this.cancelled) {
- if (this.onProgress) {
- this.onProgress(0, this.file.size);
- }
- return sendFile(this);
- }
- }
- }
- export { GDrive };
- async function authFromCode(gdrive, options) {
- const httpResponse = await fetch(TOKEN_URL, {
- method: "POST",
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
- body: "client_id=" + gdrive.clientId +
- "&grant_type=authorization_code" +
- "&code=" + options.code +
- "&redirect_uri=" + gdrive.redirectURI
- });
- const response = await getJSON(httpResponse);
- gdrive.accessToken = response.access_token;
- gdrive.refreshToken = response.refresh_token;
- gdrive.expirationDate = Date.now() + (response.expires_in * 1000);
- return { accessToken: gdrive.accessToken, refreshToken: gdrive.refreshToken, expirationDate: gdrive.expirationDate };
- }
- async function initAuth(gdrive, options) {
- let code;
- if (options.extractAuthCode) {
- options.extractAuthCode(getAuthURL(gdrive, options))
- .then(authCode => code = authCode)
- .catch(() => { /* ignored */ });
- }
- try {
- if (browser.identity && browser.identity.launchWebAuthFlow && !options.forceWebAuthFlow) {
- return await browser.identity.launchWebAuthFlow({
- interactive: options.interactive,
- url: gdrive.authURL
- });
- } else if (options.launchWebAuthFlow) {
- return await options.launchWebAuthFlow({ url: gdrive.authURL });
- } else {
- throw new Error("auth_not_supported");
- }
- }
- catch (error) {
- if (error.message && (error.message == "code_required" || error.message.includes("access"))) {
- if (!options.auto && !code && options.promptAuthCode) {
- code = await options.promptAuthCode();
- }
- if (code) {
- options.code = code;
- return await authFromCode(gdrive, options);
- } else {
- throw new Error("code_required");
- }
- } else {
- throw error;
- }
- }
- }
- function getAuthURL(gdrive, options = {}) {
- gdrive.redirectURI = encodeURIComponent("urn:ietf:wg:oauth:2.0:oob" + (options.auto ? ":auto" : ""));
- gdrive.authURL = AUTH_URL +
- "?client_id=" + gdrive.clientId +
- "&response_type=code" +
- "&access_type=offline" +
- "&redirect_uri=" + gdrive.redirectURI +
- "&scope=" + gdrive.scopes.join(" ");
- return gdrive.authURL;
- }
- function nativeAuth(options = {}) {
- return Boolean(browser.identity && browser.identity.getAuthToken) && !options.forceWebAuthFlow;
- }
- async function getParentFolderId(gdrive, filename, retry = true) {
- const fileParts = filename.split("/");
- fileParts.pop();
- const folderId = gdrive.folderIds.get(fileParts.join("/"));
- if (folderId) {
- return folderId;
- }
- let parentFolderId = "root";
- if (fileParts.length) {
- let fullFolderName = "";
- for (const folderName of fileParts) {
- if (fullFolderName) {
- fullFolderName += "/";
- }
- fullFolderName += folderName;
- const folderId = gdrive.folderIds.get(fullFolderName);
- if (folderId) {
- parentFolderId = folderId;
- } else {
- try {
- parentFolderId = await getOrCreateFolder(gdrive, folderName, parentFolderId);
- gdrive.folderIds.set(fullFolderName, parentFolderId);
- } catch (error) {
- if (error.message == "path_not_found" && retry) {
- gdrive.folderIds.clear();
- return getParentFolderId(gdrive, filename, false);
- } else {
- throw error;
- }
- }
- }
- }
- }
- return parentFolderId;
- }
- async function getOrCreateFolder(gdrive, folderName, parentFolderId) {
- const response = await getFolder(gdrive, folderName, parentFolderId);
- if (response.files.length) {
- return response.files[0].id;
- } else {
- const response = await createFolder(gdrive, folderName, parentFolderId);
- return response.id;
- }
- }
- async function getFolder(gdrive, folderName, parentFolderId) {
- const httpResponse = await fetch(GDRIVE_URL + "?q=mimeType = 'application/vnd.google-apps.folder' and name = '" + folderName + "' and trashed != true and '" + parentFolderId + "' in parents", {
- headers: {
- "Authorization": "Bearer " + gdrive.accessToken
- }
- });
- return getJSON(httpResponse);
- }
- async function createFolder(gdrive, folderName, parentFolderId) {
- const httpResponse = await fetch(GDRIVE_URL, {
- method: "POST",
- headers: {
- "Authorization": "Bearer " + gdrive.accessToken,
- "Content-Type": "application/json"
- },
- body: JSON.stringify({
- name: folderName,
- parents: [parentFolderId],
- mimeType: "application/vnd.google-apps.folder"
- })
- });
- return getJSON(httpResponse);
- }
- async function sendFile(mediaUploader) {
- let content = mediaUploader.file, end = mediaUploader.file.size;
- if (mediaUploader.offset || mediaUploader.chunkSize) {
- if (mediaUploader.chunkSize) {
- end = Math.min(mediaUploader.offset + mediaUploader.chunkSize, mediaUploader.file.size);
- }
- content = content.slice(mediaUploader.offset, end);
- }
- const httpResponse = await fetch(mediaUploader.url, {
- method: "PUT",
- headers: {
- "Authorization": "Bearer " + mediaUploader.token,
- "Content-Type": mediaUploader.contentType,
- "Content-Range": "bytes " + mediaUploader.offset + "-" + (end - 1) + "/" + mediaUploader.file.size,
- "X-Upload-Content-Type": mediaUploader.contentType
- },
- body: content
- });
- if (mediaUploader.onProgress && !mediaUploader.cancelled) {
- mediaUploader.onProgress(mediaUploader.offset + mediaUploader.chunkSize, mediaUploader.file.size);
- }
- if (httpResponse.status == 200 || httpResponse.status == 201) {
- return httpResponse.json();
- } else if (httpResponse.status == 308) {
- const range = httpResponse.headers.get("Range");
- if (range) {
- mediaUploader.offset = parseInt(range.match(/\d+/g).pop(), 10) + 1;
- }
- if (mediaUploader.cancelled) {
- throw new Error("upload_cancelled");
- } else {
- return sendFile(mediaUploader);
- }
- } else {
- getResponse(httpResponse);
- }
- }
- async function getJSON(httpResponse) {
- httpResponse = getResponse(httpResponse);
- const response = await httpResponse.json();
- if (response.error) {
- throw new Error(response.error);
- } else {
- return response;
- }
- }
- function getResponse(httpResponse) {
- if (httpResponse.status == 200) {
- return httpResponse;
- } else if (httpResponse.status == 404) {
- throw new Error("path_not_found");
- } else if (httpResponse.status == 401) {
- throw new Error("invalid_token");
- } else {
- throw new Error("unknown_error (" + httpResponse.status + ")");
- }
- }
|