diff --git a/api/src/Scanner.js b/api/src/Scanner.js index a6860d42..55a574f3 100644 --- a/api/src/Scanner.js +++ b/api/src/Scanner.js @@ -18,7 +18,7 @@ const isImage = async path => { return type != null } -const isRawImage = async path => { +export const isRawImage = async path => { const buffer = await readChunk(path, 0, 12) const { ext } = imageType(buffer) @@ -37,6 +37,9 @@ class PhotoScanner { 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() { @@ -53,6 +56,8 @@ class PhotoScanner { let session = this.driver.session() + let allUserScans = [] + session.run('MATCH (u:User) return u').subscribe({ onNext: record => { const user = record.toObject().u.properties @@ -64,23 +69,38 @@ class PhotoScanner { return } - this.scanUser(user) + allUserScans.push(this.scanUser(user)) console.log(`Scanning ${user.username}...`) }, onCompleted: () => { session.close() this.isRunning = false - console.log('Done scanning') - this.pubsub.publish(EVENT_SCANNER_PROGRESS, { - scannerStatusUpdate: { - progress: 100, - finished: true, - error: false, - errorMessage: '', - }, - }) + 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 => { + this.pubsub.publish(EVENT_SCANNER_PROGRESS, { + scannerStatusUpdate: { + progress: 0, + finished: false, + error: true, + errorMessage: error.message, + }, + }) + }) }, onError: error => { console.error(error) @@ -187,6 +207,8 @@ class PhotoScanner { 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`, { @@ -197,6 +219,20 @@ class PhotoScanner { 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() @@ -248,16 +284,23 @@ class PhotoScanner { // Resize image console.log('Resizing image', resizeBaseImg) await sharp(resizeBaseImg) + .jpeg({ quality: 80 }) .resize(1440, 1080, { fit: 'inside', withoutEnlargement: true }) .toFile(path.resolve(imagePath, 'thumbnail.jpg')) - await sharp(resizeBaseImg) - .jpeg({ quality: 85 }) - .toFile(path.resolve(imagePath, 'original.jpg')) - 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: '', + }, + }) } } diff --git a/api/src/index.js b/api/src/index.js index b2c4b438..ec939d75 100644 --- a/api/src/index.js +++ b/api/src/index.js @@ -72,6 +72,8 @@ const driver = neo4j.driver( const scanner = new PhotoScanner(driver) +setInterval(scanner.scanAll, 1000 * 60 * 60 * 4) + // Specify port and path for GraphQL endpoint const port = process.env.GRAPHQL_LISTEN_PORT || 4001 const graphPath = '/graphql' diff --git a/api/src/resolvers/photos.js b/api/src/resolvers/photos.js index 0a9e2e16..20b68716 100644 --- a/api/src/resolvers/photos.js +++ b/api/src/resolvers/photos.js @@ -3,6 +3,7 @@ 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')) @@ -147,7 +148,53 @@ function photoResolver(image) { } const Photo = { - original: photoResolver('original.jpg'), + // 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, + } + }, thumbnail: photoResolver('thumbnail.jpg'), } diff --git a/api/src/schema.graphql b/api/src/schema.graphql index b1a4a35d..13374c2a 100644 --- a/api/src/schema.graphql +++ b/api/src/schema.graphql @@ -6,8 +6,8 @@ enum Role { type User @isAuthenticated { id: ID! username: String! - #: password: String albums: [Album] @relation(name: "OWNS", direction: "OUT") + # Local filepath for the user's photos rootPath: String! @hasRole(roles: [Admin]) admin: Boolean } @@ -20,22 +20,25 @@ type Album { path: String } -type UnimportedAlbum { - id: ID! - relativePath: String -} - type PhotoURL { + # URL for the image path: String + # Width of the image in pixels width: Int + # Height of the image in pixels height: Int } type Photo { id: ID! title: String + # Local filepath for the photo + path: String + # URL to display the photo in full resolution original: PhotoURL @neo4j_ignore + # URL to display the photo in a smaller resolution thumbnail: PhotoURL @neo4j_ignore + # The album that holds the photo album: Album! @relation(name: "CONTAINS", direction: "IN") } diff --git a/ui/src/Messages.js b/ui/src/Messages.js index ec6e1687..506d9a9a 100644 --- a/ui/src/Messages.js +++ b/ui/src/Messages.js @@ -1,4 +1,4 @@ -import React, { Component } from 'react' +import React, { Component, useState } from 'react' import styled from 'styled-components' import { Message, Progress } from 'semantic-ui-react' import gql from 'graphql-tag' @@ -47,10 +47,24 @@ const MessageProgress = ({ header, content, percent, ...props }) => { } class Messages extends Component { + constructor(props) { + super(props) + + this.state = { + showSyncMessage: true, + } + } + render() { return ( - + { + this.setState({ showSyncMessage: true }) + }} + > {({ loading, error, data }) => { if (error) return
error {error.message}
if (loading) return null @@ -60,6 +74,10 @@ class Messages extends Component { return (