diff --git a/api/src/index.js b/api/src/index.js
index 6fe506a..e262765 100644
--- a/api/src/index.js
+++ b/api/src/index.js
@@ -29,6 +29,12 @@ const driver = neo4j.driver(
 
 const scanner = new PhotoScanner(driver)
 
+app.use((req, res, next) => {
+  req.driver = driver
+  req.scanner = scanner
+  next()
+})
+
 // Every 4th hour
 setInterval(scanner.scanAll, 1000 * 60 * 60 * 4)
 
@@ -85,8 +91,10 @@ const server = new ApolloServer({
 server.applyMiddleware({ app, path: graphPath })
 
 import loadImageRoutes from './routes/images'
+import loadDownloadRoutes from './routes/downloads'
 
-loadImageRoutes({ app, driver, scanner })
+loadImageRoutes(app)
+loadDownloadRoutes(app)
 
 const httpServer = http.createServer(app)
 server.installSubscriptionHandlers(httpServer)
diff --git a/api/src/resolvers/photos.js b/api/src/resolvers/photos.js
index 327232f..381072b 100644
--- a/api/src/resolvers/photos.js
+++ b/api/src/resolvers/photos.js
@@ -124,7 +124,7 @@ const Query = {
   },
 }
 
-const PhotoURL = {
+const urlResolve = {
   url(root, args, ctx, info) {
     let url = new URL(root.url, config.host)
     if (ctx.shareToken) url.search = `?token=${ctx.shareToken}`
@@ -134,5 +134,6 @@ const PhotoURL = {
 
 export default {
   Query,
-  PhotoURL,
+  PhotoURL: urlResolve,
+  PhotoDownload: urlResolve,
 }
diff --git a/api/src/routes/downloads.js b/api/src/routes/downloads.js
new file mode 100644
index 0000000..e18f16b
--- /dev/null
+++ b/api/src/routes/downloads.js
@@ -0,0 +1,26 @@
+import { getImageFromRequest, RequestError } from './images'
+import fs from 'fs-extra'
+import path from 'path'
+
+async function sendDownload(req, res) {
+  let { photo, cachePath } = req
+  const cacheBasename = path.basename(cachePath)
+  const photoBasename = path.basename(photo.path)
+
+  if (cacheBasename == photoBasename) {
+    if (!(await fs.exists(photo.path))) {
+      throw new RequestError(500, 'Image missing from the server')
+    }
+
+    return res.sendFile(photo.path)
+  }
+
+  throw new RequestError(404, 'Image could not be found')
+}
+
+const loadDownloadRoutes = app => {
+  app.use('/download/:id/:image', getImageFromRequest)
+  app.use('/download/:id/:image', sendDownload)
+}
+
+export default loadDownloadRoutes
diff --git a/api/src/routes/images.js b/api/src/routes/images.js
index ca7a322..4989699 100644
--- a/api/src/routes/images.js
+++ b/api/src/routes/images.js
@@ -5,49 +5,90 @@ import config from '../config'
 import { isRawImage, getImageCachePath } from '../scanner/utils'
 import { getUserFromToken, getTokenFromBearer } from '../token'
 
-class RequestError extends Error {
+export class RequestError extends Error {
   constructor(httpCode, message) {
     super(message)
     this.httpCode = httpCode
   }
 }
 
-async function sendImage({ photo, res, id, albumId, image, scanner }) {
-  let imagePath = path.resolve(getImageCachePath(id, albumId), image)
+export async function getImageFromRequest(req, res, next) {
+  const { id, image } = req.params
+  const driver = req.driver
 
-  if (!(await fs.exists(imagePath))) {
-    if (image == 'thumbnail.jpg') {
+  const shareToken = req.query.token
+
+  let photo, albumId
+
+  try {
+    let verify = null
+
+    if (shareToken) {
+      verify = await verifyShareToken({ shareToken, id, driver })
+    }
+
+    if (!verify) {
+      verify = await verifyUser(req, id)
+    }
+
+    if (verify == null) throw RequestError(500, 'Unable to verify request')
+
+    photo = verify.photo
+    albumId = verify.albumId
+  } catch (error) {
+    return res.status(error.status || 500).send(error.message)
+  }
+
+  let cachePath = path.resolve(getImageCachePath(id, albumId), image)
+  req.cachePath = cachePath
+  req.photo = photo
+
+  next()
+}
+
+async function sendImage(req, res) {
+  let { photo, cachePath } = req
+  const photoBasename = path.basename(cachePath)
+
+  if (!(await fs.exists(cachePath))) {
+    if (photoBasename == 'thumbnail.jpg') {
       console.log('Thumbnail not found, generating', photo.path)
-      await scanner.processImage(photo.id)
+      await req.scanner.processImage(photo.id)
 
-      if (!(await fs.exists(imagePath))) {
+      if (!(await fs.exists(cachePath))) {
         throw new Error('Thumbnail not found after image processing')
       }
 
-      return res.sendFile(imagePath)
+      return res.sendFile(cachePath)
     }
 
-    imagePath = photo.path
+    cachePath = photo.path
   }
 
-  if (await isRawImage(imagePath)) {
-    console.log('RAW preview image not found, generating', imagePath)
-    await scanner.processImage(id)
+  if (await isRawImage(cachePath)) {
+    console.log('RAW preview image not found, generating', cachePath)
+    await req.scanner.processImage(photo.id)
 
-    imagePath = path.resolve(config.cachePath, 'images', id, image)
+    cachePath = path.resolve(
+      config.cachePath,
+      'images',
+      photo.id,
+      photoBasename
+    )
 
-    if (!(await fs.exists(imagePath))) {
+    if (!(await fs.exists(cachePath))) {
       throw new Error('RAW preview not found after image processing')
     }
 
-    return res.sendFile(imagePath)
+    return res.sendFile(cachePath)
   }
 
-  res.sendFile(imagePath)
+  res.sendFile(cachePath)
 }
 
-async function verifyUser({ req, driver, id }) {
+async function verifyUser(req, id) {
   let user = null
+  const { driver } = req
 
   try {
     const token = getTokenFromBearer(req.headers.authorization)
@@ -136,39 +177,9 @@ async function verifyShareToken({ shareToken, id, driver }) {
   }
 }
 
-function loadImageRoutes({ app, driver, scanner }) {
-  app.use('/images/:id/:image', async (req, res) => {
-    const { id, image } = req.params
-
-    const shareToken = req.query.token
-
-    let photo, albumId
-
-    try {
-      let verify = null
-
-      if (shareToken) {
-        verify = await verifyShareToken({ shareToken, id, driver })
-      }
-
-      if (!verify) {
-        verify = await verifyUser({
-          req,
-          driver,
-          id,
-        })
-      }
-
-      if (verify == null) throw RequestError(500, 'Unable to verify request')
-
-      photo = verify.photo
-      albumId = verify.albumId
-    } catch (error) {
-      return res.status(error.status || 500).send(error.message)
-    }
-
-    sendImage({ photo, res, id, albumId, image, scanner })
-  })
+function loadImageRoutes(app) {
+  app.use('/images/:id/:image', getImageFromRequest)
+  app.use('/images/:id/:image', sendImage)
 }
 
 export default loadImageRoutes
diff --git a/api/src/scanner/processImage.js b/api/src/scanner/processImage.js
index 6364846..78357f6 100644
--- a/api/src/scanner/processImage.js
+++ b/api/src/scanner/processImage.js
@@ -157,19 +157,35 @@ export default async function processImage({ driver, markFinishedImage }, id) {
 
     await session.run(
       `MATCH (p:Photo { id: {id} })
-    CREATE (thumbnail:PhotoURL { url: {thumbnailUrl}, width: {thumbnailWidth}, height: {thumbnailHeight} })
-    CREATE (original:PhotoURL { url: {originalUrl}, width: {originalWidth}, height: {originalHeight} })
-    CREATE (p)-[:THUMBNAIL_URL]->(thumbnail)
-    CREATE (p)-[:ORIGINAL_URL]->(original)
+    CREATE (p)-[:THUMBNAIL_URL]->(thumbnail:PhotoURL { thumbnail })
+    CREATE (p)-[:ORIGINAL_URL]->(original:PhotoURL { original })
     `,
       {
         id,
-        thumbnailUrl: `/images/${id}/${path.basename(thumbnailPath)}`,
-        thumbnailWidth,
-        thumbnailHeight,
-        originalUrl: `/images/${id}/${path.basename(originalPath)}`,
-        originalWidth,
-        originalHeight,
+        thumbnail: {
+          url: `/images/${id}/${path.basename(thumbnailPath)}`,
+          width: thumbnailWidth,
+          height: thumbnailHeight,
+        },
+        original: {
+          url: `/images/${id}/${path.basename(originalPath)}`,
+          width: originalWidth,
+          height: originalHeight,
+        },
+      }
+    )
+
+    await session.run(
+      `
+      MATCH (p:Photo { id: {id} })
+      CREATE (p)-[:DOWNLOAD]->(original:PhotoDownload {original})
+    `,
+      {
+        id,
+        original: {
+          title: 'Original',
+          url: `/download/${id}/${path.basename(photo.path)}`,
+        },
       }
     )
   } catch (e) {
diff --git a/api/src/schema.graphql b/api/src/schema.graphql
index fb8fb70..27bd932 100644
--- a/api/src/schema.graphql
+++ b/api/src/schema.graphql
@@ -26,7 +26,7 @@ type Album {
 }
 
 type PhotoURL {
-  # URL for the image
+  # URL for previewing the image
   url: String
   # Width of the image in pixels
   width: Int
@@ -34,6 +34,11 @@ type PhotoURL {
   height: Int
 }
 
+type PhotoDownload {
+  title: String
+  url: String
+}
+
 type PhotoEXIF {
   photo: Photo @relation(name: "EXIF", direction: "IN")
   camera: String
@@ -62,6 +67,7 @@ type Photo {
   exif: PhotoEXIF @relation(name: "EXIF", direction: "OUT")
 
   shares: [ShareToken] @relation(name: "SHARES", direction: "IN")
+  downloads: [PhotoDownload] @relation(name: "DOWNLOAD", direction: "OUT")
 }
 
 type ShareToken {
diff --git a/ui/src/components/sidebar/PhotoSidebar.js b/ui/src/components/sidebar/PhotoSidebar.js
index 2e3b245..6ecb9d1 100644
--- a/ui/src/components/sidebar/PhotoSidebar.js
+++ b/ui/src/components/sidebar/PhotoSidebar.js
@@ -4,10 +4,9 @@ import styled from 'styled-components'
 import { Query } from 'react-apollo'
 import gql from 'graphql-tag'
 import SidebarItem from './SidebarItem'
-import { Loader } from 'semantic-ui-react'
 import ProtectedImage from '../photoGallery/ProtectedImage'
 import SidebarShare from './Sharing'
-import { SidebarConsumer } from './Sidebar'
+import SidebarDownload from './SidebarDownload'
 
 const photoQuery = gql`
   query sidebarPhoto($id: ID!) {
@@ -96,6 +95,7 @@ const SidebarContent = ({ photo, hidePreview }) => {
       {!hidePreview && <PreviewImage src={previewUrl} />}
       <Name>{photo && photo.title}</Name>
       <div>{exifItems}</div>
+      <SidebarDownload photoId={photo.id} />
       <SidebarShare photo={photo} />
     </div>
   )
diff --git a/ui/src/components/sidebar/SidebarDownload.js b/ui/src/components/sidebar/SidebarDownload.js
new file mode 100644
index 0000000..6d9e780
--- /dev/null
+++ b/ui/src/components/sidebar/SidebarDownload.js
@@ -0,0 +1,62 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { Menu, Dropdown, Button } from 'semantic-ui-react'
+import { Query } from 'react-apollo'
+import gql from 'graphql-tag'
+
+const downloadQuery = gql`
+  query sidebarDownloadQuery($photoId: ID!) {
+    photo(id: $photoId) {
+      id
+      downloads {
+        title
+        url
+      }
+    }
+  }
+`
+
+const downloadPhoto = async url => {
+  const request = await fetch(url, {
+    headers: {
+      Authorization: `Bearer ${localStorage.getItem('token')}`,
+    },
+  })
+
+  const content = await request.blob()
+  const contentUrl = URL.createObjectURL(content)
+
+  var downloadAnchor = document.createElement('a', contentUrl)
+  downloadAnchor.setAttribute('href', contentUrl)
+  downloadAnchor.setAttribute('download', url.match(/[^/]*$/)[0])
+  downloadAnchor.click()
+}
+
+const SidebarDownload = ({ photoId }) => {
+  if (!photoId) return null
+
+  return (
+    <div style={{ marginBottom: 24 }}>
+      <h2>Download</h2>
+      <Query query={downloadQuery} variables={{ photoId }}>
+        {({ loading, error, data }) => {
+          if (error) return <div>Error {error.message}</div>
+          if (!data || !data.photo) return null
+
+          let buttons = data.photo.downloads.map(x => (
+            <Button key={x.url} onClick={() => downloadPhoto(x.url)}>
+              {x.title}
+            </Button>
+          ))
+          return <Button.Group>{buttons}</Button.Group>
+        }}
+      </Query>
+    </div>
+  )
+}
+
+SidebarDownload.propTypes = {
+  photoId: PropTypes.string,
+}
+
+export default SidebarDownload
