Parcourir la source

Merge branch 'master' into docs/improvements

Artur Paikin il y a 7 ans
Parent
commit
855c5e647e

+ 4 - 0
CHANGELOG.md

@@ -48,6 +48,8 @@ Ideas that will be planned and find their way into a release at one point
 - [ ] feature: improved UI for Provider, Google Drive and Instagram, grid/list views
 - [ ] feature: improved UI for Provider, Google Drive and Instagram, grid/list views
 - [ ] feature: React Native support
 - [ ] feature: React Native support
 - [ ] consider iframe / more security for Transloadit/Uppy integration widget and Uppy itself. Page can’t get files from Google Drive if its an iframe; possibility for folder restriction for provider plugins
 - [ ] consider iframe / more security for Transloadit/Uppy integration widget and Uppy itself. Page can’t get files from Google Drive if its an iframe; possibility for folder restriction for provider plugins
+- [ ] It would be nice in the long run to have a dynamic package builder here right on the website where you can select the plugins you need/want and it builds and downloads a minified version of them?
+Sort of like jQuery UI: https://jqueryui.com/download/
 - [ ] test: add https://github.com/pa11y/pa11y for automated accessibility testing?
 - [ ] test: add https://github.com/pa11y/pa11y for automated accessibility testing?
 - [ ] test: add tests for `npm pack`
 - [ ] test: add tests for `npm pack`
 - [ ] test: add deepFreeze to test that state in not mutated anywhere by accident #320
 - [ ] test: add deepFreeze to test that state in not mutated anywhere by accident #320
@@ -55,6 +57,8 @@ Ideas that will be planned and find their way into a release at one point
 - [ ] add typescript definitions and JSDoc everywhere? https://github.com/Microsoft/TypeScript/wiki/Type-Checking-JavaScript-Files
 - [ ] add typescript definitions and JSDoc everywhere? https://github.com/Microsoft/TypeScript/wiki/Type-Checking-JavaScript-Files
 - [ ] transloadit plugin: maybe add option to disable uppy server endpoint overrides
 - [ ] transloadit plugin: maybe add option to disable uppy server endpoint overrides
 - [ ] dragdrop: change border color when files doesn’t pass restrictions on drag https://github.com/transloadit/uppy/issues/607
 - [ ] dragdrop: change border color when files doesn’t pass restrictions on drag https://github.com/transloadit/uppy/issues/607
+- [ ] website: automatically generated page with all locale strings used in plugins
+- [ ] transloadit: option for StatusBar’s upload button to act as a "Start assembly" button? Useful if an assembly uses only import robots, such as /s3/import to start a batch transcoding job.
 
 
 ## 1.0 Goals
 ## 1.0 Goals
 
 

+ 1 - 2
examples/bundled-example/main.js

@@ -15,8 +15,7 @@ const Form = require('../../src/plugins/Form')
 // const DragDrop = require('../../src/plugins/DragDrop')
 // const DragDrop = require('../../src/plugins/DragDrop')
 // const GoldenRetriever = require('../../src/plugins/GoldenRetriever')
 // const GoldenRetriever = require('../../src/plugins/GoldenRetriever')
 
 
