mirror of
https://github.com/immich-app/immich.git
synced 2026-03-01 18:19:10 +03:00
fix(server): tighten asset visibility (#18699)
* tighten visibility * update sql * elevated access util function * fix potential sync issue * include in user stats * include hidden assets in size usage * filter visibility in search duplicates query * stack visibility
This commit is contained in:
@@ -5,6 +5,7 @@ import { InjectKysely } from 'nestjs-kysely';
|
||||
import { columns } from 'src/database';
|
||||
import { Activity, DB } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AssetVisibility } from 'src/enum';
|
||||
import { asUuid } from 'src/utils/database';
|
||||
|
||||
export interface ActivitySearch {
|
||||
@@ -76,6 +77,7 @@ export class ActivityRepository {
|
||||
.where('activity.albumId', '=', albumId)
|
||||
.where('activity.isLiked', '=', false)
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.where('assets.visibility', '!=', sql.lit(AssetVisibility.LOCKED))
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
return count;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { columns, Exif } from 'src/database';
|
||||
import { Albums, DB } from 'src/db';
|
||||
import { Chunked, ChunkedArray, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AlbumUserCreateDto } from 'src/dtos/album.dto';
|
||||
import { withDefaultVisibility } from 'src/utils/database';
|
||||
|
||||
export interface AlbumAssetCount {
|
||||
albumId: string;
|
||||
@@ -58,6 +59,7 @@ const withAssets = (eb: ExpressionBuilder<DB, 'albums'>) => {
|
||||
.innerJoin('albums_assets_assets', 'albums_assets_assets.assetsId', 'assets.id')
|
||||
.whereRef('albums_assets_assets.albumsId', '=', 'albums.id')
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.$call(withDefaultVisibility)
|
||||
.orderBy('assets.fileCreatedAt', 'desc')
|
||||
.as('asset'),
|
||||
)
|
||||
@@ -121,6 +123,7 @@ export class AlbumRepository {
|
||||
return (
|
||||
this.db
|
||||
.selectFrom('assets')
|
||||
.$call(withDefaultVisibility)
|
||||
.innerJoin('albums_assets_assets as album_assets', 'album_assets.assetsId', 'assets.id')
|
||||
.select('album_assets.albumsId as albumId')
|
||||
.select((eb) => eb.fn.min(sql<Date>`("assets"."localDateTime" AT TIME ZONE 'UTC'::text)::date`).as('startDate'))
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
anyUuid,
|
||||
asUuid,
|
||||
toJson,
|
||||
withDefaultVisibility,
|
||||
withExif,
|
||||
withExifInner,
|
||||
withFaces,
|
||||
@@ -140,9 +141,9 @@ export class AssetJobRepository {
|
||||
return this.db
|
||||
.selectFrom('assets')
|
||||
.select(['assets.id'])
|
||||
.where('assets.visibility', '!=', AssetVisibility.HIDDEN)
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.innerJoin('smart_search', 'assets.id', 'smart_search.assetId')
|
||||
.$call(withDefaultVisibility)
|
||||
.$if(!force, (qb) =>
|
||||
qb
|
||||
.innerJoin('asset_job_status as job_status', 'job_status.assetId', 'assets.id')
|
||||
@@ -226,7 +227,7 @@ export class AssetJobRepository {
|
||||
.select(['asset_stack.id', 'asset_stack.primaryAssetId'])
|
||||
.select((eb) => eb.fn<Asset[]>('array_agg', [eb.table('stacked')]).as('assets'))
|
||||
.where('stacked.deletedAt', 'is not', null)
|
||||
.where('stacked.visibility', '!=', AssetVisibility.ARCHIVE)
|
||||
.where('stacked.visibility', '=', AssetVisibility.TIMELINE)
|
||||
.whereRef('stacked.stackId', '=', 'asset_stack.id')
|
||||
.groupBy('asset_stack.id')
|
||||
.as('stacked_assets'),
|
||||
|
||||
@@ -300,7 +300,6 @@ export class AssetRepository {
|
||||
.select(withFacesAndPeople)
|
||||
.select(withTags)
|
||||
.$call(withExif)
|
||||
.leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId')
|
||||
.where('assets.id', '=', anyUuid(ids))
|
||||
.execute();
|
||||
}
|
||||
@@ -523,8 +522,8 @@ export class AssetRepository {
|
||||
.selectFrom('assets')
|
||||
.selectAll('assets')
|
||||
.$call(withExif)
|
||||
.$call(withDefaultVisibility)
|
||||
.where('ownerId', '=', anyUuid(userIds))
|
||||
.where('visibility', '!=', AssetVisibility.HIDDEN)
|
||||
.where('deletedAt', 'is', null)
|
||||
.orderBy((eb) => eb.fn('random'))
|
||||
.limit(take)
|
||||
@@ -634,8 +633,6 @@ export class AssetRepository {
|
||||
)
|
||||
.$if(!!options.personId, (qb) => hasPeople(qb, [options.personId!]))
|
||||
.$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!)))
|
||||
.$if(options.visibility == undefined, withDefaultVisibility)
|
||||
.$if(!!options.visibility, (qb) => qb.where('assets.visibility', '=', options.visibility!))
|
||||
.$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!))
|
||||
.$if(!!options.withStacked, (qb) =>
|
||||
qb
|
||||
@@ -656,7 +653,7 @@ export class AssetRepository {
|
||||
.select(sql`array[stacked."stackId"::text, count('stacked')::text]`.as('stack'))
|
||||
.whereRef('stacked.stackId', '=', 'assets.stackId')
|
||||
.where('stacked.deletedAt', 'is', null)
|
||||
.where('stacked.visibility', '!=', AssetVisibility.ARCHIVE)
|
||||
.where('stacked.visibility', '=', AssetVisibility.TIMELINE)
|
||||
.groupBy('stacked.stackId')
|
||||
.as('stacked_assets'),
|
||||
(join) => join.onTrue(),
|
||||
@@ -709,6 +706,7 @@ export class AssetRepository {
|
||||
.with('duplicates', (qb) =>
|
||||
qb
|
||||
.selectFrom('assets')
|
||||
.$call(withDefaultVisibility)
|
||||
.leftJoinLateral(
|
||||
(qb) =>
|
||||
qb
|
||||
@@ -727,7 +725,6 @@ export class AssetRepository {
|
||||
.where('assets.duplicateId', 'is not', null)
|
||||
.$narrowType<{ duplicateId: NotNull }>()
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.where('assets.visibility', '!=', AssetVisibility.HIDDEN)
|
||||
.where('assets.stackId', 'is', null)
|
||||
.groupBy('assets.duplicateId'),
|
||||
)
|
||||
|
||||
@@ -38,11 +38,6 @@ export interface PersonStatistics {
|
||||
assets: number;
|
||||
}
|
||||
|
||||
export interface PeopleStatistics {
|
||||
total: number;
|
||||
hidden: number;
|
||||
}
|
||||
|
||||
export interface DeleteFacesOptions {
|
||||
sourceType: SourceType;
|
||||
}
|
||||
@@ -151,7 +146,7 @@ export class PersonRepository {
|
||||
.innerJoin('assets', (join) =>
|
||||
join
|
||||
.onRef('asset_faces.assetId', '=', 'assets.id')
|
||||
.on('assets.visibility', '!=', AssetVisibility.ARCHIVE)
|
||||
.on('assets.visibility', '=', sql.lit(AssetVisibility.TIMELINE))
|
||||
.on('assets.deletedAt', 'is', null),
|
||||
)
|
||||
.where('person.ownerId', '=', userId)
|
||||
@@ -341,7 +336,7 @@ export class PersonRepository {
|
||||
join
|
||||
.onRef('assets.id', '=', 'asset_faces.assetId')
|
||||
.on('asset_faces.personId', '=', personId)
|
||||
.on('assets.visibility', '!=', AssetVisibility.ARCHIVE)
|
||||
.on('assets.visibility', '=', sql.lit(AssetVisibility.TIMELINE))
|
||||
.on('assets.deletedAt', 'is', null),
|
||||
)
|
||||
.select((eb) => eb.fn.count(eb.fn('distinct', ['assets.id'])).as('count'))
|
||||
@@ -354,35 +349,31 @@ export class PersonRepository {
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getNumberOfPeople(userId: string): Promise<PeopleStatistics> {
|
||||
const items = await this.db
|
||||
getNumberOfPeople(userId: string) {
|
||||
const zero = sql.lit(0);
|
||||
return this.db
|
||||
.selectFrom('person')
|
||||
.innerJoin('asset_faces', 'asset_faces.personId', 'person.id')
|
||||
.where((eb) =>
|
||||
eb.exists((eb) =>
|
||||
eb
|
||||
.selectFrom('asset_faces')
|
||||
.whereRef('asset_faces.personId', '=', 'person.id')
|
||||
.where('asset_faces.deletedAt', 'is', null)
|
||||
.where((eb) =>
|
||||
eb.exists((eb) =>
|
||||
eb
|
||||
.selectFrom('assets')
|
||||
.whereRef('assets.id', '=', 'asset_faces.assetId')
|
||||
.where('assets.visibility', '=', sql.lit(AssetVisibility.TIMELINE))
|
||||
.where('assets.deletedAt', 'is', null),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.where('person.ownerId', '=', userId)
|
||||
.where('asset_faces.deletedAt', 'is', null)
|
||||
.innerJoin('assets', (join) =>
|
||||
join
|
||||
.onRef('assets.id', '=', 'asset_faces.assetId')
|
||||
.on('assets.deletedAt', 'is', null)
|
||||
.on('assets.visibility', '!=', AssetVisibility.ARCHIVE),
|
||||
)
|
||||
.select((eb) => eb.fn.count(eb.fn('distinct', ['person.id'])).as('total'))
|
||||
.select((eb) =>
|
||||
eb.fn
|
||||
.count(eb.fn('distinct', ['person.id']))
|
||||
.filterWhere('person.isHidden', '=', true)
|
||||
.as('hidden'),
|
||||
)
|
||||
.executeTakeFirst();
|
||||
|
||||
if (items == undefined) {
|
||||
return { total: 0, hidden: 0 };
|
||||
}
|
||||
|
||||
return {
|
||||
total: Number(items.total),
|
||||
hidden: Number(items.hidden),
|
||||
};
|
||||
.select((eb) => eb.fn.coalesce(eb.fn.countAll<number>(), zero).as('total'))
|
||||
.select((eb) => eb.fn.coalesce(eb.fn.countAll<number>().filterWhere('isHidden', '=', true), zero).as('hidden'))
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
create(person: Insertable<Person>) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AssetStatus, AssetType, AssetVisibility, VectorIndex } from 'src/enum';
|
||||
import { probes } from 'src/repositories/database.repository';
|
||||
import { anyUuid, asUuid, searchAssetBuilder } from 'src/utils/database';
|
||||
import { anyUuid, asUuid, searchAssetBuilder, withDefaultVisibility } from 'src/utils/database';
|
||||
import { paginationHelper } from 'src/utils/pagination';
|
||||
import { isValidInteger } from 'src/validation';
|
||||
|
||||
@@ -268,6 +268,7 @@ export class SearchRepository {
|
||||
.with('cte', (qb) =>
|
||||
qb
|
||||
.selectFrom('assets')
|
||||
.$call(withDefaultVisibility)
|
||||
.select([
|
||||
'assets.id as assetId',
|
||||
'assets.duplicateId',
|
||||
@@ -276,7 +277,6 @@ export class SearchRepository {
|
||||
.innerJoin('smart_search', 'assets.id', 'smart_search.assetId')
|
||||
.where('assets.ownerId', '=', anyUuid(userIds))
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.where('assets.visibility', '!=', AssetVisibility.HIDDEN)
|
||||
.where('assets.type', '=', type)
|
||||
.where('assets.id', '!=', asUuid(assetId))
|
||||
.where('assets.stackId', 'is', null)
|
||||
@@ -472,7 +472,7 @@ export class SearchRepository {
|
||||
.distinctOn(field)
|
||||
.innerJoin('assets', 'assets.id', 'exif.assetId')
|
||||
.where('ownerId', '=', anyUuid(userIds))
|
||||
.where('visibility', '!=', AssetVisibility.HIDDEN)
|
||||
.where('visibility', '=', AssetVisibility.TIMELINE)
|
||||
.where('deletedAt', 'is', null)
|
||||
.where(field, 'is not', null);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { InjectKysely } from 'nestjs-kysely';
|
||||
import { columns } from 'src/database';
|
||||
import { AssetStack, DB } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { asUuid } from 'src/utils/database';
|
||||
import { asUuid, withDefaultVisibility } from 'src/utils/database';
|
||||
|
||||
export interface StackSearch {
|
||||
ownerId: string;
|
||||
@@ -34,7 +34,8 @@ const withAssets = (eb: ExpressionBuilder<DB, 'asset_stack'>, withTags = false)
|
||||
)
|
||||
.select((eb) => eb.fn.toJson('exifInfo').as('exifInfo'))
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.whereRef('assets.stackId', '=', 'asset_stack.id'),
|
||||
.whereRef('assets.stackId', '=', 'asset_stack.id')
|
||||
.$call(withDefaultVisibility),
|
||||
).as('assets');
|
||||
};
|
||||
|
||||
|
||||
@@ -210,9 +210,9 @@ export class UserRepository {
|
||||
getUserStats() {
|
||||
return this.db
|
||||
.selectFrom('users')
|
||||
.leftJoin('assets', 'assets.ownerId', 'users.id')
|
||||
.leftJoin('assets', (join) => join.onRef('assets.ownerId', '=', 'users.id').on('assets.deletedAt', 'is', null))
|
||||
.leftJoin('exif', 'exif.assetId', 'assets.id')
|
||||
.select(['users.id as userId', 'users.name as userName', 'users.quotaSizeInBytes as quotaSizeInBytes'])
|
||||
.select(['users.id as userId', 'users.name as userName', 'users.quotaSizeInBytes'])
|
||||
.select((eb) => [
|
||||
eb.fn
|
||||
.countAll<number>()
|
||||
@@ -256,7 +256,6 @@ export class UserRepository {
|
||||
)
|
||||
.as('usageVideos'),
|
||||
])
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.groupBy('users.id')
|
||||
.orderBy('users.createdAt', 'asc')
|
||||
.execute();
|
||||
|
||||
Reference in New Issue
Block a user