asset.controller.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. import {
  2. Controller,
  3. Post,
  4. UseInterceptors,
  5. Body,
  6. Get,
  7. Param,
  8. ValidationPipe,
  9. Query,
  10. Response,
  11. Headers,
  12. Delete,
  13. HttpCode,
  14. Header,
  15. Put,
  16. UploadedFiles,
  17. Patch,
  18. } from '@nestjs/common';
  19. import { Authenticated } from '../../decorators/authenticated.decorator';
  20. import { AssetService } from './asset.service';
  21. import { FileFieldsInterceptor } from '@nestjs/platform-express';
  22. import { assetUploadOption } from '../../config/asset-upload.config';
  23. import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
  24. import { ServeFileDto } from './dto/serve-file.dto';
  25. import { Response as Res } from 'express';
  26. import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
  27. import { DeleteAssetDto } from './dto/delete-asset.dto';
  28. import { SearchAssetDto } from './dto/search-asset.dto';
  29. import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
  30. import { ApiBearerAuth, ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger';
  31. import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
  32. import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
  33. import { AssetResponseDto } from './response-dto/asset-response.dto';
  34. import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
  35. import { AssetFileUploadDto } from './dto/asset-file-upload.dto';
  36. import { CreateAssetDto } from './dto/create-asset.dto';
  37. import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
  38. import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
  39. import { GetAssetThumbnailDto } from './dto/get-asset-thumbnail.dto';
  40. import { AssetCountByTimeBucketResponseDto } from './response-dto/asset-count-by-time-group-response.dto';
  41. import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-bucket.dto';
  42. import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
  43. import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
  44. import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
  45. import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto';
  46. import { UpdateAssetDto } from './dto/update-asset.dto';
  47. import { DownloadDto } from './dto/download-library.dto';
  48. import {
  49. IMMICH_ARCHIVE_COMPLETE,
  50. IMMICH_ARCHIVE_FILE_COUNT,
  51. IMMICH_CONTENT_LENGTH_HINT,
  52. } from '../../constants/download.constant';
  53. import { DownloadFilesDto } from './dto/download-files.dto';
  54. import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
  55. import { SharedLinkResponseDto } from '../share/response-dto/shared-link-response.dto';
  56. import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
  57. @ApiBearerAuth()
  58. @ApiTags('Asset')
  59. @Controller('asset')
  60. export class AssetController {
  61. constructor(private assetService: AssetService, private backgroundTaskService: BackgroundTaskService) {}
  62. @Authenticated({ isShared: true })
  63. @Post('upload')
  64. @UseInterceptors(
  65. FileFieldsInterceptor(
  66. [
  67. { name: 'assetData', maxCount: 1 },
  68. { name: 'livePhotoData', maxCount: 1 },
  69. ],
  70. assetUploadOption,
  71. ),
  72. )
  73. @ApiConsumes('multipart/form-data')
  74. @ApiBody({
  75. description: 'Asset Upload Information',
  76. type: AssetFileUploadDto,
  77. })
  78. async uploadFile(
  79. @GetAuthUser() authUser: AuthUserDto,
  80. @UploadedFiles() files: { assetData: Express.Multer.File[]; livePhotoData?: Express.Multer.File[] },
  81. @Body(ValidationPipe) createAssetDto: CreateAssetDto,
  82. @Response({ passthrough: true }) res: Res,
  83. ): Promise<AssetFileUploadResponseDto> {
  84. const originalAssetData = files.assetData[0];
  85. const livePhotoAssetData = files.livePhotoData?.[0];
  86. return this.assetService.handleUploadedAsset(authUser, createAssetDto, res, originalAssetData, livePhotoAssetData);
  87. }
  88. @Authenticated({ isShared: true })
  89. @Get('/download/:assetId')
  90. async downloadFile(
  91. @GetAuthUser() authUser: AuthUserDto,
  92. @Response({ passthrough: true }) res: Res,
  93. @Query(new ValidationPipe({ transform: true })) query: ServeFileDto,
  94. @Param('assetId') assetId: string,
  95. ): Promise<any> {
  96. this.assetService.checkDownloadAccess(authUser);
  97. await this.assetService.checkAssetsAccess(authUser, [assetId]);
  98. return this.assetService.downloadFile(query, assetId, res);
  99. }
  100. @Authenticated({ isShared: true })
  101. @Post('/download-files')
  102. async downloadFiles(
  103. @GetAuthUser() authUser: AuthUserDto,
  104. @Response({ passthrough: true }) res: Res,
  105. @Body(new ValidationPipe()) dto: DownloadFilesDto,
  106. ): Promise<any> {
  107. this.assetService.checkDownloadAccess(authUser);
  108. await this.assetService.checkAssetsAccess(authUser, [...dto.assetIds]);
  109. const { stream, fileName, fileSize, fileCount, complete } = await this.assetService.downloadFiles(dto);
  110. res.attachment(fileName);
  111. res.setHeader(IMMICH_CONTENT_LENGTH_HINT, fileSize);
  112. res.setHeader(IMMICH_ARCHIVE_FILE_COUNT, fileCount);
  113. res.setHeader(IMMICH_ARCHIVE_COMPLETE, `${complete}`);
  114. return stream;
  115. }
  116. /**
  117. * Current this is not used in any UI element
  118. */
  119. @Authenticated({ isShared: true })
  120. @Get('/download-library')
  121. async downloadLibrary(
  122. @GetAuthUser() authUser: AuthUserDto,
  123. @Query(new ValidationPipe({ transform: true })) dto: DownloadDto,
  124. @Response({ passthrough: true }) res: Res,
  125. ): Promise<any> {
  126. this.assetService.checkDownloadAccess(authUser);
  127. const { stream, fileName, fileSize, fileCount, complete } = await this.assetService.downloadLibrary(authUser, dto);
  128. res.attachment(fileName);
  129. res.setHeader(IMMICH_CONTENT_LENGTH_HINT, fileSize);
  130. res.setHeader(IMMICH_ARCHIVE_FILE_COUNT, fileCount);
  131. res.setHeader(IMMICH_ARCHIVE_COMPLETE, `${complete}`);
  132. return stream;
  133. }
  134. @Authenticated({ isShared: true })
  135. @Get('/file/:assetId')
  136. @Header('Cache-Control', 'max-age=31536000')
  137. async serveFile(
  138. @GetAuthUser() authUser: AuthUserDto,
  139. @Headers() headers: Record<string, string>,
  140. @Response({ passthrough: true }) res: Res,
  141. @Query(new ValidationPipe({ transform: true })) query: ServeFileDto,
  142. @Param('assetId') assetId: string,
  143. ): Promise<any> {
  144. await this.assetService.checkAssetsAccess(authUser, [assetId]);
  145. return this.assetService.serveFile(authUser, assetId, query, res, headers);
  146. }
  147. @Authenticated({ isShared: true })
  148. @Get('/thumbnail/:assetId')
  149. @Header('Cache-Control', 'max-age=31536000')
  150. async getAssetThumbnail(
  151. @GetAuthUser() authUser: AuthUserDto,
  152. @Headers() headers: Record<string, string>,
  153. @Response({ passthrough: true }) res: Res,
  154. @Param('assetId') assetId: string,
  155. @Query(new ValidationPipe({ transform: true })) query: GetAssetThumbnailDto,
  156. ): Promise<any> {
  157. await this.assetService.checkAssetsAccess(authUser, [assetId]);
  158. return this.assetService.getAssetThumbnail(assetId, query, res, headers);
  159. }
  160. @Authenticated()
  161. @Get('/curated-objects')
  162. async getCuratedObjects(@GetAuthUser() authUser: AuthUserDto): Promise<CuratedObjectsResponseDto[]> {
  163. return this.assetService.getCuratedObject(authUser);
  164. }
  165. @Authenticated()
  166. @Get('/curated-locations')
  167. async getCuratedLocations(@GetAuthUser() authUser: AuthUserDto): Promise<CuratedLocationsResponseDto[]> {
  168. return this.assetService.getCuratedLocation(authUser);
  169. }
  170. @Authenticated()
  171. @Get('/search-terms')
  172. async getAssetSearchTerms(@GetAuthUser() authUser: AuthUserDto): Promise<string[]> {
  173. return this.assetService.getAssetSearchTerm(authUser);
  174. }
  175. @Authenticated()
  176. @Post('/search')
  177. async searchAsset(
  178. @GetAuthUser() authUser: AuthUserDto,
  179. @Body(ValidationPipe) searchAssetDto: SearchAssetDto,
  180. ): Promise<AssetResponseDto[]> {
  181. return this.assetService.searchAsset(authUser, searchAssetDto);
  182. }
  183. @Authenticated()
  184. @Post('/count-by-time-bucket')
  185. async getAssetCountByTimeBucket(
  186. @GetAuthUser() authUser: AuthUserDto,
  187. @Body(ValidationPipe) getAssetCountByTimeGroupDto: GetAssetCountByTimeBucketDto,
  188. ): Promise<AssetCountByTimeBucketResponseDto> {
  189. return this.assetService.getAssetCountByTimeBucket(authUser, getAssetCountByTimeGroupDto);
  190. }
  191. @Authenticated()
  192. @Get('/count-by-user-id')
  193. async getAssetCountByUserId(@GetAuthUser() authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
  194. return this.assetService.getAssetCountByUserId(authUser);
  195. }
  196. /**
  197. * Get all AssetEntity belong to the user
  198. */
  199. @Authenticated()
  200. @Get('/')
  201. @ApiHeader({
  202. name: 'if-none-match',
  203. description: 'ETag of data already cached on the client',
  204. required: false,
  205. schema: { type: 'string' },
  206. })
  207. async getAllAssets(@GetAuthUser() authUser: AuthUserDto): Promise<AssetResponseDto[]> {
  208. const assets = await this.assetService.getAllAssets(authUser);
  209. return assets;
  210. }
  211. @Authenticated()
  212. @Post('/time-bucket')
  213. async getAssetByTimeBucket(
  214. @GetAuthUser() authUser: AuthUserDto,
  215. @Body(ValidationPipe) getAssetByTimeBucketDto: GetAssetByTimeBucketDto,
  216. ): Promise<AssetResponseDto[]> {
  217. return await this.assetService.getAssetByTimeBucket(authUser, getAssetByTimeBucketDto);
  218. }
  219. /**
  220. * Get all asset of a device that are in the database, ID only.
  221. */
  222. @Authenticated()
  223. @Get('/:deviceId')
  224. async getUserAssetsByDeviceId(@GetAuthUser() authUser: AuthUserDto, @Param('deviceId') deviceId: string) {
  225. return await this.assetService.getUserAssetsByDeviceId(authUser, deviceId);
  226. }
  227. /**
  228. * Get a single asset's information
  229. */
  230. @Authenticated({ isShared: true })
  231. @Get('/assetById/:assetId')
  232. async getAssetById(
  233. @GetAuthUser() authUser: AuthUserDto,
  234. @Param('assetId') assetId: string,
  235. ): Promise<AssetResponseDto> {
  236. await this.assetService.checkAssetsAccess(authUser, [assetId]);
  237. return await this.assetService.getAssetById(authUser, assetId);
  238. }
  239. /**
  240. * Update an asset
  241. */
  242. @Authenticated()
  243. @Put('/:assetId')
  244. async updateAsset(
  245. @GetAuthUser() authUser: AuthUserDto,
  246. @Param('assetId') assetId: string,
  247. @Body(ValidationPipe) dto: UpdateAssetDto,
  248. ): Promise<AssetResponseDto> {
  249. await this.assetService.checkAssetsAccess(authUser, [assetId], true);
  250. return await this.assetService.updateAsset(authUser, assetId, dto);
  251. }
  252. @Authenticated()
  253. @Delete('/')
  254. async deleteAsset(
  255. @GetAuthUser() authUser: AuthUserDto,
  256. @Body(ValidationPipe) assetIds: DeleteAssetDto,
  257. ): Promise<DeleteAssetResponseDto[]> {
  258. await this.assetService.checkAssetsAccess(authUser, assetIds.ids, true);
  259. const deleteAssetList: AssetResponseDto[] = [];
  260. for (const id of assetIds.ids) {
  261. const assets = await this.assetService.getAssetById(authUser, id);
  262. if (!assets) {
  263. continue;
  264. }
  265. deleteAssetList.push(assets);
  266. if (assets.livePhotoVideoId) {
  267. const livePhotoVideo = await this.assetService.getAssetById(authUser, assets.livePhotoVideoId);
  268. if (livePhotoVideo) {
  269. deleteAssetList.push(livePhotoVideo);
  270. assetIds.ids = [...assetIds.ids, livePhotoVideo.id];
  271. }
  272. }
  273. }
  274. const result = await this.assetService.deleteAssetById(assetIds);
  275. result.forEach((res) => {
  276. deleteAssetList.filter((a) => a.id == res.id && res.status == DeleteAssetStatusEnum.SUCCESS);
  277. });
  278. await this.backgroundTaskService.deleteFileOnDisk(deleteAssetList as any[]);
  279. return result;
  280. }
  281. /**
  282. * Check duplicated asset before uploading - for Web upload used
  283. */
  284. @Authenticated({ isShared: true })
  285. @Post('/check')
  286. @HttpCode(200)
  287. async checkDuplicateAsset(
  288. @GetAuthUser() authUser: AuthUserDto,
  289. @Body(ValidationPipe) checkDuplicateAssetDto: CheckDuplicateAssetDto,
  290. ): Promise<CheckDuplicateAssetResponseDto> {
  291. return await this.assetService.checkDuplicatedAsset(authUser, checkDuplicateAssetDto);
  292. }
  293. /**
  294. * Checks if multiple assets exist on the server and returns all existing - used by background backup
  295. */
  296. @Authenticated()
  297. @Post('/exist')
  298. @HttpCode(200)
  299. async checkExistingAssets(
  300. @GetAuthUser() authUser: AuthUserDto,
  301. @Body(ValidationPipe) checkExistingAssetsDto: CheckExistingAssetsDto,
  302. ): Promise<CheckExistingAssetsResponseDto> {
  303. return await this.assetService.checkExistingAssets(authUser, checkExistingAssetsDto);
  304. }
  305. @Authenticated()
  306. @Post('/shared-link')
  307. async createAssetsSharedLink(
  308. @GetAuthUser() authUser: AuthUserDto,
  309. @Body(ValidationPipe) dto: CreateAssetsShareLinkDto,
  310. ): Promise<SharedLinkResponseDto> {
  311. return await this.assetService.createAssetsSharedLink(authUser, dto);
  312. }
  313. @Authenticated({ isShared: true })
  314. @Patch('/shared-link')
  315. async updateAssetsInSharedLink(
  316. @GetAuthUser() authUser: AuthUserDto,
  317. @Body(ValidationPipe) dto: UpdateAssetsToSharedLinkDto,
  318. ): Promise<SharedLinkResponseDto> {
  319. return await this.assetService.updateAssetsInSharedLink(authUser, dto);
  320. }
  321. }