perf(server): check shared link asset access without scanning the album (#32411)

* perf(server): check shared link asset access without scanning the album

checkSharedLinkAccess runs on every thumbnail, preview, playback and asset
info request made through a shared link. It joined every asset of the
link's album and only then filtered on an array overlap across four
outer-joined columns, so each request cost O(album size).

Split it into two branches (link assets, album assets) that find the
requested ids through the asset pkey / livePhotoVideoId index and then
probe the album_asset pkey.

Median per request, PG 14, 1.36M-asset instance:

| album behind the link | before | after   |
|-----------------------|--------|---------|
| 290 assets            | 2.1 ms | 0.17 ms |
| 150k assets           | 296 ms | 0.19 ms |

* chore: clean up

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
This commit is contained in:
bo0tzz
2026-10-14 00:33:29 +02:00
committed by GitHub
parent aec0dd1e6b
commit bb024c8626
2 changed files with 83 additions and 49 deletions
+44 -20
View File
@@ -125,29 +125,53 @@ where
and "asset"."id" in ($2)
-- AccessRepository.asset.checkSharedLinkAccess
with
"target" as (
select
array[$1]::uuid[] as "ids"
),
"link" as (
select
"shared_link"."id",
"shared_link"."albumId"
from
"shared_link"
where
"shared_link"."id" = $2
)
select
"asset"."id" as "assetId",
"asset"."livePhotoVideoId" as "assetLivePhotoVideoId",
"albumAssets"."id" as "albumAssetId",
"albumAssets"."livePhotoVideoId" as "albumAssetLivePhotoVideoId"
"asset"."id",
"asset"."livePhotoVideoId"
from
"shared_link"
left join "album" on "album"."id" = "shared_link"."albumId"
and "album"."deletedAt" is null
left join "shared_link_asset" on "shared_link_asset"."sharedLinkId" = "shared_link"."id"
left join "asset" on "asset"."id" = "shared_link_asset"."assetId"
and "asset"."deletedAt" is null
left join "album_asset" on "album_asset"."albumId" = "album"."id"
left join "asset" as "albumAssets" on "albumAssets"."id" = "album_asset"."assetId"
and "albumAssets"."deletedAt" is null
"asset"
cross join "target"
inner join lateral (
select
1 as "match"
from
"link"
inner join "shared_link_asset" on "shared_link_asset"."sharedLinkId" = "link"."id"
where
"shared_link_asset"."assetId" = "asset"."id"
union all
select
1 as "match"
from
"link"
inner join "album" on "album"."id" = "link"."albumId"
and "album"."deletedAt" is null
inner join "album_asset" on "album_asset"."albumId" = "album"."id"
where
"album_asset"."assetId" = "asset"."id"
limit
$3
) as "membership" on true
where
"shared_link"."id" = $1
and array[
"asset"."id",
"asset"."livePhotoVideoId",
"albumAssets"."id",
"albumAssets"."livePhotoVideoId"
] && array[$2]::uuid[]
"asset"."deletedAt" is null
and (
"asset"."id" = any ("target"."ids")
or "asset"."livePhotoVideoId" = any ("target"."ids")
)
-- AccessRepository.assetFile.checkOwnerAccess
select
+39 -29
View File
@@ -236,43 +236,53 @@ class AssetAccess {
}
return this.db
.selectFrom('shared_link')
.leftJoin('album', (join) => join.onRef('album.id', '=', 'shared_link.albumId').on('album.deletedAt', 'is', null))
.leftJoin('shared_link_asset', 'shared_link_asset.sharedLinkId', 'shared_link.id')
.leftJoin('asset', (join) =>
join.onRef('asset.id', '=', 'shared_link_asset.assetId').on('asset.deletedAt', 'is', null),
.with('target', (qb) => qb.selectNoFrom(sql<string[]>`array[${sql.join([...assetIds])}]::uuid[]`.as('ids')))
.with('link', (qb) =>
qb
.selectFrom('shared_link')
.select(['shared_link.id', 'shared_link.albumId'])
.where('shared_link.id', '=', sharedLinkId),
)
.leftJoin('album_asset', 'album_asset.albumId', 'album.id')
.leftJoin('asset as albumAssets', (join) =>
join.onRef('albumAssets.id', '=', 'album_asset.assetId').on('albumAssets.deletedAt', 'is', null),
.selectFrom('asset')
.crossJoin('target')
.innerJoinLateral(
(eb) =>
eb
.selectFrom('link')
.innerJoin('shared_link_asset', 'shared_link_asset.sharedLinkId', 'link.id')
.select(sql.lit(1).as('match'))
.whereRef('shared_link_asset.assetId', '=', 'asset.id')
.unionAll(
eb
.selectFrom('link')
.innerJoin('album', (join) =>
join.onRef('album.id', '=', 'link.albumId').on('album.deletedAt', 'is', null),
)
.innerJoin('album_asset', 'album_asset.albumId', 'album.id')
.select(sql.lit(1).as('match'))
.whereRef('album_asset.assetId', '=', 'asset.id'),
)
.limit(1)
.as('membership'),
(join) => join.onTrue(),
)
.select([
'asset.id as assetId',
'asset.livePhotoVideoId as assetLivePhotoVideoId',
'albumAssets.id as albumAssetId',
'albumAssets.livePhotoVideoId as albumAssetLivePhotoVideoId',
])
.where('shared_link.id', '=', sharedLinkId)
.where(
sql`array["asset"."id", "asset"."livePhotoVideoId", "albumAssets"."id", "albumAssets"."livePhotoVideoId"]`,
'&&',
sql`array[${sql.join([...assetIds])}]::uuid[] `,
.select(['asset.id', 'asset.livePhotoVideoId'])
.where('asset.deletedAt', 'is', null)
.where((eb) =>
eb.or([
eb('asset.id', '=', eb.fn.any('target.ids')),
eb('asset.livePhotoVideoId', '=', eb.fn.any('target.ids')),
]),
)
.execute()
.then((rows) => {
const allowedIds = new Set<string>();
for (const row of rows) {
if (row.assetId && assetIds.has(row.assetId)) {
allowedIds.add(row.assetId);
if (assetIds.has(row.id)) {
allowedIds.add(row.id);
}
if (row.assetLivePhotoVideoId && assetIds.has(row.assetLivePhotoVideoId)) {
allowedIds.add(row.assetLivePhotoVideoId);
}
if (row.albumAssetId && assetIds.has(row.albumAssetId)) {
allowedIds.add(row.albumAssetId);
}
if (row.albumAssetLivePhotoVideoId && assetIds.has(row.albumAssetLivePhotoVideoId)) {
allowedIds.add(row.albumAssetLivePhotoVideoId);
if (row.livePhotoVideoId && assetIds.has(row.livePhotoVideoId)) {
allowedIds.add(row.livePhotoVideoId);
}
}
return allowedIds;