2025-10-10 19:35:18 -04:00
|
|
|
import { Controller, Delete, Head, HttpCode, HttpStatus, Options, Param, Patch, Post, Req, Res } from '@nestjs/common';
|
2025-10-06 21:00:20 -04:00
|
|
|
import { ApiHeader, ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
2025-10-03 01:24:38 -04:00
|
|
|
import { Request, Response } from 'express';
|
2025-10-10 20:59:53 -04:00
|
|
|
import { GetUploadStatusDto, Header, ResumeUploadDto, StartUploadDto, UploadOkDto } from 'src/dtos/asset-upload.dto';
|
2025-09-24 13:56:46 -04:00
|
|
|
import { AuthDto } from 'src/dtos/auth.dto';
|
2025-09-29 18:09:06 -04:00
|
|
|
import { ImmichHeader, Permission } from 'src/enum';
|
2025-10-03 01:24:38 -04:00
|
|
|
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
2025-09-24 13:56:46 -04:00
|
|
|
import { AssetUploadService } from 'src/services/asset-upload.service';
|
2025-10-06 15:14:26 -04:00
|
|
|
import { validateSyncOrReject } from 'src/utils/request';
|
2025-09-24 13:56:46 -04:00
|
|
|
import { UUIDParamDto } from 'src/validation';
|
|
|
|
|
|
2025-09-29 18:09:06 -04:00
|
|
|
const apiInteropVersion = {
|
2025-10-10 20:59:53 -04:00
|
|
|
name: Header.InteropVersion,
|
2025-09-29 18:09:06 -04:00
|
|
|
description: `Indicates the version of the RUFH protocol supported by the client.`,
|
|
|
|
|
required: true,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const apiUploadComplete = {
|
2025-10-10 20:59:53 -04:00
|
|
|
name: Header.UploadComplete,
|
2025-09-29 18:09:06 -04:00
|
|
|
description:
|
|
|
|
|
'Structured boolean indicating whether this request completes the file. Use Upload-Incomplete instead for version <= 3.',
|
|
|
|
|
required: true,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const apiContentLength = {
|
2025-10-10 20:59:53 -04:00
|
|
|
name: Header.ContentLength,
|
2025-09-29 18:09:06 -04:00
|
|
|
description: 'Non-negative size of the request body in bytes.',
|
|
|
|
|
required: true,
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-07 14:13:00 -04:00
|
|
|
// This is important to let go of the asset lock for an inactive request
|
|
|
|
|
const SOCKET_TIMEOUT_MS = 30_000;
|
|
|
|
|
|
2025-09-24 13:56:46 -04:00
|
|
|
@ApiTags('Upload')
|
|
|
|
|
@Controller('upload')
|
|
|
|
|
export class AssetUploadController {
|
|
|
|
|
constructor(private service: AssetUploadService) {}
|
|
|
|
|
|
2025-09-28 18:37:16 -04:00
|
|
|
@Post()
|
2025-09-24 13:56:46 -04:00
|
|
|
@Authenticated({ sharedLink: true, permission: Permission.AssetUpload })
|
2025-09-29 18:09:06 -04:00
|
|
|
@ApiHeader({
|
|
|
|
|
name: ImmichHeader.AssetData,
|
2025-10-03 01:24:38 -04:00
|
|
|
description: `RFC 9651 structured dictionary containing asset metadata with the following keys:
|
|
|
|
|
- device-asset-id (string, required): Unique device asset identifier
|
|
|
|
|
- device-id (string, required): Device identifier
|
|
|
|
|
- file-created-at (string/date, required): ISO 8601 date string or Unix timestamp
|
|
|
|
|
- file-modified-at (string/date, required): ISO 8601 date string or Unix timestamp
|
|
|
|
|
- filename (string, required): Original filename
|
|
|
|
|
- is-favorite (boolean, optional): Favorite status
|
2025-10-09 16:12:47 -04:00
|
|
|
- live-photo-video-id (string, optional): Live photo ID for assets from iOS devices
|
2025-10-03 01:24:38 -04:00
|
|
|
- icloud-id (string, optional): iCloud identifier for assets from iOS devices`,
|
2025-09-29 18:09:06 -04:00
|
|
|
required: true,
|
2025-10-03 01:24:38 -04:00
|
|
|
example:
|
|
|
|
|
'device-asset-id="abc123", device-id="phone1", filename="photo.jpg", file-created-at="2024-01-01T00:00:00Z", file-modified-at="2024-01-01T00:00:00Z"',
|
2025-09-29 18:09:06 -04:00
|
|
|
})
|
|
|
|
|
@ApiHeader({
|
2025-10-10 20:59:53 -04:00
|
|
|
name: Header.ReprDigest,
|
2025-09-29 18:09:06 -04:00
|
|
|
description:
|
2025-10-03 01:24:38 -04:00
|
|
|
'RFC 9651 structured dictionary containing an `sha` (bytesequence) checksum used to detect duplicate files and validate data integrity.',
|
2025-09-29 18:09:06 -04:00
|
|
|
required: true,
|
|
|
|
|
})
|
2025-10-12 19:29:11 -04:00
|
|
|
@ApiHeader({ ...apiInteropVersion, required: false })
|
|
|
|
|
@ApiHeader({ ...apiUploadComplete, required: false })
|
2025-09-29 18:09:06 -04:00
|
|
|
@ApiHeader(apiContentLength)
|
2025-10-06 21:00:20 -04:00
|
|
|
@ApiOkResponse({ type: UploadOkDto })
|
2025-10-03 01:24:38 -04:00
|
|
|
startUpload(@Auth() auth: AuthDto, @Req() req: Request, @Res() res: Response): Promise<void> {
|
2025-10-07 14:13:00 -04:00
|
|
|
res.setTimeout(SOCKET_TIMEOUT_MS);
|
2025-10-06 15:14:26 -04:00
|
|
|
return this.service.startUpload(auth, req, res, validateSyncOrReject(StartUploadDto, req.headers));
|
2025-09-24 13:56:46 -04:00
|
|
|
}
|
|
|
|
|
|
2025-09-28 18:37:16 -04:00
|
|
|
@Patch(':id')
|
2025-09-24 13:56:46 -04:00
|
|
|
@Authenticated({ sharedLink: true, permission: Permission.AssetUpload })
|
2025-09-29 18:09:06 -04:00
|
|
|
@ApiHeader({
|
2025-10-10 20:59:53 -04:00
|
|
|
name: Header.UploadOffset,
|
2025-09-29 18:09:06 -04:00
|
|
|
description:
|
|
|
|
|
'Non-negative byte offset indicating the starting position of the data in the request body within the entire file.',
|
|
|
|
|
required: true,
|
|
|
|
|
})
|
|
|
|
|
@ApiHeader(apiInteropVersion)
|
|
|
|
|
@ApiHeader(apiUploadComplete)
|
|
|
|
|
@ApiHeader(apiContentLength)
|
2025-10-06 21:00:20 -04:00
|
|
|
@ApiOkResponse({ type: UploadOkDto })
|
2025-10-03 01:24:38 -04:00
|
|
|
resumeUpload(@Auth() auth: AuthDto, @Req() req: Request, @Res() res: Response, @Param() { id }: UUIDParamDto) {
|
2025-10-07 14:13:00 -04:00
|
|
|
res.setTimeout(SOCKET_TIMEOUT_MS);
|
2025-10-06 15:14:26 -04:00
|
|
|
return this.service.resumeUpload(auth, req, res, id, validateSyncOrReject(ResumeUploadDto, req.headers));
|
2025-09-24 13:56:46 -04:00
|
|
|
}
|
|
|
|
|
|
2025-09-28 18:37:16 -04:00
|
|
|
@Delete(':id')
|
|
|
|
|
@Authenticated({ sharedLink: true, permission: Permission.AssetUpload })
|
2025-09-29 18:09:06 -04:00
|
|
|
cancelUpload(@Auth() auth: AuthDto, @Res() res: Response, @Param() { id }: UUIDParamDto) {
|
2025-10-07 14:13:00 -04:00
|
|
|
res.setTimeout(SOCKET_TIMEOUT_MS);
|
2025-09-29 18:09:06 -04:00
|
|
|
return this.service.cancelUpload(auth, id, res);
|
2025-09-28 18:37:16 -04:00
|
|
|
}
|
|
|
|
|
|
2025-09-29 03:40:24 -04:00
|
|
|
@Head(':id')
|
2025-09-28 18:37:16 -04:00
|
|
|
@Authenticated({ sharedLink: true, permission: Permission.AssetUpload })
|
2025-09-29 18:09:06 -04:00
|
|
|
@ApiHeader(apiInteropVersion)
|
2025-10-03 01:24:38 -04:00
|
|
|
getUploadStatus(@Auth() auth: AuthDto, @Req() req: Request, @Res() res: Response, @Param() { id }: UUIDParamDto) {
|
2025-10-07 14:13:00 -04:00
|
|
|
res.setTimeout(SOCKET_TIMEOUT_MS);
|
2025-10-06 15:14:26 -04:00
|
|
|
return this.service.getUploadStatus(auth, res, id, validateSyncOrReject(GetUploadStatusDto, req.headers));
|
2025-09-28 18:37:16 -04:00
|
|
|
}
|
|
|
|
|
|
2025-09-29 03:40:24 -04:00
|
|
|
@Options()
|
2025-10-10 19:35:18 -04:00
|
|
|
@HttpCode(HttpStatus.NO_CONTENT)
|
2025-10-10 19:26:22 -04:00
|
|
|
getUploadOptions(@Res() res: Response) {
|
|
|
|
|
return this.service.getUploadOptions(res);
|
|
|
|
|
}
|
2025-09-24 13:56:46 -04:00
|
|
|
}
|