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

Extend dropbox provider to support teams folders #5638

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

vikas-rajvanshy
Copy link

In cases where the logged in user is part of a team, the current implementation does not show the teams folders. This is because the default home directory is often a child of the team folders. This change sets the active root to be root_namespace_id which will enable the provider to list all folders.

See: https://developers.dropbox.com/dbx-team-files-guide

Copy link
Contributor

github-actions bot commented Feb 5, 2025

Diff output files
diff --git a/packages/@uppy/companion/lib/server/provider/dropbox/index.d.ts b/packages/@uppy/companion/lib/server/provider/dropbox/index.d.ts
index d4d625f..8297b7a 100644
--- a/packages/@uppy/companion/lib/server/provider/dropbox/index.d.ts
+++ b/packages/@uppy/companion/lib/server/provider/dropbox/index.d.ts
@@ -4,6 +4,7 @@ export = DropBox;
  */
 declare class DropBox extends Provider {
     constructor(options: any);
+    rootNamespaceId: any;
     download({ id, token }: {
         id: any;
         token: any;
diff --git a/packages/@uppy/companion/lib/server/provider/dropbox/index.js b/packages/@uppy/companion/lib/server/provider/dropbox/index.js
index 8c7eb6a..7cf757d 100644
--- a/packages/@uppy/companion/lib/server/provider/dropbox/index.js
+++ b/packages/@uppy/companion/lib/server/provider/dropbox/index.js
@@ -17,19 +17,22 @@ function httpHeaderSafeJson(v) {
     return `\\u${(`000${c.charCodeAt(0).toString(16)}`).slice(-4)}`;
   });
 }
-const getClient = async ({ token }) =>
+const getClient = async ({ token, rootNamespaceId }) =>
   (await got).extend({
     prefixUrl: "https://api.dropboxapi.com/2",
     headers: {
       authorization: `Bearer ${token}`,
+      ...(rootNamespaceId && {
+        "Dropbox-API-Path-Root": httpHeaderSafeJson({ ".tag": "root", "root": rootNamespaceId }),
+      }),
     },
   });
 const getOauthClient = async () =>
   (await got).extend({
     prefixUrl: "https://api.dropboxapi.com/oauth2",
   });
-async function list({ directory, query, token }) {
-  const client = await getClient({ token });
+async function list({ directory, query, token, rootNamespaceId }) {
+  const client = await getClient({ token, rootNamespaceId });
   if (query.cursor) {
     return client.post("files/list_folder/continue", { json: { cursor: query.cursor }, responseType: "json" }).json();
   }
@@ -45,7 +48,8 @@ async function list({ directory, query, token }) {
   }).json();
 }
 async function userInfo({ token }) {
-  return (await getClient({ token })).post("users/get_current_account", { responseType: "json" }).json();
+  return (await getClient({ token, rootNamespaceId: null })).post("users/get_current_account", { responseType: "json" })
+    .json();
 }
 /**
  * Adapter for API https://www.dropbox.com/developers/documentation/http/documentation
@@ -54,6 +58,7 @@ class DropBox extends Provider {
   constructor(options) {
     super(options);
     this.needsCookieAuth = true;
+    this.rootNamespaceId = null;
   }
   static get oauthProvider() {
     return "dropbox";
@@ -66,18 +71,26 @@ class DropBox extends Provider {
    */
   async list(options) {
     return this.#withErrorHandling("provider.dropbox.list.error", async () => {
-      const responses = await Promise.all([
-        list(options),
-        userInfo(options),
-      ]);
-      // @ts-ignore
-      const [stats, { email }] = responses;
+      const userInfoResponse = await userInfo(options);
+      const { email, root_info } = userInfoResponse;
+      // Store rootNamespaceId as class member
+      this.rootNamespaceId = root_info?.root_namespace_id;
+      // Then call list with the directory path and root namespace
+      const stats = await list({
+        ...options,
+        rootNamespaceId: this.rootNamespaceId,
+      });
       return adaptData(stats, email, options.companion.buildURL);
     });
   }
   async download({ id, token }) {
     return this.#withErrorHandling("provider.dropbox.download.error", async () => {
-      const stream = (await getClient({ token })).stream.post("files/download", {
+      // Fetch rootNamespaceId if not already set
+      if (!this.rootNamespaceId) {
+        const userInfoResponse = await userInfo({ token });
+        this.rootNamespaceId = userInfoResponse.root_info?.root_namespace_id;
+      }
+      const stream = (await getClient({ token, rootNamespaceId: this.rootNamespaceId })).stream.post("files/download", {
         prefixUrl: "https://content.dropboxapi.com/2",
         headers: {
           "Dropbox-API-Arg": httpHeaderSafeJson({ path: String(id) }),
@@ -92,34 +105,54 @@ class DropBox extends Provider {
   }
   async thumbnail({ id, token }) {
     return this.#withErrorHandling("provider.dropbox.thumbnail.error", async () => {
-      const stream = (await getClient({ token })).stream.post("files/get_thumbnail_v2", {
-        prefixUrl: "https://content.dropboxapi.com/2",
-        headers: {
-          "Dropbox-API-Arg": httpHeaderSafeJson({
-            resource: { ".tag": "path", path: `${id}` },
-            size: "w256h256",
-            format: "jpeg",
-          }),
+      // Fetch rootNamespaceId if not already set
+      if (!this.rootNamespaceId) {
+        const userInfoResponse = await userInfo({ token });
+        this.rootNamespaceId = userInfoResponse.root_info?.root_namespace_id;
+      }
+      const stream = (await getClient({ token, rootNamespaceId: this.rootNamespaceId })).stream.post(
+        "files/get_thumbnail_v2",
+        {
+          prefixUrl: "https://content.dropboxapi.com/2",
+          headers: {
+            "Dropbox-API-Arg": httpHeaderSafeJson({
+              resource: { ".tag": "path", path: `${id}` },
+              size: "w256h256",
+              format: "jpeg",
+            }),
+          },
+          body: Buffer.alloc(0),
+          responseType: "json",
         },
-        body: Buffer.alloc(0),
-        responseType: "json",
-      });
+      );
       await prepareStream(stream);
       return { stream, contentType: "image/jpeg" };
     });
   }
   async size({ id, token }) {
     return this.#withErrorHandling("provider.dropbox.size.error", async () => {
-      const { size } = await (await getClient({ token })).post("files/get_metadata", {
-        json: { path: id },
-        responseType: "json",
-      }).json();
+      // Fetch rootNamespaceId if not already set
+      if (!this.rootNamespaceId) {
+        const userInfoResponse = await userInfo({ token });
+        this.rootNamespaceId = userInfoResponse.root_info?.root_namespace_id;
+      }
+      const { size } = await (await getClient({ token, rootNamespaceId: this.rootNamespaceId })).post(
+        "files/get_metadata",
+        { json: { path: id }, responseType: "json" },
+      ).json();
       return parseInt(size, 10);
     });
   }
   async logout({ token }) {
     return this.#withErrorHandling("provider.dropbox.logout.error", async () => {
-      await (await getClient({ token })).post("auth/token/revoke", { responseType: "json" });
+      // Fetch rootNamespaceId if not already set
+      if (!this.rootNamespaceId) {
+        const userInfoResponse = await userInfo({ token });
+        this.rootNamespaceId = userInfoResponse.root_info?.root_namespace_id;
+      }
+      await (await getClient({ token, rootNamespaceId: this.rootNamespaceId })).post("auth/token/revoke", {
+        responseType: "json",
+      });
       return { revoked: true };
     });
   }

@Murderlon Murderlon requested a review from mifi February 10, 2025 11:35
Copy link
Member

@Murderlon Murderlon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this 👍

const { email, root_info } = userInfoResponse

// Store rootNamespaceId as class member
this.rootNamespaceId = root_info?.root_namespace_id
Copy link
Member

@Murderlon Murderlon Feb 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to double check, do we initialize this class once or per user? Because setting this will only work for the latter.

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.

2 participants