mirror of
https://github.com/xuthus83/pigallery2.git
synced 2024-11-03 21:04:03 +08:00
345 lines
13 KiB
TypeScript
345 lines
13 KiB
TypeScript
import {IGalleryManager} from '../interfaces/IGalleryManager';
|
|
import {ParentDirectoryDTO, SubDirectoryDTO} from '../../../../common/entities/DirectoryDTO';
|
|
import * as path from 'path';
|
|
import * as fs from 'fs';
|
|
import {DirectoryEntity} from './enitites/DirectoryEntity';
|
|
import {SQLConnection} from './SQLConnection';
|
|
import {PhotoEntity} from './enitites/PhotoEntity';
|
|
import {ProjectPath} from '../../../ProjectPath';
|
|
import {Config} from '../../../../common/config/private/Config';
|
|
import {ISQLGalleryManager} from './IGalleryManager';
|
|
import {PhotoDTO} from '../../../../common/entities/PhotoDTO';
|
|
import {Brackets, Connection, WhereExpression} from 'typeorm';
|
|
import {MediaEntity} from './enitites/MediaEntity';
|
|
import {VideoEntity} from './enitites/VideoEntity';
|
|
import {DiskMangerWorker} from '../../threading/DiskMangerWorker';
|
|
import {Logger} from '../../../Logger';
|
|
import {FaceRegionEntry} from './enitites/FaceRegionEntry';
|
|
import {ObjectManagers} from '../../ObjectManagers';
|
|
import {DuplicatesDTO} from '../../../../common/entities/DuplicatesDTO';
|
|
import {ReIndexingSensitivity} from '../../../../common/config/private/PrivateConfig';
|
|
|
|
|
|
const LOG_TAG = '[GalleryManager]';
|
|
|
|
export class GalleryManager implements IGalleryManager, ISQLGalleryManager {
|
|
|
|
public static parseRelativeDirePath(relativeDirectoryName: string): { name: string, parent: string } {
|
|
|
|
relativeDirectoryName = DiskMangerWorker.normalizeDirPath(relativeDirectoryName);
|
|
return {
|
|
name: path.basename(relativeDirectoryName),
|
|
parent: path.join(path.dirname(relativeDirectoryName), path.sep),
|
|
};
|
|
}
|
|
|
|
public async listDirectory(relativeDirectoryName: string,
|
|
knownLastModified?: number,
|
|
knownLastScanned?: number): Promise<ParentDirectoryDTO> {
|
|
const directoryPath = GalleryManager.parseRelativeDirePath(relativeDirectoryName);
|
|
const connection = await SQLConnection.getConnection();
|
|
const stat = fs.statSync(path.join(ProjectPath.ImageFolder, relativeDirectoryName));
|
|
const lastModified = DiskMangerWorker.calcLastModified(stat);
|
|
|
|
const dir = await this.selectParentDir(connection, directoryPath.name, directoryPath.parent);
|
|
if (dir && dir.lastScanned != null) {
|
|
// If it seems that the content did not changed, do not work on it
|
|
if (knownLastModified && knownLastScanned
|
|
&& lastModified === knownLastModified &&
|
|
dir.lastScanned === knownLastScanned) {
|
|
if (Config.Server.Indexing.reIndexingSensitivity === ReIndexingSensitivity.low) {
|
|
return null;
|
|
}
|
|
if (Date.now() - dir.lastScanned <= Config.Server.Indexing.cachedFolderTimeout &&
|
|
Config.Server.Indexing.reIndexingSensitivity === ReIndexingSensitivity.medium) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
|
|
if (dir.lastModified !== lastModified) {
|
|
Logger.silly(LOG_TAG, 'Reindexing reason: lastModified mismatch: known: '
|
|
+ dir.lastModified + ', current:' + lastModified);
|
|
const ret = await ObjectManagers.getInstance().IndexingManager.indexDirectory(relativeDirectoryName);
|
|
for (const subDir of ret.directories) {
|
|
if (!subDir.preview) { // if sub directories does not have photos, so cannot show a preview, try get one from DB
|
|
await this.fillPreviewForSubDir(connection, subDir);
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
|
|
// not indexed since a while, index it in a lazy manner
|
|
if ((Date.now() - dir.lastScanned > Config.Server.Indexing.cachedFolderTimeout &&
|
|
Config.Server.Indexing.reIndexingSensitivity >= ReIndexingSensitivity.medium) ||
|
|
Config.Server.Indexing.reIndexingSensitivity >= ReIndexingSensitivity.high) {
|
|
// on the fly reindexing
|
|
|
|
Logger.silly(LOG_TAG, 'lazy reindexing reason: cache timeout: lastScanned: '
|
|
+ (Date.now() - dir.lastScanned) + 'ms ago, cachedFolderTimeout:' + Config.Server.Indexing.cachedFolderTimeout);
|
|
ObjectManagers.getInstance().IndexingManager.indexDirectory(relativeDirectoryName).catch(console.error);
|
|
}
|
|
await this.fillParentDir(connection, dir);
|
|
return dir;
|
|
}
|
|
|
|
// never scanned (deep indexed), do it and return with it
|
|
Logger.silly(LOG_TAG, 'Reindexing reason: never scanned');
|
|
return ObjectManagers.getInstance().IndexingManager.indexDirectory(relativeDirectoryName);
|
|
|
|
|
|
}
|
|
|
|
async countDirectories(): Promise<number> {
|
|
const connection = await SQLConnection.getConnection();
|
|
return await connection.getRepository(DirectoryEntity)
|
|
.createQueryBuilder('directory')
|
|
.getCount();
|
|
}
|
|
|
|
async countMediaSize(): Promise<number> {
|
|
const connection = await SQLConnection.getConnection();
|
|
const {sum} = await connection.getRepository(MediaEntity)
|
|
.createQueryBuilder('media')
|
|
.select('SUM(media.metadata.fileSize)', 'sum')
|
|
.getRawOne();
|
|
return sum || 0;
|
|
}
|
|
|
|
async countPhotos(): Promise<number> {
|
|
const connection = await SQLConnection.getConnection();
|
|
return await connection.getRepository(PhotoEntity)
|
|
.createQueryBuilder('directory')
|
|
.getCount();
|
|
}
|
|
|
|
async countVideos(): Promise<number> {
|
|
const connection = await SQLConnection.getConnection();
|
|
return await connection.getRepository(VideoEntity)
|
|
.createQueryBuilder('directory')
|
|
.getCount();
|
|
}
|
|
|
|
public async getPossibleDuplicates(): Promise<DuplicatesDTO[]> {
|
|
const connection = await SQLConnection.getConnection();
|
|
const mediaRepository = connection.getRepository(MediaEntity);
|
|
|
|
let duplicates = await mediaRepository.createQueryBuilder('media')
|
|
.innerJoin((query): any => query.from(MediaEntity, 'innerMedia')
|
|
.select(['innerMedia.name as name', 'innerMedia.metadata.fileSize as fileSize', 'count(*)'])
|
|
.groupBy('innerMedia.name, innerMedia.metadata.fileSize')
|
|
.having('count(*)>1'),
|
|
'innerMedia',
|
|
'media.name=innerMedia.name AND media.metadata.fileSize = innerMedia.fileSize')
|
|
.innerJoinAndSelect('media.directory', 'directory')
|
|
.orderBy('media.name, media.metadata.fileSize')
|
|
.limit(Config.Server.Duplicates.listingLimit).getMany();
|
|
|
|
|
|
const duplicateParis: DuplicatesDTO[] = [];
|
|
const processDuplicates = (duplicateList: MediaEntity[],
|
|
equalFn: (a: MediaEntity, b: MediaEntity) => boolean,
|
|
checkDuplicates: boolean = false): void => {
|
|
let i = duplicateList.length - 1;
|
|
while (i >= 0) {
|
|
const list = [duplicateList[i]];
|
|
let j = i - 1;
|
|
while (j >= 0 && equalFn(duplicateList[i], duplicateList[j])) {
|
|
list.push(duplicateList[j]);
|
|
j--;
|
|
}
|
|
i = j;
|
|
// if we cut the select list with the SQL LIMIT, filter unpaired media
|
|
if (list.length < 2) {
|
|
continue;
|
|
}
|
|
if (checkDuplicates) {
|
|
// ad to group if one already existed
|
|
const foundDuplicates = duplicateParis.find((dp): boolean =>
|
|
!!dp.media.find((m): boolean =>
|
|
!!list.find((lm): boolean => lm.id === m.id)));
|
|
if (foundDuplicates) {
|
|
list.forEach((lm): void => {
|
|
if (!!foundDuplicates.media.find((m): boolean => m.id === lm.id)) {
|
|
return;
|
|
}
|
|
foundDuplicates.media.push(lm);
|
|
});
|
|
continue;
|
|
}
|
|
}
|
|
|
|
duplicateParis.push({media: list});
|
|
}
|
|
};
|
|
|
|
processDuplicates(duplicates,
|
|
(a, b): boolean => a.name === b.name &&
|
|
a.metadata.fileSize === b.metadata.fileSize);
|
|
|
|
|
|
duplicates = await mediaRepository.createQueryBuilder('media')
|
|
.innerJoin((query): any => query.from(MediaEntity, 'innerMedia')
|
|
.select(['innerMedia.metadata.creationDate as creationDate', 'innerMedia.metadata.fileSize as fileSize', 'count(*)'])
|
|
.groupBy('innerMedia.metadata.creationDate, innerMedia.metadata.fileSize')
|
|
.having('count(*)>1'),
|
|
'innerMedia',
|
|
'media.metadata.creationDate=innerMedia.creationDate AND media.metadata.fileSize = innerMedia.fileSize')
|
|
.innerJoinAndSelect('media.directory', 'directory')
|
|
.orderBy('media.metadata.creationDate, media.metadata.fileSize')
|
|
.limit(Config.Server.Duplicates.listingLimit).getMany();
|
|
|
|
processDuplicates(duplicates,
|
|
(a, b): boolean => a.metadata.creationDate === b.metadata.creationDate &&
|
|
a.metadata.fileSize === b.metadata.fileSize, true);
|
|
|
|
return duplicateParis;
|
|
|
|
}
|
|
|
|
/**
|
|
* Returns with the directories only, does not include media or metafiles
|
|
*/
|
|
public async selectDirStructure(relativeDirectoryName: string): Promise<DirectoryEntity> {
|
|
const directoryPath = GalleryManager.parseRelativeDirePath(relativeDirectoryName);
|
|
const connection = await SQLConnection.getConnection();
|
|
const query = connection
|
|
.getRepository(DirectoryEntity)
|
|
.createQueryBuilder('directory')
|
|
.where('directory.name = :name AND directory.path = :path', {
|
|
name: directoryPath.name,
|
|
path: directoryPath.parent
|
|
})
|
|
.leftJoinAndSelect('directory.directories', 'directories');
|
|
|
|
return await query.getOne();
|
|
}
|
|
|
|
/**
|
|
* Sets preview for the directory and caches it in the DB
|
|
*/
|
|
public async fillPreviewForSubDir(connection: Connection, dir: SubDirectoryDTO): Promise<void> {
|
|
|
|
if (!dir.preview || !dir.validPreview) {
|
|
|
|
dir.preview = await ObjectManagers.getInstance().PreviewManager.getPreviewForDirectory(dir);
|
|
// write preview back to db
|
|
await connection.createQueryBuilder()
|
|
.update(DirectoryEntity).set({preview: dir.preview, validPreview: true}).where('id = :dir', {
|
|
dir: dir.id
|
|
}).execute();
|
|
}
|
|
|
|
|
|
dir.media = [];
|
|
dir.isPartial = true;
|
|
if (dir.preview) {
|
|
dir.preview.readyThumbnails = [];
|
|
dir.preview.readyIcon = false;
|
|
}
|
|
}
|
|
|
|
public async onNewDataVersion(changedDir: ParentDirectoryDTO): Promise<void> {
|
|
// Invalidating Album preview
|
|
let fullPath = DiskMangerWorker.normalizeDirPath(path.join(changedDir.path, changedDir.name));
|
|
const query = (await SQLConnection.getConnection())
|
|
.createQueryBuilder()
|
|
.update(DirectoryEntity)
|
|
.set({validPreview: false});
|
|
|
|
let i = 0;
|
|
const root = DiskMangerWorker.pathFromRelativeDirName('.');
|
|
while (fullPath !== root) {
|
|
const name = DiskMangerWorker.dirName(fullPath);
|
|
const parentPath = DiskMangerWorker.pathFromRelativeDirName(fullPath);
|
|
fullPath = parentPath;
|
|
++i;
|
|
query.orWhere(new Brackets((q: WhereExpression) => {
|
|
const param: { [key: string]: string } = {};
|
|
param['name' + i] = name;
|
|
param['path' + i] = parentPath;
|
|
q.where(`path = :path${i}`, param);
|
|
q.andWhere(`name = :name${i}`, param);
|
|
}));
|
|
}
|
|
|
|
++i;
|
|
query.orWhere(new Brackets((q: WhereExpression) => {
|
|
const param: { [key: string]: string } = {};
|
|
param['name' + i] = DiskMangerWorker.dirName('.');
|
|
param['path' + i] = DiskMangerWorker.pathFromRelativeDirName('.');
|
|
q.where(`path = :path${i}`, param);
|
|
q.andWhere(`name = :name${i}`, param);
|
|
}));
|
|
|
|
|
|
await query.execute();
|
|
}
|
|
|
|
protected async selectParentDir(connection: Connection, directoryName: string, directoryParent: string): Promise<ParentDirectoryDTO> {
|
|
const query = connection
|
|
.getRepository(DirectoryEntity)
|
|
.createQueryBuilder('directory')
|
|
.where('directory.name = :name AND directory.path = :path', {
|
|
name: directoryName,
|
|
path: directoryParent
|
|
})
|
|
.leftJoinAndSelect('directory.directories', 'directories')
|
|
.leftJoinAndSelect('directory.media', 'media')
|
|
.leftJoinAndSelect('directories.preview', 'preview')
|
|
.leftJoinAndSelect('preview.directory', 'previewDirectory')
|
|
.select(['directory',
|
|
'directories',
|
|
'media',
|
|
'preview.name',
|
|
'previewDirectory.name',
|
|
'previewDirectory.path']);
|
|
|
|
|
|
// TODO: do better filtering
|
|
// NOTE: it should not cause an issue as it also do not shave to the DB
|
|
if (Config.Client.MetaFile.gpx === true ||
|
|
Config.Client.MetaFile.pg2conf === true ||
|
|
Config.Client.MetaFile.markdown === true) {
|
|
query.leftJoinAndSelect('directory.metaFile', 'metaFile');
|
|
}
|
|
|
|
return await query.getOne();
|
|
}
|
|
|
|
protected async fillParentDir(connection: Connection, dir: ParentDirectoryDTO): Promise<void> {
|
|
if (dir.media) {
|
|
const indexedFaces = await connection.getRepository(FaceRegionEntry)
|
|
.createQueryBuilder('face')
|
|
.leftJoinAndSelect('face.media', 'media')
|
|
.where('media.directory = :directory', {
|
|
directory: dir.id
|
|
})
|
|
.leftJoinAndSelect('face.person', 'person')
|
|
.select(['face.id', 'face.box.left',
|
|
'face.box.top', 'face.box.width', 'face.box.height',
|
|
'media.id', 'person.name', 'person.id'])
|
|
.getMany();
|
|
for (const item of dir.media) {
|
|
item.directory = dir;
|
|
item.readyThumbnails = [];
|
|
item.readyIcon = false;
|
|
(item as PhotoDTO).metadata.faces = indexedFaces
|
|
.filter((fe): boolean => fe.media.id === item.id)
|
|
.map((f): { name: any; box: any } => ({box: f.box, name: f.person.name}));
|
|
}
|
|
}
|
|
if (dir.metaFile) {
|
|
for (const item of dir.metaFile) {
|
|
item.directory = dir;
|
|
}
|
|
}
|
|
if (dir.directories) {
|
|
for (const item of dir.directories) {
|
|
await this.fillPreviewForSubDir(connection, item);
|
|
}
|
|
}
|
|
}
|
|
}
|