Bläddra i källkod

@uppy/provider-views: add support for remote file paths (#4537)

Fixes: https://github.com/transloadit/uppy/issues/4034
Co-authored-by: Merlijn Vos <merlijn@soverin.net>
Mikael Finstad 1 år sedan
förälder
incheckning
e054a25ab2

+ 1 - 1
packages/@uppy/google-drive/src/GoogleDrive.jsx

@@ -72,7 +72,7 @@ export default class GoogleDrive extends UIPlugin {
   onFirstRender () {
     return Promise.all([
       this.provider.fetchPreAuthToken(),
-      this.view.getFolder('root', '/'),
+      this.view.getFolder('root'),
     ])
   }
 

+ 1 - 1
packages/@uppy/provider-views/README.md

@@ -26,7 +26,7 @@ class GoogleDrive extends UIPlugin {
   onFirstRender () {
     return Promise.all([
       this.provider.fetchPreAuthToken(),
-      this.view.getFolder('root', '/'),
+      this.view.getFolder('root'),
     ])
   }
 

+ 5 - 5
packages/@uppy/provider-views/src/Breadcrumbs.jsx

@@ -18,18 +18,18 @@ const Breadcrumb = (props) => {
 }
 
 export default (props) => {
-  const { getFolder, title, breadcrumbsIcon, directories } = props
+  const { getFolder, title, breadcrumbsIcon, breadcrumbs } = props
 
   return (
     <div className="uppy-Provider-breadcrumbs">
       <div className="uppy-Provider-breadcrumbsIcon">{breadcrumbsIcon}</div>
       {
-        directories.map((directory, i) => (
+        breadcrumbs.map((directory, i) => (
           <Breadcrumb
             key={directory.id}
-            getFolder={() => getFolder(directory.id)}
-            title={i === 0 ? title : directory.title}
-            isLast={i + 1 === directories.length}
+            getFolder={() => getFolder(directory.requestPath)}
+            title={i === 0 ? title : directory.name}
+            isLast={i + 1 === breadcrumbs.length}
           />
         ))
       }

+ 1 - 1
packages/@uppy/provider-views/src/ProviderView/Header.jsx

@@ -6,7 +6,7 @@ export default (props) => {
   if (props.showBreadcrumbs) {
     components.push(Breadcrumbs({
       getFolder: props.getFolder,
-      directories: props.directories,
+      breadcrumbs: props.breadcrumbs,
       breadcrumbsIcon: props.pluginIcon && props.pluginIcon(),
       title: props.title,
     }))

+ 109 - 52
packages/@uppy/provider-views/src/ProviderView/ProviderView.jsx

@@ -32,6 +32,15 @@ function isOriginAllowed (origin, allowedOrigin) {
     .some((pattern) => pattern?.test(origin) || pattern?.test(`${origin}/`)) // allowing for trailing '/'
 }
 
+function formatBreadcrumbs (breadcrumbs) {
+  return breadcrumbs.slice(1).map((directory) => directory.name).join('/')
+}
+
+function prependPath (path, component) {
+  if (!path) return component
+  return `${path}/${component}`
+}
+
 /**
  * Class to easily generate generic views for Provider plugins
  */
@@ -74,7 +83,7 @@ export default class ProviderView extends View {
       authenticated: false,
       files: [],
       folders: [],
-      directories: [],
+      breadcrumbs: [],
       filterInput: '',
       isSearchVisible: false,
       currentSelection: [],
@@ -86,9 +95,30 @@ export default class ProviderView extends View {
     // Nothing.
   }
 
-  #updateFilesAndFolders (res, files, folders) {
-    this.nextPagePath = res.nextPagePath
-    res.items.forEach((item) => {
+  async #list ({ requestPath, absDirPath, signal }) {
+    const { username, nextPagePath, items } = await this.provider.list(requestPath, { signal })
+    this.username = username || this.username
+
+    return {
+      items: items.map((item) => ({
+        ...item,
+        absDirPath,
+      })),
+      nextPagePath,
+    }
+  }
+
+  async #listFilesAndFolders ({ requestPath, breadcrumbs, signal }) {
+    const absDirPath = formatBreadcrumbs(breadcrumbs)
+
+    const { items, nextPagePath } = await this.#list({ requestPath, absDirPath, signal })
+
+    this.nextPagePath = nextPagePath
+
+    const files = []
+    const folders = []
+
+    items.forEach((item) => {
       if (item.isFolder) {
         folders.push(item)
       } else {
@@ -96,59 +126,60 @@ export default class ProviderView extends View {
       }
     })
 
-    this.plugin.setPluginState({ folders, files })
+    return { files, folders }
   }
 
   /**
-   * Based on folder ID, fetch a new folder and update it to state
+   * Select a folder based on its id: fetches the folder and then updates state with its contents
+   * TODO rename to something better like selectFolder or navigateToFolder (breaking change?)
    *
-   * @param  {string} id Folder id
+   * @param  {string} requestPath
+   * the path we need to use when sending list request to companion (for some providers it's different from ID)
+   * @param  {string} name used in the UI and to build the absDirPath
    * @returns {Promise}   Folders/files in folder
    */
-  async getFolder (id, name) {
+  async getFolder (requestPath, name) {
     const controller = new AbortController()
     const cancelRequest = () => {
       controller.abort()
       this.clearSelection()
     }
-    const getNewBreadcrumbsDirectories = () => {
-      const state = this.plugin.getPluginState()
-      const index = state.directories.findIndex((dir) => id === dir.id)
-
-      if (index !== -1) {
-        return state.directories.slice(0, index + 1)
-      }
-      return state.directories.concat([{ id, title: name }])
-    }
 
     this.plugin.uppy.on('dashboard:close-panel', cancelRequest)
     this.plugin.uppy.on('cancel-all', cancelRequest)
-    this.setLoading(true)
 
+    this.setLoading(true)
     try {
-      const folders = []
-      const files = []
-      this.nextPagePath = id
+      this.lastCheckbox = undefined
+
+      let { breadcrumbs } = this.plugin.getPluginState()
+
+      const index = breadcrumbs.findIndex((dir) => requestPath === dir.requestPath)
+
+      if (index !== -1) {
+        // means we navigated back to a known directory (already in the stack), so cut the stack off there
+        breadcrumbs = breadcrumbs.slice(0, index + 1)
+      } else {
+        // we have navigated into a new (unknown) folder, add it to the stack
+        breadcrumbs = [...breadcrumbs, { requestPath, name }]
+      }
 
+      let files = []
+      let folders = []
       do {
-        const res = await this.provider.list(this.nextPagePath, { signal: controller.signal })
+        const { files: newFiles, folders: newFolders } = await this.#listFilesAndFolders({
+          requestPath, breadcrumbs, signal: controller.signal,
+        })
 
-        for (const f of res.items) {
-          if (f.isFolder) folders.push(f)
-          else files.push(f)
-        }
+        files = files.concat(newFiles)
+        folders = folders.concat(newFolders)
 
-        this.nextPagePath = res.nextPagePath
-        if (res.username) this.username = res.username
         this.setLoading(this.plugin.uppy.i18n('loadedXFiles', { numFiles: files.length + folders.length }))
       } while (
-        this.nextPagePath && this.opts.loadAllFiles
+        this.opts.loadAllFiles && this.nextPagePath
       )
 
-      const directories = getNewBreadcrumbsDirectories(this.nextPagePath)
-
-      this.plugin.setPluginState({ files, folders, directories, filterInput: '' })
-      this.lastCheckbox = undefined
+      this.plugin.setPluginState({ folders, files, breadcrumbs, filterInput: '' })
     } catch (err) {
       if (err.cause?.name === 'AbortError') {
         // Expected, user clicked “cancel”
@@ -191,7 +222,7 @@ export default class ProviderView extends View {
             authenticated: false,
             files: [],
             folders: [],
-            directories: [],
+            breadcrumbs: [],
             filterInput: '',
           }
           this.plugin.setPluginState(newState)
@@ -250,16 +281,22 @@ export default class ProviderView extends View {
   }
 
   async handleScroll (event) {
-    const path = this.nextPagePath || null
+    const requestPath = this.nextPagePath || null
 
-    if (this.shouldHandleScroll(event) && path) {
+    if (this.shouldHandleScroll(event) && requestPath) {
       this.isHandlingScroll = true
 
       try {
-        const response = await this.provider.list(path)
-        const { files, folders } = this.plugin.getPluginState()
+        const { files, folders, breadcrumbs } = this.plugin.getPluginState()
 
-        this.#updateFilesAndFolders(response, files, folders)
+        const { files: newFiles, folders: newFolders } = await this.#listFilesAndFolders({
+          requestPath, breadcrumbs,
+        })
+
+        const combinedFiles = files.concat(newFiles)
+        const combinedFolders = folders.concat(newFolders)
+
+        this.plugin.setPluginState({ folders: combinedFolders, files: combinedFiles })
       } catch (error) {
         this.handleError(error)
       } finally {
@@ -268,11 +305,11 @@ export default class ProviderView extends View {
     }
   }
 
-  async recursivelyListAllFiles (path, queue, onFiles) {
-    let curPath = path
+  async #recursivelyListAllFiles ({ requestPath, absDirPath, relDirPath, queue, onFiles }) {
+    let curPath = requestPath
 
     while (curPath) {
-      const res = await this.provider.list(curPath)
+      const res = await this.#list({ requestPath: curPath, absDirPath })
       curPath = res.nextPagePath
 
       const files = res.items.filter((item) => !item.isFolder)
@@ -282,7 +319,13 @@ export default class ProviderView extends View {
 
       // recursively queue call to self for each folder
       const promises = folders.map(async (folder) => queue.add(async () => (
-        this.recursivelyListAllFiles(folder.requestPath, queue, onFiles)
+        this.#recursivelyListAllFiles({
+          requestPath: folder.requestPath,
+          absDirPath: prependPath(absDirPath, folder.name),
+          relDirPath: prependPath(relDirPath, folder.name),
+          queue,
+          onFiles,
+        })
       )))
       await Promise.all(promises) // in case we get an error
     }
@@ -296,9 +339,17 @@ export default class ProviderView extends View {
       const messages = []
       const newFiles = []
 
-      for (const file of currentSelection) {
-        if (file.isFolder) {
-          const { requestPath, name } = file
+      for (const selectedItem of currentSelection) {
+        const { requestPath } = selectedItem
+
+        const withRelDirPath = (newItem) => ({
+          ...newItem,
+          // calculate the file's path relative to the user's selected item's path
+          // see https://github.com/transloadit/uppy/pull/4537#issuecomment-1614236655
+          relDirPath: newItem.absDirPath.replace(selectedItem.absDirPath, '').replace(/^\//, ''),
+        })
+
+        if (selectedItem.isFolder) {
           let isEmpty = true
           let numNewFiles = 0
 
@@ -313,7 +364,7 @@ export default class ProviderView extends View {
               // the folder was already added. This checks if all files are duplicate,
               // if that's the case, we don't add the files.
               if (!this.plugin.uppy.checkIfFileAlreadyExists(id)) {
-                newFiles.push(newFile)
+                newFiles.push(withRelDirPath(newFile))
                 numNewFiles++
                 this.setLoading(this.plugin.uppy.i18n('addedNumFiles', { numFiles: numNewFiles }))
               }
@@ -321,7 +372,13 @@ export default class ProviderView extends View {
             }
           }
 
-          await this.recursivelyListAllFiles(requestPath, queue, onFiles)
+          await this.#recursivelyListAllFiles({
+            requestPath,
+            absDirPath: prependPath(selectedItem.absDirPath, selectedItem.name),
+            relDirPath: selectedItem.name,
+            queue,
+            onFiles,
+          })
           await queue.onIdle()
 
           let message
@@ -329,20 +386,20 @@ export default class ProviderView extends View {
             message = this.plugin.uppy.i18n('emptyFolderAdded')
           } else if (numNewFiles === 0) {
             message = this.plugin.uppy.i18n('folderAlreadyAdded', {
-              folder: name,
+              folder: selectedItem.name,
             })
           } else {
             // TODO we don't really know at this point whether any files were actually added
             // (only later after addFiles has been called) so we should probably rewrite this.
             // Example: If all files fail to add due to restriction error, it will still say "Added 100 files from folder"
             message = this.plugin.uppy.i18n('folderAdded', {
-              smart_count: numNewFiles, folder: name,
+              smart_count: numNewFiles, folder: selectedItem.name,
             })
           }
 
           messages.push(message)
         } else {
-          newFiles.push(file)
+          newFiles.push(withRelDirPath(selectedItem))
         }
       }
 
@@ -380,7 +437,7 @@ export default class ProviderView extends View {
     const headerProps = {
       showBreadcrumbs: targetViewOptions.showBreadcrumbs,
       getFolder: this.getFolder,
-      directories: this.plugin.getPluginState().directories,
+      breadcrumbs: this.plugin.getPluginState().breadcrumbs,
       pluginIcon: this.plugin.icon,
       title: this.plugin.title,
       logout: this.logout,

+ 1 - 1
packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.jsx

@@ -46,7 +46,7 @@ export default class SearchProviderView extends View {
       isInputMode: true,
       files: [],
       folders: [],
-      directories: [],
+      breadcrumbs: [],
       filterInput: '',
       currentSelection: [],
       searchTerm: null,

+ 5 - 0
packages/@uppy/provider-views/src/View.js

@@ -92,6 +92,11 @@ export default class View {
       if (file.author.url) tagFile.meta.authorUrl = file.author.url
     }
 
+    // add relativePath similar to non-remote files: https://github.com/transloadit/uppy/pull/4486#issuecomment-1579203717
+    if (file.relDirPath != null) tagFile.meta.relativePath = file.relDirPath ? `${file.relDirPath}/${tagFile.name}` : null
+    // and absolutePath (with leading slash) https://github.com/transloadit/uppy/pull/4537#issuecomment-1614236655
+    if (file.absDirPath != null) tagFile.meta.absolutePath = file.absDirPath ? `/${file.absDirPath}/${tagFile.name}` : `/${tagFile.name}`
+
     return tagFile
   }
 

+ 4 - 2
packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/index.js

@@ -27,17 +27,19 @@ function getAsFileSystemHandleFromEntry (entry, logDropError) {
 }
 
 async function* createPromiseToAddFileOrParseDirectory (entry, relativePath, lastResortFile = undefined) {
+  const getNextRelativePath = () => `${relativePath}/${entry.name}`
+
   // For each dropped item, - make sure it's a file/directory, and start deepening in!
   if (entry.kind === 'file') {
     const file = await entry.getFile()
     if (file != null) {
-      file.relativePath = relativePath ? `${relativePath}/${entry.name}` : null
+      file.relativePath = relativePath ? getNextRelativePath() : null
       yield file
     } else if (lastResortFile != null) yield lastResortFile
   } else if (entry.kind === 'directory') {
     for await (const handle of entry.values()) {
       // Recurse on the directory, appending the dir name to the relative path
-      yield* createPromiseToAddFileOrParseDirectory(handle, `${relativePath}/${entry.name}`)
+      yield* createPromiseToAddFileOrParseDirectory(handle, relativePath ? getNextRelativePath() : entry.name)
     }
   } else if (lastResortFile != null) yield lastResortFile
 }