diff --git a/api/graphql/generated.go b/api/graphql/generated.go
index 2f55bff..c6aafd1 100644
--- a/api/graphql/generated.go
+++ b/api/graphql/generated.go
@@ -106,8 +106,10 @@ type ComplexityRoot struct {
 	}
 
 	PhotoDownload struct {
-		Title func(childComplexity int) int
-		URL   func(childComplexity int) int
+		Height func(childComplexity int) int
+		Title  func(childComplexity int) int
+		URL    func(childComplexity int) int
+		Width  func(childComplexity int) int
 	}
 
 	PhotoExif struct {
@@ -571,6 +573,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.Photo.Title(childComplexity), true
 
+	case "PhotoDownload.height":
+		if e.complexity.PhotoDownload.Height == nil {
+			break
+		}
+
+		return e.complexity.PhotoDownload.Height(childComplexity), true
+
 	case "PhotoDownload.title":
 		if e.complexity.PhotoDownload.Title == nil {
 			break
@@ -585,6 +594,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.PhotoDownload.URL(childComplexity), true
 
+	case "PhotoDownload.width":
+		if e.complexity.PhotoDownload.Width == nil {
+			break
+		}
+
+		return e.complexity.PhotoDownload.Width(childComplexity), true
+
 	case "PhotoEXIF.aperture":
 		if e.complexity.PhotoExif.Aperture == nil {
 			break
@@ -1151,6 +1167,8 @@ type PhotoURL {
 
 type PhotoDownload {
   title: String!
+  width: Int!
+  height: Int!
   url: String!
 }
 
@@ -3252,6 +3270,80 @@ func (ec *executionContext) _PhotoDownload_title(ctx context.Context, field grap
 	return ec.marshalNString2string(ctx, field.Selections, res)
 }
 
+func (ec *executionContext) _PhotoDownload_width(ctx context.Context, field graphql.CollectedField, obj *models.PhotoDownload) (ret graphql.Marshaler) {
+	ctx = ec.Tracer.StartFieldExecution(ctx, field)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+		ec.Tracer.EndFieldExecution(ctx)
+	}()
+	rctx := &graphql.ResolverContext{
+		Object:   "PhotoDownload",
+		Field:    field,
+		Args:     nil,
+		IsMethod: false,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Width, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !ec.HasError(rctx) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(int)
+	rctx.Result = res
+	ctx = ec.Tracer.StartFieldChildExecution(ctx)
+	return ec.marshalNInt2int(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) _PhotoDownload_height(ctx context.Context, field graphql.CollectedField, obj *models.PhotoDownload) (ret graphql.Marshaler) {
+	ctx = ec.Tracer.StartFieldExecution(ctx, field)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+		ec.Tracer.EndFieldExecution(ctx)
+	}()
+	rctx := &graphql.ResolverContext{
+		Object:   "PhotoDownload",
+		Field:    field,
+		Args:     nil,
+		IsMethod: false,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Height, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !ec.HasError(rctx) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(int)
+	rctx.Result = res
+	ctx = ec.Tracer.StartFieldChildExecution(ctx)
+	return ec.marshalNInt2int(ctx, field.Selections, res)
+}
+
 func (ec *executionContext) _PhotoDownload_url(ctx context.Context, field graphql.CollectedField, obj *models.PhotoDownload) (ret graphql.Marshaler) {
 	ctx = ec.Tracer.StartFieldExecution(ctx, field)
 	defer func() {
@@ -6402,6 +6494,16 @@ func (ec *executionContext) _PhotoDownload(ctx context.Context, sel ast.Selectio
 			if out.Values[i] == graphql.Null {
 				invalids++
 			}
+		case "width":
+			out.Values[i] = ec._PhotoDownload_width(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				invalids++
+			}
+		case "height":
+			out.Values[i] = ec._PhotoDownload_height(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				invalids++
+			}
 		case "url":
 			out.Values[i] = ec._PhotoDownload_url(ctx, field, obj)
 			if out.Values[i] == graphql.Null {
diff --git a/api/graphql/models/generated.go b/api/graphql/models/generated.go
index 0b68552..2f54781 100644
--- a/api/graphql/models/generated.go
+++ b/api/graphql/models/generated.go
@@ -33,8 +33,10 @@ type Notification struct {
 }
 
 type PhotoDownload struct {
-	Title string `json:"title"`
-	URL   string `json:"url"`
+	Title  string `json:"title"`
+	Width  int    `json:"width"`
+	Height int    `json:"height"`
+	URL    string `json:"url"`
 }
 
 // EXIF metadata from the camera
diff --git a/api/graphql/models/photo.go b/api/graphql/models/photo.go
index e8c1bde..b502552 100644
--- a/api/graphql/models/photo.go
+++ b/api/graphql/models/photo.go
@@ -79,3 +79,17 @@ func NewPhotoURLFromRow(row *sql.Row) (*PhotoURL, error) {
 
 	return &url, nil
 }
+
+func NewPhotoURLFromRows(rows *sql.Rows) ([]*PhotoURL, error) {
+	urls := make([]*PhotoURL, 0)
+
+	for rows.Next() {
+		var url PhotoURL
+		if err := rows.Scan(&url.UrlID, &url.PhotoId, &url.PhotoName, &url.Width, &url.Height, &url.Purpose, &url.ContentType); err != nil {
+			return nil, err
+		}
+		urls = append(urls, &url)
+	}
+
+	return urls, nil
+}
diff --git a/api/graphql/resolvers/photo.go b/api/graphql/resolvers/photo.go
index 4444167..3123870 100644
--- a/api/graphql/resolvers/photo.go
+++ b/api/graphql/resolvers/photo.go
@@ -69,8 +69,39 @@ func (r *photoResolver) Shares(ctx context.Context, obj *models.Photo) ([]*model
 }
 
 func (r *photoResolver) Downloads(ctx context.Context, obj *models.Photo) ([]*models.PhotoDownload, error) {
-	log.Println("Photo: downloads not implemented")
+
+	rows, err := r.Database.Query("SELECT * FROM photo_url WHERE photo_id = ?", obj.PhotoID)
+	if err != nil {
+		return nil, err
+	}
+
+	photoUrls, err := models.NewPhotoURLFromRows(rows)
+	if err != nil {
+		return nil, err
+	}
+
 	downloads := make([]*models.PhotoDownload, 0)
+
+	for _, url := range photoUrls {
+
+		var title string
+		switch {
+		case url.Purpose == models.PhotoOriginal:
+			title = "Original"
+		case url.Purpose == models.PhotoThumbnail:
+			title = "Small"
+		case url.Purpose == models.PhotoHighRes:
+			title = "Large"
+		}
+
+		downloads = append(downloads, &models.PhotoDownload{
+			Title:  title,
+			Width:  url.Width,
+			Height: url.Height,
+			URL:    url.URL(),
+		})
+	}
+
 	return downloads, nil
 }
 
diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql
index 92d9804..81b6e83 100644
--- a/api/graphql/schema.graphql
+++ b/api/graphql/schema.graphql
@@ -177,6 +177,8 @@ type PhotoURL {
 
 type PhotoDownload {
   title: String!
+  width: Int!
+  height: Int!
   url: String!
 }
 
diff --git a/api/server/cors_middleware.go b/api/server/cors_middleware.go
index 39fa9c4..e131b0d 100644
--- a/api/server/cors_middleware.go
+++ b/api/server/cors_middleware.go
@@ -14,11 +14,13 @@ import (
 func CORSMiddleware(devMode bool) mux.MiddlewareFunc {
 	return func(next http.Handler) http.Handler {
 		return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+
 			methods := []string{http.MethodGet, http.MethodPost, http.MethodOptions}
-			headers := []string{"authorization", "content-type"}
+			headers := []string{"authorization", "content-type", "content-length"}
 
 			w.Header().Set("Access-Control-Allow-Methods", strings.Join(methods, ","))
 			w.Header().Set("Access-Control-Allow-Headers", strings.Join(headers, ","))
+			w.Header().Set("Access-Control-Expose-Headers", "content-length")
 
 			endpoint, err := url.Parse(os.Getenv("API_ENDPOINT"))
 			if err != nil {
diff --git a/ui/src/Pages/SharePage/SharePage.js b/ui/src/Pages/SharePage/SharePage.js
index 5f60058..1b4c228 100644
--- a/ui/src/Pages/SharePage/SharePage.js
+++ b/ui/src/Pages/SharePage/SharePage.js
@@ -46,6 +46,12 @@ const tokenQuery = gql`
       width
       height
     }
+    downloads {
+      title
+      url
+      width
+      height
+    }
     highRes {
       url
     }
diff --git a/ui/src/apolloClient.js b/ui/src/apolloClient.js
index ccbc25c..d8b73c6 100644
--- a/ui/src/apolloClient.js
+++ b/ui/src/apolloClient.js
@@ -59,7 +59,7 @@ const linkError = onError(({ graphQLErrors, networkError }) => {
     if (graphQLErrors.length == 1) {
       errorMessages.push({
         header: 'Something went wrong',
-        content: graphQLErrors[0].message,
+        content: `Server error: ${graphQLErrors[0].message} at (${graphQLErrors[0].path})`,
       })
     } else if (graphQLErrors.length > 1) {
       errorMessages.push({
diff --git a/ui/src/components/messages/Messages.js b/ui/src/components/messages/Messages.js
index 62a73bd..38a8be8 100644
--- a/ui/src/components/messages/Messages.js
+++ b/ui/src/components/messages/Messages.js
@@ -17,17 +17,22 @@ export let MessageState = {
   set: null,
   get: null,
   add: message => {
-    MessageState.set(messages => [...messages, message])
+    MessageState.set(messages => {
+      const newMessages = messages.filter(msg => msg.key != message.key)
+      newMessages.push(message)
+
+      return newMessages
+    })
+  },
+  removeKey: key => {
+    MessageState.set(messages => {
+      const newMessages = messages.filter(msg => msg.key != key)
+      return newMessages
+    })
   },
 }
 
 const Messages = () => {
-  if (!localStorage.getItem('token')) {
-    return null
-  }
-
-  console.log('Rendering messages')
-
   const [messages, setMessages] = useState([])
   MessageState.set = setMessages
   MessageState.get = messages
@@ -76,7 +81,6 @@ const Messages = () => {
       resolveFunc = resolve
     })
 
-    console.log(resolveFunc, waitPromise)
     refHooks.set(message.key, {
       done: resolveFunc,
       promise: waitPromise,
@@ -89,13 +93,8 @@ const Messages = () => {
       height: '0px',
     },
     enter: item => async next => {
-      console.log('HERE', refMap, item)
-
       const refPromise = refHooks.get(item.key).promise
-      console.log('promise', refPromise)
-
       await refPromise
-      console.log('AFTER PROMISE', refMap, item)
 
       await next({
         opacity: 1,
@@ -109,7 +108,6 @@ const Messages = () => {
     <Container>
       {transitions.map(({ item, props: style, key }) => {
         const getRef = ref => {
-          console.log('GET REF', refMap, refHooks, item.key)
           refMap.set(item, ref)
           if (refHooks.has(item.key)) {
             refHooks.get(item.key).done()
diff --git a/ui/src/components/messages/SubscriptionsHook.js b/ui/src/components/messages/SubscriptionsHook.js
index 7292372..beffa8f 100644
--- a/ui/src/components/messages/SubscriptionsHook.js
+++ b/ui/src/components/messages/SubscriptionsHook.js
@@ -18,6 +18,10 @@ const notificationSubscription = gql`
 `
 
 const SubscriptionsHook = ({ messages, setMessages }) => {
+  if (!localStorage.getItem('token')) {
+    return null
+  }
+
   const { data, error } = useSubscription(notificationSubscription)
 
   useEffect(() => {
@@ -62,34 +66,6 @@ const SubscriptionsHook = ({ messages, setMessages }) => {
       newMessages.push(newNotification)
     }
 
-    // const update = data.scannerStatusUpdate
-
-    // if (update.success) {
-    //   newMessages[0] = {
-    //     key: 'primary',
-    //     type: 'progress',
-    //     props: {
-    //       header: update.finished ? 'Synced' : 'Syncing',
-    //       content: update.message,
-    //       percent: update.progress,
-    //       positive: update.finished,
-    //     },
-    //   }
-
-    //   if (!update.finished) newMessages[0].props.onDismiss = null
-    // } else {
-    //   const key = Math.random().toString(26)
-    //   newMessages.push({
-    //     key,
-    //     type: 'message',
-    //     props: {
-    //       header: 'Sync error',
-    //       content: update.message,
-    //       negative: true,
-    //     },
-    //   })
-    // }
-
     setMessages(newMessages)
   }, [data, error])
 
diff --git a/ui/src/components/photoGallery/ProtectedImage.js b/ui/src/components/photoGallery/ProtectedImage.js
index dcb089e..2111a8e 100644
--- a/ui/src/components/photoGallery/ProtectedImage.js
+++ b/ui/src/components/photoGallery/ProtectedImage.js
@@ -48,7 +48,6 @@ const ProtectedImage = ({ src, ...props }) => {
       // Get share token if not authorized
 
       const token = location.pathname.match(/^\/share\/([\d\w]+)(\/?.*)$/)
-      console.log('share token', location.pathname, token)
       if (token) {
         imgUrl.searchParams.set('token', token[1])
       }
diff --git a/ui/src/components/sidebar/PhotoSidebar.js b/ui/src/components/sidebar/PhotoSidebar.js
index 614accf..394d4c1 100644
--- a/ui/src/components/sidebar/PhotoSidebar.js
+++ b/ui/src/components/sidebar/PhotoSidebar.js
@@ -93,7 +93,7 @@ const SidebarContent = ({ photo, hidePreview }) => {
       {!hidePreview && <PreviewImage src={previewUrl} />}
       <Name>{photo && photo.title}</Name>
       <div>{exifItems}</div>
-      <SidebarDownload photoId={photo.id} />
+      <SidebarDownload photo={photo} />
       <SidebarShare photo={photo} />
     </div>
   )
diff --git a/ui/src/components/sidebar/SidebarDownload.js b/ui/src/components/sidebar/SidebarDownload.js
index 868796e..1574e76 100644
--- a/ui/src/components/sidebar/SidebarDownload.js
+++ b/ui/src/components/sidebar/SidebarDownload.js
@@ -1,7 +1,8 @@
 import React from 'react'
 import PropTypes from 'prop-types'
 import { Menu, Dropdown, Button } from 'semantic-ui-react'
-import { Query } from 'react-apollo'
+import { MessageState } from '../messages/Messages'
+import { Query, useLazyQuery } from 'react-apollo'
 import gql from 'graphql-tag'
 import download from 'downloadjs'
 
@@ -12,49 +13,157 @@ const downloadQuery = gql`
       downloads {
         title
         url
+        width
+        height
       }
     }
   }
 `
 
+function formatBytes(bytes) {
+  var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
+  if (bytes == 0) return '0 Byte'
+  var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)))
+  return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i]
+}
+
 const downloadPhoto = async url => {
-  const request = await fetch(url, {
-    headers: {
-      Authorization: `Bearer ${localStorage.getItem('token')}`,
+  const imgUrl = new URL(url)
+  let headers = {
+    Authorization: `Bearer ${localStorage.getItem('token')}`,
+  }
+
+  if (localStorage.getItem('token') == null) {
+    // Get share token if not authorized
+    const token = location.pathname.match(/^\/share\/([\d\w]+)(\/?.*)$/)
+    if (token) {
+      imgUrl.searchParams.set('token', token[1])
+    }
+
+    headers = {}
+  }
+
+  const response = await fetch(imgUrl.href, {
+    headers,
+  })
+
+  const totalBytes = Number(response.headers.get('content-length'))
+  console.log(totalBytes)
+
+  if (totalBytes == 0) {
+    MessageState.add({
+      key: Math.random().toString(26),
+      type: 'message',
+      props: {
+        header: 'Error downloading photo',
+        content: `Could not get size of photo from server`,
+        negative: true,
+      },
+    })
+    return
+  }
+
+  const notifKey = Math.random().toString(26)
+  MessageState.add({
+    key: notifKey,
+    type: 'progress',
+    props: {
+      header: 'Downloading photo',
+      content: `Starting download`,
+      progress: 0,
     },
   })
 
-  const content = await request.blob()
+  const reader = response.body.getReader()
+  let data = new Uint8Array(totalBytes)
+
+  let receivedBytes = 0
+  let result
+  do {
+    result = await reader.read()
+
+    if (result.value) data.set(result.value, receivedBytes)
+
+    receivedBytes += result.value ? result.value.length : 0
+
+    MessageState.add({
+      key: notifKey,
+      type: 'progress',
+      props: {
+        header: 'Downloading photo',
+        percent: (receivedBytes / totalBytes) * 100,
+        content: `${formatBytes(receivedBytes)} of ${formatBytes(
+          totalBytes
+        )} bytes downloaded`,
+      },
+    })
+  } while (!result.done)
+
+  MessageState.add({
+    key: notifKey,
+    type: 'progress',
+    props: {
+      header: 'Downloading photo completed',
+      content: `The photo has been downloaded`,
+      percent: 100,
+      positive: true,
+    },
+  })
+
+  setTimeout(() => {
+    MessageState.removeKey(notifKey)
+  }, 2000)
+
+  const content = new Blob([data.buffer], {
+    type: response.headers.get('content-type'),
+  })
   const filename = url.match(/[^/]*$/)[0]
 
   download(content, filename)
 }
 
-const SidebarDownload = ({ photoId }) => {
-  if (!photoId) return null
+const SidebarDownload = ({ photo }) => {
+  if (!photo || !photo.id) return null
+
+  const [
+    loadPhotoDownloads,
+    { called, loading, data },
+  ] = useLazyQuery(downloadQuery, { variables: { photoId: photo.id } })
+
+  let downloads = []
+
+  if (called) {
+    if (!loading) {
+      downloads = data && data.photo.downloads
+    }
+  } else {
+    if (!photo.downloads) {
+      loadPhotoDownloads()
+    } else {
+      downloads = photo.downloads
+    }
+  }
+
+  let buttons = downloads.map(x => (
+    <Button
+      style={{ marginTop: 4 }}
+      key={x.url}
+      onClick={() => downloadPhoto(x.url)}
+    >
+      {`${x.title} (${x.width} x ${x.height})`}
+    </Button>
+  ))
 
   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>{buttons}</div>
     </div>
   )
 }
 
 SidebarDownload.propTypes = {
-  photoId: PropTypes.number,
+  photo: PropTypes.object,
 }
 
 export default SidebarDownload
