From d75a307be662f79586bce513b8a5b4cb8b6063e4 Mon Sep 17 00:00:00 2001 From: "Patrik J. Braun" Date: Sun, 4 Dec 2022 22:23:51 +0100 Subject: [PATCH] Adding more indices to fields and improving SQL queries #437 --- src/backend/middlewares/PersonMWs.ts | 4 +- .../thumbnail/ThumbnailGeneratorMWs.ts | 23 +-- .../database/interfaces/IPersonManager.ts | 9 +- .../model/database/memory/PersonManager.ts | 2 +- .../model/database/sql/GalleryManager.ts | 143 +++++++----------- .../model/database/sql/IndexingManager.ts | 88 +++++------ .../model/database/sql/PersonManager.ts | 52 +++---- .../model/database/sql/SQLConnection.ts | 6 +- .../model/database/sql/SearchManager.ts | 40 +---- .../database/sql/enitites/MediaEntity.ts | 17 ++- .../database/sql/enitites/PersonEntry.ts | 25 +-- .../model/fileprocessing/PhotoProcessing.ts | 19 +-- src/common/DataStructureVersion.ts | 4 +- src/common/entities/PersonDTO.ts | 6 - test/backend/DBTestHelper.ts | 47 +++--- .../unit/model/sql/AlbumManager.spec.ts | 5 +- .../unit/model/sql/GalleryManager.spec.ts | 12 +- .../unit/model/sql/IndexingManager.spec.ts | 94 ++++++------ .../unit/model/sql/PersonManager.spec.ts | 6 +- .../unit/model/sql/PreviewManager.spec.ts | 24 +-- .../unit/model/sql/SearchManager.spec.ts | 25 +-- 21 files changed, 306 insertions(+), 345 deletions(-) diff --git a/src/backend/middlewares/PersonMWs.ts b/src/backend/middlewares/PersonMWs.ts index 8e49a448..a9abf843 100644 --- a/src/backend/middlewares/PersonMWs.ts +++ b/src/backend/middlewares/PersonMWs.ts @@ -3,9 +3,9 @@ import { ErrorCodes, ErrorDTO } from '../../common/entities/Error'; import { ObjectManagers } from '../model/ObjectManagers'; import { PersonDTO, - PersonWithSampleRegion, } from '../../common/entities/PersonDTO'; import { Utils } from '../../common/Utils'; +import {PersonEntry} from '../model/database/sql/enitites/PersonEntry'; export class PersonMWs { public static async updatePerson( @@ -90,7 +90,7 @@ export class PersonMWs { return next(); } try { - const persons = Utils.clone(req.resultPipe as PersonWithSampleRegion[]); + const persons = Utils.clone(req.resultPipe as PersonEntry[]); for (const item of persons) { delete item.sampleRegion; } diff --git a/src/backend/middlewares/thumbnail/ThumbnailGeneratorMWs.ts b/src/backend/middlewares/thumbnail/ThumbnailGeneratorMWs.ts index 0ae2eb69..583b4db7 100644 --- a/src/backend/middlewares/thumbnail/ThumbnailGeneratorMWs.ts +++ b/src/backend/middlewares/thumbnail/ThumbnailGeneratorMWs.ts @@ -4,6 +4,7 @@ import {NextFunction, Request, Response} from 'express'; import {ErrorCodes, ErrorDTO} from '../../../common/entities/Error'; import {ContentWrapper} from '../../../common/entities/ConentWrapper'; import { + DirectoryPathDTO, ParentDirectoryDTO, SubDirectoryDTO, } from '../../../common/entities/DirectoryDTO'; @@ -12,8 +13,8 @@ import {Config} from '../../../common/config/private/Config'; import {ThumbnailSourceType} from '../../model/threading/PhotoWorker'; import {MediaDTO} from '../../../common/entities/MediaDTO'; import {PhotoProcessing} from '../../model/fileprocessing/PhotoProcessing'; -import {PersonWithSampleRegion} from '../../../common/entities/PersonDTO'; import {ServerTime} from '../ServerTimingMWs'; +import {PersonEntry} from '../../model/database/sql/enitites/PersonEntry'; export class ThumbnailGeneratorMWs { private static ThumbnailMapEntries = @@ -71,7 +72,7 @@ export class ThumbnailGeneratorMWs { try { const size: number = Config.Client.Media.Thumbnail.personThumbnailSize; - const persons: PersonWithSampleRegion[] = req.resultPipe as PersonWithSampleRegion[]; + const persons: PersonEntry[] = req.resultPipe as PersonEntry[]; for (const item of persons) { if (!item.sampleRegion) { @@ -88,7 +89,7 @@ export class ThumbnailGeneratorMWs { // generate thumbnail path const thPath = PhotoProcessing.generatePersonThumbnailPath( mediaPath, - item.sampleRegion, + item.sampleRegion.media.metadata.faces.find(f => f.name === item.name), size ); @@ -115,7 +116,7 @@ export class ThumbnailGeneratorMWs { if (!req.resultPipe) { return next(); } - const person: PersonWithSampleRegion = req.resultPipe as PersonWithSampleRegion; + const person: PersonEntry = req.resultPipe as PersonEntry; try { req.resultPipe = await PhotoProcessing.generatePersonThumbnail(person); return next(); @@ -214,24 +215,24 @@ export class ThumbnailGeneratorMWs { directory: ParentDirectoryDTO | SubDirectoryDTO ): void { if (typeof directory.media !== 'undefined') { - ThumbnailGeneratorMWs.addThInfoToPhotos(directory.media); + ThumbnailGeneratorMWs.addThInfoToPhotos(directory.media, directory); } if (directory.preview) { - ThumbnailGeneratorMWs.addThInfoToAPhoto(directory.preview); + ThumbnailGeneratorMWs.addThInfoToAPhoto(directory.preview, directory); } } - private static addThInfoToPhotos(photos: MediaDTO[]): void { + private static addThInfoToPhotos(photos: MediaDTO[], directory?: DirectoryPathDTO): void { for (let i = 0; i < photos.length; ++i) { - this.addThInfoToAPhoto(photos[i]); + this.addThInfoToAPhoto(photos[i], directory ? directory : photos[i].directory); } } - private static addThInfoToAPhoto(photo: MediaDTO): void { + private static addThInfoToAPhoto(photo: MediaDTO, directory: DirectoryPathDTO): void { const fullMediaPath = path.join( ProjectPath.ImageFolder, - photo.directory.path, - photo.directory.name, + directory.path, + directory.name, photo.name ); for (let i = 0; i < ThumbnailGeneratorMWs.ThumbnailMapEntries.length; ++i) { diff --git a/src/backend/model/database/interfaces/IPersonManager.ts b/src/backend/model/database/interfaces/IPersonManager.ts index 7df1fe3e..00f482bd 100644 --- a/src/backend/model/database/interfaces/IPersonManager.ts +++ b/src/backend/model/database/interfaces/IPersonManager.ts @@ -1,7 +1,6 @@ -import { PersonEntry } from '../sql/enitites/PersonEntry'; -import { PersonDTO } from '../../../../common/entities/PersonDTO'; -import { IObjectManager } from './IObjectManager'; -import { FaceRegion } from '../../../../common/entities/PhotoDTO'; +import {PersonEntry} from '../sql/enitites/PersonEntry'; +import {PersonDTO} from '../../../../common/entities/PersonDTO'; +import {IObjectManager} from './IObjectManager'; export interface IPersonManager extends IObjectManager { getAll(): Promise; @@ -9,7 +8,7 @@ export interface IPersonManager extends IObjectManager { get(name: string): Promise; // saving a Person with a sample region. Person entry cannot exist without a face region - saveAll(person: { name: string; faceRegion: FaceRegion }[]): Promise; + saveAll(person: { name: string; mediaId: number }[]): Promise; updatePerson(name: string, partialPerson: PersonDTO): Promise; diff --git a/src/backend/model/database/memory/PersonManager.ts b/src/backend/model/database/memory/PersonManager.ts index 611b0fed..a14ea4ae 100644 --- a/src/backend/model/database/memory/PersonManager.ts +++ b/src/backend/model/database/memory/PersonManager.ts @@ -7,7 +7,7 @@ export class PersonManager implements IPersonManager { throw new Error('not supported by memory DB'); } - saveAll(person: { name: string; faceRegion: FaceRegion }[]): Promise { + saveAll(person: { name: string; mediaId: number }[]): Promise { throw new Error('not supported by memory DB'); } diff --git a/src/backend/model/database/sql/GalleryManager.ts b/src/backend/model/database/sql/GalleryManager.ts index 44828739..58cc63e0 100644 --- a/src/backend/model/database/sql/GalleryManager.ts +++ b/src/backend/model/database/sql/GalleryManager.ts @@ -1,26 +1,24 @@ -import { IGalleryManager } from '../interfaces/IGalleryManager'; +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 { Connection } 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'; +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 {Connection} from 'typeorm'; +import {MediaEntity} from './enitites/MediaEntity'; +import {VideoEntity} from './enitites/VideoEntity'; +import {DiskMangerWorker} from '../../threading/DiskMangerWorker'; +import {Logger} from '../../../Logger'; +import {ObjectManagers} from '../../ObjectManagers'; +import {DuplicatesDTO} from '../../../../common/entities/DuplicatesDTO'; +import {ReIndexingSensitivity} from '../../../../common/config/private/PrivateConfig'; const LOG_TAG = '[GalleryManager]'; @@ -52,13 +50,11 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager { ); const lastModified = DiskMangerWorker.calcLastModified(stat); - const dir = await this.selectParentDir( - connection, - directoryPath.name, - directoryPath.parent - ); + + const dir = await this.getDirIdAndTime(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 it seems that the content did not change, do not work on it if ( knownLastModified && knownLastScanned && @@ -73,9 +69,9 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager { } if ( Date.now() - dir.lastScanned <= - Config.Server.Indexing.cachedFolderTimeout && + Config.Server.Indexing.cachedFolderTimeout && Config.Server.Indexing.reIndexingSensitivity === - ReIndexingSensitivity.medium + ReIndexingSensitivity.medium ) { return null; } @@ -85,9 +81,9 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager { Logger.silly( LOG_TAG, 'Reindexing reason: lastModified mismatch: known: ' + - dir.lastModified + - ', current:' + - lastModified + dir.lastModified + + ', current:' + + lastModified ); const ret = await ObjectManagers.getInstance().IndexingManager.indexDirectory( @@ -95,7 +91,7 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager { ); 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 + // if subdirectories do not have photos, so cannot show a preview, try getting one from DB await this.fillPreviewForSubDir(connection, subDir); } } @@ -107,25 +103,24 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager { (Date.now() - dir.lastScanned > Config.Server.Indexing.cachedFolderTimeout && Config.Server.Indexing.reIndexingSensitivity >= - ReIndexingSensitivity.medium) || + ReIndexingSensitivity.medium) || Config.Server.Indexing.reIndexingSensitivity >= - ReIndexingSensitivity.high + 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 + (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; + return await this.getParentDirFromId(connection, dir.id); } // never scanned (deep indexed), do it and return with it @@ -145,7 +140,7 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager { async countMediaSize(): Promise { const connection = await SQLConnection.getConnection(); - const { sum } = await connection + const {sum} = await connection .getRepository(MediaEntity) .createQueryBuilder('media') .select('SUM(media.metadata.fileSize)', 'sum') @@ -234,7 +229,7 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager { } } - duplicateParis.push({ media: list }); + duplicateParis.push({media: list}); } }; @@ -318,17 +313,30 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager { dir.isPartial = true; } - protected async selectParentDir( + protected async getDirIdAndTime(connection: Connection, name: string, path: string): Promise<{ id: number, lastScanned: number, lastModified: number }> { + return await connection + .getRepository(DirectoryEntity) + .createQueryBuilder('directory') + .where('directory.name = :name AND directory.path = :path', { + name: name, + path: path, + }) + .select([ + 'directory.id', + 'directory.lastScanned', + 'directory.lastModified', + ]).getOne(); + } + + protected async getParentDirFromId( connection: Connection, - directoryName: string, - directoryParent: string + partialDirId: number ): Promise { const query = connection .getRepository(DirectoryEntity) .createQueryBuilder('directory') - .where('directory.name = :name AND directory.path = :path', { - name: directoryName, - path: directoryParent, + .where('directory.id = :id', { + id: partialDirId }) .leftJoinAndSelect('directory.directories', 'directories') .leftJoinAndSelect('directory.media', 'media') @@ -344,7 +352,7 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager { ]); // TODO: do better filtering - // NOTE: it should not cause an issue as it also do not shave to the DB + // NOTE: it should not cause an issue as it also do not save to the DB if ( Config.Client.MetaFile.gpx === true || Config.Client.MetaFile.pg2conf === true || @@ -353,52 +361,13 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager { query.leftJoinAndSelect('directory.metaFile', 'metaFile'); } - return await query.getOne(); - } - - protected async fillParentDir( - connection: Connection, - dir: ParentDirectoryDTO - ): Promise { - 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 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; - } - } + const dir = await query.getOne(); if (dir.directories) { for (const item of dir.directories) { await this.fillPreviewForSubDir(connection, item); } } + + return dir; } } diff --git a/src/backend/model/database/sql/IndexingManager.ts b/src/backend/model/database/sql/IndexingManager.ts index 3dce212a..4805a6e1 100644 --- a/src/backend/model/database/sql/IndexingManager.ts +++ b/src/backend/model/database/sql/IndexingManager.ts @@ -15,7 +15,6 @@ import {VideoEntity} from './enitites/VideoEntity'; import {FileEntity} from './enitites/FileEntity'; import {FileDTO} from '../../../../common/entities/FileDTO'; import {NotificationManager} from '../../NotifocationManager'; -import {FaceRegionEntry} from './enitites/FaceRegionEntry'; import {ObjectManagers} from '../../ObjectManagers'; import {IIndexingManager} from '../interfaces/IIndexingManager'; import {DiskMangerWorker} from '../../threading/DiskMangerWorker'; @@ -29,6 +28,7 @@ import * as path from 'path'; import * as fs from 'fs'; import {SearchQueryDTO} from '../../../../common/entities/SearchQueryDTO'; import {PersonEntry} from './enitites/PersonEntry'; +import {PersonJunctionTable} from './enitites/PersonJunctionTable'; const LOG_TAG = '[IndexingManager]'; @@ -346,16 +346,16 @@ export class IndexingManager implements IIndexingManager { }) .getMany(); - const mediaChange: any = { - saveP: [], // save/update photo - saveV: [], // save/update video - insertP: [], // insert photo - insertV: [], // insert video + const mediaChange = { + saveP: [] as MediaDTO[], // save/update photo + saveV: [] as MediaDTO[], // save/update video + insertP: [] as MediaDTO[], // insert photo + insertV: [] as MediaDTO[], // insert video }; - const facesPerPhoto: { faces: FaceRegionEntry[]; mediaName: string }[] = []; + const personsPerPhoto: { faces: { name: string, mediaId?: number }[]; mediaName: string }[] = []; // eslint-disable-next-line @typescript-eslint/prefer-for-of for (let i = 0; i < media.length; i++) { - let mediaItem: MediaEntity = null; + let mediaItem: MediaDTO = null; for (let j = 0; j < indexedMedia.length; j++) { if (indexedMedia[j].name === media[i].name) { mediaItem = indexedMedia[j]; @@ -364,7 +364,7 @@ export class IndexingManager implements IIndexingManager { } } - const scannedFaces = (media[i].metadata as PhotoMetadata).faces || []; + const scannedFaces: { name: string }[] = (media[i].metadata as PhotoMetadata).faces || []; if ((media[i].metadata as PhotoMetadata).faces) { // if it has faces, cache them // make the list distinct (some photos may contain the same person multiple times) @@ -374,22 +374,22 @@ export class IndexingManager implements IIndexingManager { ), ]; } - delete (media[i].metadata as PhotoMetadata).faces; // this is a separated DB, lets save separately + if (mediaItem == null) { - // not in DB yet + // Media not in DB yet media[i].directory = null; - mediaItem = Utils.clone(media[i]) as any; + mediaItem = Utils.clone(media[i]); mediaItem.directory = {id: parentDirId} as any; (MediaDTOUtils.isPhoto(mediaItem) ? mediaChange.insertP : mediaChange.insertV ).push(mediaItem); } else { - // already in the DB, only needs to be updated + // Media already in the DB, only needs to be updated delete (mediaItem.metadata as PhotoMetadata).faces; if (!Utils.equalsFilter(mediaItem.metadata, media[i].metadata)) { - mediaItem.metadata = media[i].metadata as any; + mediaItem.metadata = media[i].metadata; (MediaDTOUtils.isPhoto(mediaItem) ? mediaChange.saveP : mediaChange.saveV @@ -397,9 +397,9 @@ export class IndexingManager implements IIndexingManager { } } - facesPerPhoto.push({ - faces: scannedFaces as FaceRegionEntry[], - mediaName: mediaItem.name, + personsPerPhoto.push({ + faces: scannedFaces, + mediaName: mediaItem.name }); } @@ -416,44 +416,44 @@ export class IndexingManager implements IIndexingManager { .select(['media.name', 'media.id']) .getMany(); - const faces: FaceRegionEntry[] = []; - facesPerPhoto.forEach((group): void => { + const persons: { name: string; mediaId: number }[] = []; + personsPerPhoto.forEach((group): void => { const mIndex = indexedMedia.findIndex( (m): boolean => m.name === group.mediaName ); - group.faces.forEach( - (sf: FaceRegionEntry): any => - (sf.media = {id: indexedMedia[mIndex].id} as any) + group.faces.forEach((sf) => + (sf.mediaId = indexedMedia[mIndex].id) ); - faces.push(...group.faces); + persons.push(...group.faces as { name: string; mediaId: number }[]); indexedMedia.splice(mIndex, 1); }); - await this.saveFaces(connection, parentDirId, faces); + await this.savePersonsToMedia(connection, parentDirId, persons); await mediaRepository.remove(indexedMedia); } - protected async saveFaces( + protected async savePersonsToMedia( connection: Connection, parentDirId: number, - scannedFaces: FaceRegion[] + scannedFaces: { name: string; mediaId: number }[] ): Promise { - const faceRepository = connection.getRepository(FaceRegionEntry); + const personJunctionTable = connection.getRepository(PersonJunctionTable); const personRepository = connection.getRepository(PersonEntry); - const persons: { name: string; faceRegion: FaceRegion }[] = []; + const persons: { name: string; mediaId: number }[] = []; + // Make a set for (const face of scannedFaces) { if (persons.findIndex((f) => f.name === face.name) === -1) { - persons.push({name: face.name, faceRegion: face}); + persons.push(face); } } await ObjectManagers.getInstance().PersonManager.saveAll(persons); // get saved persons without triggering denormalized data update (i.e.: do not use PersonManager.get). const savedPersons = await personRepository.find(); - const indexedFaces = await faceRepository + const indexedFaces = await personJunctionTable .createQueryBuilder('face') .leftJoin('face.media', 'media') .where('media.directory = :directory', { @@ -462,19 +462,13 @@ export class IndexingManager implements IIndexingManager { .leftJoinAndSelect('face.person', 'person') .getMany(); - const faceToInsert = []; + const faceToInsert: { person: { id: number }, media: { id: number } }[] = []; // eslint-disable-next-line @typescript-eslint/prefer-for-of for (let i = 0; i < scannedFaces.length; i++) { - // was the face region already indexed - let face: FaceRegionEntry = null; + // was the Person - media connection already indexed + let face: PersonJunctionTable = null; for (let j = 0; j < indexedFaces.length; j++) { - if ( - indexedFaces[j].box.height === scannedFaces[i].box.height && - indexedFaces[j].box.width === scannedFaces[i].box.width && - indexedFaces[j].box.left === scannedFaces[i].box.left && - indexedFaces[j].box.top === scannedFaces[i].box.top && - indexedFaces[j].person.name === scannedFaces[i].name - ) { + if (indexedFaces[j].person.name === scannedFaces[i].name) { face = indexedFaces[j]; indexedFaces.splice(j, 1); break; // region found, stop processing @@ -482,16 +476,18 @@ export class IndexingManager implements IIndexingManager { } if (face == null) { - (scannedFaces[i] as FaceRegionEntry).person = savedPersons.find( - (p) => p.name === scannedFaces[i].name - ); - faceToInsert.push(scannedFaces[i]); + faceToInsert.push({ + person: savedPersons.find( + (p) => p.name === scannedFaces[i].name + ), + media: {id: scannedFaces[i].mediaId} + }); } } if (faceToInsert.length > 0) { - await this.insertChunk(faceRepository, faceToInsert, 100); + await this.insertChunk(personJunctionTable, faceToInsert, 100); } - await faceRepository.remove(indexedFaces, { + await personJunctionTable.remove(indexedFaces, { chunk: Math.max(Math.ceil(indexedFaces.length / 500), 1), }); } diff --git a/src/backend/model/database/sql/PersonManager.ts b/src/backend/model/database/sql/PersonManager.ts index 85d456d3..7cc05cf3 100644 --- a/src/backend/model/database/sql/PersonManager.ts +++ b/src/backend/model/database/sql/PersonManager.ts @@ -1,11 +1,11 @@ -import { SQLConnection } from './SQLConnection'; -import { PersonEntry } from './enitites/PersonEntry'; -import { FaceRegionEntry } from './enitites/FaceRegionEntry'; -import { PersonDTO } from '../../../../common/entities/PersonDTO'; -import { ISQLPersonManager } from './IPersonManager'; -import { Logger } from '../../../Logger'; -import { FaceRegion } from '../../../../common/entities/PhotoDTO'; -import { SQL_COLLATE } from './enitites/EntityUtils'; +import {SQLConnection} from './SQLConnection'; +import {PersonEntry} from './enitites/PersonEntry'; +import {PersonDTO} from '../../../../common/entities/PersonDTO'; +import {ISQLPersonManager} from './IPersonManager'; +import {Logger} from '../../../Logger'; +import {FaceRegion} from '../../../../common/entities/PhotoDTO'; +import {SQL_COLLATE} from './enitites/EntityUtils'; +import {PersonJunctionTable} from './enitites/PersonJunctionTable'; const LOG_TAG = '[PersonManager]'; @@ -20,7 +20,7 @@ export class PersonManager implements ISQLPersonManager { const connection = await SQLConnection.getConnection(); await connection.query( 'UPDATE person_entry SET count = ' + - ' (SELECT COUNT(1) FROM face_region_entry WHERE face_region_entry.personId = person_entry.id)' + ' (SELECT COUNT(1) FROM person_junction_table WHERE person_junction_table.personId = person_entry.id)' ); // remove persons without photo @@ -36,11 +36,11 @@ export class PersonManager implements ISQLPersonManager { const connection = await SQLConnection.getConnection(); await connection.query( 'update person_entry set sampleRegionId = ' + - '(Select face_region_entry.id from media_entity ' + - 'left join face_region_entry on media_entity.id = face_region_entry.mediaId ' + - 'where face_region_entry.personId=person_entry.id ' + - 'order by media_entity.metadataCreationdate desc ' + - 'limit 1)' + '(Select person_junction_table.id from media_entity ' + + 'left join person_junction_table on media_entity.id = person_junction_table.mediaId ' + + 'where person_junction_table.personId=person_entry.id ' + + 'order by media_entity.metadataCreationdate desc ' + + 'limit 1)' ); } @@ -54,7 +54,7 @@ export class PersonManager implements ISQLPersonManager { const person = await repository .createQueryBuilder('person') .limit(1) - .where('person.name LIKE :name COLLATE ' + SQL_COLLATE, { name }) + .where('person.name LIKE :name COLLATE ' + SQL_COLLATE, {name}) .getOne(); if (typeof partialPerson.name !== 'undefined') { @@ -83,8 +83,8 @@ export class PersonManager implements ISQLPersonManager { public async countFaces(): Promise { const connection = await SQLConnection.getConnection(); return await connection - .getRepository(FaceRegionEntry) - .createQueryBuilder('faceRegion') + .getRepository(PersonJunctionTable) + .createQueryBuilder('personJunction') .getCount(); } @@ -96,12 +96,12 @@ export class PersonManager implements ISQLPersonManager { } public async saveAll( - persons: { name: string; faceRegion: FaceRegion }[] + persons: { name: string; mediaId: number }[] ): Promise { - const toSave: { name: string; faceRegion: FaceRegion }[] = []; + const toSave: { name: string; mediaId: number }[] = []; const connection = await SQLConnection.getConnection(); const personRepository = connection.getRepository(PersonEntry); - const faceRegionRepository = connection.getRepository(FaceRegionEntry); + const personJunction = connection.getRepository(PersonJunctionTable); const savedPersons = await personRepository.find(); // filter already existing persons @@ -117,14 +117,13 @@ export class PersonManager implements ISQLPersonManager { if (toSave.length > 0) { for (let i = 0; i < toSave.length / 200; i++) { const saving = toSave.slice(i * 200, (i + 1) * 200); + // saving person const inserted = await personRepository.insert( - saving.map((p) => ({ name: p.name })) + saving.map((p) => ({name: p.name})) ); - // setting Person id - inserted.identifiers.forEach((idObj: { id: number }, j: number) => { - (saving[j].faceRegion as FaceRegionEntry).person = idObj as any; - }); - await faceRegionRepository.insert(saving.map((p) => p.faceRegion)); + // saving junction table + const junctionTable = inserted.identifiers.map((idObj, j) => ({person: idObj, media: {id: saving[j].mediaId}})); + await personJunction.insert(junctionTable); } } this.isDBValid = false; @@ -148,6 +147,7 @@ export class PersonManager implements ISQLPersonManager { 'sampleRegion', 'sampleRegion.media', 'sampleRegion.media.directory', + 'sampleRegion.media.metadata', ], }); } diff --git a/src/backend/model/database/sql/SQLConnection.ts b/src/backend/model/database/sql/SQLConnection.ts index 298b8c2e..cdda3318 100644 --- a/src/backend/model/database/sql/SQLConnection.ts +++ b/src/backend/model/database/sql/SQLConnection.ts @@ -19,7 +19,6 @@ import { MediaEntity } from './enitites/MediaEntity'; import { VideoEntity } from './enitites/VideoEntity'; import { DataStructureVersion } from '../../../../common/DataStructureVersion'; import { FileEntity } from './enitites/FileEntity'; -import { FaceRegionEntry } from './enitites/FaceRegionEntry'; import { PersonEntry } from './enitites/PersonEntry'; import { Utils } from '../../../../common/Utils'; import * as path from 'path'; @@ -31,6 +30,7 @@ import { import { AlbumBaseEntity } from './enitites/album/AlbumBaseEntity'; import { SavedSearchEntity } from './enitites/album/SavedSearchEntity'; import { NotificationManager } from '../../NotifocationManager'; +import {PersonJunctionTable} from './enitites/PersonJunctionTable'; const LOG_TAG = '[SQLConnection]'; @@ -45,7 +45,7 @@ export class SQLConnection { options.entities = [ UserEntity, FileEntity, - FaceRegionEntry, + PersonJunctionTable, PersonEntry, MediaEntity, PhotoEntity, @@ -84,7 +84,7 @@ export class SQLConnection { options.entities = [ UserEntity, FileEntity, - FaceRegionEntry, + PersonJunctionTable, PersonEntry, MediaEntity, PhotoEntity, diff --git a/src/backend/model/database/sql/SearchManager.ts b/src/backend/model/database/sql/SearchManager.ts index e05655b3..02e0d3c3 100644 --- a/src/backend/model/database/sql/SearchManager.ts +++ b/src/backend/model/database/sql/SearchManager.ts @@ -37,19 +37,12 @@ import {FileEntity} from './enitites/FileEntity'; import {SQL_COLLATE} from './enitites/EntityUtils'; export class SearchManager implements ISQLSearchManager { - // This trick enables us to list less rows as faces will be concatenated into one row - // Also typeorm does not support automatic mapping of nested foreign keys - // (i.e: leftJoinAndSelect('media.metadata.faces', 'faces') does not work) - private FACE_SELECT = - Config.Server.Database.type === DatabaseType.mysql - ? 'CONCAT(\'[\' , GROUP_CONCAT( \'{"name": "\' , person.name , \'", "box": {"top":\' , faces.box.top , \', "left":\' , faces.box.left , \', "height":\' , faces.box.height ,\', "width":\' , faces.box.width , \'}}\' ) ,\']\') as media_metadataFaces' - : '\'[\' || GROUP_CONCAT( \'{"name": "\' || person.name || \'", "box": {"top":\' || faces.box.top || \', "left":\' || faces.box.left || \', "height":\' || faces.box.height ||\', "width":\' || faces.box.width || \'}}\' ) ||\']\' as media_metadataFaces'; private DIRECTORY_SELECT = [ 'directory.id', 'directory.name', 'directory.path', ]; - // makes all search query params unique, so typeorm wont mix them + // makes all search query params unique, so typeorm won't mix them private queryIdBase = 0; private static autoCompleteItemsUnique( @@ -293,47 +286,28 @@ export class SearchManager implements ISQLSearchManager { resultOverflow: false, }; - const rawAndEntries = await connection + result.media = await connection .getRepository(MediaEntity) .createQueryBuilder('media') - .select(['media', ...this.DIRECTORY_SELECT, this.FACE_SELECT]) + .select(['media', ...this.DIRECTORY_SELECT]) .where(this.buildWhereQuery(query)) .leftJoin('media.directory', 'directory') - .leftJoin('media.metadata.faces', 'faces') - .leftJoin('faces.person', 'person') .limit(Config.Client.Search.maxMediaResult + 1) - .groupBy('media.id') - .getRawAndEntities(); + .getMany(); - for (let i = 0; i < rawAndEntries.entities.length; ++i) { - if (rawAndEntries.raw[i].media_metadataFaces) { - rawAndEntries.entities[i].metadata.faces = JSON.parse( - rawAndEntries.raw[i].media_metadataFaces - ); - } - } - - result.media = rawAndEntries.entities; if (result.media.length > Config.Client.Search.maxMediaResult) { result.resultOverflow = true; } + if (Config.Client.Search.listMetafiles === true) { + const dIds = Array.from(new Set(result.media.map(m => (m.directory as unknown as { id: number }).id))); result.metaFile = await connection .getRepository(FileEntity) .createQueryBuilder('file') .select(['file', ...this.DIRECTORY_SELECT]) - .innerJoin( - (q) => - q - .from(MediaEntity, 'media') - .select('distinct directory.id') - .where(this.buildWhereQuery(query)) - .leftJoin('media.directory', 'directory'), - 'dir', - 'file.directory=dir.id' - ) + .where(`file.directoryId IN(${dIds})`) .leftJoin('file.directory', 'directory') .getMany(); } diff --git a/src/backend/model/database/sql/enitites/MediaEntity.ts b/src/backend/model/database/sql/enitites/MediaEntity.ts index d56f43c5..bee31ffe 100644 --- a/src/backend/model/database/sql/enitites/MediaEntity.ts +++ b/src/backend/model/database/sql/enitites/MediaEntity.ts @@ -14,10 +14,10 @@ import { MediaDTO, MediaMetadata, } from '../../../../../common/entities/MediaDTO'; -import { FaceRegionEntry } from './FaceRegionEntry'; +import { PersonJunctionTable} from './PersonJunctionTable'; import { columnCharsetCS } from './EntityUtils'; import { - CameraMetadata, + CameraMetadata, FaceRegion, GPSMetadata, PositionMetaData, } from '../../../../../common/entities/PhotoDTO'; @@ -117,6 +117,7 @@ export class MediaMetadataEntity implements MediaMetadata { to: (v) => v, }, }) + @Index() creationDate: number; @Column('int', { unsigned: true }) @@ -136,10 +137,18 @@ export class MediaMetadataEntity implements MediaMetadata { positionData: PositionMetaDataEntity; @Column('tinyint', { unsigned: true }) + @Index() rating: 0 | 1 | 2 | 3 | 4 | 5; - @OneToMany((type) => FaceRegionEntry, (faceRegion) => faceRegion.media) - faces: FaceRegionEntry[]; + @OneToMany((type) => PersonJunctionTable, (junctionTable) => junctionTable.media) + personJunction: PersonJunctionTable[]; + + @Column({ + type:'simple-json', + nullable: true, + charset: columnCharsetCS.charset, + collation: columnCharsetCS.collation}) + faces: FaceRegion[]; /** * Caches the list of persons. Only used for searching diff --git a/src/backend/model/database/sql/enitites/PersonEntry.ts b/src/backend/model/database/sql/enitites/PersonEntry.ts index 777725dc..a5e1fa64 100644 --- a/src/backend/model/database/sql/enitites/PersonEntry.ts +++ b/src/backend/model/database/sql/enitites/PersonEntry.ts @@ -7,32 +7,35 @@ import { PrimaryGeneratedColumn, Unique, } from 'typeorm'; -import { FaceRegionEntry } from './FaceRegionEntry'; -import { columnCharsetCS } from './EntityUtils'; -import { PersonWithSampleRegion } from '../../../../../common/entities/PersonDTO'; +import {PersonJunctionTable} from './PersonJunctionTable'; +import {columnCharsetCS} from './EntityUtils'; +import { PersonDTO } from '../../../../../common/entities/PersonDTO'; @Entity() @Unique(['name']) -export class PersonEntry implements PersonWithSampleRegion { +export class PersonEntry implements PersonDTO { @Index() - @PrimaryGeneratedColumn({ unsigned: true }) + @PrimaryGeneratedColumn({unsigned: true}) id: number; @Column(columnCharsetCS) name: string; - @Column('int', { unsigned: true, default: 0 }) + @Column('int', {unsigned: true, default: 0}) count: number; - @Column({ default: false }) + @Column({default: false}) isFavourite: boolean; - @OneToMany((type) => FaceRegionEntry, (faceRegion) => faceRegion.person) - public faces: FaceRegionEntry[]; + @OneToMany((type) => PersonJunctionTable, (junctionTable) => junctionTable.person) + public faces: PersonJunctionTable[]; - @ManyToOne((type) => FaceRegionEntry, { + @ManyToOne((type) => PersonJunctionTable, { onDelete: 'SET NULL', nullable: true, }) - sampleRegion: FaceRegionEntry; + sampleRegion: PersonJunctionTable; + + // does not store in the DB, temporal field + missingThumbnail?: boolean; } diff --git a/src/backend/model/fileprocessing/PhotoProcessing.ts b/src/backend/model/fileprocessing/PhotoProcessing.ts index 0c26e7de..cf33c672 100644 --- a/src/backend/model/fileprocessing/PhotoProcessing.ts +++ b/src/backend/model/fileprocessing/PhotoProcessing.ts @@ -12,7 +12,7 @@ import { import {ITaskExecuter, TaskExecuter} from '../threading/TaskExecuter'; import {FaceRegion, PhotoDTO} from '../../../common/entities/PhotoDTO'; import {SupportedFormats} from '../../../common/SupportedFormats'; -import {PersonWithSampleRegion} from '../../../common/entities/PersonDTO'; +import {PersonEntry} from '../database/sql/enitites/PersonEntry'; export class PhotoProcessing { private static initDone = false; @@ -47,7 +47,7 @@ export class PhotoProcessing { } public static async generatePersonThumbnail( - person: PersonWithSampleRegion + person: PersonEntry ): Promise { // load parameters const photo: PhotoDTO = person.sampleRegion.media; @@ -58,10 +58,11 @@ export class PhotoProcessing { photo.name ); const size: number = Config.Client.Media.Thumbnail.personThumbnailSize; + const faceRegion = person.sampleRegion.media.metadata.faces.find(f => f.name === person.name); // generate thumbnail path const thPath = PhotoProcessing.generatePersonThumbnailPath( mediaPath, - person.sampleRegion, + faceRegion, size ); @@ -75,11 +76,11 @@ export class PhotoProcessing { const margin = { x: Math.round( - person.sampleRegion.box.width * + faceRegion.box.width * Config.Server.Media.Thumbnail.personFaceMargin ), y: Math.round( - person.sampleRegion.box.height * + faceRegion.box.height * Config.Server.Media.Thumbnail.personFaceMargin ), }; @@ -93,13 +94,13 @@ export class PhotoProcessing { makeSquare: false, cut: { left: Math.round( - Math.max(0, person.sampleRegion.box.left - margin.x / 2) + Math.max(0, faceRegion.box.left - margin.x / 2) ), top: Math.round( - Math.max(0, person.sampleRegion.box.top - margin.y / 2) + Math.max(0, faceRegion.box.top - margin.y / 2) ), - width: person.sampleRegion.box.width + margin.x, - height: person.sampleRegion.box.height + margin.y, + width: faceRegion.box.width + margin.x, + height: faceRegion.box.height + margin.y, }, useLanczos3: Config.Server.Media.Thumbnail.useLanczos3, quality: Config.Server.Media.Thumbnail.quality, diff --git a/src/common/DataStructureVersion.ts b/src/common/DataStructureVersion.ts index 1352d623..15f48ded 100644 --- a/src/common/DataStructureVersion.ts +++ b/src/common/DataStructureVersion.ts @@ -1,4 +1,4 @@ /** - * This version indicates that the SQL sql/entities/*Entity.ts files got changed and the db needs to be recreated + * This version indicates that the sql/entities/*Entity.ts files got changed and the db needs to be recreated */ -export const DataStructureVersion = 28; +export const DataStructureVersion = 30; diff --git a/src/common/entities/PersonDTO.ts b/src/common/entities/PersonDTO.ts index 5a7ea14c..822ecad2 100644 --- a/src/common/entities/PersonDTO.ts +++ b/src/common/entities/PersonDTO.ts @@ -1,9 +1,3 @@ -import { FaceRegionEntry } from '../../backend/model/database/sql/enitites/FaceRegionEntry'; - -export interface PersonWithSampleRegion extends PersonDTO { - sampleRegion: FaceRegionEntry; -} - export interface PersonDTO { id: number; name: string; diff --git a/test/backend/DBTestHelper.ts b/test/backend/DBTestHelper.ts index eabf184c..ad09745d 100644 --- a/test/backend/DBTestHelper.ts +++ b/test/backend/DBTestHelper.ts @@ -14,6 +14,7 @@ import {Utils} from '../../src/common/Utils'; import {TestHelper} from '../TestHelper'; import {VideoDTO} from '../../src/common/entities/VideoDTO'; import {PhotoDTO} from '../../src/common/entities/PhotoDTO'; +import {Logger} from '../../src/backend/Logger'; declare let describe: any; const savedDescribe = describe; @@ -27,15 +28,18 @@ class IndexingManagerTest extends IndexingManager { class GalleryManagerTest extends GalleryManager { - public async selectParentDir(connection: Connection, directoryName: string, directoryParent: string): Promise { - return super.selectParentDir(connection, directoryName, directoryParent); + public async getDirIdAndTime(connection: Connection, directoryName: string, directoryParent: string) { + return super.getDirIdAndTime(connection, directoryName, directoryParent); } - public async fillParentDir(connection: Connection, dir: ParentDirectoryDTO): Promise { - return super.fillParentDir(connection, dir); + public async getParentDirFromId(connection: Connection, dir: number): Promise { + return super.getParentDirFromId(connection, dir); } + } +const LOG_TAG = 'DBTestHelper'; + export class DBTestHelper { static enable = { @@ -44,7 +48,7 @@ export class DBTestHelper { mysql: process.env.TEST_MYSQL !== 'false' }; public static readonly savedDescribe = savedDescribe; - tempDir: string; + public tempDir: string; public readonly testGalleyEntities: { dir: ParentDirectoryDTO, subDir: SubDirectoryDTO, @@ -101,7 +105,6 @@ export class DBTestHelper { const helper = new DBTestHelper(DatabaseType.mysql); savedDescribe('mysql', function(): void { this.timeout(99999999); // hint for the test environment - // @ts-ignore return tests(helper); }); } @@ -132,14 +135,15 @@ export class DBTestHelper { } const gm = new GalleryManagerTest(); - const dir = await gm.selectParentDir(connection, directory.name, path.join(path.dirname('.'), path.sep)); - await gm.fillParentDir(connection, dir); + + const dir = await gm.getParentDirFromId(connection, + (await gm.getDirIdAndTime(connection, directory.name, path.join(path.dirname('.'), path.sep))).id); const populateDir = async (d: DirectoryBaseDTO) => { for (let i = 0; i < d.directories.length; i++) { - d.directories[i] = await gm.selectParentDir(connection, d.directories[i].name, - path.join(DiskMangerWorker.pathFromParent(d), path.sep)); - await gm.fillParentDir(connection, d.directories[i] as any); + d.directories[i] = await gm.getParentDirFromId(connection, + (await gm.getDirIdAndTime(connection, d.directories[i].name, + path.join(DiskMangerWorker.pathFromParent(d), path.sep))).id); await populateDir(d.directories[i]); } }; @@ -184,10 +188,15 @@ export class DBTestHelper { this.testGalleyEntities.subDir = this.testGalleyEntities.dir.directories[0]; this.testGalleyEntities.subDir2 = this.testGalleyEntities.dir.directories[1]; this.testGalleyEntities.p = (this.testGalleyEntities.dir.media.filter(m => m.name === this.testGalleyEntities.p.name)[0] as any); + this.testGalleyEntities.p.directory = this.testGalleyEntities.dir; this.testGalleyEntities.p2 = (this.testGalleyEntities.dir.media.filter(m => m.name === this.testGalleyEntities.p2.name)[0] as any); + this.testGalleyEntities.p2.directory = this.testGalleyEntities.dir; this.testGalleyEntities.v = (this.testGalleyEntities.dir.media.filter(m => m.name === this.testGalleyEntities.v.name)[0] as any); + this.testGalleyEntities.v.directory = this.testGalleyEntities.dir; this.testGalleyEntities.p3 = (this.testGalleyEntities.dir.directories[0].media[0] as any); + this.testGalleyEntities.p3.directory = this.testGalleyEntities.dir.directories[0]; this.testGalleyEntities.p4 = (this.testGalleyEntities.dir.directories[1].media[0] as any); + this.testGalleyEntities.p2.directory = this.testGalleyEntities.dir.directories[1]; } private async initMySQL(): Promise { @@ -195,20 +204,20 @@ export class DBTestHelper { } private async resetMySQL(): Promise { - await ObjectManagers.reset(); - Config.Server.Database.type = DatabaseType.mysql; - Config.Server.Database.mysql.database = 'pigallery2_test'; + Logger.debug(LOG_TAG, 'resetting up mysql'); + await this.clearUpMysql(); const conn = await SQLConnection.getConnection(); - await conn.query('DROP DATABASE IF EXISTS ' + conn.options.database); await conn.query('CREATE DATABASE IF NOT EXISTS ' + conn.options.database); await SQLConnection.close(); await ObjectManagers.InitSQLManagers(); } private async clearUpMysql(): Promise { + Logger.debug(LOG_TAG, 'clearing up mysql'); await ObjectManagers.reset(); Config.Server.Database.type = DatabaseType.mysql; Config.Server.Database.mysql.database = 'pigallery2_test'; + await fs.promises.rm(this.tempDir, {recursive: true, force: true}); const conn = await SQLConnection.getConnection(); await conn.query('DROP DATABASE IF EXISTS ' + conn.options.database); await SQLConnection.close(); @@ -219,15 +228,13 @@ export class DBTestHelper { } private async resetSQLite(): Promise { - Config.Server.Database.type = DatabaseType.sqlite; - Config.Server.Database.dbFolder = this.tempDir; - ProjectPath.reset(); - await ObjectManagers.reset(); - await fs.promises.rm(this.tempDir, {recursive: true, force: true}); + Logger.debug(LOG_TAG, 'resetting sqlite'); + await this.clearUpSQLite(); await ObjectManagers.InitSQLManagers(); } private async clearUpSQLite(): Promise { + Logger.debug(LOG_TAG, 'clearing up sqlite'); Config.Server.Database.type = DatabaseType.sqlite; Config.Server.Database.dbFolder = this.tempDir; ProjectPath.reset(); diff --git a/test/backend/unit/model/sql/AlbumManager.spec.ts b/test/backend/unit/model/sql/AlbumManager.spec.ts index 63485276..c2976f5a 100644 --- a/test/backend/unit/model/sql/AlbumManager.spec.ts +++ b/test/backend/unit/model/sql/AlbumManager.spec.ts @@ -1,9 +1,6 @@ import {DBTestHelper} from '../../../DBTestHelper'; import {ParentDirectoryDTO, SubDirectoryDTO} from '../../../../../src/common/entities/DirectoryDTO'; -import {TestHelper} from '../../../../TestHelper'; import {ObjectManagers} from '../../../../../src/backend/model/ObjectManagers'; -import {PhotoDTO} from '../../../../../src/common/entities/PhotoDTO'; -import {VideoDTO} from '../../../../../src/common/entities/VideoDTO'; import {AlbumManager} from '../../../../../src/backend/model/database/sql/AlbumManager'; import {SearchQueryTypes, TextSearch} from '../../../../../src/common/entities/SearchQueryDTO'; import {SQLConnection} from '../../../../../src/backend/model/database/sql/SQLConnection'; @@ -13,7 +10,9 @@ import {MediaDTO} from '../../../../../src/common/entities/MediaDTO'; import {SavedSearchDTO} from '../../../../../src/common/entities/album/SavedSearchDTO'; +// eslint-disable-next-line @typescript-eslint/no-var-requires const deepEqualInAnyOrder = require('deep-equal-in-any-order'); +// eslint-disable-next-line @typescript-eslint/no-var-requires const chai = require('chai'); chai.use(deepEqualInAnyOrder); diff --git a/test/backend/unit/model/sql/GalleryManager.spec.ts b/test/backend/unit/model/sql/GalleryManager.spec.ts index 06fd7ace..225dbc67 100644 --- a/test/backend/unit/model/sql/GalleryManager.spec.ts +++ b/test/backend/unit/model/sql/GalleryManager.spec.ts @@ -6,7 +6,9 @@ import {DirectoryEntity} from '../../../../../src/backend/model/database/sql/eni import {ParentDirectoryDTO} from '../../../../../src/common/entities/DirectoryDTO'; import {Connection} from 'typeorm'; +// eslint-disable-next-line @typescript-eslint/no-var-requires const deepEqualInAnyOrder = require('deep-equal-in-any-order'); +// eslint-disable-next-line @typescript-eslint/no-var-requires const chai = require('chai'); chai.use(deepEqualInAnyOrder); @@ -22,15 +24,13 @@ describe = DBTestHelper.describe(); class GalleryManagerTest extends GalleryManager { - - public async selectParentDir(connection: Connection, directoryName: string, directoryParent: string): Promise { - return super.selectParentDir(connection, directoryName, directoryParent); + public async getDirIdAndTime(connection: Connection, directoryName: string, directoryParent: string) { + return super.getDirIdAndTime(connection, directoryName, directoryParent); } - public async fillParentDir(connection: Connection, dir: ParentDirectoryDTO): Promise { - return super.fillParentDir(connection, dir); + public async getParentDirFromId(connection: Connection, dir: number): Promise { + return super.getParentDirFromId(connection, dir); } - } describe('GalleryManager', (sqlHelper: DBTestHelper) => { diff --git a/test/backend/unit/model/sql/IndexingManager.spec.ts b/test/backend/unit/model/sql/IndexingManager.spec.ts index 165f0140..0f578e8e 100644 --- a/test/backend/unit/model/sql/IndexingManager.spec.ts +++ b/test/backend/unit/model/sql/IndexingManager.spec.ts @@ -5,7 +5,6 @@ import {GalleryManager} from '../../../../../src/backend/model/database/sql/Gall import {DirectoryBaseDTO, DirectoryDTOUtils, ParentDirectoryDTO} from '../../../../../src/common/entities/DirectoryDTO'; import {TestHelper} from '../../../../TestHelper'; import {Connection} from 'typeorm'; -import {DirectoryEntity} from '../../../../../src/backend/model/database/sql/enitites/DirectoryEntity'; import {Utils} from '../../../../../src/common/Utils'; import {MediaDTO} from '../../../../../src/common/entities/MediaDTO'; import {FileDTO} from '../../../../../src/common/entities/FileDTO'; @@ -13,7 +12,7 @@ import {IndexingManager} from '../../../../../src/backend/model/database/sql/Ind import {ObjectManagers} from '../../../../../src/backend/model/ObjectManagers'; import {DBTestHelper} from '../../../DBTestHelper'; import {DiskMangerWorker} from '../../../../../src/backend/model/threading/DiskMangerWorker'; -import {ReIndexingSensitivity} from '../../../../../src/common/config/private/PrivateConfig'; +import {ReIndexingSensitivity, SQLLogLevel} from '../../../../../src/common/config/private/PrivateConfig'; import {SearchQueryTypes, TextSearch, TextSearchQueryMatchTypes} from '../../../../../src/common/entities/SearchQueryDTO'; import {ProjectPath} from '../../../../../src/backend/ProjectPath'; import * as path from 'path'; @@ -31,15 +30,13 @@ const {expect} = chai; class GalleryManagerTest extends GalleryManager { - - public async selectParentDir(connection: Connection, directoryName: string, directoryParent: string): Promise { - return super.selectParentDir(connection, directoryName, directoryParent); + public async getDirIdAndTime(connection: Connection, directoryName: string, directoryParent: string) { + return super.getDirIdAndTime(connection, directoryName, directoryParent); } - public async fillParentDir(connection: Connection, dir: ParentDirectoryDTO): Promise { - return super.fillParentDir(connection, dir); + public async getParentDirFromId(connection: Connection, dir: number): Promise { + return super.getParentDirFromId(connection, dir); } - } class IndexingManagerTest extends IndexingManager { @@ -149,8 +146,8 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { await im.saveToDB(Utils.clone(parent) as ParentDirectoryDTO); const conn = await SQLConnection.getConnection(); - const selected = await gm.selectParentDir(conn, parent.name, parent.path); - await gm.fillParentDir(conn, selected); + const selected = await gm.getParentDirFromId(conn, + (await gm.getDirIdAndTime(conn, parent.name, parent.path)).id); DirectoryDTOUtils.removeReferences(selected); @@ -175,14 +172,16 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { '.' ); const conn = await SQLConnection.getConnection(); - const selected = await gm.selectParentDir(conn, directoryPath.name, - directoryPath.parent); - await gm.fillParentDir(conn, selected); + const selected = await gm.getParentDirFromId(conn, + (await gm.getDirIdAndTime(conn, directoryPath.name, directoryPath.parent)).id); expect(selected?.media?.length) .to.be.greaterThan(0); - const tmpDir = path.join(__dirname, '/../../../tmp/rnd5sdf_emptyDir'); + if (!fs.existsSync(sqlHelper.tempDir)) { + fs.mkdirSync(sqlHelper.tempDir); + } + const tmpDir = path.join(sqlHelper.tempDir, '/rnd5sdf_emptyDir'); fs.mkdirSync(tmpDir); ProjectPath.ImageFolder = tmpDir; let notFailed = false; @@ -214,8 +213,9 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { await im.saveToDB(Utils.clone(parent) as ParentDirectoryDTO); const conn = await SQLConnection.getConnection(); - const selected = await gm.selectParentDir(conn, parent.name, parent.path); - await gm.fillParentDir(conn, selected); + + const selected = await gm.getParentDirFromId(conn, + (await gm.getDirIdAndTime(conn, parent.name, parent.path)).id); DirectoryDTOUtils.removeReferences(selected); removeIds(selected); @@ -244,8 +244,8 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { const conn = await SQLConnection.getConnection(); { - const selected = await gm.selectParentDir(conn, parent1.name, parent1.path); - await gm.fillParentDir(conn, selected); + const selected = await gm.getParentDirFromId(conn, + (await gm.getDirIdAndTime(conn, parent1.name, parent1.path)).id); DirectoryDTOUtils.removeReferences(selected); removeIds(selected); @@ -254,8 +254,8 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { .to.deep.equalInAnyOrder(Utils.removeNullOrEmptyObj(indexifyReturn(parent1))); } { - const selected = await gm.selectParentDir(conn, parent2.name, parent2.path); - await gm.fillParentDir(conn, selected); + const selected = await gm.getParentDirFromId(conn, + (await gm.getDirIdAndTime(conn, parent2.name, parent2.path)).id); DirectoryDTOUtils.removeReferences(selected); removeIds(selected); @@ -277,8 +277,9 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { await im.saveToDB(Utils.clone(parent) as ParentDirectoryDTO); const conn = await SQLConnection.getConnection(); - const selected = await gm.selectParentDir(conn, parent.name, parent.path); - await gm.fillParentDir(conn, selected); + + const selected = await gm.getParentDirFromId(conn, + (await gm.getDirIdAndTime(conn, parent.name, parent.path)).id); DirectoryDTOUtils.removeReferences(selected); @@ -290,8 +291,8 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { it('should select preview', async () => { const selectDirectory = async (gmTest: GalleryManagerTest, dir: DirectoryBaseDTO): Promise => { const conn = await SQLConnection.getConnection(); - const selected = await gmTest.selectParentDir(conn, dir.name, dir.path); - await gmTest.fillParentDir(conn, selected); + const selected = await gmTest.getParentDirFromId(conn, + (await gmTest.getDirIdAndTime(conn, dir.name, dir.path)).id); DirectoryDTOUtils.removeReferences(selected); removeIds(selected); @@ -369,8 +370,9 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { await im.saveToDB(Utils.clone(parent) as ParentDirectoryDTO); const conn = await SQLConnection.getConnection(); - const selected = await gm.selectParentDir(conn, parent.name, parent.path); - await gm.fillParentDir(conn, selected); + + const selected = await gm.getParentDirFromId(conn, + (await gm.getDirIdAndTime(conn, parent.name, parent.path)).id); DirectoryDTOUtils.removeReferences(selected); removeIds(selected); @@ -407,8 +409,8 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { await im.saveToDB(Utils.clone(parent) as ParentDirectoryDTO); const conn = await SQLConnection.getConnection(); - const selected = await gm.selectParentDir(conn, parent.name, parent.path); - await gm.fillParentDir(conn, selected); + const selected = await gm.getParentDirFromId(conn, + (await gm.getDirIdAndTime(conn, parent.name, parent.path)).id); DirectoryDTOUtils.removeReferences(selected); removeIds(selected); @@ -437,8 +439,8 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { await im.saveToDB(Utils.clone(parent) as ParentDirectoryDTO); const conn = await SQLConnection.getConnection(); - const selected = await gm.selectParentDir(conn, parent.name, parent.path); - await gm.fillParentDir(conn, selected); + const selected = await gm.getParentDirFromId(conn, + (await gm.getDirIdAndTime(conn, parent.name, parent.path)).id); DirectoryDTOUtils.removeReferences(selected); removeIds(selected); @@ -473,8 +475,8 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { await im.saveToDB(Utils.clone(parent) as ParentDirectoryDTO); const conn = await SQLConnection.getConnection(); - const selected = await gm.selectParentDir(conn, parent.name, parent.path); - await gm.fillParentDir(conn, selected); + const selected = await gm.getParentDirFromId(conn, + (await gm.getDirIdAndTime(conn, parent.name, parent.path)).id); DirectoryDTOUtils.removeReferences(selected); removeIds(selected); @@ -499,8 +501,8 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { Config.Client.MetaFile.markdown = false; Config.Client.MetaFile.pg2conf = false; const conn = await SQLConnection.getConnection(); - const selected = await gm.selectParentDir(conn, parent.name, parent.path); - await gm.fillParentDir(conn, selected); + const selected = await gm.getParentDirFromId(conn, + (await gm.getDirIdAndTime(conn, parent.name, parent.path)).id); delete parent.metaFile; DirectoryDTOUtils.removeReferences(selected); @@ -530,8 +532,8 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { await im.saveToDB(Utils.clone(subDir) as ParentDirectoryDTO); const conn = await SQLConnection.getConnection(); - const selected = await gm.selectParentDir(conn, subDir.name, subDir.path); - await gm.fillParentDir(conn, selected); + const selected = await gm.getParentDirFromId(conn, + (await gm.getDirIdAndTime(conn, subDir.name, subDir.path)).id); // subDir.isPartial = true; // delete subDir.directories; @@ -570,8 +572,8 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { await Promise.all([s1, s2, s3]); - const selected = await gm.selectParentDir(conn, parent.name, parent.path); - await gm.fillParentDir(conn, selected); + const selected = await gm.getParentDirFromId(conn, + (await gm.getDirIdAndTime(conn, parent.name, parent.path)).id); DirectoryDTOUtils.removeReferences(selected); removeIds(selected); @@ -595,8 +597,8 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { await im.saveToDB(Utils.clone(parent) as ParentDirectoryDTO); const conn = await SQLConnection.getConnection(); - const selected = await gm.selectParentDir(conn, parent.name, parent.path); - await gm.fillParentDir(conn, selected); + const selected = await gm.getParentDirFromId(conn, + (await gm.getDirIdAndTime(conn, parent.name, parent.path)).id); DirectoryDTOUtils.removeReferences(selected); removeIds(selected); @@ -604,7 +606,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { .to.deep.equal(Utils.removeNullOrEmptyObj(indexifyReturn(parent))); await im.resetDB(); - const selectReset = await gm.selectParentDir(conn, parent.name, parent.path); + const selectReset = await gm.getDirIdAndTime(conn, parent.name, parent.path); expect(selectReset).to.deep.equal(null); }); @@ -627,8 +629,8 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { DirectoryDTOUtils.removeReferences(parent); await im.saveToDB(subDir as ParentDirectoryDTO); - - const selected = await gm.selectParentDir(conn, subDir.name, subDir.path); + const selected = await gm.getParentDirFromId(conn, + (await gm.getDirIdAndTime(conn, subDir.name, subDir.path)).id); expect(selected.media.length).to.equal(subDir.media.length); }) as any).timeout(40000); @@ -661,11 +663,11 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { // @ts-ignore fs.statSync = () => ({ctime: new Date(dirTime), mtime: new Date(dirTime)}); const gm = new GalleryManagerTest(); - gm.selectParentDir = (connection: Connection, directoryName: string, directoryParent: string) => { + gm.getDirIdAndTime = () => { return Promise.resolve(indexedTime as any); }; - gm.fillParentDir = (connection: Connection, dir: DirectoryEntity) => { - return Promise.resolve(); + gm.getParentDirFromId = ():Promise => { + return Promise.resolve(indexedTime) as unknown as Promise; }; ObjectManagers.getInstance().IndexingManager.indexDirectory = (...args) => { diff --git a/test/backend/unit/model/sql/PersonManager.spec.ts b/test/backend/unit/model/sql/PersonManager.spec.ts index f2cae330..5e982835 100644 --- a/test/backend/unit/model/sql/PersonManager.spec.ts +++ b/test/backend/unit/model/sql/PersonManager.spec.ts @@ -4,7 +4,6 @@ import {DBTestHelper} from '../../../DBTestHelper'; import {TestHelper} from '../../../../TestHelper'; import {PhotoDTO} from '../../../../../src/common/entities/PhotoDTO'; import {Utils} from '../../../../../src/common/Utils'; -import {PersonWithSampleRegion} from '../../../../../src/common/entities/PersonDTO'; import {ParentDirectoryDTO} from '../../../../../src/common/entities/DirectoryDTO'; import {VideoDTO} from '../../../../../src/common/entities/VideoDTO'; import {SQLConnection} from '../../../../../src/backend/model/database/sql/SQLConnection'; @@ -18,6 +17,7 @@ declare const before: any; declare const it: any; +// eslint-disable-next-line prefer-const describe = DBTestHelper.describe(); describe('PersonManager', (sqlHelper: DBTestHelper) => { @@ -30,7 +30,7 @@ describe('PersonManager', (sqlHelper: DBTestHelper) => { let p2: PhotoDTO; let pFaceLess: PhotoDTO; - let savedPerson: PersonWithSampleRegion[] = []; + let savedPerson: PersonEntry[] = []; const setUpSqlDB = async () => { await sqlHelper.initDB(); @@ -73,7 +73,7 @@ describe('PersonManager', (sqlHelper: DBTestHelper) => { person.count = 1; expect(selected).to.deep.equal(person); - expect((await pm.get('Boba Fett') as PersonWithSampleRegion).sampleRegion.media.name).to.deep.equal(p.name); + expect((await pm.get('Boba Fett') as PersonEntry).sampleRegion.media.name).to.deep.equal(p.name); }); diff --git a/test/backend/unit/model/sql/PreviewManager.spec.ts b/test/backend/unit/model/sql/PreviewManager.spec.ts index c3cb8e25..63433243 100644 --- a/test/backend/unit/model/sql/PreviewManager.spec.ts +++ b/test/backend/unit/model/sql/PreviewManager.spec.ts @@ -17,7 +17,9 @@ import {Utils} from '../../../../../src/common/Utils'; import {SQLConnection} from '../../../../../src/backend/model/database/sql/SQLConnection'; import {DirectoryEntity} from '../../../../../src/backend/model/database/sql/enitites/DirectoryEntity'; +// eslint-disable-next-line @typescript-eslint/no-var-requires const deepEqualInAnyOrder = require('deep-equal-in-any-order'); +// eslint-disable-next-line @typescript-eslint/no-var-requires const chai = require('chai'); chai.use(deepEqualInAnyOrder); @@ -48,12 +50,12 @@ class SearchManagerTest extends SearchManager { class GalleryManagerTest extends GalleryManager { - public async selectParentDir(connection: Connection, directoryName: string, directoryParent: string): Promise { - return super.selectParentDir(connection, directoryName, directoryParent); + public async getDirIdAndTime(connection: Connection, directoryName: string, directoryParent: string) { + return super.getDirIdAndTime(connection, directoryName, directoryParent); } - public async fillParentDir(connection: Connection, dir: ParentDirectoryDTO): Promise { - return super.fillParentDir(connection, dir); + public async getParentDirFromId(connection: Connection, dir: number): Promise { + return super.getParentDirFromId(connection, dir); } } @@ -66,7 +68,6 @@ describe('PreviewManager', (sqlHelper: DBTestHelper) => { * |- v * |- p * |- p2 - * |- gpx * |-> subDir2 * |- p4 */ @@ -79,7 +80,6 @@ describe('PreviewManager', (sqlHelper: DBTestHelper) => { let p2: PhotoDTO; let pFaceLess: PhotoDTO; let p4: PhotoDTO; - let gpx: FileDTO; const setUpTestGallery = async (): Promise => { @@ -94,7 +94,6 @@ describe('PreviewManager', (sqlHelper: DBTestHelper) => { p2.metadata.creationDate = 20000; v = TestHelper.getVideoEntry1(subDir); v.metadata.creationDate = 500; - gpx = TestHelper.getRandomizedGPXEntry(subDir); const pFaceLessTmp = TestHelper.getPhotoEntry3(subDir); pFaceLessTmp.metadata.rating = 0; pFaceLessTmp.metadata.creationDate = 400000; @@ -108,11 +107,15 @@ describe('PreviewManager', (sqlHelper: DBTestHelper) => { subDir = dir.directories[0]; subDir2 = dir.directories[1]; p = (subDir.media.filter(m => m.name === p.name)[0] as any); + p.directory = subDir; p2 = (subDir.media.filter(m => m.name === p2.name)[0] as any); - gpx = (subDir.metaFile[0] as any); + p2.directory = subDir; v = (subDir.media.filter(m => m.name === v.name)[0] as any); + v.directory = subDir; pFaceLess = (subDir.media.filter(m => m.name === pFaceLessTmp.name)[0] as any); + pFaceLess.directory = subDir; p4 = (subDir2.media[0] as any); + p4.directory = subDir2; }; const setUpSqlDB = async () => { @@ -274,8 +277,9 @@ describe('PreviewManager', (sqlHelper: DBTestHelper) => { .set({validPreview: false, preview: null}).execute(); expect((await selectDir()).preview).to.equal(null); - const res = await gm.selectParentDir(conn, dir.name, dir.path); - await gm.fillParentDir(conn, res); + + const res = await gm.getParentDirFromId(conn, + (await gm.getDirIdAndTime(conn, dir.name, dir.path)).id); subdir = await selectDir(); expect(subdir.validPreview).to.equal(true); expect(subdir.preview.id).to.equal(p2.id); diff --git a/test/backend/unit/model/sql/SearchManager.spec.ts b/test/backend/unit/model/sql/SearchManager.spec.ts index a7d15841..67b819b1 100644 --- a/test/backend/unit/model/sql/SearchManager.spec.ts +++ b/test/backend/unit/model/sql/SearchManager.spec.ts @@ -34,7 +34,9 @@ import {Config} from '../../../../../src/common/config/private/Config'; import {SearchQueryParser} from '../../../../../src/common/SearchQueryParser'; import {FileDTO} from '../../../../../src/common/entities/FileDTO'; +// eslint-disable-next-line @typescript-eslint/no-var-requires const deepEqualInAnyOrder = require('deep-equal-in-any-order'); +// eslint-disable-next-line @typescript-eslint/no-var-requires const chai = require('chai'); chai.use(deepEqualInAnyOrder); @@ -65,12 +67,12 @@ class SearchManagerTest extends SearchManager { class GalleryManagerTest extends GalleryManager { - public async selectParentDir(connection: Connection, directoryName: string, directoryParent: string): Promise { - return super.selectParentDir(connection, directoryName, directoryParent); + public async getDirIdAndTime(connection: Connection, directoryName: string, directoryParent: string) { + return super.getDirIdAndTime(connection, directoryName, directoryParent); } - public async fillParentDir(connection: Connection, dir: ParentDirectoryDTO): Promise { - return super.fillParentDir(connection, dir); + public async getParentDirFromId(connection: Connection, dir: number): Promise { + return super.getParentDirFromId(connection, dir); } } @@ -116,11 +118,17 @@ describe('SearchManager', (sqlHelper: DBTestHelper) => { subDir = dir.directories[0]; subDir2 = dir.directories[1]; p = (dir.media.filter(m => m.name === p.name)[0] as any); + p.directory = dir; p2 = (dir.media.filter(m => m.name === p2.name)[0] as any); + p2.directory = dir; gpx = (dir.metaFile[0] as any); + gpx.directory = dir; v = (dir.media.filter(m => m.name === v.name)[0] as any); + v.directory = dir; p4 = (dir.directories[1].media[0] as any); + p4.directory = dir.directories[1]; pFaceLess = (dir.directories[0].media[0] as any); + pFaceLess.directory = dir.directories[0]; }; const setUpSqlDB = async () => { @@ -1149,7 +1157,7 @@ describe('SearchManager', (sqlHelper: DBTestHelper) => { }); /** - * flattenSameOfQueries converts converts some-of querries to AND and OR queries + * flattenSameOfQueries converts some-of queries to AND and OR queries * E.g.: * 2-of:(A B C) to (A and (B or C)) or (B and C) * this tests makes sure that all queries has at least 2 constraints @@ -1169,13 +1177,8 @@ describe('SearchManager', (sqlHelper: DBTestHelper) => { } return depth; } - // its an or + // it's an OR const lengths = (q as SearchListQuery).list.map(l => shortestDepth(l)).sort(); - - if (lengths[0] !== lengths[lengths.length - 1]) { - for (const l of (q as SearchListQuery).list) { - } - } return lengths[0]; } return 1;