refactor(server): download file (#1512)

* refactor(server): download file

* chore: generate open-api and remove unused refs

* chore(server): tests

* chore: remove unused code
This commit is contained in:
Jason Rasmussen
2023-02-03 10:16:25 -05:00
committed by GitHub
parent e39507552f
commit 2b0b2bb1ae
29 changed files with 210 additions and 291 deletions

View File

@@ -15,6 +15,7 @@ import {
Put,
UploadedFiles,
Patch,
StreamableFile,
} from '@nestjs/common';
import { Authenticated } from '../../decorators/authenticated.decorator';
import { AssetService } from './asset.service';
@@ -28,7 +29,7 @@ import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger';
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
import { AssetResponseDto } from '@app/domain';
import { AssetResponseDto, ImmichReadStream } from '@app/domain';
import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
import { AssetFileUploadDto } from './dto/asset-file-upload.dto';
import { CreateAssetDto, mapToUploadFile } from './dto/create-asset.dto';
@@ -55,6 +56,10 @@ import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto
import { AssetSearchDto } from './dto/asset-search.dto';
import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config';
function asStreamableFile({ stream, type, length }: ImmichReadStream) {
return new StreamableFile(stream, { type, length });
}
@ApiBearerAuth()
@ApiTags('Asset')
@Controller('asset')
@@ -103,12 +108,9 @@ export class AssetController {
async downloadFile(
@GetAuthUser() authUser: AuthUserDto,
@Response({ passthrough: true }) res: Res,
@Query(new ValidationPipe({ transform: true })) query: ServeFileDto,
@Param('assetId') assetId: string,
): Promise<any> {
this.assetService.checkDownloadAccess(authUser);
await this.assetService.checkAssetsAccess(authUser, [assetId]);
return this.assetService.downloadFile(query, assetId, res);
return this.assetService.downloadFile(authUser, assetId).then(asStreamableFile);
}
@Authenticated({ isShared: true })

View File

@@ -9,12 +9,13 @@ import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-use
import { DownloadService } from '../../modules/download/download.service';
import { AlbumRepository, IAlbumRepository } from '../album/album-repository';
import { StorageService } from '@app/storage';
import { ICryptoRepository, IJobRepository, ISharedLinkRepository, JobName } from '@app/domain';
import { ICryptoRepository, IJobRepository, ISharedLinkRepository, IStorageRepository, JobName } from '@app/domain';
import {
authStub,
newCryptoRepositoryMock,
newJobRepositoryMock,
newSharedLinkRepositoryMock,
newStorageRepositoryMock,
sharedLinkResponseStub,
sharedLinkStub,
} from '@app/domain/../test';
@@ -110,6 +111,7 @@ describe('AssetService', () => {
let sharedLinkRepositoryMock: jest.Mocked<ISharedLinkRepository>;
let cryptoMock: jest.Mocked<ICryptoRepository>;
let jobMock: jest.Mocked<IJobRepository>;
let storageMock: jest.Mocked<IStorageRepository>;
beforeEach(() => {
assetRepositoryMock = {
@@ -154,6 +156,7 @@ describe('AssetService', () => {
sharedLinkRepositoryMock = newSharedLinkRepositoryMock();
jobMock = newJobRepositoryMock();
cryptoMock = newCryptoRepositoryMock();
storageMock = newStorageRepositoryMock();
sut = new AssetService(
assetRepositoryMock,
@@ -164,6 +167,7 @@ describe('AssetService', () => {
sharedLinkRepositoryMock,
jobMock,
cryptoMock,
storageMock,
);
});
@@ -413,4 +417,15 @@ describe('AssetService', () => {
expect(() => sut.checkDownloadAccess(authStub.readonlySharedLink)).toThrow(ForbiddenException);
});
});
describe('downloadFile', () => {
it('should download a single file', async () => {
assetRepositoryMock.countByIdAndUser.mockResolvedValue(1);
assetRepositoryMock.get.mockResolvedValue(_getAsset_1());
await sut.downloadFile(authStub.admin, 'id_1');
expect(storageMock.createReadStream).toHaveBeenCalledWith('fake_path/asset_1.jpeg', 'image/jpeg');
});
});
});

View File

@@ -10,7 +10,6 @@ import {
StreamableFile,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { createHash } from 'node:crypto';
import { QueryFailedError, Repository } from 'typeorm';
import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { AssetEntity, AssetType, SharedLinkType } from '@app/infra';
@@ -23,7 +22,14 @@ import { SearchAssetDto } from './dto/search-asset.dto';
import fs from 'fs/promises';
import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
import { AssetResponseDto, JobName, mapAsset, mapAssetWithoutExif } from '@app/domain';
import {
AssetResponseDto,
ImmichReadStream,
IStorageRepository,
JobName,
mapAsset,
mapAssetWithoutExif,
} from '@app/domain';
import { CreateAssetDto, UploadFile } from './dto/create-asset.dto';
import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto';
@@ -73,6 +79,7 @@ export class AssetService {
@Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
@Inject(IStorageRepository) private storage: IStorageRepository,
) {
this.assetCore = new AssetCore(_assetRepository, jobRepository, storageService);
this.shareCore = new ShareCore(sharedLinkRepository, cryptoRepository);
@@ -189,62 +196,21 @@ export class AssetService {
return this.downloadService.downloadArchive(`immich-${now}`, assetToDownload);
}
public async downloadFile(query: ServeFileDto, assetId: string, res: Res) {
public async downloadFile(authUser: AuthUserDto, assetId: string): Promise<ImmichReadStream> {
this.checkDownloadAccess(authUser);
await this.checkAssetsAccess(authUser, [assetId]);
try {
let fileReadStream = null;
const asset = await this._assetRepository.getById(assetId);
// Download Video
if (asset.type === AssetType.VIDEO) {
const { size } = await fileInfo(asset.originalPath);
res.set({
'Content-Type': asset.mimeType,
'Content-Length': size,
});
await fs.access(asset.originalPath, constants.R_OK | constants.W_OK);
fileReadStream = createReadStream(asset.originalPath);
} else {
// Download Image
if (!query.isThumb) {
/**
* Download Image Original File
*/
const { size } = await fileInfo(asset.originalPath);
res.set({
'Content-Type': asset.mimeType,
'Content-Length': size,
});
await fs.access(asset.originalPath, constants.R_OK | constants.W_OK);
fileReadStream = createReadStream(asset.originalPath);
} else {
/**
* Download Image Resize File
*/
if (!asset.resizePath) {
throw new NotFoundException('resizePath not set');
}
const { size } = await fileInfo(asset.resizePath);
res.set({
'Content-Type': 'image/jpeg',
'Content-Length': size,
});
await fs.access(asset.resizePath, constants.R_OK | constants.W_OK);
fileReadStream = createReadStream(asset.resizePath);
}
const asset = await this._assetRepository.get(assetId);
if (asset && asset.originalPath && asset.mimeType) {
return this.storage.createReadStream(asset.originalPath, asset.mimeType);
}
return new StreamableFile(fileReadStream);
} catch (e) {
Logger.error(`Error download asset ${e}`, 'downloadFile');
throw new InternalServerErrorException(`Failed to download asset ${e}`, 'DownloadFile');
}
throw new NotFoundException();
}
public async getAssetThumbnail(
@@ -255,8 +221,7 @@ export class AssetService {
) {
let fileReadStream: ReadStream;
const asset = await this.assetRepository.findOne({ where: { id: assetId } });
const asset = await this._assetRepository.get(assetId);
if (!asset) {
throw new NotFoundException('Asset not found');
}
@@ -584,18 +549,6 @@ export class AssetService {
return this._assetRepository.getAssetByChecksum(userId, checksum);
}
calculateChecksum(filePath: string): Promise<Buffer> {
const fileReadStream = createReadStream(filePath);
const sha1Hash = createHash('sha1');
const deferred = new Promise<Buffer>((resolve, reject) => {
sha1Hash.once('error', (err) => reject(err));
sha1Hash.once('finish', () => resolve(sha1Hash.read()));
});
fileReadStream.pipe(sha1Hash);
return deferred;
}
getAssetCountByUserId(authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
return this._assetRepository.getAssetCountByUserId(authUser.id);
}