1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2025-11-23 22:24:44 +02:00

Implement user allow and blocklist and apply it to general directory result #1015

This commit is contained in:
Patrik J. Braun
2025-08-14 16:59:07 +02:00
parent b660b3c9f8
commit 602a3a08c0
24 changed files with 1007 additions and 312 deletions

View File

@@ -30,6 +30,7 @@ import {
} from '../src/common/entities/SearchQueryDTO'; } from '../src/common/entities/SearchQueryDTO';
import {defaultQueryKeywords, QueryKeywords, SearchQueryParser} from '../src/common/SearchQueryParser'; import {defaultQueryKeywords, QueryKeywords, SearchQueryParser} from '../src/common/SearchQueryParser';
import {ParentDirectoryDTO} from '../src/common/entities/DirectoryDTO'; import {ParentDirectoryDTO} from '../src/common/entities/DirectoryDTO';
import {SessionContext} from '../src/backend/model/SessionContext';
export interface BenchmarkResult { export interface BenchmarkResult {
@@ -268,7 +269,8 @@ export class BenchmarkRunner {
const queue = ['/']; const queue = ['/'];
while (queue.length > 0) { while (queue.length > 0) {
const dirPath = queue.shift(); 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))); dir.directories.forEach((d): number => queue.push(path.join(d.path + d.name)));
if (biggest < dir.media.length) { if (biggest < dir.media.length) {
biggestPath = path.join(dir.path + dir.name); biggestPath = path.join(dir.path + dir.name);

View File

@@ -41,6 +41,7 @@ export class GalleryMWs {
try { try {
const directory = const directory =
await ObjectManagers.getInstance().GalleryManager.listDirectory( await ObjectManagers.getInstance().GalleryManager.listDirectory(
req.session.context,
directoryName, directoryName,
parseInt( parseInt(
req.query[QueryParams.gallery.knownLastModified] as string, req.query[QueryParams.gallery.knownLastModified] as string,
@@ -57,12 +58,12 @@ export class GalleryMWs {
return next(); return next();
} }
if ( if (
req.session['user'].permissions && req.session.context?.user.permissions &&
req.session['user'].permissions.length > 0 && req.session.context?.user.permissions.length > 0 &&
req.session['user'].permissions[0] !== '/*' req.session.context?.user.permissions[0] !== '/*'
) { ) {
directory.directories = directory.directories.filter((d): boolean => 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); req.resultPipe = new ContentWrapper(directory, null);

View File

@@ -4,7 +4,7 @@ import {NotificationManager} from '../model/NotifocationManager';
export class NotificationMWs { export class NotificationMWs {
public static list(req: Request, res: Response, next: NextFunction): void { 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; req.resultPipe = NotificationManager.notifications;
} else if (NotificationManager.notifications.length > 0) { } else if (NotificationManager.notifications.length > 0) {
req.resultPipe = NotificationManager.HasNotification; req.resultPipe = NotificationManager.HasNotification;

View File

@@ -31,16 +31,17 @@ export class RenderingMWs {
res: Response, res: Response,
next: NextFunction next: NextFunction
): void { ): void {
if (!req.session['user']) { if (!req.session.context?.user) {
return next(new ErrorDTO(ErrorCodes.GENERAL_ERROR, 'User not exists')); return next(new ErrorDTO(ErrorCodes.GENERAL_ERROR, 'User not exists'));
} }
const user = { const user = {
id: req.session['user'].id, id: req.session.context.user.id,
name: req.session['user'].name, name: req.session.context.user.name,
role: req.session['user'].role, role: req.session.context.user.role,
usedSharingKey: req.session['user'].usedSharingKey, usedSharingKey: req.session.context.user.usedSharingKey,
permissions: req.session['user'].permissions, permissions: req.session.context.user.permissions,
projectionKey: req.session.context.user.projectionKey,
} as UserDTO; } as UserDTO;
@@ -142,8 +143,8 @@ export class RenderingMWs {
!( !(
forcedDebug || forcedDebug ||
(req.session && (req.session &&
req.session['user'] && req.session.context?.user &&
req.session['user'].role >= UserRoles.Developer) req.session.context?.user.role >= UserRoles.Developer)
) )
) { ) {
delete err.detailsStr; delete err.detailsStr;

View File

@@ -102,9 +102,9 @@ export class SharingMWs {
sharingKey, sharingKey,
path: directoryName, path: directoryName,
password: createSharing.password, password: createSharing.password,
creator: req.session['user'], creator: req.session.context?.user,
expires: expires:
createSharing.valid >= 0 // if === -1 its forever createSharing.valid >= 0 // if === -1 it's forever
? Date.now() + createSharing.valid ? Date.now() + createSharing.valid
: new Date(9999, 0, 1).getTime(), // never expire : new Date(9999, 0, 1).getTime(), // never expire
includeSubfolders: createSharing.includeSubfolders, includeSubfolders: createSharing.includeSubfolders,
@@ -155,7 +155,7 @@ export class SharingMWs {
updateSharing.password && updateSharing.password !== '' updateSharing.password && updateSharing.password !== ''
? updateSharing.password ? updateSharing.password
: null, : null,
creator: req.session['user'], creator: req.session.context?.user,
expires: expires:
updateSharing.valid >= 0 // if === -1 its forever updateSharing.valid >= 0 // if === -1 its forever
? Date.now() + updateSharing.valid ? Date.now() + updateSharing.valid
@@ -165,7 +165,7 @@ export class SharingMWs {
}; };
try { try {
const forceUpdate = req.session['user'].role >= UserRoles.Admin; const forceUpdate = req.session.context.user.role >= UserRoles.Admin;
req.resultPipe = req.resultPipe =
await ObjectManagers.getInstance().SharingManager.updateSharing( await ObjectManagers.getInstance().SharingManager.updateSharing(
sharing, sharing,
@@ -203,9 +203,9 @@ export class SharingMWs {
try { try {
// Check if user has the right to delete sharing. // 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); 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.')); 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'] || '/'); const dir = path.normalize(req.params['directory'] || '/');
try { try {
if (req.session['user'].role >= UserRoles.Admin) { if (req.session.context?.user.role >= UserRoles.Admin) {
req.resultPipe = req.resultPipe =
await ObjectManagers.getInstance().SharingManager.listAllForDir(dir); await ObjectManagers.getInstance().SharingManager.listAllForDir(dir);
} else { } else {
req.resultPipe = req.resultPipe =
await ObjectManagers.getInstance().SharingManager.listAllForDir(dir, req.session['user']); await ObjectManagers.getInstance().SharingManager.listAllForDir(dir, req.session.context?.user);
} }
return next(); return next();
} catch (err) { } catch (err) {

View File

@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import {LoginCredential} from '../../../common/entities/LoginCredential'; import {LoginCredential} from '../../../common/entities/LoginCredential';
import {UserDTO} from '../../../common/entities/UserDTO'; import {UserDTO} from '../../../common/entities/UserDTO';
import {SessionContext} from '../../model/SessionContext';
declare global { declare global {
namespace Express { namespace Express {
@@ -10,16 +11,16 @@ declare global {
loginCredential?: LoginCredential; loginCredential?: LoginCredential;
}; };
locale?: string; locale?: string;
session: {
context?: SessionContext;
rememberMe?: boolean;
};
} }
interface Response { interface Response {
tpl?: Record<string, any>; tpl?: Record<string, any>;
} }
interface Session {
user?: UserDTO;
rememberMe?: boolean;
}
} }
} }

View File

@@ -18,16 +18,17 @@ export class AuthenticationMWs {
next: NextFunction next: NextFunction
): Promise<void> { ): Promise<void> {
if (Config.Users.authenticationRequired === false) { if (Config.Users.authenticationRequired === false) {
req.session['user'] = { const user = {
name: UserRoles[Config.Users.unAuthenticatedUserRole], name: UserRoles[Config.Users.unAuthenticatedUserRole],
role: Config.Users.unAuthenticatedUserRole, role: Config.Users.unAuthenticatedUserRole,
} as UserDTO; } as UserDTO;
req.session.context = await ObjectManagers.getInstance().buildContext(user);
return next(); return next();
} }
try { try {
const user = await AuthenticationMWs.getSharingUser(req); const user = await AuthenticationMWs.getSharingUser(req);
if (user) { if (user) {
req.session['user'] = user; req.session.context = await ObjectManagers.getInstance().buildContext(user);
return next(); return next();
} }
// eslint-disable-next-line no-empty // eslint-disable-next-line no-empty
@@ -43,28 +44,29 @@ export class AuthenticationMWs {
next: NextFunction next: NextFunction
): Promise<void> { ): Promise<void> {
if (Config.Users.authenticationRequired === false) { if (Config.Users.authenticationRequired === false) {
req.session['user'] = { const user = {
name: UserRoles[Config.Users.unAuthenticatedUserRole], name: UserRoles[Config.Users.unAuthenticatedUserRole],
role: Config.Users.unAuthenticatedUserRole, role: Config.Users.unAuthenticatedUserRole,
} as UserDTO; } as UserDTO;
req.session.context = await ObjectManagers.getInstance().buildContext(user);
return next(); return next();
} }
// if already authenticated, do not try to use sharing authentication // if already authenticated, do not try to use sharing authentication
if (typeof req.session['user'] !== 'undefined') { if (typeof req.session.context !== 'undefined') {
return next(); return next();
} }
try { try {
const user = await AuthenticationMWs.getSharingUser(req); const user = await AuthenticationMWs.getSharingUser(req);
if (user) { if (user) {
req.session['user'] = user; req.session.context = await ObjectManagers.getInstance().buildContext(user);
return next(); return next();
} }
} catch (err) { } catch (err) {
return next(new ErrorDTO(ErrorCodes.CREDENTIAL_NOT_FOUND, null, 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); res.status(401);
return next( return next(
new ErrorDTO(ErrorCodes.NOT_AUTHENTICATED, 'Not authenticated') new ErrorDTO(ErrorCodes.NOT_AUTHENTICATED, 'Not authenticated')
@@ -104,7 +106,7 @@ export class AuthenticationMWs {
} }
if ( if (
!UserDTOUtils.isDirectoryPathAvailable(p, req.session['user'].permissions) !UserDTOUtils.isDirectoryPathAvailable(p, req.session.context.user.permissions)
) { ) {
return res.sendStatus(403); return res.sendStatus(403);
} }
@@ -121,7 +123,7 @@ export class AuthenticationMWs {
res: Response, res: Response,
next: NextFunction next: NextFunction
): void { ): void {
if (req.session['user'].role < role) { if (req.session.context?.user.role < role) {
return next(new ErrorDTO(ErrorCodes.NOT_AUTHORISED)); return next(new ErrorDTO(ErrorCodes.NOT_AUTHORISED));
} }
return next(); return next();
@@ -170,12 +172,13 @@ export class AuthenticationMWs {
sharingPath += '*'; sharingPath += '*';
} }
req.session['user'] = { const user = {
name: 'Guest', name: 'Guest',
role: UserRoles.LimitedGuest, role: UserRoles.LimitedGuest,
permissions: [sharingPath], permissions: [sharingPath],
usedSharingKey: sharing.sharingKey, usedSharingKey: sharing.sharingKey,
} as UserDTO; } as UserDTO;
req.session.context = await ObjectManagers.getInstance().buildContext(user);
return next(); return next();
} catch (err) { } catch (err) {
return next(new ErrorDTO(ErrorCodes.GENERAL_ERROR, null, err)); return next(new ErrorDTO(ErrorCodes.GENERAL_ERROR, null, err));
@@ -187,7 +190,7 @@ export class AuthenticationMWs {
res: Response, res: Response,
next: NextFunction next: NextFunction
): void { ): void {
if (typeof req.session['user'] !== 'undefined') { if (typeof req.session.context?.user !== 'undefined') {
return next(new ErrorDTO(ErrorCodes.ALREADY_AUTHENTICATED)); return next(new ErrorDTO(ErrorCodes.ALREADY_AUTHENTICATED));
} }
return next(); return next();
@@ -202,7 +205,7 @@ export class AuthenticationMWs {
return res.sendStatus(404); return res.sendStatus(404);
} }
// not enough parameter // not enough parameters
if ( if (
typeof req.body === 'undefined' || typeof req.body === 'undefined' ||
typeof req.body.loginCredential === 'undefined' || typeof req.body.loginCredential === 'undefined' ||
@@ -226,7 +229,7 @@ export class AuthenticationMWs {
}) })
); );
delete user.password; delete user.password;
req.session['user'] = user; req.session.context = await ObjectManagers.getInstance().buildContext(user);
if (req.body.loginCredential.rememberMe) { if (req.body.loginCredential.rememberMe) {
req.sessionOptions.expires = new Date( req.sessionOptions.expires = new Date(
Date.now() + Config.Server.sessionTimeout Date.now() + Config.Server.sessionTimeout
@@ -247,7 +250,7 @@ export class AuthenticationMWs {
} }
public static logout(req: Request, res: Response, next: NextFunction): void { public static logout(req: Request, res: Response, next: NextFunction): void {
delete req.session['user']; delete req.session.context;
return next(); return next();
} }

View File

@@ -16,7 +16,7 @@ export class UserRequestConstrainsMWs {
) { ) {
return next(); 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)); return next(new ErrorDTO(ErrorCodes.NOT_AUTHORISED));
} }
@@ -35,7 +35,7 @@ export class UserRequestConstrainsMWs {
return next(); 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)); return next(new ErrorDTO(ErrorCodes.NOT_AUTHORISED));
} }
@@ -54,7 +54,7 @@ export class UserRequestConstrainsMWs {
return next(); 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(); return next();
} }

View File

@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-var-requires */
import * as crypto from 'crypto';
import {SQLConnection} from './database/SQLConnection'; import {SQLConnection} from './database/SQLConnection';
import {Logger} from '../Logger'; import {Logger} from '../Logger';
import {LocationManager} from './database/LocationManager'; import {LocationManager} from './database/LocationManager';
@@ -15,6 +16,9 @@ import {PersonManager} from './database/PersonManager';
import {SharingManager} from './database/SharingManager'; import {SharingManager} from './database/SharingManager';
import {IObjectManager} from './database/IObjectManager'; import {IObjectManager} from './database/IObjectManager';
import {ExtensionManager} from './extension/ExtensionManager'; 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]'; const LOG_TAG = '[ObjectManagers]';
@@ -269,4 +273,27 @@ export class ObjectManagers {
this.extensionManager = value; this.extensionManager = value;
this.managers.push(this.extensionManager as IObjectManager); this.managers.push(this.extensionManager as IObjectManager);
} }
async buildContext(user: UserEntity): Promise<SessionContext> {
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;
}
} }

View File

@@ -0,0 +1,8 @@
import {Brackets} from 'typeorm';
import {UserDTO} from '../../common/entities/UserDTO';
export class SessionContext {
user: UserDTO;
projectionQuery: Brackets;
}

View File

@@ -6,7 +6,7 @@ import {SQLConnection} from './SQLConnection';
import {PhotoEntity} from './enitites/PhotoEntity'; import {PhotoEntity} from './enitites/PhotoEntity';
import {ProjectPath} from '../../ProjectPath'; import {ProjectPath} from '../../ProjectPath';
import {Config} from '../../../common/config/private/Config'; import {Config} from '../../../common/config/private/Config';
import {Connection} from 'typeorm'; import {Brackets, Connection} from 'typeorm';
import {MediaEntity} from './enitites/MediaEntity'; import {MediaEntity} from './enitites/MediaEntity';
import {VideoEntity} from './enitites/VideoEntity'; import {VideoEntity} from './enitites/VideoEntity';
import {Logger} from '../../Logger'; import {Logger} from '../../Logger';
@@ -14,6 +14,7 @@ import {ObjectManagers} from '../ObjectManagers';
import {DuplicatesDTO} from '../../../common/entities/DuplicatesDTO'; import {DuplicatesDTO} from '../../../common/entities/DuplicatesDTO';
import {ReIndexingSensitivity} from '../../../common/config/private/PrivateConfig'; 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]'; const LOG_TAG = '[GalleryManager]';
@@ -32,6 +33,7 @@ export class GalleryManager {
} }
public async listDirectory( public async listDirectory(
session: SessionContext,
relativeDirectoryName: string, relativeDirectoryName: string,
knownLastModified?: number, knownLastModified?: number,
knownLastScanned?: number knownLastScanned?: number
@@ -103,7 +105,7 @@ export class GalleryManager {
return ret; return ret;
} }
// not indexed since a while, index it in a lazy manner // not indexed since a while, index it lazily
if ( if (
(Date.now() - dir.lastScanned > (Date.now() - dir.lastScanned >
Config.Indexing.cachedFolderTimeout && Config.Indexing.cachedFolderTimeout &&
@@ -125,7 +127,7 @@ export class GalleryManager {
.IndexingManager.indexDirectory(relativeDirectoryName) .IndexingManager.indexDirectory(relativeDirectoryName)
.catch(console.error); .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 // never scanned (deep indexed), do it and return with it
@@ -339,8 +341,33 @@ export class GalleryManager {
protected async getParentDirFromId( protected async getParentDirFromId(
connection: Connection, connection: Connection,
session: SessionContext,
partialDirId: number partialDirId: number
): Promise<ParentDirectoryDTO> { ): Promise<ParentDirectoryDTO> {
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 const query = connection
.getRepository(DirectoryEntity) .getRepository(DirectoryEntity)
.createQueryBuilder('directory') .createQueryBuilder('directory')
@@ -348,7 +375,10 @@ export class GalleryManager {
id: partialDirId id: partialDirId
}) })
.leftJoinAndSelect('directory.directories', 'directories') .leftJoinAndSelect('directory.directories', 'directories')
.leftJoinAndSelect('directory.media', 'media') .leftJoinAndSelect(
'directory.media',
'media', sql, params
)
.leftJoinAndSelect('directories.cover', 'cover') .leftJoinAndSelect('directories.cover', 'cover')
.leftJoinAndSelect('cover.directory', 'coverDirectory') .leftJoinAndSelect('cover.directory', 'coverDirectory')
.select([ .select([

View File

@@ -292,15 +292,16 @@ export class ConfigDiagnostics {
settings.SearchQuery 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. * Removes unsupported image formats.
* It is possible that some OS support one or the other image formats (like Mac os does with HEIC) * It is possible that some OS support one or the other image formats (like macOS does with HEIC),
* , but others not. * but others do not.
* Those formats are added to the config, but dynamically removed. * Those formats are added to the config but dynamically removed.
* @param config * @param config
*/ */
static async removeUnsupportedPhotoExtensions(config: ClientPhotoConfig): Promise<void> { static async removeUnsupportedPhotoExtensions(config: ClientPhotoConfig): Promise<void> {
@@ -328,7 +329,7 @@ export class ConfigDiagnostics {
} as MediaRendererInput, true } as MediaRendererInput, true
); );
} catch (e) { } 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.'); Logger.verbose(LOG_TAG, 'The current OS does not support the following photo format:' + ext + ', removing it form config.');
config.supportedFormats.splice(i, 1); config.supportedFormats.splice(i, 1);
removedSome = true; removedSome = true;

View File

@@ -57,7 +57,7 @@ export class ExpressRouteWrapper implements IExtensionRESTRoute {
this.router[this.func](fullPaths, this.router[this.func](fullPaths,
...(this.getAuthMWs(minRole).concat([ ...(this.getAuthMWs(minRole).concat([
async (req: Request, res: Response, next: NextFunction) => { 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(); next();
}, },
RenderingMWs.renderResult RenderingMWs.renderResult

View File

@@ -11,7 +11,7 @@ import * as fs from 'fs';
const LOG_TAG = '[ExtensionConfigWrapper]'; 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 { export class ExtensionConfigWrapper {

View File

@@ -74,13 +74,14 @@ export class PublicRouter {
res.tpl = {}; res.tpl = {};
res.tpl.user = null; res.tpl.user = null;
if (req.session['user']) { if (req.session.context?.user) {
res.tpl.user = { res.tpl.user = {
id: req.session['user'].id, id: req.session.context.user.id,
name: req.session['user'].name, name: req.session.context.user.name,
role: req.session['user'].role, role: req.session.context.user.role,
usedSharingKey: req.session['user'].usedSharingKey, usedSharingKey: req.session.context.user.usedSharingKey,
permissions: req.session['user'].permissions, permissions:req.session.context.user.permissions,
projectionKey:req.session.context.user.projectionKey,
} as UserDTO; } as UserDTO;
} }

View File

@@ -16,6 +16,7 @@ export interface UserDTO {
role: UserRoles; role: UserRoles;
usedSharingKey?: string; usedSharingKey?: string;
permissions: string[]; // user can only see these permissions. if ends with *, its recursive 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 = { export const UserDTOUtils = {

View File

@@ -15,6 +15,7 @@ import {TestHelper} from '../TestHelper';
import {VideoDTO} from '../../src/common/entities/VideoDTO'; import {VideoDTO} from '../../src/common/entities/VideoDTO';
import {PhotoDTO} from '../../src/common/entities/PhotoDTO'; import {PhotoDTO} from '../../src/common/entities/PhotoDTO';
import {Logger} from '../../src/backend/Logger'; import {Logger} from '../../src/backend/Logger';
import {SessionContext} from '../../src/backend/model/SessionContext';
declare let describe: any; declare let describe: any;
const savedDescribe = describe; const savedDescribe = describe;
@@ -32,8 +33,8 @@ class GalleryManagerTest extends GalleryManager {
return super.getDirIdAndTime(connection, directoryName, directoryParent); return super.getDirIdAndTime(connection, directoryName, directoryParent);
} }
public async getParentDirFromId(connection: Connection, dir: number): Promise<ParentDirectoryDTO> { public async getParentDirFromId(connection: Connection, session: SessionContext, dir: number): Promise<ParentDirectoryDTO> {
return super.getParentDirFromId(connection, dir); return super.getParentDirFromId(connection, session, dir);
} }
} }
@@ -125,13 +126,14 @@ export class DBTestHelper {
} }
const gm = new GalleryManagerTest(); const gm = new GalleryManagerTest();
const session = new SessionContext();
const dir = await gm.getParentDirFromId(connection, const dir = await gm.getParentDirFromId(connection, session,
(await gm.getDirIdAndTime(connection, directory.name, path.join(directory.path, path.sep))).id); (await gm.getDirIdAndTime(connection, directory.name, path.join(directory.path, path.sep))).id);
const populateDir = async (d: DirectoryBaseDTO) => { const populateDir = async (d: DirectoryBaseDTO) => {
for (let i = 0; i < d.directories.length; i++) { for (let i = 0; i < d.directories.length; i++) {
d.directories[i] = await gm.getParentDirFromId(connection, d.directories[i] = await gm.getParentDirFromId(connection, session,
(await gm.getDirIdAndTime(connection, d.directories[i].name, (await gm.getDirIdAndTime(connection, d.directories[i].name,
path.join(DiskManager.pathFromParent(d), path.sep))).id); path.join(DiskManager.pathFromParent(d), path.sep))).id);
await populateDir(d.directories[i]); await populateDir(d.directories[i]);

View File

@@ -7,6 +7,7 @@ import {ObjectManagers} from '../../../../../src/backend/model/ObjectManagers';
import {Config} from '../../../../../src/common/config/private/Config'; import {Config} from '../../../../../src/common/config/private/Config';
import * as path from 'path'; import * as path from 'path';
import {UserManager} from '../../../../../src/backend/model/database/UserManager'; import {UserManager} from '../../../../../src/backend/model/database/UserManager';
import {SearchQueryTypes, TextSearchQueryMatchTypes} from '../../../../../src/common/entities/SearchQueryDTO';
declare const describe: any; declare const describe: any;
@@ -23,7 +24,9 @@ describe('Authentication middleware', () => {
it('should call next on authenticated', (done: (err?: any) => void) => { it('should call next on authenticated', (done: (err?: any) => void) => {
const req: any = { const req: any = {
session: { session: {
context: {
user: 'A user' user: 'A user'
}
}, },
sessionOptions: {}, sessionOptions: {},
query: {}, query: {},
@@ -72,7 +75,9 @@ describe('Authentication middleware', () => {
const req = { const req = {
session: { session: {
context: {
user: {permissions: null as string[]} user: {permissions: null as string[]}
}
}, },
sessionOptions: {}, sessionOptions: {},
query: {}, query: {},
@@ -93,14 +98,14 @@ describe('Authentication middleware', () => {
}; };
it('should catch unauthorized path usage', async () => { 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('/sub/subsub')).to.be.eql('ok');
expect(await test('/test')).to.be.eql(403); expect(await test('/test')).to.be.eql(403);
expect(await 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/test')).to.be.eql(403);
expect(await test('/sub/subsub/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); 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/subsub2')).to.be.eql('ok');
expect(await test('/sub/subsub')).to.be.eql('ok'); expect(await test('/sub/subsub')).to.be.eql('ok');
expect(await test('/test')).to.be.eql(403); 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/test')).to.be.eql(403);
expect(await test('/sub/subsub/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); 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('/b')).to.be.eql(403);
expect(await test('/sub')).to.be.eql(403); expect(await test('/sub')).to.be.eql(403);
expect(await test('/sub/subsub2')).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) => { it('should call next error on authenticated', (done: (err?: any) => void) => {
const req: any = { const req: any = {
session: { session: {
context: {
user: 'A user' user: 'A user'
}
}, },
sessionOptions: {}, sessionOptions: {},
}; };
@@ -162,9 +169,11 @@ describe('Authentication middleware', () => {
it('should call next on authorised', (done: (err?: any) => void) => { it('should call next on authorised', (done: (err?: any) => void) => {
const req: any = { const req: any = {
session: { session: {
context: {
user: { user: {
role: UserRoles.LimitedGuest role: UserRoles.LimitedGuest
} }
}
}, },
sessionOptions: {} sessionOptions: {}
}; };
@@ -183,9 +192,11 @@ describe('Authentication middleware', () => {
it('should call next with error on not authorised', (done: (err?: any) => void) => { it('should call next with error on not authorised', (done: (err?: any) => void) => {
const req: any = { const req: any = {
session: { session: {
context: {
user: { user: {
role: UserRoles.LimitedGuest role: UserRoles.LimitedGuest
} }
}
}, },
sessionOptions: {} sessionOptions: {}
}; };
@@ -200,8 +211,8 @@ describe('Authentication middleware', () => {
}); });
describe('login', () => { describe('login', () => {
beforeEach(() => { beforeEach(async () => {
ObjectManagers.reset(); await ObjectManagers.reset();
}); });
describe('should call input ErrorDTO next on missing...', () => { describe('should call input ErrorDTO next on missing...', () => {
@@ -303,19 +314,226 @@ describe('Authentication middleware', () => {
query: {}, query: {},
params: {} params: {}
}; };
const testUser = 'test user' as any;
const testContext = {user: testUser};
const next: any = (err: ErrorDTO) => { const next: any = (err: ErrorDTO) => {
expect(err).to.be.undefined; expect(err).to.be.undefined;
expect(req.session['user']).to.be.eql('test user'); expect(req.session.context).to.be.eql(testContext);
done(); done();
}; };
ObjectManagers.getInstance().UserManager = { ObjectManagers.getInstance().UserManager = {
findOne: (filter: never) => { findOne: (filter: never) => {
return Promise.resolve('test user' as any); return Promise.resolve(testUser);
} }
} as UserManager; } 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); 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) => { it('should call next on logout', (done: (err?: any) => void) => {
const req: any = { const req: any = {
session: { session: {
context: {
user: { user: {
role: UserRoles.LimitedGuest role: UserRoles.LimitedGuest
} }
} }
}
}; };
const next: any = (err: ErrorDTO) => { const next: any = (err: ErrorDTO) => {
try { try {
expect(err).to.be.undefined; expect(err).to.be.undefined;
expect(req.user).to.be.undefined; expect(req.session.context).to.be.undefined;
done(); done();
} catch (err) { } catch (err) {
done(err); done(err);

View File

@@ -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);
});
});
});

View File

@@ -17,6 +17,7 @@ import {Utils} from '../../../../../src/common/Utils';
import {SQLConnection} from '../../../../../src/backend/model/database/SQLConnection'; import {SQLConnection} from '../../../../../src/backend/model/database/SQLConnection';
import {DirectoryEntity} from '../../../../../src/backend/model/database/enitites/DirectoryEntity'; import {DirectoryEntity} from '../../../../../src/backend/model/database/enitites/DirectoryEntity';
import {ClientSortingConfig} from '../../../../../src/common/config/public/ClientConfig'; import {ClientSortingConfig} from '../../../../../src/common/config/public/ClientConfig';
import {SessionContext} from '../../../../../src/backend/model/SessionContext';
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const deepEqualInAnyOrder = require('deep-equal-in-any-order'); const deepEqualInAnyOrder = require('deep-equal-in-any-order');
@@ -55,8 +56,8 @@ class GalleryManagerTest extends GalleryManager {
return super.getDirIdAndTime(connection, directoryName, directoryParent); return super.getDirIdAndTime(connection, directoryName, directoryParent);
} }
public async getParentDirFromId(connection: Connection, dir: number): Promise<ParentDirectoryDTO> { public async getParentDirFromId(connection: Connection, session: SessionContext, dir: number): Promise<ParentDirectoryDTO> {
return super.getParentDirFromId(connection, dir); return super.getParentDirFromId(connection, session, dir);
} }
} }
@@ -272,7 +273,7 @@ describe('CoverManager', (sqlHelper: DBTestHelper) => {
expect(subdir.validCover).to.equal(true); expect(subdir.validCover).to.equal(true);
expect(subdir.cover.id).to.equal(p2.id); expect(subdir.cover.id).to.equal(p2.id);
// new version should invalidate // The new version should invalidate
await pm.onNewDataVersion(subDir as ParentDirectoryDTO); await pm.onNewDataVersion(subDir as ParentDirectoryDTO);
subdir = await selectDir(); subdir = await selectDir();
expect(subdir.validCover).to.equal(false); expect(subdir.validCover).to.equal(false);
@@ -284,8 +285,9 @@ describe('CoverManager', (sqlHelper: DBTestHelper) => {
.set({validCover: false, cover: null}).execute(); .set({validCover: false, cover: null}).execute();
expect((await selectDir()).cover).to.equal(null); expect((await selectDir()).cover).to.equal(null);
const session = new SessionContext();
const res = await gm.getParentDirFromId(conn, const res = await gm.getParentDirFromId(conn, session,
(await gm.getDirIdAndTime(conn, dir.name, dir.path)).id); (await gm.getDirIdAndTime(conn, dir.name, dir.path)).id);
subdir = await selectDir(); subdir = await selectDir();
expect(subdir.validCover).to.equal(true); expect(subdir.validCover).to.equal(true);

View File

@@ -1,7 +1,20 @@
import {DBTestHelper} from '../../../DBTestHelper'; import {DBTestHelper} from '../../../DBTestHelper';
import {GalleryManager} from '../../../../../src/backend/model/database/GalleryManager'; import {GalleryManager} from '../../../../../src/backend/model/database/GalleryManager';
import {ParentDirectoryDTO} from '../../../../../src/common/entities/DirectoryDTO'; import {ParentDirectoryDTO, SubDirectoryDTO} from '../../../../../src/common/entities/DirectoryDTO';
import {Connection} from 'typeorm'; 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 // eslint-disable-next-line @typescript-eslint/no-var-requires
const deepEqualInAnyOrder = require('deep-equal-in-any-order'); const deepEqualInAnyOrder = require('deep-equal-in-any-order');
@@ -15,6 +28,9 @@ const {expect} = chai;
declare let describe: any; declare let describe: any;
declare const before: any; declare const before: any;
declare const after: any; declare const after: any;
declare const beforeEach: any;
declare const afterEach: any;
declare const it: any;
const tmpDescribe = describe; const tmpDescribe = describe;
describe = DBTestHelper.describe(); describe = DBTestHelper.describe();
@@ -25,12 +41,191 @@ class GalleryManagerTest extends GalleryManager {
return super.getDirIdAndTime(connection, directoryName, directoryParent); return super.getDirIdAndTime(connection, directoryName, directoryParent);
} }
public async getParentDirFromId(connection: Connection, dir: number): Promise<ParentDirectoryDTO> { public async getParentDirFromId(connection: Connection, session: SessionContext, dir: number): Promise<ParentDirectoryDTO> {
return super.getParentDirFromId(connection, dir); return super.getParentDirFromId(connection, session, dir);
} }
} }
describe('GalleryManager', (sqlHelper: DBTestHelper) => { describe('GalleryManager', (sqlHelper: DBTestHelper) => {
describe = tmpDescribe; 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<void> => {
// 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);
});
});
}); });

View File

@@ -19,6 +19,7 @@ import {AlbumManager} from '../../../../../src/backend/model/database/AlbumManag
import {SortByTypes} from '../../../../../src/common/entities/SortingMethods'; import {SortByTypes} from '../../../../../src/common/entities/SortingMethods';
import {ClientSortingConfig} from '../../../../../src/common/config/public/ClientConfig'; 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 // eslint-disable-next-line @typescript-eslint/no-var-requires
const deepEqualInAnyOrder = require('deep-equal-in-any-order'); const deepEqualInAnyOrder = require('deep-equal-in-any-order');
@@ -34,8 +35,8 @@ class GalleryManagerTest extends GalleryManager {
return super.getDirIdAndTime(connection, directoryName, directoryParent); return super.getDirIdAndTime(connection, directoryName, directoryParent);
} }
public async getParentDirFromId(connection: Connection, dir: number): Promise<ParentDirectoryDTO> { public async getParentDirFromId(connection: Connection, session: SessionContext, dir: number): Promise<ParentDirectoryDTO> {
return super.getParentDirFromId(connection, dir); return super.getParentDirFromId(connection, session, dir);
} }
} }
@@ -135,6 +136,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
it('should support case sensitive file names', async () => { it('should support case sensitive file names', async () => {
const gm = new GalleryManagerTest(); const gm = new GalleryManagerTest();
const im = new IndexingManagerTest(); const im = new IndexingManagerTest();
const session = new SessionContext();
const parent = TestHelper.getRandomizedDirectoryEntry(); const parent = TestHelper.getRandomizedDirectoryEntry();
const p1 = TestHelper.getRandomizedPhotoEntry(parent, 'Photo1'); const p1 = TestHelper.getRandomizedPhotoEntry(parent, 'Photo1');
@@ -146,7 +148,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
await im.saveToDB(Utils.clone(parent) as ParentDirectoryDTO); await im.saveToDB(Utils.clone(parent) as ParentDirectoryDTO);
const conn = await SQLConnection.getConnection(); 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); (await gm.getDirIdAndTime(conn, parent.name, parent.path)).id);
DirectoryDTOUtils.removeReferences(selected); DirectoryDTOUtils.removeReferences(selected);
@@ -158,6 +160,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
it('should stop indexing on empty folder', async () => { it('should stop indexing on empty folder', async () => {
const gm = new GalleryManagerTest(); const gm = new GalleryManagerTest();
const session = new SessionContext();
ProjectPath.reset(); ProjectPath.reset();
ProjectPath.ImageFolder = path.join(__dirname, '/../../../assets'); ProjectPath.ImageFolder = path.join(__dirname, '/../../../assets');
@@ -171,7 +174,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
'.' '.'
); );
const conn = await SQLConnection.getConnection(); 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); (await gm.getDirIdAndTime(conn, directoryPath.name, directoryPath.parent)).id);
@@ -200,6 +203,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
it('should support case sensitive directory', async () => { it('should support case sensitive directory', async () => {
const gm = new GalleryManagerTest(); const gm = new GalleryManagerTest();
const im = new IndexingManagerTest(); const im = new IndexingManagerTest();
const session = new SessionContext();
const parent = TestHelper.getRandomizedDirectoryEntry(null, 'parent'); const parent = TestHelper.getRandomizedDirectoryEntry(null, 'parent');
const subDir1 = TestHelper.getRandomizedDirectoryEntry(parent, 'subDir'); const subDir1 = TestHelper.getRandomizedDirectoryEntry(parent, 'subDir');
@@ -213,7 +217,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
const conn = await SQLConnection.getConnection(); 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); (await gm.getDirIdAndTime(conn, parent.name, parent.path)).id);
DirectoryDTOUtils.removeReferences(selected); DirectoryDTOUtils.removeReferences(selected);
@@ -227,6 +231,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
it('should support case sensitive directory path', async () => { it('should support case sensitive directory path', async () => {
const gm = new GalleryManagerTest(); const gm = new GalleryManagerTest();
const im = new IndexingManagerTest(); const im = new IndexingManagerTest();
const session = new SessionContext();
const parent1 = TestHelper.getRandomizedDirectoryEntry(null, 'parent'); const parent1 = TestHelper.getRandomizedDirectoryEntry(null, 'parent');
const parent2 = TestHelper.getRandomizedDirectoryEntry(null, 'PARENT'); const parent2 = TestHelper.getRandomizedDirectoryEntry(null, 'PARENT');
@@ -243,7 +248,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
const conn = await SQLConnection.getConnection(); 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); (await gm.getDirIdAndTime(conn, parent1.name, parent1.path)).id);
DirectoryDTOUtils.removeReferences(selected); DirectoryDTOUtils.removeReferences(selected);
@@ -253,7 +258,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
.to.deep.equalInAnyOrder(Utils.removeNullOrEmptyObj(indexifyReturn(parent1))); .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); (await gm.getDirIdAndTime(conn, parent2.name, parent2.path)).id);
DirectoryDTOUtils.removeReferences(selected); DirectoryDTOUtils.removeReferences(selected);
@@ -267,6 +272,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
it('should support emoji in names', async () => { it('should support emoji in names', async () => {
const gm = new GalleryManagerTest(); const gm = new GalleryManagerTest();
const im = new IndexingManagerTest(); const im = new IndexingManagerTest();
const session = new SessionContext();
const parent = TestHelper.getRandomizedDirectoryEntry(null, 'parent dir 😀'); const parent = TestHelper.getRandomizedDirectoryEntry(null, 'parent dir 😀');
const p1 = TestHelper.getRandomizedPhotoEntry(parent, 'Photo1'); const p1 = TestHelper.getRandomizedPhotoEntry(parent, 'Photo1');
@@ -277,7 +283,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
const conn = await SQLConnection.getConnection(); 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); (await gm.getDirIdAndTime(conn, parent.name, parent.path)).id);
DirectoryDTOUtils.removeReferences(selected); DirectoryDTOUtils.removeReferences(selected);
@@ -290,7 +296,8 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
it('should select cover', async () => { it('should select cover', async () => {
const selectDirectory = async (gmTest: GalleryManagerTest, dir: DirectoryBaseDTO): Promise<ParentDirectoryDTO> => { const selectDirectory = async (gmTest: GalleryManagerTest, dir: DirectoryBaseDTO): Promise<ParentDirectoryDTO> => {
const conn = await SQLConnection.getConnection(); 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); (await gmTest.getDirIdAndTime(conn, dir.name, dir.path)).id);
DirectoryDTOUtils.removeReferences(selected); DirectoryDTOUtils.removeReferences(selected);
@@ -346,6 +353,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
it('should save parent after child', async () => { it('should save parent after child', async () => {
const gm = new GalleryManagerTest(); const gm = new GalleryManagerTest();
const im = new IndexingManagerTest(); const im = new IndexingManagerTest();
const session = new SessionContext();
const parent = TestHelper.getRandomizedDirectoryEntry(null, 'parentDir'); const parent = TestHelper.getRandomizedDirectoryEntry(null, 'parentDir');
const p1 = TestHelper.getRandomizedPhotoEntry(parent, 'Photo1'); const p1 = TestHelper.getRandomizedPhotoEntry(parent, 'Photo1');
@@ -370,7 +378,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
const conn = await SQLConnection.getConnection(); 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); (await gm.getDirIdAndTime(conn, parent.name, parent.path)).id);
DirectoryDTOUtils.removeReferences(selected); DirectoryDTOUtils.removeReferences(selected);
@@ -384,6 +392,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
it('should save root parent after child', async () => { it('should save root parent after child', async () => {
const gm = new GalleryManagerTest(); const gm = new GalleryManagerTest();
const im = new IndexingManagerTest(); const im = new IndexingManagerTest();
const session = new SessionContext();
const parent = TestHelper.getRandomizedDirectoryEntry(null, '.'); const parent = TestHelper.getRandomizedDirectoryEntry(null, '.');
const p1 = TestHelper.getRandomizedPhotoEntry(parent, 'Photo1'); const p1 = TestHelper.getRandomizedPhotoEntry(parent, 'Photo1');
@@ -408,7 +417,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
await im.saveToDB(Utils.clone(parent) as ParentDirectoryDTO); await im.saveToDB(Utils.clone(parent) as ParentDirectoryDTO);
const conn = await SQLConnection.getConnection(); 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); (await gm.getDirIdAndTime(conn, parent.name, parent.path)).id);
DirectoryDTOUtils.removeReferences(selected); DirectoryDTOUtils.removeReferences(selected);
@@ -421,6 +430,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
it('should save parent directory', async () => { it('should save parent directory', async () => {
const gm = new GalleryManagerTest(); const gm = new GalleryManagerTest();
const im = new IndexingManagerTest(); const im = new IndexingManagerTest();
const session = new SessionContext();
const parent = TestHelper.getRandomizedDirectoryEntry(); const parent = TestHelper.getRandomizedDirectoryEntry();
const p1 = TestHelper.getRandomizedPhotoEntry(parent, 'Photo1'); const p1 = TestHelper.getRandomizedPhotoEntry(parent, 'Photo1');
@@ -438,7 +448,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
await im.saveToDB(Utils.clone(parent) as ParentDirectoryDTO); await im.saveToDB(Utils.clone(parent) as ParentDirectoryDTO);
const conn = await SQLConnection.getConnection(); 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); (await gm.getDirIdAndTime(conn, parent.name, parent.path)).id);
DirectoryDTOUtils.removeReferences(selected); DirectoryDTOUtils.removeReferences(selected);
@@ -452,6 +462,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
it('should save photos with extreme parameters', async () => { it('should save photos with extreme parameters', async () => {
const gm = new GalleryManagerTest(); const gm = new GalleryManagerTest();
const im = new IndexingManagerTest(); const im = new IndexingManagerTest();
const session = new SessionContext();
const parent = TestHelper.getRandomizedDirectoryEntry(); const parent = TestHelper.getRandomizedDirectoryEntry();
const p1 = TestHelper.getRandomizedPhotoEntry(parent, 'Photo1'); const p1 = TestHelper.getRandomizedPhotoEntry(parent, 'Photo1');
@@ -474,7 +485,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
await im.saveToDB(Utils.clone(parent) as ParentDirectoryDTO); await im.saveToDB(Utils.clone(parent) as ParentDirectoryDTO);
const conn = await SQLConnection.getConnection(); 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); (await gm.getDirIdAndTime(conn, parent.name, parent.path)).id);
DirectoryDTOUtils.removeReferences(selected); DirectoryDTOUtils.removeReferences(selected);
@@ -486,6 +497,8 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
it('should skip meta files', async () => { it('should skip meta files', async () => {
const gm = new GalleryManagerTest(); const gm = new GalleryManagerTest();
const im = new IndexingManagerTest(); const im = new IndexingManagerTest();
const session = new SessionContext();
const parent = TestHelper.getRandomizedDirectoryEntry(); const parent = TestHelper.getRandomizedDirectoryEntry();
const p1 = TestHelper.getRandomizedPhotoEntry(parent, 'Photo1'); const p1 = TestHelper.getRandomizedPhotoEntry(parent, 'Photo1');
const p2 = TestHelper.getRandomizedPhotoEntry(parent, 'Photo2'); const p2 = TestHelper.getRandomizedPhotoEntry(parent, 'Photo2');
@@ -500,7 +513,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
Config.MetaFile.markdown = false; Config.MetaFile.markdown = false;
Config.MetaFile.pg2conf = false; Config.MetaFile.pg2conf = false;
const conn = await SQLConnection.getConnection(); 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); (await gm.getDirIdAndTime(conn, parent.name, parent.path)).id);
delete parent.metaFile; delete parent.metaFile;
@@ -513,6 +526,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
it('should update sub directory', async () => { it('should update sub directory', async () => {
const gm = new GalleryManagerTest(); const gm = new GalleryManagerTest();
const im = new IndexingManagerTest(); const im = new IndexingManagerTest();
const session = new SessionContext();
const parent = TestHelper.getRandomizedDirectoryEntry(); const parent = TestHelper.getRandomizedDirectoryEntry();
parent.name = 'parent'; parent.name = 'parent';
@@ -531,7 +545,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
await im.saveToDB(Utils.clone(subDir) as ParentDirectoryDTO); await im.saveToDB(Utils.clone(subDir) as ParentDirectoryDTO);
const conn = await SQLConnection.getConnection(); 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); (await gm.getDirIdAndTime(conn, subDir.name, subDir.path)).id);
// subDir.isPartial = true; // subDir.isPartial = true;
@@ -549,6 +563,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
const conn = await SQLConnection.getConnection(); const conn = await SQLConnection.getConnection();
const gm = new GalleryManagerTest(); const gm = new GalleryManagerTest();
const im = new IndexingManagerTest(); const im = new IndexingManagerTest();
const session = new SessionContext();
Config.MetaFile.gpx = true; Config.MetaFile.gpx = true;
Config.MetaFile.markdown = true; Config.MetaFile.markdown = true;
Config.MetaFile.pg2conf = true; Config.MetaFile.pg2conf = true;
@@ -571,7 +586,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
await Promise.all([s1, s2, s3]); 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); (await gm.getDirIdAndTime(conn, parent.name, parent.path)).id);
DirectoryDTOUtils.removeReferences(selected); DirectoryDTOUtils.removeReferences(selected);
@@ -587,6 +602,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
it('should reset DB', async () => { it('should reset DB', async () => {
const gm = new GalleryManagerTest(); const gm = new GalleryManagerTest();
const im = new IndexingManagerTest(); const im = new IndexingManagerTest();
const session = new SessionContext();
const parent = TestHelper.getRandomizedDirectoryEntry(); const parent = TestHelper.getRandomizedDirectoryEntry();
const p1 = TestHelper.getRandomizedPhotoEntry(parent, 'Photo1'); const p1 = TestHelper.getRandomizedPhotoEntry(parent, 'Photo1');
@@ -596,7 +612,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
await im.saveToDB(Utils.clone(parent) as ParentDirectoryDTO); await im.saveToDB(Utils.clone(parent) as ParentDirectoryDTO);
const conn = await SQLConnection.getConnection(); 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); (await gm.getDirIdAndTime(conn, parent.name, parent.path)).id);
DirectoryDTOUtils.removeReferences(selected); DirectoryDTOUtils.removeReferences(selected);
@@ -614,6 +630,8 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
const conn = await SQLConnection.getConnection(); const conn = await SQLConnection.getConnection();
const gm = new GalleryManagerTest(); const gm = new GalleryManagerTest();
const im = new IndexingManagerTest(); const im = new IndexingManagerTest();
const session = new SessionContext();
Config.MetaFile.gpx = true; Config.MetaFile.gpx = true;
Config.MetaFile.markdown = true; Config.MetaFile.markdown = true;
Config.MetaFile.pg2conf = true; Config.MetaFile.pg2conf = true;
@@ -628,7 +646,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
DirectoryDTOUtils.removeReferences(parent); DirectoryDTOUtils.removeReferences(parent);
await im.saveToDB(subDir as ParentDirectoryDTO); 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); (await gm.getDirIdAndTime(conn, subDir.name, subDir.path)).id);
expect(selected.media.length).to.equal(subDir.media.length); expect(selected.media.length).to.equal(subDir.media.length);
}) as any).timeout(40000); }) as any).timeout(40000);
@@ -641,12 +659,13 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
ProjectPath.ImageFolder = path.join(__dirname, '/../../../assets'); ProjectPath.ImageFolder = path.join(__dirname, '/../../../assets');
const im = new IndexingManagerTest(); const im = new IndexingManagerTest();
const gm = new GalleryManagerTest(); const gm = new GalleryManagerTest();
const session = new SessionContext();
const d = await DiskManager.scanDirectory('/'); const d = await DiskManager.scanDirectory('/');
await im.saveToDB(d); 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.an('array');
expect(dir.metaFile).to.be.deep.equal([ expect(dir.metaFile).to.be.deep.equal([
@@ -687,6 +706,7 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
// @ts-ignore // @ts-ignore
fs.statSync = () => ({ctime: new Date(dirTime), mtime: new Date(dirTime)}); fs.statSync = () => ({ctime: new Date(dirTime), mtime: new Date(dirTime)});
const gm = new GalleryManagerTest(); const gm = new GalleryManagerTest();
const session = new SessionContext();
gm.getDirIdAndTime = () => { gm.getDirIdAndTime = () => {
return Promise.resolve(indexedTime as any); return Promise.resolve(indexedTime as any);
}; };
@@ -699,15 +719,15 @@ describe('IndexingManager', (sqlHelper: DBTestHelper) => {
}; };
indexedTime.lastScanned = null; indexedTime.lastScanned = null;
expect(await gm.listDirectory('./')).to.be.equal('indexing'); expect(await gm.listDirectory(session, './')).to.be.equal('indexing');
indexedTime.lastModified = 0; indexedTime.lastModified = 0;
dirTime = 1; dirTime = 1;
expect(await gm.listDirectory('./')).to.be.equal('indexing'); expect(await gm.listDirectory(session, './')).to.be.equal('indexing');
indexedTime.lastScanned = 10; indexedTime.lastScanned = 10;
indexedTime.lastModified = 1; indexedTime.lastModified = 1;
dirTime = 1; dirTime = 1;
expect(await gm.listDirectory('./')).to.be.equal(indexedTime); expect(await gm.listDirectory(session, './')).to.be.equal(indexedTime);
expect(await gm.listDirectory('./', 1, 10)) expect(await gm.listDirectory(session, './', 1, 10))
.to.be.equal(null); .to.be.equal(null);

View File

@@ -25,12 +25,9 @@ import {
TextSearchQueryMatchTypes, TextSearchQueryMatchTypes,
ToDateSearch ToDateSearch
} from '../../../../../src/common/entities/SearchQueryDTO'; } from '../../../../../src/common/entities/SearchQueryDTO';
import {IndexingManager} from '../../../../../src/backend/model/database/IndexingManager';
import {DirectoryBaseDTO, ParentDirectoryDTO, SubDirectoryDTO} from '../../../../../src/common/entities/DirectoryDTO'; import {DirectoryBaseDTO, ParentDirectoryDTO, SubDirectoryDTO} from '../../../../../src/common/entities/DirectoryDTO';
import {TestHelper} from '../../../../TestHelper'; import {TestHelper} from '../../../../TestHelper';
import {ObjectManagers} from '../../../../../src/backend/model/ObjectManagers'; 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 {GPSMetadata, PhotoDTO, PhotoMetadata} from '../../../../../src/common/entities/PhotoDTO';
import {VideoDTO} from '../../../../../src/common/entities/VideoDTO'; import {VideoDTO} from '../../../../../src/common/entities/VideoDTO';
import {AutoCompleteItem} from '../../../../../src/common/entities/AutoCompleteItem'; 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 {SearchQueryParser} from '../../../../../src/common/SearchQueryParser';
import {FileDTO} from '../../../../../src/common/entities/FileDTO'; import {FileDTO} from '../../../../../src/common/entities/FileDTO';
import {SortByTypes} from '../../../../../src/common/entities/SortingMethods'; import {SortByTypes} from '../../../../../src/common/entities/SortingMethods';
import {LogLevel} from '../../../../../src/common/config/private/PrivateConfig';
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const deepEqualInAnyOrder = require('deep-equal-in-any-order'); 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) describe = DBTestHelper.describe(); // fake it os IDE plays nicely (recognize the test)
class IndexingManagerTest extends IndexingManager {
public async saveToDB(scannedDirectory: ParentDirectoryDTO): Promise<void> {
return super.saveToDB(scannedDirectory);
}
}
class SearchManagerTest extends SearchManager { class SearchManagerTest extends SearchManager {
public flattenSameOfQueries(query: SearchQueryDTO): SearchQueryDTO { 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<ParentDirectoryDTO> {
return super.getParentDirFromId(connection, dir);
}
}
describe('SearchManager', (sqlHelper: DBTestHelper) => { describe('SearchManager', (sqlHelper: DBTestHelper) => {
describe = tmpDescribe; describe = tmpDescribe;
@@ -115,19 +94,19 @@ describe('SearchManager', (sqlHelper: DBTestHelper) => {
subDir2 = TestHelper.getDirectoryEntry(directory, 'Return of the Jedi'); subDir2 = TestHelper.getDirectoryEntry(directory, 'Return of the Jedi');
p = TestHelper.getPhotoEntry1(directory); p = TestHelper.getPhotoEntry1(directory);
p.metadata.creationDate = Date.now(); p.metadata.creationDate = Date.now();
p.metadata.creationDateOffset = "+02:00"; p.metadata.creationDateOffset = '+02:00';
p2 = TestHelper.getPhotoEntry2(directory); p2 = TestHelper.getPhotoEntry2(directory);
p2.metadata.creationDate = Date.now() - 60 * 60 * 24 * 1000; p2.metadata.creationDate = Date.now() - 60 * 60 * 24 * 1000;
p2.metadata.creationDateOffset = "+02:00"; p2.metadata.creationDateOffset = '+02:00';
v = TestHelper.getVideoEntry1(directory); v = TestHelper.getVideoEntry1(directory);
v.metadata.creationDate = Date.now() - 60 * 60 * 24 * 7 * 1000; v.metadata.creationDate = Date.now() - 60 * 60 * 24 * 7 * 1000;
v.metadata.creationDateOffset = "+02:00"; v.metadata.creationDateOffset = '+02:00';
gpx = TestHelper.getRandomizedGPXEntry(directory); gpx = TestHelper.getRandomizedGPXEntry(directory);
p4 = TestHelper.getPhotoEntry4(subDir2); p4 = TestHelper.getPhotoEntry4(subDir2);
let d = new Date(); let d = new Date();
//set creation date to one year and one day earlier //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.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); const pFaceLessTmp = TestHelper.getPhotoEntry3(subDir);
delete pFaceLessTmp.metadata.faces; delete pFaceLessTmp.metadata.faces;
d = new Date(); d = new Date();
@@ -135,7 +114,7 @@ describe('SearchManager', (sqlHelper: DBTestHelper) => {
d = Utils.addMonthToDate(d, -1); //subtract 1 month in the "human way" 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.creationDate = d.getTime();
pFaceLessTmp.metadata.creationDateOffset = "+02:00"; pFaceLessTmp.metadata.creationDateOffset = '+02:00';
dir = await DBTestHelper.persistTestDir(directory); dir = await DBTestHelper.persistTestDir(directory);
subDir = dir.directories[0]; subDir = dir.directories[0];