From b07891089f44a401aef6fd248d6c7caacabd47cd Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 21 Jan 2023 22:15:16 -0600 Subject: [PATCH] feat(web/server) Add more options to public shared link (#1348) * Added migration files * Added logic for shared album level * Added permission for EXIF * Update shared link response dto * Added condition to show download button * Create and edit link with new parameter: * Remove deadcode * PR feedback * More refactor * Move logic of allow original file to service * Simplify * Wording --- docs/docs/developer/setup.md | 2 +- mobile/openapi/doc/CreateAlbumShareLinkDto.md | Bin 554 -> 641 bytes .../openapi/doc/CreateAssetsShareLinkDto.md | Bin 583 -> 670 bytes mobile/openapi/doc/EditSharedLinkDto.md | Bin 566 -> 653 bytes mobile/openapi/doc/SharedLinkResponseDto.md | Bin 835 -> 900 bytes .../model/create_album_share_link_dto.dart | Bin 5651 -> 7079 bytes .../model/create_assets_share_link_dto.dart | Bin 5754 -> 7182 bytes .../lib/model/edit_shared_link_dto.dart | Bin 6086 -> 7514 bytes .../lib/model/shared_link_response_dto.dart | Bin 6571 -> 7119 bytes .../create_album_share_link_dto_test.dart | Bin 906 -> 1114 bytes .../create_assets_share_link_dto_test.dart | Bin 943 -> 1151 bytes .../test/edit_shared_link_dto_test.dart | Bin 904 -> 1112 bytes .../test/shared_link_response_dto_test.dart | Bin 1526 -> 1734 bytes .../src/api-v1/album/album.controller.ts | 2 + .../immich/src/api-v1/album/album.service.ts | 10 +++- .../album/dto/create-album-shared-link.dto.ts | 8 +++ .../src/api-v1/asset/asset.controller.ts | 14 +++-- .../immich/src/api-v1/asset/asset.service.ts | 39 +++++++++++--- .../asset/dto/create-asset-shared-link.dto.ts | 8 +++ .../asset/response-dto/asset-response.dto.ts | 23 +++++++++ .../share/dto/create-shared-link.dto.ts | 2 + .../api-v1/share/dto/edit-shared-link.dto.ts | 6 +++ .../response-dto/shared-link-response.dto.ts | 30 ++++++++++- .../src/api-v1/share/share.controller.ts | 2 +- .../immich/src/api-v1/share/share.core.ts | 13 ++++- .../immich/src/api-v1/share/share.service.ts | 26 +++++++--- .../metadata-extraction.processor.ts | 1 - server/immich-openapi-specs.json | 30 ++++++++++- .../libs/domain/src/auth/dto/auth-user.dto.ts | 2 + .../src/db/entities/shared-link.entity.ts | 8 ++- ...907194740-AddMorePermissionToSharedLink.ts | 15 ++++++ web/src/api/open-api/api.ts | 48 ++++++++++++++++++ .../components/album-page/album-viewer.svelte | 19 ++++--- .../asset-viewer/asset-viewer-nav-bar.svelte | 14 +++-- .../asset-viewer/asset-viewer.svelte | 12 ++++- .../individual-shared-viewer.svelte | 14 ++--- .../shared-components/base-modal.svelte | 2 +- .../create-shared-link-modal.svelte | 34 ++++++++++--- .../shared-components/dropdown-button.svelte | 2 +- .../gallery-viewer/gallery-viewer.svelte | 9 ++-- .../sharedlinks-page/shared-link-card.svelte | 20 +++++++- 41 files changed, 348 insertions(+), 67 deletions(-) create mode 100644 server/libs/infra/src/db/migrations/1673907194740-AddMorePermissionToSharedLink.ts diff --git a/docs/docs/developer/setup.md b/docs/docs/developer/setup.md index 13846ed9df..e2ffd9bfb7 100644 --- a/docs/docs/developer/setup.md +++ b/docs/docs/developer/setup.md @@ -99,7 +99,7 @@ After making any changes in the `server/libs/database/src/entities`, a database 2. Run ```bash -npm run typeorm -- migration:generate ./libs/database/src/ -d libs/database/src/config/database.config.ts +npm run typeorm -- migration:generate ./libs/infra/src/db/ -d ./libs/infra/src/db/config/database.config.ts ``` 3. Check if the migration file makes sense. diff --git a/mobile/openapi/doc/CreateAlbumShareLinkDto.md b/mobile/openapi/doc/CreateAlbumShareLinkDto.md index dd305b4ff72ec5acf30d992764c2d6617553b2e7..254f455899f16b3fb838043fd0685d367b5165c4 100644 GIT binary patch delta 34 pcmZ3*(#X0Yl94kpCnvw$CBHmxvI(OqM{!1exobsc+GGpHPXNg93;zHB delta 11 ScmZo-v3^f1% delta 11 ScmeBW-Nv#ZiE;8y#w!3BKm>sR diff --git a/mobile/openapi/doc/SharedLinkResponseDto.md b/mobile/openapi/doc/SharedLinkResponseDto.md index c11a1a48966f64ce510aecf4d9e17ec14ffb6949..b8de598bee85cd607e88dc75fca6d594bc6166e8 100644 GIT binary patch delta 57 zcmX@i*22DF9+R$?R$@+0ez{A2d0tL_Vv3fQLXCo!R#JX`4wwn#acOB4XXKZ=R%E74 I?qb>r03oLm+yDRo delta 11 ScmZo+Kg_ma9@FIeOiKY9C=ocD-m)YxIii@OU~7wNu!5Qz*l30Pl8n?MxZxmmFk>d$vH$0U zn=+Y)L&6H?EJeMH#NrI+{FGEp1#Pf78ZccT6Y@%Paun2&ohG}seF@PHe@|vv* z3RefN)K&#%*5)@Hs~LGwlue$%DLGl6i&Yxt6!lmITSx%G)lK&1(%^u}PkzfO#{n}C zq+1HcLFj~uO!nuJN2mpA+&rD@01HwW$ntB*zzkN`ghUP~g5U-y@*@n`?9G3I4FKsc BqZI%E delta 39 xcmV+?0NDShHlJ6@m%CPErft5*SjRH?B?~tT zNYUnRENP5 z1%;~vS8A&QGiwv)T4Wb!a4AW{T%aDSU<(NsxG9qpxir9GGP#>ei34WFWF0O^DHsQ# r3nnu8BbPi}?dEoKSMqDfz%A5-#04lm;F@dsku)#ozsv>zXltBQ delta 40 ycmV+@0N4MHIQlHG=>fA80&@hjYzYAYlf4S@lN=1RvjPpP0kgpm1_iT66~_iWLJorf diff --git a/mobile/openapi/lib/model/edit_shared_link_dto.dart b/mobile/openapi/lib/model/edit_shared_link_dto.dart index 8d8b98844620edeb5db9455d7504dd030e080a87..09d43db554805952c7b75423c4b8c8ed3736ebce 100644 GIT binary patch delta 500 zcmX@6f6Hpa8AgS~oSgh}m;Ca)oczQT9WDhRD9Oky)+^4)FL$lTOxt{r@d+y@T-9bv zwj4$dxTGyxDw88z5@dvetu3mV3TkR#Qx)<{GE$4+=7QA0jhW1$!5EEUOkQbDj)J{{ zfr1sPiHdp|iNzVt`6;QI3ff?^HDD%zOhwWL6P~<+^MV12`L-%3TphSuY*k>IHkWZp zG4Y}(o4kNW5>=ymtb#2hz)%(LUnGDp4Q2UHqZu115 hy)0s-s%mK6a0oetUwF>>S0t^`elZg$zvv>{|0<)wMy#%x27I_AAArQp? diff --git a/mobile/openapi/lib/model/shared_link_response_dto.dart b/mobile/openapi/lib/model/shared_link_response_dto.dart index 9adf91f7a3f06930dcac49a02d5679f04a051653..e39891043aad3afd4654417db5eb9a0c09711d7d 100644 GIT binary patch delta 479 zcmZ2&eBOLRH`C;+i~_ugIXU^|F8SqoIr)hxlP@xga1>|cm%CPErfqI#TEwi9l%JoY zfTGBni%UTPEDTqv!nT$%KtW9nC;AHvW9pTrUhmaLN!cevjFc!R&E7^n)$SivqLA0-gi2j|Qj#vxf+X0h34y=d)o8)&jGW4$B3z;1t9L27NmU GeF_TjjS{T@ diff --git a/mobile/openapi/test/create_album_share_link_dto_test.dart b/mobile/openapi/test/create_album_share_link_dto_test.dart index bb9bf91663e51636813573e9ad592b514d9be2d5..ebbfd4720b55b677739ff29966fc27bb5a6a78d4 100644 GIT binary patch delta 69 zcmeBTzs0dZhM6xZKR-tyF()U#+$Fy}Z}NU7Ep`wiCqHqr9Fra|Sh_ePzudJVGi|aS Mvmyta^PXuo07SMJjQ{`u delta 11 Scmcb`(Z#+&hI#TE<~#ry)&w&E diff --git a/mobile/openapi/test/create_assets_share_link_dto_test.dart b/mobile/openapi/test/create_assets_share_link_dto_test.dart index 813e6bced8434a24ab801bca096307465561b5de..612d60ad910ce1ffa4561b115ecc1f686edad1cc 100644 GIT binary patch delta 79 zcmZ3_{-0xmBePIaetwQZVopwexl4X|UQT{u%H&2SMNYWLWC3PFUa+F#jQn!fip(^S PJO`Xptin$a3bf6pO delta 11 Scmey*v7UW{BlBcImOKC(2?NLg diff --git a/mobile/openapi/test/edit_shared_link_dto_test.dart b/mobile/openapi/test/edit_shared_link_dto_test.dart index b7815e0ed068e34a6c68537f6b2dd70b4d1ed1a4..7379e8b0826c6d2c01d501ee2992408b1a266647 100644 GIT binary patch delta 60 zcmeBRzrnFVoS8E*Cnvw$CBHmx@>V7-7BJ%>lNm>GMt-?#MP}OMmyC)CP7RaMWO3#~ E0FS>FUjP6A delta 11 Scmcb?(ZRk!oO$wd=0X4&gajx6 diff --git a/mobile/openapi/test/shared_link_response_dto_test.dart b/mobile/openapi/test/shared_link_response_dto_test.dart index de19ef71b88cd80093e2d08579582635ba2e0d9e..c89ae970fcac0d2ded772606e52a50b4bb78051a 100644 GIT binary patch delta 79 zcmeyyeT;X*8`jDFEbNoJSh?6;^2_sb@)IWqFe&jQ=H%p;!vyS^^>~x=^K%r6GxEz_ WD>Bn22Qn#gz&Q@gYLnlvasdEz*c-n9 delta 11 ScmX@c`;B|U8`jBt*th^86a>`( diff --git a/server/apps/immich/src/api-v1/album/album.controller.ts b/server/apps/immich/src/api-v1/album/album.controller.ts index a10a992979..c7623e082c 100644 --- a/server/apps/immich/src/api-v1/album/album.controller.ts +++ b/server/apps/immich/src/api-v1/album/album.controller.ts @@ -140,6 +140,8 @@ export class AlbumController { @Query(new ValidationPipe({ transform: true })) dto: DownloadDto, @Response({ passthrough: true }) res: Res, ): Promise { + this.albumService.checkDownloadAccess(authUser); + const { stream, fileName, fileSize, fileCount, complete } = await this.albumService.downloadArchive( authUser, albumId, diff --git a/server/apps/immich/src/api-v1/album/album.service.ts b/server/apps/immich/src/api-v1/album/album.service.ts index 7577abbba5..0da631d997 100644 --- a/server/apps/immich/src/api-v1/album/album.service.ts +++ b/server/apps/immich/src/api-v1/album/album.service.ts @@ -15,7 +15,7 @@ import { DownloadService } from '../../modules/download/download.service'; import { DownloadDto } from '../asset/dto/download-library.dto'; import { ShareCore } from '../share/share.core'; import { ISharedLinkRepository } from '../share/shared-link.repository'; -import { mapSharedLinkToResponseDto, SharedLinkResponseDto } from '../share/response-dto/shared-link-response.dto'; +import { mapSharedLink, SharedLinkResponseDto } from '../share/response-dto/shared-link-response.dto'; import { CreateAlbumShareLinkDto } from './dto/create-album-shared-link.dto'; import _ from 'lodash'; @@ -210,8 +210,14 @@ export class AlbumService { album: album, assets: [], description: dto.description, + allowDownload: dto.allowDownload, + showExif: dto.showExif, }); - return mapSharedLinkToResponseDto(sharedLink); + return mapSharedLink(sharedLink); + } + + checkDownloadAccess(authUser: AuthUserDto) { + this.shareCore.checkDownloadAccess(authUser); } } diff --git a/server/apps/immich/src/api-v1/album/dto/create-album-shared-link.dto.ts b/server/apps/immich/src/api-v1/album/dto/create-album-shared-link.dto.ts index a0ab83c1c3..d34c6310de 100644 --- a/server/apps/immich/src/api-v1/album/dto/create-album-shared-link.dto.ts +++ b/server/apps/immich/src/api-v1/album/dto/create-album-shared-link.dto.ts @@ -13,6 +13,14 @@ export class CreateAlbumShareLinkDto { @IsOptional() allowUpload?: boolean; + @IsBoolean() + @IsOptional() + allowDownload?: boolean; + + @IsBoolean() + @IsOptional() + showExif?: boolean; + @IsString() @IsOptional() description?: string; diff --git a/server/apps/immich/src/api-v1/asset/asset.controller.ts b/server/apps/immich/src/api-v1/asset/asset.controller.ts index 223fac9e78..14c9cac5c7 100644 --- a/server/apps/immich/src/api-v1/asset/asset.controller.ts +++ b/server/apps/immich/src/api-v1/asset/asset.controller.ts @@ -97,6 +97,7 @@ export class AssetController { @Query(new ValidationPipe({ transform: true })) query: ServeFileDto, @Param('assetId') assetId: string, ): Promise { + this.assetService.checkDownloadAccess(authUser); await this.assetService.checkAssetsAccess(authUser, [assetId]); return this.assetService.downloadFile(query, assetId, res); } @@ -108,6 +109,7 @@ export class AssetController { @Response({ passthrough: true }) res: Res, @Body(new ValidationPipe()) dto: DownloadFilesDto, ): Promise { + this.assetService.checkDownloadAccess(authUser); await this.assetService.checkAssetsAccess(authUser, [...dto.assetIds]); const { stream, fileName, fileSize, fileCount, complete } = await this.assetService.downloadFiles(dto); res.attachment(fileName); @@ -117,6 +119,9 @@ export class AssetController { return stream; } + /** + * Current this is not used in any UI element + */ @Authenticated({ isShared: true }) @Get('/download-library') async downloadLibrary( @@ -124,6 +129,7 @@ export class AssetController { @Query(new ValidationPipe({ transform: true })) dto: DownloadDto, @Response({ passthrough: true }) res: Res, ): Promise { + this.assetService.checkDownloadAccess(authUser); const { stream, fileName, fileSize, fileCount, complete } = await this.assetService.downloadLibrary(authUser, dto); res.attachment(fileName); res.setHeader(IMMICH_CONTENT_LENGTH_HINT, fileSize); @@ -143,7 +149,7 @@ export class AssetController { @Param('assetId') assetId: string, ): Promise { await this.assetService.checkAssetsAccess(authUser, [assetId]); - return this.assetService.serveFile(assetId, query, res, headers); + return this.assetService.serveFile(authUser, assetId, query, res, headers); } @Authenticated({ isShared: true }) @@ -246,7 +252,7 @@ export class AssetController { @Param('assetId') assetId: string, ): Promise { await this.assetService.checkAssetsAccess(authUser, [assetId]); - return await this.assetService.getAssetById(assetId); + return await this.assetService.getAssetById(authUser, assetId); } /** @@ -274,14 +280,14 @@ export class AssetController { const deleteAssetList: AssetResponseDto[] = []; for (const id of assetIds.ids) { - const assets = await this.assetService.getAssetById(id); + const assets = await this.assetService.getAssetById(authUser, id); if (!assets) { continue; } deleteAssetList.push(assets); if (assets.livePhotoVideoId) { - const livePhotoVideo = await this.assetService.getAssetById(assets.livePhotoVideoId); + const livePhotoVideo = await this.assetService.getAssetById(authUser, assets.livePhotoVideoId); if (livePhotoVideo) { deleteAssetList.push(livePhotoVideo); assetIds.ids = [...assetIds.ids, livePhotoVideo.id]; diff --git a/server/apps/immich/src/api-v1/asset/asset.service.ts b/server/apps/immich/src/api-v1/asset/asset.service.ts index 0f6438f0b0..ce2d22d44d 100644 --- a/server/apps/immich/src/api-v1/asset/asset.service.ts +++ b/server/apps/immich/src/api-v1/asset/asset.service.ts @@ -23,7 +23,7 @@ import { SearchAssetDto } from './dto/search-asset.dto'; import fs from 'fs/promises'; import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto'; import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto'; -import { AssetResponseDto, mapAsset } from './response-dto/asset-response.dto'; +import { AssetResponseDto, mapAsset, mapAssetWithoutExif } from './response-dto/asset-response.dto'; import { CreateAssetDto } from './dto/create-asset.dto'; import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto'; import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto'; @@ -52,7 +52,7 @@ import { ShareCore } from '../share/share.core'; import { ISharedLinkRepository } from '../share/shared-link.repository'; import { DownloadFilesDto } from './dto/download-files.dto'; import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto'; -import { mapSharedLinkToResponseDto, SharedLinkResponseDto } from '../share/response-dto/shared-link-response.dto'; +import { mapSharedLink, SharedLinkResponseDto } from '../share/response-dto/shared-link-response.dto'; import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto'; const fileInfo = promisify(stat); @@ -215,10 +215,15 @@ export class AssetService { return assets.map((asset) => mapAsset(asset)); } - public async getAssetById(assetId: string): Promise { + public async getAssetById(authUser: AuthUserDto, assetId: string): Promise { + const allowExif = this.getExifPermission(authUser); const asset = await this._assetRepository.getById(assetId); - return mapAsset(asset); + if (allowExif) { + return mapAsset(asset); + } else { + return mapAssetWithoutExif(asset); + } } public async updateAsset(authUser: AuthUserDto, assetId: string, dto: UpdateAssetDto): Promise { @@ -356,7 +361,15 @@ export class AssetService { } } - public async serveFile(assetId: string, query: ServeFileDto, res: Res, headers: Record) { + public async serveFile( + authUser: AuthUserDto, + assetId: string, + query: ServeFileDto, + res: Res, + headers: Record, + ) { + const allowOriginalFile = !authUser.isPublicUser || authUser.isAllowDownload; + let fileReadStream: ReadStream; const asset = await this._assetRepository.getById(assetId); @@ -390,7 +403,7 @@ export class AssetService { /** * Serve thumbnail image for both web and mobile app */ - if (!query.isThumb) { + if (!query.isThumb && allowOriginalFile) { res.set({ 'Content-Type': asset.mimeType, }); @@ -676,6 +689,10 @@ export class AssetService { } } + checkDownloadAccess(authUser: AuthUserDto) { + this.shareCore.checkDownloadAccess(authUser); + } + async createAssetsSharedLink(authUser: AuthUserDto, dto: CreateAssetsShareLinkDto): Promise { const assets = []; @@ -691,9 +708,11 @@ export class AssetService { allowUpload: dto.allowUpload, assets: assets, description: dto.description, + allowDownload: dto.allowDownload, + showExif: dto.showExif, }); - return mapSharedLinkToResponseDto(sharedLink); + return mapSharedLink(sharedLink); } async updateAssetsInSharedLink( @@ -709,7 +728,11 @@ export class AssetService { } const updatedLink = await this.shareCore.updateAssetsInSharedLink(authUser.sharedLinkId, assets); - return mapSharedLinkToResponseDto(updatedLink); + return mapSharedLink(updatedLink); + } + + getExifPermission(authUser: AuthUserDto) { + return !authUser.isPublicUser || authUser.isShowExif; } } diff --git a/server/apps/immich/src/api-v1/asset/dto/create-asset-shared-link.dto.ts b/server/apps/immich/src/api-v1/asset/dto/create-asset-shared-link.dto.ts index 7a2f72be97..407aaa6285 100644 --- a/server/apps/immich/src/api-v1/asset/dto/create-asset-shared-link.dto.ts +++ b/server/apps/immich/src/api-v1/asset/dto/create-asset-shared-link.dto.ts @@ -25,6 +25,14 @@ export class CreateAssetsShareLinkDto { @IsOptional() allowUpload?: boolean; + @IsBoolean() + @IsOptional() + allowDownload?: boolean; + + @IsBoolean() + @IsOptional() + showExif?: boolean; + @IsString() @IsOptional() description?: string; diff --git a/server/apps/immich/src/api-v1/asset/response-dto/asset-response.dto.ts b/server/apps/immich/src/api-v1/asset/response-dto/asset-response.dto.ts index 87014deb8b..9a1f09e625 100644 --- a/server/apps/immich/src/api-v1/asset/response-dto/asset-response.dto.ts +++ b/server/apps/immich/src/api-v1/asset/response-dto/asset-response.dto.ts @@ -49,3 +49,26 @@ export function mapAsset(entity: AssetEntity): AssetResponseDto { tags: entity.tags?.map(mapTag), }; } + +export function mapAssetWithoutExif(entity: AssetEntity): AssetResponseDto { + return { + id: entity.id, + deviceAssetId: entity.deviceAssetId, + ownerId: entity.userId, + deviceId: entity.deviceId, + type: entity.type, + originalPath: entity.originalPath, + resizePath: entity.resizePath, + createdAt: entity.createdAt, + modifiedAt: entity.modifiedAt, + isFavorite: entity.isFavorite, + mimeType: entity.mimeType, + webpPath: entity.webpPath, + encodedVideoPath: entity.encodedVideoPath, + duration: entity.duration ?? '0:00:00.00000', + exifInfo: undefined, + smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined, + livePhotoVideoId: entity.livePhotoVideoId, + tags: entity.tags?.map(mapTag), + }; +} diff --git a/server/apps/immich/src/api-v1/share/dto/create-shared-link.dto.ts b/server/apps/immich/src/api-v1/share/dto/create-shared-link.dto.ts index 0d9e420c10..b5ef2ca138 100644 --- a/server/apps/immich/src/api-v1/share/dto/create-shared-link.dto.ts +++ b/server/apps/immich/src/api-v1/share/dto/create-shared-link.dto.ts @@ -8,4 +8,6 @@ export class CreateSharedLinkDto { assets!: AssetEntity[]; album?: AlbumEntity; allowUpload?: boolean; + allowDownload?: boolean; + showExif?: boolean; } diff --git a/server/apps/immich/src/api-v1/share/dto/edit-shared-link.dto.ts b/server/apps/immich/src/api-v1/share/dto/edit-shared-link.dto.ts index fb9a794958..e787e3c5bb 100644 --- a/server/apps/immich/src/api-v1/share/dto/edit-shared-link.dto.ts +++ b/server/apps/immich/src/api-v1/share/dto/edit-shared-link.dto.ts @@ -10,6 +10,12 @@ export class EditSharedLinkDto { @IsOptional() allowUpload?: boolean; + @IsOptional() + allowDownload?: boolean; + + @IsOptional() + showExif?: boolean; + @IsNotEmpty() isEditExpireTime?: boolean; } diff --git a/server/apps/immich/src/api-v1/share/response-dto/shared-link-response.dto.ts b/server/apps/immich/src/api-v1/share/response-dto/shared-link-response.dto.ts index f4cdd8e0b5..78b13c1281 100644 --- a/server/apps/immich/src/api-v1/share/response-dto/shared-link-response.dto.ts +++ b/server/apps/immich/src/api-v1/share/response-dto/shared-link-response.dto.ts @@ -2,7 +2,7 @@ import { SharedLinkEntity, SharedLinkType } from '@app/infra'; import { ApiProperty } from '@nestjs/swagger'; import _ from 'lodash'; import { AlbumResponseDto, mapAlbumExcludeAssetInfo } from '../../album/response-dto/album-response.dto'; -import { AssetResponseDto, mapAsset } from '../../asset/response-dto/asset-response.dto'; +import { AssetResponseDto, mapAsset, mapAssetWithoutExif } from '../../asset/response-dto/asset-response.dto'; export class SharedLinkResponseDto { id!: string; @@ -17,9 +17,11 @@ export class SharedLinkResponseDto { assets!: AssetResponseDto[]; album?: AlbumResponseDto; allowUpload!: boolean; + allowDownload!: boolean; + showExif!: boolean; } -export function mapSharedLinkToResponseDto(sharedLink: SharedLinkEntity): SharedLinkResponseDto { +export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseDto { const linkAssets = sharedLink.assets || []; const albumAssets = (sharedLink?.album?.assets || []).map((albumAsset) => albumAsset.assetInfo); @@ -36,5 +38,29 @@ export function mapSharedLinkToResponseDto(sharedLink: SharedLinkEntity): Shared assets: assets.map(mapAsset), album: sharedLink.album ? mapAlbumExcludeAssetInfo(sharedLink.album) : undefined, allowUpload: sharedLink.allowUpload, + allowDownload: sharedLink.allowDownload, + showExif: sharedLink.showExif, + }; +} + +export function mapSharedLinkWithNoExif(sharedLink: SharedLinkEntity): SharedLinkResponseDto { + const linkAssets = sharedLink.assets || []; + const albumAssets = (sharedLink?.album?.assets || []).map((albumAsset) => albumAsset.assetInfo); + + const assets = _.uniqBy([...linkAssets, ...albumAssets], (asset) => asset.id); + + return { + id: sharedLink.id, + description: sharedLink.description, + userId: sharedLink.userId, + key: sharedLink.key.toString('hex'), + type: sharedLink.type, + createdAt: sharedLink.createdAt, + expiresAt: sharedLink.expiresAt, + assets: assets.map(mapAssetWithoutExif), + album: sharedLink.album ? mapAlbumExcludeAssetInfo(sharedLink.album) : undefined, + allowUpload: sharedLink.allowUpload, + allowDownload: sharedLink.allowDownload, + showExif: sharedLink.showExif, }; } diff --git a/server/apps/immich/src/api-v1/share/share.controller.ts b/server/apps/immich/src/api-v1/share/share.controller.ts index 705116cd13..013c1dd92c 100644 --- a/server/apps/immich/src/api-v1/share/share.controller.ts +++ b/server/apps/immich/src/api-v1/share/share.controller.ts @@ -25,7 +25,7 @@ export class ShareController { @Authenticated() @Get(':id') getSharedLinkById(@Param('id') id: string): Promise { - return this.shareService.getById(id); + return this.shareService.getById(id, true); } @Authenticated() diff --git a/server/apps/immich/src/api-v1/share/share.core.ts b/server/apps/immich/src/api-v1/share/share.core.ts index f5c1f6f182..87797fa2f3 100644 --- a/server/apps/immich/src/api-v1/share/share.core.ts +++ b/server/apps/immich/src/api-v1/share/share.core.ts @@ -2,9 +2,10 @@ import { SharedLinkEntity } from '@app/infra'; import { CreateSharedLinkDto } from './dto/create-shared-link.dto'; import { ISharedLinkRepository } from './shared-link.repository'; import crypto from 'node:crypto'; -import { BadRequestException, InternalServerErrorException, Logger } from '@nestjs/common'; +import { BadRequestException, ForbiddenException, InternalServerErrorException, Logger } from '@nestjs/common'; import { AssetEntity } from '@app/infra'; import { EditSharedLinkDto } from './dto/edit-shared-link.dto'; +import { AuthUserDto } from '../../decorators/auth-user.decorator'; export class ShareCore { readonly logger = new Logger(ShareCore.name); @@ -24,6 +25,8 @@ export class ShareCore { sharedLink.assets = dto.assets; sharedLink.album = dto.album; sharedLink.allowUpload = dto.allowUpload ?? false; + sharedLink.allowDownload = dto.allowDownload ?? true; + sharedLink.showExif = dto.showExif ?? true; return this.sharedLinkRepository.create(sharedLink); } catch (error: any) { @@ -74,6 +77,8 @@ export class ShareCore { link.description = dto.description ?? link.description; link.allowUpload = dto.allowUpload ?? link.allowUpload; + link.allowDownload = dto.allowDownload ?? link.allowDownload; + link.showExif = dto.showExif ?? link.showExif; if (dto.isEditExpireTime && dto.expiredAt) { link.expiresAt = dto.expiredAt; @@ -87,4 +92,10 @@ export class ShareCore { async hasAssetAccess(id: string, assetId: string): Promise { return this.sharedLinkRepository.hasAssetAccess(id, assetId); } + + checkDownloadAccess(user: AuthUserDto) { + if (user.isPublicUser && !user.isAllowDownload) { + throw new ForbiddenException(); + } + } } diff --git a/server/apps/immich/src/api-v1/share/share.service.ts b/server/apps/immich/src/api-v1/share/share.service.ts index 2b35d5c061..2b33f8ed33 100644 --- a/server/apps/immich/src/api-v1/share/share.service.ts +++ b/server/apps/immich/src/api-v1/share/share.service.ts @@ -9,7 +9,7 @@ import { import { UserService } from '@app/domain'; import { AuthUserDto } from '../../decorators/auth-user.decorator'; import { EditSharedLinkDto } from './dto/edit-shared-link.dto'; -import { mapSharedLinkToResponseDto, SharedLinkResponseDto } from './response-dto/shared-link-response.dto'; +import { mapSharedLink, mapSharedLinkWithNoExif, SharedLinkResponseDto } from './response-dto/shared-link-response.dto'; import { ShareCore } from './share.core'; import { ISharedLinkRepository } from './shared-link.repository'; @@ -39,6 +39,8 @@ export class ShareService { isPublicUser: true, sharedLinkId: link.id, isAllowUpload: link.allowUpload, + isAllowDownload: link.allowDownload, + isShowExif: link.showExif, }; } } @@ -48,7 +50,7 @@ export class ShareService { async getAll(authUser: AuthUserDto): Promise { const links = await this.shareCore.getSharedLinks(authUser.id); - return links.map(mapSharedLinkToResponseDto); + return links.map(mapSharedLink); } async getMine(authUser: AuthUserDto): Promise { @@ -56,15 +58,25 @@ export class ShareService { throw new ForbiddenException(); } - return this.getById(authUser.sharedLinkId); + let allowExif = true; + if (authUser.isShowExif != undefined) { + allowExif = authUser.isShowExif; + } + + return this.getById(authUser.sharedLinkId, allowExif); } - async getById(id: string): Promise { + async getById(id: string, allowExif: boolean): Promise { const link = await this.shareCore.getSharedLinkById(id); if (!link) { throw new BadRequestException('Shared link not found'); } - return mapSharedLinkToResponseDto(link); + + if (allowExif) { + return mapSharedLink(link); + } else { + return mapSharedLinkWithNoExif(link); + } } async remove(id: string, userId: string): Promise { @@ -77,11 +89,11 @@ export class ShareService { if (!link) { throw new BadRequestException('Shared link not found'); } - return mapSharedLinkToResponseDto(link); + return mapSharedLink(link); } async edit(id: string, authUser: AuthUserDto, dto: EditSharedLinkDto) { const link = await this.shareCore.updateSharedLink(id, authUser.id, dto); - return mapSharedLinkToResponseDto(link); + return mapSharedLink(link); } } diff --git a/server/apps/microservices/src/processors/metadata-extraction.processor.ts b/server/apps/microservices/src/processors/metadata-extraction.processor.ts index d31069df0f..b409e6af07 100644 --- a/server/apps/microservices/src/processors/metadata-extraction.processor.ts +++ b/server/apps/microservices/src/processors/metadata-extraction.processor.ts @@ -139,7 +139,6 @@ export class MetadataExtractionProcessor { async extractExifInfo(job: Job) { try { const { asset, fileName }: { asset: AssetEntity; fileName: string } = job.data; - const exifData = await exiftool.read(asset.originalPath).catch((e) => { this.logger.warn(`The exifData parsing failed due to: ${e} on file ${asset.originalPath}`); return null; diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index a1b42eb5ec..c2df710b75 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -736,7 +736,7 @@ "/asset/download-library": { "get": { "operationId": "downloadLibrary", - "description": "", + "description": "Current this is not used in any UI element", "parameters": [ { "name": "skip", @@ -3786,6 +3786,12 @@ "allowUpload": { "type": "boolean" }, + "allowDownload": { + "type": "boolean" + }, + "showExif": { + "type": "boolean" + }, "description": { "type": "string" } @@ -3887,6 +3893,12 @@ }, "allowUpload": { "type": "boolean" + }, + "allowDownload": { + "type": "boolean" + }, + "showExif": { + "type": "boolean" } }, "required": [ @@ -3897,7 +3909,9 @@ "createdAt", "expiresAt", "assets", - "allowUpload" + "allowUpload", + "allowDownload", + "showExif" ] }, "UpdateAssetsToSharedLinkDto": { @@ -3926,6 +3940,12 @@ "allowUpload": { "type": "boolean" }, + "allowDownload": { + "type": "boolean" + }, + "showExif": { + "type": "boolean" + }, "isEditExpireTime": { "type": "boolean" } @@ -4085,6 +4105,12 @@ "allowUpload": { "type": "boolean" }, + "allowDownload": { + "type": "boolean" + }, + "showExif": { + "type": "boolean" + }, "description": { "type": "string" } diff --git a/server/libs/domain/src/auth/dto/auth-user.dto.ts b/server/libs/domain/src/auth/dto/auth-user.dto.ts index a135924a06..25d1cae1d1 100644 --- a/server/libs/domain/src/auth/dto/auth-user.dto.ts +++ b/server/libs/domain/src/auth/dto/auth-user.dto.ts @@ -5,4 +5,6 @@ export class AuthUserDto { isPublicUser?: boolean; sharedLinkId?: string; isAllowUpload?: boolean; + isAllowDownload?: boolean; + isShowExif?: boolean; } diff --git a/server/libs/infra/src/db/entities/shared-link.entity.ts b/server/libs/infra/src/db/entities/shared-link.entity.ts index f096e361ea..27030758a2 100644 --- a/server/libs/infra/src/db/entities/shared-link.entity.ts +++ b/server/libs/infra/src/db/entities/shared-link.entity.ts @@ -30,6 +30,12 @@ export class SharedLinkEntity { @Column({ type: 'boolean', default: false }) allowUpload!: boolean; + @Column({ type: 'boolean', default: true }) + allowDownload!: boolean; + + @Column({ type: 'boolean', default: true }) + showExif!: boolean; + @ManyToMany(() => AssetEntity, (asset) => asset.sharedLinks) assets!: AssetEntity[]; @@ -47,4 +53,4 @@ export enum SharedLinkType { INDIVIDUAL = 'INDIVIDUAL', } -// npm run typeorm -- migration:generate ./libs/database/src/AddSharedLinkTable -d libs/database/src/config/database.config.ts +// npm run typeorm -- migration:generate ./libs/infra/src/db/AddMorePermissionToSharedLink -d ./libs/infra/src/db/config/database.config.ts diff --git a/server/libs/infra/src/db/migrations/1673907194740-AddMorePermissionToSharedLink.ts b/server/libs/infra/src/db/migrations/1673907194740-AddMorePermissionToSharedLink.ts new file mode 100644 index 0000000000..af0a0280a8 --- /dev/null +++ b/server/libs/infra/src/db/migrations/1673907194740-AddMorePermissionToSharedLink.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddMorePermissionToSharedLink1673907194740 implements MigrationInterface { + name = 'AddMorePermissionToSharedLink1673907194740'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "shared_links" ADD "allowDownload" boolean NOT NULL DEFAULT true`); + await queryRunner.query(`ALTER TABLE "shared_links" ADD "showExif" boolean NOT NULL DEFAULT true`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "shared_links" DROP COLUMN "showExif"`); + await queryRunner.query(`ALTER TABLE "shared_links" DROP COLUMN "allowDownload"`); + } +} diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index a2e80b807e..fbf72b2465 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -665,6 +665,18 @@ export interface CreateAlbumShareLinkDto { * @memberof CreateAlbumShareLinkDto */ 'allowUpload'?: boolean; + /** + * + * @type {boolean} + * @memberof CreateAlbumShareLinkDto + */ + 'allowDownload'?: boolean; + /** + * + * @type {boolean} + * @memberof CreateAlbumShareLinkDto + */ + 'showExif'?: boolean; /** * * @type {string} @@ -696,6 +708,18 @@ export interface CreateAssetsShareLinkDto { * @memberof CreateAssetsShareLinkDto */ 'allowUpload'?: boolean; + /** + * + * @type {boolean} + * @memberof CreateAssetsShareLinkDto + */ + 'allowDownload'?: boolean; + /** + * + * @type {boolean} + * @memberof CreateAssetsShareLinkDto + */ + 'showExif'?: boolean; /** * * @type {string} @@ -987,6 +1011,18 @@ export interface EditSharedLinkDto { * @memberof EditSharedLinkDto */ 'allowUpload'?: boolean; + /** + * + * @type {boolean} + * @memberof EditSharedLinkDto + */ + 'allowDownload'?: boolean; + /** + * + * @type {boolean} + * @memberof EditSharedLinkDto + */ + 'showExif'?: boolean; /** * * @type {boolean} @@ -1612,6 +1648,18 @@ export interface SharedLinkResponseDto { * @memberof SharedLinkResponseDto */ 'allowUpload': boolean; + /** + * + * @type {boolean} + * @memberof SharedLinkResponseDto + */ + 'allowDownload': boolean; + /** + * + * @type {boolean} + * @memberof SharedLinkResponseDto + */ + 'showExif': boolean; } /** * diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte index 566b5627f8..63e51b8d3c 100644 --- a/web/src/lib/components/album-page/album-viewer.svelte +++ b/web/src/lib/components/album-page/album-viewer.svelte @@ -320,6 +320,7 @@ } } } catch (e) { + $downloadAssets = {}; console.error('Error downloading file ', e); notificationController.show({ type: NotificationType.Error, @@ -460,11 +461,13 @@ {/if} - downloadAlbum()} - logo={FolderDownloadOutline} - /> + {#if !isPublicShared || (isPublicShared && sharedLink?.allowDownload)} + downloadAlbum()} + logo={FolderDownloadOutline} + /> + {/if} {#if !isPublicShared} 0} - + {:else}
diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index c4efa91bf9..0edf2339fa 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -22,6 +22,7 @@ export let showCopyButton: boolean; export let showMotionPlayButton: boolean; export let isMotionPhotoPlaying = false; + export let showDownloadButton: boolean; const isOwner = asset.ownerId === $page.data.user?.id; @@ -77,11 +78,14 @@ }} /> {/if} - dispatch('download')} - title="Download" - /> + + {#if showDownloadButton} + dispatch('download')} + title="Download" + /> + {/if} dispatch('showDetail')} diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 7439caac51..d11dff9d00 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -10,7 +10,13 @@ import { downloadAssets } from '$lib/stores/download'; import VideoViewer from './video-viewer.svelte'; import AlbumSelectionModal from '../shared-components/album-selection-modal.svelte'; - import { api, AssetResponseDto, AssetTypeEnum, AlbumResponseDto } from '@api'; + import { + api, + AssetResponseDto, + AssetTypeEnum, + AlbumResponseDto, + SharedLinkResponseDto + } from '@api'; import { notificationController, NotificationType @@ -22,6 +28,7 @@ export let asset: AssetResponseDto; export let publicSharedKey = ''; export let showNavigation = true; + export let sharedLink: SharedLinkResponseDto | undefined = undefined; const dispatch = createEventDispatcher(); let halfLeftHover = false; @@ -31,6 +38,7 @@ let isShowAlbumPicker = false; let addToSharedAlbum = true; let shouldPlayMotionPhoto = false; + let shouldShowDownloadButton = sharedLink ? sharedLink.allowDownload : true; const onKeyboardPress = (keyInfo: KeyboardEvent) => handleKeyboardPress(keyInfo.key); onMount(async () => { @@ -166,6 +174,7 @@ }, 2000); } } catch (e) { + $downloadAssets = {}; console.error('Error downloading file ', e); notificationController.show({ type: NotificationType.Error, @@ -247,6 +256,7 @@ isMotionPhotoPlaying={shouldPlayMotionPhoto} showCopyButton={asset.type === AssetTypeEnum.Image} showMotionPlayButton={!!asset.livePhotoVideoId} + showDownloadButton={shouldShowDownloadButton} on:goBack={closeViewer} on:showDetail={showDetailInfoHandler} on:download={handleDownload} diff --git a/web/src/lib/components/share-page/individual-shared-viewer.svelte b/web/src/lib/components/share-page/individual-shared-viewer.svelte index cfc64d0b13..6dd723e087 100644 --- a/web/src/lib/components/share-page/individual-shared-viewer.svelte +++ b/web/src/lib/components/share-page/individual-shared-viewer.svelte @@ -136,15 +136,17 @@ /> {/if} - downloadAssets(true)} - logo={FolderDownloadOutline} - /> + {#if sharedLink?.allowDownload} + downloadAssets(true)} + logo={FolderDownloadOutline} + /> + {/if} {/if}
- +
diff --git a/web/src/lib/components/shared-components/base-modal.svelte b/web/src/lib/components/shared-components/base-modal.svelte index c1bcae920b..848e5d1964 100644 --- a/web/src/lib/components/shared-components/base-modal.svelte +++ b/web/src/lib/components/shared-components/base-modal.svelte @@ -36,7 +36,7 @@
dispatch('close')} - class="bg-immich-bg dark:bg-immich-dark-gray dark:text-immich-dark-fg w-[450px] min-h-[200px] max-h-[500px] rounded-lg shadow-md" + class="bg-immich-bg dark:bg-immich-dark-gray dark:text-immich-dark-fg w-[450px] min-h-[200px] max-h-[600px] rounded-lg shadow-md" >
diff --git a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte index 5b3eabb6dc..b0caaf99f5 100644 --- a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte +++ b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte @@ -29,6 +29,8 @@ let sharedLink = ''; let description = ''; let shouldChangeExpirationTime = false; + let isAllowDownload = true; + let shouldShowExif = true; const dispatch = createEventDispatcher(); const expiredDateOption: ImmichDropDownOption = { @@ -42,6 +44,8 @@ description = editingLink.description; } isAllowUpload = editingLink.allowUpload; + isAllowDownload = editingLink.allowDownload; + shouldShowExif = editingLink.showExif; } }); @@ -58,7 +62,9 @@ albumId: album.id, expiredAt: expirationDate, allowUpload: isAllowUpload, - description: description + description: description, + allowDownload: isAllowDownload, + showExif: shouldShowExif }); buildSharedLink(data); } else { @@ -66,7 +72,9 @@ assetIds: sharedAssets.map((a) => a.id), expiredAt: expirationDate, allowUpload: isAllowUpload, - description: description + description: description, + allowDownload: isAllowDownload, + showExif: shouldShowExif }); buildSharedLink(data); } @@ -132,7 +140,9 @@ description: description, expiredAt: expirationDate, allowUpload: isAllowUpload, - isEditExpireTime: shouldChangeExpirationTime + isEditExpireTime: shouldChangeExpirationTime, + allowDownload: isAllowDownload, + showExif: shouldShowExif }); notificationController.show({ @@ -185,12 +195,12 @@ {/if} {/if} -
+

LINK OPTIONS

-
+
- +
+ +
-
+
+ +
+ +
+ +
+ +
{#if editingLink}

{selected} diff --git a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte index d0c8f385ed..67ed469196 100644 --- a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte +++ b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte @@ -1,13 +1,13 @@