diff --git a/README.md b/README.md
index 5aaabea..d7188af 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,7 @@ server and then offer them as file downloads with a unique URL. The option to de
 after a set time period discourages users from using the server as a permanent file store.
 
 #### Dependencies
+* golang (https://golang.org/dl/ - for building as standalone)
 * yarn (https://yarnpkg.com/ - for the kiwiirc plugin UI)
 
 #### Downloading the file upload server's source code
@@ -25,6 +26,7 @@ $ go run .
 ```
 
 #### Building the server for production
+
 ```console
 $ go build
 ```
@@ -75,7 +77,7 @@ Add the plugin javascript file to your kiwiirc `config.json` and configure the s
 If you're running the fileuploader server as a webircgateway plugin, use the webircgateway hostname, e.g.
 
 ```json
-		"server": "https://ws.irc.example.com/files",
+	"server": "https://ws.irc.example.com/files",
 ```
 
 ## Database configuration
diff --git a/expirer/expirer.go b/expirer/expirer.go
index fe4d2ea..c163854 100644
--- a/expirer/expirer.go
+++ b/expirer/expirer.go
@@ -12,17 +12,19 @@ type Expirer struct {
 	store              *shardedfilestore.ShardedFileStore
 	maxAge             time.Duration
 	identifiedMaxAge   time.Duration
+	deletedMaxAge      time.Duration
 	jwtSecretsByIssuer map[string]string
 	quitChan           chan struct{} // closes when ticker has been stopped
 	log                *zerolog.Logger
 }
 
-func New(store *shardedfilestore.ShardedFileStore, maxAge, identifiedMaxAge, checkInterval time.Duration, jwtSecretsByIssuer map[string]string, log *zerolog.Logger) *Expirer {
+func New(store *shardedfilestore.ShardedFileStore, maxAge, identifiedMaxAge, deletedMaxAge, checkInterval time.Duration, jwtSecretsByIssuer map[string]string, log *zerolog.Logger) *Expirer {
 	expirer := &Expirer{
 		ticker:             time.NewTicker(checkInterval),
 		store:              store,
 		maxAge:             maxAge,
 		identifiedMaxAge:   identifiedMaxAge,
+		deletedMaxAge:      deletedMaxAge,
 		jwtSecretsByIssuer: jwtSecretsByIssuer,
 		quitChan:           make(chan struct{}),
 		log:                log,
@@ -81,6 +83,13 @@ func (expirer *Expirer) gc(t time.Time) {
 			Str("id", id).
 			Msg("Terminated upload id")
 	}
+
+	err = expirer.deleteExpired()
+	if err != nil {
+		expirer.log.Error().
+			Err(err).
+			Msg("Failed to purge deleted uploads from database")
+	}
 }
 
 func (expirer *Expirer) getExpired() (expiredIds []string, err error) {
@@ -115,3 +124,38 @@ func (expirer *Expirer) getExpired() (expiredIds []string, err error) {
 
 	return
 }
+
+func (expirer *Expirer) deleteExpired() (err error) {
+	switch expirer.store.DBConn.DBConfig.DriverName {
+	case "sqlite3":
+		_, err = expirer.store.DBConn.DB.Exec(`
+			DELETE FROM uploads
+			WHERE
+				CAST(strftime('%s', 'now') AS INTEGER) -- current time
+				>=
+				created_at + (CASE WHEN jwt_account IS NULL THEN $1 ELSE $2 END) + $3 -- expiration time
+			AND deleted == 1
+			`,
+			expirer.maxAge.Seconds(),
+			expirer.identifiedMaxAge.Seconds(),
+			expirer.deletedMaxAge.Seconds(),
+		)
+	case "mysql":
+		_, err = expirer.store.DBConn.DB.Exec(`
+			DELETE FROM uploads
+			WHERE
+				UNIX_TIMESTAMP() -- current time
+				>=
+				created_at + (CASE WHEN jwt_account IS NULL THEN ? ELSE ? END) + ? -- expiration time
+			AND deleted == 1
+			`,
+			expirer.maxAge.Seconds(),
+			expirer.identifiedMaxAge.Seconds(),
+			expirer.deletedMaxAge.Seconds(),
+		)
+	default:
+		panic("Unhandled database driver")
+	}
+
+	return
+}
diff --git a/fallback-embed/fallback-embed-provider.go b/fallback-embed/fallback-embed-provider.go
new file mode 100644
index 0000000..10ff07c
--- /dev/null
+++ b/fallback-embed/fallback-embed-provider.go
@@ -0,0 +1,135 @@
+package fallbackembed
+
+import (
+	"encoding/json"
+	"errors"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"regexp"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// FallbackEmbed represents this package
+type FallbackEmbed struct {
+	data        *Data
+	httpClient  *http.Client
+	targetKey   string
+	providerURL string
+}
+
+// Data represents the data for FallbackEmbed providers
+type Data []struct {
+	Name     string  `json:"name"`
+	Patterns []Regex `json:"patterns"`
+}
+
+// New returns a FallbackEmbed object
+func New(providerURL, targetKey string) *FallbackEmbed {
+	obj := &FallbackEmbed{
+		httpClient: &http.Client{
+			Timeout: time.Second * 30,
+		},
+		providerURL: providerURL,
+		targetKey:   targetKey,
+	}
+
+	return obj
+}
+
+// ParseProviders parses the raw json obtained from noembed.com
+func (f *FallbackEmbed) ParseProviders(buf io.Reader) error {
+	data, err := ioutil.ReadAll(buf)
+	if err != nil {
+		return err
+	}
+
+	var providerData Data
+	err = json.Unmarshal(data, &providerData)
+	if err != nil {
+		return err
+	}
+
+	f.data = &providerData
+	return nil
+}
+
+// Get returns html string
+func (f *FallbackEmbed) Get(wantedURL string, width int, height int) (html string, err error) {
+	if !f.ValidURL(wantedURL) {
+		return
+	}
+
+	// Do replacements
+	reqURL := strings.Replace(f.providerURL, "{url}", url.QueryEscape(wantedURL), 1)
+	reqURL = strings.Replace(reqURL, "{width}", strconv.Itoa(width), 1)
+	reqURL = strings.Replace(reqURL, "{height}", strconv.Itoa(height), 1)
+
+	var httpResp *http.Response
+	httpResp, err = f.httpClient.Get(reqURL)
+	if err != nil {
+		return
+	}
+	defer httpResp.Body.Close()
+
+	var body []byte
+	body, err = ioutil.ReadAll(httpResp.Body)
+	if err != nil {
+		return
+	}
+
+	// Try to parse json response
+	resp := make(map[string]interface{})
+	err = json.Unmarshal(body, &resp)
+	if err != nil {
+		return
+	}
+
+	// Check targetKey exists
+	if jsonVal, ok := resp[f.targetKey]; ok {
+		// Check targetVal is string
+		if htmlString, ok := jsonVal.(string); ok {
+			html = htmlString
+			return
+		}
+	}
+
+	// Check for error key in json response
+	if jsonVal, ok := resp["error"]; ok {
+		// Check error is string
+		if errorString, ok := jsonVal.(string); ok {
+			err = errors.New(errorString)
+			return
+		}
+	}
+
+	err = errors.New(`Fallback embed provider did not include a JSON property of "` + f.targetKey + `"`)
+	return
+}
+
+// ValidURL is used to test if a url is supported by noembed
+func (f *FallbackEmbed) ValidURL(url string) bool {
+	for _, entry := range *f.data {
+		for _, pattern := range entry.Patterns {
+			if pattern.Regexp.MatchString(url) {
+				return true
+			}
+		}
+	}
+	return false
+}
+
+// Regex Unmarshaler
+type Regex struct {
+	regexp.Regexp
+}
+
+// UnmarshalText used to unmarshal regexp's from text
+func (r *Regex) UnmarshalText(text []byte) error {
+	reg, err := regexp.Compile(string(text))
+	r.Regexp = *reg
+	return err
+}
diff --git a/fileuploader-kiwiirc-plugin/src/components/WebPreview.vue b/fileuploader-kiwiirc-plugin/src/components/WebPreview.vue
new file mode 100644
index 0000000..2485976
--- /dev/null
+++ b/fileuploader-kiwiirc-plugin/src/components/WebPreview.vue
@@ -0,0 +1,146 @@
+
+    {{ error }}
+    
+
+
+
+
+
diff --git a/fileuploader-kiwiirc-plugin/src/fileuploader-entry.js b/fileuploader-kiwiirc-plugin/src/fileuploader-entry.js
index 1c5e76f..940b45e 100644
--- a/fileuploader-kiwiirc-plugin/src/fileuploader-entry.js
+++ b/fileuploader-kiwiirc-plugin/src/fileuploader-entry.js
@@ -7,6 +7,7 @@ import '@uppy/dashboard/dist/style.css'
 import '@uppy/webcam/dist/style.css'
 
 import sidebarFileList from './components/SidebarFileList.vue'
+import webPreview from './components/WebPreview.vue'
 import { MiB } from './constants/data-size'
 import { showDashboardOnDragEnter } from './handlers/show-dashboard-on-drag-enter';
 import { uploadOnPaste } from './handlers/upload-on-paste'
@@ -24,6 +25,14 @@ kiwi.plugin('fileuploader', function (kiwiApi, log) {
     setDefaultSetting(kiwiApi, 'fileuploader.server', '/files')
     setDefaultSetting(kiwiApi, 'fileuploader.textPastePromptMinimumLines', 5)
     setDefaultSetting(kiwiApi, 'fileuploader.textPasteNeverPrompt', false)
+    setDefaultSetting(kiwiApi, 'fileuploader.webpreview.enable', true)
+    setDefaultSetting(kiwiApi, 'fileuploader.webpreview.url', '/embed?url={url}¢er={center}&width={width}&height={height}')
+    setDefaultSetting(kiwiApi, 'fileuploader.webpreview.maxHeight', 400)
+    setDefaultSetting(kiwiApi, 'fileuploader.webpreview.maxWidth', 1000)
+
+    if (kiwiApi.state.setting('fileuploader.webpreview.enable')) {
+        kiwiApi.replaceModule('components/UrlEmbed', webPreview)
+    }
 
     // add button to input bar
     const uploadFileButton = document.createElement('i')
diff --git a/fileuploader.config.example.toml b/fileuploader.config.example.toml
index 1ad8ce1..cbc5ceb 100644
--- a/fileuploader.config.example.toml
+++ b/fileuploader.config.example.toml
@@ -43,8 +43,21 @@ Path = "./uploads.db"
 # Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
 MaxAge = "24h" # 1 day
 IdentifiedMaxAge = "168h" # 1 week
+DeletedMaxAge = "720h" # 30 days
 CheckInterval = "5m"
 
+[WebPreview]
+OembedProviderFile = "oembed-providers.json"
+TemplatesDirectory = "templates"
+CacheMaxAge = "1h"
+CacheCleanInterval = "15m"
+
+# Fallback provider specific
+FallbackProviderDisabled = false
+FallbackProviderURL = "https://noembed.com/embed?url={url}"
+FallbackProviderFile = "fallback-providers.json"
+FallbackProviderJsonKey = "html"
+
 # If EXTJWT is supported by the gateway or network, a validated token with an account present (when
 # the user is authenticated to an irc services account) will use the IdentifiedMaxAge setting above
 # instead of the base MaxAge.
diff --git a/go.mod b/go.mod
index 44cfa81..ee58665 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/kiwiirc/plugin-fileuploader
 
-go 1.12
+go 1.16
 
 require (
 	github.com/BurntSushi/toml v0.3.1
@@ -8,6 +8,7 @@ require (
 	github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40 // indirect
 	github.com/c2h5oh/datasize v0.0.0-20171227191756-4eba002a5eae
 	github.com/dgrijalva/jwt-go v3.2.0+incompatible
+	github.com/dyatlov/go-oembed v0.0.0-20191103150536-a57c85b3b37c
 	github.com/gin-contrib/sse v0.1.0 // indirect
 	github.com/gin-gonic/gin v1.4.0
 	github.com/go-sql-driver/mysql v1.4.1
@@ -16,12 +17,15 @@ require (
 	github.com/golang/protobuf v1.3.2 // indirect
 	github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect
 	github.com/gorilla/websocket v1.4.1 // indirect
+	github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc
 	github.com/jmoiron/sqlx v1.2.0
 	github.com/kiwiirc/webircgateway v0.0.0-20200226172020-f8a71090407a
 	github.com/lib/pq v1.1.1 // indirect
 	github.com/mattn/go-isatty v0.0.8 // indirect
 	github.com/mattn/go-sqlite3 v1.10.0
+	github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
 	github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6 // indirect
+	github.com/peterbourgon/diskv v0.0.0-20171120014656-2973218375c3
 	github.com/rs/zerolog v1.14.3
 	github.com/rubenv/sql-migrate v0.0.0-20190618074426-f4d34eae5a5c
 	github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0 // indirect
@@ -32,10 +36,10 @@ require (
 	github.com/ziutek/mymysql v1.5.4 // indirect
 	golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d // indirect
 	golang.org/x/net v0.0.0-20200226121028-0de0cce0169b // indirect
-	golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 // indirect
 	golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect
 	google.golang.org/appengine v1.6.1 // indirect
 	gopkg.in/Acconut/lockfile.v1 v1.1.0
 	gopkg.in/gorp.v1 v1.7.2 // indirect
 	gopkg.in/ini.v1 v1.52.0 // indirect
+	willnorris.com/go/imageproxy v0.10.0
 )
diff --git a/go.sum b/go.sum
index a2ca4bd..74477db 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,16 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.37.1/go.mod h1:SAbnLi6YTSPKSI0dTUEOVLCkyPfKXK8n4ibqiMoj4ok=
+contrib.go.opencensus.io/exporter/ocagent v0.4.9/go.mod h1:ueLzZcP7LPhPulEBukGn4aLh7Mx9YJwpVJ9nL2FYltw=
+git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
+git.apache.org/thrift.git v0.12.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
+github.com/Azure/azure-sdk-for-go v26.5.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
+github.com/Azure/go-autorest v11.5.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/Jeffail/gabs v1.4.0 h1://5fYRRTq1edjfIrQGvdkcd22pkYUrHZ5YC/H2GJVAo=
+github.com/Jeffail/gabs v1.4.0/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 github.com/OneOfOne/xxhash v1.2.4 h1:HZ+j9jn/+mcsaDSQRZuK00pXWdE25AQLtgm8kZct1Ew=
 github.com/OneOfOne/xxhash v1.2.4/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
@@ -7,34 +18,71 @@ github.com/OneOfOne/xxhash v1.2.5 h1:zl/OfRA6nftbBK9qTohYBJ5xvw6C/oNKizR7cZGl3cI
 github.com/OneOfOne/xxhash v1.2.5/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
 github.com/OneOfOne/xxhash v1.2.7 h1:fzrmmkskv067ZQbd9wERNGuxckWw67dyzoMG62p7LMo=
 github.com/OneOfOne/xxhash v1.2.7/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
+github.com/PaulARoy/azurestoragecache v0.0.0-20170906084534-3c249a3ba788/go.mod h1:lY1dZd8HBzJ10eqKERHn3CU59tfhzcAVb2c0ZhIWSOk=
+github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
+github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
+github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/aws/aws-sdk-go v1.19.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40 h1:y4B3+GPxKlrigF1ha5FFErxK+sr6sWxQovRMzwMhejo=
 github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40/go.mod h1:8rLXio+WjiTceGBHIoTvn60HIbs7Hm7bcHjyrSqYB9c=
+github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
 github.com/c2h5oh/datasize v0.0.0-20171227191756-4eba002a5eae h1:2Zmk+8cNvAGuY8AyvZuWpUdpQUAXwfom4ReVMe/CTIo=
 github.com/c2h5oh/datasize v0.0.0-20171227191756-4eba002a5eae/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
+github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/die-net/lrucache v0.0.0-20181227122439-19a39ef22a11/go.mod h1:ew0MSjCVDdtGMjF3kzLK9hwdgF5mOE8SbYVF3Rc7mkU=
+github.com/disintegration/imaging v1.6.0 h1:nVPXRUUQ36Z7MNf0O77UzgnOb1mkMMor7lmJMJXc/mA=
+github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ=
+github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
+github.com/dyatlov/go-oembed v0.0.0-20191103150536-a57c85b3b37c h1:MEV1LrQtCBGacXajlT4CSuYWbZuLl/qaZVqwoOmwAbU=
+github.com/dyatlov/go-oembed v0.0.0-20191103150536-a57c85b3b37c/go.mod h1:DjlDZiZGRRKbiJZmiEiiXozsBQAQzHmxwHKFeXifL2g=
+github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
+github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
+github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
+github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g=
 github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
 github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
 github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
 github.com/gin-gonic/gin v1.4.0 h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ=
 github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
+github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
 github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
 github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/gobuffalo/envy v1.7.0 h1:GlXgaiBkmrYMHco6t4j7SacKO4XUjvh5pwXh0f4uxXU=
 github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
 github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
@@ -45,53 +93,96 @@ github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIavi
 github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw=
 github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
 github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s=
 github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
+github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
 github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4=
 github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
+github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
 github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
 github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
 github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
 github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
+github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc h1:f8eY6cV/x1x+HLjOp4r72s/31/V2aTUtg5oKRRPf8/Q=
+github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
+github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
+github.com/grpc-ecosystem/grpc-gateway v1.6.2/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
+github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/igm/sockjs-go v0.0.0-20181115114233-fd48fe90e436 h1:02X+bCfYLK5AdKlM6RYZ+LlI4MTIBwOf+hRENcv9Mpk=
 github.com/igm/sockjs-go v0.0.0-20181115114233-fd48fe90e436/go.mod h1:Yu6pvqjNniWNJe07LPObeCG6R77Qc97C6Kss0roF8tU=
 github.com/igm/sockjs-go v0.0.0-20191119074118-cd6986df5bcc h1:BKaqvDSmr77q6jHXHxdpsyZSSNntGktvWwvP+pg+cdg=
 github.com/igm/sockjs-go v0.0.0-20191119074118-cd6986df5bcc/go.mod h1:Yu6pvqjNniWNJe07LPObeCG6R77Qc97C6Kss0roF8tU=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/jamiealquiza/envy v1.1.0/go.mod h1:MP36BriGCLwEHhi1OU8E9569JNZrjWfCvzG7RsPnHus=
+github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
 github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
 github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
 github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
 github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE=
 github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/kiwiirc/webircgateway v0.0.0-20190709195406-2c86038cda4f h1:vlP3syZmsRUeGOBk9eaC5cZTzC5w3d/m/A/b+fmGZZ4=
 github.com/kiwiirc/webircgateway v0.0.0-20190709195406-2c86038cda4f/go.mod h1:WoHdFzCnR24cCEdcImTt141bKmhYKs60eUH4Woav2Ls=
 github.com/kiwiirc/webircgateway v0.0.0-20200226172020-f8a71090407a h1:947kcsvRCG/vqoiCN9B7WgXoCfoMVMZQ70PBmoQUQO4=
 github.com/kiwiirc/webircgateway v0.0.0-20200226172020-f8a71090407a/go.mod h1:3OveolwWkB00OKF/05GgyHaPmwe0coJ5RIDk44WAZhw=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
 github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/marstr/guid v0.0.0-20170427235115-8bdf7d1a087c/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho=
 github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
 github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
@@ -99,12 +190,27 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
 github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
 github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/muesli/smartcrop v0.2.1-0.20181030220600-548bbf0c0965 h1:BdOnvj+P+06ZFwYd07iFWXHPfRyrJd5sAXpX9+E8bxM=
+github.com/muesli/smartcrop v0.2.1-0.20181030220600-548bbf0c0965/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
+github.com/openzipkin/zipkin-go v0.1.3/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
+github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
 github.com/orcaman/concurrent-map v0.0.0-20190107190726-7ed82d9cb717 h1:2v7IYkog9ZFN04bv5hkwjpyHkc6wujPPOVYDPp2rfwA=
 github.com/orcaman/concurrent-map v0.0.0-20190107190726-7ed82d9cb717/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
 github.com/orcaman/concurrent-map v0.0.0-20190314100340-2693aad1ed75 h1:IV56VwUb9Ludyr7s53CMuEh4DdTnnQtEPLEgLyJ0kHI=
@@ -112,9 +218,38 @@ github.com/orcaman/concurrent-map v0.0.0-20190314100340-2693aad1ed75/go.mod h1:L
 github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6 h1:lNCW6THrCKBiJBpz8kbVGjC7MgdCGKwuvBgc7LoD6sw=
 github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/peterbourgon/diskv v0.0.0-20171120014656-2973218375c3 h1:ZKRE3mqKoxObHs5oWjLnA1WxXhmlDDAVuE0VsuLIoNk=
+github.com/peterbourgon/diskv v0.0.0-20171120014656-2973218375c3/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
+github.com/peterbourgon/diskv v1.0.0 h1:bRU92KzrX3TQ6IYobfie/PnZkFC+1opBfHpf/PHPDoo=
+github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.4.1 h1:FFSuS004yOQEtDdTq+TAOLP5xUq63KqAFYyOi8zA+Y8=
+github.com/prometheus/client_golang v1.4.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
+github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U=
+github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
+github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8=
+github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
+github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
+github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@@ -124,8 +259,12 @@ github.com/rs/zerolog v1.14.3/go.mod h1:3WXPzbXEEliJ+a6UFE4vhIxV8qR1EML6ngzP9ug4
 github.com/rubenv/sql-migrate v0.0.0-20190618074426-f4d34eae5a5c h1:LCELEbde3/GT141OpHRs+jJZrI1tI3ayVd4VqW7Ui2U=
 github.com/rubenv/sql-migrate v0.0.0-20190618074426-f4d34eae5a5c/go.mod h1:WS0rl9eEliYI8DPnr3TOwz4439pay+qNgzJoVya/DmY=
 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
+github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
+github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
+github.com/satori/go.uuid v0.0.0-20180103174451-36e9d2ebbde5/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
 github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0 h1:X9XMOYjxEfAYSy3xK1DzO5dMkkWhs9E9UCcS1IERx2k=
 github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0/go.mod h1:Ad7IjTpvzZO8Fl0vh9AzQ+j/jYZfyp2diGwI8m5q+ns=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
@@ -150,6 +289,8 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
 github.com/tus/tusd v0.0.0-20190712143443-30811b6579c5 h1:YBHQiRr7TH20CDlIcoH2Fzqqm1C8asoj0NHlOK5Pm4I=
 github.com/tus/tusd v0.0.0-20190712143443-30811b6579c5/go.mod h1:BBkwF03jAYYdT5yGkoojt46c3NLjziO9OYu0zR4ptWg=
 github.com/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw=
@@ -164,6 +305,13 @@ github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:
 github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
 github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
 github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
+go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
+go.opencensus.io v0.19.1/go.mod h1:gug0GbSHa8Pafr0d2urOSgoXHZ6x/RUlaiT0d9pqb4A=
+go.opencensus.io v0.19.2/go.mod h1:NO/8qkisMZLZ1FCsKNqtJPwc8/TaclWyY0B6wcYNg9M=
+go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
+golang.org/x/build v0.0.0-20190314133821-5284462c4bec/go.mod h1:atTaCNAy0f16Ah5aV1gMSwgiKVHwu/JncqDpuRr7lS4=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
@@ -174,18 +322,56 @@ golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20200208060501-ecb85df21340/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw=
 golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
+golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f h1:FO4MZ3N56GnxbqxGKqh+YTzUWQ2sDwtFQEZgLOxh9Jc=
+golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw=
 golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU=
 golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181218192612-074acd46bca6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
 golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -196,40 +382,89 @@ golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 h1:LepdCS8Gf/MVejFIt8lsiexZATdoGVyp5bcyS+rYoUI=
 golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181219222714-6e267b5cc78e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
+google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
+google.golang.org/api v0.0.0-20181220000619-583d854617af/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
+google.golang.org/api v0.2.0/go.mod h1:IfRCZScioGtypHNTlz3gFk67J8uePVW7uDTBzXuIkhU=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I=
 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20181219182458-5a97ab628bfb/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190321212433-e79c0c59cdb5/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
+google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
+google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 gopkg.in/Acconut/lockfile.v1 v1.1.0 h1:c5AMZOxgM1y+Zl8eSbaCENzVYp/LCaWosbQSXzb3FVI=
 gopkg.in/Acconut/lockfile.v1 v1.1.0/go.mod h1:6UCz3wJ8tSFUsPR6uP/j8uegEtDuEEqFxlpi0JI4Umw=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
 gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
 gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
 gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
 gopkg.in/gorp.v1 v1.7.2 h1:j3DWlAyGVv8whO7AcIWznQ2Yj7yJkn34B8s63GViAAw=
 gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk=
 gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/ini.v1 v1.44.0 h1:YRJzTUp0kSYWUVFF5XAbDFfyiqwsl0Vb9R8TVP5eRi0=
 gopkg.in/ini.v1 v1.44.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/ini.v1 v1.52.0 h1:j+Lt/M1oPPejkniCg1TkWE2J3Eh1oZTsHSXzMTzUXn4=
 gopkg.in/ini.v1 v1.52.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c=
+gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
+honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20180920025451-e3ad64cb4ed3/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+willnorris.com/go/gifresize v1.0.0 h1:GKS68zjNhHMqkgNTv4iFAO/j/sNcVSOHQ7SqmDAIAmM=
+willnorris.com/go/gifresize v1.0.0/go.mod h1:eBM8gogBGCcaH603vxSpnfjwXIpq6nmnj/jauBDKtAk=
+willnorris.com/go/imageproxy v0.10.0 h1:onR4Q88jfC+uYKaFaDKJH57xY6UDl9GJ/x7La6MmBsk=
+willnorris.com/go/imageproxy v0.10.0/go.mod h1:2tWdKRneln3E9X/zwH1RINpQAQWPeUiNynZ7UQ9OROk=
diff --git a/server/config.go b/server/config.go
index 5dafb66..d301964 100644
--- a/server/config.go
+++ b/server/config.go
@@ -41,10 +41,26 @@ type Config struct {
 	Expiration struct {
 		MaxAge           duration
 		IdentifiedMaxAge duration
+		DeletedMaxAge    duration
 		CheckInterval    duration
 	}
 	JwtSecretsByIssuer map[string]string
 	Loggers            []LoggerConfig
+
+	// WebPreview config options
+	WebPreview struct {
+		WebPreviewDisabled bool
+		OembedProviderFile string
+		TemplatesDirectory string
+		CacheMaxAge        duration
+		CacheCleanInterval duration
+
+		// Fallback provider configuration
+		FallbackProviderDisabled bool
+		FallbackProviderURL      string
+		FallbackProviderFile     string
+		FallbackProviderJsonKey  string
+	}
 }
 
 func NewConfig() *Config {
diff --git a/server/image-proxy-cache.go b/server/image-proxy-cache.go
new file mode 100644
index 0000000..2771a7c
--- /dev/null
+++ b/server/image-proxy-cache.go
@@ -0,0 +1,101 @@
+package server
+
+import (
+	"bytes"
+	"sync"
+
+	"github.com/kiwiirc/plugin-fileuploader/shardedfilestore"
+	"github.com/rs/zerolog"
+	"github.com/tus/tusd"
+)
+
+// ImageProxyCache is an implementation of httpcache.Cache that supplements the in-memory map with persistent storage
+type ImageProxyCache struct {
+	store  *shardedfilestore.ShardedFileStore
+	log    *zerolog.Logger
+	urlMap sync.Map
+}
+
+// Get returns the response corresponding to key if present
+func (c *ImageProxyCache) Get(key string) (resp []byte, ok bool) {
+	urlHash := getHash(key)
+
+	idInterface, ok := c.urlMap.Load(urlHash)
+	if !ok {
+		// Not in map
+		return []byte{}, false
+	}
+	id := idInterface.(string)
+
+	reader, err := c.store.GetReader(id)
+	if err != nil {
+		// No file to read
+		c.log.Debug().
+			Err(err).
+			Msg("Image missing from shardedfilestore, maybe it was cleaned")
+		c.urlMap.Delete(urlHash)
+		return []byte{}, false
+	}
+
+	buffer := new(bytes.Buffer)
+	_, err = buffer.ReadFrom(reader)
+	if err != nil {
+		// Read error
+		c.log.Debug().
+			Err(err).
+			Msg("Failed to read image from shardedfilestore")
+		c.urlMap.Delete(urlHash)
+		return []byte{}, false
+	}
+
+	bytes := buffer.Bytes()
+	return bytes, true
+}
+
+// Set saves a response to the cache as key
+func (c *ImageProxyCache) Set(key string, resp []byte) {
+	urlHash := getHash(key)
+
+	metaData := tusd.MetaData{
+		"Url": key,
+	}
+	fileInfo := tusd.FileInfo{
+		Size:           int64(len(resp)),
+		SizeIsDeferred: false,
+		MetaData:       metaData,
+		IsFinal:        false,
+	}
+
+	id, err := c.store.NewUpload(fileInfo)
+	if err != nil {
+		c.log.Error().
+			Err(err).
+			Msg("Failed to create new upload")
+		return
+	}
+
+	_, err = c.store.WriteChunk(id, 0, bytes.NewReader(resp))
+	if err != nil {
+		c.log.Error().
+			Err(err).
+			Msg("Failed to write chunk")
+		return
+	}
+
+	c.store.FinishUpload(id)
+	c.urlMap.Store(urlHash, id)
+}
+
+// Delete removes the response with key from the cache
+func (c *ImageProxyCache) Delete(key string) {
+	urlHash := getHash(key)
+	c.urlMap.Delete(urlHash)
+}
+
+// NewImageProxyCache returns a new Cache that will store files in basePath
+func NewImageProxyCache(store *shardedfilestore.ShardedFileStore, log *zerolog.Logger) *ImageProxyCache {
+	return &ImageProxyCache{
+		store: store,
+		log:   log,
+	}
+}
diff --git a/server/uploadserver.go b/server/uploadserver.go
index ca28a43..b6e80aa 100644
--- a/server/uploadserver.go
+++ b/server/uploadserver.go
@@ -1,6 +1,7 @@
 package server
 
 import (
+	"context"
 	"net/http"
 	"sync"
 
@@ -18,6 +19,7 @@ import (
 type UploadServer struct {
 	DBConn *db.DatabaseConnection
 	Router *gin.Engine
+	ctx    *RunContext
 
 	cfg                 Config
 	log                 *zerolog.Logger
@@ -66,11 +68,15 @@ func (serv *UploadServer) Run(replaceableHandler *ReplaceableHandler) error {
 		serv.store,
 		serv.cfg.Expiration.MaxAge.Duration,
 		serv.cfg.Expiration.IdentifiedMaxAge.Duration,
+		serv.cfg.Expiration.DeletedMaxAge.Duration,
 		serv.cfg.Expiration.CheckInterval.Duration,
 		serv.cfg.JwtSecretsByIssuer,
 		serv.log,
 	)
 
+	// If this fails to start it will log its own errors and not register any handlers
+	serv.registerWebPreviewHandlers(serv.Router, serv.cfg)
+
 	err := serv.registerTusHandlers(serv.Router, serv.store)
 	if err != nil {
 		return err
@@ -104,7 +110,7 @@ func (serv *UploadServer) Shutdown() {
 
 	// wait for all requests to finish
 	if serv.httpServer != nil {
-		serv.httpServer.Shutdown(nil)
+		serv.httpServer.Shutdown(context.TODO())
 	}
 
 	// stop running FileStore GC cycles
diff --git a/server/web-preview.go b/server/web-preview.go
new file mode 100644
index 0000000..8f25a90
--- /dev/null
+++ b/server/web-preview.go
@@ -0,0 +1,540 @@
+package server
+
+import (
+	"bufio"
+	"bytes"
+	"crypto/sha256"
+	"encoding/hex"
+	"errors"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"os"
+	"path"
+	"regexp"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/dyatlov/go-oembed/oembed"
+	"github.com/gin-gonic/gin"
+	"willnorris.com/go/imageproxy"
+
+	// required for embeding web-preview.html
+	_ "embed"
+
+	fallbackembed "github.com/kiwiirc/plugin-fileuploader/fallback-embed"
+)
+
+type cacheItem struct {
+	url     string
+	html    string
+	created int64
+	wg      sync.WaitGroup
+}
+
+type imgWaiterItem struct {
+	url     string
+	status  int
+	created int64
+	wg      sync.WaitGroup
+}
+
+var httpClient *http.Client
+
+// HTML template
+//go:embed web-preview.html
+var template string
+var templateLock sync.RWMutex
+
+// In memory HTML cache
+var cache = make(map[string]*cacheItem)
+var cacheMutex sync.Mutex
+var cacheTicker *time.Ticker
+
+// Image waiter
+var imgWaiter = make(map[string]*imgWaiterItem)
+var imgWaiterMutex sync.Mutex
+
+var oEmbed *oembed.Oembed
+var fallbackEmbed *fallbackembed.FallbackEmbed
+var fallbackEmbedDisabled bool
+var imgProxy *imageproxy.Proxy
+
+// Used to detect possible image urls
+var isImage = regexp.MustCompile(`\.(jpe?g|png|gifv?)$`)
+
+func (serv *UploadServer) registerWebPreviewHandlers(r *gin.Engine, cfg Config) error {
+	httpClient = &http.Client{
+		Timeout: time.Second * 30,
+	}
+
+	// Check config defaults
+	webPreviewDisabled := cfg.WebPreview.WebPreviewDisabled
+	if webPreviewDisabled {
+		return nil
+	}
+
+	serv.log.Info().
+		Msg("Starting web preview handlers")
+
+	cacheCleanInterval := cfg.WebPreview.CacheCleanInterval.Duration
+	if cacheCleanInterval == time.Duration(0) {
+		cacheCleanInterval, _ = time.ParseDuration("15m")
+	}
+
+	cacheMaxAge := cfg.WebPreview.CacheMaxAge.Duration
+	if cacheMaxAge == time.Duration(0) {
+		cacheMaxAge, _ = time.ParseDuration("1h")
+	}
+
+	templatesDir := cfg.WebPreview.TemplatesDirectory
+	if templatesDir == "" {
+		templatesDir = "templates"
+	}
+
+	oembedProviderFile := cfg.WebPreview.OembedProviderFile
+	if oembedProviderFile == "" {
+		oembedProviderFile = "oembed-providers.json"
+	}
+
+	fallbackProviderURL := cfg.WebPreview.FallbackProviderURL
+	if fallbackProviderURL == "" {
+		fallbackProviderURL = "https://noembed.com/embed?url={url}"
+	}
+
+	fallbackProviderFile := cfg.WebPreview.FallbackProviderFile
+	if fallbackProviderFile == "" {
+		fallbackProviderFile = "fallback-providers.json"
+	}
+
+	fallbackProviderJsonKey := cfg.WebPreview.FallbackProviderJsonKey
+	if fallbackProviderJsonKey == "" {
+		fallbackProviderJsonKey = "html"
+	}
+
+	fallbackEmbedDisabled = cfg.WebPreview.FallbackProviderDisabled
+
+	// Prepare oEmbed provider
+	oembedJSON, err := serv.getProviderFileOrURL(oembedProviderFile, "https://oembed.com/providers.json")
+	if err != nil {
+		serv.log.Error().
+			Err(err).
+			Msg("Failed to get oembed providers json")
+		return err
+	}
+	oEmbed = oembed.NewOembed()
+	err = oEmbed.ParseProviders(oembedJSON)
+	if err != nil {
+		serv.log.Error().
+			Err(err).
+			Msg("Failed to parse oembed providers json")
+		return err
+	}
+
+	if !fallbackEmbedDisabled {
+		err := serv.initFallbackProvider(
+			fallbackProviderFile,
+			fallbackProviderURL,
+			fallbackProviderJsonKey,
+		)
+
+		if err != nil {
+			serv.log.Error().
+				Err(err).
+				Msg("Failed to init fallback provider, disabling")
+			fallbackEmbedDisabled = true
+		}
+	}
+
+	// Start the cleanup ticker
+	serv.startCleanupTicker(
+		cacheCleanInterval,
+		cacheMaxAge,
+	)
+
+	// Load embed html template
+	serv.initTemplates(templatesDir)
+
+	// Register our handler
+	rg := r.Group("/embed")
+	rg.GET("", serv.handleWebPreview)
+
+	// Create imageproxy and provide interface to shardedfilestore
+	imgCache := NewImageProxyCache(serv.store, serv.log)
+	imgProxy = imageproxy.NewProxy(nil, imgCache)
+
+	// Attach imageproxy
+	ic := r.Group("/image-cache/*id")
+	ic.GET("", serv.handleImageCache)
+
+	return nil
+}
+
+func (serv *UploadServer) handleImageCache(c *gin.Context) {
+	r := c.Request
+	r.URL.Path = strings.Replace(r.URL.Path, "/image-cache", "", -1)
+
+	hash := getHash(r.URL.Path)
+
+	serv.log.Debug().
+		Msgf("Image request\n\turl: %s\n\thash: %s", r.URL.Path, hash)
+
+	imgWaiterMutex.Lock()
+	item, ok := imgWaiter[hash]
+	if !ok {
+		// This is the first client to request this url
+		// create a waiter item and add it to the map
+		item = &imgWaiterItem{
+			url:     r.URL.Path,
+			created: time.Now().Unix(),
+		}
+		// Other requests will wait on this waitgroup once the mutex is unlocked
+		item.wg.Add(1)
+		imgWaiter[hash] = item
+
+		// Other requests are currently waiting for this mutex
+		imgWaiterMutex.Unlock()
+
+		// Pass this request to the image proxy
+		imgProxy.ServeHTTP(c.Writer, c.Request)
+
+		// Image proxy is done, store resulting status
+		item.status = c.Writer.Status()
+
+		// Ready for other clients to access this url
+		item.wg.Done()
+	} else {
+		// Not the first client to request this url
+		// We no longer need the mutex as we will use the waitgroup
+		imgWaiterMutex.Unlock()
+		item.wg.Wait()
+
+		// Waitgroup is complete check if the first request was successful
+		if item.status == 200 {
+			// The first request was successful pass this request to the image proxy
+			imgProxy.ServeHTTP(c.Writer, c.Request)
+		} else {
+			// First request failed return its status code to the client
+			c.Status(item.status)
+		}
+	}
+}
+
+func (serv *UploadServer) handleWebPreview(c *gin.Context) {
+	queryURL := c.Query("url")
+
+	if !isValidURL(queryURL) {
+		c.AbortWithStatus(http.StatusBadRequest)
+		return
+	}
+
+	queryCenter := c.Query("center")
+	queryWidth := c.Query("width")
+	queryHeight := c.Query("height")
+
+	// Convert queryCenter to boolean
+	center, err := strconv.ParseBool(queryCenter)
+	if err != nil {
+		center = false
+	}
+
+	width, err := strconv.Atoi(queryWidth)
+	if err != nil {
+		width = 1000
+	}
+
+	height, err := strconv.Atoi(queryHeight)
+	if err != nil {
+		height = 400
+	}
+
+	hash := getHash(queryURL)
+
+	serv.log.Debug().
+		Msgf("Embed request\n\turl: %s\n\thash: %s", queryURL, hash)
+
+	cacheMutex.Lock()
+	item, ok := cache[hash]
+	if !ok {
+		// Cache miss create new cache item
+		serv.log.Debug().
+			Msgf("HTML cache miss")
+		item = &cacheItem{
+			url:     queryURL,
+			html:    "",
+			created: time.Now().Unix(),
+		}
+
+		// Add to waitgroup so other clients can wait for the embed result
+		item.wg.Add(1)
+		cache[hash] = item
+
+		// Item added to cache, unlock so other requests can see the new item
+		cacheMutex.Unlock()
+
+		// Check if the url looks like an image
+		if isImage.MatchString(queryURL) {
+			item.html = getImageHTML(c, queryURL, height)
+		}
+
+		// Attempt to fetch oEmbed data
+		embedItem := oEmbed.FindItem(queryURL)
+		if embedItem != nil {
+			options := oembed.Options{
+				URL:       queryURL,
+				MaxHeight: height,
+				MaxWidth:  width,
+			}
+			info, err := embedItem.FetchOembed(options)
+			if err != nil {
+				serv.log.Error().
+					Err(err).
+					Msg("Unexpected error in oEmbed")
+			} else if info.Status >= 300 {
+				// oEmbed returned a bad status code
+				serv.log.Debug().
+					Msgf("Bad response code from oEmbed: %d", info.Status)
+			} else if info.HTML != "" {
+				// oEmbed returned embedable html
+				serv.log.Debug().
+					Msgf("oEmbed info:\n%s", info)
+				item.html = info.HTML
+			} else if info.Type == "photo" {
+				// oEmbed returned a photo type the url should be an image
+				serv.log.Debug().
+					Msgf("oEmbed info:\n%s", info)
+				item.html = getImageHTML(c, info.URL, height)
+			}
+		}
+
+		// No embedable html, time to try our fallback provider
+		if item.html == "" && !fallbackEmbedDisabled {
+			fallbackEmbedResp, err := fallbackEmbed.Get(queryURL, width, height)
+			if err != nil {
+				serv.log.Error().
+					Err(err).
+					Msg("Unexpected error in fallback embed")
+			} else {
+				item.html = fallbackEmbedResp
+			}
+		}
+
+		// Still no html send an error to the parent
+		if item.html == "" {
+			item.html = ""
+		}
+
+		// Decrease the waitgroup so other requests can complete
+		item.wg.Done()
+	} else {
+		// Cache hit unlock the cache
+		serv.log.Debug().
+			Msg("HTML cache hit")
+		cacheMutex.Unlock()
+	}
+
+	// Wait until the cache item is fulfilled
+	item.wg.Wait()
+
+	// Prepare html and send it to the client
+	style := `
+	body {
+		display: flex; justify-content: center;
+	}`
+	if !center {
+		style = `
+		body {
+			overflow: hidden;
+		}
+		#kiwi-embed-container {
+			position: absolute;
+			top: 0;
+			left: 0;
+			bottom: 0;
+			overflow: auto;
+		}`
+	}
+	templateLock.RLock()
+	htmlData := strings.Replace(template, "{{body.html}}", item.html, -1)
+	templateLock.RUnlock()
+	htmlData = strings.Replace(htmlData, "/* style.extras */", style, -1)
+	c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(htmlData))
+}
+
+func (serv *UploadServer) getProviderFileOrURL(filename, url string) (io.Reader, error) {
+	if _, err := os.Stat(filename); err == nil {
+		serv.log.Debug().
+			Str("file", filename).
+			Msg("Fetching embed providers from")
+		file, err := os.Open(filename)
+		if err == nil {
+			return file, nil
+		}
+
+		serv.log.Error().
+			Err(err).
+			Msg("Failed to open providers json")
+	}
+
+	serv.log.Debug().
+		Str("url", url).
+		Msg("Fetching embed providers from")
+	return getEmbedProviders(url)
+}
+
+func (serv *UploadServer) startCleanupTicker(cleanInterval, cacheMaxAge time.Duration) {
+	cacheTicker = time.NewTicker(cleanInterval)
+	go func() {
+		for range cacheTicker.C {
+			serv.cleanCache(cacheMaxAge)
+		}
+	}()
+}
+
+func (serv *UploadServer) cleanCache(cacheMaxAge time.Duration) {
+	createdBefore := time.Now().Unix() - int64(cacheMaxAge.Seconds())
+
+	// Find expired items in HTML cache
+	var expired []string
+	for hash, item := range cache {
+		if item.created >= createdBefore {
+			continue
+		}
+		expired = append(expired, hash)
+	}
+
+	// Find expired items in imgWaiter
+	var expiredWaiters []string
+	for hash, item := range imgWaiter {
+		if item.created >= createdBefore {
+			continue
+		}
+		expiredWaiters = append(expiredWaiters, hash)
+	}
+
+	// Remove expired items from HTML cache
+	if len(expired) > 0 {
+		serv.log.Debug().
+			Msgf("Cleaning %d item from HTML cache", len(expired))
+
+		cacheMutex.Lock()
+		for _, hash := range expired {
+			delete(cache, hash)
+			serv.log.Debug().
+				Str("event", "expired").
+				Str("hash", hash).
+				Msg("Pruned from HTML cache")
+		}
+		cacheMutex.Unlock()
+	}
+
+	// Remove expired items from img waiter
+	if len(expiredWaiters) > 0 {
+		serv.log.Debug().
+			Msgf("Cleaning %d item from img waiter cache", len(expiredWaiters))
+
+		imgWaiterMutex.Lock()
+		for _, hash := range expiredWaiters {
+			delete(imgWaiter, hash)
+			serv.log.Debug().
+				Str("event", "expired").
+				Str("hash", hash).
+				Msg("Pruned from image waiter")
+		}
+		imgWaiterMutex.Unlock()
+	}
+}
+
+func (serv *UploadServer) initFallbackProvider(fallbackProviderFile, fallbackProviderURL, fallbackProviderJsonKey string) error {
+	if _, err := os.Stat(fallbackProviderFile); err == nil {
+		// Fallback provider file exists attempt to read and parse it
+		file, err := os.Open(fallbackProviderFile)
+		if err != nil {
+			return errors.New("Failed to open fallback providers json: " + err.Error())
+		}
+		err = fallbackEmbed.ParseProviders(bufio.NewReader(file))
+		if err != nil {
+			return errors.New("Failed to parse fallback providers json: " + err.Error())
+		}
+
+		return nil
+	}
+
+	if strings.HasPrefix(fallbackProviderURL, "https://noembed.com/") {
+		// No fallback provider file and the url is noembed.com
+		// attempt to fetch and parse the providers.json from noembed.com
+		noembedJSON, err := getEmbedProviders("https://noembed.com/providers")
+		if err != nil {
+			return errors.New("Failed to get fallback providers json: " + err.Error())
+		}
+		fallbackEmbed = fallbackembed.New(fallbackProviderURL, fallbackProviderJsonKey)
+		err = fallbackEmbed.ParseProviders(noembedJSON)
+		if err != nil {
+			return errors.New("Failed to parse fallback providers json: " + err.Error())
+		}
+
+		return nil
+	}
+
+	return errors.New("Tried to init fallback provider without provider json file")
+}
+
+func (serv *UploadServer) initTemplates(templatesDir string) {
+	templatePath := path.Join(templatesDir, "web-preview.html")
+	if _, err := os.Stat(templatePath); os.IsNotExist(err) {
+		// No template file use content from binary
+		return
+	}
+
+	// Template file exists read it from disk
+	html, err := ioutil.ReadFile(templatePath)
+	if err != nil {
+		serv.log.Error().
+			Err(err).
+			Str("path", templatePath).
+			Msg("Failed to read webpreview template")
+	}
+	templateLock.Lock()
+	template = string(html)
+	templateLock.Unlock()
+}
+
+func getEmbedProviders(url string) (*bytes.Reader, error) {
+	var httpResp *http.Response
+	httpResp, err := httpClient.Get(url)
+	if err != nil {
+		return nil, errors.New("Failed to fetch embed providers: " + err.Error())
+	}
+	defer httpResp.Body.Close()
+
+	body, err := ioutil.ReadAll(httpResp.Body)
+	if err != nil {
+		return nil, errors.New("Failed to read embed providers: " + err.Error())
+	}
+	return bytes.NewReader(body), nil
+}
+
+func getImageHTML(c *gin.Context, url string, height int) string {
+	newURL := "http://"
+	if c.Request.TLS != nil {
+		newURL = "https://"
+	}
+	newURL += c.Request.Host
+	// fixed height proportional width
+	newURL += "/image-cache/x" + strconv.Itoa(height) + "/" + url
+	return " "
+}
+
+func getHash(url string) string {
+	hasher := sha256.New()
+	hasher.Write([]byte(url))
+	return hex.EncodeToString(hasher.Sum(nil))
+}
+
+func isValidURL(str string) bool {
+	u, err := url.Parse(str)
+	return err == nil && u.Host != ""
+}
diff --git a/server/web-preview.html b/server/web-preview.html
new file mode 100644
index 0000000..2744901
--- /dev/null
+++ b/server/web-preview.html
@@ -0,0 +1,60 @@
+
+
+
"
+}
+
+func getHash(url string) string {
+	hasher := sha256.New()
+	hasher.Write([]byte(url))
+	return hex.EncodeToString(hasher.Sum(nil))
+}
+
+func isValidURL(str string) bool {
+	u, err := url.Parse(str)
+	return err == nil && u.Host != ""
+}
diff --git a/server/web-preview.html b/server/web-preview.html
new file mode 100644
index 0000000..2744901
--- /dev/null
+++ b/server/web-preview.html
@@ -0,0 +1,60 @@
+
+
+    
+        
+        
+    
+    
+        {{body.html}}
+    
+
diff --git a/shardedfilestore/shardedfilestore.go b/shardedfilestore/shardedfilestore.go
index 29ce355..1860187 100644
--- a/shardedfilestore/shardedfilestore.go
+++ b/shardedfilestore/shardedfilestore.go
@@ -200,7 +200,7 @@ func RemoveWithDirs(path string, basePath string) (err error) {
 			return err
 		}
 
-		empty, err := isDirEmpty(parent);
+		empty, err := isDirEmpty(parent)
 		if empty {
 			err = os.Remove(parent)
 		}
@@ -431,14 +431,28 @@ func (store *ShardedFileStore) FinishUpload(id string) error {
 	newPath := store.completeBinPath(hash)
 	os.MkdirAll(filepath.Dir(newPath), defaultDirectoryPerm)
 	oldPath := store.incompleteBinPath(id)
-	err = os.Rename(oldPath, newPath)
-	if err != nil {
-		store.log.Error().
-			Err(err).
-			Str("oldPath", oldPath).
-			Str("newPath", newPath).
-			Msg("Failed to rename")
+
+	if _, err := os.Stat(newPath); err != nil {
+		// file needs moving to the sharded filestore
+		err = os.Rename(oldPath, newPath)
+		if err != nil {
+			store.log.Error().
+				Err(err).
+				Str("oldPath", oldPath).
+				Str("newPath", newPath).
+				Msg("Failed to rename")
+		}
+	} else {
+		// file already exists just remove the tempoary upload
+		err = os.Remove(oldPath)
+		if err != nil {
+			store.log.Error().
+				Err(err).
+				Str("oldPath", oldPath).
+				Msg("Failed to remove")
+		}
 	}
+
 	return err
 }
 
@@ -465,8 +479,8 @@ func isDirEmpty(path string) (bool, error) {
 	defer f.Close()
 
 	_, err = f.Readdirnames(1)
-    if err == io.EOF {
-        return true, nil
-    }
-    return false, err
+	if err == io.EOF {
+		return true, nil
+	}
+	return false, err
 }