add note about RFC 9651

authdto

remove excess logs

use structured dictionary
This commit is contained in:
mertalev 2025-10-03 01:24:38 -04:00
parent 0105c9e2b6
commit 597382a25f
No known key found for this signature in database
GPG key ID: DF6ABC77AAD98C95
4 changed files with 99 additions and 92 deletions

View file

@ -3,6 +3,7 @@ import { createHash, randomBytes } from 'node:crypto';
import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, asBearerAuth, baseUrl, utils } from 'src/utils';
import { serializeDictionary } from 'structured-headers';
import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest';
@ -12,7 +13,7 @@ describe('/upload', () => {
let quotaUser: LoginResponseDto;
let cancelQuotaUser: LoginResponseDto;
let base64Metadata: string;
let assetData: string;
beforeAll(async () => {
await utils.resetDatabase();
@ -20,16 +21,15 @@ describe('/upload', () => {
user = await utils.userSetup(admin.accessToken, createUserDto.user1);
cancelQuotaUser = await utils.userSetup(admin.accessToken, createUserDto.user2);
quotaUser = await utils.userSetup(admin.accessToken, createUserDto.userQuota);
base64Metadata = Buffer.from(
JSON.stringify({
filename: 'test-image.jpg',
deviceAssetId: 'rufh',
deviceId: 'test',
fileCreatedAt: new Date('2025-01-02T00:00:00Z').toISOString(),
fileModifiedAt: new Date('2025-01-01T00:00:00Z').toISOString(),
isFavorite: false,
}),
).toString('base64');
assetData = serializeDictionary({
filename: 'test-image.jpg',
'device-asset-id': 'rufh',
'device-id': 'test',
'file-created-at': new Date('2025-01-02T00:00:00Z').toISOString(),
'file-modified-at': new Date('2025-01-01T00:00:00Z').toISOString(),
'is-favorite': false,
'icloud-id': 'example-icloud-id',
});
});
describe('startUpload', () => {
@ -39,7 +39,7 @@ describe('/upload', () => {
const { status, headers } = await request(app)
.post('/upload')
.set('Upload-Draft-Interop-Version', '8')
.set('X-Immich-Asset-Data', base64Metadata)
.set('X-Immich-Asset-Data', assetData)
.set('Repr-Digest', `sha=:${createHash('sha1').update(content).digest('base64')}:`)
.set('Upload-Complete', '?1')
.set('Content-Type', 'image/jpeg')
@ -57,7 +57,7 @@ describe('/upload', () => {
.post('/upload')
.set('Authorization', `Bearer ${user.accessToken}`)
.set('Upload-Draft-Interop-Version', '8')
.set('X-Immich-Asset-Data', base64Metadata)
.set('X-Immich-Asset-Data', assetData)
.set('Repr-Digest', `sha=:${createHash('sha1').update(content).digest('base64')}:`)
.set('Upload-Complete', '?1')
.set('Content-Type', 'image/jpeg')
@ -75,7 +75,7 @@ describe('/upload', () => {
.post('/upload')
.set('Authorization', `Bearer ${user.accessToken}`)
.set('Upload-Draft-Interop-Version', '3')
.set('X-Immich-Asset-Data', base64Metadata)
.set('X-Immich-Asset-Data', assetData)
.set('Repr-Digest', `sha=:${createHash('sha1').update(content).digest('base64')}:`)
.set('Upload-Incomplete', '?0')
.set('Content-Type', 'image/jpeg')
@ -93,7 +93,7 @@ describe('/upload', () => {
.post('/upload')
.set('Authorization', `Bearer ${user.accessToken}`)
.set('Upload-Draft-Interop-Version', '8')
.set('X-Immich-Asset-Data', base64Metadata)
.set('X-Immich-Asset-Data', assetData)
.set('Repr-Digest', `sha=:${createHash('sha1').update(content).digest('base64')}:`)
.set('Upload-Complete', '?1')
.set('Upload-Length', '2000')
@ -115,7 +115,7 @@ describe('/upload', () => {
.post('/upload')
.set('Authorization', `Bearer ${user.accessToken}`)
.set('Upload-Draft-Interop-Version', '8')
.set('X-Immich-Asset-Data', base64Metadata)
.set('X-Immich-Asset-Data', assetData)
.set('Repr-Digest', `sha=:${createHash('sha1').update(content).digest('base64')}:`)
.set('Upload-Complete', '?1')
.set('Content-Type', 'image/jpeg')
@ -137,7 +137,7 @@ describe('/upload', () => {
.post('/upload')
.set('Authorization', `Bearer ${user.accessToken}`)
.set('Upload-Draft-Interop-Version', '8')
.set('X-Immich-Asset-Data', base64Metadata)
.set('X-Immich-Asset-Data', assetData)
.set('Repr-Digest', `sha=:${createHash('sha1').update(partialContent).digest('base64')}:`)
.set('Upload-Complete', '?0')
.set('Content-Length', '512')
@ -156,7 +156,7 @@ describe('/upload', () => {
.post('/upload')
.set('Authorization', `Bearer ${user.accessToken}`)
.set('Upload-Draft-Interop-Version', '3')
.set('X-Immich-Asset-Data', base64Metadata)
.set('X-Immich-Asset-Data', assetData)
.set('Repr-Digest', `sha=:${createHash('sha1').update(partialContent).digest('base64')}:`)
.set('Upload-Incomplete', '?1')
.set('Content-Length', '512')
@ -175,7 +175,7 @@ describe('/upload', () => {
.post('/upload')
.set('Authorization', `Bearer ${user.accessToken}`)
.set('Upload-Draft-Interop-Version', '8')
.set('X-Immich-Asset-Data', base64Metadata)
.set('X-Immich-Asset-Data', assetData)
.set('Repr-Digest', `sha=:INVALID:`)
.set('Upload-Complete', '?1')
.set('Content-Type', 'image/jpeg')
@ -194,7 +194,7 @@ describe('/upload', () => {
.post('/upload')
.set('Authorization', `Bearer ${user.accessToken}`)
.set('Upload-Draft-Interop-Version', '8')
.set('X-Immich-Asset-Data', base64Metadata)
.set('X-Immich-Asset-Data', assetData)
.set('Repr-Digest', `sha=:${createHash('sha1').update(content).digest('base64')}:`)
.set('Upload-Complete', '?1')
.set('Content-Type', 'image/jpeg')
@ -208,7 +208,7 @@ describe('/upload', () => {
.post('/upload')
.set('Authorization', `Bearer ${user.accessToken}`)
.set('Upload-Draft-Interop-Version', '8')
.set('X-Immich-Asset-Data', base64Metadata)
.set('X-Immich-Asset-Data', assetData)
.set('Repr-Digest', `sha=:${createHash('sha1').update(content).digest('base64')}:`)
.set('Upload-Complete', '?1')
.set('Content-Type', 'image/jpeg')
@ -231,7 +231,7 @@ describe('/upload', () => {
.post('/upload')
.set('Authorization', `Bearer ${user.accessToken}`)
.set('Upload-Draft-Interop-Version', '8')
.set('X-Immich-Asset-Data', base64Metadata)
.set('X-Immich-Asset-Data', assetData)
.set('Repr-Digest', `sha=:${createHash('sha1').update(content).digest('base64')}:`)
.set('Upload-Complete', '?0')
.set('Content-Type', 'image/jpeg')
@ -246,7 +246,7 @@ describe('/upload', () => {
.post('/upload')
.set('Authorization', `Bearer ${user.accessToken}`)
.set('Upload-Draft-Interop-Version', '8')
.set('X-Immich-Asset-Data', base64Metadata)
.set('X-Immich-Asset-Data', assetData)
.set('Repr-Digest', `sha=:${createHash('sha1').update(content).digest('base64')}:`)
.set('Upload-Complete', '?1')
.set('Content-Type', 'image/jpeg')
@ -264,7 +264,7 @@ describe('/upload', () => {
.post('/upload')
.set('Authorization', `Bearer ${user.accessToken}`)
.set('Upload-Draft-Interop-Version', '8')
.set('X-Immich-Asset-Data', base64Metadata)
.set('X-Immich-Asset-Data', assetData)
.set('Repr-Digest', `sha=:${createHash('sha1').update('').digest('base64')}:`)
.set('Upload-Complete', '?1')
.set('Content-Type', 'image/jpeg')
@ -283,7 +283,7 @@ describe('/upload', () => {
.post('/upload')
.set('Authorization', `Bearer ${quotaUser.accessToken}`)
.set('Upload-Draft-Interop-Version', '8')
.set('X-Immich-Asset-Data', base64Metadata)
.set('X-Immich-Asset-Data', assetData)
.set('Repr-Digest', `sha=:${createHash('sha1').update(content).digest('base64')}:`)
.set('Upload-Complete', '?1')
.set('Content-Type', 'image/jpeg')
@ -325,7 +325,7 @@ describe('/upload', () => {
.post('/upload')
.set('Authorization', `Bearer ${user.accessToken}`)
.set('Upload-Draft-Interop-Version', '8')
.set('X-Immich-Asset-Data', base64Metadata)
.set('X-Immich-Asset-Data', assetData)
.set('Repr-Digest', `sha=:${createHash('sha1').update(fullContent).digest('base64')}:`)
.set('Upload-Complete', '?0')
.set('Upload-Length', '2750')
@ -358,7 +358,7 @@ describe('/upload', () => {
.send(chunks[2]);
expect(status).toBe(404);
expect(headers['upload-complete']).toBeUndefined();
expect(headers['upload-complete']).toEqual('?0');
});
it('should append data with correct offset', async () => {
@ -502,7 +502,7 @@ describe('/upload', () => {
.post('/upload')
.set('Authorization', `Bearer ${user.accessToken}`)
.set('Upload-Draft-Interop-Version', '8')
.set('X-Immich-Asset-Data', base64Metadata)
.set('X-Immich-Asset-Data', assetData)
.set('Repr-Digest', `sha=:${createHash('sha1').update(totalContent).digest('base64')}:`)
.set('Upload-Complete', '?0')
.set('Upload-Length', '5000')
@ -539,7 +539,7 @@ describe('/upload', () => {
.post('/upload')
.set('Authorization', `Bearer ${user.accessToken}`)
.set('Upload-Draft-Interop-Version', '8')
.set('X-Immich-Asset-Data', base64Metadata)
.set('X-Immich-Asset-Data', assetData)
.set('Repr-Digest', `sha=:${hash.digest('base64')}:`)
.set('Upload-Complete', '?0')
.set('Upload-Length', '10000')
@ -591,7 +591,7 @@ describe('/upload', () => {
.post('/upload')
.set('Authorization', `Bearer ${user.accessToken}`)
.set('Upload-Draft-Interop-Version', '8')
.set('X-Immich-Asset-Data', base64Metadata)
.set('X-Immich-Asset-Data', assetData)
.set('Repr-Digest', `sha=:${createHash('sha1').update(content).digest('base64')}:`)
.set('Upload-Complete', '?0')
.set('Upload-Length', '200')
@ -631,7 +631,7 @@ describe('/upload', () => {
.post('/upload')
.set('Authorization', `Bearer ${user.accessToken}`)
.set('Upload-Draft-Interop-Version', '8')
.set('X-Immich-Asset-Data', base64Metadata)
.set('X-Immich-Asset-Data', assetData)
.set('Repr-Digest', `sha=:${createHash('sha1').update(content).digest('base64')}:`)
.set('Upload-Complete', '?0')
.set('Content-Type', 'image/jpeg')
@ -687,7 +687,7 @@ describe('/upload', () => {
.post('/upload')
.set('Authorization', `Bearer ${cancelQuotaUser.accessToken}`)
.set('Upload-Draft-Interop-Version', '8')
.set('X-Immich-Asset-Data', base64Metadata)
.set('X-Immich-Asset-Data', assetData)
.set('Repr-Digest', `sha=:${createHash('sha1').update(content).digest('base64')}:`)
.set('Upload-Complete', '?0')
.set('Upload-Length', '200')
@ -723,7 +723,7 @@ describe('/upload', () => {
.post('/upload')
.set('Authorization', `Bearer ${user.accessToken}`)
.set('Upload-Draft-Interop-Version', '8')
.set('X-Immich-Asset-Data', base64Metadata)
.set('X-Immich-Asset-Data', assetData)
.set('Repr-Digest', `sha=:${createHash('sha1').update(content).digest('base64')}:`)
.set('Upload-Complete', '?0')
.set('Upload-Length', '512')

View file

@ -16,12 +16,12 @@ import {
import { ApiHeader, ApiTags } from '@nestjs/swagger';
import { plainToInstance } from 'class-transformer';
import { validateSync } from 'class-validator';
import { Response } from 'express';
import { Request, Response } from 'express';
import { IncomingHttpHeaders } from 'node:http';
import { AuthDto } from 'src/dtos/auth.dto';
import { GetUploadStatusDto, ResumeUploadDto, StartUploadDto, UploadHeader } from 'src/dtos/upload.dto';
import { ImmichHeader, Permission } from 'src/enum';
import { Auth, Authenticated, AuthenticatedRequest } from 'src/middleware/auth.guard';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { AssetUploadService } from 'src/services/asset-upload.service';
import { UUIDParamDto } from 'src/validation';
@ -53,23 +53,31 @@ export class AssetUploadController {
@Authenticated({ sharedLink: true, permission: Permission.AssetUpload })
@ApiHeader({
name: ImmichHeader.AssetData,
description:
'Base64-encoded JSON of asset metadata. The expected content is the same as AssetMediaCreateDto, except that `filename` is required and `sidecarData` is ignored.',
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
- duration (string, optional): Duration for video assets
- is-favorite (boolean, optional): Favorite status
- icloud-id (string, optional): iCloud identifier for assets from iOS devices`,
required: true,
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"',
})
@ApiHeader({
name: UploadHeader.ReprDigest,
description:
'Structured dictionary containing an SHA-1 checksum used to detect duplicate files and validate data integrity.',
'RFC 9651 structured dictionary containing an `sha` (bytesequence) checksum used to detect duplicate files and validate data integrity.',
required: true,
})
@ApiHeader(apiInteropVersion)
@ApiHeader(apiUploadComplete)
@ApiHeader(apiContentLength)
startUpload(@Req() req: AuthenticatedRequest, @Res() res: Response): Promise<void> {
startUpload(@Auth() auth: AuthDto, @Req() req: Request, @Res() res: Response): Promise<void> {
const dto = this.getDto(StartUploadDto, req.headers);
console.log('Starting upload with dto:', JSON.stringify(dto));
return this.service.startUpload(req, res, dto);
return this.service.startUpload(auth, req, res, dto);
}
@Patch(':id')
@ -83,10 +91,9 @@ export class AssetUploadController {
@ApiHeader(apiInteropVersion)
@ApiHeader(apiUploadComplete)
@ApiHeader(apiContentLength)
resumeUpload(@Req() req: AuthenticatedRequest, @Res() res: Response, @Param() { id }: UUIDParamDto) {
resumeUpload(@Auth() auth: AuthDto, @Req() req: Request, @Res() res: Response, @Param() { id }: UUIDParamDto) {
const dto = this.getDto(ResumeUploadDto, req.headers);
console.log('Resuming upload with dto:', JSON.stringify(dto));
return this.service.resumeUpload(req, res, id, dto);
return this.service.resumeUpload(auth, req, res, id, dto);
}
@Delete(':id')
@ -98,10 +105,9 @@ export class AssetUploadController {
@Head(':id')
@Authenticated({ sharedLink: true, permission: Permission.AssetUpload })
@ApiHeader(apiInteropVersion)
getUploadStatus(@Req() req: AuthenticatedRequest, @Res() res: Response, @Param() { id }: UUIDParamDto) {
getUploadStatus(@Auth() auth: AuthDto, @Req() req: Request, @Res() res: Response, @Param() { id }: UUIDParamDto) {
const dto = this.getDto(GetUploadStatusDto, req.headers);
console.log('Getting upload status with dto:', JSON.stringify(dto));
return this.service.getUploadStatus(req.auth, res, id, dto);
return this.service.getUploadStatus(auth, res, id, dto);
}
@Options()

View file

@ -1,7 +1,6 @@
import { BadRequestException } from '@nestjs/common';
import { Expose, plainToInstance, Transform, Type } from 'class-transformer';
import { Equals, IsArray, IsEnum, IsInt, IsNotEmpty, IsString, Min, ValidateIf, ValidateNested } from 'class-validator';
import { AssetMetadataUpsertItemDto } from 'src/dtos/asset.dto';
import { Equals, IsEnum, IsInt, IsNotEmpty, IsString, Min, ValidateIf, ValidateNested } from 'class-validator';
import { ImmichHeader } from 'src/enum';
import { Optional, ValidateBoolean, ValidateDate } from 'src/validation';
import { parseDictionary } from 'structured-headers';
@ -32,19 +31,10 @@ export class UploadAssetDataDto {
@ValidateBoolean({ optional: true })
isFavorite?: boolean;
@Transform(({ value }) => {
try {
const json = JSON.parse(value);
const items = Array.isArray(json) ? json : [json];
return items.map((item) => plainToInstance(AssetMetadataUpsertItemDto, item));
} catch {
throw new BadRequestException(['metadata must be valid JSON']);
}
})
@Optional()
@ValidateNested({ each: true })
@IsArray()
metadata!: AssetMetadataUpsertItemDto[];
@IsString()
@IsNotEmpty()
iCloudId!: string;
}
export enum StructuredBoolean {
@ -78,12 +68,12 @@ export class BaseUploadHeadersDto extends BaseRufhHeadersDto {
contentLength!: number;
@Expose({ name: UploadHeader.UploadComplete })
@ValidateIf((o) => o.version === null || o.version! > 3)
@ValidateIf((o) => o.version === null || o.version > 3)
@IsEnum(StructuredBoolean)
uploadComplete!: StructuredBoolean;
@Expose({ name: UploadHeader.UploadIncomplete })
@ValidateIf((o) => o.version !== null && o.version! <= 3)
@ValidateIf((o) => o.version !== null && o.version <= 3)
@IsEnum(StructuredBoolean)
uploadIncomplete!: StructuredBoolean;
@ -97,19 +87,26 @@ export class BaseUploadHeadersDto extends BaseRufhHeadersDto {
export class StartUploadDto extends BaseUploadHeadersDto {
@Expose({ name: ImmichHeader.AssetData })
// @ValidateNested()
// @IsObject()
@Type(() => UploadAssetDataDto)
@ValidateNested()
@Transform(({ value }) => {
if (!value) {
return null;
throw new BadRequestException(`${ImmichHeader.AssetData} header is required`);
}
const json = Buffer.from(value, 'base64').toString('utf-8');
try {
return JSON.parse(json);
} catch {
throw new BadRequestException(`${ImmichHeader.AssetData} must be valid base64-encoded JSON`);
const dict = parseDictionary(value);
return plainToInstance(UploadAssetDataDto, {
deviceAssetId: dict.get('device-asset-id')?.[0],
deviceId: dict.get('device-id')?.[0],
filename: dict.get('filename')?.[0],
duration: dict.get('duration')?.[0],
fileCreatedAt: dict.get('file-created-at')?.[0],
fileModifiedAt: dict.get('file-modified-at')?.[0],
isFavorite: dict.get('is-favorite')?.[0],
iCloudId: dict.get('icloud-id')?.[0],
});
} catch (error: any) {
throw new BadRequestException(`${ImmichHeader.AssetData} must be a valid structured dictionary`);
}
})
assetData!: UploadAssetDataDto;

View file

@ -9,8 +9,16 @@ import { StorageCore } from 'src/cores/storage.core';
import { OnJob } from 'src/decorators';
import { AuthDto } from 'src/dtos/auth.dto';
import { GetUploadStatusDto, ResumeUploadDto, StartUploadDto } from 'src/dtos/upload.dto';
import { AssetStatus, AssetType, AssetVisibility, JobName, JobStatus, QueueName, StorageFolder } from 'src/enum';
import { AuthenticatedRequest } from 'src/middleware/auth.guard';
import {
AssetMetadataKey,
AssetStatus,
AssetType,
AssetVisibility,
JobName,
JobStatus,
QueueName,
StorageFolder,
} from 'src/enum';
import { BaseService } from 'src/services/base.service';
import { JobItem, JobOf } from 'src/types';
import { isAssetChecksumConstraint } from 'src/utils/database';
@ -21,12 +29,12 @@ export const MAX_RUFH_INTEROP_VERSION = 8;
@Injectable()
export class AssetUploadService extends BaseService {
async startUpload(req: AuthenticatedRequest, res: Response, dto: StartUploadDto): Promise<void> {
async startUpload(auth: AuthDto, req: Readable, res: Response, dto: StartUploadDto): Promise<void> {
this.logger.verboseFn(() => `Starting upload: ${JSON.stringify(dto)}`);
const { isComplete, assetData, uploadLength, contentLength, version } = dto;
const assetId = this.cryptoRepository.randomUUID();
const folder = StorageCore.getNestedFolder(StorageFolder.Upload, req.auth.user.id, assetId);
const folder = StorageCore.getNestedFolder(StorageFolder.Upload, auth.user.id, assetId);
const extension = extname(assetData.filename);
const path = join(folder, `${assetId}${extension}`);
const type = mimeTypes.assetType(path);
@ -35,13 +43,13 @@ export class AssetUploadService extends BaseService {
throw new BadRequestException(`${assetData.filename} is an unsupported file type`);
}
this.validateQuota(req.auth, uploadLength ?? contentLength);
this.validateQuota(auth, uploadLength ?? contentLength);
try {
await this.assetRepository.createWithMetadata(
{
id: assetId,
ownerId: req.auth.user.id,
ownerId: auth.user.id,
libraryId: null,
checksum: dto.checksum,
originalPath: path,
@ -58,7 +66,7 @@ export class AssetUploadService extends BaseService {
status: AssetStatus.Partial,
},
uploadLength,
assetData.metadata,
assetData.iCloudId ? [{ key: AssetMetadataKey.MobileApp, value: { iCloudId: assetData.iCloudId } }] : undefined,
);
} catch (error: any) {
if (!isAssetChecksumConstraint(error)) {
@ -67,7 +75,7 @@ export class AssetUploadService extends BaseService {
return;
}
const duplicate = await this.assetRepository.getUploadAssetIdByChecksum(req.auth.user.id, dto.checksum);
const duplicate = await this.assetRepository.getUploadAssetIdByChecksum(auth.user.id, dto.checksum);
if (!duplicate) {
res.status(500).send('Error locating duplicate for checksum constraint');
return;
@ -126,12 +134,12 @@ export class AssetUploadService extends BaseService {
await new Promise((resolve) => writeStream.on('close', resolve));
}
resumeUpload(req: AuthenticatedRequest, res: Response, id: string, dto: ResumeUploadDto): Promise<void> {
resumeUpload(auth: AuthDto, req: Readable, res: Response, id: string, dto: ResumeUploadDto): Promise<void> {
this.logger.verboseFn(() => `Resuming upload for ${id}: ${JSON.stringify(dto)}`);
const { isComplete, uploadLength, uploadOffset, contentLength, version } = dto;
this.setCompleteHeader(res, version, false);
return this.databaseRepository.withUuidLock(id, async () => {
const completionData = await this.assetRepository.getCompletionMetadata(id, req.auth.user.id);
const completionData = await this.assetRepository.getCompletionMetadata(id, auth.user.id);
if (!completionData) {
res.status(404).send('Asset not found');
return;
@ -139,30 +147,25 @@ export class AssetUploadService extends BaseService {
const { fileModifiedAt, path, status, checksum: providedChecksum, size } = completionData;
if (status !== AssetStatus.Partial) {
this.setCompleteHeader(res, version, false);
return this.sendAlreadyCompletedProblem(res);
}
if (uploadLength && size && size !== uploadLength) {
this.setCompleteHeader(res, version, false);
return this.sendInconsistentLengthProblem(res);
}
const expectedOffset = await this.getCurrentOffset(path);
if (expectedOffset !== uploadOffset) {
this.setCompleteHeader(res, version, false);
return this.sendOffsetMismatchProblem(res, expectedOffset, uploadOffset);
}
const newLength = uploadOffset + contentLength;
if (uploadLength !== undefined && newLength > uploadLength) {
this.setCompleteHeader(res, version, false);
res.status(400).send('Upload would exceed declared length');
return;
}
if (contentLength === 0 && !isComplete) {
this.setCompleteHeader(res, version, false);
res.status(204).setHeader('Upload-Offset', expectedOffset.toString()).send();
return;
}
@ -192,22 +195,23 @@ export class AssetUploadService extends BaseService {
});
}
cancelUpload(auth: AuthDto, assetId: string, response: Response): Promise<void> {
cancelUpload(auth: AuthDto, assetId: string, res: Response): Promise<void> {
return this.databaseRepository.withUuidLock(assetId, async () => {
const asset = await this.assetRepository.getCompletionMetadata(assetId, auth.user.id);
if (!asset) {
response.status(404).send('Asset not found');
res.status(404).send('Asset not found');
return;
}
if (asset.status !== AssetStatus.Partial) {
return this.sendAlreadyCompletedProblem(response);
return this.sendAlreadyCompletedProblem(res);
}
await this.onCancel(assetId, asset.path);
response.status(204).send();
res.status(204).send();
});
}
async getUploadStatus(auth: AuthDto, res: Response, id: string, { version }: GetUploadStatusDto): Promise<void> {
this.logger.verboseFn(() => `Getting upload status for ${id} with version ${version}`);
return this.databaseRepository.withUuidLock(id, async () => {
const asset = await this.assetRepository.getCompletionMetadata(id, auth.user.id);
if (!asset) {
@ -333,7 +337,7 @@ export class AssetUploadService extends BaseService {
socket.write(
'HTTP/1.1 104 Upload Resumption Supported\r\n' +
`Location: ${location}\r\n` +
`Upload-Limit: min-size=0\r\n` +
'Upload-Limit: min-size=0\r\n' +
`Upload-Draft-Interop-Version: ${interopVersion}\r\n\r\n`,
);
}