mirror of
https://github.com/immich-app/immich
synced 2025-06-08 00:20:58 +00: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:
parent
b5c3a675b2
commit
fa22e865a4
@ -73,3 +73,4 @@ where
|
|||||||
and "activity"."albumId" = $2
|
and "activity"."albumId" = $2
|
||||||
and "activity"."isLiked" = $3
|
and "activity"."isLiked" = $3
|
||||||
and "assets"."deletedAt" is null
|
and "assets"."deletedAt" is null
|
||||||
|
and "assets"."visibility" != 'locked'
|
||||||
|
@ -80,6 +80,7 @@ select
|
|||||||
where
|
where
|
||||||
"albums_assets_assets"."albumsId" = "albums"."id"
|
"albums_assets_assets"."albumsId" = "albums"."id"
|
||||||
and "assets"."deletedAt" is null
|
and "assets"."deletedAt" is null
|
||||||
|
and "assets"."visibility" in ('archive', 'timeline')
|
||||||
order by
|
order by
|
||||||
"assets"."fileCreatedAt" desc
|
"assets"."fileCreatedAt" desc
|
||||||
) as "asset"
|
) as "asset"
|
||||||
@ -178,7 +179,8 @@ from
|
|||||||
"assets"
|
"assets"
|
||||||
inner join "albums_assets_assets" as "album_assets" on "album_assets"."assetsId" = "assets"."id"
|
inner join "albums_assets_assets" as "album_assets" on "album_assets"."assetsId" = "assets"."id"
|
||||||
where
|
where
|
||||||
"album_assets"."albumsId" in ($1)
|
"assets"."visibility" in ('archive', 'timeline')
|
||||||
|
and "album_assets"."albumsId" in ($1)
|
||||||
and "assets"."deletedAt" is null
|
and "assets"."deletedAt" is null
|
||||||
group by
|
group by
|
||||||
"album_assets"."albumsId"
|
"album_assets"."albumsId"
|
||||||
|
@ -186,8 +186,8 @@ from
|
|||||||
inner join "smart_search" on "assets"."id" = "smart_search"."assetId"
|
inner join "smart_search" on "assets"."id" = "smart_search"."assetId"
|
||||||
inner join "asset_job_status" as "job_status" on "job_status"."assetId" = "assets"."id"
|
inner join "asset_job_status" as "job_status" on "job_status"."assetId" = "assets"."id"
|
||||||
where
|
where
|
||||||
"assets"."visibility" != $1
|
"assets"."deletedAt" is null
|
||||||
and "assets"."deletedAt" is null
|
and "assets"."visibility" in ('archive', 'timeline')
|
||||||
and "job_status"."duplicatesDetectedAt" is null
|
and "job_status"."duplicatesDetectedAt" is null
|
||||||
|
|
||||||
-- AssetJobRepository.streamForEncodeClip
|
-- AssetJobRepository.streamForEncodeClip
|
||||||
@ -349,7 +349,7 @@ from
|
|||||||
"assets" as "stacked"
|
"assets" as "stacked"
|
||||||
where
|
where
|
||||||
"stacked"."deletedAt" is not null
|
"stacked"."deletedAt" is not null
|
||||||
and "stacked"."visibility" != $1
|
and "stacked"."visibility" = $1
|
||||||
and "stacked"."stackId" = "asset_stack"."id"
|
and "stacked"."stackId" = "asset_stack"."id"
|
||||||
group by
|
group by
|
||||||
"asset_stack"."id"
|
"asset_stack"."id"
|
||||||
|
@ -130,7 +130,6 @@ select
|
|||||||
from
|
from
|
||||||
"assets"
|
"assets"
|
||||||
left join "exif" on "assets"."id" = "exif"."assetId"
|
left join "exif" on "assets"."id" = "exif"."assetId"
|
||||||
left join "asset_stack" on "asset_stack"."id" = "assets"."stackId"
|
|
||||||
where
|
where
|
||||||
"assets"."id" = any ($1::uuid[])
|
"assets"."id" = any ($1::uuid[])
|
||||||
|
|
||||||
@ -240,10 +239,7 @@ with
|
|||||||
"assets"
|
"assets"
|
||||||
where
|
where
|
||||||
"assets"."deletedAt" is null
|
"assets"."deletedAt" is null
|
||||||
and (
|
and "assets"."visibility" in ('archive', 'timeline')
|
||||||
"assets"."visibility" = $1
|
|
||||||
or "assets"."visibility" = $2
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
select
|
select
|
||||||
"timeBucket",
|
"timeBucket",
|
||||||
@ -300,21 +296,14 @@ with
|
|||||||
where
|
where
|
||||||
"stacked"."stackId" = "assets"."stackId"
|
"stacked"."stackId" = "assets"."stackId"
|
||||||
and "stacked"."deletedAt" is null
|
and "stacked"."deletedAt" is null
|
||||||
and "stacked"."visibility" != $1
|
and "stacked"."visibility" = $1
|
||||||
group by
|
group by
|
||||||
"stacked"."stackId"
|
"stacked"."stackId"
|
||||||
) as "stacked_assets" on true
|
) as "stacked_assets" on true
|
||||||
where
|
where
|
||||||
"assets"."deletedAt" is null
|
"assets"."deletedAt" is null
|
||||||
and (
|
and "assets"."visibility" in ('archive', 'timeline')
|
||||||
"assets"."visibility" = $2
|
and date_trunc('MONTH', "localDateTime" at time zone 'UTC') at time zone 'UTC' = $2
|
||||||
or "assets"."visibility" = $3
|
|
||||||
)
|
|
||||||
and date_trunc('MONTH', "localDateTime" at time zone 'UTC') at time zone 'UTC' = $4
|
|
||||||
and (
|
|
||||||
"assets"."visibility" = $5
|
|
||||||
or "assets"."visibility" = $6
|
|
||||||
)
|
|
||||||
and not exists (
|
and not exists (
|
||||||
select
|
select
|
||||||
from
|
from
|
||||||
@ -374,10 +363,10 @@ with
|
|||||||
"exif"."assetId" = "assets"."id"
|
"exif"."assetId" = "assets"."id"
|
||||||
) as "asset" on true
|
) as "asset" on true
|
||||||
where
|
where
|
||||||
"assets"."ownerId" = $1::uuid
|
"assets"."visibility" in ('archive', 'timeline')
|
||||||
|
and "assets"."ownerId" = $1::uuid
|
||||||
and "assets"."duplicateId" is not null
|
and "assets"."duplicateId" is not null
|
||||||
and "assets"."deletedAt" is null
|
and "assets"."deletedAt" is null
|
||||||
and "assets"."visibility" != $2
|
|
||||||
and "assets"."stackId" is null
|
and "assets"."stackId" is null
|
||||||
group by
|
group by
|
||||||
"assets"."duplicateId"
|
"assets"."duplicateId"
|
||||||
@ -388,12 +377,12 @@ with
|
|||||||
from
|
from
|
||||||
"duplicates"
|
"duplicates"
|
||||||
where
|
where
|
||||||
json_array_length("assets") = $3
|
json_array_length("assets") = $2
|
||||||
),
|
),
|
||||||
"removed_unique" as (
|
"removed_unique" as (
|
||||||
update "assets"
|
update "assets"
|
||||||
set
|
set
|
||||||
"duplicateId" = $4
|
"duplicateId" = $3
|
||||||
from
|
from
|
||||||
"unique"
|
"unique"
|
||||||
where
|
where
|
||||||
|
@ -182,27 +182,42 @@ from
|
|||||||
"asset_faces"
|
"asset_faces"
|
||||||
left join "assets" on "assets"."id" = "asset_faces"."assetId"
|
left join "assets" on "assets"."id" = "asset_faces"."assetId"
|
||||||
and "asset_faces"."personId" = $1
|
and "asset_faces"."personId" = $1
|
||||||
and "assets"."visibility" != $2
|
and "assets"."visibility" = 'timeline'
|
||||||
and "assets"."deletedAt" is null
|
and "assets"."deletedAt" is null
|
||||||
where
|
where
|
||||||
"asset_faces"."deletedAt" is null
|
"asset_faces"."deletedAt" is null
|
||||||
|
|
||||||
-- PersonRepository.getNumberOfPeople
|
-- PersonRepository.getNumberOfPeople
|
||||||
select
|
select
|
||||||
count(distinct ("person"."id")) as "total",
|
coalesce(count(*), 0) as "total",
|
||||||
count(distinct ("person"."id")) filter (
|
coalesce(
|
||||||
where
|
count(*) filter (
|
||||||
"person"."isHidden" = $1
|
where
|
||||||
|
"isHidden" = $1
|
||||||
|
),
|
||||||
|
0
|
||||||
) as "hidden"
|
) as "hidden"
|
||||||
from
|
from
|
||||||
"person"
|
"person"
|
||||||
inner join "asset_faces" on "asset_faces"."personId" = "person"."id"
|
|
||||||
inner join "assets" on "assets"."id" = "asset_faces"."assetId"
|
|
||||||
and "assets"."deletedAt" is null
|
|
||||||
and "assets"."visibility" != $2
|
|
||||||
where
|
where
|
||||||
"person"."ownerId" = $3
|
exists (
|
||||||
and "asset_faces"."deletedAt" is null
|
select
|
||||||
|
from
|
||||||
|
"asset_faces"
|
||||||
|
where
|
||||||
|
"asset_faces"."personId" = "person"."id"
|
||||||
|
and "asset_faces"."deletedAt" is null
|
||||||
|
and exists (
|
||||||
|
select
|
||||||
|
from
|
||||||
|
"assets"
|
||||||
|
where
|
||||||
|
"assets"."id" = "asset_faces"."assetId"
|
||||||
|
and "assets"."visibility" = 'timeline'
|
||||||
|
and "assets"."deletedAt" is null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
and "person"."ownerId" = $2
|
||||||
|
|
||||||
-- PersonRepository.refreshFaces
|
-- PersonRepository.refreshFaces
|
||||||
with
|
with
|
||||||
|
@ -102,23 +102,23 @@ with
|
|||||||
"assets"
|
"assets"
|
||||||
inner join "smart_search" on "assets"."id" = "smart_search"."assetId"
|
inner join "smart_search" on "assets"."id" = "smart_search"."assetId"
|
||||||
where
|
where
|
||||||
"assets"."ownerId" = any ($2::uuid[])
|
"assets"."visibility" in ('archive', 'timeline')
|
||||||
|
and "assets"."ownerId" = any ($2::uuid[])
|
||||||
and "assets"."deletedAt" is null
|
and "assets"."deletedAt" is null
|
||||||
and "assets"."visibility" != $3
|
and "assets"."type" = $3
|
||||||
and "assets"."type" = $4
|
and "assets"."id" != $4::uuid
|
||||||
and "assets"."id" != $5::uuid
|
|
||||||
and "assets"."stackId" is null
|
and "assets"."stackId" is null
|
||||||
order by
|
order by
|
||||||
"distance"
|
"distance"
|
||||||
limit
|
limit
|
||||||
$6
|
$5
|
||||||
)
|
)
|
||||||
select
|
select
|
||||||
*
|
*
|
||||||
from
|
from
|
||||||
"cte"
|
"cte"
|
||||||
where
|
where
|
||||||
"cte"."distance" <= $7
|
"cte"."distance" <= $6
|
||||||
commit
|
commit
|
||||||
|
|
||||||
-- SearchRepository.searchFaces
|
-- SearchRepository.searchFaces
|
||||||
@ -241,7 +241,7 @@ from
|
|||||||
inner join "assets" on "assets"."id" = "exif"."assetId"
|
inner join "assets" on "assets"."id" = "exif"."assetId"
|
||||||
where
|
where
|
||||||
"ownerId" = any ($1::uuid[])
|
"ownerId" = any ($1::uuid[])
|
||||||
and "visibility" != $2
|
and "visibility" = $2
|
||||||
and "deletedAt" is null
|
and "deletedAt" is null
|
||||||
and "state" is not null
|
and "state" is not null
|
||||||
|
|
||||||
@ -253,7 +253,7 @@ from
|
|||||||
inner join "assets" on "assets"."id" = "exif"."assetId"
|
inner join "assets" on "assets"."id" = "exif"."assetId"
|
||||||
where
|
where
|
||||||
"ownerId" = any ($1::uuid[])
|
"ownerId" = any ($1::uuid[])
|
||||||
and "visibility" != $2
|
and "visibility" = $2
|
||||||
and "deletedAt" is null
|
and "deletedAt" is null
|
||||||
and "city" is not null
|
and "city" is not null
|
||||||
|
|
||||||
@ -265,7 +265,7 @@ from
|
|||||||
inner join "assets" on "assets"."id" = "exif"."assetId"
|
inner join "assets" on "assets"."id" = "exif"."assetId"
|
||||||
where
|
where
|
||||||
"ownerId" = any ($1::uuid[])
|
"ownerId" = any ($1::uuid[])
|
||||||
and "visibility" != $2
|
and "visibility" = $2
|
||||||
and "deletedAt" is null
|
and "deletedAt" is null
|
||||||
and "make" is not null
|
and "make" is not null
|
||||||
|
|
||||||
@ -277,6 +277,6 @@ from
|
|||||||
inner join "assets" on "assets"."id" = "exif"."assetId"
|
inner join "assets" on "assets"."id" = "exif"."assetId"
|
||||||
where
|
where
|
||||||
"ownerId" = any ($1::uuid[])
|
"ownerId" = any ($1::uuid[])
|
||||||
and "visibility" != $2
|
and "visibility" = $2
|
||||||
and "deletedAt" is null
|
and "deletedAt" is null
|
||||||
and "model" is not null
|
and "model" is not null
|
||||||
|
@ -52,6 +52,7 @@ select
|
|||||||
where
|
where
|
||||||
"assets"."deletedAt" is null
|
"assets"."deletedAt" is null
|
||||||
and "assets"."stackId" = "asset_stack"."id"
|
and "assets"."stackId" = "asset_stack"."id"
|
||||||
|
and "assets"."visibility" in ('archive', 'timeline')
|
||||||
) as agg
|
) as agg
|
||||||
) as "assets"
|
) as "assets"
|
||||||
from
|
from
|
||||||
@ -135,6 +136,7 @@ select
|
|||||||
where
|
where
|
||||||
"assets"."deletedAt" is null
|
"assets"."deletedAt" is null
|
||||||
and "assets"."stackId" = "asset_stack"."id"
|
and "assets"."stackId" = "asset_stack"."id"
|
||||||
|
and "assets"."visibility" in ('archive', 'timeline')
|
||||||
) as agg
|
) as agg
|
||||||
) as "assets"
|
) as "assets"
|
||||||
from
|
from
|
||||||
|
@ -290,7 +290,7 @@ order by
|
|||||||
select
|
select
|
||||||
"users"."id" as "userId",
|
"users"."id" as "userId",
|
||||||
"users"."name" as "userName",
|
"users"."name" as "userName",
|
||||||
"users"."quotaSizeInBytes" as "quotaSizeInBytes",
|
"users"."quotaSizeInBytes",
|
||||||
count(*) filter (
|
count(*) filter (
|
||||||
where
|
where
|
||||||
(
|
(
|
||||||
@ -335,9 +335,8 @@ select
|
|||||||
from
|
from
|
||||||
"users"
|
"users"
|
||||||
left join "assets" on "assets"."ownerId" = "users"."id"
|
left join "assets" on "assets"."ownerId" = "users"."id"
|
||||||
|
and "assets"."deletedAt" is null
|
||||||
left join "exif" on "exif"."assetId" = "assets"."id"
|
left join "exif" on "exif"."assetId" = "assets"."id"
|
||||||
where
|
|
||||||
"assets"."deletedAt" is null
|
|
||||||
group by
|
group by
|
||||||
"users"."id"
|
"users"."id"
|
||||||
order by
|
order by
|
||||||
|
@ -5,6 +5,7 @@ import { InjectKysely } from 'nestjs-kysely';
|
|||||||
import { columns } from 'src/database';
|
import { columns } from 'src/database';
|
||||||
import { Activity, DB } from 'src/db';
|
import { Activity, DB } from 'src/db';
|
||||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
|
import { AssetVisibility } from 'src/enum';
|
||||||
import { asUuid } from 'src/utils/database';
|
import { asUuid } from 'src/utils/database';
|
||||||
|
|
||||||
export interface ActivitySearch {
|
export interface ActivitySearch {
|
||||||
@ -76,6 +77,7 @@ export class ActivityRepository {
|
|||||||
.where('activity.albumId', '=', albumId)
|
.where('activity.albumId', '=', albumId)
|
||||||
.where('activity.isLiked', '=', false)
|
.where('activity.isLiked', '=', false)
|
||||||
.where('assets.deletedAt', 'is', null)
|
.where('assets.deletedAt', 'is', null)
|
||||||
|
.where('assets.visibility', '!=', sql.lit(AssetVisibility.LOCKED))
|
||||||
.executeTakeFirstOrThrow();
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
return count;
|
return count;
|
||||||
|
@ -6,6 +6,7 @@ import { columns, Exif } from 'src/database';
|
|||||||
import { Albums, DB } from 'src/db';
|
import { Albums, DB } from 'src/db';
|
||||||
import { Chunked, ChunkedArray, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
|
import { Chunked, ChunkedArray, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { AlbumUserCreateDto } from 'src/dtos/album.dto';
|
import { AlbumUserCreateDto } from 'src/dtos/album.dto';
|
||||||
|
import { withDefaultVisibility } from 'src/utils/database';
|
||||||
|
|
||||||
export interface AlbumAssetCount {
|
export interface AlbumAssetCount {
|
||||||
albumId: string;
|
albumId: string;
|
||||||
@ -58,6 +59,7 @@ const withAssets = (eb: ExpressionBuilder<DB, 'albums'>) => {
|
|||||||
.innerJoin('albums_assets_assets', 'albums_assets_assets.assetsId', 'assets.id')
|
.innerJoin('albums_assets_assets', 'albums_assets_assets.assetsId', 'assets.id')
|
||||||
.whereRef('albums_assets_assets.albumsId', '=', 'albums.id')
|
.whereRef('albums_assets_assets.albumsId', '=', 'albums.id')
|
||||||
.where('assets.deletedAt', 'is', null)
|
.where('assets.deletedAt', 'is', null)
|
||||||
|
.$call(withDefaultVisibility)
|
||||||
.orderBy('assets.fileCreatedAt', 'desc')
|
.orderBy('assets.fileCreatedAt', 'desc')
|
||||||
.as('asset'),
|
.as('asset'),
|
||||||
)
|
)
|
||||||
@ -121,6 +123,7 @@ export class AlbumRepository {
|
|||||||
return (
|
return (
|
||||||
this.db
|
this.db
|
||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
|
.$call(withDefaultVisibility)
|
||||||
.innerJoin('albums_assets_assets as album_assets', 'album_assets.assetsId', 'assets.id')
|
.innerJoin('albums_assets_assets as album_assets', 'album_assets.assetsId', 'assets.id')
|
||||||
.select('album_assets.albumsId as albumId')
|
.select('album_assets.albumsId as albumId')
|
||||||
.select((eb) => eb.fn.min(sql<Date>`("assets"."localDateTime" AT TIME ZONE 'UTC'::text)::date`).as('startDate'))
|
.select((eb) => eb.fn.min(sql<Date>`("assets"."localDateTime" AT TIME ZONE 'UTC'::text)::date`).as('startDate'))
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
anyUuid,
|
anyUuid,
|
||||||
asUuid,
|
asUuid,
|
||||||
toJson,
|
toJson,
|
||||||
|
withDefaultVisibility,
|
||||||
withExif,
|
withExif,
|
||||||
withExifInner,
|
withExifInner,
|
||||||
withFaces,
|
withFaces,
|
||||||
@ -140,9 +141,9 @@ export class AssetJobRepository {
|
|||||||
return this.db
|
return this.db
|
||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
.select(['assets.id'])
|
.select(['assets.id'])
|
||||||
.where('assets.visibility', '!=', AssetVisibility.HIDDEN)
|
|
||||||
.where('assets.deletedAt', 'is', null)
|
.where('assets.deletedAt', 'is', null)
|
||||||
.innerJoin('smart_search', 'assets.id', 'smart_search.assetId')
|
.innerJoin('smart_search', 'assets.id', 'smart_search.assetId')
|
||||||
|
.$call(withDefaultVisibility)
|
||||||
.$if(!force, (qb) =>
|
.$if(!force, (qb) =>
|
||||||
qb
|
qb
|
||||||
.innerJoin('asset_job_status as job_status', 'job_status.assetId', 'assets.id')
|
.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(['asset_stack.id', 'asset_stack.primaryAssetId'])
|
||||||
.select((eb) => eb.fn<Asset[]>('array_agg', [eb.table('stacked')]).as('assets'))
|
.select((eb) => eb.fn<Asset[]>('array_agg', [eb.table('stacked')]).as('assets'))
|
||||||
.where('stacked.deletedAt', 'is not', null)
|
.where('stacked.deletedAt', 'is not', null)
|
||||||
.where('stacked.visibility', '!=', AssetVisibility.ARCHIVE)
|
.where('stacked.visibility', '=', AssetVisibility.TIMELINE)
|
||||||
.whereRef('stacked.stackId', '=', 'asset_stack.id')
|
.whereRef('stacked.stackId', '=', 'asset_stack.id')
|
||||||
.groupBy('asset_stack.id')
|
.groupBy('asset_stack.id')
|
||||||
.as('stacked_assets'),
|
.as('stacked_assets'),
|
||||||
|
@ -300,7 +300,6 @@ export class AssetRepository {
|
|||||||
.select(withFacesAndPeople)
|
.select(withFacesAndPeople)
|
||||||
.select(withTags)
|
.select(withTags)
|
||||||
.$call(withExif)
|
.$call(withExif)
|
||||||
.leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId')
|
|
||||||
.where('assets.id', '=', anyUuid(ids))
|
.where('assets.id', '=', anyUuid(ids))
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
@ -523,8 +522,8 @@ export class AssetRepository {
|
|||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
.selectAll('assets')
|
.selectAll('assets')
|
||||||
.$call(withExif)
|
.$call(withExif)
|
||||||
|
.$call(withDefaultVisibility)
|
||||||
.where('ownerId', '=', anyUuid(userIds))
|
.where('ownerId', '=', anyUuid(userIds))
|
||||||
.where('visibility', '!=', AssetVisibility.HIDDEN)
|
|
||||||
.where('deletedAt', 'is', null)
|
.where('deletedAt', 'is', null)
|
||||||
.orderBy((eb) => eb.fn('random'))
|
.orderBy((eb) => eb.fn('random'))
|
||||||
.limit(take)
|
.limit(take)
|
||||||
@ -634,8 +633,6 @@ export class AssetRepository {
|
|||||||
)
|
)
|
||||||
.$if(!!options.personId, (qb) => hasPeople(qb, [options.personId!]))
|
.$if(!!options.personId, (qb) => hasPeople(qb, [options.personId!]))
|
||||||
.$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!)))
|
.$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.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!))
|
||||||
.$if(!!options.withStacked, (qb) =>
|
.$if(!!options.withStacked, (qb) =>
|
||||||
qb
|
qb
|
||||||
@ -656,7 +653,7 @@ export class AssetRepository {
|
|||||||
.select(sql`array[stacked."stackId"::text, count('stacked')::text]`.as('stack'))
|
.select(sql`array[stacked."stackId"::text, count('stacked')::text]`.as('stack'))
|
||||||
.whereRef('stacked.stackId', '=', 'assets.stackId')
|
.whereRef('stacked.stackId', '=', 'assets.stackId')
|
||||||
.where('stacked.deletedAt', 'is', null)
|
.where('stacked.deletedAt', 'is', null)
|
||||||
.where('stacked.visibility', '!=', AssetVisibility.ARCHIVE)
|
.where('stacked.visibility', '=', AssetVisibility.TIMELINE)
|
||||||
.groupBy('stacked.stackId')
|
.groupBy('stacked.stackId')
|
||||||
.as('stacked_assets'),
|
.as('stacked_assets'),
|
||||||
(join) => join.onTrue(),
|
(join) => join.onTrue(),
|
||||||
@ -709,6 +706,7 @@ export class AssetRepository {
|
|||||||
.with('duplicates', (qb) =>
|
.with('duplicates', (qb) =>
|
||||||
qb
|
qb
|
||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
|
.$call(withDefaultVisibility)
|
||||||
.leftJoinLateral(
|
.leftJoinLateral(
|
||||||
(qb) =>
|
(qb) =>
|
||||||
qb
|
qb
|
||||||
@ -727,7 +725,6 @@ export class AssetRepository {
|
|||||||
.where('assets.duplicateId', 'is not', null)
|
.where('assets.duplicateId', 'is not', null)
|
||||||
.$narrowType<{ duplicateId: NotNull }>()
|
.$narrowType<{ duplicateId: NotNull }>()
|
||||||
.where('assets.deletedAt', 'is', null)
|
.where('assets.deletedAt', 'is', null)
|
||||||
.where('assets.visibility', '!=', AssetVisibility.HIDDEN)
|
|
||||||
.where('assets.stackId', 'is', null)
|
.where('assets.stackId', 'is', null)
|
||||||
.groupBy('assets.duplicateId'),
|
.groupBy('assets.duplicateId'),
|
||||||
)
|
)
|
||||||
|
@ -38,11 +38,6 @@ export interface PersonStatistics {
|
|||||||
assets: number;
|
assets: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PeopleStatistics {
|
|
||||||
total: number;
|
|
||||||
hidden: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DeleteFacesOptions {
|
export interface DeleteFacesOptions {
|
||||||
sourceType: SourceType;
|
sourceType: SourceType;
|
||||||
}
|
}
|
||||||
@ -151,7 +146,7 @@ export class PersonRepository {
|
|||||||
.innerJoin('assets', (join) =>
|
.innerJoin('assets', (join) =>
|
||||||
join
|
join
|
||||||
.onRef('asset_faces.assetId', '=', 'assets.id')
|
.onRef('asset_faces.assetId', '=', 'assets.id')
|
||||||
.on('assets.visibility', '!=', AssetVisibility.ARCHIVE)
|
.on('assets.visibility', '=', sql.lit(AssetVisibility.TIMELINE))
|
||||||
.on('assets.deletedAt', 'is', null),
|
.on('assets.deletedAt', 'is', null),
|
||||||
)
|
)
|
||||||
.where('person.ownerId', '=', userId)
|
.where('person.ownerId', '=', userId)
|
||||||
@ -341,7 +336,7 @@ export class PersonRepository {
|
|||||||
join
|
join
|
||||||
.onRef('assets.id', '=', 'asset_faces.assetId')
|
.onRef('assets.id', '=', 'asset_faces.assetId')
|
||||||
.on('asset_faces.personId', '=', personId)
|
.on('asset_faces.personId', '=', personId)
|
||||||
.on('assets.visibility', '!=', AssetVisibility.ARCHIVE)
|
.on('assets.visibility', '=', sql.lit(AssetVisibility.TIMELINE))
|
||||||
.on('assets.deletedAt', 'is', null),
|
.on('assets.deletedAt', 'is', null),
|
||||||
)
|
)
|
||||||
.select((eb) => eb.fn.count(eb.fn('distinct', ['assets.id'])).as('count'))
|
.select((eb) => eb.fn.count(eb.fn('distinct', ['assets.id'])).as('count'))
|
||||||
@ -354,35 +349,31 @@ export class PersonRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
async getNumberOfPeople(userId: string): Promise<PeopleStatistics> {
|
getNumberOfPeople(userId: string) {
|
||||||
const items = await this.db
|
const zero = sql.lit(0);
|
||||||
|
return this.db
|
||||||
.selectFrom('person')
|
.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('person.ownerId', '=', userId)
|
||||||
.where('asset_faces.deletedAt', 'is', null)
|
.select((eb) => eb.fn.coalesce(eb.fn.countAll<number>(), zero).as('total'))
|
||||||
.innerJoin('assets', (join) =>
|
.select((eb) => eb.fn.coalesce(eb.fn.countAll<number>().filterWhere('isHidden', '=', true), zero).as('hidden'))
|
||||||
join
|
.executeTakeFirstOrThrow();
|
||||||
.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),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
create(person: Insertable<Person>) {
|
create(person: Insertable<Person>) {
|
||||||
|
@ -7,7 +7,7 @@ import { DummyValue, GenerateSql } from 'src/decorators';
|
|||||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import { AssetStatus, AssetType, AssetVisibility, VectorIndex } from 'src/enum';
|
import { AssetStatus, AssetType, AssetVisibility, VectorIndex } from 'src/enum';
|
||||||
import { probes } from 'src/repositories/database.repository';
|
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 { paginationHelper } from 'src/utils/pagination';
|
||||||
import { isValidInteger } from 'src/validation';
|
import { isValidInteger } from 'src/validation';
|
||||||
|
|
||||||
@ -268,6 +268,7 @@ export class SearchRepository {
|
|||||||
.with('cte', (qb) =>
|
.with('cte', (qb) =>
|
||||||
qb
|
qb
|
||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
|
.$call(withDefaultVisibility)
|
||||||
.select([
|
.select([
|
||||||
'assets.id as assetId',
|
'assets.id as assetId',
|
||||||
'assets.duplicateId',
|
'assets.duplicateId',
|
||||||
@ -276,7 +277,6 @@ export class SearchRepository {
|
|||||||
.innerJoin('smart_search', 'assets.id', 'smart_search.assetId')
|
.innerJoin('smart_search', 'assets.id', 'smart_search.assetId')
|
||||||
.where('assets.ownerId', '=', anyUuid(userIds))
|
.where('assets.ownerId', '=', anyUuid(userIds))
|
||||||
.where('assets.deletedAt', 'is', null)
|
.where('assets.deletedAt', 'is', null)
|
||||||
.where('assets.visibility', '!=', AssetVisibility.HIDDEN)
|
|
||||||
.where('assets.type', '=', type)
|
.where('assets.type', '=', type)
|
||||||
.where('assets.id', '!=', asUuid(assetId))
|
.where('assets.id', '!=', asUuid(assetId))
|
||||||
.where('assets.stackId', 'is', null)
|
.where('assets.stackId', 'is', null)
|
||||||
@ -472,7 +472,7 @@ export class SearchRepository {
|
|||||||
.distinctOn(field)
|
.distinctOn(field)
|
||||||
.innerJoin('assets', 'assets.id', 'exif.assetId')
|
.innerJoin('assets', 'assets.id', 'exif.assetId')
|
||||||
.where('ownerId', '=', anyUuid(userIds))
|
.where('ownerId', '=', anyUuid(userIds))
|
||||||
.where('visibility', '!=', AssetVisibility.HIDDEN)
|
.where('visibility', '=', AssetVisibility.TIMELINE)
|
||||||
.where('deletedAt', 'is', null)
|
.where('deletedAt', 'is', null)
|
||||||
.where(field, 'is not', null);
|
.where(field, 'is not', null);
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import { InjectKysely } from 'nestjs-kysely';
|
|||||||
import { columns } from 'src/database';
|
import { columns } from 'src/database';
|
||||||
import { AssetStack, DB } from 'src/db';
|
import { AssetStack, DB } from 'src/db';
|
||||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { asUuid } from 'src/utils/database';
|
import { asUuid, withDefaultVisibility } from 'src/utils/database';
|
||||||
|
|
||||||
export interface StackSearch {
|
export interface StackSearch {
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
@ -34,7 +34,8 @@ const withAssets = (eb: ExpressionBuilder<DB, 'asset_stack'>, withTags = false)
|
|||||||
)
|
)
|
||||||
.select((eb) => eb.fn.toJson('exifInfo').as('exifInfo'))
|
.select((eb) => eb.fn.toJson('exifInfo').as('exifInfo'))
|
||||||
.where('assets.deletedAt', 'is', null)
|
.where('assets.deletedAt', 'is', null)
|
||||||
.whereRef('assets.stackId', '=', 'asset_stack.id'),
|
.whereRef('assets.stackId', '=', 'asset_stack.id')
|
||||||
|
.$call(withDefaultVisibility),
|
||||||
).as('assets');
|
).as('assets');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -210,9 +210,9 @@ export class UserRepository {
|
|||||||
getUserStats() {
|
getUserStats() {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('users')
|
.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')
|
.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) => [
|
.select((eb) => [
|
||||||
eb.fn
|
eb.fn
|
||||||
.countAll<number>()
|
.countAll<number>()
|
||||||
@ -256,7 +256,6 @@ export class UserRepository {
|
|||||||
)
|
)
|
||||||
.as('usageVideos'),
|
.as('usageVideos'),
|
||||||
])
|
])
|
||||||
.where('assets.deletedAt', 'is', null)
|
|
||||||
.groupBy('users.id')
|
.groupBy('users.id')
|
||||||
.orderBy('users.createdAt', 'asc')
|
.orderBy('users.createdAt', 'asc')
|
||||||
.execute();
|
.execute();
|
||||||
|
@ -17,11 +17,16 @@ import { AuthDto } from 'src/dtos/auth.dto';
|
|||||||
import { AssetStatus, AssetVisibility, JobName, JobStatus, Permission, QueueName } from 'src/enum';
|
import { AssetStatus, AssetVisibility, JobName, JobStatus, Permission, QueueName } from 'src/enum';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { ISidecarWriteJob, JobItem, JobOf } from 'src/types';
|
import { ISidecarWriteJob, JobItem, JobOf } from 'src/types';
|
||||||
|
import { requireElevatedPermission } from 'src/utils/access';
|
||||||
import { getAssetFiles, getMyPartnerIds, onAfterUnlink, onBeforeLink, onBeforeUnlink } from 'src/utils/asset.util';
|
import { getAssetFiles, getMyPartnerIds, onAfterUnlink, onBeforeLink, onBeforeUnlink } from 'src/utils/asset.util';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AssetService extends BaseService {
|
export class AssetService extends BaseService {
|
||||||
async getStatistics(auth: AuthDto, dto: AssetStatsDto) {
|
async getStatistics(auth: AuthDto, dto: AssetStatsDto) {
|
||||||
|
if (dto.visibility === AssetVisibility.LOCKED) {
|
||||||
|
requireElevatedPermission(auth);
|
||||||
|
}
|
||||||
|
|
||||||
const stats = await this.assetRepository.getStatistics(auth.user.id, dto);
|
const stats = await this.assetRepository.getStatistics(auth.user.id, dto);
|
||||||
return mapStats(stats);
|
return mapStats(stats);
|
||||||
}
|
}
|
||||||
|
@ -14,8 +14,9 @@ import {
|
|||||||
SearchSuggestionType,
|
SearchSuggestionType,
|
||||||
SmartSearchDto,
|
SmartSearchDto,
|
||||||
} from 'src/dtos/search.dto';
|
} from 'src/dtos/search.dto';
|
||||||
import { AssetOrder } from 'src/enum';
|
import { AssetOrder, AssetVisibility } from 'src/enum';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
|
import { requireElevatedPermission } from 'src/utils/access';
|
||||||
import { getMyPartnerIds } from 'src/utils/asset.util';
|
import { getMyPartnerIds } from 'src/utils/asset.util';
|
||||||
import { isSmartSearchEnabled } from 'src/utils/misc';
|
import { isSmartSearchEnabled } from 'src/utils/misc';
|
||||||
|
|
||||||
@ -40,9 +41,11 @@ export class SearchService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async searchMetadata(auth: AuthDto, dto: MetadataSearchDto): Promise<SearchResponseDto> {
|
async searchMetadata(auth: AuthDto, dto: MetadataSearchDto): Promise<SearchResponseDto> {
|
||||||
let checksum: Buffer | undefined;
|
if (dto.visibility === AssetVisibility.LOCKED) {
|
||||||
const userIds = await this.getUserIdsToSearch(auth);
|
requireElevatedPermission(auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
let checksum: Buffer | undefined;
|
||||||
if (dto.checksum) {
|
if (dto.checksum) {
|
||||||
const encoding = dto.checksum.length === 28 ? 'base64' : 'hex';
|
const encoding = dto.checksum.length === 28 ? 'base64' : 'hex';
|
||||||
checksum = Buffer.from(dto.checksum, encoding);
|
checksum = Buffer.from(dto.checksum, encoding);
|
||||||
@ -50,6 +53,7 @@ export class SearchService extends BaseService {
|
|||||||
|
|
||||||
const page = dto.page ?? 1;
|
const page = dto.page ?? 1;
|
||||||
const size = dto.size || 250;
|
const size = dto.size || 250;
|
||||||
|
const userIds = await this.getUserIdsToSearch(auth);
|
||||||
const { hasNextPage, items } = await this.searchRepository.searchMetadata(
|
const { hasNextPage, items } = await this.searchRepository.searchMetadata(
|
||||||
{ page, size },
|
{ page, size },
|
||||||
{
|
{
|
||||||
@ -64,12 +68,20 @@ export class SearchService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async searchRandom(auth: AuthDto, dto: RandomSearchDto): Promise<AssetResponseDto[]> {
|
async searchRandom(auth: AuthDto, dto: RandomSearchDto): Promise<AssetResponseDto[]> {
|
||||||
|
if (dto.visibility === AssetVisibility.LOCKED) {
|
||||||
|
requireElevatedPermission(auth);
|
||||||
|
}
|
||||||
|
|
||||||
const userIds = await this.getUserIdsToSearch(auth);
|
const userIds = await this.getUserIdsToSearch(auth);
|
||||||
const items = await this.searchRepository.searchRandom(dto.size || 250, { ...dto, userIds });
|
const items = await this.searchRepository.searchRandom(dto.size || 250, { ...dto, userIds });
|
||||||
return items.map((item) => mapAsset(item, { auth }));
|
return items.map((item) => mapAsset(item, { auth }));
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchSmart(auth: AuthDto, dto: SmartSearchDto): Promise<SearchResponseDto> {
|
async searchSmart(auth: AuthDto, dto: SmartSearchDto): Promise<SearchResponseDto> {
|
||||||
|
if (dto.visibility === AssetVisibility.LOCKED) {
|
||||||
|
requireElevatedPermission(auth);
|
||||||
|
}
|
||||||
|
|
||||||
const { machineLearning } = await this.getConfig({ withCache: false });
|
const { machineLearning } = await this.getConfig({ withCache: false });
|
||||||
if (!isSmartSearchEnabled(machineLearning)) {
|
if (!isSmartSearchEnabled(machineLearning)) {
|
||||||
throw new BadRequestException('Smart search is not enabled');
|
throw new BadRequestException('Smart search is not enabled');
|
||||||
|
@ -4,6 +4,7 @@ import { TimeBucketAssetDto, TimeBucketDto, TimeBucketsResponseDto } from 'src/d
|
|||||||
import { AssetVisibility, Permission } from 'src/enum';
|
import { AssetVisibility, Permission } from 'src/enum';
|
||||||
import { TimeBucketOptions } from 'src/repositories/asset.repository';
|
import { TimeBucketOptions } from 'src/repositories/asset.repository';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
|
import { requireElevatedPermission } from 'src/utils/access';
|
||||||
import { getMyPartnerIds } from 'src/utils/asset.util';
|
import { getMyPartnerIds } from 'src/utils/asset.util';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -44,6 +45,10 @@ export class TimelineService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) {
|
private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) {
|
||||||
|
if (dto.visibility === AssetVisibility.LOCKED) {
|
||||||
|
requireElevatedPermission(auth);
|
||||||
|
}
|
||||||
|
|
||||||
if (dto.albumId) {
|
if (dto.albumId) {
|
||||||
await this.requireAccess({ auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] });
|
await this.requireAccess({ auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] });
|
||||||
} else {
|
} else {
|
||||||
|
@ -304,3 +304,9 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const requireElevatedPermission = (auth: AuthDto) => {
|
||||||
|
if (!auth.session?.hasElevatedPermission) {
|
||||||
|
throw new UnauthorizedException('Elevated permission is required');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -153,17 +153,12 @@ export function toJson<DB, TB extends keyof DB & string, T extends TB | Expressi
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_checksum';
|
export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_checksum';
|
||||||
// TODO come up with a better query that only selects the fields we need
|
|
||||||
|
|
||||||
export function withDefaultVisibility<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
|
export function withDefaultVisibility<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
|
||||||
return qb.where((qb) =>
|
return qb.where('assets.visibility', 'in', [sql.lit(AssetVisibility.ARCHIVE), sql.lit(AssetVisibility.TIMELINE)]);
|
||||||
qb.or([
|
|
||||||
qb('assets.visibility', '=', AssetVisibility.TIMELINE),
|
|
||||||
qb('assets.visibility', '=', AssetVisibility.ARCHIVE),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO come up with a better query that only selects the fields we need
|
||||||
export function withExif<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
|
export function withExif<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
|
||||||
return qb
|
return qb
|
||||||
.leftJoin('exif', 'assets.id', 'exif.assetId')
|
.leftJoin('exif', 'assets.id', 'exif.assetId')
|
||||||
|
Loading…
x
Reference in New Issue
Block a user