mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
infer upload length when possible
This commit is contained in:
parent
b0aa68d83a
commit
504d8dc96c
5 changed files with 93 additions and 109 deletions
|
|
@ -112,11 +112,7 @@ describe(AssetUploadController.name, () => {
|
||||||
.send(buffer);
|
.send(buffer);
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(expect.objectContaining({ message: 'Expected valid upload-complete header' }));
|
||||||
expect.objectContaining({
|
|
||||||
message: expect.arrayContaining([expect.stringContaining('uploadComplete')]),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require Upload-Length header', async () => {
|
it('should require Upload-Length header', async () => {
|
||||||
|
|
@ -125,18 +121,23 @@ describe(AssetUploadController.name, () => {
|
||||||
.set('Upload-Draft-Interop-Version', '8')
|
.set('Upload-Draft-Interop-Version', '8')
|
||||||
.set('X-Immich-Asset-Data', makeAssetData())
|
.set('X-Immich-Asset-Data', makeAssetData())
|
||||||
.set('Repr-Digest', checksum)
|
.set('Repr-Digest', checksum)
|
||||||
.set('Upload-Complete', '?1')
|
.set('Upload-Complete', '?0')
|
||||||
.send(buffer);
|
.send(buffer);
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(expect.objectContaining({ message: 'Missing upload-length header' }));
|
||||||
expect.objectContaining({
|
});
|
||||||
message: expect.arrayContaining([
|
|
||||||
'uploadLength must be an integer number',
|
it('should infer upload length from content length if complete upload', async () => {
|
||||||
'uploadLength must not be less than 0',
|
const { status } = await request(ctx.getHttpServer())
|
||||||
]),
|
.post('/upload')
|
||||||
}),
|
.set('Upload-Draft-Interop-Version', '8')
|
||||||
);
|
.set('X-Immich-Asset-Data', makeAssetData())
|
||||||
|
.set('Repr-Digest', checksum)
|
||||||
|
.set('Upload-Complete', '?1')
|
||||||
|
.send(buffer);
|
||||||
|
|
||||||
|
expect(status).toBe(201);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject invalid Repr-Digest format', async () => {
|
it('should reject invalid Repr-Digest format', async () => {
|
||||||
|
|
@ -229,15 +230,17 @@ describe(AssetUploadController.name, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should accept Upload-Incomplete header for version 3', async () => {
|
it('should accept Upload-Incomplete header for version 3', async () => {
|
||||||
const { status } = await request(ctx.getHttpServer())
|
const { body, status } = await request(ctx.getHttpServer())
|
||||||
.post('/upload')
|
.post('/upload')
|
||||||
.set('Upload-Draft-Interop-Version', '3')
|
.set('Upload-Draft-Interop-Version', '3')
|
||||||
.set('X-Immich-Asset-Data', makeAssetData())
|
.set('X-Immich-Asset-Data', makeAssetData())
|
||||||
.set('Repr-Digest', checksum)
|
.set('Repr-Digest', checksum)
|
||||||
.set('Upload-Incomplete', '?0')
|
.set('Upload-Incomplete', '?0')
|
||||||
|
.set('Upload-Complete', '?1')
|
||||||
.set('Upload-Length', '1024')
|
.set('Upload-Length', '1024')
|
||||||
.send(buffer);
|
.send(buffer);
|
||||||
|
|
||||||
|
expect(body).toEqual({});
|
||||||
expect(status).not.toBe(400);
|
expect(status).not.toBe(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -252,11 +255,7 @@ describe(AssetUploadController.name, () => {
|
||||||
.send(buffer);
|
.send(buffer);
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(expect.objectContaining({ message: 'Expected valid upload-complete header' }));
|
||||||
expect.objectContaining({
|
|
||||||
message: expect.arrayContaining([expect.stringContaining('uploadComplete')]),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate Upload-Length is a non-negative integer', async () => {
|
it('should validate Upload-Length is a non-negative integer', async () => {
|
||||||
|
|
@ -327,11 +326,7 @@ describe(AssetUploadController.name, () => {
|
||||||
.send(Buffer.from('test'));
|
.send(Buffer.from('test'));
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(expect.objectContaining({ message: 'Expected valid upload-complete header' }));
|
||||||
expect.objectContaining({
|
|
||||||
message: expect.arrayContaining([expect.stringContaining('uploadComplete')]),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate UUID parameter', async () => {
|
it('should validate UUID parameter', async () => {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,7 @@
|
||||||
import { Controller, Delete, Head, HttpCode, HttpStatus, Options, Param, Patch, Post, Req, Res } from '@nestjs/common';
|
import { Controller, Delete, Head, HttpCode, HttpStatus, Options, Param, Patch, Post, Req, Res } from '@nestjs/common';
|
||||||
import { ApiHeader, ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
import { ApiHeader, ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import {
|
import { GetUploadStatusDto, Header, ResumeUploadDto, StartUploadDto, UploadOkDto } from 'src/dtos/asset-upload.dto';
|
||||||
GetUploadStatusDto,
|
|
||||||
ResumeUploadDto,
|
|
||||||
StartUploadDto,
|
|
||||||
UploadHeader,
|
|
||||||
UploadOkDto,
|
|
||||||
} from 'src/dtos/asset-upload.dto';
|
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { ImmichHeader, Permission } from 'src/enum';
|
import { ImmichHeader, Permission } from 'src/enum';
|
||||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||||
|
|
@ -16,20 +10,20 @@ import { validateSyncOrReject } from 'src/utils/request';
|
||||||
import { UUIDParamDto } from 'src/validation';
|
import { UUIDParamDto } from 'src/validation';
|
||||||
|
|
||||||
const apiInteropVersion = {
|
const apiInteropVersion = {
|
||||||
name: UploadHeader.InteropVersion,
|
name: Header.InteropVersion,
|
||||||
description: `Indicates the version of the RUFH protocol supported by the client.`,
|
description: `Indicates the version of the RUFH protocol supported by the client.`,
|
||||||
required: true,
|
required: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const apiUploadComplete = {
|
const apiUploadComplete = {
|
||||||
name: UploadHeader.UploadComplete,
|
name: Header.UploadComplete,
|
||||||
description:
|
description:
|
||||||
'Structured boolean indicating whether this request completes the file. Use Upload-Incomplete instead for version <= 3.',
|
'Structured boolean indicating whether this request completes the file. Use Upload-Incomplete instead for version <= 3.',
|
||||||
required: true,
|
required: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const apiContentLength = {
|
const apiContentLength = {
|
||||||
name: UploadHeader.ContentLength,
|
name: Header.ContentLength,
|
||||||
description: 'Non-negative size of the request body in bytes.',
|
description: 'Non-negative size of the request body in bytes.',
|
||||||
required: true,
|
required: true,
|
||||||
};
|
};
|
||||||
|
|
@ -60,7 +54,7 @@ export class AssetUploadController {
|
||||||
'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"',
|
'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({
|
@ApiHeader({
|
||||||
name: UploadHeader.ReprDigest,
|
name: Header.ReprDigest,
|
||||||
description:
|
description:
|
||||||
'RFC 9651 structured dictionary containing an `sha` (bytesequence) 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,
|
required: true,
|
||||||
|
|
@ -77,7 +71,7 @@ export class AssetUploadController {
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
@Authenticated({ sharedLink: true, permission: Permission.AssetUpload })
|
@Authenticated({ sharedLink: true, permission: Permission.AssetUpload })
|
||||||
@ApiHeader({
|
@ApiHeader({
|
||||||
name: UploadHeader.UploadOffset,
|
name: Header.UploadOffset,
|
||||||
description:
|
description:
|
||||||
'Non-negative byte offset indicating the starting position of the data in the request body within the entire file.',
|
'Non-negative byte offset indicating the starting position of the data in the request body within the entire file.',
|
||||||
required: true,
|
required: true,
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,22 @@
|
||||||
import { BadRequestException } from '@nestjs/common';
|
import { BadRequestException } from '@nestjs/common';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Expose, plainToInstance, Transform, Type } from 'class-transformer';
|
import { Expose, plainToInstance, Transform, Type } from 'class-transformer';
|
||||||
import { Equals, IsEmpty, IsEnum, IsInt, IsNotEmpty, IsString, Min, ValidateIf, ValidateNested } from 'class-validator';
|
import { Equals, IsInt, IsNotEmpty, IsString, Min, ValidateIf, ValidateNested } from 'class-validator';
|
||||||
import { ImmichHeader } from 'src/enum';
|
import { ImmichHeader } from 'src/enum';
|
||||||
import { Optional, ValidateBoolean, ValidateDate } from 'src/validation';
|
import { Optional, ValidateBoolean, ValidateDate } from 'src/validation';
|
||||||
import { parseDictionary } from 'structured-headers';
|
import { parseDictionary } from 'structured-headers';
|
||||||
|
|
||||||
|
export enum Header {
|
||||||
|
ContentLength = 'content-length',
|
||||||
|
ContentType = 'content-type',
|
||||||
|
InteropVersion = 'upload-draft-interop-version',
|
||||||
|
ReprDigest = 'repr-digest',
|
||||||
|
UploadComplete = 'upload-complete',
|
||||||
|
UploadIncomplete = 'upload-incomplete',
|
||||||
|
UploadLength = 'upload-length',
|
||||||
|
UploadOffset = 'upload-offset',
|
||||||
|
}
|
||||||
|
|
||||||
export class UploadAssetDataDto {
|
export class UploadAssetDataDto {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|
@ -39,24 +50,8 @@ export class UploadAssetDataDto {
|
||||||
iCloudId!: string;
|
iCloudId!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum StructuredBoolean {
|
|
||||||
False = '?0',
|
|
||||||
True = '?1',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum UploadHeader {
|
|
||||||
ContentLength = 'content-length',
|
|
||||||
ContentType = 'content-type',
|
|
||||||
InteropVersion = 'upload-draft-interop-version',
|
|
||||||
ReprDigest = 'repr-digest',
|
|
||||||
UploadComplete = 'upload-complete',
|
|
||||||
UploadIncomplete = 'upload-incomplete',
|
|
||||||
UploadLength = 'upload-length',
|
|
||||||
UploadOffset = 'upload-offset',
|
|
||||||
}
|
|
||||||
|
|
||||||
class BaseRufhHeadersDto {
|
class BaseRufhHeadersDto {
|
||||||
@Expose({ name: UploadHeader.InteropVersion })
|
@Expose({ name: Header.InteropVersion })
|
||||||
@Min(3)
|
@Min(3)
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
|
|
@ -64,28 +59,15 @@ class BaseRufhHeadersDto {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BaseUploadHeadersDto extends BaseRufhHeadersDto {
|
export class BaseUploadHeadersDto extends BaseRufhHeadersDto {
|
||||||
@Expose({ name: UploadHeader.ContentLength })
|
@Expose({ name: Header.ContentLength })
|
||||||
@Min(0)
|
@Min(0)
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
contentLength!: number;
|
contentLength!: number;
|
||||||
|
|
||||||
@Expose({ name: UploadHeader.UploadComplete })
|
@Expose()
|
||||||
@ValidateIf((o) => o.version === null || o.version > 3)
|
@Transform(({ obj }) => isUploadComplete(obj))
|
||||||
@IsEnum(StructuredBoolean)
|
uploadComplete!: boolean;
|
||||||
uploadComplete!: StructuredBoolean;
|
|
||||||
|
|
||||||
@Expose({ name: UploadHeader.UploadIncomplete })
|
|
||||||
@ValidateIf((o) => o.version !== null && o.version <= 3)
|
|
||||||
@IsEnum(StructuredBoolean)
|
|
||||||
uploadIncomplete!: StructuredBoolean;
|
|
||||||
|
|
||||||
get isComplete(): boolean {
|
|
||||||
if (this.version <= 3) {
|
|
||||||
return this.uploadIncomplete === StructuredBoolean.False;
|
|
||||||
}
|
|
||||||
return this.uploadComplete === StructuredBoolean.True;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StartUploadDto extends BaseUploadHeadersDto {
|
export class StartUploadDto extends BaseUploadHeadersDto {
|
||||||
|
|
@ -115,66 +97,81 @@ export class StartUploadDto extends BaseUploadHeadersDto {
|
||||||
})
|
})
|
||||||
assetData!: UploadAssetDataDto;
|
assetData!: UploadAssetDataDto;
|
||||||
|
|
||||||
@Expose({ name: UploadHeader.ReprDigest })
|
@Expose({ name: Header.ReprDigest })
|
||||||
@Transform(({ value }) => {
|
@Transform(({ value }) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
throw new BadRequestException(`Missing ${UploadHeader.ReprDigest} header`);
|
throw new BadRequestException(`Missing ${Header.ReprDigest} header`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const checksum = parseDictionary(value).get('sha')?.[0];
|
const checksum = parseDictionary(value).get('sha')?.[0];
|
||||||
if (checksum instanceof ArrayBuffer && checksum.byteLength === 20) {
|
if (checksum instanceof ArrayBuffer && checksum.byteLength === 20) {
|
||||||
return Buffer.from(checksum);
|
return Buffer.from(checksum);
|
||||||
}
|
}
|
||||||
throw new BadRequestException(`Invalid ${UploadHeader.ReprDigest} header`);
|
throw new BadRequestException(`Invalid ${Header.ReprDigest} header`);
|
||||||
})
|
})
|
||||||
checksum!: Buffer;
|
checksum!: Buffer;
|
||||||
|
|
||||||
@Expose({ name: UploadHeader.UploadLength })
|
@Expose()
|
||||||
@Min(0)
|
@Min(0)
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@Transform(({ obj, value }) => Number(value === undefined ? obj['x-upload-length'] : value))
|
@Transform(({ obj }) => {
|
||||||
uploadLength!: number;
|
const uploadLength = obj[Header.UploadLength];
|
||||||
|
if (uploadLength != undefined) {
|
||||||
|
return Number(uploadLength);
|
||||||
|
}
|
||||||
|
|
||||||
@Expose({ name: UploadHeader.UploadOffset })
|
const contentLength = obj[Header.ContentLength];
|
||||||
@IsEmpty()
|
if (contentLength != undefined && isUploadComplete(obj)) {
|
||||||
uploadOffset?: string;
|
return Number(contentLength);
|
||||||
|
}
|
||||||
|
throw new BadRequestException(`Missing ${Header.UploadLength} header`);
|
||||||
|
})
|
||||||
|
uploadLength!: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ResumeUploadDto extends BaseUploadHeadersDto {
|
export class ResumeUploadDto extends BaseUploadHeadersDto {
|
||||||
@Expose({ name: UploadHeader.ContentType })
|
@Expose({ name: Header.ContentType })
|
||||||
@ValidateIf((o) => o.version && o.version >= 6)
|
@ValidateIf((o) => o.version && o.version >= 6)
|
||||||
@Equals('application/partial-upload')
|
@Equals('application/partial-upload')
|
||||||
contentType!: string;
|
contentType!: string;
|
||||||
|
|
||||||
@Expose({ name: UploadHeader.UploadLength })
|
@Expose({ name: Header.UploadLength })
|
||||||
@Min(0)
|
@Min(0)
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
@Optional()
|
@Optional()
|
||||||
uploadLength?: number;
|
uploadLength?: number;
|
||||||
|
|
||||||
@Expose({ name: UploadHeader.UploadOffset })
|
@Expose({ name: Header.UploadOffset })
|
||||||
@Min(0)
|
@Min(0)
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
uploadOffset!: number;
|
uploadOffset!: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GetUploadStatusDto extends BaseRufhHeadersDto {
|
export class GetUploadStatusDto extends BaseRufhHeadersDto {}
|
||||||
@Expose({ name: UploadHeader.UploadComplete })
|
|
||||||
@IsEmpty()
|
|
||||||
uploadComplete?: string;
|
|
||||||
|
|
||||||
@Expose({ name: UploadHeader.UploadIncomplete })
|
|
||||||
@IsEmpty()
|
|
||||||
uploadIncomplete?: string;
|
|
||||||
|
|
||||||
@Expose({ name: UploadHeader.UploadOffset })
|
|
||||||
@IsEmpty()
|
|
||||||
uploadOffset?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UploadOkDto {
|
export class UploadOkDto {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
id!: string;
|
id!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const STRUCTURED_TRUE = '?1';
|
||||||
|
const STRUCTURED_FALSE = '?0';
|
||||||
|
|
||||||
|
function isUploadComplete(obj: any): boolean {
|
||||||
|
const uploadComplete = obj[Header.UploadComplete];
|
||||||
|
if (uploadComplete === STRUCTURED_TRUE) {
|
||||||
|
return true;
|
||||||
|
} else if (uploadComplete === STRUCTURED_FALSE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadIncomplete = obj[Header.UploadIncomplete];
|
||||||
|
if (uploadIncomplete === STRUCTURED_TRUE) {
|
||||||
|
return false;
|
||||||
|
} else if (uploadIncomplete === STRUCTURED_FALSE) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
throw new BadRequestException(`Expected valid ${Header.UploadComplete} header`);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { BadRequestException, InternalServerErrorException } from '@nestjs/common';
|
import { BadRequestException, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { StructuredBoolean } from 'src/dtos/asset-upload.dto';
|
|
||||||
import { AssetMetadataKey, AssetStatus, AssetType, AssetVisibility, JobName, JobStatus } from 'src/enum';
|
import { AssetMetadataKey, AssetStatus, AssetType, AssetVisibility, JobName, JobStatus } from 'src/enum';
|
||||||
import { AssetUploadService } from 'src/services/asset-upload.service';
|
import { AssetUploadService } from 'src/services/asset-upload.service';
|
||||||
import { ASSET_CHECKSUM_CONSTRAINT } from 'src/utils/database';
|
import { ASSET_CHECKSUM_CONSTRAINT } from 'src/utils/database';
|
||||||
|
|
@ -28,8 +27,7 @@ describe(AssetUploadService.name, () => {
|
||||||
},
|
},
|
||||||
checksum: Buffer.from('checksum'),
|
checksum: Buffer.from('checksum'),
|
||||||
uploadLength: 1024,
|
uploadLength: 1024,
|
||||||
uploadComplete: StructuredBoolean.True,
|
uploadComplete: true,
|
||||||
uploadIncomplete: StructuredBoolean.False,
|
|
||||||
contentLength: 1024,
|
contentLength: 1024,
|
||||||
isComplete: true,
|
isComplete: true,
|
||||||
version: 8,
|
version: 8,
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ export class AssetUploadService extends BaseService {
|
||||||
|
|
||||||
async startUpload(auth: AuthDto, req: Readable, 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)}`);
|
this.logger.verboseFn(() => `Starting upload: ${JSON.stringify(dto)}`);
|
||||||
const { isComplete, assetData, uploadLength, contentLength, version } = dto;
|
const { uploadComplete, assetData, uploadLength, contentLength, version } = dto;
|
||||||
const { backup } = await this.getConfig({ withCache: true });
|
const { backup } = await this.getConfig({ withCache: true });
|
||||||
|
|
||||||
const asset = await this.onStart(auth, dto);
|
const asset = await this.onStart(auth, dto);
|
||||||
|
|
@ -72,7 +72,7 @@ export class AssetUploadService extends BaseService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isComplete && uploadLength !== contentLength) {
|
if (uploadComplete && uploadLength !== contentLength) {
|
||||||
return this.sendInconsistentLength(res);
|
return this.sendInconsistentLength(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,14 +85,14 @@ export class AssetUploadService extends BaseService {
|
||||||
await this.databaseRepository.withUuidLock(asset.id, async () => {
|
await this.databaseRepository.withUuidLock(asset.id, async () => {
|
||||||
let checksumBuffer: Buffer | undefined;
|
let checksumBuffer: Buffer | undefined;
|
||||||
const writeStream = this.pipe(req, asset.path, contentLength);
|
const writeStream = this.pipe(req, asset.path, contentLength);
|
||||||
if (isComplete) {
|
if (uploadComplete) {
|
||||||
const hash = createHash('sha1');
|
const hash = createHash('sha1');
|
||||||
req.on('data', (data: Buffer) => hash.update(data));
|
req.on('data', (data: Buffer) => hash.update(data));
|
||||||
writeStream.on('finish', () => (checksumBuffer = hash.digest()));
|
writeStream.on('finish', () => (checksumBuffer = hash.digest()));
|
||||||
}
|
}
|
||||||
await new Promise((resolve, reject) => writeStream.on('close', resolve).on('error', reject));
|
await new Promise((resolve, reject) => writeStream.on('close', resolve).on('error', reject));
|
||||||
this.setCompleteHeader(res, dto.version, isComplete);
|
this.setCompleteHeader(res, dto.version, uploadComplete);
|
||||||
if (!isComplete) {
|
if (!uploadComplete) {
|
||||||
res.status(201).set('Location', location).setHeader('Upload-Limit', this.getUploadLimits(backup)).send();
|
res.status(201).set('Location', location).setHeader('Upload-Limit', this.getUploadLimits(backup)).send();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -107,7 +107,7 @@ export class AssetUploadService extends BaseService {
|
||||||
|
|
||||||
resumeUpload(auth: AuthDto, req: Readable, 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)}`);
|
this.logger.verboseFn(() => `Resuming upload for ${id}: ${JSON.stringify(dto)}`);
|
||||||
const { isComplete, uploadLength, uploadOffset, contentLength, version } = dto;
|
const { uploadComplete, uploadLength, uploadOffset, contentLength, version } = dto;
|
||||||
this.setCompleteHeader(res, version, false);
|
this.setCompleteHeader(res, version, false);
|
||||||
this.addRequest(id, req);
|
this.addRequest(id, req);
|
||||||
return this.databaseRepository.withUuidLock(id, async () => {
|
return this.databaseRepository.withUuidLock(id, async () => {
|
||||||
|
|
@ -137,15 +137,15 @@ export class AssetUploadService extends BaseService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contentLength === 0 && !isComplete) {
|
if (contentLength === 0 && !uploadComplete) {
|
||||||
res.status(204).setHeader('Upload-Offset', expectedOffset.toString()).send();
|
res.status(204).setHeader('Upload-Offset', expectedOffset.toString()).send();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const writeStream = this.pipe(req, path, contentLength);
|
const writeStream = this.pipe(req, path, contentLength);
|
||||||
await new Promise((resolve, reject) => writeStream.on('close', resolve).on('error', reject));
|
await new Promise((resolve, reject) => writeStream.on('close', resolve).on('error', reject));
|
||||||
this.setCompleteHeader(res, version, isComplete);
|
this.setCompleteHeader(res, version, uploadComplete);
|
||||||
if (!isComplete) {
|
if (!uploadComplete) {
|
||||||
try {
|
try {
|
||||||
const offset = await this.getCurrentOffset(path);
|
const offset = await this.getCurrentOffset(path);
|
||||||
res.status(204).setHeader('Upload-Offset', offset.toString()).send();
|
res.status(204).setHeader('Upload-Offset', offset.toString()).send();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue