1
Fork 0

Add download option to sidemenu for photos

This commit is contained in:
viktorstrate 2019-08-21 16:17:45 +02:00
parent a5513e3343
commit 108f6c2d0c
8 changed files with 196 additions and 66 deletions

View File

@ -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)

View File

@ -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,
}

View File

@ -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

View File

@ -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

View File

@ -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) {

View File

@ -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 {

View File

@ -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>
)

View File

@ -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