mirror of
https://github.com/immich-app/immich.git
synced 2026-07-28 14:47:30 -07:00
fix(server): dissolve stack when its non-primary assets are deleted (#29354)
This commit is contained in:
@@ -568,6 +568,34 @@ describe(AssetService.name, () => {
|
||||
expect(mocks.stack.delete).toHaveBeenCalledWith(asset.stackId);
|
||||
});
|
||||
|
||||
it('should delete the stack when a non-primary asset is deleted and only the primary would remain', async () => {
|
||||
const asset = AssetFactory.from().build();
|
||||
const deletionAsset = {
|
||||
...getForAssetDeletion(asset),
|
||||
stack: { id: newUuid(), primaryAssetId: newUuid(), assets: [{ id: asset.id }] },
|
||||
};
|
||||
mocks.stack.delete.mockResolvedValue();
|
||||
mocks.assetJob.getForAssetDeletion.mockResolvedValue(deletionAsset);
|
||||
|
||||
await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true });
|
||||
|
||||
expect(mocks.stack.delete).toHaveBeenCalledWith(deletionAsset.stack.id);
|
||||
});
|
||||
|
||||
it('should keep the stack when a non-primary asset is deleted and the primary plus another asset remain', async () => {
|
||||
const asset = AssetFactory.from().build();
|
||||
const deletionAsset = {
|
||||
...getForAssetDeletion(asset),
|
||||
stack: { id: newUuid(), primaryAssetId: newUuid(), assets: [{ id: asset.id }, { id: newUuid() }] },
|
||||
};
|
||||
mocks.assetJob.getForAssetDeletion.mockResolvedValue(deletionAsset);
|
||||
|
||||
await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true });
|
||||
|
||||
expect(mocks.stack.delete).not.toHaveBeenCalled();
|
||||
expect(mocks.stack.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should delete a live photo', async () => {
|
||||
const motionAsset = AssetFactory.from({ type: AssetType.Video, visibility: AssetVisibility.Hidden }).build();
|
||||
const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id });
|
||||
|
||||
@@ -316,18 +316,25 @@ export class AssetService extends BaseService {
|
||||
return JobStatus.Failed;
|
||||
}
|
||||
|
||||
// replace the parent of the stack children with a new asset
|
||||
if (asset.stack?.primaryAssetId === id) {
|
||||
// this only includes timeline visible assets and excludes the primary asset
|
||||
const stackAssetIds = asset.stack.assets.map((a) => a.id);
|
||||
if (stackAssetIds.length >= 2) {
|
||||
const newPrimaryAssetId = stackAssetIds.find((a) => a !== id)!;
|
||||
if (asset.stack) {
|
||||
// asset.stack.assets only includes timeline visible assets and excludes the primary asset
|
||||
const remainingStackAssetIds = asset.stack.assets.map((a) => a.id).filter((assetId) => assetId !== id);
|
||||
|
||||
// the primary survives unless it is the asset being deleted
|
||||
let remainingCount = remainingStackAssetIds.length;
|
||||
if (asset.stack.primaryAssetId !== id) {
|
||||
remainingCount++;
|
||||
}
|
||||
|
||||
if (remainingCount < 2) {
|
||||
// 0 or 1 asset would remain: dissolve the stack so it does not linger as a single-asset stack
|
||||
await this.stackRepository.delete(asset.stack.id);
|
||||
} else if (asset.stack.primaryAssetId === id) {
|
||||
// the primary is being deleted but others remain: promote a new primary
|
||||
await this.stackRepository.update(asset.stack.id, {
|
||||
id: asset.stack.id,
|
||||
primaryAssetId: newPrimaryAssetId,
|
||||
primaryAssetId: remainingStackAssetIds[0],
|
||||
});
|
||||
} else {
|
||||
await this.stackRepository.delete(asset.stack.id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user