Explorar o código

companion,box: Box provider implementation (#2549)

* boilerplate for Box provider

TODO:
  - adapter
  - index
  - website/src/docs
  - tests

* Box provider implementation

- adapter
- index
- npm bufoonery
- lil' fixes

and MORE!

TODO:
- thumbnails 🤬
- tests
- docs

* Box provider: add brand icon, fix file icons

- token issues with thumbnail/logout

* Box provider: fix thumbnails, upload

- fix tokens for box
- implement `size()`
- make debug logging easier to find (bold blue)

learned:
- box needs cookie auth
- thumbnail requests come back as 202s

TODO:
- fix logout with form POST (zoom example)

* Box provider: fix logout

- add client info as formData to revoke post

TODO:
- tests cleanup
- package-lock fix
- docs

* Box provider: docs

TODO:
- tests

* Box provider: cleanup

TODO:
- tests

* fix error msg path for Box

- remove comment

* keeping up

- update ProviderViews import
- fix version string on Box package.json

* companion,box: exclude box from global module/examples while in beta

Co-authored-by: Ifedapo .A. Olarewaju <ifedapoolarewaju@gmail.com>
Carter Konz %!s(int64=4) %!d(string=hai) anos
pai
achega
970ac8df5b
Modificáronse 40 ficheiros con 844 adicións e 12 borrados
  1. 1 1
      .github/CONTRIBUTING.md
  2. 4 3
      README.md
  3. 2 2
      bin/remove-accidental-git-tags.sh
  4. 4 0
      env.example.sh
  5. 2 0
      examples/dev/Dashboard.js
  6. 9 1
      examples/uppy-with-companion/server/index.js
  7. 11 0
      package-lock.json
  8. 1 0
      package.json
  9. 21 0
      packages/@uppy/box/LICENSE
  10. 42 0
      packages/@uppy/box/README.md
  11. 104 0
      packages/@uppy/box/package-lock.json
  12. 31 0
      packages/@uppy/box/package.json
  13. 60 0
      packages/@uppy/box/src/index.js
  14. 17 0
      packages/@uppy/box/types/index.d.ts
  15. 2 0
      packages/@uppy/box/types/index.test-d.ts
  16. 2 0
      packages/@uppy/companion/KUBERNETES.md
  17. 3 0
      packages/@uppy/companion/env.test.sh
  18. 4 0
      packages/@uppy/companion/env_example
  19. 1 0
      packages/@uppy/companion/package.json
  20. 6 0
      packages/@uppy/companion/src/config/grant.js
  21. 2 1
      packages/@uppy/companion/src/server/logger.js
  22. 54 0
      packages/@uppy/companion/src/server/provider/box/adapter.js
  23. 175 0
      packages/@uppy/companion/src/server/provider/box/index.js
  24. 1 1
      packages/@uppy/companion/src/server/provider/dropbox/index.js
  25. 2 1
      packages/@uppy/companion/src/server/provider/index.js
  26. 4 0
      packages/@uppy/companion/src/standalone/helper.js
  27. 1 0
      packages/@uppy/companion/test/__tests__/companion.js
  28. 20 0
      packages/@uppy/companion/test/__tests__/provider-manager.js
  29. 1 0
      packages/@uppy/transloadit/src/index.js
  30. 2 0
      packages/uppy/package.json
  31. 2 0
      packages/uppy/types/index.d.ts
  32. 2 0
      test/endtoend/providers/main.js
  33. 90 0
      test/endtoend/providers/provider.box.test.js
  34. 2 0
      test/endtoend/typescript/main.ts
  35. 2 0
      test/endtoend/utils.js
  36. 15 0
      test/resources/1020-percent-state.json
  37. 1 0
      website/inject.js
  38. 1 1
      website/src/_template/contributing.md
  39. 131 0
      website/src/docs/box.md
  40. 9 1
      website/src/docs/companion.md

+ 1 - 1
.github/CONTRIBUTING.md

@@ -103,7 +103,7 @@ Other things to keep in mind during release:
 After a release, the demos on transloadit.com should also be updated. After updating, check that some things work locally:
 
  - the demos in the demo section work (try one that uses an import robot, and one that you need to upload to)
- - the demos on the homepage work and can import from GDrive, Insta, Dropbox
+ - the demos on the homepage work and can import from Google Drive, Instagram, Dropbox, etc.
 
 If you don't have access to the transloadit.com source code ping @arturi or @goto-bus-stop and we'll pick it up. :sparkles:
 

+ 4 - 3
README.md

@@ -7,7 +7,7 @@
 
 Uppy is a sleek, modular JavaScript file uploader that integrates seamlessly with any application. It’s fast, easy to use and lets you worry about more important problems than building a file uploader.
 
-- **Fetch** files from local disk, remote URLs, Google Drive, Dropbox, Instagram or snap and record selfies with a camera
+- **Fetch** files from local disk, remote URLs, Google Drive, Dropbox, Box, Instagram or snap and record selfies with a camera
 - **Preview** and edit metadata with a nice interface
 - **Upload** to the final destination, optionally process/encode
 
@@ -48,7 +48,7 @@ const uppy = new Uppy({ autoProceed: false })
 
 - Lightweight, modular plugin-based architecture, easy on dependencies :zap:
 - Resumable file uploads via the open [tus](https://tus.io/) standard, so large uploads survive network hiccups
-- Supports picking files from: Webcam, Dropbox, Google Drive, Instagram, bypassing the user’s device where possible, syncing between servers directly via [@uppy/companion](https://uppy.io/docs/companion)
+- Supports picking files from: Webcam, Dropbox, Box, Google Drive, Instagram, bypassing the user’s device where possible, syncing between servers directly via [@uppy/companion](https://uppy.io/docs/companion)
 - Works great with file encoding and processing backends, such as [Transloadit](https://transloadit.com), works great without (just roll your own Apache/Nginx/Node/FFmpeg/etc backend)
 - Sleek user interface :sparkles:
 - Optional file recovery (after a browser crash) with [Golden Retriever](https://uppy.io/docs/golden-retriever/)
@@ -91,7 +91,7 @@ Alternatively, you can also use a pre-built bundle from Transloadit's CDN: Edgly
 
 - [Uppy](https://uppy.io/docs/uppy/) — full list of options, methods and events
 - [Plugins](https://uppy.io/docs/plugins/) — list of Uppy plugins and their options
-- [Companion](https://uppy.io/docs/companion/) — setting up and running a Companion instance, which adds support for Instagram, Dropbox, Google Drive and remote URLs
+- [Companion](https://uppy.io/docs/companion/) — setting up and running a Companion instance, which adds support for Instagram, Dropbox, Box, Google Drive and remote URLs
 - [React](https://uppy.io/docs/react/) — components to integrate Uppy UI plugins with React apps
 - [Architecture & Writing a Plugin](https://uppy.io/docs/writing-plugins/) — how to write a plugin for Uppy
 
@@ -113,6 +113,7 @@ Alternatively, you can also use a pre-built bundle from Transloadit's CDN: Edgly
 - [`Webcam`](https://uppy.io/docs/webcam/) — snap and record those selfies 📷
 - ⓒ [`Google Drive`](https://uppy.io/docs/google-drive/) — import files from Google Drive
 - ⓒ [`Dropbox`](https://uppy.io/docs/dropbox/) — import files from Dropbox
+- ⓒ [`Box`](https://uppy.io/docs/box/) — import files from Box
 - ⓒ [`Instagram`](https://uppy.io/docs/instagram/) — import images and videos from Instagram
 - ⓒ [`Facebook`](https://uppy.io/docs/facebook/) — import images and videos from Facebook
 - ⓒ [`OneDrive`](https://uppy.io/docs/onedrive/) — import files from Microsoft OneDrive

+ 2 - 2
bin/remove-accidental-git-tags.sh

@@ -1,11 +1,11 @@
 #!/bin/bash
 
-# removes tags that Lerna generated, but then failed to release, 
+# removes tags that Lerna generated, but then failed to release,
 # and is now unfortunately stuck
 # usage: ./remove-tags.sh VERSION_NUMBER
 # where VERSION_NUMBER is something like 0.30.0
 
-Packages=(aws-s3 file-input react transloadit aws-s3-multipart form redux-dev-tools tus companion golden-retriever robodog url companion-client google-drive server-utils utils core informer status-bar webcam dashboard instagram store-default xhr-upload drag-drop progress-bar store-redux dropbox provider-views thumbnail-generator)
+Packages=(aws-s3 file-input react transloadit aws-s3-multipart form redux-dev-tools tus companion golden-retriever robodog url companion-client google-drive server-utils utils core informer status-bar webcam dashboard instagram store-default xhr-upload drag-drop progress-bar store-redux dropbox box provider-views thumbnail-generator)
 Version = $*
 
 for i in "${Packages[@]}"

+ 4 - 0
env.example.sh

@@ -4,6 +4,8 @@
 export NODE_ENV="${NODE_ENV:-development}"
 export COMPANION_DROPBOX_KEY="***"
 export COMPANION_DROPBOX_SECRET="***"
+export COMPANION_BOX_KEY="***"
+export COMPANION_BOX_SECRET="***"
 export COMPANION_GOOGLE_KEY="***"
 export COMPANION_GOOGLE_SECRET="***"
 export COMPANION_INSTAGRAM_KEY="***"
@@ -23,6 +25,8 @@ export COMPANION_ZOOM_SECRET="***"
 # travis encrypt --add GHPAGES_URL=https://secret_access_token@github.com/transloadit/uppy.git
 # travis encrypt --add env.global "COMPANION_DROPBOX_KEY=${COMPANION_DROPBOX_KEY}"
 # travis encrypt --add env.global "COMPANION_DROPBOX_SECRET=${COMPANION_DROPBOX_SECRET}"
+# travis encrypt --add env.global "COMPANION_BOX_KEY=${COMPANION_BOX_KEY}"
+# travis encrypt --add env.global "COMPANION_BOX_SECRET=${COMPANION_BOX_SECRET}"
 # travis encrypt --add env.global "COMPANION_GOOGLE_KEY=${COMPANION_GOOGLE_KEY}"
 # travis encrypt --add env.global "COMPANION_GOOGLE_SECRET=${COMPANION_GOOGLE_SECRET}"
 # travis encrypt --add env.global "COMPANION_INSTAGRAM_KEY=${COMPANION_INSTAGRAM_KEY}"

+ 2 - 0
examples/dev/Dashboard.js

@@ -4,6 +4,7 @@ const Instagram = require('@uppy/instagram/src')
 const Facebook = require('@uppy/facebook/src')
 const OneDrive = require('@uppy/onedrive/src')
 const Dropbox = require('@uppy/dropbox/src')
+const Box = require('@uppy/box/src')
 const GoogleDrive = require('@uppy/google-drive/src')
 const Unsplash = require('@uppy/unsplash/src')
 const Zoom = require('@uppy/zoom/src')
@@ -67,6 +68,7 @@ module.exports = () => {
     .use(GoogleDrive, { target: Dashboard, companionUrl: COMPANION_URL })
     .use(Instagram, { target: Dashboard, companionUrl: COMPANION_URL })
     .use(Dropbox, { target: Dashboard, companionUrl: COMPANION_URL })
+    .use(Box, { target: Dashboard, companionUrl: COMPANION_URL })
     .use(Facebook, { target: Dashboard, companionUrl: COMPANION_URL })
     .use(OneDrive, { target: Dashboard, companionUrl: COMPANION_URL })
     .use(Zoom, { target: Dashboard, companionUrl: COMPANION_URL })

+ 9 - 1
examples/uppy-with-companion/server/index.js

@@ -41,8 +41,16 @@ const uppyOptions = {
     instagram: {
       key: 'your instagram key',
       secret: 'your instagram secret'
+    },
+    dropbox: {
+      key: 'your dropbox key',
+      secret: 'your dropbox secret'
+    },
+    box: {
+      key: 'your box key',
+      secret: 'your box secret'
     }
-    // you can also add options for dropbox here
+    // you can also add options for additional providers here
   },
   server: {
     host: 'localhost:3020',

+ 11 - 0
package-lock.json

@@ -8169,6 +8169,15 @@
         "@uppy/utils": "file:packages/@uppy/utils"
       }
     },
+    "@uppy/box": {
+      "version": "file:packages/@uppy/box",
+      "requires": {
+        "@uppy/companion-client": "file:packages/@uppy/companion-client",
+        "@uppy/provider-views": "file:packages/@uppy/provider-views",
+        "@uppy/utils": "file:packages/@uppy/utils",
+        "preact": "8.2.9"
+      }
+    },
     "@uppy/companion": {
       "version": "file:packages/@uppy/companion",
       "requires": {
@@ -8497,6 +8506,7 @@
     "@uppy/robodog": {
       "version": "file:packages/@uppy/robodog",
       "requires": {
+        "@uppy/box": "file:packages/@uppy/box",
         "@uppy/core": "file:packages/@uppy/core",
         "@uppy/dashboard": "file:packages/@uppy/dashboard",
         "@uppy/dropbox": "file:packages/@uppy/dropbox",
@@ -40564,6 +40574,7 @@
       "requires": {
         "@uppy/aws-s3": "file:packages/@uppy/aws-s3",
         "@uppy/aws-s3-multipart": "file:packages/@uppy/aws-s3-multipart",
+        "@uppy/box": "file:packages/@uppy/box",
         "@uppy/companion-client": "file:packages/@uppy/companion-client",
         "@uppy/core": "file:packages/@uppy/core",
         "@uppy/dashboard": "file:packages/@uppy/dashboard",

+ 1 - 0
package.json

@@ -40,6 +40,7 @@
     "@uppy-example/xhr-bundle": "file:examples/xhr-bundle",
     "@uppy/aws-s3": "file:packages/@uppy/aws-s3",
     "@uppy/aws-s3-multipart": "file:packages/@uppy/aws-s3-multipart",
+    "@uppy/box": "file:packages/@uppy/box",
     "@uppy/companion": "file:packages/@uppy/companion",
     "@uppy/companion-client": "file:packages/@uppy/companion-client",
     "@uppy/core": "file:packages/@uppy/core",

+ 21 - 0
packages/@uppy/box/LICENSE

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2018 Transloadit
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 42 - 0
packages/@uppy/box/README.md

@@ -0,0 +1,42 @@
+# @uppy/box
+
+<img src="https://uppy.io/images/logos/uppy-dog-head-arrow.svg" width="120" alt="Uppy logo: a superman puppy in a pink suit" align="right">
+
+<a href="https://www.npmjs.com/package/@uppy/box"><img src="https://img.shields.io/npm/v/@uppy/box.svg?style=flat-square"></a>
+<a href="https://travis-ci.org/transloadit/uppy"><img src="https://img.shields.io/travis/transloadit/uppy/master.svg?style=flat-square" alt="Build Status"></a>
+
+The Box plugin for Uppy lets users import files from their Box account.
+
+A Companion instance is required for the Box plugin to work. Companion handles authentication with Box, downloads files from Box and uploads them to the destination. This saves the user bandwidth, especially helpful if they are on a mobile connection.
+
+Uppy is being developed by the folks at [Transloadit](https://transloadit.com), a versatile file encoding service.
+
+## Example
+
+```js
+const Uppy = require('@uppy/core')
+const Box = require('@uppy/box')
+
+const uppy = new Uppy()
+uppy.use(Box, {
+  // Options
+})
+```
+
+## Installation
+
+```bash
+$ npm install @uppy/box
+```
+
+We recommend installing from npm and then using a module bundler such as [Webpack](https://webpack.js.org/), [Browserify](http://browserify.org/) or [Rollup.js](http://rollupjs.org/).
+
+Alternatively, you can also use this plugin in a pre-built bundle from Transloadit's CDN: Edgly. In that case `Uppy` will attach itself to the global `window.Uppy` object. See the [main Uppy documentation](https://uppy.io/docs/#Installation) for instructions.
+
+## Documentation
+
+Documentation for this plugin can be found on the [Uppy website](https://uppy.io/docs/box).
+
+## License
+
+[The MIT License](./LICENSE).

+ 104 - 0
packages/@uppy/box/package-lock.json

@@ -0,0 +1,104 @@
+{
+  "name": "@uppy/box",
+  "version": "1.4.15",
+  "lockfileVersion": 1,
+  "requires": true,
+  "dependencies": {
+    "@uppy/companion-client": {
+      "version": "file:../companion-client",
+      "requires": {
+        "@uppy/utils": "file:../utils",
+        "namespace-emitter": "^2.0.1"
+      },
+      "dependencies": {
+        "@uppy/utils": {
+          "version": "file:../utils",
+          "requires": {
+            "abortcontroller-polyfill": "^1.4.0",
+            "lodash.throttle": "^4.1.1"
+          },
+          "dependencies": {
+            "abortcontroller-polyfill": {
+              "version": "1.5.0",
+              "resolved": "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.5.0.tgz",
+              "integrity": "sha512-O6Xk757Jb4o0LMzMOMdWvxpHWrQzruYBaUruFaIOfAQRnWFxfdXYobw12jrVHGtoXk6WiiyYzc0QWN9aL62HQA=="
+            },
+            "lodash.throttle": {
+              "version": "4.1.1",
+              "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
+              "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ="
+            }
+          }
+        },
+        "namespace-emitter": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/namespace-emitter/-/namespace-emitter-2.0.1.tgz",
+          "integrity": "sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g=="
+        }
+      }
+    },
+    "@uppy/provider-views": {
+      "version": "file:../provider-views",
+      "requires": {
+        "@uppy/utils": "file:../utils",
+        "classnames": "^2.2.6",
+        "preact": "8.2.9"
+      },
+      "dependencies": {
+        "@uppy/utils": {
+          "version": "file:../utils",
+          "requires": {
+            "abortcontroller-polyfill": "^1.4.0",
+            "lodash.throttle": "^4.1.1"
+          },
+          "dependencies": {
+            "abortcontroller-polyfill": {
+              "version": "1.5.0",
+              "resolved": "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.5.0.tgz",
+              "integrity": "sha512-O6Xk757Jb4o0LMzMOMdWvxpHWrQzruYBaUruFaIOfAQRnWFxfdXYobw12jrVHGtoXk6WiiyYzc0QWN9aL62HQA=="
+            },
+            "lodash.throttle": {
+              "version": "4.1.1",
+              "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
+              "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ="
+            }
+          }
+        },
+        "classnames": {
+          "version": "2.2.6",
+          "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
+          "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q=="
+        },
+        "preact": {
+          "version": "8.2.9",
+          "resolved": "https://registry.npmjs.org/preact/-/preact-8.2.9.tgz",
+          "integrity": "sha512-ThuGXBmJS3VsT+jIP+eQufD3L8pRw/PY3FoCys6O9Pu6aF12Pn9zAJDX99TfwRAFOCEKm/P0lwiPTbqKMJp0fA=="
+        }
+      }
+    },
+    "@uppy/utils": {
+      "version": "file:../utils",
+      "requires": {
+        "abortcontroller-polyfill": "^1.4.0",
+        "lodash.throttle": "^4.1.1"
+      },
+      "dependencies": {
+        "abortcontroller-polyfill": {
+          "version": "1.5.0",
+          "resolved": "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.5.0.tgz",
+          "integrity": "sha512-O6Xk757Jb4o0LMzMOMdWvxpHWrQzruYBaUruFaIOfAQRnWFxfdXYobw12jrVHGtoXk6WiiyYzc0QWN9aL62HQA=="
+        },
+        "lodash.throttle": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
+          "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ="
+        }
+      }
+    },
+    "preact": {
+      "version": "8.2.9",
+      "resolved": "https://registry.npmjs.org/preact/-/preact-8.2.9.tgz",
+      "integrity": "sha512-ThuGXBmJS3VsT+jIP+eQufD3L8pRw/PY3FoCys6O9Pu6aF12Pn9zAJDX99TfwRAFOCEKm/P0lwiPTbqKMJp0fA=="
+    }
+  }
+}

+ 31 - 0
packages/@uppy/box/package.json

@@ -0,0 +1,31 @@
+{
+  "name": "@uppy/box",
+  "description": "Import files from Box, into Uppy.",
+  "version": "1.0.0",
+  "license": "MIT",
+  "main": "lib/index.js",
+  "types": "types/index.d.ts",
+  "keywords": [
+    "file uploader",
+    "uppy",
+    "uppy-plugin",
+    "box"
+  ],
+  "homepage": "https://uppy.io",
+  "bugs": {
+    "url": "https://github.com/transloadit/uppy/issues"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/transloadit/uppy.git"
+  },
+  "dependencies": {
+    "@uppy/companion-client": "file:../companion-client",
+    "@uppy/provider-views": "file:../provider-views",
+    "@uppy/utils": "file:../utils",
+    "preact": "8.2.9"
+  },
+  "peerDependencies": {
+    "@uppy/core": "^1.0.0"
+  }
+}

+ 60 - 0
packages/@uppy/box/src/index.js

@@ -0,0 +1,60 @@
+const { Plugin } = require('@uppy/core')
+const { Provider } = require('@uppy/companion-client')
+const { ProviderViews } = require('@uppy/provider-views')
+const { h } = require('preact')
+
+module.exports = class Box extends Plugin {
+  static VERSION = require('../package.json').version
+
+  constructor (uppy, opts) {
+    super(uppy, opts)
+    this.id = this.opts.id || 'Box'
+    Provider.initPlugin(this, opts)
+    this.title = this.opts.title || 'Box'
+    this.icon = () => (
+      <svg aria-hidden="true" focusable="false" width="32" height="32" viewBox="0 0 32 32">
+        <g fill="none" fill-rule="evenodd">
+          <rect fill="#0061D5" width="32" height="32" rx="16" />
+          <g fill="#fff" fill-rule="nonzero">
+            <path d="m16.4 13.5c-1.6 0-3 0.9-3.7 2.2-0.7-1.3-2.1-2.2-3.7-2.2-1 0-1.8 0.3-2.5 0.8v-3.6c-0.1-0.3-0.5-0.7-1-0.7s-0.8 0.4-0.8 0.8v7c0 2.3 1.9 4.2 4.2 4.2 1.6 0 3-0.9 3.7-2.2 0.7 1.3 2.1 2.2 3.7 2.2 2.3 0 4.2-1.9 4.2-4.2 0.1-2.4-1.8-4.3-4.1-4.3m-7.5 6.8c-1.4 0-2.5-1.1-2.5-2.5s1.1-2.5 2.5-2.5 2.5 1.1 2.5 2.5-1.1 2.5-2.5 2.5m7.5 0c-1.4 0-2.5-1.1-2.5-2.5s1.1-2.5 2.5-2.5 2.5 1.1 2.5 2.5-1.1 2.5-2.5 2.5" />
+            <path d="m27.2 20.6l-2.3-2.8 2.3-2.8c0.3-0.4 0.2-0.9-0.2-1.2s-1-0.2-1.3 0.2l-2 2.4-2-2.4c-0.3-0.4-0.9-0.4-1.3-0.2-0.4 0.3-0.5 0.8-0.2 1.2l2.3 2.8-2.3 2.8c-0.3 0.4-0.2 0.9 0.2 1.2s1 0.2 1.3-0.2l2-2.4 2 2.4c0.3 0.4 0.9 0.4 1.3 0.2 0.4-0.3 0.4-0.8 0.2-1.2" />
+          </g>
+        </g>
+      </svg>
+    )
+
+    this.provider = new Provider(uppy, {
+      companionUrl: this.opts.companionUrl,
+      companionHeaders: this.opts.companionHeaders || this.opts.serverHeaders,
+      provider: 'box',
+      pluginId: this.id
+    })
+
+    this.onFirstRender = this.onFirstRender.bind(this)
+    this.render = this.render.bind(this)
+  }
+
+  install () {
+    this.view = new ProviderViews(this, {
+      provider: this.provider
+    })
+
+    const target = this.opts.target
+    if (target) {
+      this.mount(target, this)
+    }
+  }
+
+  uninstall () {
+    this.view.tearDown()
+    this.unmount()
+  }
+
+  onFirstRender () {
+    return this.view.getFolder()
+  }
+
+  render (state) {
+    return this.view.render(state)
+  }
+}

+ 17 - 0
packages/@uppy/box/types/index.d.ts

@@ -0,0 +1,17 @@
+import Uppy = require('@uppy/core')
+import CompanionClient = require('@uppy/companion-client')
+
+declare module Box {
+  interface BoxOptions
+    extends Uppy.PluginOptions,
+      CompanionClient.PublicProviderOptions {
+    replaceTargetContent?: boolean
+    target?: Uppy.PluginTarget
+    title?: string
+    storage?: CompanionClient.TokenStorage
+  }
+}
+
+declare class Box extends Uppy.Plugin<Box.BoxOptions> {}
+
+export = Box

+ 2 - 0
packages/@uppy/box/types/index.test-d.ts

@@ -0,0 +1,2 @@
+import Box = require('../')
+// TODO implement

+ 2 - 0
packages/@uppy/companion/KUBERNETES.md

@@ -30,6 +30,8 @@ data:
   COMPANION_SECRET: "shh!Issa Secret!"
   COMPANION_DROPBOX_KEY: "YOUR DROPBOX KEY"
   COMPANION_DROPBOX_SECRET: "YOUR DROPBOX SECRET"
+  COMPANION_BOX_KEY: "YOUR BOX KEY"
+  COMPANION_BOX_SECRET: "YOUR BOX SECRET"
   COMPANION_GOOGLE_KEY: "YOUR GOOGLE KEY"
   COMPANION_GOOGLE_SECRET: "YOUR GOOGLE SECRET"
   COMPANION_INSTAGRAM_KEY: "YOUR INSTAGRAM KEY"

+ 3 - 0
packages/@uppy/companion/env.test.sh

@@ -12,6 +12,9 @@ export COMPANION_SECRET="secret"
 export COMPANION_DROPBOX_KEY="dropbox_key"
 export COMPANION_DROPBOX_SECRET="dropbox_secret"
 
+export COMPANION_BOX_KEY="box_key"
+export COMPANION_BOX_SECRET="box_secret"
+
 export COMPANION_GOOGLE_KEY="google_key"
 export COMPANION_GOOGLE_SECRET="google_secret"
 

+ 4 - 0
packages/@uppy/companion/env_example

@@ -14,6 +14,10 @@ COMPANION_DROPBOX_KEY="dropbox_key"
 COMPANION_DROPBOX_SECRET="dropbox_secret"
 COMPANION_DROPBOX_SECRET_FILE="path/to/dropbox/secret"
 
+COMPANION_BOX_KEY="box_key"
+COMPANION_BOX_SECRET="box_secret"
+COMPANION_BOX_SECRET_FILE="path/to/box/secret"
+
 COMPANION_GOOGLE_KEY=
 COMPANION_GOOGLE_SECRET=
 COMPANION_GOOGLE_SECRET_FILE=

+ 1 - 0
packages/@uppy/companion/package.json

@@ -20,6 +20,7 @@
     "s3",
     "google drive",
     "dropbox",
+    "box",
     "backend",
     "websocket",
     "express",

+ 6 - 0
packages/@uppy/companion/src/config/grant.js

@@ -15,6 +15,12 @@ module.exports = () => {
       access_url: 'https://api.dropbox.com/oauth2/token',
       callback: '/dropbox/callback'
     },
+    box: {
+      transport: 'session',
+      authorize_url: 'https://account.box.com/api/oauth2/authorize',
+      access_url: 'https://api.box.com/oauth2/token',
+      callback: '/box/callback'
+    },
     instagram: {
       transport: 'session',
       callback: '/instagram/callback'

+ 2 - 1
packages/@uppy/companion/src/server/logger.js

@@ -56,7 +56,8 @@ exports.error = (msg, tag, traceId, shouldLogStackTrace) => {
  */
 exports.debug = (msg, tag, traceId) => {
   if (process.env.NODE_ENV !== 'production') {
-    log(msg, tag, 'debug', traceId)
+    // @ts-ignore
+    log(msg, tag, 'debug', traceId, chalk.bold.blue)
   }
 }
 

+ 54 - 0
packages/@uppy/companion/src/server/provider/box/adapter.js

@@ -0,0 +1,54 @@
+const mime = require('mime-types')
+const querystring = require('querystring')
+
+exports.getUsername = (data) => {
+  return data.login
+}
+
+exports.isFolder = (item) => {
+  return item.type === 'folder'
+}
+
+exports.getItemSize = (item) => {
+  return item.size
+}
+
+exports.getItemIcon = (item) => {
+  return item.type
+}
+
+exports.getItemSubList = (item) => {
+  return item.entries
+}
+
+exports.getItemName = (item) => {
+  return item.name || ''
+}
+
+exports.getMimeType = (item) => {
+  return mime.lookup(exports.getItemName(item)) || null
+}
+
+exports.getItemId = (item) => {
+  return item.id
+}
+
+exports.getItemRequestPath = (item) => {
+  return item.id
+}
+
+exports.getItemModifiedDate = (item) => {
+  return item.modified_at
+}
+
+exports.getItemThumbnailUrl = (item) => {
+  return `/box/thumbnail/${exports.getItemRequestPath(item)}`
+}
+
+exports.getNextPagePath = (data) => {
+  if (data.total_count < data.limit || data.offset + data.limit > data.total_count) {
+    return null
+  }
+  const query = { offset: data.offset + data.limit }
+  return `?${querystring.stringify(query)}`
+}

+ 175 - 0
packages/@uppy/companion/src/server/provider/box/index.js

@@ -0,0 +1,175 @@
+const Provider = require('../Provider')
+
+const request = require('request')
+const purest = require('purest')({ request })
+const logger = require('../../logger')
+const adapter = require('./adapter')
+const { ProviderApiError, ProviderAuthError } = require('../error')
+
+const BOX_FILES_FIELDS = 'id,modified_at,name,permissions,size,type'
+const BOX_THUMBNAIL_SIZE = 256
+
+/**
+ * Adapter for API https://developer.box.com/reference/
+ */
+class Box extends Provider {
+  constructor (options) {
+    super(options)
+    this.authProvider = options.provider = Box.authProvider
+    this.client = purest(options)
+    // needed for the thumbnails fetched via companion
+    this.needsCookieAuth = true
+  }
+
+  static get authProvider () {
+    return 'box'
+  }
+
+  _userInfo ({ token }, done) {
+    this.client
+      .get('users/me')
+      .auth(token)
+      .request(done)
+  }
+
+  /**
+   * Lists files and folders from Box API
+   *
+   * @param {object} options
+   * @param {function} done
+   */
+  list ({ directory, token, companion }, done) {
+    const rootFolderID = '0'
+    const path = `folders/${directory || rootFolderID}/items`
+
+    this.client
+      .get(path)
+      .qs({ fields: BOX_FILES_FIELDS })
+      .auth(token)
+      .request((err, resp, body) => {
+        if (err || resp.statusCode !== 200) {
+          err = this._error(err, resp)
+          logger.error(err, 'provider.box.list.error')
+          return done(err)
+        } else {
+          this._userInfo({ token }, (err, infoResp) => {
+            if (err || infoResp.statusCode !== 200) {
+              err = this._error(err, infoResp)
+              logger.error(err, 'provider.token.user.error')
+              return done(err)
+            }
+            done(null, this.adaptData(body, companion))
+          })
+        }
+      })
+  }
+
+  download ({ id, token }, onData) {
+    return this.client
+      .get(`files/${id}/content`)
+      .auth(token)
+      .request()
+      .on('response', (resp) => {
+        if (resp.statusCode !== 200) {
+          onData(this._error(null, resp))
+        } else {
+          resp.on('data', (chunk) => onData(null, chunk))
+        }
+      })
+      .on('end', () => onData(null, null))
+      .on('error', (err) => {
+        logger.error(err, 'provider.box.download.error')
+        onData(err)
+      })
+  }
+
+  thumbnail ({ id, token }, done) {
+    return this.client
+      .get(`files/${id}/thumbnail.png`)
+      .qs({ max_height: BOX_THUMBNAIL_SIZE, max_width: BOX_THUMBNAIL_SIZE })
+      .auth(token)
+      .request()
+      .on('response', (resp) => {
+        if (![200, 202].includes(resp.statusCode)) {
+          const err = this._error(null, resp)
+          logger.error(err, 'provider.box.thumbnail.error')
+          return done(err)
+        }
+        done(null, resp)
+      })
+      .on('error', (err) => {
+        logger.error(err, 'provider.box.thumbnail.error')
+      })
+  }
+
+  size ({ id, token }, done) {
+    return this.client
+      .get(`files/${id}`)
+      .auth(token)
+      .request((err, resp, body) => {
+        if (err || resp.statusCode !== 200) {
+          err = this._error(err, resp)
+          logger.error(err, 'provider.box.size.error')
+          return done(err)
+        }
+        done(null, parseInt(body.size))
+      })
+  }
+
+  logout ({ companion, token }, done) {
+    const { key, secret } = companion.options.providerOptions.box
+
+    return this.client
+      .post('https://api.box.com/oauth2/revoke')
+      .options({
+        formData: {
+          client_id: key,
+          client_secret: secret,
+          token
+        }
+      })
+      .auth(token)
+      .request((err, resp) => {
+        if (err || resp.statusCode !== 200) {
+          logger.error(err, 'provider.box.logout.error')
+          done(this._error(err, resp))
+          return
+        }
+        done(null, { revoked: true })
+      })
+  }
+
+  adaptData (res, companion) {
+    const data = { username: adapter.getUsername(res), items: [] }
+    const items = adapter.getItemSubList(res)
+    items.forEach((item) => {
+      data.items.push({
+        isFolder: adapter.isFolder(item),
+        icon: adapter.getItemIcon(item),
+        name: adapter.getItemName(item),
+        mimeType: adapter.getMimeType(item),
+        id: adapter.getItemId(item),
+        thumbnail: companion.buildURL(adapter.getItemThumbnailUrl(item), true),
+        requestPath: adapter.getItemRequestPath(item),
+        modifiedDate: adapter.getItemModifiedDate(item),
+        size: adapter.getItemSize(item)
+      })
+    })
+
+    data.nextPagePath = adapter.getNextPagePath(res)
+
+    return data
+  }
+
+  _error (err, resp) {
+    if (resp) {
+      const fallbackMessage = `request to ${this.authProvider} returned ${resp.statusCode}`
+      const errMsg = (resp.body || {}).message ? resp.body.message : fallbackMessage
+      return resp.statusCode === 401 ? new ProviderAuthError() : new ProviderApiError(errMsg, resp.statusCode)
+    }
+
+    return err
+  }
+}
+
+module.exports = Box

+ 1 - 1
packages/@uppy/companion/src/server/provider/dropbox/index.js

@@ -189,7 +189,7 @@ class DropBox extends Provider {
       .auth(token)
       .request((err, resp) => {
         if (err || resp.statusCode !== 200) {
-          logger.error(err, 'provider.dropbox.size.error')
+          logger.error(err, 'provider.dropbox.logout.error')
           done(this._error(err, resp))
           return
         }

+ 2 - 1
packages/@uppy/companion/src/server/provider/index.js

@@ -4,6 +4,7 @@
 // @ts-ignore
 const config = require('@purest/providers')
 const dropbox = require('./dropbox')
+const box = require('./box')
 const drive = require('./drive')
 const instagram = require('./instagram/graph')
 const facebook = require('./facebook')
@@ -74,7 +75,7 @@ module.exports.getProviderMiddleware = (providers) => {
  */
 module.exports.getDefaultProviders = (companionOptions) => {
   // @todo: we should rename drive to googledrive or google-drive or google
-  const providers = { dropbox, drive, facebook, onedrive, zoom, instagram }
+  const providers = { dropbox, box, drive, facebook, onedrive, zoom, instagram }
 
   return providers
 }

+ 4 - 0
packages/@uppy/companion/src/standalone/helper.js

@@ -37,6 +37,10 @@ const getConfigFromEnv = () => {
         key: process.env.COMPANION_DROPBOX_KEY,
         secret: getSecret('COMPANION_DROPBOX_SECRET')
       },
+      box: {
+        key: process.env.COMPANION_BOX_KEY,
+        secret: getSecret('COMPANION_BOX_SECRET')
+      },
       instagram: {
         key: process.env.COMPANION_INSTAGRAM_KEY,
         secret: getSecret('COMPANION_INSTAGRAM_SECRET')

+ 1 - 0
packages/@uppy/companion/test/__tests__/companion.js

@@ -10,6 +10,7 @@ const { getServer } = require('../mockserver')
 const authServer = getServer()
 const authData = {
   dropbox: 'token value',
+  box: 'token value',
   drive: 'token value'
 }
 const token = tokenService.generateToken(authData, process.env.COMPANION_SECRET)

+ 20 - 0
packages/@uppy/companion/test/__tests__/provider-manager.js

@@ -16,11 +16,15 @@ describe('Test Provider options', () => {
     expect(grantConfig.dropbox.key).toBe('dropbox_key')
     expect(grantConfig.dropbox.secret).toBe('dropbox_secret')
 
+    expect(grantConfig.box.key).toBe('box_key')
+    expect(grantConfig.box.secret).toBe('box_secret')
+
     expect(grantConfig.google.key).toBe('google_key')
     expect(grantConfig.google.secret).toBe('google_secret')
 
     expect(grantConfig.instagram.key).toBe('instagram_key')
     expect(grantConfig.instagram.secret).toBe('instagram_secret')
+
     expect(grantConfig.zoom.key).toBe('zoom_key')
     expect(grantConfig.zoom.secret).toBe('zoom_secret')
   })
@@ -48,6 +52,16 @@ describe('Test Provider options', () => {
       callback: '/dropbox/callback'
     })
 
+    expect(grantConfig.box).toEqual({
+      key: 'box_key',
+      secret: 'box_secret',
+      transport: 'session',
+      redirect_uri: 'http://localhost:3020/box/redirect',
+      authorize_url: 'https://account.box.com/api/oauth2/authorize',
+      access_url: 'https://api.box.com/oauth2/token',
+      callback: '/box/callback'
+    })
+
     expect(grantConfig.google).toEqual({
       key: 'google_key',
       secret: 'google_secret',
@@ -71,6 +85,7 @@ describe('Test Provider options', () => {
 
   test('adds provider options for secret files', () => {
     process.env.COMPANION_DROPBOX_SECRET_FILE = process.env.PWD + '/test/resources/dropbox_secret_file'
+    process.env.COMPANION_BOX_SECRET_FILE = process.env.PWD + '/test/resources/box_secret_file'
     process.env.COMPANION_GOOGLE_SECRET_FILE = process.env.PWD + '/test/resources/google_secret_file'
     process.env.COMPANION_INSTAGRAM_SECRET_FILE = process.env.PWD + '/test/resources/instagram_secret_file'
     process.env.COMPANION_ZOOM_SECRET_FILE = process.env.PWD + '/test/resources/zoom_secret_file'
@@ -81,6 +96,7 @@ describe('Test Provider options', () => {
     providerManager.addProviderOptions(companionOptions, grantConfig)
 
     expect(grantConfig.dropbox.secret).toBe('xobpord')
+    expect(grantConfig.box.secret).toBe('xobpord')
     expect(grantConfig.google.secret).toBe('elgoog')
     expect(grantConfig.instagram.secret).toBe('margatsni')
     expect(grantConfig.zoom.secret).toBe('u8Z5ceq')
@@ -95,6 +111,9 @@ describe('Test Provider options', () => {
     expect(grantConfig.dropbox.key).toBeUndefined()
     expect(grantConfig.dropbox.secret).toBeUndefined()
 
+    expect(grantConfig.box.key).toBeUndefined()
+    expect(grantConfig.box.secret).toBeUndefined()
+
     expect(grantConfig.google.key).toBeUndefined()
     expect(grantConfig.google.secret).toBeUndefined()
 
@@ -110,6 +129,7 @@ describe('Test Provider options', () => {
     providerManager.addProviderOptions(companionOptions, grantConfig)
 
     expect(grantConfig.dropbox.redirect_uri).toBe('http://domain.com/dropbox/redirect')
+    expect(grantConfig.box.redirect_uri).toBe('http://domain.com/box/redirect')
     expect(grantConfig.google.redirect_uri).toBe('http://domain.com/drive/redirect')
     expect(grantConfig.instagram.redirect_uri).toBe('http://domain.com/instagram/redirect')
     expect(grantConfig.zoom.redirect_uri).toBe('http://domain.com/zoom/redirect')

+ 1 - 0
packages/@uppy/transloadit/src/index.js

@@ -124,6 +124,7 @@ module.exports = class Transloadit extends Plugin {
     }
 
     addPluginVersion('Dropbox', 'uppy-dropbox')
+    addPluginVersion('Box', 'uppy-box')
     addPluginVersion('Facebook', 'uppy-facebook')
     addPluginVersion('GoogleDrive', 'uppy-google-drive')
     addPluginVersion('Instagram', 'uppy-instagram')

+ 2 - 0
packages/uppy/package.json

@@ -19,6 +19,7 @@
     "s3",
     "google drive",
     "dropbox",
+    "box",
     "webcam"
   ],
   "homepage": "https://uppy.io",
@@ -32,6 +33,7 @@
   "dependencies": {
     "@uppy/aws-s3": "file:../@uppy/aws-s3",
     "@uppy/aws-s3-multipart": "file:../@uppy/aws-s3-multipart",
+    "@uppy/box": "file:../@uppy/box",
     "@uppy/companion-client": "file:../@uppy/companion-client",
     "@uppy/core": "file:../@uppy/core",
     "@uppy/dashboard": "file:../@uppy/dashboard",

+ 2 - 0
packages/uppy/types/index.d.ts

@@ -29,6 +29,8 @@ export { StatusBar };
 // Acquirers
 import Dropbox = require('@uppy/dropbox');
 export { Dropbox };
+import Box = require('@uppy/box');
+export { Box };
 import GoogleDrive = require('@uppy/google-drive');
 export { GoogleDrive };
 import Instagram = require('@uppy/instagram');

+ 2 - 0
test/endtoend/providers/main.js

@@ -5,6 +5,7 @@ const Dashboard = require('@uppy/dashboard')
 const GoogleDrive = require('@uppy/google-drive')
 const Instagram = require('@uppy/instagram')
 const Dropbox = require('@uppy/dropbox')
+const Box = require('@uppy/box')
 const Tus = require('@uppy/tus')
 
 const isOnTravis = !!(process.env.TRAVIS && process.env.CI)
@@ -22,6 +23,7 @@ window.uppy = new Uppy({
   .use(GoogleDrive, { target: Dashboard, companionUrl })
   .use(Instagram, { target: Dashboard, companionUrl })
   .use(Dropbox, { target: Dashboard, companionUrl })
+  .use(Box, { target: Dashboard, companionUrl })
   .use(Tus, { endpoint: 'https://master.tus.io/files/' })
 
 if (window.location.search === '?socketerr=true') {

+ 90 - 0
test/endtoend/providers/provider.box.test.js

@@ -0,0 +1,90 @@
+/* global browser  */
+
+/*
+  WARNING! PLEASE READ THIS BEFORE ENABLING THIS TEST ON TRAVIS.
+
+  Before enabling this test on travis, please keep in mind that with this "no_ssl_bump_domains" option set
+  here https://github.com/transloadit/uppy/blob/998c7b1805acb8d305a562dd9726ebae98575e07/.travis.yml#L33
+  SSL encryption may not be enabled between the running companion and the testing browser client.
+
+  Hence, provider tokens (Google, Instagram, Dropbox) may be at risk of getting hijacked.
+*/
+// TODO: @cartfisk - box provider
+const { finishUploadTest, startUploadTest, uploadWithRetry } = require('./helper')
+const testURL = 'http://localhost:4567/providers'
+
+describe('File upload with Box Provider', () => {
+  beforeEach(async () => {
+    await browser.url(testURL)
+  })
+
+  // not using arrow functions as cb so to keep mocha in the 'this' context
+  it('should upload a file completely with Box', async function () {
+    if (!process.env.UPPY_GOOGLE_EMAIL) {
+      console.log('skipping Box integration test')
+      return this.skip()
+    }
+
+    // ensure session is cleared
+    await startUploadTest(browser, 'Box', /box/)
+    // do oauth authentication
+    const authButton = await browser.$('button.auth-google')
+    await authButton.waitForDisplayed()
+    await authButton.click()
+    await browser.pause(3000)
+    // we login with google to avoid captcha
+    await signIntoGoogle(browser)
+    await browser.pause(5000)
+    // if we box displays a warning about trusting the companion app
+    //  we allow it because we trust companion. Companion is our friend.
+    const acceptWarning = await browser.$('#warning-button-continue')
+    if (await acceptWarning.isExisting()) {
+      await acceptWarning.click()
+    }
+
+    await browser.pause(3000)
+    // finish oauth
+    const allowAccessButton = await browser.$('button[name=allow_access]')
+    await allowAccessButton.waitForDisplayed()
+    await allowAccessButton.click()
+
+    await finishUploadTest(browser)
+  })
+
+  // not using arrow functions as cb so to keep mocha in the 'this' context
+  it('should resume uploads when retry is triggered with Box', async function () {
+    if (!process.env.UPPY_GOOGLE_EMAIL) {
+      console.log('skipping Box integration test')
+      return this.skip()
+    }
+
+    await uploadWithRetry(browser, 'Box', testURL)
+  })
+})
+
+const signIntoGoogle = async (browser) => {
+  const emailInput = await browser.$('#identifierId')
+  await emailInput.waitForExist(30000)
+  await emailInput.setValue(process.env.UPPY_GOOGLE_EMAIL)
+  let nextButton = await browser.$('#identifierNext')
+  await nextButton.click()
+
+  const passwordInput = await browser.$('input[name=password]')
+  await passwordInput.waitForDisplayed(30000)
+  await passwordInput.setValue(process.env.UPPY_GOOGLE_PASSWORD)
+  nextButton = await browser.$('#passwordNext')
+  await nextButton.click()
+  await browser.pause(3000)
+
+  const emailListItem = await browser.$(`li div[data-identifier="${process.env.UPPY_GOOGLE_EMAIL}"]`)
+  if (await emailListItem.isExisting()) {
+    // if user is already signed in, just select user
+    await emailListItem.click()
+  }
+
+  const allowBoxButton = await browser.$('#submit_approve_access')
+  if (await allowBoxButton.isExisting()) {
+    // if box has never been allowed, allow it
+    await allowBoxButton.click()
+  }
+}

+ 2 - 0
test/endtoend/typescript/main.ts

@@ -5,6 +5,7 @@ import {
   Dashboard,
   Instagram,
   Dropbox,
+  Box,
   GoogleDrive,
   Url,
   Webcam,
@@ -37,6 +38,7 @@ const uppy = Core<Core.StrictTypes>({
   .use(GoogleDrive, { target: Dashboard, companionUrl: 'http://localhost:3020' })
   .use(Instagram, { target: Dashboard, companionUrl: 'http://localhost:3020' })
   .use(Dropbox, { target: Dashboard, companionUrl: 'http://localhost:3020' })
+  .use(Box, { target: Dashboard, companionUrl: 'http://localhost:3020' })
   .use(Url, { target: Dashboard, companionUrl: 'http://localhost:3020' })
   .use(Webcam, { target: Dashboard })
   .use(Tus, { endpoint: TUS_ENDPOINT })

+ 2 - 0
test/endtoend/utils.js

@@ -79,6 +79,8 @@ class CompanionService {
         COMPANION_SECRET: process.env.TEST_COMPANION_SECRET,
         COMPANION_DROPBOX_KEY: process.env.TEST_COMPANION_DROPBOX_KEY,
         COMPANION_DROPBOX_SECRET: process.env.TEST_COMPANION_DROPBOX_SECRET,
+        COMPANION_BOX_KEY: process.env.TEST_COMPANION_BOX_KEY,
+        COMPANION_BOX_SECRET: process.env.TEST_COMPANION_BOX_SECRET,
         COMPANION_GOOGLE_KEY: process.env.TEST_COMPANION_GOOGLE_KEY,
         COMPANION_GOOGLE_SECRET: process.env.TEST_COMPANION_GOOGLE_SECRET
       }

+ 15 - 0
test/resources/1020-percent-state.json

@@ -44,6 +44,11 @@
           "name": "Dropbox",
           "type": "acquirer"
         },
+        {
+          "id": "Box",
+          "name": "Box",
+          "type": "acquirer"
+        },
         {
           "id": "Url",
           "name": "Link",
@@ -529,6 +534,16 @@
       "filterInput": "",
       "isSearchVisible": false
     },
+    "Box": {
+      "currentSelection": [],
+      "authenticated": false,
+      "files": [],
+      "folders": [],
+      "directories": [],
+      "activeRow": -1,
+      "filterInput": "",
+      "isSearchVisible": false
+    },
     "Webcam": {
       "cameraReady": false
     }

+ 1 - 0
website/inject.js

@@ -43,6 +43,7 @@ const packages = [
   '@uppy/dashboard',
   '@uppy/drag-drop',
   '@uppy/dropbox',
+  '@uppy/box',
   '@uppy/file-input',
   '@uppy/form',
   '@uppy/golden-retriever',

+ 1 - 1
website/src/_template/contributing.md

@@ -105,7 +105,7 @@ Other things to keep in mind during release:
 After a release, the demos on transloadit.com should also be updated. After updating, check that some things work locally:
 
  - the demos in the demo section work (try one that uses an import robot, and one that you need to upload to)
- - the demos on the homepage work and can import from GDrive, Insta, Dropbox
+ - the demos on the homepage work and can import from Google Drive, Instagram, Dropbox, etc.
 
 If you don't have access to the transloadit.com source code ping @arturi or @goto-bus-stop and we'll pick it up. :sparkles:
 

+ 131 - 0
website/src/docs/box.md

@@ -0,0 +1,131 @@
+---
+type: docs
+order: 11
+title: "Box"
+menu_prefix: "<span title='Requires Companion'>ⓒ </span>"
+module: "@uppy/box"
+permalink: docs/box/
+category: "Sources"
+tagline: "import files from Box"
+---
+
+The `@uppy/box` plugin lets users import files from their Box account.
+
+A Companion instance is required for the Box plugin to work. Companion handles authentication with Box, downloads the files, and uploads them to the destination. This saves the user bandwidth, especially helpful if they are on a mobile connection.
+
+```js
+const Box = require('@uppy/box')
+
+uppy.use(Box, {
+  // Options
+})
+```
+
+<a class="TryButton" href="/examples/dashboard/">Try it live</a>
+
+## Installation
+
+This plugin is published as the `@uppy/box` package.
+
+Install from NPM:
+
+```shell
+npm install @uppy/box
+```
+
+In the [CDN package](/docs/#With-a-script-tag), it is available on the `Uppy` global object:
+
+```js
+const Box = Uppy.Box
+```
+
+## Setting Up
+
+To use the Box provider, you need to configure the Box keys that Companion should use. With the standalone Companion server, specify environment variables:
+```shell
+export COMPANION_BOX_KEY="Box API key"
+export COMPANION_BOX_SECRET="Box API secret"
+```
+
+When using the Companion Node.js API, configure these options:
+```js
+companion.app({
+  providerOptions: {
+    box: {
+      key: 'Box API key',
+      secret: 'Box API secret'
+    }
+  }
+})
+```
+
+You can create a Box App on the [Box Developers site](https://app.box.com/developers/console).
+
+Things to note:
+- Choose "Custom App" and select the "Standard OAuth 2.0 (User Authentication)" app type.
+
+You'll be redirected to the app page. This page lists the client ID (app key) and client secret (app secret), which you should use to configure Companion as shown above.
+
+The app page has a "Redirect URIs" field. Here, add:
+```
+https://$YOUR_COMPANION_HOST_NAME/box/redirect
+```
+
+You can only use the integration with your own account initially—make sure to apply for production status on the app page before you publish your app, or your users will not be able to sign in!
+
+## CSS
+
+Dashboard plugin is recommended as a container to all Provider plugins, including Box. If you are using Dashboard, it [comes with all the nessesary styles](/docs/dashboard/#CSS) for Box as well.
+
+⚠️ If you are feeling adventurous, and want to use Box plugin separately, without Dashboard, make sure to include `@uppy/provider-views/dist/style.css` (or `style.min.css`) CSS file. This is experimental, not officially supported and not recommended.
+
+## Options
+
+The `@uppy/box` plugin has the following configurable options:
+
+```js
+uppy.use(Box, {
+  target: Dashboard,
+  companionUrl: 'https://companion.uppy.io/',
+})
+```
+
+### `id: 'Box'`
+
+A unique identifier for this plugin. It defaults to `'Box'`.
+
+### `title: 'Box'`
+
+Title / name shown in the UI, such as Dashboard tabs. It defaults to `'Box'`.
+
+### `target: null`
+
+DOM element, CSS selector, or plugin to mount the Box provider into. This should normally be the [`@uppy/dashboard`](/docs/dashboard) plugin.
+
+### `companionUrl: null`
+
+URL to a [Companion](/docs/companion) instance.
+
+### `companionHeaders: {}`
+
+Custom headers that should be sent along to [Companion](/docs/companion) on every request.
+
+### `companionAllowedHosts: companionUrl`
+
+The valid and authorised URL(s) from which OAuth responses should be accepted.
+
+This value can be a `String`, a `Regex` pattern, or an `Array` of both.
+
+This is useful when you have your [Companion](/docs/companion) running on multiple hosts. Otherwise, the default value should do just fine.
+
+### `locale: {}`
+
+Localize text that is shown to the user.
+
+The default English strings are:
+
+```js
+strings: {
+  // TODO
+}
+```

+ 9 - 1
website/src/docs/companion.md

@@ -21,6 +21,7 @@ As of now, Companion is integrated to work with:
 
 - Google Drive (name `drive`) - [Set up instructions](/docs/google-drive/#Setting-Up)
 - Dropbox (name `dropbox`) - [Set up instructions](/docs/dropbox/#Setting-Up)
+- Box (name `box`) - [Set up instructions](/docs/box/#Setting-Up)
 - Instagram (name `instagram`)
 - Facebook (name `facebook`)
 - OneDrive (name `onedrive`)
@@ -174,6 +175,12 @@ export COMPANION_DROPBOX_SECRET="YOUR DROPBOX SECRET"
 # specifying a secret file will override a directly set secret
 export COMPANION_DROPBOX_SECRET_FILE="PATH/TO/DROPBOX/SECRET/FILE"
 
+# to enable Box
+export COMPANION_BOX_KEY="YOUR BOX KEY"
+export COMPANION_BOX_SECRET="YOUR BOX SECRET"
+# specifying a secret file will override a directly set secret
+export COMPANION_BOX_SECRET_FILE="PATH/TO/BOX/SECRET/FILE"
+
 # to enable Google Drive
 export COMPANION_GOOGLE_KEY="YOUR GOOGLE DRIVE KEY"
 export COMPANION_GOOGLE_SECRET="YOUR GOOGLE DRIVE SECRET"
@@ -502,6 +509,7 @@ to:
 | Dropbox | `https://$COMPANION_HOST_NAME/dropbox/redirect` |
 | Google Drive | `https://$COMPANION_HOST_NAME/drive/redirect` |
 | OneDrive | `https://$COMPANION_HOST_NAME/onedrive/redirect` |
+| Box | `https://$YOUR_COMPANION_HOST_NAME/box/redirect` |
 | Facebook | `https://$COMPANION_HOST_NAME/facebook/redirect` |
 | Instagram | `https://$COMPANION_HOST_NAME/instagram/redirect` |
 
@@ -537,7 +545,7 @@ An example server is running at <https://companion.uppy.io>, which is deployed w
 
 ## How the Authentication and Token mechanism works
 
-This section describes how Authentication works between Companion and Providers. While this behaviour is the same for all Providers (Dropbox, Instagram, Google Drive), we are going to be referring to Dropbox in place of any Provider throughout this section.
+This section describes how Authentication works between Companion and Providers. While this behaviour is the same for all Providers (Dropbox, Instagram, Google Drive, etc.), we are going to be referring to Dropbox in place of any Provider throughout this section.
 
 The following steps describe the actions that take place when a user Authenticates and Uploads from Dropbox through Companion: