diff --git a/api/src/index.js b/api/src/index.js
index 6fe506ae..e2627658 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 327232fc..381072b5 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 00000000..e18f16be
--- /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 ca7a322d..49896998 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 63648465..78357f6d 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 fb8fb70b..27bd9327 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 2e3b2456..6ecb9d11 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 &&