-const PROTOCOL = location.protocol === 'https:' ? 'https' : 'http'
-const TUS_ENDPOINT = PROTOCOL + '://master.tus.io/files/'
+const TUS_ENDPOINT = 'https://master.tus.io/files/'
 
 
 const uppy = Uppy({
 const uppy = Uppy({
   debug: true,
   debug: true,

+ 3 - 1
src/core/Utils.js

@@ -124,7 +124,9 @@ function getFileType (file) {
     'svg': 'image/svg+xml',
     'svg': 'image/svg+xml',
     'jpg': 'image/jpeg',
     'jpg': 'image/jpeg',
     'png': 'image/png',
     'png': 'image/png',
-    'gif': 'image/gif'
+    'gif': 'image/gif',
+    'yaml': 'text/yaml',
+    'yml': 'text/yaml'
   }
   }
 
 
   const fileExtension = file.name ? getFileNameAndExtension(file.name).extension : null
   const fileExtension = file.name ? getFileNameAndExtension(file.name).extension : null

+ 8 - 8
src/plugins/Transloadit/index.js

@@ -25,7 +25,7 @@ module.exports = class Transloadit extends Plugin {
     const defaultLocale = {
     const defaultLocale = {
       strings: {
       strings: {
         creatingAssembly: 'Preparing upload...',
         creatingAssembly: 'Preparing upload...',
-        creatingAssemblyFailed: 'Transloadit: Could not create assembly',
+        creatingAssemblyFailed: 'Transloadit: Could not create Assembly',
         encoding: 'Encoding...'
         encoding: 'Encoding...'
       }
       }
     }
     }
@@ -144,7 +144,7 @@ module.exports = class Transloadit extends Plugin {
   createAssembly (fileIDs, uploadID, options) {
   createAssembly (fileIDs, uploadID, options) {
     const pluginOptions = this.opts
     const pluginOptions = this.opts
 
 
-    this.uppy.log('[Transloadit] create assembly')
+    this.uppy.log('[Transloadit] create Assembly')
 
 
     return this.client.createAssembly({
     return this.client.createAssembly({
       params: options.params,
       params: options.params,
@@ -227,7 +227,7 @@ module.exports = class Transloadit extends Plugin {
       return this.connectSocket(assembly)
       return this.connectSocket(assembly)
         .then(() => assembly)
         .then(() => assembly)
     }).then((assembly) => {
     }).then((assembly) => {
-      this.uppy.log('[Transloadit] Created assembly')
+      this.uppy.log('[Transloadit] Created Assembly')
       return assembly
       return assembly
     }).catch((err) => {
     }).catch((err) => {
       this.uppy.info(this.i18n('creatingAssemblyFailed'), 'error', 0)
       this.uppy.info(this.i18n('creatingAssemblyFailed'), 'error', 0)
@@ -396,7 +396,7 @@ module.exports = class Transloadit extends Plugin {
 
 
       const newAssemblies = state.assemblies
       const newAssemblies = state.assemblies
       const previousAssemblies = prevState.assemblies
       const previousAssemblies = prevState.assemblies
-      this.uppy.log('[Transloadit] Current assembly status after restore')
+      this.uppy.log('[Transloadit] Current Assembly status after restore')
       this.uppy.log(newAssemblies)
       this.uppy.log(newAssemblies)
       this.uppy.log('[Transloadit] Assembly status before restore')
       this.uppy.log('[Transloadit] Assembly status before restore')
       this.uppy.log(previousAssemblies)
       this.uppy.log(previousAssemblies)
@@ -689,10 +689,10 @@ module.exports = class Transloadit extends Plugin {
       const onAssemblyFinished = (assembly) => {
       const onAssemblyFinished = (assembly) => {
         // An assembly for a different upload just finished. We can ignore it.
         // An assembly for a different upload just finished. We can ignore it.
         if (assemblyIDs.indexOf(assembly.assembly_id) === -1) {
         if (assemblyIDs.indexOf(assembly.assembly_id) === -1) {
-          this.uppy.log(`[Transloadit] afterUpload(): Ignoring finished assembly ${assembly.assembly_id}`)
+          this.uppy.log(`[Transloadit] afterUpload(): Ignoring finished Assembly ${assembly.assembly_id}`)
           return
           return
         }
         }
-        this.uppy.log(`[Transloadit] afterUpload(): Got assembly finish ${assembly.assembly_id}`)
+        this.uppy.log(`[Transloadit] afterUpload(): Got Assembly finish ${assembly.assembly_id}`)
 
 
         // TODO set the `file.uploadURL` to a result?
         // TODO set the `file.uploadURL` to a result?
         // We will probably need an option here so the plugin user can tell us
         // We will probably need an option here so the plugin user can tell us
@@ -709,10 +709,10 @@ module.exports = class Transloadit extends Plugin {
       const onAssemblyError = (assembly, error) => {
       const onAssemblyError = (assembly, error) => {
         // An assembly for a different upload just errored. We can ignore it.
         // An assembly for a different upload just errored. We can ignore it.
         if (assemblyIDs.indexOf(assembly.assembly_id) === -1) {
         if (assemblyIDs.indexOf(assembly.assembly_id) === -1) {
-          this.uppy.log(`[Transloadit] afterUpload(): Ignoring errored assembly ${assembly.assembly_id}`)
+          this.uppy.log(`[Transloadit] afterUpload(): Ignoring errored Assembly ${assembly.assembly_id}`)
           return
           return
         }
         }
-        this.uppy.log(`[Transloadit] afterUpload(): Got assembly error ${assembly.assembly_id}`)
+        this.uppy.log(`[Transloadit] afterUpload(): Got Assembly error ${assembly.assembly_id}`)
         this.uppy.log(error)
         this.uppy.log(error)
 
 
         // Clear postprocessing state for all our files.
         // Clear postprocessing state for all our files.

+ 4 - 4
src/plugins/Transloadit/index.test.js

@@ -98,7 +98,7 @@ describe('Transloadit', () => {
     })
     })
   })
   })
 
 
-  it('Should merge files with same parameters into one assembly', () => {
+  it('Should merge files with same parameters into one Assembly', () => {
     const uppy = new Core({ autoProceed: false })
     const uppy = new Core({ autoProceed: false })
 
 
     uppy.use(Transloadit, {
     uppy.use(Transloadit, {
@@ -145,11 +145,11 @@ describe('Transloadit', () => {
     })
     })
   })
   })
 
 
-  it('Does not create an assembly if no files are being uploaded', () => {
+  it('Does not create an Assembly if no files are being uploaded', () => {
     const uppy = new Core()
     const uppy = new Core()
     uppy.use(Transloadit, {
     uppy.use(Transloadit, {
       getAssemblyOptions () {
       getAssemblyOptions () {
-        throw new Error('should not create assembly')
+        throw new Error('should not create Assembly')
       }
       }
     })
     })
     uppy.run()
     uppy.run()
@@ -157,7 +157,7 @@ describe('Transloadit', () => {
     return uppy.upload()
     return uppy.upload()
   })
   })
 
 
-  it('Creates an assembly if no files are being uploaded but `alwaysRunAssembly` is enabled', () => {
+  it('Creates an Assembly if no files are being uploaded but `alwaysRunAssembly` is enabled', () => {
     const uppy = new Core()
     const uppy = new Core()
     uppy.use(Transloadit, {
     uppy.use(Transloadit, {
       alwaysRunAssembly: true,
       alwaysRunAssembly: true,

+ 0 - 8
website/src/docs/how-to-plugin.md

@@ -1,8 +0,0 @@
----
-title: "How To Make A Plugin"
-permalink: docs/how-to-plugin/
-order: 20
-published: false
----
-
-There are a few useful Uppy plugins out there, but there might come a time when you’ll want to build your own. This document will guide you.

+ 122 - 68
website/src/docs/server.md

@@ -77,113 +77,180 @@ uppy.socket(server, options)
 ```
 ```
 This takes your `server` instance and your Uppy [options](#Options) as parameters.
 This takes your `server` instance and your Uppy [options](#Options) as parameters.
 
 
-### Run as standalone server
-
-> Please ensure that the required environment variables are set before running/using Uppy Server as a standalone server. See [Configure Standalone](#Configure-Standalone) for the variables required.
-
-Set environment variables first:
+### Run uppy-server on kuberenetes
 
 
+You can use our docker container to run uppy-server on kubernetes with the following configuration.
 ```bash
 ```bash
-export UPPYSERVER_SECRET="shh!Issa Secret!"
-export UPPYSERVER_DOMAIN="YOUR SERVER DOMAIN"
-export UPPYSERVER_DATADIR="PATH/TO/DOWNLOAD/DIRECTORY"
+kubectl create ns uppy
 ```
 ```
-
-And then run:
+We will need a Redis container that we can get through [helm](https://github.com/kubernetes/helm)
 
 
 ```bash
 ```bash
-uppy-server
+ helm install --name uppy \
+  --namespace uppy \
+  --set redisPassword=superSecretPassword \
+    stable/redis
 ```
 ```
 
 
-If you cloned the repo from GitHub and want to run it as a standalone server, you may also run the following command from within its directory:
-
-```bash
-npm start
+> uppy-server-env.yml
+```yaml
+apiVersion: v1
+data:
+  UPPY_ENDPOINTS: "localhost:3452,uppy.io"
+  UPPYSERVER_DATADIR: "PATH/TO/DOWNLOAD/DIRECTORY"
+  UPPYSERVER_DOMAIN: "YOUR SERVER DOMAIN"
+  UPPYSERVER_DOMAINS: "sub1.domain.com,sub2.domain.com,sub3.domain.com"
+  UPPYSERVER_PROTOCOL: "YOUR SERVER PROTOCOL"
+  UPPYSERVER_REDIS_URL: redis://:superSecretPassword@uppy-redis.uppy.svc.cluster.local:6379
+  UPPYSERVER_SECRET: "shh!Issa Secret!"
+  UPPYSERVER_DROPBOX_KEY: "YOUR DROPBOX KEY"
+  UPPYSERVER_DROPBOX_SECRET: "YOUR DROPBOX SECRET"
+  UPPYSERVER_GOOGLE_KEY: "YOUR GOOGLE KEY"
+  UPPYSERVER_GOOGLE_SECRET: "YOUR GOOGLE SECRET"
+  UPPYSERVER_INSTAGRAM_KEY: "YOUR INSTAGRAM KEY"
+  UPPYSERVER_INSTAGRAM_SECRET: "YOUR INSTAGRAM SECRET"
+  UPPYSERVER_AWS_KEY: "YOUR AWS KEY"
+  UPPYSERVER_AWS_SECRET: "YOUR AWS SECRET"
+  UPPYSERVER_AWS_BUCKET: "YOUR AWS S3 BUCKET"
+  UPPYSERVER_AWS_REGION: "AWS REGION"
+  UPPYSERVER_OAUTH_DOMAIN: "sub.domain.com"
+  UPPYSERVER_UPLOAD_URLS: "http://master.tus.io/files/,https://master.tus.io/files/"
+kind: Secret
+metadata:
+  name: uppy-server-env
+  namespace: uppy
+type: Opaque
 ```
 ```
 
 
-You can also pass in the path to your json config file like so:
-
-```bash
-uppy-server --config /path/to/uppyconf.json
+> uppy-server-deployment.yml
+```yaml
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+  name: uppy-server
+  namespace: uppy
+spec:
+  replicas: 2
+  minReadySeconds: 5
+  strategy:
+    type: RollingUpdate
+    rollingUpdate:
+      maxSurge: 2
+      maxUnavailable: 1
+  template:
+    metadata:
+      labels:
+        app: uppy-server
+    spec:
+      containers:
+      - image: docker.io/transloadit/uppy-server:latest
+        imagePullPolicy: ifNotPresent
+        name: uppy-server        
+        resources:
+          limits:
+            memory: 150Mi
+          requests:
+            memory: 100Mi
+        envFrom:
+        - secretRef:
+            name: uppy-server-env
+        ports:
+        - containerPort: 3020
+        volumeMounts:
+        - name: uppy-server-data
+          mountPath: /mnt/uppy-server-data
+      volumes:
+      - name: uppy-server-data
+        emptyDir: {}
 ```
 ```
 
 
-or
-
-```bash
-npm start -- --config /path/to/uppyconf.json
+`kubectl apply -f uppy-server-deployment.yml`
+
+> uppy-server-service.yml
+
+```yaml
+apiVersion: v1
+kind: Service
+metadata:
+  name: uppy-server
+  namespace: uppy
+spec:
+  ports:
+  - port: 80
+    targetPort: 3020
+    protocol: TCP
+  selector:
+    app: uppy-server
 ```
 ```
 
 
-Please see [options](#Options) for possible options.
+`kubectl apply -f uppy-server-service.yml`
 
 
-#### Configure Standalone
 
 
-To run Uppy Server as a standalone server, you are required to set your Uppy [options](#Options) via environment variables:
+You can check the full list of possible environment variables here: 
 
 
 ```bash
 ```bash
 ####### Mandatory variables ###########
 ####### Mandatory variables ###########
 
 
 # any long set of random characters for the server session
 # any long set of random characters for the server session
-export UPPYSERVER_SECRET="shh!Issa Secret!"
+UPPYSERVER_SECRET="shh!Issa Secret!"
 # corresponds to the server.host option
 # corresponds to the server.host option
-export UPPYSERVER_DOMAIN="YOUR SERVER DOMAIN"
+UPPYSERVER_DOMAIN="YOUR SERVER DOMAIN"
 # corresponds to the filePath option
 # corresponds to the filePath option
-export UPPYSERVER_DATADIR="PATH/TO/DOWNLOAD/DIRECTORY"
+UPPYSERVER_DATADIR="PATH/TO/DOWNLOAD/DIRECTORY"
 
 
 ###### Optional variables ##########
 ###### Optional variables ##########
 
 
 # corresponds to the server.protocol option. defaults to http
 # corresponds to the server.protocol option. defaults to http
-export UPPYSERVER_PROTOCOL="YOUR SERVER PROTOCOL"
+UPPYSERVER_PROTOCOL="YOUR SERVER PROTOCOL"
 # the port to start the server on. defaults to 3020
 # the port to start the server on. defaults to 3020
-export UPPYSERVER_PORT="YOUR SERVER PORT"
+UPPYSERVER_PORT="YOUR SERVER PORT"
 # corresponds to the server.port option. defaults to ''
 # corresponds to the server.port option. defaults to ''
-export UPPYSERVER_PATH="/SERVER/PATH/TO/WHERE/UPPY-SERVER/LIVES"
+UPPYSERVER_PATH="/SERVER/PATH/TO/WHERE/UPPY-SERVER/LIVES"
 
 
 # use this in place of UPPYSERVER_PATH if the server path should not be
 # use this in place of UPPYSERVER_PATH if the server path should not be
 # handled by the express.js app but maybe by an external server configuration
 # handled by the express.js app but maybe by an external server configuration
 # instead (e.g Nginx).
 # instead (e.g Nginx).
-export UPPYSERVER_IMPLICIT_PATH="/SERVER/PATH/TO/WHERE/UPPY/SERVER/LIVES"
+UPPYSERVER_IMPLICIT_PATH="/SERVER/PATH/TO/WHERE/UPPY/SERVER/LIVES"
 
 
 # comma separated client hosts to whitlelist by the server
 # comma separated client hosts to whitlelist by the server
 # if not specified, the server would allow any host
 # if not specified, the server would allow any host
-export UPPY_ENDPOINTS="localhost:3452,uppy.io"
+UPPY_ENDPOINTS="localhost:3452,uppy.io"
 
 
 # corresponds to the redisUrl option
 # corresponds to the redisUrl option
 # This also enables redis session storage if set
 # This also enables redis session storage if set
-export UPPYSERVER_REDIS_URL="REDIS URL"
+UPPYSERVER_REDIS_URL="REDIS URL"
 
 
 # to enable Dropbox
 # to enable Dropbox
-export UPPYSERVER_DROPBOX_KEY="YOUR DROPBOX KEY"
-export UPPYSERVER_DROPBOX_SECRET="YOUR DROPBOX SECRET"
+UPPYSERVER_DROPBOX_KEY="YOUR DROPBOX KEY"
+UPPYSERVER_DROPBOX_SECRET="YOUR DROPBOX SECRET"
 
 
 # to enable Google Drive
 # to enable Google Drive
-export UPPYSERVER_GOOGLE_KEY="YOUR GOOGLE KEY"
-export UPPYSERVER_GOOGLE_SECRET="YOUR GOOGLE SECRET"
+UPPYSERVER_GOOGLE_KEY="YOUR GOOGLE KEY"
+UPPYSERVER_GOOGLE_SECRET="YOUR GOOGLE SECRET"
 
 
 # to enable Instagram
 # to enable Instagram
-export UPPYSERVER_INSTAGRAM_KEY="YOUR INSTAGRAM KEY"
-export UPPYSERVER_INSTAGRAM_SECRET="YOUR INSTAGRAM SECRET"
+UPPYSERVER_INSTAGRAM_KEY="YOUR INSTAGRAM KEY"
+UPPYSERVER_INSTAGRAM_SECRET="YOUR INSTAGRAM SECRET"
 
 
 # to enable s3
 # to enable s3
-export UPPYSERVER_AWS_KEY="YOUR AWS KEY"
-export UPPYSERVER_AWS_SECRET="YOUR AWS SECRET"
-export UPPYSERVER_AWS_BUCKET="YOUR AWS S3 BUCKET"
-export UPPYSERVER_AWS_REGION="AWS REGION"
+UPPYSERVER_AWS_KEY="YOUR AWS KEY"
+UPPYSERVER_AWS_SECRET="YOUR AWS SECRET"
+UPPYSERVER_AWS_BUCKET="YOUR AWS S3 BUCKET"
+UPPYSERVER_AWS_REGION="AWS REGION"
 
 
 # corresponds to the server.oauthDomain option
 # corresponds to the server.oauthDomain option
-export UPPYSERVER_OAUTH_DOMAIN="sub.domain.com"
+UPPYSERVER_OAUTH_DOMAIN="sub.domain.com"
 # corresponds to the server.validHosts option
 # corresponds to the server.validHosts option
-export UPPYSERVER_DOMAINS="sub1.domain.com,sub2.domain.com,sub3.domain.com"
+UPPYSERVER_DOMAINS="sub1.domain.com,sub2.domain.com,sub3.domain.com"
 
 
 # corresponds to the sendSelfEndpoint option
 # corresponds to the sendSelfEndpoint option
-export UPPYSERVER_SELF_ENDPOINT="THIS SHOULD BE SAME AS YOUR DOMAIN + PATH"
+UPPYSERVER_SELF_ENDPOINT="THIS SHOULD BE SAME AS YOUR DOMAIN + PATH"
 
 
 # comma separated urls
 # comma separated urls
 # corresponds to the uploadUrls option
 # corresponds to the uploadUrls option
-export UPPYSERVER_UPLOAD_URLS="http://master.tus.io/files/,https://master.tus.io/files/"
+UPPYSERVER_UPLOAD_URLS="http://master.tus.io/files/,https://master.tus.io/files/"
 ```
 ```
 
 
-See [env.example.sh](https://github.com/transloadit/uppy-server/blob/master/env.example.sh) for an example configuration script.
-
 ### Options
 ### Options
 
 
 ```javascript
 ```javascript
@@ -329,27 +396,14 @@ npm run start:dev
 
 
 This would get the Uppy Server running on `http://localhost:3020`.
 This would get the Uppy Server running on `http://localhost:3020`.
 
 
-It also expects the [Uppy](https://github.com/transloadit/uppy) (client) to be running on `http://localhost:3452` by default.
-
 ## Running example
 ## Running example
 
 
-An example server is running at http://server.uppy.io, which is deployed via
-[Frey](https://github.com/kvz/frey), using the following [Freyfile](infra/Freyfile.toml).
-
-All the secrets are stored in `env.infra.sh`, so using `env.infra.example.sh`, you could use the same Freyfile but target a different cloud vendor with different secrets, and run your own Uppy Server.
+You can checkout uppy-server [repository](https://github.com/transloadit/uppy-server) to see how we run [server.uppy.io](https://server.uppy.io).
 
 
 ## Logging
 ## Logging
 
 
-Requires Frey, if you haven't set it up yet type
+You can check the production logs for the production pod using: 
 
 
 ```bash
 ```bash
-npm run install:frey
-```
-
-afterwards, production logs are available through:
-
-```bash
-npm run logtail
-```
-
-This requires at least the `FREY_ENCRYPTION_SECRET` key present in your `./env.sh`.
+kubectl logs my-pod-name 
+```

+ 6 - 0
website/src/docs/uppy.md

@@ -275,6 +275,12 @@ uppy.setState({files: updatedFiles})
 
 
 Returns `uppy.state`, which you can also use directly.
 Returns `uppy.state`, which you can also use directly.
 
 
+### `uppy.setFileState(fileID, state)`
+
+Update the state for a single file. This is mostly useful for plugins that may want to store data on file objects, or that need to pass file-spcecific configuration to other plugins that support it.
+
+`fileID` is the string file ID. `state` is an object that will be merged into the file's state object.
+
 ### `uppy.setMeta(data)`
 ### `uppy.setMeta(data)`
 
 
 Alters global `meta` object is state, the one that can be set in Uppy options and gets merged with all newly added files. Calling `setMeta` will also merge newly added meta data with previously selected files.
 Alters global `meta` object is state, the one that can be set in Uppy options and gets merged with all newly added files. Calling `setMeta` will also merge newly added meta data with previously selected files.

+ 195 - 0
website/src/docs/writing-plugins.md

@@ -0,0 +1,195 @@
+---
+type: docs
+title: "Writing Plugins"
+permalink: docs/writing-plugins/
+order: 20
+---
+
+There are a few useful Uppy plugins out there, but there might come a time when you’ll want to build your own.
+Plugins can hook into the upload process or render a custom UI, typically to:
+
+ - Render some custom UI element, e.g. [StatusBar](/docs/statusbar) or [Dashboard](/docs/dashboard).
+ - Do the actual uploading, e.g. [XHRUpload](/docs/xhrupload) or [Tus](/docs/tus).
+ - Interact with a third party service to process uploads correctly, e.g. [Transloadit](/docs/transloadit) or [AwsS3](/docs/aws-s3).
+
+## Creating A Plugin
+
+Plugins are classes that extend from Uppy's `Plugin` class. Each plugin has an `id` and a `type`. `id`s are used to uniquely identify plugins. A `type` can be anything—some plugins use `type`s to determine whether to do something to some other plugin. For example, when targeting plugins at the builtin `Dashboard` plugin, the Dashboard uses the `type` to figure out where to mount different UI elements. `'acquirer'` type plugins are mounted into the tab bar, while `'progressindicator'` type plugins are mounted into the progress bar area.
+
+The plugin constructor receives the Uppy instance in the first parameter, and any options passed to `uppy.use()` in the second parameter.
+
+```js
+const Plugin = require('uppy/lib/plugins/Plugin')
+module.exports = class MyPlugin extends Plugin {
+  constructor (uppy, opts) {
+    super(uppy, opts)
+    this.id = opts.id || 'MyPlugin'
+    this.type = 'example'
+  }
+}
+```
+
+## Methods
+
+Plugins can implement methods in order to execute certain tasks. The most important method is `install()`, which is called when a plugin is `.use`d.
+
+All of the below methods are optional! Only implement the methods you need.
+
+### `install()`
+
+Called when the plugin is `.use`d. Do any setup work here, like attaching events or adding [upload hooks](#Upload-Hooks).
+
+```js
+install () {
+  this.uppy.on('upload-progress', this.onProgress)
+  this.uppy.addPostProcessor(this.afterUpload)
+}
+```
+
+### `uninstall()`
+
+Called when the plugin is removed, or the Uppy instance is closed. This should undo all of the work done in the `install()` method.
+
+```js
+uninstall () {
+  this.uppy.off('upload-progress', this.onProgress)
+  this.uppy.removePostProcessor(this.afterUpload)
+}
+```
+
+### `update(state)`
+
+Called on each state update. It's rare that you'd need to use this, it's mostly handy if you want to build a UI plugin using something other than preact.
+
+## Upload Hooks
+
+When creating an upload, Uppy runs files through an upload pipeline. The pipeline consists of three parts, each of which can be hooked into: Preprocessing, Uploading, and Postprocessing. Preprocessors can be used to configure uploader plugins, encrypt files, resize images, etc., before uploading them. Uploaders do the actual uploading work, such as creating an XMLHttpRequest object and sending the file. Postprocessors do work after files have been uploaded completely. This could be anything from waiting for a file to propagate across a CDN, to sending another request to relate some metadata to the file.
+
+Each hook is a function that receives an array containing the file IDs that are being uploaded, and returns a Promise to signal completion. Hooks are added and removed through `Uppy` methods: `addPreProcessor`, `addUploader`, `addPostProcessor`, and their `remove*` counterparts. Normally, hooks should be added during the plugin's `install()` method, and removed during the `uninstall()` method.
+
+Additionally, upload hooks can fire events to signal progress.
+
+When adding hooks, make sure to bind the hook `fn` beforehand! Otherwise it will be impossible to remove. For example:
+
+```js
+class MyPlugin extends Plugin {
+  constructor (uppy, opts) {
+    super(uppy, opts)
+    this.id = opts.id || 'MyPlugin'
+    this.type = 'example'
+    this.prepareUpload = this.prepareUpload.bind(this) // ← this!
+  }
+
+  prepareUpload (fileIDs) {
+    return Promise.resolve()
+  }
+
+  install () {
+    this.uppy.addPreProcessor(this.prepareUpload)
+  }
+
+  uninstall () {
+    this.uppy.removePreProcessor(this.prepareUpload)
+  }
+}
+```
+
+### `addPreProcessor(fn)`
+
+Add a preprocessing function. `fn` gets called with a list of file IDs before an upload starts. `fn` should return a Promise. Its resolution value is ignored. To change file data and such, use Uppy state updates, for example using [`setFileState`][core.setfilestate].
+
+### `addUploader(fn)`
+
+Add an uploader function. `fn` gets called with a list of file IDs when an upload should start. Uploader functions should do the actual uploading work, such as creating and sending an XMLHttpRequest or calling into some upload service's SDK. `fn` should return a Promise that resolves once all files have been uploaded.
+
+You may choose to still resolve the Promise if some file uploads fail. This way any postprocessing will still run on the files that were uploaded successfully, while uploads that failed will be retried when `uppy.retryAll` is called.
+
+### `addPostProcessor(fn)`
+
+Add a postprocessing function. `fn` is called with a list of file IDs when an upload has finished. `fn` should return a Promise that resolves when the processing work is complete. Again, the resolution value of the Promise is ignored. This hook can be used to do any finishing work. For example, you could wait for file encoding or CDN propagation to complete, or you could do an HTTP API call to create an album containing all images that were just uploaded.
+
+### `removePreProcessor/removeUploader/removePostProcessor(fn)`
+
+Remove a processor or uploader function that was added previously. Normally this should be done in the `uninstall()` method.
+
+## Progress Events
+
+Progress events can be fired for individual files to show feedback to the user. For upload progress events, only emitting how many bytes are expected and how many have been uploaded is enough. Uppy will handle calculating progress percentages, upload speed, etc.
+
+Preprocessing and postprocessing progress events are plugin-dependent and can refer to anything, so Uppy doesn't try to be smart about them. There are two types of processing progress events: determinate and indeterminate. Some processing does not have meaningful progress beyond "not done" and "done". For example, sending a request to initialize a server-side resource that will be uploaded to. In those situations, indeterminate progress is suitable. Other processing does have meaningful progress. For example, encrypting a large file. In those situations, determinate progress is suitable.
+
+### `preprocess-progress(fileID, progress)`
+
+`progress` is an object with properties:
+
+ - `mode` - Either `'determinate'` or `'indeterminate'`.
+ - `message` - A message to show to the user. Something like `'Preparing upload...'`, but be more specific if possible.
+
+When `mode` is `'determinate'`, also add the `value` property:
+
+ - `value` - A progress value between 0 and 1.
+
+### `upload-progress(progress)`
+
+`progress` is an object with properties:
+
+ - `uploader` - The uploader plugin that fired the event (`this`).
+ - `id` - The file ID.
+ - `bytesTotal` - The full amount of bytes to be uploaded.
+ - `bytesUploaded` - The amount of bytes that have been uploaded so far.
+
+### `postprocess-progress(fileID, progress)`
+
+`progress` is an object with properties:
+
+ - `mode` - Either `'determinate'` or `'indeterminate'`.
+ - `message` - A message to show to the user. Something like `'Preparing upload...'`, but be more specific if possible.
+
+When `mode` is `'determinate'`, also add the `value` property:
+
+ - `value` - A progress value between 0 and 1.
+
+## UI Plugins
+
+UI Plugins can be used to show a user interface. Uppy plugins use [preact](https://preactjs.com) for rendering. preact is a very small React-like library that works really well with Uppy's state architecture. Uppy implements preact rendering in the `mount(target)` and `update()` plugin methods, so if you want to write a custom UI plugin using some other library, you can override those methods.
+
+Plugins can implement certain methods to do so, that will be called by Uppy when necessary:
+
+### `mount(target)`
+
+Mount this plugin to the `target` element. `target` can be a CSS query selector, a DOM element, or another Plugin. If `target` is a Plugin, the source (current) plugin will register with the target plugin, and the latter can decide how and where to render the source plugin.
+
+This method can be overridden to support for different render engines.
+
+### `render()`
+
+Render this plugin's UI. Uppy uses [preact](https://preactjs.com) as its view engine, so `render()` should return a preact element.
+`render` is automatically called by Uppy on each state change.
+
+Note that we are looking into ways to make Uppy render engine agnostic, so that plugins can choose their own favourite library—whether it's preact, choo, jQuery, or anything else. This means that the `render()` API may change in the future, but we'll detail exactly what you need to do on the [blog](https://uppy.io/blog) if and when that happens.
+
+### JSX
+
+Since Uppy uses Preact and not React, the default Babel configuration for JSX elements does not work. You have to import the preact `h` function and tell Babel to use it by adding a `/** @jsx h */` comment at the top of the file.
+
+See the Preact [Getting Started Guide](https://preactjs.com/guide/getting-started) for more on Babel and JSX.
+
+```js
+/** @jsx h */
+const Plugin = require('uppy/lib/core/Plugin')
+const { h } = require('preact')
+
+class NumFiles extends Plugin {
+  render () {
+    const numFiles = Object.keys(this.uppy.state.files).length
+
+    return (
+      <div>
+        Number of files: {numFiles}
+      </div>
+    )
+  }
+}
+```
+
+[core.setfilestate]: /docs/uppy#uppy-setFileState-fileID-state