Add download option to sidemenu for photos
This commit is contained in:
parent
a5513e3343
commit
108f6c2d0c
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue