From 81a66350f604993eff7fb7a6933984356067cd5d Mon Sep 17 00:00:00 2001 From: mertalev <101130780+mertalev@users.noreply.github.com> Date: Fri, 3 Oct 2025 01:24:38 -0400 Subject: [PATCH] add note about RFC 9651 authdto remove excess logs use structured dictionary --- e2e/src/api/specs/asset-upload.e2e-spec.ts | 66 +++++++++---------- .../controllers/asset-upload.controller.ts | 34 ++++++---- server/src/dtos/upload.dto.ts | 45 ++++++------- server/src/services/asset-upload.service.ts | 46 +++++++------ 4 files changed, 99 insertions(+), 92 deletions(-) diff --git a/e2e/src/api/specs/asset-upload.e2e-spec.ts b/e2e/src/api/specs/asset-upload.e2e-spec.ts index 7956172f04..237cdc48e0 100644 --- a/e2e/src/api/specs/asset-upload.e2e-spec.ts +++ b/e2e/src/api/specs/asset-upload.e2e-spec.ts @@ -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') diff --git a/server/src/controllers/asset-upload.controller.ts b/server/src/controllers/asset-upload.controller.ts index 33e97079de..f5a02ab628 100644 --- a/server/src/controllers/asset-upload.controller.ts +++ b/server/src/controllers/asset-upload.controller.ts @@ -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 { + startUpload(@Auth() auth: AuthDto, @Req() req: Request, @Res() res: Response): Promise { 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() diff --git a/server/src/dtos/upload.dto.ts b/server/src/dtos/upload.dto.ts index 959334d3aa..566c0fbf80 100644 --- a/server/src/dtos/upload.dto.ts +++ b/server/src/dtos/upload.dto.ts @@ -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; diff --git a/server/src/services/asset-upload.service.ts b/server/src/services/asset-upload.service.ts index 7ad648d88e..fe5be05c55 100644 --- a/server/src/services/asset-upload.service.ts +++ b/server/src/services/asset-upload.service.ts @@ -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 { + async startUpload(auth: AuthDto, req: Readable, res: Response, dto: StartUploadDto): Promise { 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 { + resumeUpload(auth: AuthDto, req: Readable, res: Response, id: string, dto: ResumeUploadDto): Promise { 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 { + cancelUpload(auth: AuthDto, assetId: string, res: Response): Promise { 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 { + 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`, ); }