Refactor scanner into multiple files
This commit is contained in:
parent
7a54fc9bc9
commit
df0f2e9140
|
@ -1,398 +0,0 @@
|
||||||
import fs from 'fs-extra'
|
|
||||||
import path from 'path'
|
|
||||||
import { resolve as pathResolve, basename as pathBasename } from 'path'
|
|
||||||
import { PubSub } from 'apollo-server'
|
|
||||||
import uuid from 'uuid'
|
|
||||||
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 => {
|
|
||||||
const buffer = await readChunk(path, 0, 12)
|
|
||||||
const type = imageType(buffer)
|
|
||||||
|
|
||||||
return type != null
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isRawImage = async path => {
|
|
||||||
const buffer = await readChunk(path, 0, 12)
|
|
||||||
const { ext } = imageType(buffer)
|
|
||||||
|
|
||||||
const rawTypes = ['cr2', 'arw', 'crw', 'dng']
|
|
||||||
|
|
||||||
return rawTypes.includes(ext)
|
|
||||||
}
|
|
||||||
|
|
||||||
class PhotoScanner {
|
|
||||||
constructor(driver) {
|
|
||||||
this.driver = driver
|
|
||||||
this.isRunning = false
|
|
||||||
this.pubsub = new PubSub()
|
|
||||||
|
|
||||||
this.scanAll = this.scanAll.bind(this)
|
|
||||||
this.scanAlbum = this.scanAlbum.bind(this)
|
|
||||||
this.scanUser = this.scanUser.bind(this)
|
|
||||||
this.processImage = this.processImage.bind(this)
|
|
||||||
|
|
||||||
this.imagesToProgress = 0
|
|
||||||
this.finishedImages = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
async scanAll() {
|
|
||||||
this.isRunning = true
|
|
||||||
|
|
||||||
this.pubsub.publish(EVENT_SCANNER_PROGRESS, {
|
|
||||||
scannerStatusUpdate: {
|
|
||||||
progress: 0,
|
|
||||||
finished: false,
|
|
||||||
error: false,
|
|
||||||
errorMessage: '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
let session = this.driver.session()
|
|
||||||
|
|
||||||
let allUserScans = []
|
|
||||||
|
|
||||||
session.run('MATCH (u:User) return u').subscribe({
|
|
||||||
onNext: record => {
|
|
||||||
const user = record.toObject().u.properties
|
|
||||||
|
|
||||||
console.log('USER', user)
|
|
||||||
|
|
||||||
if (!user.rootPath) {
|
|
||||||
console.log(`User ${user.username}, has no root path, skipping`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Scanning ${user.username}...`)
|
|
||||||
allUserScans.push(this.scanUser(user))
|
|
||||||
},
|
|
||||||
onCompleted: () => {
|
|
||||||
session.close()
|
|
||||||
this.isRunning = false
|
|
||||||
|
|
||||||
Promise.all(allUserScans)
|
|
||||||
.then(() => {
|
|
||||||
console.log(
|
|
||||||
`Done scanning ${this.finishedImages} of ${this.imagesToProgress}`
|
|
||||||
)
|
|
||||||
this.pubsub.publish(EVENT_SCANNER_PROGRESS, {
|
|
||||||
scannerStatusUpdate: {
|
|
||||||
progress: 100,
|
|
||||||
finished: true,
|
|
||||||
error: false,
|
|
||||||
errorMessage: '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.log('SYNC ERROR', JSON.stringify(error))
|
|
||||||
this.pubsub.publish(EVENT_SCANNER_PROGRESS, {
|
|
||||||
scannerStatusUpdate: {
|
|
||||||
progress: 0,
|
|
||||||
finished: false,
|
|
||||||
error: true,
|
|
||||||
errorMessage: error.message,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onError: error => {
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
this.pubsub.publish(EVENT_SCANNER_PROGRESS, {
|
|
||||||
scannerStatusUpdate: {
|
|
||||||
progress: 0,
|
|
||||||
finished: false,
|
|
||||||
error: true,
|
|
||||||
errorMessage: error.message,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async scanUser(user) {
|
|
||||||
console.log('Scanning path', user.rootPath)
|
|
||||||
|
|
||||||
const driver = this.driver
|
|
||||||
const scanAlbum = this.scanAlbum
|
|
||||||
|
|
||||||
let foundAlbumIds = []
|
|
||||||
|
|
||||||
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)
|
|
||||||
// console.log(`Scanning item ${itemPath}...`)
|
|
||||||
const stat = fs.statSync(itemPath)
|
|
||||||
|
|
||||||
if (stat.isDirectory()) {
|
|
||||||
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 new album at ${itemPath}`)
|
|
||||||
const session = driver.session()
|
|
||||||
|
|
||||||
console.log('Adding album')
|
|
||||||
const albumId = uuid()
|
|
||||||
const albumResult = await session.run(
|
|
||||||
`MATCH (u:User { id: {userId} })
|
|
||||||
CREATE (a:Album { id: {id}, title: {title}, path: {path} })
|
|
||||||
CREATE (u)-[own:OWNS]->(a)
|
|
||||||
RETURN a`,
|
|
||||||
{
|
|
||||||
id: albumId,
|
|
||||||
userId: user.id,
|
|
||||||
title: item,
|
|
||||||
path: itemPath,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!foundImage && (await isImage(itemPath))) {
|
|
||||||
foundImage = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { foundImage, newAlbums }
|
|
||||||
}
|
|
||||||
|
|
||||||
await scanPath(user.rootPath)
|
|
||||||
|
|
||||||
console.log('Found album ids', foundAlbumIds)
|
|
||||||
|
|
||||||
const session = this.driver.session()
|
|
||||||
|
|
||||||
const userAlbumsResult = await session.run(
|
|
||||||
'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 }
|
|
||||||
)
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Deleted ${userAlbumsResult.records.length} albums from ${user.username} that was not found locally`
|
|
||||||
)
|
|
||||||
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
console.log('User scan complete')
|
|
||||||
}
|
|
||||||
|
|
||||||
async scanAlbum(album) {
|
|
||||||
const { title, path, id } = album
|
|
||||||
console.log('Scanning album', title)
|
|
||||||
|
|
||||||
const list = fs.readdirSync(path)
|
|
||||||
|
|
||||||
for (const item of list) {
|
|
||||||
const itemPath = pathResolve(path, item)
|
|
||||||
|
|
||||||
if (await isImage(itemPath)) {
|
|
||||||
const session = this.driver.session()
|
|
||||||
|
|
||||||
this.imagesToProgress++
|
|
||||||
|
|
||||||
const photoResult = await session.run(
|
|
||||||
`MATCH (p:Photo {path: {imgPath} })<--(a:Album {id: {albumId}}) RETURN p`,
|
|
||||||
{
|
|
||||||
imgPath: itemPath,
|
|
||||||
albumId: id,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (photoResult.records.length != 0) {
|
|
||||||
console.log(`Photo already exists ${item}`)
|
|
||||||
|
|
||||||
const id = photoResult.records[0].get('p').properties.id
|
|
||||||
|
|
||||||
const thumbnailPath = pathResolve(
|
|
||||||
config.cachePath,
|
|
||||||
id,
|
|
||||||
'thumbnail.jpg'
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!(await fs.exists(thumbnailPath))) {
|
|
||||||
this.processImage(id)
|
|
||||||
} else {
|
|
||||||
this.finishedImages++
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`Found new image at ${itemPath}`)
|
|
||||||
const imageId = uuid()
|
|
||||||
await session.run(
|
|
||||||
`MATCH (a:Album { id: {albumId} })
|
|
||||||
CREATE (p:Photo {id: {id}, path: {path}, title: {title} })
|
|
||||||
CREATE (a)-[:CONTAINS]->(p)`,
|
|
||||||
{
|
|
||||||
id: imageId,
|
|
||||||
path: itemPath,
|
|
||||||
title: item,
|
|
||||||
albumId: id,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
this.processImage(imageId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async processImage(id) {
|
|
||||||
const session = this.driver.session()
|
|
||||||
|
|
||||||
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('Processing photo', photo.path)
|
|
||||||
|
|
||||||
const imagePath = path.resolve(config.cachePath, 'images', id)
|
|
||||||
|
|
||||||
await fs.remove(imagePath)
|
|
||||||
await fs.mkdirp(imagePath)
|
|
||||||
|
|
||||||
let originalPath = photo.path
|
|
||||||
|
|
||||||
if (await isRawImage(photo.path)) {
|
|
||||||
console.log('Processing RAW image')
|
|
||||||
|
|
||||||
const extractedPath = path.resolve(imagePath, 'extracted.jpg')
|
|
||||||
await exiftool.extractPreview(photo.path, extractedPath)
|
|
||||||
|
|
||||||
originalPath = extractedPath
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resize image
|
|
||||||
const thumbnailPath = path.resolve(imagePath, 'thumbnail.jpg')
|
|
||||||
await sharp(originalPath)
|
|
||||||
.jpeg({ quality: 80 })
|
|
||||||
.resize(1440, 1080, { fit: 'inside', withoutEnlargement: true })
|
|
||||||
.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()
|
|
||||||
|
|
||||||
console.log('Processing done')
|
|
||||||
this.finishedImages++
|
|
||||||
|
|
||||||
this.pubsub.publish(EVENT_SCANNER_PROGRESS, {
|
|
||||||
scannerStatusUpdate: {
|
|
||||||
progress: this.finishedImages / this.imagesToProgress,
|
|
||||||
finished: false,
|
|
||||||
error: false,
|
|
||||||
errorMessage: '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PhotoScanner
|
|
|
@ -8,7 +8,7 @@ import { v1 as neo4j } from 'neo4j-driver'
|
||||||
import { makeAugmentedSchema } from 'neo4j-graphql-js'
|
import { makeAugmentedSchema } from 'neo4j-graphql-js'
|
||||||
import dotenv from 'dotenv'
|
import dotenv from 'dotenv'
|
||||||
import http from 'http'
|
import http from 'http'
|
||||||
import PhotoScanner from './Scanner'
|
import PhotoScanner from './scanner/Scanner'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import config from './config'
|
import config from './config'
|
||||||
|
|
||||||
|
@ -124,8 +124,6 @@ server.applyMiddleware({ app, graphPath })
|
||||||
app.use('/images/:id/:image', async function(req, res) {
|
app.use('/images/:id/:image', async function(req, res) {
|
||||||
const { id, image } = req.params
|
const { id, image } = req.params
|
||||||
|
|
||||||
console.log('image', image)
|
|
||||||
|
|
||||||
let user = null
|
let user = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { EVENT_SCANNER_PROGRESS } from '../scanner'
|
import { EVENT_SCANNER_PROGRESS } from '../scanner/Scanner'
|
||||||
|
|
||||||
const Mutation = {
|
const Mutation = {
|
||||||
async scanAll(root, args, ctx, info) {
|
async scanAll(root, args, ctx, info) {
|
||||||
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
import fs from 'fs-extra'
|
||||||
|
import path from 'path'
|
||||||
|
import { resolve as pathResolve, basename as pathBasename } from 'path'
|
||||||
|
import { PubSub } from 'apollo-server'
|
||||||
|
import uuid from 'uuid'
|
||||||
|
import { exiftool } from 'exiftool-vendored'
|
||||||
|
import sharp from 'sharp'
|
||||||
|
import { isImage, isRawImage } from './utils'
|
||||||
|
import { promisify } from 'util'
|
||||||
|
import config from '../config'
|
||||||
|
import _scanUser from './scanUser'
|
||||||
|
import _scanAlbum from './scanAlbum'
|
||||||
|
import _processImage from './processImage'
|
||||||
|
import _scanAll from './scanAll'
|
||||||
|
|
||||||
|
export const EVENT_SCANNER_PROGRESS = 'SCANNER_PROGRESS'
|
||||||
|
|
||||||
|
class PhotoScanner {
|
||||||
|
constructor(driver) {
|
||||||
|
this.driver = driver
|
||||||
|
this.isRunning = false
|
||||||
|
this.pubsub = new PubSub()
|
||||||
|
|
||||||
|
this.processImage = this.processImage.bind(this)
|
||||||
|
this.scanAlbum = this.scanAlbum.bind(this)
|
||||||
|
this.scanUser = this.scanUser.bind(this)
|
||||||
|
this.scanAll = this.scanAll.bind(this)
|
||||||
|
|
||||||
|
this.imagesToProgress = 0
|
||||||
|
this.finishedImages = 0
|
||||||
|
|
||||||
|
this.addImageToProgress = () => {
|
||||||
|
this.imagesToProgress++
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addFinishedImage = () => {
|
||||||
|
this.finishedImages++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async scanUser(user) {
|
||||||
|
await _scanUser({ driver: this.driver, scanAlbum: this.scanAlbum }, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
async scanAlbum(album) {
|
||||||
|
await _scanAlbum(
|
||||||
|
{
|
||||||
|
driver: this.driver,
|
||||||
|
addImageToProgress: this.addImageToProgress,
|
||||||
|
addFinishedImage: this.addFinishedImage,
|
||||||
|
processImage: this.processImage,
|
||||||
|
},
|
||||||
|
album
|
||||||
|
)
|
||||||
|
|
||||||
|
this.pubsub.publish(EVENT_SCANNER_PROGRESS, {
|
||||||
|
scannerStatusUpdate: {
|
||||||
|
progress: this.finishedImages / this.imagesToProgress,
|
||||||
|
finished: false,
|
||||||
|
error: false,
|
||||||
|
errorMessage: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async processImage(id) {
|
||||||
|
await _processImage(
|
||||||
|
{
|
||||||
|
driver: this.driver,
|
||||||
|
addFinishedImage: this.addFinishedImage,
|
||||||
|
},
|
||||||
|
id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async scanAll() {
|
||||||
|
this.pubsub.publish(EVENT_SCANNER_PROGRESS, {
|
||||||
|
scannerStatusUpdate: {
|
||||||
|
progress: 0,
|
||||||
|
finished: false,
|
||||||
|
error: false,
|
||||||
|
errorMessage: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _scanAll({ driver: this.driver, scanUser: this.scanUser })
|
||||||
|
} catch (error) {
|
||||||
|
this.pubsub.publish(EVENT_SCANNER_PROGRESS, {
|
||||||
|
scannerStatusUpdate: {
|
||||||
|
progress: 0,
|
||||||
|
finished: false,
|
||||||
|
error: true,
|
||||||
|
errorMessage: error.message,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Done scanning ${this.finishedImages} of ${this.imagesToProgress}`
|
||||||
|
)
|
||||||
|
this.pubsub.publish(EVENT_SCANNER_PROGRESS, {
|
||||||
|
scannerStatusUpdate: {
|
||||||
|
progress: 100,
|
||||||
|
finished: true,
|
||||||
|
error: false,
|
||||||
|
errorMessage: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PhotoScanner
|
|
@ -0,0 +1,79 @@
|
||||||
|
import fs from 'fs-extra'
|
||||||
|
import path from 'path'
|
||||||
|
import { exiftool } from 'exiftool-vendored'
|
||||||
|
import sharp from 'sharp'
|
||||||
|
import { isRawImage, imageSize } from './utils'
|
||||||
|
import config from '../config'
|
||||||
|
|
||||||
|
export default async function processImage({ driver, addFinishedImage }, id) {
|
||||||
|
const session = driver.session()
|
||||||
|
|
||||||
|
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 }
|
||||||
|
)
|
||||||
|
|
||||||
|
const photo = result.records[0].get('p').properties
|
||||||
|
|
||||||
|
console.log('Processing photo', photo.path)
|
||||||
|
|
||||||
|
const imagePath = path.resolve(config.cachePath, 'images', id)
|
||||||
|
|
||||||
|
await fs.remove(imagePath)
|
||||||
|
await fs.mkdirp(imagePath)
|
||||||
|
|
||||||
|
let originalPath = photo.path
|
||||||
|
|
||||||
|
if (await isRawImage(photo.path)) {
|
||||||
|
// console.log('Processing RAW image')
|
||||||
|
|
||||||
|
const extractedPath = path.resolve(imagePath, 'extracted.jpg')
|
||||||
|
await exiftool.extractPreview(photo.path, extractedPath)
|
||||||
|
|
||||||
|
originalPath = extractedPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize image
|
||||||
|
const thumbnailPath = path.resolve(imagePath, 'thumbnail.jpg')
|
||||||
|
await sharp(originalPath)
|
||||||
|
.jpeg({ quality: 80 })
|
||||||
|
.resize(1440, 1080, { fit: 'inside', withoutEnlargement: true })
|
||||||
|
.toFile(thumbnailPath)
|
||||||
|
|
||||||
|
try {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Create photo url failed', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
addFinishedImage()
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
import fs from 'fs-extra'
|
||||||
|
import path from 'path'
|
||||||
|
import uuid from 'uuid'
|
||||||
|
import { isImage } from './utils'
|
||||||
|
import config from '../config'
|
||||||
|
|
||||||
|
export default async function scanAlbum(
|
||||||
|
{ driver, addImageToProgress, addFinishedImage, processImage },
|
||||||
|
album
|
||||||
|
) {
|
||||||
|
const { title, path: albumPath, id } = album
|
||||||
|
console.log('Scanning album', title)
|
||||||
|
|
||||||
|
const list = await fs.readdir(albumPath)
|
||||||
|
let processingImagePromises = []
|
||||||
|
|
||||||
|
for (const item of list) {
|
||||||
|
const itemPath = path.resolve(albumPath, item)
|
||||||
|
|
||||||
|
if (await isImage(itemPath)) {
|
||||||
|
const session = driver.session()
|
||||||
|
|
||||||
|
addImageToProgress()
|
||||||
|
|
||||||
|
const photoResult = await session.run(
|
||||||
|
`MATCH (p:Photo {path: {imgPath} })<--(a:Album {id: {albumId}}) RETURN p`,
|
||||||
|
{
|
||||||
|
imgPath: itemPath,
|
||||||
|
albumId: id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (photoResult.records.length != 0) {
|
||||||
|
// console.log(`Photo already exists ${item}`)
|
||||||
|
|
||||||
|
const id = photoResult.records[0].get('p').properties.id
|
||||||
|
|
||||||
|
const thumbnailPath = path.resolve(
|
||||||
|
config.cachePath,
|
||||||
|
id,
|
||||||
|
'thumbnail.jpg'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!(await fs.exists(thumbnailPath))) {
|
||||||
|
processingImagePromises.push(processImage(id))
|
||||||
|
} else {
|
||||||
|
addFinishedImage()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`Found new image at ${itemPath}`)
|
||||||
|
const imageId = uuid()
|
||||||
|
await session.run(
|
||||||
|
`MATCH (a:Album { id: {albumId} })
|
||||||
|
CREATE (p:Photo {id: {id}, path: {path}, title: {title} })
|
||||||
|
CREATE (a)-[:CONTAINS]->(p)`,
|
||||||
|
{
|
||||||
|
id: imageId,
|
||||||
|
path: itemPath,
|
||||||
|
title: item,
|
||||||
|
albumId: id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
processingImagePromises.push(processImage(imageId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(processingImagePromises)
|
||||||
|
console.log('Done processing album', album.title)
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
export default function scanAll({ driver, scanUser }) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let session = driver.session()
|
||||||
|
|
||||||
|
let allUserScans = []
|
||||||
|
|
||||||
|
session.run('MATCH (u:User) return u').subscribe({
|
||||||
|
onNext: record => {
|
||||||
|
const user = record.toObject().u.properties
|
||||||
|
|
||||||
|
if (!user.rootPath) {
|
||||||
|
console.log(`User ${user.username}, has no root path, skipping`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
allUserScans.push(
|
||||||
|
scanUser(user).catch(reason => {
|
||||||
|
console.log(
|
||||||
|
`User scan exception for user ${user.username} ${reason}`
|
||||||
|
)
|
||||||
|
reject(reason)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onCompleted: () => {
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
Promise.all(allUserScans).then(() => {
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onError: error => {
|
||||||
|
session.close()
|
||||||
|
reject(error)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,144 @@
|
||||||
|
import fs from 'fs-extra'
|
||||||
|
import { resolve as pathResolve } from 'path'
|
||||||
|
import uuid from 'uuid'
|
||||||
|
import { isImage } from './utils'
|
||||||
|
|
||||||
|
export default async function scanUser({ driver, scanAlbum }, user) {
|
||||||
|
console.log('Scanning user', user.username, 'at', user.path)
|
||||||
|
|
||||||
|
let foundAlbumIds = []
|
||||||
|
let albumScanPromises = []
|
||||||
|
|
||||||
|
async function scanPath(path, parentAlbum) {
|
||||||
|
console.log('SCAN PATH', path)
|
||||||
|
const list = fs.readdirSync(path)
|
||||||
|
|
||||||
|
let foundImage = false
|
||||||
|
let newAlbums = []
|
||||||
|
|
||||||
|
for (const item of list) {
|
||||||
|
const itemPath = pathResolve(path, item)
|
||||||
|
// console.log(`Scanning item ${itemPath}...`)
|
||||||
|
let stat = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
stat = await fs.stat(itemPath)
|
||||||
|
} catch {
|
||||||
|
console.log('ERROR reading file stat for item:', itemPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stat && stat.isDirectory()) {
|
||||||
|
const session = driver.session()
|
||||||
|
let nextParentAlbum = null
|
||||||
|
|
||||||
|
const findAlbumResult = await session.run(
|
||||||
|
'MATCH (a:Album { path: {path} }) RETURN a',
|
||||||
|
{
|
||||||
|
path: itemPath,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
const {
|
||||||
|
foundImage: imagesInDirectory,
|
||||||
|
newAlbums: childAlbums,
|
||||||
|
} = await scanPath(itemPath, nextParentAlbum)
|
||||||
|
|
||||||
|
if (findAlbumResult.records.length > 0) {
|
||||||
|
const album = findAlbumResult.records[0].toObject().a.properties
|
||||||
|
console.log('Found existing album', album.title)
|
||||||
|
|
||||||
|
nextParentAlbum = album.id
|
||||||
|
foundAlbumIds.push(album.id)
|
||||||
|
albumScanPromises.push(scanAlbum(album))
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imagesInDirectory) {
|
||||||
|
console.log(`Found new album at ${itemPath}`)
|
||||||
|
const session = driver.session()
|
||||||
|
|
||||||
|
console.log('Adding album')
|
||||||
|
const albumId = uuid()
|
||||||
|
const albumResult = await session.run(
|
||||||
|
`MATCH (u:User { id: {userId} })
|
||||||
|
CREATE (a:Album { id: {id}, title: {title}, path: {path} })
|
||||||
|
CREATE (u)-[own:OWNS]->(a)
|
||||||
|
RETURN a`,
|
||||||
|
{
|
||||||
|
id: albumId,
|
||||||
|
userId: user.id,
|
||||||
|
title: item,
|
||||||
|
path: itemPath,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
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} })
|
||||||
|
MERGE (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,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
foundAlbumIds.push(album.id)
|
||||||
|
albumScanPromises.push(scanAlbum(album))
|
||||||
|
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundImage && (await isImage(itemPath))) {
|
||||||
|
foundImage = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { foundImage, newAlbums }
|
||||||
|
}
|
||||||
|
|
||||||
|
await scanPath(user.rootPath)
|
||||||
|
|
||||||
|
console.log('Found album ids', foundAlbumIds)
|
||||||
|
|
||||||
|
const session = driver.session()
|
||||||
|
|
||||||
|
const userAlbumsResult = await session.run(
|
||||||
|
'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 }
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Deleted ${userAlbumsResult.records.length} albums from ${user.username} that was not found locally`
|
||||||
|
)
|
||||||
|
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
await Promise.all(albumScanPromises)
|
||||||
|
|
||||||
|
console.log('User scan complete')
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import fs from 'fs-extra'
|
||||||
|
import readChunk from 'read-chunk'
|
||||||
|
import imageType from 'image-type'
|
||||||
|
import { promisify } from 'util'
|
||||||
|
|
||||||
|
export const isImage = async path => {
|
||||||
|
if ((await fs.stat(path)).isDirectory()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const buffer = await readChunk(path, 0, 12)
|
||||||
|
const type = imageType(buffer)
|
||||||
|
return type != null
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`isImage error at ${path}: ${JSON.stringify(e)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isRawImage = async path => {
|
||||||
|
try {
|
||||||
|
const buffer = await readChunk(path, 0, 12)
|
||||||
|
const { ext } = imageType(buffer)
|
||||||
|
|
||||||
|
const rawTypes = ['cr2', 'arw', 'crw', 'dng']
|
||||||
|
|
||||||
|
return rawTypes.includes(ext)
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`isRawImage error at ${path}: ${JSON.stringify(e)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const imageSize = promisify(require('image-size'))
|
|
@ -4,6 +4,7 @@ import { Query } from 'react-apollo'
|
||||||
import Layout from '../../Layout'
|
import Layout from '../../Layout'
|
||||||
import AlbumSidebar from './AlbumSidebar'
|
import AlbumSidebar from './AlbumSidebar'
|
||||||
import PhotoGallery from '../../PhotoGallery'
|
import PhotoGallery from '../../PhotoGallery'
|
||||||
|
import AlbumGallery from '../AllAlbumsPage/AlbumGallery'
|
||||||
|
|
||||||
const albumQuery = gql`
|
const albumQuery = gql`
|
||||||
query albumQuery($id: ID) {
|
query albumQuery($id: ID) {
|
||||||
|
@ -85,15 +86,31 @@ class AlbumPage extends Component {
|
||||||
{({ loading, error, data }) => {
|
{({ loading, error, data }) => {
|
||||||
if (error) return <div>Error</div>
|
if (error) return <div>Error</div>
|
||||||
|
|
||||||
|
let subAlbumElement = null
|
||||||
|
|
||||||
if (data.album) {
|
if (data.album) {
|
||||||
this.photoAmount = data.album.photos.length
|
this.photoAmount = data.album.photos.length
|
||||||
|
|
||||||
|
if (data.album.subAlbums.length > 0) {
|
||||||
|
subAlbumElement = (
|
||||||
|
<AlbumGallery
|
||||||
|
loading={loading}
|
||||||
|
error={error}
|
||||||
|
albums={data.album.subAlbums}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<h1>{data.album && data.album.title}</h1>
|
||||||
|
{subAlbumElement}
|
||||||
|
{data.album && data.album.subAlbums.length > 0 && (
|
||||||
|
<h2>Images</h2>
|
||||||
|
)}
|
||||||
<PhotoGallery
|
<PhotoGallery
|
||||||
loading={loading}
|
loading={loading}
|
||||||
title={data.album && data.album.title}
|
|
||||||
photos={data.album && data.album.photos}
|
photos={data.album && data.album.photos}
|
||||||
activeIndex={this.state.activeImage}
|
activeIndex={this.state.activeImage}
|
||||||
onSelectImage={index => {
|
onSelectImage={index => {
|
||||||
|
|
|
@ -4,10 +4,8 @@ import { Loader } from 'semantic-ui-react'
|
||||||
import { AlbumBox } from './AlbumBox'
|
import { AlbumBox } from './AlbumBox'
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
margin: -10px;
|
margin: 20px -10px;
|
||||||
margin-top: 20px;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
min-height: 500px;
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const AlbumGallery = ({ loading, error, albums }) => {
|
const AlbumGallery = ({ loading, error, albums }) => {
|
||||||
|
|
|
@ -60,13 +60,7 @@ const PhotoFiller = styled.div`
|
||||||
flex-grow: 999999;
|
flex-grow: 999999;
|
||||||
`
|
`
|
||||||
|
|
||||||
const PhotoGallery = ({
|
const PhotoGallery = ({ activeIndex = -1, photos, loading, onSelectImage }) => {
|
||||||
activeIndex = -1,
|
|
||||||
photos,
|
|
||||||
loading,
|
|
||||||
title,
|
|
||||||
onSelectImage,
|
|
||||||
}) => {
|
|
||||||
let photoElements = null
|
let photoElements = null
|
||||||
if (photos) {
|
if (photos) {
|
||||||
photoElements = photos.map((photo, index) => {
|
photoElements = photos.map((photo, index) => {
|
||||||
|
@ -96,7 +90,6 @@ const PhotoGallery = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>{title}</h1>
|
|
||||||
<Gallery>
|
<Gallery>
|
||||||
<Loader active={loading}>Loading images</Loader>
|
<Loader active={loading}>Loading images</Loader>
|
||||||
{photoElements}
|
{photoElements}
|
||||||
|
|
Loading…
Reference in New Issue