title: "Adding Custom Providers: Step by Step Tutorial to add Custom Providers" date: 2020-06-08 author: ife
In this post we will be going through a step by step tutorial on how to develop, and add custom providers to Uppy and Companion. For the purpose of this tutorial we will be building a custom provider for Unsplash.
Provider Plugins help you connect to your accounts with remote file storage providers such as Dropbox, Google Drive, Instagram and remote URLs.
They work tightly with Companion — Uppy's server-to-server communication handler between your server and file storage providers.
Read more on Provider Plugins.
Creating a custom provider composes of two parts; Custom Companion Provider and Custom Uppy Plugin
On the Companion side, we need to create a Provider that receives the provider related requests that would come from Uppy.
We'll start off by setting up a minimal express server.
mkdir custom-provider
cd custom-provider
npm init
to setup your package.json
filenpm i -S express express-session body-parser request @uppy/companion uppy
with all that done, my package.json file looks something like this:
{
"name": "custom-provider",
"dependencies": {
"body-parser": "^1.18.2",
"express": "^4.16.2",
"express-session": "^1.15.6",
"request": "^2.88.0",
"uppy": "^1.16.1",
"@uppy/companion": "^2.0.0"
},
"private": true,
"scripts": {}
}
server/index.js
file within the project and add initiate your express serverconst express = require('express')
const companion = require('@uppy/companion')
const bodyParser = require('body-parser')
const session = require('express-session')
const app = express()
app.use(bodyParser.json())
app.use(session({
secret: 'some-secret',
resave: true,
saveUninitialized: true,
}))
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*')
res.setHeader(
'Access-Control-Allow-Methods',
'GET, POST, OPTIONS, PUT, PATCH, DELETE'
)
res.setHeader(
'Access-Control-Allow-Headers',
'Authorization, Origin, Content-Type, Accept'
)
next()
})
// initialize uppy
const companionOptions = {
providerOptions: {
dropbox: {
key: 'your Dropbox key',
secret: 'your Dropbox secret',
},
},
server: {
host: 'localhost:3020',
protocol: 'http',
},
filePath: './output',
secret: 'some-secret',
debug: true,
}
app.use(companion.app(companionOptions))
// handle 404
app.use((req, res, next) => {
return res.status(404).json({ message: 'Not Found' })
})
companion.socket(app.listen(3020))
console.log('Welcome to Companion!')
console.log(`Listening on http://0.0.0.0:3020`)
The code snippet above sets up an express server and plugs Companion into it. However, the Companion setup doesn't include a custom provider yet. It only includes the Dropbox provider.
To be sure everything is working correctly so far, you can start the server by running:
node server/index.js
It should log the following message in the console:
Welcome to Companion!
Listening on http://0.0.0.0:3020
Now we need to proceed to add our custom provider.
Head to Unsplash's developer platform (you'd need to create an account if you don't already have one).
Click "New Application" to create a new application. For the sake of this tutorial, I am naming my application "companion-provider". Once it is created, your page should look something like this:
Back to our project directory, we are going to create a server/customprovider.js
file. This will contain the code that implements the Unsplash provider.
According to the docs, we need to create a class that implements the methods list
, download
, and size
.
The constructor of our class will look something like this:
class MyCustomProvider {
constructor (options) {
this.authProvider = 'myunsplash' // the name of our provider (lowercased)
}
// ...
}
Our list
method would look something like this
list ({ token, directory }, done) {
const path = directory ? `/${directory}/photos` : ''
const options = {
url: `${BASE_URL}/collections${path}`,
method: 'GET',
json: true,
headers: {
Authorization: `Bearer ${token}`
}
}
request(options, (err, resp, body) => {
if (err) {
console.log(err)
done(err)
return
}
done(null, this._adaptData(body))
})
}
_adaptData (res) {
const data = {
username: null,
items: [],
nextPagePath: null
}
const items = res
items.forEach((item) => {
const isFolder = !!item.published_at
data.items.push({
isFolder: isFolder,
icon: isFolder ? item.cover_photo.urls.thumb : item.urls.thumb,
name: item.title || item.description,
mimeType: isFolder ? null : 'image/jpeg',
id: item.id,
thumbnail: isFolder ? item.cover_photo.urls.thumb : item.urls.thumb,
requestPath: item.id,
modifiedDate: item.updated_at,
size: null
})
})
return data
}
the method makes an http request to the unsplash API based on the documentation to list collections and list collection's photos. The _adaptData
method exists to help us conform to the list data
Our size
method will look something like this:
size ({ id, token }, done) {
const options = {
url: `${BASE_URL}/photos/${id}`,
method: 'GET',
json: true,
headers: {
Authorization: `Bearer ${token}`
}
}
request(options, (err, resp, body) => {
if (err) {
console.log(err)
done(err)
return
}
done(null, body.width * body.height)
})
}
Unsplash's API doesn't provide the file size, so we are estimating it by multiplying the width and height of the image.
The download
method will look like this:
download ({ id, token }, onData) {
const options = {
url: `${BASE_URL}/photos/${id}`,
method: 'GET',
json: true,
headers: {
Authorization: `Bearer ${token}`
}
}
request(options, (err, resp, body) => {
if (err) {
console.log(err)
return
}
const url = body.links.download
request.get(url)
.on('data', (chunk) => onData(null, chunk))
.on('end', () => onData(null, null))
.on('error', (err) => console.log(err))
})
}
With all of this put together the entire file would look something like this:
const request = require('request')
const BASE_URL = 'https://api.unsplash.com'
class MyCustomProvider {
constructor (options) {
this.authProvider = 'myunsplash'
}
list ({ token, directory }, done) {
const path = directory ? `/${directory}/photos` : ''
const options = {
url: `${BASE_URL}/collections${path}`,
method: 'GET',
json: true,
headers: {
Authorization: `Bearer ${token}`,
},
}
request(options, (err, resp, body) => {
if (err) {
console.log(err)
done(err)
return
}
done(null, this._adaptData(body))
})
}
download ({ id, token }, onData) {
const options = {
url: `${BASE_URL}/photos/${id}`,
method: 'GET',
json: true,
headers: {
Authorization: `Bearer ${token}`,
},
}
request(options, (err, resp, body) => {
if (err) {
console.log(err)
return
}
const url = body.links.download
request.get(url)
.on('data', (chunk) => onData(null, chunk))
.on('end', () => onData(null, null))
.on('error', (err) => console.log(err))
})
}
size ({ id, token }, done) {
const options = {
url: `${BASE_URL}/photos/${id}`,
method: 'GET',
json: true,
headers: {
Authorization: `Bearer ${token}`,
},
}
request(options, (err, resp, body) => {
if (err) {
console.log(err)
done(err)
return
}
done(null, body.width * body.height)
})
}
_adaptData (res) {
const data = {
username: null,
items: [],
nextPagePath: null,
}
const items = res
items.forEach((item) => {
const isFolder = !!item.published_at
data.items.push({
isFolder,
icon: isFolder ? item.cover_photo.urls.thumb : item.urls.thumb,
name: item.title || item.description,
mimeType: isFolder ? null : 'image/jpeg',
id: item.id,
thumbnail: isFolder ? item.cover_photo.urls.thumb : item.urls.thumb,
requestPath: item.id,
modifiedDate: item.updated_at,
size: null,
})
})
return data
}
}
module.exports = MyCustomProvider
Now we can go back to server/index.js
to enable our custom provider:
// initialize uppy
const uppyOptions = {
providerOptions: {
dropbox: {
key: 'your Dropbox key',
secret: 'your Dropbox secret',
},
},
customProviders: {
myunsplash: {
config: {
// source https://unsplash.com/documentation#user-authentication
authorize_url: 'https://unsplash.com/oauth/authorize',
access_url: 'https://unsplash.com/oauth/token',
oauth: 2,
key: 'YOUR UNSPLASH API KEY',
secret: 'YOUR UNSPLASH API SECRET',
},
// you provider module
module: require('./customprovider'),
},
},
server: {
host: 'localhost:3020',
protocol: 'http',
},
filePath: './output',
secret: 'some-secret',
debug: true,
}
You can find your unsplash API key on your Unsplash app dashboard
Now we need to implement the client part of this. To do this we need to implement an Uppy Plugin.
First, we'll create a client/MyCustomProvider.js
file. Following the instructions here, our Uppy Plugin (aka client/MyCustomProvider.js
file) could look something like this:
const { UIPlugin } = require('@uppy/core')
const { Provider } = require('@uppy/companion-client')
const { ProviderViews } = require('@uppy/provider-views')
const { h } = require('preact')
module.exports = class MyCustomProvider extends UIPlugin {
constructor (uppy, opts) {
super(uppy, opts)
this.type = 'acquirer'
this.id = this.opts.id || 'MyCustomProvider'
Provider.initPlugin(this, opts)
this.title = 'MyUnsplash'
this.icon = () => (
<svg width="32" height="32" xmlns="http://www.w3.org/2000/svg">
<path d="M10 9V0h12v9H10zm12 5h10v18H0V14h10v9h12v-9z" fill="#000000" fill-rule="nonzero" />
</svg>
)
this.provider = new Provider(uppy, {
companionUrl: this.opts.companionUrl,
companionHeaders: this.opts.companionHeaders,
provider: 'myunsplash',
pluginId: this.id,
})
this.files = []
this.onFirstRender = this.onFirstRender.bind(this)
this.render = this.render.bind(this)
// merge default options with the ones set by user
this.opts = { ...opts }
}
install () {
this.view = new ProviderViews(this, {
provider: this.provider,
})
const { target } = this.opts
if (target) {
this.mount(target, this)
}
}
uninstall () {
this.view.tearDown()
this.unmount()
}
onFirstRender () {
return this.view.getFolder()
}
render (state) {
return this.view.render(state)
}
}
Asides from implementing the methods required by the Uppy Plugin, we are also implmeneting the method onFirstRender
. This is because we are using the @uppy/provider-views package to reuse its UI componenets. The @uppy/provider-views package requires that our plugin implements an onFirstRender
method.
We are also pre-setting a default plugin with a default object structure which is used by the @uppy/provider-views
package.
With that done, we can now use our new plugin with Uppy. Create a file client/main.js
and initiate Uppy in there like so:
const Uppy = require('@uppy/core')
const Dropbox = require('@uppy/dropbox')
const Tus = require('@uppy/tus')
const Dashboard = require('@uppy/dashboard')
const MyCustomProvider = require('./MyCustomProvider')
const uppy = Uppy({
debug: true,
})
uppy.use(Dropbox, {
companionUrl: 'http://localhost:3020',
})
uppy.use(MyCustomProvider, {
companionUrl: 'http://localhost:3020',
})
uppy.use(Dashboard, {
inline: true,
target: 'body',
plugins: ['Dropbox', 'MyCustomProvider'],
})
uppy.use(Tus, { endpoint: 'https://master.tus.io/files/' })
Now we need to bundle the file so it can be loaded on the browser. To do this, we'd install budo:
npm install budo -g
.babel.config.js
file looking like so:module.exports = (api) => {
api.env('test')
return {
presets: [
['@babel/preset-env', {
modules: false,
loose: true,
}],
],
plugins: [
['@babel/plugin-transform-react-jsx', { pragma: 'h' }],
].filter(Boolean),
}
}
index.html
file (in the root of the project) that uses a bundle file like so:<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Uppy Custom provider Example</title>
<link href="https://releases.transloadit.com/uppy/v1.15.0/uppy.min.css" rel="stylesheet">
</head>
<body>
<script src="bundle.js"></script>
</body>
</html>
budo client/main.js:bundle.js -- -t babelify
. This will bundle your client
side work and have it running on a local server. Please check the console log to see on what port the server is running. As at the time of this writing, it defaults to 9966
(URL will be http://localhost:9966/).You can now load the URL of your client on your browser to see it running. When I run mine, it looks something like this:
In order to use this with our Companion implementation, we need to also start our Companion server by running node server/index.js
.
Congrats! 🎉We have completed our custom plugin at this point. For an example source code on this tutorial, please see the custom provider example.