diff --git a/src/backend/model/SessionContext.ts b/src/backend/model/SessionContext.ts index 97d61d41..9dd98734 100644 --- a/src/backend/model/SessionContext.ts +++ b/src/backend/model/SessionContext.ts @@ -6,6 +6,7 @@ export class SessionContext { user: ContextUser; // New structured projection with prebuilt SQL and params projectionQuery?: Brackets; + projectionQueryForSubDir?: Brackets; // only the directory part of the query, where it filters 'directories' instead of 'directory' aliases hasDirectoryProjection: boolean; } diff --git a/src/backend/model/database/GalleryManager.ts b/src/backend/model/database/GalleryManager.ts index bff706a8..91809718 100644 --- a/src/backend/model/database/GalleryManager.ts +++ b/src/backend/model/database/GalleryManager.ts @@ -15,7 +15,6 @@ import {DuplicatesDTO} from '../../../common/entities/DuplicatesDTO'; import {ReIndexingSensitivity} from '../../../common/config/private/PrivateConfig'; import {DiskManager} from '../fileaccess/DiskManager'; import {SessionContext} from '../SessionContext'; -import {ProjectedDirectoryCacheEntity} from './enitites/ProjectedDirectoryCacheEntity'; const LOG_TAG = '[GalleryManager]'; @@ -339,16 +338,6 @@ export class GalleryManager { partialDirId: number ): Promise { - const select = [ - 'directory', - 'directories', - 'cover.name', - 'coverDirectory.name', - 'coverDirectory.path', - 'dcover.name', - 'dcoverDirectory.name', - 'dcoverDirectory.path', - ]; const query = connection .getRepository(DirectoryEntity) .createQueryBuilder('directory') @@ -375,6 +364,12 @@ export class GalleryManager { 'dcoverDirectory.path', ]); + // search does not return a directory if that is recursively having 0 media + // gallery listing should otherwise, we won't be able to trigger lazy indexing + // this behavior lets us explicitly hid a directory if it is explicitly blocked + if(session.projectionQueryForSubDir) { + query.andWhere(session.projectionQueryForSubDir); + } if (!session.projectionQuery) { query.leftJoinAndSelect('directory.media', 'media'); } diff --git a/src/backend/model/database/SearchManager.ts b/src/backend/model/database/SearchManager.ts index 73150b9a..97b53e0c 100644 --- a/src/backend/model/database/SearchManager.ts +++ b/src/backend/model/database/SearchManager.ts @@ -478,11 +478,13 @@ export class SearchManager { } public async prepareAndBuildWhereQuery( - queryIN: SearchQueryDTO, - directoryOnly = false - ): Promise { - const query = await this.prepareQuery(queryIN); - return this.buildWhereQuery(query, directoryOnly); + queryIN: SearchQueryDTO, directoryOnly = false, + aliases: { [key: string]: string } = {}): Promise { + let query = await this.prepareQuery(queryIN); + if (directoryOnly) { + query = this.filterDirectoryQuery(query); + } + return this.buildWhereQuery(query, directoryOnly, aliases); } public async prepareQuery(queryIN: SearchQueryDTO): Promise { @@ -496,11 +498,13 @@ export class SearchManager { * Builds the SQL Where query from search query * @param query input search query * @param directoryOnly Only builds directory related queries + * @param aliases for SQL alias mapping * @private */ public buildWhereQuery( query: SearchQueryDTO, - directoryOnly = false + directoryOnly = false, + aliases: { [key: string]: string } = {} ): Brackets { const queryId = (query as SearchQueryDTOWithID).queryId; switch (query.type) { @@ -1039,10 +1043,10 @@ export class SearchManager { new RegExp('\\\\', 'g'), '/' ); - + const alias = aliases['directory'] ?? 'directory'; textParam['fullPath' + queryId] = createMatchString(dirPathStr); q[whereFN]( - `directory.path ${LIKE} :fullPath${queryId} COLLATE ` + SQL_COLLATE, + `${alias}.path ${LIKE} :fullPath${queryId} COLLATE ` + SQL_COLLATE, textParam ); @@ -1053,7 +1057,7 @@ export class SearchManager { directoryPath.name ); dq[whereFNRev]( - `directory.name ${LIKE} :dirName${queryId} COLLATE ${SQL_COLLATE}`, + `${alias}.name ${LIKE} :dirName${queryId} COLLATE ${SQL_COLLATE}`, textParam ); if (dirPathStr.includes('/')) { @@ -1061,7 +1065,7 @@ export class SearchManager { directoryPath.parent ); dq[whereFNRev]( - `directory.path ${LIKE} :parentName${queryId} COLLATE ${SQL_COLLATE}`, + `${alias}.path ${LIKE} :parentName${queryId} COLLATE ${SQL_COLLATE}`, textParam ); } diff --git a/src/backend/model/database/SessionManager.ts b/src/backend/model/database/SessionManager.ts index b2a1203f..b8b7f79f 100644 --- a/src/backend/model/database/SessionManager.ts +++ b/src/backend/model/database/SessionManager.ts @@ -6,6 +6,7 @@ import {ANDSearchQuery, SearchQueryDTO, SearchQueryTypes} from '../../../common/ import {SharingEntity} from './enitites/SharingEntity'; import {ObjectManagers} from '../ObjectManagers'; import {Logger} from '../../Logger'; +import {Utils} from '../../../common/Utils'; const LOG_TAG = '[SessionManager]'; @@ -68,6 +69,9 @@ export class SessionManager { // Build the Brackets-based query context.projectionQuery = await ObjectManagers.getInstance().SearchManager.prepareAndBuildWhereQuery(finalQuery); context.hasDirectoryProjection = ObjectManagers.getInstance().SearchManager.hasDirectoryQuery(finalQuery); + if (context.hasDirectoryProjection) { + context.projectionQueryForSubDir = await ObjectManagers.getInstance().SearchManager.prepareAndBuildWhereQuery(finalQuery, true, {directory: 'directories'}); + } context.user.projectionKey = this.createProjectionKey(finalQuery); if (SearchQueryUtils.isQueryEmpty(finalQuery)) { Logger.silly(LOG_TAG, 'Empty Projection query.'); diff --git a/src/common/SearchQueryUtils.ts b/src/common/SearchQueryUtils.ts index 2995ba9c..c3eb7a74 100644 --- a/src/common/SearchQueryUtils.ts +++ b/src/common/SearchQueryUtils.ts @@ -12,6 +12,7 @@ import {Utils} from './Utils'; export const SearchQueryUtils = { negate: (query: SearchQueryDTO): SearchQueryDTO => { + query = Utils.clone(query) switch (query.type) { case SearchQueryTypes.AND: query.type = SearchQueryTypes.OR; diff --git a/test/backend/unit/model/sql/GalleryManager.spec.ts b/test/backend/unit/model/sql/GalleryManager.spec.ts index 6bcdd6f6..294c3d6c 100644 --- a/test/backend/unit/model/sql/GalleryManager.spec.ts +++ b/test/backend/unit/model/sql/GalleryManager.spec.ts @@ -64,7 +64,7 @@ describe('GalleryManager', (sqlHelper: DBTestHelper) => { it('should return all media when no projection query is provided', async () => { // Setup a session with no projection query - const session = DBTestHelper.defaultSession + const session = DBTestHelper.defaultSession; // Get the directory contents without any filtering using our directory's name and path @@ -142,7 +142,33 @@ describe('GalleryManager', (sqlHelper: DBTestHelper) => { }); + it('blocks child directory when blockQuery contains normal directory search', async () => { + const root = sqlHelper.testGalleyEntities.dir; + const child = sqlHelper.testGalleyEntities.subDir; + + const blockQuery = { + type: SearchQueryTypes.directory, + matchType: TextSearchQueryMatchTypes.like, + text: child.name + } as any; + + const session = await ObjectManagers.getInstance().SessionManager.buildContext({ + blockQuery, + overrideAllowBlockList: true + } as any); + + const dirInfo = await galleryManager.getDirIdAndTime(connection, root.name, root.path); + const directory = await galleryManager.getParentDirFromId(connection, session, dirInfo.id); + + expect(((await galleryManager.getParentDirFromId(connection, DBTestHelper.defaultSession, dirInfo.id)).directories || []) + .map(d => d.name)).to.include(child.name); + expect(((await galleryManager.getParentDirFromId(connection, session, dirInfo.id)).directories || []) + .map(d => d.name)).to.not.include(child.name); + + }); }); + + describe('GalleryManager.listDirectory - reindexing severities and projection behavior', () => { const origStatSync = fs.statSync;