gdrive.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. /*
  2. * Copyright 2010-2019 Gildas Lormeau
  3. * contact : gildas.lormeau <at> gmail.com
  4. *
  5. * This file is part of SingleFile.
  6. *
  7. * The code in this file is free software: you can redistribute it and/or
  8. * modify it under the terms of the GNU Affero General Public License
  9. * (GNU AGPL) as published by the Free Software Foundation, either version 3
  10. * of the License, or (at your option) any later version.
  11. *
  12. * The code in this file is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
  15. * General Public License for more details.
  16. *
  17. * As additional permission under GNU AGPL version 3 section 7, you may
  18. * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU
  19. * AGPL normally required by section 4, provided you include this license
  20. * notice and a URL through which recipients can access the Corresponding
  21. * Source.
  22. */
  23. /* global browser, fetch, setInterval */
  24. this.GDrive = this.GDrive || (() => {
  25. "use strict";
  26. const TOKEN_URL = "https://oauth2.googleapis.com/token";
  27. const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
  28. const REVOKE_ACCESS_URL = "https://accounts.google.com/o/oauth2/revoke";
  29. const GDRIVE_URL = "https://www.googleapis.com/drive/v3/files";
  30. const GDRIVE_UPLOAD_URL = "https://www.googleapis.com/upload/drive/v3/files";
  31. class GDrive {
  32. constructor(clientId, scopes) {
  33. this.clientId = clientId;
  34. this.scopes = scopes;
  35. this.folderIds = new Map();
  36. setInterval(() => this.folderIds.clear(), 60 * 1000);
  37. }
  38. async auth(options = { interactive: true, auto: true, requestPermissionIdentity: true }) {
  39. try {
  40. if (options.requestPermissionIdentity) {
  41. await browser.permissions.request({ permissions: ["identity"] });
  42. }
  43. }
  44. catch (error) {
  45. // ignored;
  46. }
  47. if (this.managedToken()) {
  48. const token = await browser.identity.getAuthToken({ interactive: options.interactive });
  49. if (token) {
  50. this.accessToken = token;
  51. return { accessToken: this.accessToken };
  52. }
  53. } else {
  54. this.getAuthURL(options);
  55. return options.code ? authFromCode(this, options) : initAuth(this, options);
  56. }
  57. }
  58. managedToken() {
  59. return Boolean(browser.identity.getAuthToken);
  60. }
  61. setAuthInfo(authInfo) {
  62. if (!browser.identity.getAuthToken) {
  63. if (authInfo) {
  64. this.accessToken = authInfo.accessToken;
  65. this.refreshToken = authInfo.refreshToken;
  66. this.expirationDate = authInfo.expirationDate;
  67. } else {
  68. delete this.accessToken;
  69. delete this.refreshToken;
  70. delete this.expirationDate;
  71. }
  72. }
  73. }
  74. getAuthURL(options = {}) {
  75. this.redirectURI = encodeURIComponent("urn:ietf:wg:oauth:2.0:oob" + (options.auto ? ":auto" : ""));
  76. this.authURL = AUTH_URL +
  77. "?client_id=" + this.clientId +
  78. "&response_type=code" +
  79. "&access_type=offline" +
  80. "&redirect_uri=" + this.redirectURI +
  81. "&scope=" + this.scopes.join(" ");
  82. return this.authURL;
  83. }
  84. async refreshAuthToken() {
  85. if (this.clientId && this.refreshToken) {
  86. const httpResponse = await fetch(TOKEN_URL, {
  87. method: "POST",
  88. headers: { "Content-Type": "application/x-www-form-urlencoded" },
  89. body: "client_id=" + this.clientId +
  90. "&refresh_token=" + this.refreshToken +
  91. "&grant_type=refresh_token"
  92. });
  93. if (httpResponse.status == 400) {
  94. throw new Error("unknown_token");
  95. }
  96. const response = await getJSON(httpResponse);
  97. this.accessToken = response.access_token;
  98. if (response.refresh_token) {
  99. this.refreshToken = response.refresh_token;
  100. }
  101. if (response.expires_in) {
  102. this.expirationDate = Date.now() + (response.expires_in * 1000);
  103. }
  104. return { accessToken: this.accessToken, refreshToken: this.refreshToken, expirationDate: this.expirationDate };
  105. }
  106. }
  107. async revokeAuthToken(accessToken) {
  108. if (accessToken) {
  109. if (this.managedToken()) {
  110. await browser.identity.removeCachedAuthToken({ token: accessToken });
  111. }
  112. const httpResponse = await fetch(REVOKE_ACCESS_URL, {
  113. method: "POST",
  114. headers: { "Content-Type": "application/x-www-form-urlencoded" },
  115. body: "token=" + accessToken
  116. });
  117. try {
  118. await getJSON(httpResponse);
  119. }
  120. catch (error) {
  121. if (error.message != "invalid_token") {
  122. throw error;
  123. }
  124. }
  125. finally {
  126. delete this.accessToken;
  127. delete this.refreshToken;
  128. delete this.expirationDate;
  129. }
  130. }
  131. }
  132. async upload(fullFilename, blob, retry = true) {
  133. const parentFolderId = await getParentFolderId(this, fullFilename);
  134. const fileParts = fullFilename.split("/");
  135. const filename = fileParts.pop();
  136. const uploader = new MediaUploader({
  137. token: this.accessToken,
  138. file: blob,
  139. parents: [parentFolderId],
  140. filename
  141. });
  142. try {
  143. return await uploader.upload();
  144. }
  145. catch (error) {
  146. if (error.message == "path_not_found" && retry) {
  147. this.folderIds.clear();
  148. return this.upload(fullFilename, blob, false);
  149. } else {
  150. throw error;
  151. }
  152. }
  153. }
  154. }
  155. class MediaUploader {
  156. constructor(options) {
  157. this.file = options.file;
  158. this.contentType = this.file.type || "application/octet-stream";
  159. this.metadata = {
  160. name: options.filename,
  161. mimeType: this.contentType,
  162. parents: options.parents || ["root"]
  163. };
  164. this.token = options.token;
  165. this.offset = 0;
  166. this.chunkSize = options.chunkSize || 5 * 1024 * 1024;
  167. }
  168. async upload() {
  169. const httpResponse = getResponse(await fetch(GDRIVE_UPLOAD_URL + "?uploadType=resumable", {
  170. method: "POST",
  171. headers: {
  172. "Authorization": "Bearer " + this.token,
  173. "Content-Type": "application/json",
  174. "X-Upload-Content-Length": this.file.size,
  175. "X-Upload-Content-Type": this.contentType
  176. },
  177. body: JSON.stringify(this.metadata)
  178. }));
  179. const location = httpResponse.headers.get("Location");
  180. this.url = location;
  181. return sendFile(this);
  182. }
  183. }
  184. return GDrive;
  185. async function authFromCode(gdrive, options) {
  186. const httpResponse = await fetch(TOKEN_URL, {
  187. method: "POST",
  188. headers: { "Content-Type": "application/x-www-form-urlencoded" },
  189. body: "client_id=" + gdrive.clientId +
  190. "&grant_type=authorization_code" +
  191. "&code=" + options.code +
  192. "&redirect_uri=" + gdrive.redirectURI
  193. });
  194. const response = await getJSON(httpResponse);
  195. gdrive.accessToken = response.access_token;
  196. gdrive.refreshToken = response.refresh_token;
  197. gdrive.expirationDate = Date.now() + (response.expires_in * 1000);
  198. return { accessToken: gdrive.accessToken, refreshToken: gdrive.refreshToken, expirationDate: gdrive.expirationDate };
  199. }
  200. async function initAuth(gdrive, options) {
  201. try {
  202. return await browser.identity.launchWebAuthFlow({
  203. interactive: options.interactive,
  204. url: gdrive.authURL
  205. });
  206. }
  207. catch (error) {
  208. if (error.message && error.message.includes("access")) {
  209. throw new Error("code_required");
  210. } else {
  211. throw error;
  212. }
  213. }
  214. }
  215. async function getParentFolderId(gdrive, filename, retry = true) {
  216. const fileParts = filename.split("/");
  217. fileParts.pop();
  218. const folderId = gdrive.folderIds.get(fileParts.join("/"));
  219. if (folderId) {
  220. return folderId;
  221. }
  222. let parentFolderId = "root";
  223. if (fileParts.length) {
  224. let fullFolderName = "";
  225. for (const folderName of fileParts) {
  226. if (fullFolderName) {
  227. fullFolderName += "/";
  228. }
  229. fullFolderName += folderName;
  230. const folderId = gdrive.folderIds.get(fullFolderName);
  231. if (folderId) {
  232. parentFolderId = folderId;
  233. } else {
  234. try {
  235. parentFolderId = await getOrCreateFolder(gdrive, folderName, parentFolderId);
  236. gdrive.folderIds.set(fullFolderName, parentFolderId);
  237. } catch (error) {
  238. if (error.message == "path_not_found" && retry) {
  239. gdrive.folderIds.clear();
  240. return getParentFolderId(gdrive, filename, false);
  241. } else {
  242. throw error;
  243. }
  244. }
  245. }
  246. }
  247. }
  248. return parentFolderId;
  249. }
  250. async function getOrCreateFolder(gdrive, folderName, parentFolderId) {
  251. const response = await getFolder(gdrive, folderName, parentFolderId);
  252. if (response.files.length) {
  253. return response.files[0].id;
  254. } else {
  255. const response = await createFolder(gdrive, folderName, parentFolderId);
  256. return response.id;
  257. }
  258. }
  259. async function getFolder(gdrive, folderName, parentFolderId) {
  260. const httpResponse = await fetch(GDRIVE_URL + "?q=mimeType = 'application/vnd.google-apps.folder' and name = '" + folderName + "' and trashed != true and '" + parentFolderId + "' in parents", {
  261. headers: {
  262. "Authorization": "Bearer " + gdrive.accessToken
  263. }
  264. });
  265. return getJSON(httpResponse);
  266. }
  267. async function createFolder(gdrive, folderName, parentFolderId) {
  268. const httpResponse = await fetch(GDRIVE_URL, {
  269. method: "POST",
  270. headers: {
  271. "Authorization": "Bearer " + gdrive.accessToken,
  272. "Content-Type": "application/json"
  273. },
  274. body: JSON.stringify({
  275. name: folderName,
  276. parents: [parentFolderId],
  277. mimeType: "application/vnd.google-apps.folder"
  278. })
  279. });
  280. return getJSON(httpResponse);
  281. }
  282. async function sendFile(mediaUploader) {
  283. let content = mediaUploader.file, end = mediaUploader.file.size;
  284. if (mediaUploader.offset || mediaUploader.chunkSize) {
  285. if (mediaUploader.chunkSize) {
  286. end = Math.min(mediaUploader.offset + mediaUploader.chunkSize, mediaUploader.file.size);
  287. }
  288. content = content.slice(mediaUploader.offset, end);
  289. }
  290. const httpResponse = await fetch(mediaUploader.url, {
  291. method: "PUT",
  292. headers: {
  293. "Authorization": "Bearer " + mediaUploader.token,
  294. "Content-Type": mediaUploader.contentType,
  295. "Content-Range": "bytes " + mediaUploader.offset + "-" + (end - 1) + "/" + mediaUploader.file.size,
  296. "X-Upload-Content-Type": mediaUploader.contentType
  297. },
  298. body: content
  299. });
  300. if (httpResponse.status == 200 || httpResponse.status == 201) {
  301. return httpResponse.json();
  302. } else if (httpResponse.status == 308) {
  303. const range = httpResponse.headers.get("Range");
  304. if (range) {
  305. mediaUploader.offset = parseInt(range.match(/\d+/g).pop(), 10) + 1;
  306. }
  307. sendFile(mediaUploader);
  308. } else {
  309. getResponse(httpResponse);
  310. }
  311. }
  312. async function getJSON(httpResponse) {
  313. httpResponse = getResponse(httpResponse);
  314. const response = await httpResponse.json();
  315. if (response.error) {
  316. throw new Error(response.error);
  317. } else {
  318. return response;
  319. }
  320. }
  321. function getResponse(httpResponse) {
  322. if (httpResponse.status == 200) {
  323. return httpResponse;
  324. } else if (httpResponse.status == 404) {
  325. throw new Error("path_not_found");
  326. } else if (httpResponse.status == 401) {
  327. throw new Error("invalid_token");
  328. } else {
  329. throw new Error("unknown_error (" + httpResponse.status + ")");
  330. }
  331. }
  332. })();