mirror of
https://git.vectorsigma.ru/public/photoview.git
synced 2026-08-03 20:59:03 +00:00
Move photourl to database
This commit is contained in:
1
api/.env
1
api/.env
@@ -3,5 +3,6 @@ NEO4J_USER=neo4j
|
||||
NEO4J_PASSWORD=letmein
|
||||
GRAPHQL_LISTEN_PORT=4001
|
||||
GRAPHQL_URI=http://localhost:4001/graphql
|
||||
HOST=http://localhost:4001
|
||||
|
||||
JWT_SECRET=topSecretEpicJWTKEYThing
|
||||
@@ -7,8 +7,11 @@ import { exiftool } from 'exiftool-vendored'
|
||||
import sharp from 'sharp'
|
||||
import readChunk from 'read-chunk'
|
||||
import imageType from 'image-type'
|
||||
import { promisify } from 'util'
|
||||
import config from './config'
|
||||
|
||||
const imageSize = promisify(require('image-size'))
|
||||
|
||||
export const EVENT_SCANNER_PROGRESS = 'SCANNER_PROGRESS'
|
||||
|
||||
const isImage = async path => {
|
||||
@@ -125,10 +128,11 @@ class PhotoScanner {
|
||||
|
||||
let foundAlbumIds = []
|
||||
|
||||
async function scanPath(path) {
|
||||
async function scanPath(path, parentAlbum) {
|
||||
const list = fs.readdirSync(path)
|
||||
|
||||
let foundImage = false
|
||||
let newAlbums = []
|
||||
|
||||
for (const item of list) {
|
||||
const itemPath = pathResolve(path, item)
|
||||
@@ -136,34 +140,39 @@ class PhotoScanner {
|
||||
const stat = fs.statSync(itemPath)
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
// console.log(`Entering directory ${itemPath}`)
|
||||
const imagesInDirectory = await scanPath(itemPath)
|
||||
const session = driver.session()
|
||||
let nextParentAlbum = null
|
||||
|
||||
const findAlbumResult = await session.run(
|
||||
'MATCH (a:Album { path: {path} }) RETURN a',
|
||||
{
|
||||
path: itemPath,
|
||||
}
|
||||
)
|
||||
|
||||
session.close()
|
||||
|
||||
if (findAlbumResult.records.length != 0) {
|
||||
const album = findAlbumResult.records[0].toObject().a.properties
|
||||
console.log('Found existing album', album.title)
|
||||
|
||||
foundAlbumIds.push(album.id)
|
||||
|
||||
nextParentAlbum = album.id
|
||||
scanAlbum(album)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
const {
|
||||
foundImage: imagesInDirectory,
|
||||
newAlbums: childAlbums,
|
||||
} = await scanPath(itemPath, nextParentAlbum)
|
||||
|
||||
if (imagesInDirectory) {
|
||||
console.log(`Found album at ${itemPath}`)
|
||||
console.log(`Found new album at ${itemPath}`)
|
||||
const session = driver.session()
|
||||
|
||||
const findAlbumResult = await session.run(
|
||||
'MATCH (a:Album { path: {path} }) RETURN a',
|
||||
{
|
||||
path: itemPath,
|
||||
}
|
||||
)
|
||||
|
||||
console.log('FIND ALBUM RESULT', findAlbumResult.records)
|
||||
|
||||
if (findAlbumResult.records.length != 0) {
|
||||
console.log('Album already exists')
|
||||
|
||||
const album = findAlbumResult.records[0].toObject().a.properties
|
||||
|
||||
foundAlbumIds.push(album.id)
|
||||
|
||||
scanAlbum(album)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
console.log('Adding album')
|
||||
const albumId = uuid()
|
||||
const albumResult = await session.run(
|
||||
@@ -179,7 +188,36 @@ class PhotoScanner {
|
||||
}
|
||||
)
|
||||
|
||||
foundAlbumIds.push(albumId)
|
||||
newAlbums.push(albumId)
|
||||
const album = albumResult.records[0].toObject().a.properties
|
||||
|
||||
if (parentAlbum) {
|
||||
console.log('Linking parent album for', album.title)
|
||||
await session.run(
|
||||
`MATCH (parent:Album { id: {parentId} })
|
||||
MATCH (child:Album { id: {childId} })
|
||||
CREATE (parent)-[:SUBALBUM]->(child)`,
|
||||
{
|
||||
childId: albumId,
|
||||
parentId: parentAlbum,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
console.log(`Linking ${childAlbums.length} child albums`)
|
||||
for (let childAlbum of childAlbums) {
|
||||
await session.run(
|
||||
`MATCH (parent:Album { id: {parentId} })
|
||||
MATCH (child:Album { id: {childId} })
|
||||
CREATE (parent)-[:SUBALBUM]->(child)`,
|
||||
{
|
||||
parentId: albumId,
|
||||
childId: childAlbum,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
scanAlbum(album)
|
||||
|
||||
session.close()
|
||||
@@ -193,7 +231,7 @@ class PhotoScanner {
|
||||
}
|
||||
}
|
||||
|
||||
return foundImage
|
||||
return { foundImage, newAlbums }
|
||||
}
|
||||
|
||||
await scanPath(user.rootPath)
|
||||
@@ -203,7 +241,7 @@ class PhotoScanner {
|
||||
const session = this.driver.session()
|
||||
|
||||
const userAlbumsResult = await session.run(
|
||||
'MATCH (u:User { id: {userId} })-[:OWNS]->(a:Album) WHERE NOT a.id IN {foundAlbums} DETACH DELETE a return a',
|
||||
'MATCH (u:User { id: {userId} })-[:OWNS]->(a:Album)-[:CONTAINS]->(p:Photo) WHERE NOT a.id IN {foundAlbums} DETACH DELETE a, p RETURN a',
|
||||
{ userId: user.id, foundAlbums: foundAlbumIds }
|
||||
)
|
||||
|
||||
@@ -276,22 +314,29 @@ class PhotoScanner {
|
||||
}
|
||||
|
||||
async processImage(id) {
|
||||
console.log('Processing image')
|
||||
const session = this.driver.session()
|
||||
|
||||
const result = await session.run('MATCH (p:Photo { id: {id} }) return p', {
|
||||
const result = await session.run(`MATCH (p:Photo { id: {id} }) RETURN p`, {
|
||||
id,
|
||||
})
|
||||
|
||||
await session.run(
|
||||
`MATCH (p:Photo { id: {id} })-[rel]->(url:PhotoURL) DELETE url, rel`,
|
||||
{ id }
|
||||
)
|
||||
|
||||
console.log('PROCESS IMAGE RESULT', result)
|
||||
|
||||
const photo = result.records[0].get('p').properties
|
||||
|
||||
console.log('PHOTO', photo.path)
|
||||
console.log('Processing photo', photo.path)
|
||||
|
||||
const imagePath = path.resolve(config.cachePath, 'images', id)
|
||||
|
||||
await fs.remove(imagePath)
|
||||
await fs.mkdirp(imagePath)
|
||||
|
||||
let resizeBaseImg = photo.path
|
||||
let originalPath = photo.path
|
||||
|
||||
if (await isRawImage(photo.path)) {
|
||||
console.log('Processing RAW image')
|
||||
@@ -299,15 +344,40 @@ class PhotoScanner {
|
||||
const extractedPath = path.resolve(imagePath, 'extracted.jpg')
|
||||
await exiftool.extractPreview(photo.path, extractedPath)
|
||||
|
||||
resizeBaseImg = extractedPath
|
||||
originalPath = extractedPath
|
||||
}
|
||||
|
||||
// Resize image
|
||||
console.log('Resizing image', resizeBaseImg)
|
||||
await sharp(resizeBaseImg)
|
||||
const thumbnailPath = path.resolve(imagePath, 'thumbnail.jpg')
|
||||
await sharp(originalPath)
|
||||
.jpeg({ quality: 80 })
|
||||
.resize(1440, 1080, { fit: 'inside', withoutEnlargement: true })
|
||||
.toFile(path.resolve(imagePath, 'thumbnail.jpg'))
|
||||
.toFile(thumbnailPath)
|
||||
|
||||
const { width: originalWidth, height: originalHeight } = await imageSize(
|
||||
originalPath
|
||||
)
|
||||
const { width: thumbnailWidth, height: thumbnailHeight } = await imageSize(
|
||||
thumbnailPath
|
||||
)
|
||||
|
||||
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)
|
||||
`,
|
||||
{
|
||||
id,
|
||||
thumbnailUrl: `/images/${id}/${path.basename(thumbnailPath)}`,
|
||||
thumbnailWidth,
|
||||
thumbnailHeight,
|
||||
originalUrl: `/images/${id}/${path.basename(originalPath)}`,
|
||||
originalWidth,
|
||||
originalHeight,
|
||||
}
|
||||
)
|
||||
|
||||
session.close()
|
||||
|
||||
|
||||
@@ -2,4 +2,5 @@ import path from 'path'
|
||||
|
||||
export default {
|
||||
cachePath: path.resolve(__dirname, 'cache'),
|
||||
host: process.env.HOST || 'http://localhost:4001/',
|
||||
}
|
||||
|
||||
@@ -126,10 +126,6 @@ app.use('/images/:id/:image', async function(req, res) {
|
||||
|
||||
console.log('image', image)
|
||||
|
||||
if (image != 'original.jpg' && image != 'thumbnail.jpg') {
|
||||
return res.status(404).send('Image not found')
|
||||
}
|
||||
|
||||
let user = null
|
||||
|
||||
try {
|
||||
@@ -142,7 +138,7 @@ app.use('/images/:id/:image', async function(req, res) {
|
||||
const session = driver.session()
|
||||
|
||||
const result = await session.run(
|
||||
'MATCH (p:Photo { id: {id} }) MATCH (p)<-[:CONTAINS]-(:Album)<-[:OWNS]-(u:User) RETURN p as photo, u.id as userId',
|
||||
'MATCH (p:Photo { id: {id} })<-[:CONTAINS]-(:Album)<-[:OWNS]-(u:User) RETURN p as photo, u.id as userId',
|
||||
{
|
||||
id,
|
||||
}
|
||||
@@ -153,7 +149,7 @@ app.use('/images/:id/:image', async function(req, res) {
|
||||
}
|
||||
|
||||
const userId = result.records[0].get('userId')
|
||||
const photo = result.records[0].get('photo')
|
||||
const photo = result.records[0].get('photo').properties
|
||||
|
||||
if (userId != user.id) {
|
||||
return res.status(401).send(`Image not owned by you`)
|
||||
@@ -161,7 +157,12 @@ app.use('/images/:id/:image', async function(req, res) {
|
||||
|
||||
session.close()
|
||||
|
||||
const imagePath = path.resolve(config.cachePath, 'images', id, image)
|
||||
let imagePath = path.resolve(config.cachePath, 'images', id, image)
|
||||
|
||||
if (image != 'extracted.jpg' && image != 'thumbnail.jpg') {
|
||||
imagePath = photo.path
|
||||
}
|
||||
|
||||
const imageFound = await fs.exists(imagePath)
|
||||
|
||||
if (!imageFound) {
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { cypherQuery } from 'neo4j-graphql-js'
|
||||
import { promisify } from 'util'
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import config from '../config'
|
||||
import { isRawImage } from '../Scanner'
|
||||
|
||||
const imageSize = promisify(require('image-size'))
|
||||
|
||||
function injectAt(query, index, injection) {
|
||||
return query.substr(0, index) + injection + query.substr(index)
|
||||
@@ -130,75 +124,13 @@ const Query = {
|
||||
},
|
||||
}
|
||||
|
||||
function photoResolver(image) {
|
||||
return async (root, args, ctx, info) => {
|
||||
const imgPath = path.resolve(config.cachePath, 'images', root.id, image)
|
||||
|
||||
if (!(await fs.exists(imgPath))) {
|
||||
await ctx.scanner.processImage(root.id)
|
||||
}
|
||||
|
||||
const { width, height } = await imageSize(imgPath)
|
||||
return {
|
||||
path: `${ctx.endpoint}/images/${root.id}/${image}`,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Photo = {
|
||||
// TODO: Make original point to the right path
|
||||
original: async (root, args, ctx, info) => {
|
||||
async function getPath(retryAfterScan = false) {
|
||||
let imgPath = path.resolve(
|
||||
config.cachePath,
|
||||
'images',
|
||||
root.id,
|
||||
'extracted.jpg'
|
||||
)
|
||||
|
||||
if (!(await fs.exists(imgPath))) {
|
||||
imgPath = root.path
|
||||
|
||||
if (!imgPath) {
|
||||
const session = ctx.driver.session()
|
||||
|
||||
const result = await session.run(
|
||||
'MATCH (p:Photo { id: {id} }) return p.path as path',
|
||||
{
|
||||
id: root.id,
|
||||
}
|
||||
)
|
||||
|
||||
imgPath = result.get('path')
|
||||
session.close()
|
||||
}
|
||||
|
||||
if (!(await fs.exists(imgPath)) || (await isRawImage(imgPath))) {
|
||||
if (retryAfterScan)
|
||||
throw new Error('Could not find image after rescan')
|
||||
await ctx.scanner.processImage(root.id)
|
||||
return getPath(true)
|
||||
}
|
||||
}
|
||||
|
||||
return imgPath
|
||||
}
|
||||
|
||||
const imgPath = await getPath()
|
||||
|
||||
const { width, height } = await imageSize(imgPath)
|
||||
return {
|
||||
path: `${ctx.endpoint}/images/${root.id}/${path.basename(imgPath)}`,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
const PhotoURL = {
|
||||
url(root, args, ctx, info) {
|
||||
return new URL(root.url, config.host).href
|
||||
},
|
||||
thumbnail: photoResolver('thumbnail.jpg'),
|
||||
}
|
||||
|
||||
export default {
|
||||
Query,
|
||||
Photo,
|
||||
PhotoURL,
|
||||
}
|
||||
|
||||
@@ -16,13 +16,15 @@ type Album {
|
||||
id: ID!
|
||||
title: String
|
||||
photos: [Photo] @relation(name: "CONTAINS", direction: "OUT")
|
||||
subAlbums: [Album] @relation(name: "SUBALBUM", direction: "OUT")
|
||||
parentAlbum: Album @relation(name: "SUBALBUM", direction: "IN")
|
||||
owner: User! @relation(name: "OWNS", direction: "IN")
|
||||
path: String
|
||||
}
|
||||
|
||||
type PhotoURL {
|
||||
# URL for the image
|
||||
path: String
|
||||
url: String
|
||||
# Width of the image in pixels
|
||||
width: Int
|
||||
# Height of the image in pixels
|
||||
@@ -35,9 +37,9 @@ type Photo {
|
||||
# Local filepath for the photo
|
||||
path: String
|
||||
# URL to display the photo in full resolution
|
||||
original: PhotoURL @neo4j_ignore
|
||||
original: PhotoURL @relation(name: "ORIGINAL_URL", direction: "OUT")
|
||||
# URL to display the photo in a smaller resolution
|
||||
thumbnail: PhotoURL @neo4j_ignore
|
||||
thumbnail: PhotoURL @relation(name: "THUMBNAIL_URL", direction: "OUT")
|
||||
# The album that holds the photo
|
||||
album: Album! @relation(name: "CONTAINS", direction: "IN")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user