diff --git a/benchmark/BenchmarkRunner.ts b/benchmark/BenchmarkRunner.ts index 3d60562c..9e2b0251 100644 --- a/benchmark/BenchmarkRunner.ts +++ b/benchmark/BenchmarkRunner.ts @@ -30,6 +30,7 @@ import { } from '../src/common/entities/SearchQueryDTO'; import {defaultQueryKeywords, QueryKeywords, SearchQueryParser} from '../src/common/SearchQueryParser'; import {ParentDirectoryDTO} from '../src/common/entities/DirectoryDTO'; +import {SessionContext} from '../src/backend/model/SessionContext'; export interface BenchmarkResult { @@ -268,7 +269,8 @@ export class BenchmarkRunner { const queue = ['/']; while (queue.length > 0) { const dirPath = queue.shift(); - const dir = await gm.listDirectory(dirPath); + const session = new SessionContext(); + const dir = await gm.listDirectory(session,dirPath); dir.directories.forEach((d): number => queue.push(path.join(d.path + d.name))); if (biggest < dir.media.length) { biggestPath = path.join(dir.path + dir.name); diff --git a/src/backend/middlewares/GalleryMWs.ts b/src/backend/middlewares/GalleryMWs.ts index dde536c8..036b156d 100644 --- a/src/backend/middlewares/GalleryMWs.ts +++ b/src/backend/middlewares/GalleryMWs.ts @@ -41,6 +41,7 @@ export class GalleryMWs { try { const directory = await ObjectManagers.getInstance().GalleryManager.listDirectory( + req.session.context, directoryName, parseInt( req.query[QueryParams.gallery.knownLastModified] as string, @@ -57,12 +58,12 @@ export class GalleryMWs { return next(); } if ( - req.session['user'].permissions && - req.session['user'].permissions.length > 0 && - req.session['user'].permissions[0] !== '/*' + req.session.context?.user.permissions && + req.session.context?.user.permissions.length > 0 && + req.session.context?.user.permissions[0] !== '/*' ) { directory.directories = directory.directories.filter((d): boolean => - UserDTOUtils.isDirectoryAvailable(d, req.session['user'].permissions) + UserDTOUtils.isDirectoryAvailable(d, req.session.context.user.permissions) ); } req.resultPipe = new ContentWrapper(directory, null); diff --git a/src/backend/middlewares/NotificationMWs.ts b/src/backend/middlewares/NotificationMWs.ts index 060ac6fb..263b4bd2 100644 --- a/src/backend/middlewares/NotificationMWs.ts +++ b/src/backend/middlewares/NotificationMWs.ts @@ -4,7 +4,7 @@ import {NotificationManager} from '../model/NotifocationManager'; export class NotificationMWs { public static list(req: Request, res: Response, next: NextFunction): void { - if (req.session['user'].role >= UserRoles.Admin) { + if (req.session.context?.user.role >= UserRoles.Admin) { req.resultPipe = NotificationManager.notifications; } else if (NotificationManager.notifications.length > 0) { req.resultPipe = NotificationManager.HasNotification; diff --git a/src/backend/middlewares/RenderingMWs.ts b/src/backend/middlewares/RenderingMWs.ts index 3ae1e917..e853662b 100644 --- a/src/backend/middlewares/RenderingMWs.ts +++ b/src/backend/middlewares/RenderingMWs.ts @@ -31,16 +31,17 @@ export class RenderingMWs { res: Response, next: NextFunction ): void { - if (!req.session['user']) { + if (!req.session.context?.user) { return next(new ErrorDTO(ErrorCodes.GENERAL_ERROR, 'User not exists')); } const user = { - id: req.session['user'].id, - name: req.session['user'].name, - role: req.session['user'].role, - usedSharingKey: req.session['user'].usedSharingKey, - permissions: req.session['user'].permissions, + id: req.session.context.user.id, + name: req.session.context.user.name, + role: req.session.context.user.role, + usedSharingKey: req.session.context.user.usedSharingKey, + permissions: req.session.context.user.permissions, + projectionKey: req.session.context.user.projectionKey, } as UserDTO; @@ -142,8 +143,8 @@ export class RenderingMWs { !( forcedDebug || (req.session && - req.session['user'] && - req.session['user'].role >= UserRoles.Developer) + req.session.context?.user && + req.session.context?.user.role >= UserRoles.Developer) ) ) { delete err.detailsStr; diff --git a/src/backend/middlewares/SharingMWs.ts b/src/backend/middlewares/SharingMWs.ts index e4552561..00f67095 100644 --- a/src/backend/middlewares/SharingMWs.ts +++ b/src/backend/middlewares/SharingMWs.ts @@ -102,9 +102,9 @@ export class SharingMWs { sharingKey, path: directoryName, password: createSharing.password, - creator: req.session['user'], + creator: req.session.context?.user, expires: - createSharing.valid >= 0 // if === -1 its forever + createSharing.valid >= 0 // if === -1 it's forever ? Date.now() + createSharing.valid : new Date(9999, 0, 1).getTime(), // never expire includeSubfolders: createSharing.includeSubfolders, @@ -155,7 +155,7 @@ export class SharingMWs { updateSharing.password && updateSharing.password !== '' ? updateSharing.password : null, - creator: req.session['user'], + creator: req.session.context?.user, expires: updateSharing.valid >= 0 // if === -1 its forever ? Date.now() + updateSharing.valid @@ -165,7 +165,7 @@ export class SharingMWs { }; try { - const forceUpdate = req.session['user'].role >= UserRoles.Admin; + const forceUpdate = req.session.context.user.role >= UserRoles.Admin; req.resultPipe = await ObjectManagers.getInstance().SharingManager.updateSharing( sharing, @@ -203,9 +203,9 @@ export class SharingMWs { try { // Check if user has the right to delete sharing. - if (req.session['user'].role < UserRoles.Admin) { + if (req.session.context?.user.role < UserRoles.Admin) { const s = await ObjectManagers.getInstance().SharingManager.findOne(sharingKey); - if (s.creator.id !== req.session['user'].id) { + if (s.creator.id !== req.session.context?.user.id) { return next(new ErrorDTO(ErrorCodes.NOT_AUTHORISED, 'Can\'t delete sharing.')); } } @@ -260,12 +260,12 @@ export class SharingMWs { const dir = path.normalize(req.params['directory'] || '/'); try { - if (req.session['user'].role >= UserRoles.Admin) { + if (req.session.context?.user.role >= UserRoles.Admin) { req.resultPipe = await ObjectManagers.getInstance().SharingManager.listAllForDir(dir); } else { req.resultPipe = - await ObjectManagers.getInstance().SharingManager.listAllForDir(dir, req.session['user']); + await ObjectManagers.getInstance().SharingManager.listAllForDir(dir, req.session.context?.user); } return next(); } catch (err) { diff --git a/src/backend/middlewares/customtypings/ExtendedRequest.d.ts b/src/backend/middlewares/customtypings/ExtendedRequest.d.ts index d297ae5a..a8eb3f82 100644 --- a/src/backend/middlewares/customtypings/ExtendedRequest.d.ts +++ b/src/backend/middlewares/customtypings/ExtendedRequest.d.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import {LoginCredential} from '../../../common/entities/LoginCredential'; import {UserDTO} from '../../../common/entities/UserDTO'; +import {SessionContext} from '../../model/SessionContext'; declare global { namespace Express { @@ -10,16 +11,16 @@ declare global { loginCredential?: LoginCredential; }; locale?: string; + session: { + context?: SessionContext; + rememberMe?: boolean; + }; } interface Response { tpl?: Record; } - interface Session { - user?: UserDTO; - rememberMe?: boolean; - } } } diff --git a/src/backend/middlewares/user/AuthenticationMWs.ts b/src/backend/middlewares/user/AuthenticationMWs.ts index c23246fb..edd16067 100644 --- a/src/backend/middlewares/user/AuthenticationMWs.ts +++ b/src/backend/middlewares/user/AuthenticationMWs.ts @@ -18,16 +18,17 @@ export class AuthenticationMWs { next: NextFunction ): Promise { if (Config.Users.authenticationRequired === false) { - req.session['user'] = { + const user = { name: UserRoles[Config.Users.unAuthenticatedUserRole], role: Config.Users.unAuthenticatedUserRole, } as UserDTO; + req.session.context = await ObjectManagers.getInstance().buildContext(user); return next(); } try { const user = await AuthenticationMWs.getSharingUser(req); if (user) { - req.session['user'] = user; + req.session.context = await ObjectManagers.getInstance().buildContext(user); return next(); } // eslint-disable-next-line no-empty @@ -43,28 +44,29 @@ export class AuthenticationMWs { next: NextFunction ): Promise { if (Config.Users.authenticationRequired === false) { - req.session['user'] = { + const user = { name: UserRoles[Config.Users.unAuthenticatedUserRole], role: Config.Users.unAuthenticatedUserRole, } as UserDTO; + req.session.context = await ObjectManagers.getInstance().buildContext(user); return next(); } // if already authenticated, do not try to use sharing authentication - if (typeof req.session['user'] !== 'undefined') { + if (typeof req.session.context !== 'undefined') { return next(); } try { const user = await AuthenticationMWs.getSharingUser(req); if (user) { - req.session['user'] = user; + req.session.context = await ObjectManagers.getInstance().buildContext(user); return next(); } } catch (err) { return next(new ErrorDTO(ErrorCodes.CREDENTIAL_NOT_FOUND, null, err)); } - if (typeof req.session['user'] === 'undefined') { + if (typeof req.session.context === 'undefined') { res.status(401); return next( new ErrorDTO(ErrorCodes.NOT_AUTHENTICATED, 'Not authenticated') @@ -104,7 +106,7 @@ export class AuthenticationMWs { } if ( - !UserDTOUtils.isDirectoryPathAvailable(p, req.session['user'].permissions) + !UserDTOUtils.isDirectoryPathAvailable(p, req.session.context.user.permissions) ) { return res.sendStatus(403); } @@ -121,7 +123,7 @@ export class AuthenticationMWs { res: Response, next: NextFunction ): void { - if (req.session['user'].role < role) { + if (req.session.context?.user.role < role) { return next(new ErrorDTO(ErrorCodes.NOT_AUTHORISED)); } return next(); @@ -170,12 +172,13 @@ export class AuthenticationMWs { sharingPath += '*'; } - req.session['user'] = { + const user = { name: 'Guest', role: UserRoles.LimitedGuest, permissions: [sharingPath], usedSharingKey: sharing.sharingKey, } as UserDTO; + req.session.context = await ObjectManagers.getInstance().buildContext(user); return next(); } catch (err) { return next(new ErrorDTO(ErrorCodes.GENERAL_ERROR, null, err)); @@ -187,7 +190,7 @@ export class AuthenticationMWs { res: Response, next: NextFunction ): void { - if (typeof req.session['user'] !== 'undefined') { + if (typeof req.session.context?.user !== 'undefined') { return next(new ErrorDTO(ErrorCodes.ALREADY_AUTHENTICATED)); } return next(); @@ -202,7 +205,7 @@ export class AuthenticationMWs { return res.sendStatus(404); } - // not enough parameter + // not enough parameters if ( typeof req.body === 'undefined' || typeof req.body.loginCredential === 'undefined' || @@ -226,7 +229,7 @@ export class AuthenticationMWs { }) ); delete user.password; - req.session['user'] = user; + req.session.context = await ObjectManagers.getInstance().buildContext(user); if (req.body.loginCredential.rememberMe) { req.sessionOptions.expires = new Date( Date.now() + Config.Server.sessionTimeout @@ -247,7 +250,7 @@ export class AuthenticationMWs { } public static logout(req: Request, res: Response, next: NextFunction): void { - delete req.session['user']; + delete req.session.context; return next(); } diff --git a/src/backend/middlewares/user/UserRequestConstrainsMWs.ts b/src/backend/middlewares/user/UserRequestConstrainsMWs.ts index d8ef4c9d..cbfb8c92 100644 --- a/src/backend/middlewares/user/UserRequestConstrainsMWs.ts +++ b/src/backend/middlewares/user/UserRequestConstrainsMWs.ts @@ -16,7 +16,7 @@ export class UserRequestConstrainsMWs { ) { return next(); } - if (req.session['user'].id !== parseInt(req.params.id, 10)) { + if (req.session.context?.user.id !== parseInt(req.params.id, 10)) { return next(new ErrorDTO(ErrorCodes.NOT_AUTHORISED)); } @@ -35,7 +35,7 @@ export class UserRequestConstrainsMWs { return next(); } - if (req.session['user'].id === parseInt(req.params.id, 10)) { + if (req.session.context?.user.id === parseInt(req.params.id, 10)) { return next(new ErrorDTO(ErrorCodes.NOT_AUTHORISED)); } @@ -54,7 +54,7 @@ export class UserRequestConstrainsMWs { return next(); } - if (req.session['user'].id !== parseInt(req.params.id, 10)) { + if (req.session.context?.user.id !== parseInt(req.params.id, 10)) { return next(); } diff --git a/src/backend/model/ObjectManagers.ts b/src/backend/model/ObjectManagers.ts index ed70be7e..454ff4da 100644 --- a/src/backend/model/ObjectManagers.ts +++ b/src/backend/model/ObjectManagers.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-var-requires */ +import * as crypto from 'crypto'; import {SQLConnection} from './database/SQLConnection'; import {Logger} from '../Logger'; import {LocationManager} from './database/LocationManager'; @@ -15,6 +16,9 @@ import {PersonManager} from './database/PersonManager'; import {SharingManager} from './database/SharingManager'; import {IObjectManager} from './database/IObjectManager'; import {ExtensionManager} from './extension/ExtensionManager'; +import {SessionContext} from './SessionContext'; +import {UserEntity} from './database/enitites/UserEntity'; +import {ANDSearchQuery, SearchQueryDTOUtils, SearchQueryTypes} from '../../common/entities/SearchQueryDTO'; const LOG_TAG = '[ObjectManagers]'; @@ -51,8 +55,8 @@ export class ObjectManagers { Logger.silly(LOG_TAG, 'Object manager reset begin'); if (ObjectManagers.isReady()) { if ( - ObjectManagers.getInstance().IndexingManager && - ObjectManagers.getInstance().IndexingManager.IsSavingInProgress + ObjectManagers.getInstance().IndexingManager && + ObjectManagers.getInstance().IndexingManager.IsSavingInProgress ) { await ObjectManagers.getInstance().IndexingManager.SavingReady; } @@ -111,7 +115,7 @@ export class ObjectManagers { } public async onDataChange( - changedDir: ParentDirectoryDTO = null + changedDir: ParentDirectoryDTO = null ): Promise { await this.VersionManager.onNewDataVersion(); @@ -269,4 +273,27 @@ export class ObjectManagers { this.extensionManager = value; this.managers.push(this.extensionManager as IObjectManager); } + + async buildContext(user: UserEntity): Promise { + const context = new SessionContext(); + context.user = user; + if (user.blockQuery) { + user.blockQuery = SearchQueryDTOUtils.negate(user.blockQuery); + } + if (user.allowQuery || user.blockQuery) { + let query = user.allowQuery || user.blockQuery; + if (user.allowQuery && user.blockQuery) { + query = { + type: SearchQueryTypes.AND, + list: [ + user.allowQuery, + user.blockQuery + ] + } as ANDSearchQuery; + } + context.projectionQuery = await ObjectManagers.getInstance().SearchManager.prepareAndBuildWhereQuery(query); + context.user.projectionKey = crypto.createHash('md5').update(JSON.stringify(query)).digest('hex'); + } + return context; + } } diff --git a/src/backend/model/SessionContext.ts b/src/backend/model/SessionContext.ts new file mode 100644 index 00000000..f81fae44 --- /dev/null +++ b/src/backend/model/SessionContext.ts @@ -0,0 +1,8 @@ +import {Brackets} from 'typeorm'; +import {UserDTO} from '../../common/entities/UserDTO'; + +export class SessionContext { + user: UserDTO; + projectionQuery: Brackets; + +} diff --git a/src/backend/model/database/GalleryManager.ts b/src/backend/model/database/GalleryManager.ts index 7d551e1e..72e2a929 100644 --- a/src/backend/model/database/GalleryManager.ts +++ b/src/backend/model/database/GalleryManager.ts @@ -6,14 +6,15 @@ import {SQLConnection} from './SQLConnection'; import {PhotoEntity} from './enitites/PhotoEntity'; import {ProjectPath} from '../../ProjectPath'; import {Config} from '../../../common/config/private/Config'; -import {Connection} from 'typeorm'; +import {Brackets, Connection} from 'typeorm'; import {MediaEntity} from './enitites/MediaEntity'; import {VideoEntity} from './enitites/VideoEntity'; import {Logger} from '../../Logger'; import {ObjectManagers} from '../ObjectManagers'; import {DuplicatesDTO} from '../../../common/entities/DuplicatesDTO'; import {ReIndexingSensitivity} from '../../../common/config/private/PrivateConfig'; -import { DiskManager } from '../fileaccess/DiskManager'; +import {DiskManager} from '../fileaccess/DiskManager'; +import {SessionContext} from '../SessionContext'; const LOG_TAG = '[GalleryManager]'; @@ -23,7 +24,7 @@ export class GalleryManager { parent: string; } { relativeDirectoryName = DiskManager.normalizeDirPath( - relativeDirectoryName + relativeDirectoryName ); return { name: path.basename(relativeDirectoryName), @@ -32,12 +33,13 @@ export class GalleryManager { } public async listDirectory( - relativeDirectoryName: string, - knownLastModified?: number, - knownLastScanned?: number + session: SessionContext, + relativeDirectoryName: string, + knownLastModified?: number, + knownLastScanned?: number ): Promise { const directoryPath = GalleryManager.parseRelativeDirePath( - relativeDirectoryName + relativeDirectoryName ); const connection = await SQLConnection.getConnection(); @@ -48,35 +50,35 @@ export class GalleryManager { // Return as soon as possible without touching the original data source (hdd) // See https://github.com/bpatrik/pigallery2/issues/613 if ( - Config.Indexing.reIndexingSensitivity === - ReIndexingSensitivity.never + Config.Indexing.reIndexingSensitivity === + ReIndexingSensitivity.never ) { return null; } const stat = fs.statSync( - path.join(ProjectPath.ImageFolder, relativeDirectoryName) + path.join(ProjectPath.ImageFolder, relativeDirectoryName) ); const lastModified = DiskManager.calcLastModified(stat); // If it seems that the content did not change, do not work on it if ( - knownLastModified && - knownLastScanned && - lastModified === knownLastModified && - dir.lastScanned === knownLastScanned + knownLastModified && + knownLastScanned && + lastModified === knownLastModified && + dir.lastScanned === knownLastScanned ) { if ( - Config.Indexing.reIndexingSensitivity === - ReIndexingSensitivity.low + Config.Indexing.reIndexingSensitivity === + ReIndexingSensitivity.low ) { return null; } if ( - Date.now() - dir.lastScanned <= - Config.Indexing.cachedFolderTimeout && - Config.Indexing.reIndexingSensitivity === - ReIndexingSensitivity.medium + Date.now() - dir.lastScanned <= + Config.Indexing.cachedFolderTimeout && + Config.Indexing.reIndexingSensitivity === + ReIndexingSensitivity.medium ) { return null; } @@ -84,16 +86,16 @@ export class GalleryManager { if (dir.lastModified !== lastModified) { Logger.silly( - LOG_TAG, - 'Reindexing reason: lastModified mismatch: known: ' + - dir.lastModified + - ', current:' + - lastModified + LOG_TAG, + 'Reindexing reason: lastModified mismatch: known: ' + + dir.lastModified + + ', current:' + + lastModified ); const ret = - await ObjectManagers.getInstance().IndexingManager.indexDirectory( - relativeDirectoryName - ); + await ObjectManagers.getInstance().IndexingManager.indexDirectory( + relativeDirectoryName + ); for (const subDir of ret.directories) { if (!subDir.cover) { // if subdirectories do not have photos, so cannot show a cover, try getting one from DB @@ -103,70 +105,70 @@ export class GalleryManager { return ret; } - // not indexed since a while, index it in a lazy manner + // not indexed since a while, index it lazily if ( - (Date.now() - dir.lastScanned > - Config.Indexing.cachedFolderTimeout && - Config.Indexing.reIndexingSensitivity >= - ReIndexingSensitivity.medium) || + (Date.now() - dir.lastScanned > + Config.Indexing.cachedFolderTimeout && Config.Indexing.reIndexingSensitivity >= - ReIndexingSensitivity.high + ReIndexingSensitivity.medium) || + Config.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.Indexing.cachedFolderTimeout + LOG_TAG, + 'lazy reindexing reason: cache timeout: lastScanned: ' + + (Date.now() - dir.lastScanned) + + 'ms ago, cachedFolderTimeout:' + + Config.Indexing.cachedFolderTimeout ); ObjectManagers.getInstance() - .IndexingManager.indexDirectory(relativeDirectoryName) - .catch(console.error); + .IndexingManager.indexDirectory(relativeDirectoryName) + .catch(console.error); } - return await this.getParentDirFromId(connection, dir.id); + return await this.getParentDirFromId(connection, session, dir.id); } // never scanned (deep indexed), do it and return with it Logger.silly(LOG_TAG, 'Reindexing reason: never scanned'); return ObjectManagers.getInstance().IndexingManager.indexDirectory( - relativeDirectoryName + relativeDirectoryName ); } async countDirectories(): Promise { const connection = await SQLConnection.getConnection(); return await connection - .getRepository(DirectoryEntity) - .createQueryBuilder('directory') - .getCount(); + .getRepository(DirectoryEntity) + .createQueryBuilder('directory') + .getCount(); } async countMediaSize(): Promise { const connection = await SQLConnection.getConnection(); const {sum} = await connection - .getRepository(MediaEntity) - .createQueryBuilder('media') - .select('SUM(media.metadata.fileSize)', 'sum') - .getRawOne(); + .getRepository(MediaEntity) + .createQueryBuilder('media') + .select('SUM(media.metadata.fileSize)', 'sum') + .getRawOne(); return sum || 0; } async countPhotos(): Promise { const connection = await SQLConnection.getConnection(); return await connection - .getRepository(PhotoEntity) - .createQueryBuilder('directory') - .getCount(); + .getRepository(PhotoEntity) + .createQueryBuilder('directory') + .getCount(); } async countVideos(): Promise { const connection = await SQLConnection.getConnection(); return await connection - .getRepository(VideoEntity) - .createQueryBuilder('directory') - .getCount(); + .getRepository(VideoEntity) + .createQueryBuilder('directory') + .getCount(); } public async getPossibleDuplicates(): Promise { @@ -174,31 +176,31 @@ export class GalleryManager { const mediaRepository = connection.getRepository(MediaEntity); let duplicates = await mediaRepository - .createQueryBuilder('media') - .innerJoin( - (query) => - 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.Duplicates.listingLimit) - .getMany(); + .createQueryBuilder('media') + .innerJoin( + (query) => + 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.Duplicates.listingLimit) + .getMany(); const duplicateParis: DuplicatesDTO[] = []; const processDuplicates = ( - duplicateList: MediaEntity[], - equalFn: (a: MediaEntity, b: MediaEntity) => boolean, - checkDuplicates = false + duplicateList: MediaEntity[], + equalFn: (a: MediaEntity, b: MediaEntity) => boolean, + checkDuplicates = false ): void => { let i = duplicateList.length - 1; while (i >= 0) { @@ -216,15 +218,15 @@ export class GalleryManager { 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) - ) + (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) + foundDuplicates.media.find((m): boolean => m.id === lm.id) ) { return; } @@ -239,40 +241,40 @@ export class GalleryManager { }; processDuplicates( - duplicates, - (a, b): boolean => - a.name === b.name && a.metadata.fileSize === b.metadata.fileSize + duplicates, + (a, b): boolean => + a.name === b.name && a.metadata.fileSize === b.metadata.fileSize ); duplicates = await mediaRepository - .createQueryBuilder('media') - .innerJoin( - (query) => - 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.Duplicates.listingLimit) - .getMany(); + .createQueryBuilder('media') + .innerJoin( + (query) => + 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.Duplicates.listingLimit) + .getMany(); processDuplicates( - duplicates, - (a, b): boolean => - a.metadata.creationDate === b.metadata.creationDate && - a.metadata.fileSize === b.metadata.fileSize, - true + duplicates, + (a, b): boolean => + a.metadata.creationDate === b.metadata.creationDate && + a.metadata.fileSize === b.metadata.fileSize, + true ); return duplicateParis; @@ -282,20 +284,20 @@ export class GalleryManager { * Returns with the directories only, does not include media or metafiles */ public async selectDirStructure( - relativeDirectoryName: string + relativeDirectoryName: string ): Promise { const directoryPath = GalleryManager.parseRelativeDirePath( - relativeDirectoryName + 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'); + .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(); } @@ -304,14 +306,14 @@ export class GalleryManager { * Sets cover for the directory and caches it in the DB */ public async fillCoverForSubDir( - connection: Connection, - dir: SubDirectoryDTO + connection: Connection, + dir: SubDirectoryDTO ): Promise { if (!dir.validCover) { dir.cover = - await ObjectManagers.getInstance().CoverManager.setAndGetCoverForDirectory( - dir - ); + await ObjectManagers.getInstance().CoverManager.setAndGetCoverForDirectory( + dir + ); } dir.media = []; @@ -324,48 +326,76 @@ export class GalleryManager { 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(); + .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, - partialDirId: number + connection: Connection, + session: SessionContext, + partialDirId: number ): Promise { + + const mediaJoinCondition = () => { + // Temporary QB to let Brackets build conditions and params + const dummyQb = connection.createQueryBuilder(); + session.projectionQuery.whereFactory(dummyQb); + + // Build condition string + const sql = dummyQb.expressionMap.wheres + .map(w => w.condition) + .join(' '); + + // Extract params that Brackets added + const params = dummyQb.getParameters(); + + return {sql, params}; + }; + let sql = 'media.directoryId = directory.id'; + let params = {}; + if (session.projectionQuery) { + const mj = mediaJoinCondition(); + sql += ' AND ' + mj.sql; + params = mj.params; + } + const query = connection - .getRepository(DirectoryEntity) - .createQueryBuilder('directory') - .where('directory.id = :id', { - id: partialDirId - }) - .leftJoinAndSelect('directory.directories', 'directories') - .leftJoinAndSelect('directory.media', 'media') - .leftJoinAndSelect('directories.cover', 'cover') - .leftJoinAndSelect('cover.directory', 'coverDirectory') - .select([ - 'directory', - 'directories', - 'media', - 'cover.name', - 'coverDirectory.name', - 'coverDirectory.path', - ]); + .getRepository(DirectoryEntity) + .createQueryBuilder('directory') + .where('directory.id = :id', { + id: partialDirId + }) + .leftJoinAndSelect('directory.directories', 'directories') + .leftJoinAndSelect( + 'directory.media', + 'media', sql, params + ) + .leftJoinAndSelect('directories.cover', 'cover') + .leftJoinAndSelect('cover.directory', 'coverDirectory') + .select([ + 'directory', + 'directories', + 'media', + 'cover.name', + 'coverDirectory.name', + 'coverDirectory.path', + ]); // TODO: do better filtering // NOTE: it should not cause an issue as it also do not save to the DB if ( - Config.MetaFile.gpx === true || - Config.MetaFile.pg2conf === true || - Config.MetaFile.markdown === true + Config.MetaFile.gpx === true || + Config.MetaFile.pg2conf === true || + Config.MetaFile.markdown === true ) { query.leftJoinAndSelect('directory.metaFile', 'metaFile'); } diff --git a/src/backend/model/diagnostics/ConfigDiagnostics.ts b/src/backend/model/diagnostics/ConfigDiagnostics.ts index f193e245..6b15a89e 100644 --- a/src/backend/model/diagnostics/ConfigDiagnostics.ts +++ b/src/backend/model/diagnostics/ConfigDiagnostics.ts @@ -292,15 +292,16 @@ export class ConfigDiagnostics { settings.SearchQuery ) ) { - throw new Error('SearchQuery is not valid. Got: ' + JSON.stringify(sp.parse(sp.stringify(settings.SearchQuery)))); + throw new Error('SearchQuery is not valid. Expected: ' + JSON.stringify(sp.parse(sp.stringify(settings.SearchQuery))) + + ' Got: ' + JSON.stringify(settings.SearchQuery)); } } /** * Removes unsupported image formats. - * It is possible that some OS support one or the other image formats (like Mac os does with HEIC) - * , but others not. - * Those formats are added to the config, but dynamically removed. + * It is possible that some OS support one or the other image formats (like macOS does with HEIC), + * but others do not. + * Those formats are added to the config but dynamically removed. * @param config */ static async removeUnsupportedPhotoExtensions(config: ClientPhotoConfig): Promise { @@ -328,7 +329,7 @@ export class ConfigDiagnostics { } as MediaRendererInput, true ); } catch (e) { - Logger.verbose(e) + Logger.verbose(e); Logger.verbose(LOG_TAG, 'The current OS does not support the following photo format:' + ext + ', removing it form config.'); config.supportedFormats.splice(i, 1); removedSome = true; diff --git a/src/backend/model/extension/ExpressRouterWrapper.ts b/src/backend/model/extension/ExpressRouterWrapper.ts index 550d1797..1fe2cbbf 100644 --- a/src/backend/model/extension/ExpressRouterWrapper.ts +++ b/src/backend/model/extension/ExpressRouterWrapper.ts @@ -57,7 +57,7 @@ export class ExpressRouteWrapper implements IExtensionRESTRoute { this.router[this.func](fullPaths, ...(this.getAuthMWs(minRole).concat([ async (req: Request, res: Response, next: NextFunction) => { - req.resultPipe = await cb(req.params, req.body, req.session['user']); + req.resultPipe = await cb(req.params, req.body, req.session.context.user); next(); }, RenderingMWs.renderResult diff --git a/src/backend/model/extension/ExtensionConfigWrapper.ts b/src/backend/model/extension/ExtensionConfigWrapper.ts index ec24c3a9..1af352a7 100644 --- a/src/backend/model/extension/ExtensionConfigWrapper.ts +++ b/src/backend/model/extension/ExtensionConfigWrapper.ts @@ -11,7 +11,7 @@ import * as fs from 'fs'; const LOG_TAG = '[ExtensionConfigWrapper]'; /** - * Wraps to original config and makes sure all extension related config is loaded + * Wraps to the original config and makes sure all extension-related configs are loaded */ export class ExtensionConfigWrapper { diff --git a/src/backend/routes/PublicRouter.ts b/src/backend/routes/PublicRouter.ts index 87059272..d6f0d366 100644 --- a/src/backend/routes/PublicRouter.ts +++ b/src/backend/routes/PublicRouter.ts @@ -74,13 +74,14 @@ export class PublicRouter { res.tpl = {}; res.tpl.user = null; - if (req.session['user']) { + if (req.session.context?.user) { res.tpl.user = { - id: req.session['user'].id, - name: req.session['user'].name, - role: req.session['user'].role, - usedSharingKey: req.session['user'].usedSharingKey, - permissions: req.session['user'].permissions, + id: req.session.context.user.id, + name: req.session.context.user.name, + role: req.session.context.user.role, + usedSharingKey: req.session.context.user.usedSharingKey, + permissions:req.session.context.user.permissions, + projectionKey:req.session.context.user.projectionKey, } as UserDTO; } diff --git a/src/common/entities/UserDTO.ts b/src/common/entities/UserDTO.ts index 441d7f78..11da93c5 100644 --- a/src/common/entities/UserDTO.ts +++ b/src/common/entities/UserDTO.ts @@ -16,6 +16,7 @@ export interface UserDTO { role: UserRoles; usedSharingKey?: string; permissions: string[]; // user can only see these permissions. if ends with *, its recursive + projectionKey?: string; // allow- and blocklist projection hash. if null, no projection } export const UserDTOUtils = { @@ -35,8 +36,8 @@ export const UserDTOUtils = { if (permission[permission.length - 1] === '*') { permission = permission.slice(0, -1); if ( - path.startsWith(permission) && - (!path[permission.length] || path[permission.length] === '/') + path.startsWith(permission) && + (!path[permission.length] || path[permission.length] === '/') ) { return true; } @@ -50,12 +51,12 @@ export const UserDTOUtils = { }, isDirectoryAvailable: ( - directory: DirectoryPathDTO, - permissions: string[] + directory: DirectoryPathDTO, + permissions: string[] ): boolean => { return UserDTOUtils.isDirectoryPathAvailable( - Utils.concatUrls(directory.path, directory.name), - permissions + Utils.concatUrls(directory.path, directory.name), + permissions ); }, }; diff --git a/test/backend/DBTestHelper.ts b/test/backend/DBTestHelper.ts index 964eefd9..79b9bb73 100644 --- a/test/backend/DBTestHelper.ts +++ b/test/backend/DBTestHelper.ts @@ -15,6 +15,7 @@ import {TestHelper} from '../TestHelper'; import {VideoDTO} from '../../src/common/entities/VideoDTO'; import {PhotoDTO} from '../../src/common/entities/PhotoDTO'; import {Logger} from '../../src/backend/Logger'; +import {SessionContext} from '../../src/backend/model/SessionContext'; declare let describe: any; const savedDescribe = describe; @@ -32,8 +33,8 @@ class GalleryManagerTest extends GalleryManager { return super.getDirIdAndTime(connection, directoryName, directoryParent); } - public async getParentDirFromId(connection: Connection, dir: number): Promise { - return super.getParentDirFromId(connection, dir); + public async getParentDirFromId(connection: Connection, session: SessionContext, dir: number): Promise { + return super.getParentDirFromId(connection, session, dir); } } @@ -120,20 +121,21 @@ export class DBTestHelper { // await im.saveToDB(subDir2); if (ObjectManagers.getInstance().IndexingManager && - ObjectManagers.getInstance().IndexingManager.IsSavingInProgress) { + ObjectManagers.getInstance().IndexingManager.IsSavingInProgress) { await ObjectManagers.getInstance().IndexingManager.SavingReady; } const gm = new GalleryManagerTest(); + const session = new SessionContext(); - const dir = await gm.getParentDirFromId(connection, - (await gm.getDirIdAndTime(connection, directory.name, path.join(directory.path, path.sep))).id); + const dir = await gm.getParentDirFromId(connection, session, + (await gm.getDirIdAndTime(connection, directory.name, path.join(directory.path, path.sep))).id); const populateDir = async (d: DirectoryBaseDTO) => { for (let i = 0; i < d.directories.length; i++) { - d.directories[i] = await gm.getParentDirFromId(connection, - (await gm.getDirIdAndTime(connection, d.directories[i].name, - path.join(DiskManager.pathFromParent(d), path.sep))).id); + d.directories[i] = await gm.getParentDirFromId(connection, session, + (await gm.getDirIdAndTime(connection, d.directories[i].name, + path.join(DiskManager.pathFromParent(d), path.sep))).id); await populateDir(d.directories[i]); } }; diff --git a/test/backend/unit/middlewares/admin/SettingsMWs.ts b/test/backend/unit/middlewares/admin/SettingsMWs.spec.ts similarity index 100% rename from test/backend/unit/middlewares/admin/SettingsMWs.ts rename to test/backend/unit/middlewares/admin/SettingsMWs.spec.ts diff --git a/test/backend/unit/middlewares/user/AuthenticationMWs.ts b/test/backend/unit/middlewares/user/AuthenticationMWs.spec.ts similarity index 56% rename from test/backend/unit/middlewares/user/AuthenticationMWs.ts rename to test/backend/unit/middlewares/user/AuthenticationMWs.spec.ts index add5d3a0..48f32151 100644 --- a/test/backend/unit/middlewares/user/AuthenticationMWs.ts +++ b/test/backend/unit/middlewares/user/AuthenticationMWs.spec.ts @@ -7,6 +7,7 @@ import {ObjectManagers} from '../../../../../src/backend/model/ObjectManagers'; import {Config} from '../../../../../src/common/config/private/Config'; import * as path from 'path'; import {UserManager} from '../../../../../src/backend/model/database/UserManager'; +import {SearchQueryTypes, TextSearchQueryMatchTypes} from '../../../../../src/common/entities/SearchQueryDTO'; declare const describe: any; @@ -23,7 +24,9 @@ describe('Authentication middleware', () => { it('should call next on authenticated', (done: (err?: any) => void) => { const req: any = { session: { - user: 'A user' + context: { + user: 'A user' + } }, sessionOptions: {}, query: {}, @@ -72,7 +75,9 @@ describe('Authentication middleware', () => { const req = { session: { - user: {permissions: null as string[]} + context: { + user: {permissions: null as string[]} + } }, sessionOptions: {}, query: {}, @@ -93,14 +98,14 @@ describe('Authentication middleware', () => { }; it('should catch unauthorized path usage', async () => { - req.session['user'].permissions = [path.normalize('/sub/subsub')]; + req.session.context.user.permissions = [path.normalize('/sub/subsub')]; expect(await test('/sub/subsub')).to.be.eql('ok'); expect(await test('/test')).to.be.eql(403); expect(await test('/')).to.be.eql(403); expect(await test('/sub/test')).to.be.eql(403); expect(await test('/sub/subsub/test')).to.be.eql(403); expect(await test('/sub/subsub/test/test2')).to.be.eql(403); - req.session['user'].permissions = [path.normalize('/sub/subsub'), path.normalize('/sub/subsub2')]; + req.session.context.user.permissions = [path.normalize('/sub/subsub'), path.normalize('/sub/subsub2')]; expect(await test('/sub/subsub2')).to.be.eql('ok'); expect(await test('/sub/subsub')).to.be.eql('ok'); expect(await test('/test')).to.be.eql(403); @@ -108,7 +113,7 @@ describe('Authentication middleware', () => { expect(await test('/sub/test')).to.be.eql(403); expect(await test('/sub/subsub/test')).to.be.eql(403); expect(await test('/sub/subsub2/test')).to.be.eql(403); - req.session['user'].permissions = [path.normalize('/sub/subsub*')]; + req.session.context.user.permissions = [path.normalize('/sub/subsub*')]; expect(await test('/b')).to.be.eql(403); expect(await test('/sub')).to.be.eql(403); expect(await test('/sub/subsub2')).to.be.eql(403); @@ -144,7 +149,9 @@ describe('Authentication middleware', () => { it('should call next error on authenticated', (done: (err?: any) => void) => { const req: any = { session: { - user: 'A user' + context: { + user: 'A user' + } }, sessionOptions: {}, }; @@ -162,8 +169,10 @@ describe('Authentication middleware', () => { it('should call next on authorised', (done: (err?: any) => void) => { const req: any = { session: { - user: { - role: UserRoles.LimitedGuest + context: { + user: { + role: UserRoles.LimitedGuest + } } }, sessionOptions: {} @@ -183,8 +192,10 @@ describe('Authentication middleware', () => { it('should call next with error on not authorised', (done: (err?: any) => void) => { const req: any = { session: { - user: { - role: UserRoles.LimitedGuest + context: { + user: { + role: UserRoles.LimitedGuest + } } }, sessionOptions: {} @@ -200,8 +211,8 @@ describe('Authentication middleware', () => { }); describe('login', () => { - beforeEach(() => { - ObjectManagers.reset(); + beforeEach(async () => { + await ObjectManagers.reset(); }); describe('should call input ErrorDTO next on missing...', () => { @@ -291,7 +302,7 @@ describe('Authentication middleware', () => { }); - it('should call next with user on the session on finding user', (done: (err?: any) => void) => { + it('should call next with user on the session on finding user', (done: (err?: any) => void) => { const req: any = { session: {}, body: { @@ -303,19 +314,226 @@ describe('Authentication middleware', () => { query: {}, params: {} }; + const testUser = 'test user' as any; + const testContext = {user: testUser}; + const next: any = (err: ErrorDTO) => { expect(err).to.be.undefined; - expect(req.session['user']).to.be.eql('test user'); + expect(req.session.context).to.be.eql(testContext); done(); }; + ObjectManagers.getInstance().UserManager = { findOne: (filter: never) => { - return Promise.resolve('test user' as any); + return Promise.resolve(testUser); } } as UserManager; + + // Add SearchManager mock to avoid errors in buildContext + ObjectManagers.getInstance().SearchManager = { + prepareAndBuildWhereQuery: (query: any) => { + return Promise.resolve(null); + } + } as any; + + AuthenticationMWs.login(req, null, next); + }); + + it('should create context with allowQuery', (done: (err?: any) => void) => { + const req: any = { + session: {}, + body: { + loginCredential: { + username: 'aa', + password: 'bb' + } + }, + query: {}, + params: {} + }; + + const testUser = { + name: 'testuser', + role: UserRoles.Admin, + allowQuery: { + type: SearchQueryTypes.directory, + text: '/allowed/path', + matchType: TextSearchQueryMatchTypes.exact_match + } + }; + + const projectionQuery = {someQueryObject: true}; + const testContext = { + user: testUser, + projectionQuery: projectionQuery + }; + + const next: any = (err: ErrorDTO) => { + try { + expect(err).to.be.undefined; + expect(req.session?.context).to.be.eql(testContext); + done(); + }catch (e){ + done(e); + } + }; + + ObjectManagers.getInstance().UserManager = { + findOne: (filter: never) => { + return Promise.resolve(testUser); + } + } as any; + // @ts-ignore + ObjectManagers.getInstance().buildContext = async (user: any) => { + expect(user).to.be.eql(testUser); + return testContext; + }; + + + AuthenticationMWs.login(req, null, next); + }); + + it('should create context with blockQuery', (done: (err?: any) => void) => { + const req: any = { + session: {}, + body: { + loginCredential: { + username: 'aa', + password: 'bb' + } + }, + query: {}, + params: {} + }; + + const testUser = { + name: 'testuser', + role: UserRoles.Admin, + blockQuery: { + type: SearchQueryTypes.directory, + text: '/blocked/path', + matchType: TextSearchQueryMatchTypes.exact_match + } + }; + + const projectionQuery = {someQueryObject: true}; + const testContext = { + user: testUser, + projectionQuery: projectionQuery + }; + + + const next: any = (err: ErrorDTO) => { + try { + expect(err).to.be.undefined; + expect(req.session?.context).to.be.eql(testContext); + done(); + }catch (e){ + done(e); + } + }; + + // @ts-ignore + ObjectManagers.getInstance().UserManager = { + findOne: (filter: never) => { + return Promise.resolve(testUser); + } + } as UserManager; + + ObjectManagers.getInstance().SearchManager = { + prepareAndBuildWhereQuery: (query: any) => { + // In real code, the blockQuery would be negated first + expect(query).not.to.be.undefined; + return Promise.resolve(projectionQuery); + } + } as any; + + // @ts-ignore + ObjectManagers.getInstance().buildContext = async (user: any) => { + expect(user).to.be.eql(testUser); + return testContext; + }; + + AuthenticationMWs.login(req, null, next); + }); + + it('should create context with both allowQuery and blockQuery', (done: (err?: any) => void) => { + const req: any = { + session: {}, + body: { + loginCredential: { + username: 'aa', + password: 'bb' + } + }, + query: {}, + params: {} + }; + + const testUser = { + name: 'testuser', + role: UserRoles.Admin, + allowQuery: { + type: SearchQueryTypes.directory, + text: '/allowed/path', + matchType: TextSearchQueryMatchTypes.exact_match + }, + blockQuery: { + type: SearchQueryTypes.directory, + text: '/blocked/path', + matchType: TextSearchQueryMatchTypes.exact_match + } + }; + + const projectionQuery = {someQueryObject: true}; + const testContext = { + user: { + ...testUser, + projectionKey: 'some-hash-value' + }, + projectionQuery: projectionQuery + }; + + const next: any = (err: ErrorDTO) => { + try { + expect(err).to.be.undefined; + expect(req.session.context).to.be.eql(testContext); + expect(req.session.context.user.projectionKey).not.to.be.undefined; + done(); + }catch (e){ + done(e); + } + }; + + // @ts-ignore + ObjectManagers.getInstance().UserManager = { + findOne: (filter: never) => { + return Promise.resolve({...testUser}); + } + } as UserManager; + + ObjectManagers.getInstance().SearchManager = { + prepareAndBuildWhereQuery: (query: any) => { + // In the real code, this would be an AND query combining allowQuery and negated blockQuery + expect(query.type).to.be.eql(SearchQueryTypes.AND); + expect(query.list).to.have.lengthOf(2); + return Promise.resolve(projectionQuery); + } + } as any; + + // @ts-ignore + ObjectManagers.getInstance().buildContext = async (user: any) => { + expect(user).to.deep.include({ + name: testUser.name, + role: testUser.role + }); + return testContext; + }; + + AuthenticationMWs.login(req, null, next); }); }); @@ -324,15 +542,17 @@ describe('Authentication middleware', () => { it('should call next on logout', (done: (err?: any) => void) => { const req: any = { session: { - user: { - role: UserRoles.LimitedGuest + context: { + user: { + role: UserRoles.LimitedGuest + } } } }; const next: any = (err: ErrorDTO) => { try { expect(err).to.be.undefined; - expect(req.user).to.be.undefined; + expect(req.session.context).to.be.undefined; done(); } catch (err) { done(err); diff --git a/test/backend/unit/model/ObjectManagers.spec.ts b/test/backend/unit/model/ObjectManagers.spec.ts new file mode 100644 index 00000000..b1838b09 --- /dev/null +++ b/test/backend/unit/model/ObjectManagers.spec.ts @@ -0,0 +1,201 @@ +/* eslint-disable no-unused-expressions,@typescript-eslint/no-unused-expressions */ +import {expect} from 'chai'; +import {ObjectManagers} from '../../../../src/backend/model/ObjectManagers'; +import {UserEntity} from '../../../../src/backend/model/database/enitites/UserEntity'; +import {SearchQueryTypes, TextSearch, TextSearchQueryMatchTypes} from '../../../../src/common/entities/SearchQueryDTO'; +import {UserRoles} from '../../../../src/common/entities/UserDTO'; +import {Brackets} from 'typeorm'; + +declare const describe: any; +declare const it: any; +declare const beforeEach: any; +declare const afterEach: any; + +describe('ObjectManagers', () => { + describe('buildContext', () => { + + // Reset ObjectManagers before each test + beforeEach(async () => { + await ObjectManagers.reset(); + }); + + afterEach(async () => { + await ObjectManagers.reset(); + }); + + it('should create a basic context with user and no queries', async () => { + // Create a basic user with no queries + const user = new UserEntity(); + user.id = 1; + user.name = 'testuser'; + user.role = UserRoles.Admin; + user.permissions = ['/test']; + + // Create the context + const context = await ObjectManagers.getInstance().buildContext(user); + + // Verify the context + expect(context).to.not.be.null; + expect(context.user).to.be.eql(user); + expect(context.projectionQuery).to.be.undefined; + }); + + it('should create a context with allowQuery and set projectionQuery', async () => { + // Create a user with allowQuery + const user = new UserEntity(); + user.id = 2; + user.name = 'allowuser'; + user.role = UserRoles.Admin; + user.allowQuery = { + type: SearchQueryTypes.directory, + text: '/allowed/path', + matchType: TextSearchQueryMatchTypes.exact_match + } as TextSearch; + + // Mock the SearchManager.prepareAndBuildWhereQuery method + const mockProjectionQuery = new Brackets(qb => qb.where('directory.path = :path', {path: '/allowed/path'})); + ObjectManagers.getInstance().SearchManager = { + prepareAndBuildWhereQuery: (query: any) => { + // Verify the query is passed correctly + expect(query).to.be.eql(user.allowQuery); + return Promise.resolve(mockProjectionQuery); + } + } as any; + + // Create the context + const context = await ObjectManagers.getInstance().buildContext(user); + + // Verify the context + expect(context).to.not.be.null; + expect(context.user).to.be.eql(user); + expect(context.projectionQuery).to.be.eql(mockProjectionQuery); + expect(context.user.projectionKey).to.be.a('string').and.not.empty; + }); + + it('should create a context with blockQuery, negate it, and set projectionQuery', async () => { + // Create a user with blockQuery + const user = new UserEntity(); + user.id = 3; + user.name = 'blockuser'; + user.role = UserRoles.Admin; + user.blockQuery = { + type: SearchQueryTypes.directory, + text: '/blocked/path', + matchType: TextSearchQueryMatchTypes.exact_match, + negate: false + } as TextSearch; + + // Clone the original blockQuery for later comparison + const originalBlockQuery = JSON.parse(JSON.stringify(user.blockQuery)); + + // Mock the SearchManager.prepareAndBuildWhereQuery method + const mockProjectionQuery = new Brackets(qb => qb.where('directory.path != :path', {path: '/blocked/path'})); + ObjectManagers.getInstance().SearchManager = { + prepareAndBuildWhereQuery: (query: any) => { + // Verify the query has been negated + expect(query).to.not.be.eql(originalBlockQuery); + expect(query.negate).to.be.true; + return Promise.resolve(mockProjectionQuery); + } + } as any; + + // Create the context + const context = await ObjectManagers.getInstance().buildContext(user); + + // Verify the context + expect(context).to.not.be.null; + expect(context.user).to.be.eql(user); + expect(context.projectionQuery).to.be.eql(mockProjectionQuery); + expect(context.user.projectionKey).to.be.a('string').and.not.empty; + // Verify the blockQuery was negated + expect((user.blockQuery as TextSearch).negate).to.be.true; + }); + + it('should create a context with both allowQuery and blockQuery, combine them with AND', async () => { + // Create a user with both allowQuery and blockQuery + const user = new UserEntity(); + user.id = 4; + user.name = 'bothuser'; + user.role = UserRoles.Admin; + user.allowQuery = { + type: SearchQueryTypes.directory, + text: '/allowed/path', + matchType: TextSearchQueryMatchTypes.exact_match + } as TextSearch; + user.blockQuery = { + type: SearchQueryTypes.directory, + text: '/blocked/path', + matchType: TextSearchQueryMatchTypes.exact_match, + negate: false + } as TextSearch; + + // Clone the original queries for later comparison + const originalAllowQuery = JSON.parse(JSON.stringify(user.allowQuery)); + const originalBlockQuery = JSON.parse(JSON.stringify(user.blockQuery)); + + // Mock the SearchManager.prepareAndBuildWhereQuery method + const mockProjectionQuery = new Brackets(qb => qb.where('complex combined query')); + ObjectManagers.getInstance().SearchManager = { + prepareAndBuildWhereQuery: (query: any) => { + // Verify the query is an AND combination of allowQuery and negated blockQuery + expect(query.type).to.be.eql(SearchQueryTypes.AND); + expect(query.list).to.have.lengthOf(2); + expect(query.list[0]).to.be.eql(originalAllowQuery); + expect(query.list[1].negate).to.be.true; + return Promise.resolve(mockProjectionQuery); + } + } as any; + + // Create the context + const context = await ObjectManagers.getInstance().buildContext(user); + + // Verify the context + expect(context).to.not.be.null; + expect(context.user).to.be.eql(user); + expect(context.projectionQuery).to.be.eql(mockProjectionQuery); + expect(context.user.projectionKey).to.be.a('string').and.not.empty; + // Verify the blockQuery was negated + expect((user.blockQuery as TextSearch).negate).to.be.true; + }); + + it('should generate consistent projectionKey for the same query', async () => { + // Create two identical users with the same query + const user1 = new UserEntity(); + user1.id = 5; + user1.name = 'user1'; + user1.role = UserRoles.Admin; + user1.allowQuery = { + type: SearchQueryTypes.directory, + text: '/allowed/path', + matchType: TextSearchQueryMatchTypes.exact_match + } as TextSearch; + + const user2 = new UserEntity(); + user2.id = 6; + user2.name = 'user2'; + user2.role = UserRoles.Admin; + user2.allowQuery = { + type: SearchQueryTypes.directory, + text: '/allowed/path', + matchType: TextSearchQueryMatchTypes.exact_match + } as TextSearch; + + // Mock the SearchManager.prepareAndBuildWhereQuery method + const mockProjectionQuery = new Brackets(qb => qb.where('directory.path = :path', {path: '/allowed/path'})); + ObjectManagers.getInstance().SearchManager = { + prepareAndBuildWhereQuery: (query: any) => { + return Promise.resolve(mockProjectionQuery); + } + } as any; + + // Create the contexts + const context1 = await ObjectManagers.getInstance().buildContext(user1); + const context2 = await ObjectManagers.getInstance().buildContext(user2); + + // Verify both users have the same projectionKey despite being different users + expect(context1.user.projectionKey).to.be.a('string').and.not.empty; + expect(context2.user.projectionKey).to.be.a('string').and.not.empty; + expect(context1.user.projectionKey).to.be.eql(context2.user.projectionKey); + }); + }); +}); diff --git a/test/backend/unit/model/sql/CoverManager.spec.ts b/test/backend/unit/model/sql/CoverManager.spec.ts index 5898506d..bb53837c 100644 --- a/test/backend/unit/model/sql/CoverManager.spec.ts +++ b/test/backend/unit/model/sql/CoverManager.spec.ts @@ -17,6 +17,7 @@ import {Utils} from '../../../../../src/common/Utils'; import {SQLConnection} from '../../../../../src/backend/model/database/SQLConnection'; import {DirectoryEntity} from '../../../../../src/backend/model/database/enitites/DirectoryEntity'; import {ClientSortingConfig} from '../../../../../src/common/config/public/ClientConfig'; +import {SessionContext} from '../../../../../src/backend/model/SessionContext'; // eslint-disable-next-line @typescript-eslint/no-var-requires const deepEqualInAnyOrder = require('deep-equal-in-any-order'); @@ -55,8 +56,8 @@ class GalleryManagerTest extends GalleryManager { return super.getDirIdAndTime(connection, directoryName, directoryParent); } - public async getParentDirFromId(connection: Connection, dir: number): Promise { - return super.getParentDirFromId(connection, dir); + public async getParentDirFromId(connection: Connection, session: SessionContext, dir: number): Promise { + return super.getParentDirFromId(connection, session, dir); } } @@ -179,7 +180,7 @@ describe('CoverManager', (sqlHelper: DBTestHelper) => { const conn = await SQLConnection.getConnection(); await conn.createQueryBuilder() - .update(DirectoryEntity).set({validCover: false}).execute(); + .update(DirectoryEntity).set({validCover: false}).execute(); expect(await pm.getPartialDirsWithoutCovers()).to.deep.equalInAnyOrder([dir, subDir, subDir2].map(d => partialDir(d))); }); @@ -272,7 +273,7 @@ describe('CoverManager', (sqlHelper: DBTestHelper) => { expect(subdir.validCover).to.equal(true); expect(subdir.cover.id).to.equal(p2.id); - // new version should invalidate + // The new version should invalidate await pm.onNewDataVersion(subDir as ParentDirectoryDTO); subdir = await selectDir(); expect(subdir.validCover).to.equal(false); @@ -280,13 +281,14 @@ describe('CoverManager', (sqlHelper: DBTestHelper) => { expect(subdir.cover.id).to.equal(p2.id); await conn.createQueryBuilder() - .update(DirectoryEntity) - .set({validCover: false, cover: null}).execute(); + .update(DirectoryEntity) + .set({validCover: false, cover: null}).execute(); expect((await selectDir()).cover).to.equal(null); + const session = new SessionContext(); - const res = await gm.getParentDirFromId(conn, - (await gm.getDirIdAndTime(conn, dir.name, dir.path)).id); + const res = await gm.getParentDirFromId(conn, session, + (await gm.getDirIdAndTime(conn, dir.name, dir.path)).id); subdir = await selectDir(); expect(subdir.validCover).to.equal(true); expect(subdir.cover.id).to.equal(p2.id); diff --git a/test/backend/unit/model/sql/GalleryManager.spec.ts b/test/backend/unit/model/sql/GalleryManager.spec.ts index a2b69206..5c33c110 100644 --- a/test/backend/unit/model/sql/GalleryManager.spec.ts +++ b/test/backend/unit/model/sql/GalleryManager.spec.ts @@ -1,7 +1,20 @@ import {DBTestHelper} from '../../../DBTestHelper'; import {GalleryManager} from '../../../../../src/backend/model/database/GalleryManager'; -import {ParentDirectoryDTO} from '../../../../../src/common/entities/DirectoryDTO'; -import {Connection} from 'typeorm'; +import {ParentDirectoryDTO, SubDirectoryDTO} from '../../../../../src/common/entities/DirectoryDTO'; +import {Connection, Brackets} from 'typeorm'; +import {SessionContext} from '../../../../../src/backend/model/SessionContext'; +import {SearchQueryTypes, TextSearchQueryMatchTypes} from '../../../../../src/common/entities/SearchQueryDTO'; +import {ObjectManagers} from '../../../../../src/backend/model/ObjectManagers'; +import * as path from 'path'; +import {SQLConnection} from '../../../../../src/backend/model/database/SQLConnection'; +import {TestHelper} from '../../../../TestHelper'; +import {PhotoDTO} from '../../../../../src/common/entities/PhotoDTO'; +import {VideoDTO} from '../../../../../src/common/entities/VideoDTO'; +import {FileDTO} from '../../../../../src/common/entities/FileDTO'; +import {Utils} from '../../../../../src/common/Utils'; +import {UserEntity} from '../../../../../src/backend/model/database/enitites/UserEntity'; +import {Config} from '../../../../../src/common/config/private/Config'; +import {SQLLogLevel} from '../../../../../src/common/config/private/PrivateConfig'; // eslint-disable-next-line @typescript-eslint/no-var-requires const deepEqualInAnyOrder = require('deep-equal-in-any-order'); @@ -15,6 +28,9 @@ const {expect} = chai; declare let describe: any; declare const before: any; declare const after: any; +declare const beforeEach: any; +declare const afterEach: any; +declare const it: any; const tmpDescribe = describe; describe = DBTestHelper.describe(); @@ -25,12 +41,191 @@ class GalleryManagerTest extends GalleryManager { return super.getDirIdAndTime(connection, directoryName, directoryParent); } - public async getParentDirFromId(connection: Connection, dir: number): Promise { - return super.getParentDirFromId(connection, dir); + public async getParentDirFromId(connection: Connection, session: SessionContext, dir: number): Promise { + return super.getParentDirFromId(connection, session, dir); } } describe('GalleryManager', (sqlHelper: DBTestHelper) => { describe = tmpDescribe; + let galleryManager: GalleryManagerTest; + let connection: Connection; + let dir: ParentDirectoryDTO; + let subDir: SubDirectoryDTO; + let subDir2: SubDirectoryDTO; + let p: PhotoDTO; + let p2: PhotoDTO; + let p3: PhotoDTO; + let p4: PhotoDTO; + let v: VideoDTO; + let gpx: FileDTO; + + const setUpGalleryTest = async (): Promise => { + // Create base directory structure + const directory: ParentDirectoryDTO = TestHelper.getDirectoryEntry(); + subDir = TestHelper.getDirectoryEntry(directory, 'The Phantom Menace'); + subDir2 = TestHelper.getDirectoryEntry(directory, 'Return of the Jedi'); + + // Create photo entries with specific names to match the test cases + p = TestHelper.getPhotoEntry1(directory); + p.name = 'photo1.jpg'; // Ensure we have a photo1 for the test case + p.metadata.creationDate = Date.now(); + p.metadata.creationDateOffset = '+02:00'; + + p2 = TestHelper.getPhotoEntry2(directory); + p2.name = 'photo2.jpg'; + p2.metadata.creationDate = Date.now() - 60 * 60 * 24 * 1000; // 1 day ago + p2.metadata.creationDateOffset = '+02:00'; + + // Create video entry + v = TestHelper.getVideoEntry1(directory); + v.metadata.creationDate = Date.now() - 60 * 60 * 24 * 7 * 1000; // 1 week ago + v.metadata.creationDateOffset = '+02:00'; + + // Create a GPX file + gpx = TestHelper.getRandomizedGPXEntry(directory); + + // Create photos in subdirectories + p3 = TestHelper.getPhotoEntry3(subDir); + p3.name = 'photo3.jpg'; + let d = new Date(); + d = Utils.addMonthToDate(d, -1); // 1 month ago + d.setDate(d.getDate() - 1); // minus 1 day + p3.metadata.creationDate = d.getTime(); + p3.metadata.creationDateOffset = '+02:00'; + + p4 = TestHelper.getPhotoEntry4(subDir2); + p4.name = 'photo4.jpg'; + d = new Date(); + // Set creation date to one year and one day earlier + p4.metadata.creationDate = d.getTime() - 60 * 60 * 24 * (Utils.isDateFromLeapYear(d) ? 367 : 366) * 1000; + p4.metadata.creationDateOffset = '+02:00'; + + // Persist the directory structure to the database + dir = await DBTestHelper.persistTestDir(directory); + subDir = dir.directories[0]; + subDir2 = dir.directories[1]; + + // Get the media items from the persisted directory + 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; + + v = (dir.media.filter(m => m.name === v.name)[0] as any); + v.directory = dir; + + if (dir.metaFile && dir.metaFile.length > 0) { + gpx = (dir.metaFile[0] as any); + gpx.directory = dir; + } + + p3 = (dir.directories[0].media[0] as any); + p3.directory = dir.directories[0]; + + p4 = (dir.directories[1].media[0] as any); + p4.directory = dir.directories[1]; + }; + + const setUpSqlDB = async () => { + await sqlHelper.initDB(); + await setUpGalleryTest(); + await ObjectManagers.getInstance().init(); + }; + + before(async () => { + await setUpSqlDB(); + galleryManager = new GalleryManagerTest(); + connection = await SQLConnection.getConnection(); + }); + + after(async () => { + await sqlHelper.clearDB(); + }); + + describe('Projection Query Tests', () => { + + it('should return all media when no projection query is provided', async () => { + // Setup a session with no projection query + const session = new SessionContext(); + + // Use the already persisted directory from our setup + expect(dir).to.not.be.null; + expect(dir.media).to.not.be.empty; + + // Get the directory contents without any filtering using our directory's name and path + const dirInfo = await galleryManager.getDirIdAndTime(connection, dir.name, dir.path); + expect(dirInfo).to.not.be.null; + + const directory = await galleryManager.getParentDirFromId(connection, session, dirInfo.id); + + // Verify that all media is returned + expect(directory.media).to.not.be.empty; + const originalMediaCount = directory.media.length; + expect(originalMediaCount).to.be.greaterThan(0); + }); + + it('should filter media based on the projection query', async () => { + + // Create a projection query that filters based on media name + const searchQuery = { + type: SearchQueryTypes.file_name, + text: 'photo1', // We've named our test photo specifically with this pattern + matchType: TextSearchQueryMatchTypes.like + }; + const session = await ObjectManagers.getInstance().buildContext({allowQuery: searchQuery} as any); + + + // Use the already persisted directory from our setup + expect(dir).to.not.be.null; + + // Get the directory contents with filtering using our directory's name and path + const dirInfo = await galleryManager.getDirIdAndTime(connection, dir.name, dir.path); + expect(dirInfo).to.not.be.null; + + // Get the directory contents with filtering + const directory = await galleryManager.getParentDirFromId(connection, session, dirInfo.id); + + // Verify that only media matching the query is returned + expect(directory.media).to.not.be.undefined; + + // All returned media should match the query (name contains 'photo1') + if (directory.media.length > 0) { + directory.media.forEach(media => { + expect(media.name.toLowerCase()).to.include('photo1'); + }); + } + + }); + + it('should return directory with empty media when projection filters out all media', async () => { + + const searchQuery = { + type: SearchQueryTypes.file_name, + text: 'this_file_name_does_not_exist_anywhere', + matchType: TextSearchQueryMatchTypes.like + }; + + + const session = await ObjectManagers.getInstance().buildContext({allowQuery: searchQuery} as any); + + + const dirInfo = await galleryManager.getDirIdAndTime(connection, dir.name, dir.path); + expect(dirInfo).to.not.be.null; + + const directory = await galleryManager.getParentDirFromId(connection, session, dirInfo.id); + + // The directory itself should still be returned + expect(directory).to.not.be.undefined; + expect(directory).to.not.be.null; + + // And media should be an empty array when nothing matches the projection + expect(directory.media).to.be.an('array'); + expect(directory.media.length).to.equal(0); + + }); + + }); }); diff --git a/test/backend/unit/model/sql/IndexingManager.spec.ts b/test/backend/unit/model/sql/IndexingManager.spec.ts index d0722b51..fae91e23 100644 --- a/test/backend/unit/model/sql/IndexingManager.spec.ts +++ b/test/backend/unit/model/sql/IndexingManager.spec.ts @@ -18,7 +18,8 @@ import * as path from 'path'; import {AlbumManager} from '../../../../../src/backend/model/database/AlbumManager'; import {SortByTypes} from '../../../../../src/common/entities/SortingMethods'; import {ClientSortingConfig} from '../../../../../src/common/config/public/ClientConfig'; -import { DiskManager } from '../../../../../src/backend/model/fileaccess/DiskManager'; +import {DiskManager} from '../../../../../src/backend/model/fileaccess/DiskManager'; +import {SessionContext} from '../../../../../src/backend/model/SessionContext'; // eslint-disable-next-line @typescript-eslint/no-var-requires const deepEqualInAnyOrder = require('deep-equal-in-any-order'); @@ -34,8 +35,8 @@ class GalleryManagerTest extends GalleryManager { return super.getDirIdAndTime(connection, directoryName, directoryParent); } - public async getParentDirFromId(connection: Connection, dir: number): Promise { - return super.getParentDirFromId(connection, dir); + public async getParentDirFromId(connection: Connection, session: SessionContext, dir: number): Promise { + return super.getParentDirFromId(connection, session, dir); } } @@ -135,6 +136,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { it('should support case sensitive file names', async () => { const gm = new GalleryManagerTest(); const im = new IndexingManagerTest(); + const session = new SessionContext(); const parent = TestHelper.getRandomizedDirectoryEntry(); const p1 = TestHelper.getRandomizedPhotoEntry(parent, 'Photo1'); @@ -146,7 +148,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { await im.saveToDB(Utils.clone(parent) as ParentDirectoryDTO); const conn = await SQLConnection.getConnection(); - const selected = await gm.getParentDirFromId(conn, + const selected = await gm.getParentDirFromId(conn, session, (await gm.getDirIdAndTime(conn, parent.name, parent.path)).id); DirectoryDTOUtils.removeReferences(selected); @@ -158,6 +160,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { it('should stop indexing on empty folder', async () => { const gm = new GalleryManagerTest(); + const session = new SessionContext(); ProjectPath.reset(); ProjectPath.ImageFolder = path.join(__dirname, '/../../../assets'); @@ -171,7 +174,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { '.' ); const conn = await SQLConnection.getConnection(); - const selected = await gm.getParentDirFromId(conn, + const selected = await gm.getParentDirFromId(conn, session, (await gm.getDirIdAndTime(conn, directoryPath.name, directoryPath.parent)).id); @@ -200,6 +203,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { it('should support case sensitive directory', async () => { const gm = new GalleryManagerTest(); const im = new IndexingManagerTest(); + const session = new SessionContext(); const parent = TestHelper.getRandomizedDirectoryEntry(null, 'parent'); const subDir1 = TestHelper.getRandomizedDirectoryEntry(parent, 'subDir'); @@ -213,7 +217,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { const conn = await SQLConnection.getConnection(); - const selected = await gm.getParentDirFromId(conn, + const selected = await gm.getParentDirFromId(conn, session, (await gm.getDirIdAndTime(conn, parent.name, parent.path)).id); DirectoryDTOUtils.removeReferences(selected); @@ -227,6 +231,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { it('should support case sensitive directory path', async () => { const gm = new GalleryManagerTest(); const im = new IndexingManagerTest(); + const session = new SessionContext(); const parent1 = TestHelper.getRandomizedDirectoryEntry(null, 'parent'); const parent2 = TestHelper.getRandomizedDirectoryEntry(null, 'PARENT'); @@ -243,7 +248,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { const conn = await SQLConnection.getConnection(); { - const selected = await gm.getParentDirFromId(conn, + const selected = await gm.getParentDirFromId(conn, session, (await gm.getDirIdAndTime(conn, parent1.name, parent1.path)).id); DirectoryDTOUtils.removeReferences(selected); @@ -253,7 +258,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { .to.deep.equalInAnyOrder(Utils.removeNullOrEmptyObj(indexifyReturn(parent1))); } { - const selected = await gm.getParentDirFromId(conn, + const selected = await gm.getParentDirFromId(conn, session, (await gm.getDirIdAndTime(conn, parent2.name, parent2.path)).id); DirectoryDTOUtils.removeReferences(selected); @@ -267,6 +272,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { it('should support emoji in names', async () => { const gm = new GalleryManagerTest(); const im = new IndexingManagerTest(); + const session = new SessionContext(); const parent = TestHelper.getRandomizedDirectoryEntry(null, 'parent dir 😀'); const p1 = TestHelper.getRandomizedPhotoEntry(parent, 'Photo1'); @@ -277,7 +283,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { const conn = await SQLConnection.getConnection(); - const selected = await gm.getParentDirFromId(conn, + const selected = await gm.getParentDirFromId(conn, session, (await gm.getDirIdAndTime(conn, parent.name, parent.path)).id); DirectoryDTOUtils.removeReferences(selected); @@ -290,7 +296,8 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { it('should select cover', async () => { const selectDirectory = async (gmTest: GalleryManagerTest, dir: DirectoryBaseDTO): Promise => { const conn = await SQLConnection.getConnection(); - const selected = await gmTest.getParentDirFromId(conn, + const session = new SessionContext(); + const selected = await gmTest.getParentDirFromId(conn, session, (await gmTest.getDirIdAndTime(conn, dir.name, dir.path)).id); DirectoryDTOUtils.removeReferences(selected); @@ -346,6 +353,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { it('should save parent after child', async () => { const gm = new GalleryManagerTest(); const im = new IndexingManagerTest(); + const session = new SessionContext(); const parent = TestHelper.getRandomizedDirectoryEntry(null, 'parentDir'); const p1 = TestHelper.getRandomizedPhotoEntry(parent, 'Photo1'); @@ -370,7 +378,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { const conn = await SQLConnection.getConnection(); - const selected = await gm.getParentDirFromId(conn, + const selected = await gm.getParentDirFromId(conn, session, (await gm.getDirIdAndTime(conn, parent.name, parent.path)).id); DirectoryDTOUtils.removeReferences(selected); @@ -384,6 +392,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { it('should save root parent after child', async () => { const gm = new GalleryManagerTest(); const im = new IndexingManagerTest(); + const session = new SessionContext(); const parent = TestHelper.getRandomizedDirectoryEntry(null, '.'); const p1 = TestHelper.getRandomizedPhotoEntry(parent, 'Photo1'); @@ -408,7 +417,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { await im.saveToDB(Utils.clone(parent) as ParentDirectoryDTO); const conn = await SQLConnection.getConnection(); - const selected = await gm.getParentDirFromId(conn, + const selected = await gm.getParentDirFromId(conn, session, (await gm.getDirIdAndTime(conn, parent.name, parent.path)).id); DirectoryDTOUtils.removeReferences(selected); @@ -421,6 +430,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { it('should save parent directory', async () => { const gm = new GalleryManagerTest(); const im = new IndexingManagerTest(); + const session = new SessionContext(); const parent = TestHelper.getRandomizedDirectoryEntry(); const p1 = TestHelper.getRandomizedPhotoEntry(parent, 'Photo1'); @@ -438,7 +448,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { await im.saveToDB(Utils.clone(parent) as ParentDirectoryDTO); const conn = await SQLConnection.getConnection(); - const selected = await gm.getParentDirFromId(conn, + const selected = await gm.getParentDirFromId(conn, session, (await gm.getDirIdAndTime(conn, parent.name, parent.path)).id); DirectoryDTOUtils.removeReferences(selected); @@ -452,6 +462,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { it('should save photos with extreme parameters', async () => { const gm = new GalleryManagerTest(); const im = new IndexingManagerTest(); + const session = new SessionContext(); const parent = TestHelper.getRandomizedDirectoryEntry(); const p1 = TestHelper.getRandomizedPhotoEntry(parent, 'Photo1'); @@ -474,7 +485,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { await im.saveToDB(Utils.clone(parent) as ParentDirectoryDTO); const conn = await SQLConnection.getConnection(); - const selected = await gm.getParentDirFromId(conn, + const selected = await gm.getParentDirFromId(conn, session, (await gm.getDirIdAndTime(conn, parent.name, parent.path)).id); DirectoryDTOUtils.removeReferences(selected); @@ -486,6 +497,8 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { it('should skip meta files', async () => { const gm = new GalleryManagerTest(); const im = new IndexingManagerTest(); + const session = new SessionContext(); + const parent = TestHelper.getRandomizedDirectoryEntry(); const p1 = TestHelper.getRandomizedPhotoEntry(parent, 'Photo1'); const p2 = TestHelper.getRandomizedPhotoEntry(parent, 'Photo2'); @@ -500,7 +513,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { Config.MetaFile.markdown = false; Config.MetaFile.pg2conf = false; const conn = await SQLConnection.getConnection(); - const selected = await gm.getParentDirFromId(conn, + const selected = await gm.getParentDirFromId(conn, session, (await gm.getDirIdAndTime(conn, parent.name, parent.path)).id); delete parent.metaFile; @@ -513,6 +526,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { it('should update sub directory', async () => { const gm = new GalleryManagerTest(); const im = new IndexingManagerTest(); + const session = new SessionContext(); const parent = TestHelper.getRandomizedDirectoryEntry(); parent.name = 'parent'; @@ -531,7 +545,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { await im.saveToDB(Utils.clone(subDir) as ParentDirectoryDTO); const conn = await SQLConnection.getConnection(); - const selected = await gm.getParentDirFromId(conn, + const selected = await gm.getParentDirFromId(conn, session, (await gm.getDirIdAndTime(conn, subDir.name, subDir.path)).id); // subDir.isPartial = true; @@ -549,6 +563,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { const conn = await SQLConnection.getConnection(); const gm = new GalleryManagerTest(); const im = new IndexingManagerTest(); + const session = new SessionContext(); Config.MetaFile.gpx = true; Config.MetaFile.markdown = true; Config.MetaFile.pg2conf = true; @@ -571,7 +586,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { await Promise.all([s1, s2, s3]); - const selected = await gm.getParentDirFromId(conn, + const selected = await gm.getParentDirFromId(conn, session, (await gm.getDirIdAndTime(conn, parent.name, parent.path)).id); DirectoryDTOUtils.removeReferences(selected); @@ -587,6 +602,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { it('should reset DB', async () => { const gm = new GalleryManagerTest(); const im = new IndexingManagerTest(); + const session = new SessionContext(); const parent = TestHelper.getRandomizedDirectoryEntry(); const p1 = TestHelper.getRandomizedPhotoEntry(parent, 'Photo1'); @@ -596,7 +612,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { await im.saveToDB(Utils.clone(parent) as ParentDirectoryDTO); const conn = await SQLConnection.getConnection(); - const selected = await gm.getParentDirFromId(conn, + const selected = await gm.getParentDirFromId(conn, session, (await gm.getDirIdAndTime(conn, parent.name, parent.path)).id); DirectoryDTOUtils.removeReferences(selected); @@ -614,6 +630,8 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { const conn = await SQLConnection.getConnection(); const gm = new GalleryManagerTest(); const im = new IndexingManagerTest(); + const session = new SessionContext(); + Config.MetaFile.gpx = true; Config.MetaFile.markdown = true; Config.MetaFile.pg2conf = true; @@ -628,7 +646,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { DirectoryDTOUtils.removeReferences(parent); await im.saveToDB(subDir as ParentDirectoryDTO); - const selected = await gm.getParentDirFromId(conn, + const selected = await gm.getParentDirFromId(conn, session, (await gm.getDirIdAndTime(conn, subDir.name, subDir.path)).id); expect(selected.media.length).to.equal(subDir.media.length); }) as any).timeout(40000); @@ -641,12 +659,13 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { ProjectPath.ImageFolder = path.join(__dirname, '/../../../assets'); const im = new IndexingManagerTest(); const gm = new GalleryManagerTest(); + const session = new SessionContext(); const d = await DiskManager.scanDirectory('/'); await im.saveToDB(d); - const dir = await gm.listDirectory('/'); + const dir = await gm.listDirectory(session, '/'); expect(dir.metaFile).to.be.an('array'); expect(dir.metaFile).to.be.deep.equal([ @@ -687,6 +706,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { // @ts-ignore fs.statSync = () => ({ctime: new Date(dirTime), mtime: new Date(dirTime)}); const gm = new GalleryManagerTest(); + const session = new SessionContext(); gm.getDirIdAndTime = () => { return Promise.resolve(indexedTime as any); }; @@ -699,15 +719,15 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => { }; indexedTime.lastScanned = null; - expect(await gm.listDirectory('./')).to.be.equal('indexing'); + expect(await gm.listDirectory(session, './')).to.be.equal('indexing'); indexedTime.lastModified = 0; dirTime = 1; - expect(await gm.listDirectory('./')).to.be.equal('indexing'); + expect(await gm.listDirectory(session, './')).to.be.equal('indexing'); indexedTime.lastScanned = 10; indexedTime.lastModified = 1; dirTime = 1; - expect(await gm.listDirectory('./')).to.be.equal(indexedTime); - expect(await gm.listDirectory('./', 1, 10)) + expect(await gm.listDirectory(session, './')).to.be.equal(indexedTime); + expect(await gm.listDirectory(session, './', 1, 10)) .to.be.equal(null); diff --git a/test/backend/unit/model/sql/SearchManager.spec.ts b/test/backend/unit/model/sql/SearchManager.spec.ts index e4597d0a..86b46569 100644 --- a/test/backend/unit/model/sql/SearchManager.spec.ts +++ b/test/backend/unit/model/sql/SearchManager.spec.ts @@ -25,12 +25,9 @@ import { TextSearchQueryMatchTypes, ToDateSearch } from '../../../../../src/common/entities/SearchQueryDTO'; -import {IndexingManager} from '../../../../../src/backend/model/database/IndexingManager'; import {DirectoryBaseDTO, ParentDirectoryDTO, SubDirectoryDTO} from '../../../../../src/common/entities/DirectoryDTO'; import {TestHelper} from '../../../../TestHelper'; import {ObjectManagers} from '../../../../../src/backend/model/ObjectManagers'; -import {GalleryManager} from '../../../../../src/backend/model/database/GalleryManager'; -import {Connection} from 'typeorm'; import {GPSMetadata, PhotoDTO, PhotoMetadata} from '../../../../../src/common/entities/PhotoDTO'; import {VideoDTO} from '../../../../../src/common/entities/VideoDTO'; import {AutoCompleteItem} from '../../../../../src/common/entities/AutoCompleteItem'; @@ -38,7 +35,6 @@ import {Config} from '../../../../../src/common/config/private/Config'; import {SearchQueryParser} from '../../../../../src/common/SearchQueryParser'; import {FileDTO} from '../../../../../src/common/entities/FileDTO'; import {SortByTypes} from '../../../../../src/common/entities/SortingMethods'; -import {LogLevel} from '../../../../../src/common/config/private/PrivateConfig'; // eslint-disable-next-line @typescript-eslint/no-var-requires const deepEqualInAnyOrder = require('deep-equal-in-any-order'); @@ -56,13 +52,6 @@ const tmpDescribe = describe; describe = DBTestHelper.describe(); // fake it os IDE plays nicely (recognize the test) -class IndexingManagerTest extends IndexingManager { - - public async saveToDB(scannedDirectory: ParentDirectoryDTO): Promise { - return super.saveToDB(scannedDirectory); - } -} - class SearchManagerTest extends SearchManager { public flattenSameOfQueries(query: SearchQueryDTO): SearchQueryDTO { @@ -71,16 +60,6 @@ class SearchManagerTest extends SearchManager { } -class GalleryManagerTest extends GalleryManager { - - public async getDirIdAndTime(connection: Connection, directoryName: string, directoryParent: string) { - return super.getDirIdAndTime(connection, directoryName, directoryParent); - } - - public async getParentDirFromId(connection: Connection, dir: number): Promise { - return super.getParentDirFromId(connection, dir); - } -} describe('SearchManager', (sqlHelper: DBTestHelper) => { describe = tmpDescribe; @@ -115,27 +94,27 @@ describe('SearchManager', (sqlHelper: DBTestHelper) => { subDir2 = TestHelper.getDirectoryEntry(directory, 'Return of the Jedi'); p = TestHelper.getPhotoEntry1(directory); p.metadata.creationDate = Date.now(); - p.metadata.creationDateOffset = "+02:00"; + p.metadata.creationDateOffset = '+02:00'; p2 = TestHelper.getPhotoEntry2(directory); p2.metadata.creationDate = Date.now() - 60 * 60 * 24 * 1000; - p2.metadata.creationDateOffset = "+02:00"; + p2.metadata.creationDateOffset = '+02:00'; v = TestHelper.getVideoEntry1(directory); v.metadata.creationDate = Date.now() - 60 * 60 * 24 * 7 * 1000; - v.metadata.creationDateOffset = "+02:00"; + v.metadata.creationDateOffset = '+02:00'; gpx = TestHelper.getRandomizedGPXEntry(directory); p4 = TestHelper.getPhotoEntry4(subDir2); let d = new Date(); //set creation date to one year and one day earlier p4.metadata.creationDate = d.getTime() - 60 * 60 * 24 * (Utils.isDateFromLeapYear(d) ? 367 : 366) * 1000; - p4.metadata.creationDateOffset = "+02:00"; + p4.metadata.creationDateOffset = '+02:00'; const pFaceLessTmp = TestHelper.getPhotoEntry3(subDir); delete pFaceLessTmp.metadata.faces; d = new Date(); //we create a date 1 month and 1 day before now d = Utils.addMonthToDate(d, -1); //subtract 1 month in the "human way" - d.setDate(d.getDate()-1); //subtract 1 day + d.setDate(d.getDate() - 1); //subtract 1 day pFaceLessTmp.metadata.creationDate = d.getTime(); - pFaceLessTmp.metadata.creationDateOffset = "+02:00"; + pFaceLessTmp.metadata.creationDateOffset = '+02:00'; dir = await DBTestHelper.persistTestDir(directory); subDir = dir.directories[0];