mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	fix(web/server) uploaded asset in shared link not loaded (#1766)
* fix(web/server): Uploaded asset to shared link does not get added to the shared link/album * remove unused code * Add endpoints for each remove and add assets to shared link * Update api * Added deletion logic * Convert callback to async/await * Fix linter * Fix test * Fix server test * added test * Test coverage * modify DTO * Add notification * fix test
This commit is contained in:
		@@ -1,3 +1,4 @@
 | 
			
		||||
import { AddAssetsDto } from './../album/dto/add-assets.dto';
 | 
			
		||||
import {
 | 
			
		||||
  Controller,
 | 
			
		||||
  Post,
 | 
			
		||||
@@ -52,10 +53,10 @@ import {
 | 
			
		||||
import { DownloadFilesDto } from './dto/download-files.dto';
 | 
			
		||||
import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
 | 
			
		||||
import { SharedLinkResponseDto } from '@app/domain';
 | 
			
		||||
import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
 | 
			
		||||
import { AssetSearchDto } from './dto/asset-search.dto';
 | 
			
		||||
import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config';
 | 
			
		||||
import FileNotEmptyValidator from '../validation/file-not-empty-validator';
 | 
			
		||||
import { RemoveAssetsDto } from '../album/dto/remove-assets.dto';
 | 
			
		||||
 | 
			
		||||
function asStreamableFile({ stream, type, length }: ImmichReadStream) {
 | 
			
		||||
  return new StreamableFile(stream, { type, length });
 | 
			
		||||
@@ -330,11 +331,20 @@ export class AssetController {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Authenticated({ isShared: true })
 | 
			
		||||
  @Patch('/shared-link')
 | 
			
		||||
  async updateAssetsInSharedLink(
 | 
			
		||||
  @Patch('/shared-link/add')
 | 
			
		||||
  async addAssetsToSharedLink(
 | 
			
		||||
    @GetAuthUser() authUser: AuthUserDto,
 | 
			
		||||
    @Body(ValidationPipe) dto: UpdateAssetsToSharedLinkDto,
 | 
			
		||||
    @Body(ValidationPipe) dto: AddAssetsDto,
 | 
			
		||||
  ): Promise<SharedLinkResponseDto> {
 | 
			
		||||
    return await this.assetService.updateAssetsInSharedLink(authUser, dto);
 | 
			
		||||
    return await this.assetService.addAssetsToSharedLink(authUser, dto);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Authenticated({ isShared: true })
 | 
			
		||||
  @Patch('/shared-link/remove')
 | 
			
		||||
  async removeAssetsFromSharedLink(
 | 
			
		||||
    @GetAuthUser() authUser: AuthUserDto,
 | 
			
		||||
    @Body(ValidationPipe) dto: RemoveAssetsDto,
 | 
			
		||||
  ): Promise<SharedLinkResponseDto> {
 | 
			
		||||
    return await this.assetService.removeAssetsFromSharedLink(authUser, dto);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -198,14 +198,31 @@ describe('AssetService', () => {
 | 
			
		||||
      sharedLinkRepositoryMock.get.mockResolvedValue(null);
 | 
			
		||||
      sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true);
 | 
			
		||||
 | 
			
		||||
      await expect(sut.updateAssetsInSharedLink(authDto, dto)).rejects.toBeInstanceOf(BadRequestException);
 | 
			
		||||
      await expect(sut.addAssetsToSharedLink(authDto, dto)).rejects.toBeInstanceOf(BadRequestException);
 | 
			
		||||
 | 
			
		||||
      expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
 | 
			
		||||
      expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
 | 
			
		||||
      expect(sharedLinkRepositoryMock.hasAssetAccess).toHaveBeenCalledWith(authDto.sharedLinkId, asset1.id);
 | 
			
		||||
      expect(sharedLinkRepositoryMock.save).not.toHaveBeenCalled();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should add assets to a shared link', async () => {
 | 
			
		||||
      const asset1 = _getAsset_1();
 | 
			
		||||
 | 
			
		||||
      const authDto = authStub.adminSharedLink;
 | 
			
		||||
      const dto = { assetIds: [asset1.id] };
 | 
			
		||||
 | 
			
		||||
      assetRepositoryMock.getById.mockResolvedValue(asset1);
 | 
			
		||||
      sharedLinkRepositoryMock.get.mockResolvedValue(sharedLinkStub.valid);
 | 
			
		||||
      sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true);
 | 
			
		||||
      sharedLinkRepositoryMock.save.mockResolvedValue(sharedLinkStub.valid);
 | 
			
		||||
 | 
			
		||||
      await expect(sut.addAssetsToSharedLink(authDto, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
 | 
			
		||||
 | 
			
		||||
      expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
 | 
			
		||||
      expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
 | 
			
		||||
      expect(sharedLinkRepositoryMock.save).toHaveBeenCalled();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should remove assets from a shared link', async () => {
 | 
			
		||||
      const asset1 = _getAsset_1();
 | 
			
		||||
 | 
			
		||||
@@ -217,11 +234,11 @@ describe('AssetService', () => {
 | 
			
		||||
      sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true);
 | 
			
		||||
      sharedLinkRepositoryMock.save.mockResolvedValue(sharedLinkStub.valid);
 | 
			
		||||
 | 
			
		||||
      await expect(sut.updateAssetsInSharedLink(authDto, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
 | 
			
		||||
      await expect(sut.removeAssetsFromSharedLink(authDto, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
 | 
			
		||||
 | 
			
		||||
      expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
 | 
			
		||||
      expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
 | 
			
		||||
      expect(sharedLinkRepositoryMock.hasAssetAccess).toHaveBeenCalledWith(authDto.sharedLinkId, asset1.id);
 | 
			
		||||
      expect(sharedLinkRepositoryMock.save).toHaveBeenCalled();
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -58,8 +58,9 @@ import { ISharedLinkRepository } from '@app/domain';
 | 
			
		||||
import { DownloadFilesDto } from './dto/download-files.dto';
 | 
			
		||||
import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
 | 
			
		||||
import { mapSharedLink, SharedLinkResponseDto } from '@app/domain';
 | 
			
		||||
import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
 | 
			
		||||
import { AssetSearchDto } from './dto/asset-search.dto';
 | 
			
		||||
import { AddAssetsDto } from '../album/dto/add-assets.dto';
 | 
			
		||||
import { RemoveAssetsDto } from '../album/dto/remove-assets.dto';
 | 
			
		||||
 | 
			
		||||
const fileInfo = promisify(stat);
 | 
			
		||||
 | 
			
		||||
@@ -606,23 +607,35 @@ export class AssetService {
 | 
			
		||||
    return mapSharedLink(sharedLink);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async updateAssetsInSharedLink(
 | 
			
		||||
    authUser: AuthUserDto,
 | 
			
		||||
    dto: UpdateAssetsToSharedLinkDto,
 | 
			
		||||
  ): Promise<SharedLinkResponseDto> {
 | 
			
		||||
  async addAssetsToSharedLink(authUser: AuthUserDto, dto: AddAssetsDto): Promise<SharedLinkResponseDto> {
 | 
			
		||||
    if (!authUser.sharedLinkId) {
 | 
			
		||||
      throw new ForbiddenException();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const assets = [];
 | 
			
		||||
 | 
			
		||||
    await this.checkAssetsAccess(authUser, dto.assetIds);
 | 
			
		||||
    for (const assetId of dto.assetIds) {
 | 
			
		||||
      const asset = await this._assetRepository.getById(assetId);
 | 
			
		||||
      assets.push(asset);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const updatedLink = await this.shareCore.updateAssets(authUser.id, authUser.sharedLinkId, assets);
 | 
			
		||||
    const updatedLink = await this.shareCore.addAssets(authUser.id, authUser.sharedLinkId, assets);
 | 
			
		||||
    return mapSharedLink(updatedLink);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async removeAssetsFromSharedLink(authUser: AuthUserDto, dto: RemoveAssetsDto): Promise<SharedLinkResponseDto> {
 | 
			
		||||
    if (!authUser.sharedLinkId) {
 | 
			
		||||
      throw new ForbiddenException();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const assets = [];
 | 
			
		||||
 | 
			
		||||
    for (const assetId of dto.assetIds) {
 | 
			
		||||
      const asset = await this._assetRepository.getById(assetId);
 | 
			
		||||
      assets.push(asset);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const updatedLink = await this.shareCore.removeAssets(authUser.id, authUser.sharedLinkId, assets);
 | 
			
		||||
    return mapSharedLink(updatedLink);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +0,0 @@
 | 
			
		||||
import { IsNotEmpty } from 'class-validator';
 | 
			
		||||
 | 
			
		||||
export class UpdateAssetsToSharedLinkDto {
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
  assetIds!: string[];
 | 
			
		||||
}
 | 
			
		||||
@@ -1869,9 +1869,11 @@
 | 
			
		||||
            "bearer": []
 | 
			
		||||
          }
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "/asset/shared-link/add": {
 | 
			
		||||
      "patch": {
 | 
			
		||||
        "operationId": "updateAssetsInSharedLink",
 | 
			
		||||
        "operationId": "addAssetsToSharedLink",
 | 
			
		||||
        "description": "",
 | 
			
		||||
        "parameters": [],
 | 
			
		||||
        "requestBody": {
 | 
			
		||||
@@ -1879,7 +1881,44 @@
 | 
			
		||||
          "content": {
 | 
			
		||||
            "application/json": {
 | 
			
		||||
              "schema": {
 | 
			
		||||
                "$ref": "#/components/schemas/UpdateAssetsToSharedLinkDto"
 | 
			
		||||
                "$ref": "#/components/schemas/AddAssetsDto"
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "responses": {
 | 
			
		||||
          "200": {
 | 
			
		||||
            "description": "",
 | 
			
		||||
            "content": {
 | 
			
		||||
              "application/json": {
 | 
			
		||||
                "schema": {
 | 
			
		||||
                  "$ref": "#/components/schemas/SharedLinkResponseDto"
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "tags": [
 | 
			
		||||
          "Asset"
 | 
			
		||||
        ],
 | 
			
		||||
        "security": [
 | 
			
		||||
          {
 | 
			
		||||
            "bearer": []
 | 
			
		||||
          }
 | 
			
		||||
        ]
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "/asset/shared-link/remove": {
 | 
			
		||||
      "patch": {
 | 
			
		||||
        "operationId": "removeAssetsFromSharedLink",
 | 
			
		||||
        "description": "",
 | 
			
		||||
        "parameters": [],
 | 
			
		||||
        "requestBody": {
 | 
			
		||||
          "required": true,
 | 
			
		||||
          "content": {
 | 
			
		||||
            "application/json": {
 | 
			
		||||
              "schema": {
 | 
			
		||||
                "$ref": "#/components/schemas/RemoveAssetsDto"
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
@@ -4171,7 +4210,21 @@
 | 
			
		||||
          "assetIds"
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      "UpdateAssetsToSharedLinkDto": {
 | 
			
		||||
      "AddAssetsDto": {
 | 
			
		||||
        "type": "object",
 | 
			
		||||
        "properties": {
 | 
			
		||||
          "assetIds": {
 | 
			
		||||
            "type": "array",
 | 
			
		||||
            "items": {
 | 
			
		||||
              "type": "string"
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "required": [
 | 
			
		||||
          "assetIds"
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      "RemoveAssetsDto": {
 | 
			
		||||
        "type": "object",
 | 
			
		||||
        "properties": {
 | 
			
		||||
          "assetIds": {
 | 
			
		||||
@@ -4267,20 +4320,6 @@
 | 
			
		||||
          "sharedUserIds"
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      "AddAssetsDto": {
 | 
			
		||||
        "type": "object",
 | 
			
		||||
        "properties": {
 | 
			
		||||
          "assetIds": {
 | 
			
		||||
            "type": "array",
 | 
			
		||||
            "items": {
 | 
			
		||||
              "type": "string"
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "required": [
 | 
			
		||||
          "assetIds"
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      "AddAssetsResponseDto": {
 | 
			
		||||
        "type": "object",
 | 
			
		||||
        "properties": {
 | 
			
		||||
@@ -4302,20 +4341,6 @@
 | 
			
		||||
          "alreadyInAlbum"
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      "RemoveAssetsDto": {
 | 
			
		||||
        "type": "object",
 | 
			
		||||
        "properties": {
 | 
			
		||||
          "assetIds": {
 | 
			
		||||
            "type": "array",
 | 
			
		||||
            "items": {
 | 
			
		||||
              "type": "string"
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "required": [
 | 
			
		||||
          "assetIds"
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      "UpdateAlbumDto": {
 | 
			
		||||
        "type": "object",
 | 
			
		||||
        "properties": {
 | 
			
		||||
 
 | 
			
		||||
@@ -63,13 +63,24 @@ export class ShareCore {
 | 
			
		||||
    return this.repository.remove(link);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async updateAssets(userId: string, id: string, assets: AssetEntity[]) {
 | 
			
		||||
  async addAssets(userId: string, id: string, assets: AssetEntity[]) {
 | 
			
		||||
    const link = await this.get(userId, id);
 | 
			
		||||
    if (!link) {
 | 
			
		||||
      throw new BadRequestException('Shared link not found');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return this.repository.save({ ...link, assets });
 | 
			
		||||
    return this.repository.save({ ...link, assets: [...link.assets, ...assets] });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async removeAssets(userId: string, id: string, assets: AssetEntity[]) {
 | 
			
		||||
    const link = await this.get(userId, id);
 | 
			
		||||
    if (!link) {
 | 
			
		||||
      throw new BadRequestException('Shared link not found');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const newAssets = link.assets.filter((asset) => assets.find((a) => a.id === asset.id));
 | 
			
		||||
 | 
			
		||||
    return this.repository.save({ ...link, assets: newAssets });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async hasAssetAccess(id: string, assetId: string): Promise<boolean> {
 | 
			
		||||
 
 | 
			
		||||
@@ -140,9 +140,9 @@
 | 
			
		||||
      },
 | 
			
		||||
      "./libs/domain/": {
 | 
			
		||||
        "branches": 80,
 | 
			
		||||
        "functions": 89,
 | 
			
		||||
        "functions": 88,
 | 
			
		||||
        "lines": 95,
 | 
			
		||||
        "statements": 95
 | 
			
		||||
        "statements": 94
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "testEnvironment": "node",
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user