mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	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
This commit is contained in:
		| @@ -140,6 +140,8 @@ export class AlbumController { | ||||
|     @Query(new ValidationPipe({ transform: true })) dto: DownloadDto, | ||||
|     @Response({ passthrough: true }) res: Res, | ||||
|   ): Promise<any> { | ||||
|     this.albumService.checkDownloadAccess(authUser); | ||||
|  | ||||
|     const { stream, fileName, fileSize, fileCount, complete } = await this.albumService.downloadArchive( | ||||
|       authUser, | ||||
|       albumId, | ||||
|   | ||||
| @@ -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); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -13,6 +13,14 @@ export class CreateAlbumShareLinkDto { | ||||
|   @IsOptional() | ||||
|   allowUpload?: boolean; | ||||
|  | ||||
|   @IsBoolean() | ||||
|   @IsOptional() | ||||
|   allowDownload?: boolean; | ||||
|  | ||||
|   @IsBoolean() | ||||
|   @IsOptional() | ||||
|   showExif?: boolean; | ||||
|  | ||||
|   @IsString() | ||||
|   @IsOptional() | ||||
|   description?: string; | ||||
|   | ||||
| @@ -97,6 +97,7 @@ export class AssetController { | ||||
|     @Query(new ValidationPipe({ transform: true })) query: ServeFileDto, | ||||
|     @Param('assetId') assetId: string, | ||||
|   ): Promise<any> { | ||||
|     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<any> { | ||||
|     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<any> { | ||||
|     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<any> { | ||||
|     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<AssetResponseDto> { | ||||
|     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]; | ||||
|   | ||||
| @@ -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<AssetResponseDto> { | ||||
|   public async getAssetById(authUser: AuthUserDto, assetId: string): Promise<AssetResponseDto> { | ||||
|     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<AssetResponseDto> { | ||||
| @@ -356,7 +361,15 @@ export class AssetService { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public async serveFile(assetId: string, query: ServeFileDto, res: Res, headers: Record<string, string>) { | ||||
|   public async serveFile( | ||||
|     authUser: AuthUserDto, | ||||
|     assetId: string, | ||||
|     query: ServeFileDto, | ||||
|     res: Res, | ||||
|     headers: Record<string, string>, | ||||
|   ) { | ||||
|     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<SharedLinkResponseDto> { | ||||
|     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; | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -25,6 +25,14 @@ export class CreateAssetsShareLinkDto { | ||||
|   @IsOptional() | ||||
|   allowUpload?: boolean; | ||||
|  | ||||
|   @IsBoolean() | ||||
|   @IsOptional() | ||||
|   allowDownload?: boolean; | ||||
|  | ||||
|   @IsBoolean() | ||||
|   @IsOptional() | ||||
|   showExif?: boolean; | ||||
|  | ||||
|   @IsString() | ||||
|   @IsOptional() | ||||
|   description?: string; | ||||
|   | ||||
| @@ -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), | ||||
|   }; | ||||
| } | ||||
|   | ||||
| @@ -8,4 +8,6 @@ export class CreateSharedLinkDto { | ||||
|   assets!: AssetEntity[]; | ||||
|   album?: AlbumEntity; | ||||
|   allowUpload?: boolean; | ||||
|   allowDownload?: boolean; | ||||
|   showExif?: boolean; | ||||
| } | ||||
|   | ||||
| @@ -10,6 +10,12 @@ export class EditSharedLinkDto { | ||||
|   @IsOptional() | ||||
|   allowUpload?: boolean; | ||||
|  | ||||
|   @IsOptional() | ||||
|   allowDownload?: boolean; | ||||
|  | ||||
|   @IsOptional() | ||||
|   showExif?: boolean; | ||||
|  | ||||
|   @IsNotEmpty() | ||||
|   isEditExpireTime?: boolean; | ||||
| } | ||||
|   | ||||
| @@ -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, | ||||
|   }; | ||||
| } | ||||
|   | ||||
| @@ -25,7 +25,7 @@ export class ShareController { | ||||
|   @Authenticated() | ||||
|   @Get(':id') | ||||
|   getSharedLinkById(@Param('id') id: string): Promise<SharedLinkResponseDto> { | ||||
|     return this.shareService.getById(id); | ||||
|     return this.shareService.getById(id, true); | ||||
|   } | ||||
|  | ||||
|   @Authenticated() | ||||
|   | ||||
| @@ -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<boolean> { | ||||
|     return this.sharedLinkRepository.hasAssetAccess(id, assetId); | ||||
|   } | ||||
|  | ||||
|   checkDownloadAccess(user: AuthUserDto) { | ||||
|     if (user.isPublicUser && !user.isAllowDownload) { | ||||
|       throw new ForbiddenException(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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<SharedLinkResponseDto[]> { | ||||
|     const links = await this.shareCore.getSharedLinks(authUser.id); | ||||
|     return links.map(mapSharedLinkToResponseDto); | ||||
|     return links.map(mapSharedLink); | ||||
|   } | ||||
|  | ||||
|   async getMine(authUser: AuthUserDto): Promise<SharedLinkResponseDto> { | ||||
| @@ -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<SharedLinkResponseDto> { | ||||
|   async getById(id: string, allowExif: boolean): Promise<SharedLinkResponseDto> { | ||||
|     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<string> { | ||||
| @@ -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); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -139,7 +139,6 @@ export class MetadataExtractionProcessor { | ||||
|   async extractExifInfo(job: Job<IExifExtractionProcessor>) { | ||||
|     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; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user