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

[v4.x backport] @uppy/google-photos: add plugin #5265

Merged
merged 2 commits into from
Jun 19, 2024
Merged

Conversation

aduh95
Copy link
Member

@aduh95 aduh95 commented Jun 19, 2024

Backport of #5061

Copy link
Contributor

github-actions bot commented Jun 19, 2024

Diff output files
diff --git a/packages/@uppy/companion/lib/config/grant.d.ts b/packages/@uppy/companion/lib/config/grant.d.ts
index f7df73c..97c46b1 100644
--- a/packages/@uppy/companion/lib/config/grant.d.ts
+++ b/packages/@uppy/companion/lib/config/grant.d.ts
@@ -1,12 +1,29 @@
 declare function _exports(): {
-    google: {
-        transport: string;
+    googledrive: {
+        callback: string;
         scope: string[];
+        transport: string;
+        custom_params: {
+            access_type: string;
+            prompt: string;
+        };
+        authorize_url: string;
+        access_url: string;
+        oauth: number;
+        scope_delimiter: string;
+    };
+    googlephotos: {
         callback: string;
+        scope: string[];
+        transport: string;
         custom_params: {
             access_type: string;
             prompt: string;
         };
+        authorize_url: string;
+        access_url: string;
+        oauth: number;
+        scope_delimiter: string;
     };
     dropbox: {
         transport: string;
diff --git a/packages/@uppy/companion/lib/config/grant.js b/packages/@uppy/companion/lib/config/grant.js
index 861adf7..6f113e3 100644
--- a/packages/@uppy/companion/lib/config/grant.js
+++ b/packages/@uppy/companion/lib/config/grant.js
@@ -1,21 +1,36 @@
 "use strict";
 Object.defineProperty(exports, "__esModule", { value: true });
+const google = {
+  transport: "session",
+  // access_type: offline is needed in order to get refresh tokens.
+  // prompt: 'consent' is needed because sometimes a user will get stuck in an authenticated state where we will
+  // receive no refresh tokens from them. This seems to be happen when running on different subdomains.
+  // therefore to be safe that we always get refresh tokens, we set this.
+  // https://stackoverflow.com/questions/10827920/not-receiving-google-oauth-refresh-token/65108513#65108513
+  custom_params: { access_type: "offline", prompt: "consent" },
+  // copied from https://github.com/simov/grant/blob/master/config/oauth.json
+  "authorize_url": "https://accounts.google.com/o/oauth2/v2/auth",
+  "access_url": "https://oauth2.googleapis.com/token",
+  "oauth": 2,
+  "scope_delimiter": " ",
+};
 // oauth configuration for provider services that are used.
 module.exports = () => {
   return {
-    // for drive
-    google: {
-      transport: "session",
-      scope: [
-        "https://www.googleapis.com/auth/drive.readonly",
-      ],
+    // we need separate auth providers because scopes are different,
+    // and because it would be a too big rewrite to allow reuse of the same provider.
+    googledrive: {
+      ...google,
       callback: "/drive/callback",
-      // access_type: offline is needed in order to get refresh tokens.
-      // prompt: 'consent' is needed because sometimes a user will get stuck in an authenticated state where we will
-      // receive no refresh tokens from them. This seems to be happen when running on different subdomains.
-      // therefore to be safe that we always get refresh tokens, we set this.
-      // https://stackoverflow.com/questions/10827920/not-receiving-google-oauth-refresh-token/65108513#65108513
-      custom_params: { access_type: "offline", prompt: "consent" },
+      scope: ["https://www.googleapis.com/auth/drive.readonly"],
+    },
+    googlephotos: {
+      ...google,
+      callback: "/googlephotos/callback",
+      scope: [
+        "https://www.googleapis.com/auth/photoslibrary.readonly",
+        "https://www.googleapis.com/auth/userinfo.email",
+      ], // if name is needed, then add https://www.googleapis.com/auth/userinfo.profile too
     },
     dropbox: {
       transport: "session",
diff --git a/packages/@uppy/companion/lib/server/controllers/get.js b/packages/@uppy/companion/lib/server/controllers/get.js
index c3b5129..44c2d07 100644
--- a/packages/@uppy/companion/lib/server/controllers/get.js
+++ b/packages/@uppy/companion/lib/server/controllers/get.js
@@ -10,10 +10,7 @@ async function get(req, res) {
   async function getSize() {
     return provider.size({ id, token: accessToken, query: req.query });
   }
-  async function download() {
-    const { stream } = await provider.download({ id, token: accessToken, providerUserSession, query: req.query });
-    return stream;
-  }
+  const download = () => provider.download({ id, token: accessToken, providerUserSession, query: req.query });
   try {
     await startDownUpload({ req, res, getSize, download });
   } catch (err) {
diff --git a/packages/@uppy/companion/lib/server/controllers/url.js b/packages/@uppy/companion/lib/server/controllers/url.js
index 67a67aa..3e0eff3 100644
--- a/packages/@uppy/companion/lib/server/controllers/url.js
+++ b/packages/@uppy/companion/lib/server/controllers/url.js
@@ -24,8 +24,8 @@ const downloadURL = async (url, allowLocalIPs, traceId) => {
   try {
     const protectedGot = await getProtectedGot({ allowLocalIPs });
     const stream = protectedGot.stream.get(url, { responseType: "json" });
-    await prepareStream(stream);
-    return stream;
+    const { size } = await prepareStream(stream);
+    return { stream, size };
   } catch (err) {
     logger.error(err, "controller.url.download.error", traceId);
     throw err;
@@ -71,9 +71,7 @@ const get = async (req, res) => {
     const { size } = await getURLMeta(req.body.url, allowLocalUrls);
     return size;
   }
-  async function download() {
-    return downloadURL(req.body.url, allowLocalUrls, req.id);
-  }
+  const download = () => downloadURL(req.body.url, allowLocalUrls, req.id);
   try {
     await startDownUpload({ req, res, getSize, download });
   } catch (err) {
diff --git a/packages/@uppy/companion/lib/server/helpers/oauth-state.d.ts b/packages/@uppy/companion/lib/server/helpers/oauth-state.d.ts
index f1c2709..f2b204d 100644
--- a/packages/@uppy/companion/lib/server/helpers/oauth-state.d.ts
+++ b/packages/@uppy/companion/lib/server/helpers/oauth-state.d.ts
@@ -1,4 +1,5 @@
 export function encodeState(state: any, secret: any): string;
+export function decodeState(state: any, secret: any): any;
 export function generateState(): {
     id: string;
 };
diff --git a/packages/@uppy/companion/lib/server/helpers/oauth-state.js b/packages/@uppy/companion/lib/server/helpers/oauth-state.js
index e7704f4..ebcae43 100644
--- a/packages/@uppy/companion/lib/server/helpers/oauth-state.js
+++ b/packages/@uppy/companion/lib/server/helpers/oauth-state.js
@@ -6,7 +6,7 @@ module.exports.encodeState = (state, secret) => {
   const encodedState = Buffer.from(JSON.stringify(state)).toString("base64");
   return encrypt(encodedState, secret);
 };
-const decodeState = (state, secret) => {
+module.exports.decodeState = (state, secret) => {
   const encodedState = decrypt(state, secret);
   return JSON.parse(atob(encodedState));
 };
@@ -16,7 +16,7 @@ module.exports.generateState = () => {
   };
 };
 module.exports.getFromState = (state, name, secret) => {
-  return decodeState(state, secret)[name];
+  return module.exports.decodeState(state, secret)[name];
 };
 module.exports.getGrantDynamicFromRequest = (req) => {
   return req.session.grant?.dynamic ?? {};
diff --git a/packages/@uppy/companion/lib/server/helpers/upload.js b/packages/@uppy/companion/lib/server/helpers/upload.js
index 92991a8..4d60174 100644
--- a/packages/@uppy/companion/lib/server/helpers/upload.js
+++ b/packages/@uppy/companion/lib/server/helpers/upload.js
@@ -5,12 +5,20 @@ const logger = require("../logger");
 const { respondWithError } = require("../provider/error");
 async function startDownUpload({ req, res, getSize, download }) {
   try {
-    const size = await getSize();
+    logger.debug("Starting download stream.", null, req.id);
+    const { stream, size: maybeSize } = await download();
+    let size;
+    // if the provider already knows the size, we can use that
+    if (typeof maybeSize === "number" && !Number.isNaN(maybeSize) && maybeSize > 0) {
+      size = maybeSize;
+    }
+    // if not we need to get the size
+    if (size == null) {
+      size = await getSize();
+    }
     const { clientSocketConnectTimeout } = req.companion.options;
     logger.debug("Instantiating uploader.", null, req.id);
     const uploader = new Uploader(Uploader.reqToOptions(req, size));
-    logger.debug("Starting download stream.", null, req.id);
-    const stream = await download();
     (async () => {
       // wait till the client has connected to the socket, before starting
       // the download, so that the client can receive all download/upload progress.
diff --git a/packages/@uppy/companion/lib/server/helpers/utils.js b/packages/@uppy/companion/lib/server/helpers/utils.js
index b998ab3..5985bbb 100644
--- a/packages/@uppy/companion/lib/server/helpers/utils.js
+++ b/packages/@uppy/companion/lib/server/helpers/utils.js
@@ -142,11 +142,14 @@ module.exports.StreamHttpJsonError = StreamHttpJsonError;
 module.exports.prepareStream = async (stream) =>
   new Promise((resolve, reject) => {
     stream
-      .on("response", () => {
+      .on("response", (response) => {
+        const contentLengthStr = response.headers["content-length"];
+        const contentLength = parseInt(contentLengthStr, 10);
+        const size = !Number.isNaN(contentLength) && contentLength >= 0 ? contentLength : undefined;
         // Don't allow any more data to flow yet.
         // https://github.com/request/request/issues/1990#issuecomment-184712275
         stream.pause();
-        resolve();
+        resolve({ size });
       })
       .on("error", (err) => {
         // In this case the error object is not a normal GOT HTTPError where json is already parsed,
diff --git a/packages/@uppy/companion/lib/server/provider/google/drive/index.d.ts b/packages/@uppy/companion/lib/server/provider/google/drive/index.d.ts
index 753b95d..a13c57b 100644
--- a/packages/@uppy/companion/lib/server/provider/google/drive/index.d.ts
+++ b/packages/@uppy/companion/lib/server/provider/google/drive/index.d.ts
@@ -3,7 +3,6 @@ export = Drive;
  * Adapter for API https://developers.google.com/drive/api/v3/
  */
 declare class Drive extends Provider {
-    static get authProvider(): string;
     list(options: any): Promise<any>;
     download({ id: idIn, token }: {
         id: any;
@@ -13,8 +12,9 @@ declare class Drive extends Provider {
         id: any;
         token: any;
     }): Promise<any>;
-    logout: (...args: any[]) => Promise<any>;
+    logout: typeof logout;
     refreshToken: typeof refreshToken;
 }
 import Provider = require("../../Provider");
+import { logout } from "../index";
 import { refreshToken } from "../index";
diff --git a/packages/@uppy/companion/lib/server/provider/google/drive/index.js b/packages/@uppy/companion/lib/server/provider/google/drive/index.js
index 242631d..e5bcbf2 100644
--- a/packages/@uppy/companion/lib/server/provider/google/drive/index.js
+++ b/packages/@uppy/companion/lib/server/provider/google/drive/index.js
@@ -47,7 +47,7 @@ async function getStats({ id, token }) {
  * Adapter for API https://developers.google.com/drive/api/v3/
  */
 class Drive extends Provider {
-  static get authProvider() {
+  static get oauthProvider() {
     return "googledrive";
   }
   static get authStateExpiry() {
@@ -55,7 +55,7 @@ class Drive extends Provider {
   }
   // eslint-disable-next-line class-methods-use-this
   async list(options) {
-    return withGoogleErrorHandling(Drive.authProvider, "provider.drive.list.error", async () => {
+    return withGoogleErrorHandling(Drive.oauthProvider, "provider.drive.list.error", async () => {
       const directory = options.directory || "root";
       const query = options.query || {};
       const { token } = options;
@@ -123,7 +123,7 @@ class Drive extends Provider {
         throw new ProviderAuthError();
       }
     }
-    return withGoogleErrorHandling(Drive.authProvider, "provider.drive.download.error", async () => {
+    return withGoogleErrorHandling(Drive.oauthProvider, "provider.drive.download.error", async () => {
       const client = await getClient({ token });
       const { mimeType, id, exportLinks } = await getStats({ id: idIn, token });
       let stream;
@@ -160,7 +160,7 @@ class Drive extends Provider {
   }
   // eslint-disable-next-line class-methods-use-this
   async size({ id, token }) {
-    return withGoogleErrorHandling(Drive.authProvider, "provider.drive.size.error", async () => {
+    return withGoogleErrorHandling(Drive.oauthProvider, "provider.drive.size.error", async () => {
       const { mimeType, size } = await getStats({ id, token });
       if (isGsuiteFile(mimeType)) {
         // GSuite file sizes cannot be predetermined (but are max 10MB)
@@ -170,10 +170,6 @@ class Drive extends Provider {
       return parseInt(size, 10);
     });
   }
-  // eslint-disable-next-line class-methods-use-this
-  async logout(...args) {
-    return logout(...args);
-  }
 }
 Drive.prototype.logout = logout;
 Drive.prototype.refreshToken = refreshToken;
diff --git a/packages/@uppy/companion/lib/server/provider/index.js b/packages/@uppy/companion/lib/server/provider/index.js
index 3be93fc..0c8e112 100644
--- a/packages/@uppy/companion/lib/server/provider/index.js
+++ b/packages/@uppy/companion/lib/server/provider/index.js
@@ -5,7 +5,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
  */
 const dropbox = require("./dropbox");
 const box = require("./box");
-const drive = require("./drive");
+const drive = require("./google/drive");
+const googlephotos = require("./google/googlephotos");
 const instagram = require("./instagram/graph");
 const facebook = require("./facebook");
 const onedrive = require("./onedrive");
@@ -63,7 +64,7 @@ module.exports.getProviderMiddleware = (providers, grantConfig) => {
  * @returns {Record<string, typeof Provider>}
  */
 module.exports.getDefaultProviders = () => {
-  const providers = { dropbox, box, drive, facebook, onedrive, zoom, instagram, unsplash };
+  const providers = { dropbox, box, drive, googlephotos, facebook, onedrive, zoom, instagram, unsplash };
   return providers;
 };
 /**
diff --git a/packages/@uppy/companion/lib/standalone/helper.js b/packages/@uppy/companion/lib/standalone/helper.js
index 0da1ebf..9bce6ea 100644
--- a/packages/@uppy/companion/lib/standalone/helper.js
+++ b/packages/@uppy/companion/lib/standalone/helper.js
@@ -72,6 +72,11 @@ const getConfigFromEnv = () => {
         secret: getSecret("COMPANION_GOOGLE_SECRET"),
         credentialsURL: process.env.COMPANION_GOOGLE_KEYS_ENDPOINT,
       },
+      googlephotos: {
+        key: process.env.COMPANION_GOOGLE_KEY,
+        secret: getSecret("COMPANION_GOOGLE_SECRET"),
+        credentialsURL: process.env.COMPANION_GOOGLE_KEYS_ENDPOINT,
+      },
       dropbox: {
         key: process.env.COMPANION_DROPBOX_KEY,
         secret: getSecret("COMPANION_DROPBOX_SECRET"),
diff --git a/packages/@uppy/google-photos/lib/GooglePhotos.js b/packages/@uppy/google-photos/lib/GooglePhotos.js
index 2292e1a..51bc8c5 100644
--- a/packages/@uppy/google-photos/lib/GooglePhotos.js
+++ b/packages/@uppy/google-photos/lib/GooglePhotos.js
@@ -9,6 +9,7 @@ import locale from "./locale.js";
 export default class GooglePhotos extends UIPlugin {
   constructor(uppy, opts) {
     super(uppy, opts);
+    this.rootFolderId = null;
     this.type = "acquirer";
     this.storage = this.opts.storage || tokenStorage;
     this.files = [];
@@ -69,7 +70,6 @@ export default class GooglePhotos extends UIPlugin {
     this.defaultLocale = locale;
     this.i18nInit();
     this.title = this.i18n("pluginNameGooglePhotos");
-    this.onFirstRender = this.onFirstRender.bind(this);
     this.render = this.render.bind(this);
   }
   install() {
@@ -88,9 +88,6 @@ export default class GooglePhotos extends UIPlugin {
     this.view.tearDown();
     this.unmount();
   }
-  async onFirstRender() {
-    await Promise.all([this.provider.fetchPreAuthToken(), this.view.getFolder()]);
-  }
   render(state) {
     if (this.getPluginState().files.length && !this.getPluginState().folders.length) {
       return this.view.render(state, {

@aduh95 aduh95 merged commit 12cf278 into 4.x Jun 19, 2024
19 of 20 checks passed
@aduh95 aduh95 deleted the google-photos-backport branch June 19, 2024 12:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant