1
0
forked from Cutlery/immich
immich-quadlet/server/src/immich/api-v1/asset/asset.controller.ts
shenlong 4a8887f37b
feat(server): trash asset (#4015)
* refactor(server): delete assets endpoint

* fix: formatting

* chore: cleanup

* chore: open api

* chore(mobile): replace DeleteAssetDTO with BulkIdsDTOs

* feat: trash an asset

* chore(server): formatting

* chore: open api

* chore: wording

* chore: open-api

* feat(server): add withDeleted to getAssets queries

* WIP: mobile-recycle-bin

* feat(server): recycle-bin to system config

* feat(web): use recycle-bin system config

* chore(server): domain assetcore removed

* chore(server): rename recycle-bin to trash

* chore(web): rename recycle-bin to trash

* chore(server): always send soft deleted assets for getAllByUserId

* chore(web): formatting

* feat(server): permanent delete assets older than trashed period

* feat(web): trash empty placeholder image

* feat(server): empty trash

* feat(web): empty trash

* WIP: mobile-recycle-bin

* refactor(server): empty / restore trash to separate endpoint

* test(server): handle failures

* test(server): fix e2e server-info test

* test(server): deletion test refactor

* feat(mobile): use map settings from server-config to enable / disable map

* feat(mobile): trash asset

* fix(server): operations on assets in trash

* feat(web): show trash statistics

* fix(web): handle trash enabled

* fix(mobile): restore updates from trash

* fix(server): ignore trashed assets for person

* fix(server): add / remove search index when trashed / restored

* chore(web): format

* fix(server): asset service test

* fix(server): include trashed assts for duplicates from uploads

* feat(mobile): no dialog for trash, always dialog for permanent delete

* refactor(mobile): use isar where instead of dart filter

* refactor(mobile): asset provide - handle deletes in single db txn

* chore(mobile): review changes

* feat(web): confirmation before empty trash

* server: review changes

* fix(server): handle library changes

* fix: filter external assets from getting trashed / deleted

* fix(server): empty-bin

* feat: broadcast config update events through ws

* change order of trash button on mobile

* styling

* fix(mobile): do not show trashed toast for local only assets

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-10-06 02:01:14 -05:00

229 lines
7.4 KiB
TypeScript

import { AssetResponseDto, AuthUserDto } from '@app/domain';
import {
Body,
Controller,
Get,
HttpCode,
HttpStatus,
Param,
ParseFilePipe,
Post,
Query,
Response,
UploadedFiles,
UseInterceptors,
ValidationPipe,
} from '@nestjs/common';
import { ApiBody, ApiConsumes, ApiHeader, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { Response as Res } from 'express';
import { AuthUser, Authenticated, SharedLinkRoute } from '../../app.guard';
import { FileUploadInterceptor, ImmichFile, Route, mapToUploadFile } from '../../app.interceptor';
import { UUIDParamDto } from '../../controllers/dto/uuid-param.dto';
import FileNotEmptyValidator from '../validation/file-not-empty-validator';
import { AssetService } from './asset.service';
import { AssetBulkUploadCheckDto } from './dto/asset-check.dto';
import { AssetSearchDto } from './dto/asset-search.dto';
import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
import { CreateAssetDto, ImportAssetDto } from './dto/create-asset.dto';
import { DeviceIdDto } from './dto/device-id.dto';
import { GetAssetThumbnailDto } from './dto/get-asset-thumbnail.dto';
import { SearchAssetDto } from './dto/search-asset.dto';
import { ServeFileDto } from './dto/serve-file.dto';
import { AssetBulkUploadCheckResponseDto } from './response-dto/asset-check-response.dto';
import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto';
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
interface UploadFiles {
assetData: ImmichFile[];
livePhotoData?: ImmichFile[];
sidecarData: ImmichFile[];
}
@ApiTags('Asset')
@Controller(Route.ASSET)
@Authenticated()
export class AssetController {
constructor(private assetService: AssetService) {}
@SharedLinkRoute()
@Post('upload')
@UseInterceptors(FileUploadInterceptor)
@ApiConsumes('multipart/form-data')
@ApiBody({
description: 'Asset Upload Information',
type: CreateAssetDto,
})
async uploadFile(
@AuthUser() authUser: AuthUserDto,
@UploadedFiles(new ParseFilePipe({ validators: [new FileNotEmptyValidator(['assetData'])] })) files: UploadFiles,
@Body(new ValidationPipe({ transform: true })) dto: CreateAssetDto,
@Response({ passthrough: true }) res: Res,
): Promise<AssetFileUploadResponseDto> {
const file = mapToUploadFile(files.assetData[0]);
const _livePhotoFile = files.livePhotoData?.[0];
const _sidecarFile = files.sidecarData?.[0];
let livePhotoFile;
if (_livePhotoFile) {
livePhotoFile = mapToUploadFile(_livePhotoFile);
}
let sidecarFile;
if (_sidecarFile) {
sidecarFile = mapToUploadFile(_sidecarFile);
}
const responseDto = await this.assetService.uploadFile(authUser, dto, file, livePhotoFile, sidecarFile);
if (responseDto.duplicate) {
res.status(HttpStatus.OK);
}
return responseDto;
}
@Post('import')
async importFile(
@AuthUser() authUser: AuthUserDto,
@Body(new ValidationPipe({ transform: true })) dto: ImportAssetDto,
@Response({ passthrough: true }) res: Res,
): Promise<AssetFileUploadResponseDto> {
const responseDto = await this.assetService.importFile(authUser, dto);
if (responseDto.duplicate) {
res.status(200);
}
return responseDto;
}
@SharedLinkRoute()
@Get('/file/:id')
@ApiOkResponse({
content: {
'application/octet-stream': { schema: { type: 'string', format: 'binary' } },
},
})
async serveFile(
@AuthUser() authUser: AuthUserDto,
@Response() res: Res,
@Query(new ValidationPipe({ transform: true })) query: ServeFileDto,
@Param() { id }: UUIDParamDto,
) {
await this.assetService.serveFile(authUser, id, query, res);
}
@SharedLinkRoute()
@Get('/thumbnail/:id')
@ApiOkResponse({
content: {
'image/jpeg': { schema: { type: 'string', format: 'binary' } },
'image/webp': { schema: { type: 'string', format: 'binary' } },
},
})
async getAssetThumbnail(
@AuthUser() authUser: AuthUserDto,
@Response() res: Res,
@Param() { id }: UUIDParamDto,
@Query(new ValidationPipe({ transform: true })) query: GetAssetThumbnailDto,
) {
await this.assetService.serveThumbnail(authUser, id, query, res);
}
@Get('/curated-objects')
getCuratedObjects(@AuthUser() authUser: AuthUserDto): Promise<CuratedObjectsResponseDto[]> {
return this.assetService.getCuratedObject(authUser);
}
@Get('/curated-locations')
getCuratedLocations(@AuthUser() authUser: AuthUserDto): Promise<CuratedLocationsResponseDto[]> {
return this.assetService.getCuratedLocation(authUser);
}
@Get('/search-terms')
getAssetSearchTerms(@AuthUser() authUser: AuthUserDto): Promise<string[]> {
return this.assetService.getAssetSearchTerm(authUser);
}
@Post('/search')
@HttpCode(HttpStatus.OK)
searchAsset(
@AuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) dto: SearchAssetDto,
): Promise<AssetResponseDto[]> {
return this.assetService.searchAsset(authUser, dto);
}
/**
* Get all AssetEntity belong to the user
*/
@Get('/')
@ApiHeader({
name: 'if-none-match',
description: 'ETag of data already cached on the client',
required: false,
schema: { type: 'string' },
})
getAllAssets(
@AuthUser() authUser: AuthUserDto,
@Query(new ValidationPipe({ transform: true })) dto: AssetSearchDto,
): Promise<AssetResponseDto[]> {
return this.assetService.getAllAssets(authUser, dto);
}
/**
* Get all asset of a device that are in the database, ID only.
*/
@Get('/:deviceId')
getUserAssetsByDeviceId(@AuthUser() authUser: AuthUserDto, @Param() { deviceId }: DeviceIdDto) {
return this.assetService.getUserAssetsByDeviceId(authUser, deviceId);
}
/**
* Get a single asset's information
*/
@SharedLinkRoute()
@Get('/assetById/:id')
getAssetById(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<AssetResponseDto> {
return this.assetService.getAssetById(authUser, id);
}
/**
* Check duplicated asset before uploading - for Web upload used
*/
@SharedLinkRoute()
@Post('/check')
@HttpCode(HttpStatus.OK)
checkDuplicateAsset(
@AuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) dto: CheckDuplicateAssetDto,
): Promise<CheckDuplicateAssetResponseDto> {
return this.assetService.checkDuplicatedAsset(authUser, dto);
}
/**
* Checks if multiple assets exist on the server and returns all existing - used by background backup
*/
@Post('/exist')
@HttpCode(HttpStatus.OK)
checkExistingAssets(
@AuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) dto: CheckExistingAssetsDto,
): Promise<CheckExistingAssetsResponseDto> {
return this.assetService.checkExistingAssets(authUser, dto);
}
/**
* Checks if assets exist by checksums
*/
@Post('/bulk-upload-check')
@HttpCode(HttpStatus.OK)
bulkUploadCheck(
@AuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) dto: AssetBulkUploadCheckDto,
): Promise<AssetBulkUploadCheckResponseDto> {
return this.assetService.bulkUploadCheck(authUser, dto);
}
}