mirror of
https://github.com/immich-app/immich.git
synced 2024-11-24 08:52:28 +02:00
fix(server): delete large album (#11042)
fix: large album asset operations
This commit is contained in:
parent
f0d1dbccf4
commit
66fae76af2
@ -49,23 +49,26 @@ function chunks<T>(collection: Array<T> | Set<T>, size: number): Array<Array<T>>
|
||||
* @param options.paramIndex The index of the function parameter to chunk. Defaults to 0.
|
||||
* @param options.flatten Whether to flatten the results. Defaults to false.
|
||||
*/
|
||||
export function Chunked(options: { paramIndex?: number; mergeFn?: (results: any) => any } = {}): MethodDecorator {
|
||||
export function Chunked(
|
||||
options: { paramIndex?: number; chunkSize?: number; mergeFn?: (results: any) => any } = {},
|
||||
): MethodDecorator {
|
||||
return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
|
||||
const originalMethod = descriptor.value;
|
||||
const parameterIndex = options.paramIndex ?? 0;
|
||||
const chunkSize = options.chunkSize || DATABASE_PARAMETER_CHUNK_SIZE;
|
||||
descriptor.value = async function (...arguments_: any[]) {
|
||||
const argument = arguments_[parameterIndex];
|
||||
|
||||
// Early return if argument length is less than or equal to the chunk size.
|
||||
if (
|
||||
(Array.isArray(argument) && argument.length <= DATABASE_PARAMETER_CHUNK_SIZE) ||
|
||||
(argument instanceof Set && argument.size <= DATABASE_PARAMETER_CHUNK_SIZE)
|
||||
(Array.isArray(argument) && argument.length <= chunkSize) ||
|
||||
(argument instanceof Set && argument.size <= chunkSize)
|
||||
) {
|
||||
return await originalMethod.apply(this, arguments_);
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
chunks(argument, DATABASE_PARAMETER_CHUNK_SIZE).map(async (chunk) => {
|
||||
chunks(argument, chunkSize).map(async (chunk) => {
|
||||
await Reflect.apply(originalMethod, this, [
|
||||
...arguments_.slice(0, parameterIndex),
|
||||
chunk,
|
||||
|
@ -30,6 +30,6 @@ export interface IAlbumRepository extends IBulkAsset {
|
||||
getAll(): Promise<AlbumEntity[]>;
|
||||
create(album: Partial<AlbumEntity>): Promise<AlbumEntity>;
|
||||
update(album: Partial<AlbumEntity>): Promise<AlbumEntity>;
|
||||
delete(album: AlbumEntity): Promise<void>;
|
||||
delete(id: string): Promise<void>;
|
||||
updateThumbnails(): Promise<number | undefined>;
|
||||
}
|
||||
|
@ -5,7 +5,16 @@ import { AlbumEntity } from 'src/entities/album.entity';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { Instrumentation } from 'src/utils/instrumentation';
|
||||
import { DataSource, FindOptionsOrder, FindOptionsRelations, In, IsNull, Not, Repository } from 'typeorm';
|
||||
import {
|
||||
DataSource,
|
||||
EntityManager,
|
||||
FindOptionsOrder,
|
||||
FindOptionsRelations,
|
||||
In,
|
||||
IsNull,
|
||||
Not,
|
||||
Repository,
|
||||
} from 'typeorm';
|
||||
|
||||
const withoutDeletedUsers = <T extends AlbumEntity | null>(album: T) => {
|
||||
if (album) {
|
||||
@ -255,24 +264,46 @@ export class AlbumRepository implements IAlbumRepository {
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] })
|
||||
async addAssetIds(albumId: string, assetIds: string[]): Promise<void> {
|
||||
await this.dataSource
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into('albums_assets_assets', ['albumsId', 'assetsId'])
|
||||
.values(assetIds.map((assetId) => ({ albumsId: albumId, assetsId: assetId })))
|
||||
.execute();
|
||||
await this.addAssets(this.dataSource.manager, albumId, assetIds);
|
||||
}
|
||||
|
||||
create(album: Partial<AlbumEntity>): Promise<AlbumEntity> {
|
||||
return this.save(album);
|
||||
return this.dataSource.transaction<AlbumEntity>(async (manager) => {
|
||||
const { id } = await manager.save(AlbumEntity, { ...album, assets: [] });
|
||||
const assetIds = (album.assets || []).map((asset) => asset.id);
|
||||
await this.addAssets(manager, id, assetIds);
|
||||
return manager.findOneOrFail(AlbumEntity, {
|
||||
where: { id },
|
||||
relations: {
|
||||
owner: true,
|
||||
albumUsers: { user: true },
|
||||
sharedLinks: true,
|
||||
assets: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
update(album: Partial<AlbumEntity>): Promise<AlbumEntity> {
|
||||
return this.save(album);
|
||||
}
|
||||
|
||||
async delete(album: AlbumEntity): Promise<void> {
|
||||
await this.repository.remove(album);
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.repository.delete({ id });
|
||||
}
|
||||
|
||||
@Chunked({ paramIndex: 2, chunkSize: 30_000 })
|
||||
private async addAssets(manager: EntityManager, albumId: string, assetIds: string[]): Promise<void> {
|
||||
if (assetIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into('albums_assets_assets', ['albumsId', 'assetsId'])
|
||||
.values(assetIds.map((assetId) => ({ albumsId: albumId, assetsId: assetId })))
|
||||
.execute();
|
||||
}
|
||||
|
||||
private async save(album: Partial<AlbumEntity>) {
|
||||
|
@ -302,8 +302,7 @@ describe(AlbumService.name, () => {
|
||||
|
||||
describe('delete', () => {
|
||||
it('should throw an error for an album not found', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id]));
|
||||
albumMock.getById.mockResolvedValue(null);
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set());
|
||||
|
||||
await expect(sut.delete(authStub.admin, albumStub.sharedWithAdmin.id)).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
@ -329,7 +328,7 @@ describe(AlbumService.name, () => {
|
||||
await sut.delete(authStub.admin, albumStub.empty.id);
|
||||
|
||||
expect(albumMock.delete).toHaveBeenCalledTimes(1);
|
||||
expect(albumMock.delete).toHaveBeenCalledWith(albumStub.empty);
|
||||
expect(albumMock.delete).toHaveBeenCalledWith(albumStub.empty.id);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -165,10 +165,7 @@ export class AlbumService {
|
||||
|
||||
async delete(auth: AuthDto, id: string): Promise<void> {
|
||||
await this.access.requirePermission(auth, Permission.ALBUM_DELETE, id);
|
||||
|
||||
const album = await this.findOrFail(id, { withAssets: false });
|
||||
|
||||
await this.albumRepository.delete(album);
|
||||
await this.albumRepository.delete(id);
|
||||
}
|
||||
|
||||
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||
|
Loading…
Reference in New Issue
Block a user