1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2025-01-12 04:23:09 +02:00

improving directory indexing race condition bug

This commit is contained in:
Patrik J. Braun 2018-12-09 11:37:12 +01:00
parent 38f36891bd
commit d035f167ee
9 changed files with 229 additions and 178 deletions

View File

@ -52,17 +52,22 @@ export class ThumbnailGeneratorMWs {
return next();
}
const cw: ContentWrapper = req.resultPipe;
if (cw.notModified === true) {
return next();
}
if (cw.directory) {
ThumbnailGeneratorMWs.addThInfoTODir(<DirectoryDTO>cw.directory);
}
if (cw.searchResult) {
ThumbnailGeneratorMWs.addThInfoToPhotos(cw.searchResult.media);
}
try {
const cw: ContentWrapper = req.resultPipe;
if (cw.notModified === true) {
return next();
}
if (cw.directory) {
ThumbnailGeneratorMWs.addThInfoTODir(cw.directory);
}
if (cw.searchResult && cw.searchResult.media) {
ThumbnailGeneratorMWs.addThInfoToPhotos(cw.searchResult.media);
}
} catch (error) {
return next(new ErrorDTO(ErrorCodes.SERVER_ERROR, 'error during postprocessing result', error.toString()));
}
return next();
@ -102,18 +107,14 @@ export class ThumbnailGeneratorMWs {
}
private static addThInfoTODir(directory: DirectoryDTO) {
if (typeof directory.media === 'undefined') {
directory.media = [];
if (typeof directory.media !== 'undefined') {
ThumbnailGeneratorMWs.addThInfoToPhotos(directory.media);
}
if (typeof directory.directories === 'undefined') {
directory.directories = [];
if (typeof directory.directories !== 'undefined') {
for (let i = 0; i < directory.directories.length; i++) {
ThumbnailGeneratorMWs.addThInfoTODir(directory.directories[i]);
}
}
ThumbnailGeneratorMWs.addThInfoToPhotos(directory.media);
for (let i = 0; i < directory.directories.length; i++) {
ThumbnailGeneratorMWs.addThInfoTODir(directory.directories[i]);
}
}
private static addThInfoToPhotos(photos: MediaDTO[]) {
@ -135,7 +136,6 @@ export class ThumbnailGeneratorMWs {
if (fs.existsSync(iconPath) === true) {
photos[i].readyIcon = true;
}
}
}

View File

@ -13,7 +13,7 @@ import {ISQLGalleryManager} from './IGalleryManager';
import {DatabaseType, ReIndexingSensitivity} from '../../../common/config/private/IPrivateConfig';
import {PhotoDTO} from '../../../common/entities/PhotoDTO';
import {OrientationType} from '../../../common/entities/RandomQueryDTO';
import {Brackets, Connection} from 'typeorm';
import {Brackets, Connection, Transaction, TransactionRepository, Repository} from 'typeorm';
import {MediaEntity} from './enitites/MediaEntity';
import {MediaDTO} from '../../../common/entities/MediaDTO';
import {VideoEntity} from './enitites/VideoEntity';
@ -23,6 +23,9 @@ import {NotificationManager} from '../NotifocationManager';
export class GalleryManager implements IGalleryManager, ISQLGalleryManager {
private savingQueue: DirectoryDTO[] = [];
private isSaving = false;
protected async selectParentDir(connection: Connection, directoryName: string, directoryParent: string): Promise<DirectoryEntity> {
const query = connection
.getRepository(DirectoryEntity)
@ -34,7 +37,7 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager {
.leftJoinAndSelect('directory.directories', 'directories')
.leftJoinAndSelect('directory.media', 'media');
if (Config.Client.MetaFile.enabled == true) {
if (Config.Client.MetaFile.enabled === true) {
query.leftJoinAndSelect('directory.metaFile', 'metaFile');
}
@ -75,15 +78,17 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager {
public async listDirectory(relativeDirectoryName: string,
knownLastModified?: number,
knownLastScanned?: number): Promise<DirectoryDTO> {
relativeDirectoryName = path.normalize(path.join('.' + path.sep, relativeDirectoryName));
const directoryName = path.basename(relativeDirectoryName);
const directoryParent = path.join(path.dirname(relativeDirectoryName), path.sep);
const connection = await SQLConnection.getConnection();
const stat = fs.statSync(path.join(ProjectPath.ImageFolder, relativeDirectoryName));
const lastModified = Math.max(stat.ctime.getTime(), stat.mtime.getTime());
const dir = await this.selectParentDir(connection, directoryName, directoryParent);
const dir = await this.selectParentDir(connection, directoryName, directoryParent);
if (dir && dir.lastScanned != null) {
// If it seems that the content did not changed, do not work on it
if (knownLastModified && knownLastScanned
@ -135,7 +140,7 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager {
scannedDirectory.media.forEach(p => p.readyThumbnails = []);
resolve(scannedDirectory);
await this.saveToDB(scannedDirectory);
this.queueForSave(scannedDirectory).catch(console.error);
} catch (error) {
NotificationManager.warning('Unknown indexing error for: ' + relativeDirectoryName, error.toString());
@ -204,139 +209,151 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager {
}
// Todo fix it, once typeorm support connection pools ofr sqlite
protected async queueForSave(scannedDirectory: DirectoryDTO) {
if (this.savingQueue.findIndex(dir => dir.name === scannedDirectory.name &&
dir.path === scannedDirectory.path &&
dir.lastModified === scannedDirectory.lastModified &&
dir.lastScanned === scannedDirectory.lastScanned &&
(dir.media || dir.media.length) === (scannedDirectory.media || scannedDirectory.media.length) &&
(dir.metaFile || dir.metaFile.length) === (scannedDirectory.metaFile || scannedDirectory.metaFile.length)) !== -1) {
return;
}
this.savingQueue.push(scannedDirectory);
while (this.isSaving === false && this.savingQueue.length > 0) {
await this.saveToDB(this.savingQueue[0]);
this.savingQueue.shift();
}
}
protected async saveToDB(scannedDirectory: DirectoryDTO) {
const connection = await SQLConnection.getConnection();
this.isSaving = true;
try {
const connection = await SQLConnection.getConnection();
// saving to db
const directoryRepository = connection.getRepository(DirectoryEntity);
const mediaRepository = connection.getRepository(MediaEntity);
const fileRepository = connection.getRepository(FileEntity);
// saving to db
const directoryRepository = connection.getRepository(DirectoryEntity);
const mediaRepository = connection.getRepository(MediaEntity);
const fileRepository = connection.getRepository(FileEntity);
let currentDir: DirectoryEntity = await directoryRepository.createQueryBuilder('directory')
.where('directory.name = :name AND directory.path = :path', {
name: scannedDirectory.name,
path: scannedDirectory.path
}).getOne();
if (!!currentDir) {// Updated parent dir (if it was in the DB previously)
currentDir.lastModified = scannedDirectory.lastModified;
currentDir.lastScanned = scannedDirectory.lastScanned;
// const media: MediaEntity[] = currentDir.media;
// delete currentDir.media;
currentDir = await directoryRepository.save(currentDir);
/*if (media) {
media.forEach(m => m.directory = currentDir);
currentDir.media = await this.saveMedia(connection, media);
}*/
} else {
// const media = scannedDirectory.media;
// delete scannedDirectory.media;
(<DirectoryEntity>scannedDirectory).lastScanned = scannedDirectory.lastScanned;
currentDir = await directoryRepository.save(<DirectoryEntity>scannedDirectory);
/* if (media) {
media.forEach(m => m.directory = currentDir);
currentDir.media = await this.saveMedia(connection, media);
}*/
}
// save subdirectories
const childDirectories = await directoryRepository.createQueryBuilder('directory')
.where('directory.parent = :dir', {
dir: currentDir.id
}).getMany();
for (let i = 0; i < scannedDirectory.directories.length; i++) {
// Was this child Dir already indexed before?
let directory: DirectoryEntity = null;
for (let j = 0; j < childDirectories.length; j++) {
if (childDirectories[j].name === scannedDirectory.directories[i].name) {
directory = childDirectories[j];
childDirectories.splice(j, 1);
break;
}
}
if (directory != null) { // update existing directory
if (!directory.parent || !directory.parent.id) { // set parent if not set yet
directory.parent = currentDir;
delete directory.media;
await directoryRepository.save(directory);
}
let currentDir: DirectoryEntity = await directoryRepository.createQueryBuilder('directory')
.where('directory.name = :name AND directory.path = :path', {
name: scannedDirectory.name,
path: scannedDirectory.path
}).getOne();
if (!!currentDir) {// Updated parent dir (if it was in the DB previously)
currentDir.lastModified = scannedDirectory.lastModified;
currentDir.lastScanned = scannedDirectory.lastScanned;
currentDir = await directoryRepository.save(currentDir);
} else {
scannedDirectory.directories[i].parent = currentDir;
(<DirectoryEntity>scannedDirectory.directories[i]).lastScanned = null; // new child dir, not fully scanned yet
const d = await directoryRepository.save(<DirectoryEntity>scannedDirectory.directories[i]);
for (let j = 0; j < scannedDirectory.directories[i].media.length; j++) {
scannedDirectory.directories[i].media[j].directory = d;
(<DirectoryEntity>scannedDirectory).lastScanned = scannedDirectory.lastScanned;
currentDir = await directoryRepository.save(<DirectoryEntity>scannedDirectory);
}
// save subdirectories
const childDirectories = await directoryRepository.createQueryBuilder('directory')
.where('directory.parent = :dir', {
dir: currentDir.id
}).getMany();
for (let i = 0; i < scannedDirectory.directories.length; i++) {
// Was this child Dir already indexed before?
let directory: DirectoryEntity = null;
for (let j = 0; j < childDirectories.length; j++) {
if (childDirectories[j].name === scannedDirectory.directories[i].name) {
directory = childDirectories[j];
childDirectories.splice(j, 1);
break;
}
}
await this.saveMedia(connection, scannedDirectory.directories[i].media);
}
}
if (directory != null) { // update existing directory
if (!directory.parent || !directory.parent.id) { // set parent if not set yet
directory.parent = currentDir;
delete directory.media;
await directoryRepository.save(directory);
}
} else {
scannedDirectory.directories[i].parent = currentDir;
(<DirectoryEntity>scannedDirectory.directories[i]).lastScanned = null; // new child dir, not fully scanned yet
const d = await directoryRepository.save(<DirectoryEntity>scannedDirectory.directories[i]);
for (let j = 0; j < scannedDirectory.directories[i].media.length; j++) {
scannedDirectory.directories[i].media[j].directory = d;
}
// Remove child Dirs that are not anymore in the parent dir
await directoryRepository.remove(childDirectories);
// save media
const indexedMedia = await mediaRepository.createQueryBuilder('media')
.where('media.directory = :dir', {
dir: currentDir.id
}).getMany();
const mediaToSave = [];
for (let i = 0; i < scannedDirectory.media.length; i++) {
let media: MediaDTO = null;
for (let j = 0; j < indexedMedia.length; j++) {
if (indexedMedia[j].name === scannedDirectory.media[i].name) {
media = indexedMedia[j];
indexedMedia.splice(j, 1);
break;
await this.saveMedia(connection, scannedDirectory.directories[i].media);
}
}
if (media == null) { //not in DB yet
scannedDirectory.media[i].directory = null;
media = Utils.clone(scannedDirectory.media[i]);
scannedDirectory.media[i].directory = scannedDirectory;
media.directory = currentDir;
mediaToSave.push(media);
} else if (!Utils.equalsFilter(media.metadata, scannedDirectory.media[i].metadata)) {
media.metadata = scannedDirectory.media[i].metadata;
mediaToSave.push(media);
}
}
await this.saveMedia(connection, mediaToSave);
await mediaRepository.remove(indexedMedia);
// save files
const indexedMetaFiles = await fileRepository.createQueryBuilder('file')
.where('file.directory = :dir', {
dir: currentDir.id
}).getMany();
// Remove child Dirs that are not anymore in the parent dir
await directoryRepository.remove(childDirectories);
// save media
const indexedMedia = await mediaRepository.createQueryBuilder('media')
.where('media.directory = :dir', {
dir: currentDir.id
}).getMany();
const metaFilesToSave = [];
for (let i = 0; i < scannedDirectory.metaFile.length; i++) {
let metaFile: FileDTO = null;
for (let j = 0; j < indexedMetaFiles.length; j++) {
if (indexedMetaFiles[j].name === scannedDirectory.metaFile[i].name) {
metaFile = indexedMetaFiles[j];
indexedMetaFiles.splice(j, 1);
break;
const mediaToSave = [];
for (let i = 0; i < scannedDirectory.media.length; i++) {
let media: MediaDTO = null;
for (let j = 0; j < indexedMedia.length; j++) {
if (indexedMedia[j].name === scannedDirectory.media[i].name) {
media = indexedMedia[j];
indexedMedia.splice(j, 1);
break;
}
}
if (media == null) { // not in DB yet
scannedDirectory.media[i].directory = null;
media = Utils.clone(scannedDirectory.media[i]);
scannedDirectory.media[i].directory = scannedDirectory;
media.directory = currentDir;
mediaToSave.push(media);
} else if (!Utils.equalsFilter(media.metadata, scannedDirectory.media[i].metadata)) {
media.metadata = scannedDirectory.media[i].metadata;
mediaToSave.push(media);
}
}
if (metaFile == null) { //not in DB yet
scannedDirectory.metaFile[i].directory = null;
metaFile = Utils.clone(scannedDirectory.metaFile[i]);
scannedDirectory.metaFile[i].directory = scannedDirectory;
metaFile.directory = currentDir;
metaFilesToSave.push(metaFile);
await this.saveMedia(connection, mediaToSave);
await mediaRepository.remove(indexedMedia);
// save files
const indexedMetaFiles = await fileRepository.createQueryBuilder('file')
.where('file.directory = :dir', {
dir: currentDir.id
}).getMany();
const metaFilesToSave = [];
for (let i = 0; i < scannedDirectory.metaFile.length; i++) {
let metaFile: FileDTO = null;
for (let j = 0; j < indexedMetaFiles.length; j++) {
if (indexedMetaFiles[j].name === scannedDirectory.metaFile[i].name) {
metaFile = indexedMetaFiles[j];
indexedMetaFiles.splice(j, 1);
break;
}
}
if (metaFile == null) { // not in DB yet
scannedDirectory.metaFile[i].directory = null;
metaFile = Utils.clone(scannedDirectory.metaFile[i]);
scannedDirectory.metaFile[i].directory = scannedDirectory;
metaFile.directory = currentDir;
metaFilesToSave.push(metaFile);
}
}
await fileRepository.save(metaFilesToSave);
await fileRepository.remove(indexedMetaFiles);
}catch (e){
throw e;
}finally {
this.isSaving = false;
}
await fileRepository.save(metaFilesToSave);
await fileRepository.remove(indexedMetaFiles);
}
protected async saveMedia(connection: Connection, mediaList: MediaDTO[]): Promise<MediaEntity[]> {

View File

@ -26,11 +26,9 @@ export class SQLConnection {
private static connection: Connection = null;
public static async getConnection(): Promise<Connection> {
if (this.connection == null) {
const options: any = this.getDriver(Config.Server.database);
options.name = 'main';
// options.name = 'main';
options.entities = [
UserEntity,
FileEntity,
@ -47,7 +45,6 @@ export class SQLConnection {
await SQLConnection.schemeSync(this.connection);
}
return this.connection;
}
public static async tryConnection(config: DataBaseConfig) {

View File

@ -1,9 +1,10 @@
import {Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn} from 'typeorm';
import {Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn, Unique} from 'typeorm';
import {DirectoryDTO} from '../../../../common/entities/DirectoryDTO';
import {MediaEntity} from './MediaEntity';
import {FileEntity} from './FileEntity';
@Entity()
@Unique(['name', 'path'])
export class DirectoryEntity implements DirectoryDTO {
@PrimaryGeneratedColumn()

View File

@ -1,4 +1,4 @@
import {Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance} from 'typeorm';
import {Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance, Unique} from 'typeorm';
import {DirectoryEntity} from './DirectoryEntity';
import {MediaDimension, MediaDTO, MediaMetadata} from '../../../../common/entities/MediaDTO';
import {OrientationTypes} from 'ts-exif-parser';
@ -51,6 +51,7 @@ export class MediaMetadataEntity implements MediaMetadata {
// TODO: fix inheritance once its working in typeorm
@Entity()
@Unique(['name', 'directory'])
@TableInheritance({column: {type: 'varchar', name: 'type'}})
export abstract class MediaEntity implements MediaDTO {

View File

@ -1,4 +1,4 @@
import {Column, Entity, ChildEntity} from 'typeorm';
import {Column, Entity, ChildEntity, Unique} from 'typeorm';
import {CameraMetadata, GPSMetadata, PhotoDTO, PhotoMetadata, PositionMetaData} from '../../../../common/entities/PhotoDTO';
import {OrientationTypes} from 'ts-exif-parser';
import {MediaEntity, MediaMetadataEntity} from './MediaEntity';

View File

@ -162,10 +162,6 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit {
public nextImage() {
if (this.activePhotoId + 1 < this.gridPhotoQL.length) {
this.navigateToPhoto(this.activePhotoId + 1);
/*if (this.activePhotoId + 3 >= this.gridPhotoQL.length) {
this.onLastElement.emit({}); // trigger to render more photos if there are
}*/
return;
}
}
@ -173,14 +169,13 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit {
this.pause();
if (this.activePhotoId > 0) {
this.navigateToPhoto(this.activePhotoId - 1);
return;
}
}
private navigateToPhoto(photoIndex: number) {
this.router.navigate([],
{queryParams: this.queryService.getParams(this.gridPhotoQL.toArray()[photoIndex].gridPhoto.media)});
{queryParams: this.queryService.getParams(this.gridPhotoQL.toArray()[photoIndex].gridPhoto.media)}).catch(console.error);
}
private showPhoto(photoIndex: number, resize: boolean = true) {
@ -235,6 +230,11 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit {
this.prevImage();
}
break;
case 'ArrowRight':
if (this.activePhotoId < this.gridPhotoQL.length - 1) {
this.nextImage();
}
break;
case 'i':
case 'I':
if (this.isInfoPanelAnimating()) {
@ -258,11 +258,6 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit {
case 'C':
this.controllersAlwaysOn = !this.controllersAlwaysOn;
break;
case 'ArrowRight':
if (this.activePhotoId < this.gridPhotoQL.length - 1) {
this.nextImage();
}
break;
case 'Escape': // escape
this.hide();
break;
@ -276,7 +271,7 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit {
public hide() {
this.router.navigate([],
{queryParams: this.queryService.getParams()});
{queryParams: this.queryService.getParams()}).catch(console.error);
}
private hideLigthbox() {

View File

@ -1,6 +1,6 @@
{
"name": "pigallery2",
"version": "1.5.0",
"version": "1.5.5",
"description": "This is a photo gallery optimised for running low resource servers (especially on raspberry pi)",
"author": "Patrik J. Braun",
"homepage": "https://github.com/bpatrik/PiGallery2",
@ -46,20 +46,20 @@
"winston": "2.4.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "0.11.0",
"@angular-devkit/build-optimizer": "0.11.0",
"@angular/animations": "7.1.1",
"@angular/cli": "7.1.0",
"@angular/common": "7.1.1",
"@angular/compiler": "7.1.1",
"@angular/compiler-cli": "7.1.1",
"@angular/core": "7.1.1",
"@angular/forms": "7.1.1",
"@angular/http": "7.1.1",
"@angular/language-service": "7.1.1",
"@angular/platform-browser": "7.1.1",
"@angular/platform-browser-dynamic": "7.1.1",
"@angular/router": "7.1.1",
"@angular-devkit/build-angular": "0.11.2",
"@angular-devkit/build-optimizer": "0.11.2",
"@angular/animations": "7.1.2",
"@angular/cli": "7.1.2",
"@angular/common": "7.1.2",
"@angular/compiler": "7.1.2",
"@angular/compiler-cli": "7.1.2",
"@angular/core": "7.1.2",
"@angular/forms": "7.1.2",
"@angular/http": "7.1.2",
"@angular/language-service": "7.1.2",
"@angular/platform-browser": "7.1.2",
"@angular/platform-browser-dynamic": "7.1.2",
"@angular/router": "7.1.2",
"@ngx-translate/i18n-polyfill": "1.0.0",
"@types/bcryptjs": "2.4.2",
"@types/chai": "4.1.7",
@ -70,18 +70,18 @@
"@types/fluent-ffmpeg": "2.1.8",
"@types/gm": "1.18.2",
"@types/jasmine": "3.3.0",
"@types/node": "10.12.11",
"@types/node": "10.12.12",
"@types/sharp": "0.21.0",
"@types/winston": "2.3.9",
"@yaga/leaflet-ng2": "^1.0.0",
"bootstrap": "4.1.3",
"chai": "4.2.0",
"codelyzer": "4.5.0",
"core-js": "2.5.7",
"core-js": "2.6.0",
"ejs-loader": "0.3.1",
"gulp": "3.9.1",
"gulp-json-modify": "1.0.2",
"gulp-typescript": "5.0.0-alpha.3",
"gulp-typescript": "5.0.0",
"gulp-zip": "4.2.0",
"hammerjs": "2.0.8",
"intl": "1.2.5",
@ -126,7 +126,7 @@
"bcrypt": "3.0.2",
"gm": "1.23.1",
"mysql": "2.16.0",
"sharp": "0.21.0"
"sharp": "0.21.1"
},
"engines": {
"node": ">= 6.9 <11.0"

View File

@ -12,6 +12,8 @@ import {DirectoryEntity} from '../../../../../backend/model/sql/enitites/Directo
import {Utils} from '../../../../../common/Utils';
import {MediaDTO} from '../../../../../common/entities/MediaDTO';
import {FileDTO} from '../../../../../common/entities/FileDTO';
import {PhotoEntity} from '../../../../../backend/model/sql/enitites/PhotoEntity';
import {FileEntity} from '../../../../../backend/model/sql/enitites/FileEntity';
class GalleryManagerTest extends GalleryManager {
@ -28,6 +30,10 @@ class GalleryManagerTest extends GalleryManager {
public async saveToDB(scannedDirectory: DirectoryDTO) {
return super.saveToDB(scannedDirectory);
}
public async queueForSave(scannedDirectory: DirectoryDTO): Promise<void> {
return super.queueForSave(scannedDirectory);
}
}
describe('GalleryManager', () => {
@ -171,7 +177,41 @@ describe('GalleryManager', () => {
// selected.directories[0].parent = selected;
expect(Utils.clone(Utils.removeNullOrEmptyObj(selected)))
.to.deep.equal(Utils.clone(Utils.removeNullOrEmptyObj(subDir)));
});
it('should avoid race condition', async () => {
const conn = await SQLConnection.getConnection();
const gm = new GalleryManagerTest();
Config.Client.MetaFile.enabled = true;
const parent = TestHelper.getRandomizedDirectoryEntry();
const p1 = TestHelper.getRandomizedPhotoEntry(parent, 'Photo1');
const p2 = TestHelper.getRandomizedPhotoEntry(parent, 'Photo2');
const gpx = TestHelper.getRandomizedGPXEntry(parent, 'GPX1');
const subDir = TestHelper.getRandomizedDirectoryEntry(parent, 'subDir');
const sp1 = TestHelper.getRandomizedPhotoEntry(subDir, 'subPhoto1');
const sp2 = TestHelper.getRandomizedPhotoEntry(subDir, 'subPhoto2');
DirectoryDTO.removeReferences(parent);
const s1 = gm.queueForSave(Utils.clone(parent));
const s2 = gm.queueForSave(Utils.clone(parent));
const s3 = gm.queueForSave(Utils.clone(parent));
await Promise.all([s1, s2, s3]);
const selected = await gm.selectParentDir(conn, parent.name, parent.path);
await gm.fillParentDir(conn, selected);
const query = conn.getRepository(FileEntity).createQueryBuilder('photo');
query.innerJoinAndSelect('photo.directory', 'directory');
console.log((await query.getMany()));
DirectoryDTO.removeReferences(selected);
removeIds(selected);
subDir.isPartial = true;
delete subDir.directories;
delete subDir.metaFile;
expect(Utils.clone(Utils.removeNullOrEmptyObj(selected)))
.to.deep.equal(Utils.clone(Utils.removeNullOrEmptyObj(parent)));
});
});