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 dotenv from 'dotenv'
|
||||
import http from 'http'
|
||||
import PhotoScanner from './Scanner'
|
||||
import PhotoScanner from './scanner/Scanner'
|
||||
import _ from 'lodash'
|
||||
import config from './config'
|
||||
|
||||
|
@ -124,8 +124,6 @@ server.applyMiddleware({ app, graphPath })
|
|||
app.use('/images/:id/:image', async function(req, res) {
|
||||
const { id, image } = req.params
|
||||
|
||||
console.log('image', image)
|
||||
|
||||
let user = null
|
||||
|
||||
try {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { EVENT_SCANNER_PROGRESS } from '../scanner'
|
||||
import { EVENT_SCANNER_PROGRESS } from '../scanner/Scanner'
|
||||
|
||||
const Mutation = {
|
||||
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 AlbumSidebar from './AlbumSidebar'
|
||||
import PhotoGallery from '../../PhotoGallery'
|
||||
import AlbumGallery from '../AllAlbumsPage/AlbumGallery'
|
||||
|
||||
const albumQuery = gql`
|
||||
query albumQuery($id: ID) {
|
||||
|
@ -85,15 +86,31 @@ class AlbumPage extends Component {
|
|||
{({ loading, error, data }) => {
|
||||
if (error) return <div>Error</div>
|
||||
|
||||
let subAlbumElement = null
|
||||
|
||||
if (data.album) {
|
||||
this.photoAmount = data.album.photos.length
|
||||
|
||||
if (data.album.subAlbums.length > 0) {
|
||||
subAlbumElement = (
|
||||
<AlbumGallery
|
||||
loading={loading}
|
||||
error={error}
|
||||
albums={data.album.subAlbums}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{data.album && data.album.title}</h1>
|
||||
{subAlbumElement}
|
||||
{data.album && data.album.subAlbums.length > 0 && (
|
||||
<h2>Images</h2>
|
||||
)}
|
||||
<PhotoGallery
|
||||
loading={loading}
|
||||
title={data.album && data.album.title}
|
||||
photos={data.album && data.album.photos}
|
||||
activeIndex={this.state.activeImage}
|
||||
onSelectImage={index => {
|
||||
|
|
|
@ -4,10 +4,8 @@ import { Loader } from 'semantic-ui-react'
|
|||
import { AlbumBox } from './AlbumBox'
|
||||
|
||||
const Container = styled.div`
|
||||
margin: -10px;
|
||||
margin-top: 20px;
|
||||
margin: 20px -10px;
|
||||
position: relative;
|
||||
min-height: 500px;
|
||||
`
|
||||
|
||||
const AlbumGallery = ({ loading, error, albums }) => {
|
||||
|
|
|
@ -60,13 +60,7 @@ const PhotoFiller = styled.div`
|
|||
flex-grow: 999999;
|
||||
`
|
||||
|
||||
const PhotoGallery = ({
|
||||
activeIndex = -1,
|
||||
photos,
|
||||
loading,
|
||||
title,
|
||||
onSelectImage,
|
||||
}) => {
|
||||
const PhotoGallery = ({ activeIndex = -1, photos, loading, onSelectImage }) => {
|
||||
let photoElements = null
|
||||
if (photos) {
|
||||
photoElements = photos.map((photo, index) => {
|
||||
|
@ -96,7 +90,6 @@ const PhotoGallery = ({
|
|||
|
||||
return (
|
||||
<div>
|
||||
<h1>{title}</h1>
|
||||
<Gallery>
|
||||
<Loader active={loading}>Loading images</Loader>
|
||||
{photoElements}
|
||||
|
|
Loading…
Reference in New Issue