From 2b0b2bb1ae904700287669d0d25f2267ae67ae52 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 3 Feb 2023 10:16:25 -0500 Subject: [PATCH] refactor(server): download file (#1512) * refactor(server): download file * chore: generate open-api and remove unused refs * chore(server): tests * chore: remove unused code --- .../services/image_viewer.service.dart | 6 -- mobile/lib/shared/services/share.service.dart | 2 - mobile/openapi/README.md | Bin 13549 -> 13549 bytes mobile/openapi/doc/AssetApi.md | Bin 32672 -> 32498 bytes mobile/openapi/lib/api/asset_api.dart | Bin 39449 -> 39046 bytes .../openapi/lib/model/album_response_dto.dart | Bin 5815 -> 5660 bytes .../openapi/lib/model/asset_response_dto.dart | Bin 9483 -> 9283 bytes mobile/openapi/test/asset_api_test.dart | Bin 4633 -> 4603 bytes .../src/api-v1/asset/asset.controller.ts | 12 +-- .../src/api-v1/asset/asset.service.spec.ts | 17 +++- .../immich/src/api-v1/asset/asset.service.ts | 85 ++++-------------- server/immich-openapi-specs.json | 18 ---- server/libs/domain/src/auth/auth.core.ts | 5 -- server/libs/domain/src/auth/dto/index.ts | 1 - .../domain/src/auth/dto/jwt-payload.dto.ts | 4 - server/libs/domain/src/index.ts | 1 + server/libs/domain/src/storage/index.ts | 1 + .../domain/src/storage/storage.repository.ts | 13 +++ server/libs/domain/test/index.ts | 1 + .../domain/test/storage.repository.mock.ts | 7 ++ server/libs/infra/src/infra.module.ts | 3 + .../infra/src/storage/filesystem.provider.ts | 18 ++++ server/libs/infra/src/storage/index.ts | 1 + web/src/api/open-api/api.ts | 32 ++----- web/src/api/open-api/base.ts | 2 +- web/src/api/open-api/common.ts | 2 +- web/src/api/open-api/configuration.ts | 2 +- web/src/api/open-api/index.ts | 2 +- .../asset-viewer/asset-viewer.svelte | 6 +- 29 files changed, 101 insertions(+), 140 deletions(-) delete mode 100644 server/libs/domain/src/auth/dto/jwt-payload.dto.ts create mode 100644 server/libs/domain/src/storage/index.ts create mode 100644 server/libs/domain/src/storage/storage.repository.ts create mode 100644 server/libs/domain/test/storage.repository.mock.ts create mode 100644 server/libs/infra/src/storage/filesystem.provider.ts create mode 100644 server/libs/infra/src/storage/index.ts diff --git a/mobile/lib/modules/asset_viewer/services/image_viewer.service.dart b/mobile/lib/modules/asset_viewer/services/image_viewer.service.dart index 8f90f2d77..0d2e72d60 100644 --- a/mobile/lib/modules/asset_viewer/services/image_viewer.service.dart +++ b/mobile/lib/modules/asset_viewer/services/image_viewer.service.dart @@ -26,14 +26,10 @@ class ImageViewerService { if (asset.type == AssetTypeEnum.IMAGE && asset.livePhotoVideoId != null) { var imageResponse = await _apiService.assetApi.downloadFileWithHttpInfo( asset.id, - isThumb: false, - isWeb: false, ); var motionReponse = await _apiService.assetApi.downloadFileWithHttpInfo( asset.livePhotoVideoId!, - isThumb: false, - isWeb: false, ); final AssetEntity? entity; @@ -54,8 +50,6 @@ class ImageViewerService { } else { var res = await _apiService.assetApi.downloadFileWithHttpInfo( asset.id, - isThumb: false, - isWeb: false, ); final AssetEntity? entity; diff --git a/mobile/lib/shared/services/share.service.dart b/mobile/lib/shared/services/share.service.dart index ebf1d9f0f..84d9674ad 100644 --- a/mobile/lib/shared/services/share.service.dart +++ b/mobile/lib/shared/services/share.service.dart @@ -29,8 +29,6 @@ class ShareService { final tempFile = await File('${tempDir.path}/$fileName').create(); final res = await _apiService.assetApi.downloadFileWithHttpInfo( asset.remote!.id, - isThumb: false, - isWeb: false, ); tempFile.writeAsBytesSync(res.bodyBytes); return XFile(tempFile.path); diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 53b96215f5f805b170d2a663979ed2d88d5602d0..341b88421c4e48f4a6d893efeca1f4b6789268e8 100644 GIT binary patch delta 12 TcmaEx`8IRH97e;9a|28OE6fGm delta 12 TcmaEx`8IRH97cnUa|28OE5`-g diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index 4f21fe41d6db04e71b8f685f6366684bf96ea9d9..4695c961a99256bf3f47bd83276459a26ad2c5e6 100644 GIT binary patch delta 67 zcmV-J0KEU8{{iy-0kGg7v%yQ;0h1qQ29uC{1(R)SBa=EfCzD|!6q9v0P?Ikx43k%V Zij$pk7n27mjFWIw=aYXb2(wEiRa)WY8&v=R delta 44 zcmezLmvO;=#tjeTCr4!qZoZjOz%*GzU20Dn%iY~smuCNh#}z+`dwvejd=VKT2TPd2mY|_4Xi^Zl zl$EioWbq)(;zcMx6%EwHC3M6huEjha#j&g&G=0i8ow93(?pokbgOpNu6${8nQ=I;_ zUzZlpk;W(V(6t$LwnNIozx^Aj8ox<8hn#F-Ri4Iw zSx35VkamiE3ioqxQ`c&uG+OK8aX^{=j%eWgKo*5)XzeoaVHA`}lB= z9EE{zl6E}^JaWK-rd{_aStR?m7g94`9y#OxS$!W>IYRx2waO>9*P!O_UG>)`huJ2_ Gr9J=w;CtQx delta 462 zcmbQEvt4(?bY?X!1u)1=NzE(COis+vD9Oky)=|hW$w)2IRH&(e$WPwLtUUQVvyKc{ zF<4SBGeyDHRsl$=K@FMA%c3?}k40;8Fc4R;I8ENlqB8jki{9kFEH0DnSv4o8vC2(u zVil8AP|(OP&r2=xOwr3oEY5JwPf68O&<5Hxc`d8_^B`PoG$|7A6sY{(`x z*^^CvavGZj8^kr6=do!pPCmh|J^3rU^kg9p)yb9|nv+vF?oM{*RGyr{sk?bPr#2Il zzW(Hc+`=I0GPfFt`oWzEq7ryAHTCs@KB?8ThI${&ElMpZEy`0UDJo4xh;M$$Q^d#v zRR;F`{Ti!lRPo6qw%GZUe# pT$q84d=ypcl_lnsrs^PsCr=O&;e>N1`wJCs78hk<1OTG0c`X0{ diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart index 08c7e6cc39317db579e202532064e5776e237d61..2f516e2bdac32185cdf7b47499cad242d848aaec 100644 GIT binary patch delta 477 zcma)3O-mb56lJ2|WIngFd z;_7S!7u~oOgn@#a2I{J`zoOu(C4bN4sTH%=$i8Nz%T#jN9n zP8_gqo$xrI!X?8YT(yGNY6&0I0uIzgjMaHuNY6~y&`MXq(l#J@Eb%K{!6U7LrbI_; z;7IbhX5&(7TW_MRS94B0cu$uCuN%DJK6Ob=!zaCnGkq@a@P0(aOaK1%S}!AOl=0M9 z1v5%h&Nlbi76Ui(I5wW(tFipp@q5CH8ISHqGv+GR&F2`I%lKlh;J4JzGAoJ5Tq$^C z&12V^#i7-}H>-*(%SAqWh9B7){$v+Hawp06++`~HoOe_BS;*mU!Ny!Mz^E7yLU!Gz zCh2Abje8;@vFOoTGbv5~o3#Bwcy4VRqM2O!44B zuwLYy)SKQEMA(C<7eVms(UbIA1jUPZ(`g#5w9tV!@0;(v`R3<`>zi}0a*Cxr`Y(SOh8p@ zm{7;CD7ho?OttY*Vq3z{CUHr#v80XRo{ZmWGx()BsOY6ZBAa`4-;MmMp~oB=mz%&f zePl=q;ab4NjRWc(eHc%4!W(@O-}N#yV;Gji6{C4V&hirB#ykx67kyx~@ZOk3)@-6_ zw&0tycy6|i?4CN^GQVZ;slS3B{U>oWe-@YXXW-{Y@i6}ya|H)UVH{5jFVcm9ui5m% zvC(X?(I7X9n22rLq1(ONZ8Q)Ut*oR+MZQCs5_hDNXv3NM=aECf;zR@#i-hgv)BC00 zeM4QFw;s5we3@=WVNmOp$4vEjPO9n4^4;9wwmxawK($haS}kI;`uSpS@ttRtA3&_v fZ@KM+)&5Jbh;YqaYP0k7H`-HCrzrh-dOq_9Nt>kq diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index 086bbca010c27ade845560cde5d0b01c519314c2..3bf405b8fa5f0bade738d4ca366e69ef93ca0608 100644 GIT binary patch delta 16 YcmbQK@>_YsO4i9MIm9>LW3}c106nh;asU7T delta 16 YcmeyZJX2-EO4i9S{HB`^uv+r~06caEEC2ui diff --git a/server/apps/immich/src/api-v1/asset/asset.controller.ts b/server/apps/immich/src/api-v1/asset/asset.controller.ts index ae924dfa3..d0cf3af6a 100644 --- a/server/apps/immich/src/api-v1/asset/asset.controller.ts +++ b/server/apps/immich/src/api-v1/asset/asset.controller.ts @@ -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 { - 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 }) diff --git a/server/apps/immich/src/api-v1/asset/asset.service.spec.ts b/server/apps/immich/src/api-v1/asset/asset.service.spec.ts index c215e22fe..08c4ed1ac 100644 --- a/server/apps/immich/src/api-v1/asset/asset.service.spec.ts +++ b/server/apps/immich/src/api-v1/asset/asset.service.spec.ts @@ -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; let cryptoMock: jest.Mocked; let jobMock: jest.Mocked; + let storageMock: jest.Mocked; 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'); + }); + }); }); diff --git a/server/apps/immich/src/api-v1/asset/asset.service.ts b/server/apps/immich/src/api-v1/asset/asset.service.ts index e9ee8abbb..35c14e703 100644 --- a/server/apps/immich/src/api-v1/asset/asset.service.ts +++ b/server/apps/immich/src/api-v1/asset/asset.service.ts @@ -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 { + 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 { - const fileReadStream = createReadStream(filePath); - const sha1Hash = createHash('sha1'); - const deferred = new Promise((resolve, reject) => { - sha1Hash.once('error', (err) => reject(err)); - sha1Hash.once('finish', () => resolve(sha1Hash.read())); - }); - - fileReadStream.pipe(sha1Hash); - return deferred; - } - getAssetCountByUserId(authUser: AuthUserDto): Promise { return this._assetRepository.getAssetCountByUserId(authUser.id); } diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 31b73bfeb..c31e7ee28 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -1109,24 +1109,6 @@ "operationId": "downloadFile", "description": "", "parameters": [ - { - "name": "isThumb", - "required": false, - "in": "query", - "schema": { - "title": "Is serve thumbnail (resize) file", - "type": "boolean" - } - }, - { - "name": "isWeb", - "required": false, - "in": "query", - "schema": { - "title": "Is request made from web", - "type": "boolean" - } - }, { "name": "assetId", "required": true, diff --git a/server/libs/domain/src/auth/auth.core.ts b/server/libs/domain/src/auth/auth.core.ts index 9b4d54ac5..d330baad7 100644 --- a/server/libs/domain/src/auth/auth.core.ts +++ b/server/libs/domain/src/auth/auth.core.ts @@ -6,11 +6,6 @@ import { ICryptoRepository } from '../crypto/crypto.repository'; import { LoginResponseDto, mapLoginResponse } from './response-dto'; import { IUserTokenRepository, UserTokenCore } from '../user-token'; -export type JwtValidationResult = { - status: boolean; - userId: string | null; -}; - export class AuthCore { private userTokenCore: UserTokenCore; constructor( diff --git a/server/libs/domain/src/auth/dto/index.ts b/server/libs/domain/src/auth/dto/index.ts index 87c5cc370..323d12f8f 100644 --- a/server/libs/domain/src/auth/dto/index.ts +++ b/server/libs/domain/src/auth/dto/index.ts @@ -1,5 +1,4 @@ export * from './auth-user.dto'; export * from './change-password.dto'; -export * from './jwt-payload.dto'; export * from './login-credential.dto'; export * from './sign-up.dto'; diff --git a/server/libs/domain/src/auth/dto/jwt-payload.dto.ts b/server/libs/domain/src/auth/dto/jwt-payload.dto.ts deleted file mode 100644 index 4f3b7993f..000000000 --- a/server/libs/domain/src/auth/dto/jwt-payload.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -export class JwtPayloadDto { - userId!: string; - email!: string; -} diff --git a/server/libs/domain/src/index.ts b/server/libs/domain/src/index.ts index 3113aa1dd..99456733e 100644 --- a/server/libs/domain/src/index.ts +++ b/server/libs/domain/src/index.ts @@ -8,6 +8,7 @@ export * from './domain.module'; export * from './job'; export * from './oauth'; export * from './share'; +export * from './storage'; export * from './system-config'; export * from './tag'; export * from './user'; diff --git a/server/libs/domain/src/storage/index.ts b/server/libs/domain/src/storage/index.ts new file mode 100644 index 000000000..b6109ead5 --- /dev/null +++ b/server/libs/domain/src/storage/index.ts @@ -0,0 +1 @@ +export * from './storage.repository'; diff --git a/server/libs/domain/src/storage/storage.repository.ts b/server/libs/domain/src/storage/storage.repository.ts new file mode 100644 index 000000000..53d452f31 --- /dev/null +++ b/server/libs/domain/src/storage/storage.repository.ts @@ -0,0 +1,13 @@ +import { ReadStream } from 'fs'; + +export interface ImmichReadStream { + stream: ReadStream; + type: string; + length: number; +} + +export const IStorageRepository = 'IStorageRepository'; + +export interface IStorageRepository { + createReadStream(filepath: string, mimeType: string): Promise; +} diff --git a/server/libs/domain/test/index.ts b/server/libs/domain/test/index.ts index dfec3ffa4..94c4f2750 100644 --- a/server/libs/domain/test/index.ts +++ b/server/libs/domain/test/index.ts @@ -4,6 +4,7 @@ export * from './device-info.repository.mock'; export * from './fixtures'; export * from './job.repository.mock'; export * from './shared-link.repository.mock'; +export * from './storage.repository.mock'; export * from './system-config.repository.mock'; export * from './user-token.repository.mock'; export * from './user.repository.mock'; diff --git a/server/libs/domain/test/storage.repository.mock.ts b/server/libs/domain/test/storage.repository.mock.ts new file mode 100644 index 000000000..6cad33869 --- /dev/null +++ b/server/libs/domain/test/storage.repository.mock.ts @@ -0,0 +1,7 @@ +import { IStorageRepository } from '../src'; + +export const newStorageRepositoryMock = (): jest.Mocked => { + return { + createReadStream: jest.fn(), + }; +}; diff --git a/server/libs/infra/src/infra.module.ts b/server/libs/infra/src/infra.module.ts index df16c1dfd..b71373b1b 100644 --- a/server/libs/infra/src/infra.module.ts +++ b/server/libs/infra/src/infra.module.ts @@ -4,6 +4,7 @@ import { IJobRepository, IKeyRepository, ISharedLinkRepository, + IStorageRepository, ISystemConfigRepository, IUserRepository, QueueName, @@ -29,6 +30,7 @@ import { UserTokenEntity, } from './db'; import { JobRepository } from './job'; +import { FilesystemProvider } from './storage'; const providers: Provider[] = [ { provide: ICryptoRepository, useClass: CryptoRepository }, @@ -36,6 +38,7 @@ const providers: Provider[] = [ { provide: IKeyRepository, useClass: APIKeyRepository }, { provide: IJobRepository, useClass: JobRepository }, { provide: ISharedLinkRepository, useClass: SharedLinkRepository }, + { provide: IStorageRepository, useClass: FilesystemProvider }, { provide: ISystemConfigRepository, useClass: SystemConfigRepository }, { provide: IUserRepository, useClass: UserRepository }, { provide: IUserTokenRepository, useClass: UserTokenRepository }, diff --git a/server/libs/infra/src/storage/filesystem.provider.ts b/server/libs/infra/src/storage/filesystem.provider.ts new file mode 100644 index 000000000..ca2d847f1 --- /dev/null +++ b/server/libs/infra/src/storage/filesystem.provider.ts @@ -0,0 +1,18 @@ +import { ImmichReadStream, IStorageRepository } from '@app/domain'; +import { constants, createReadStream, stat } from 'fs'; +import fs from 'fs/promises'; +import { promisify } from 'util'; + +const fileInfo = promisify(stat); + +export class FilesystemProvider implements IStorageRepository { + async createReadStream(filepath: string, mimeType: string): Promise { + const { size } = await fileInfo(filepath); + await fs.access(filepath, constants.R_OK | constants.W_OK); + return { + stream: createReadStream(filepath), + length: size, + type: mimeType, + }; + } +} diff --git a/server/libs/infra/src/storage/index.ts b/server/libs/infra/src/storage/index.ts new file mode 100644 index 000000000..ae8187f1f --- /dev/null +++ b/server/libs/infra/src/storage/index.ts @@ -0,0 +1 @@ +export * from './filesystem.provider'; diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 8eb39ea7c..6d0ac7036 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.43.0 + * The version of the OpenAPI document: 1.43.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -3729,12 +3729,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration /** * * @param {string} assetId - * @param {boolean} [isThumb] - * @param {boolean} [isWeb] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - downloadFile: async (assetId: string, isThumb?: boolean, isWeb?: boolean, options: AxiosRequestConfig = {}): Promise => { + downloadFile: async (assetId: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'assetId' is not null or undefined assertParamExists('downloadFile', 'assetId', assetId) const localVarPath = `/asset/download/{assetId}` @@ -3754,14 +3752,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration // http bearer authentication required await setBearerAuthToObject(localVarHeaderParameter, configuration) - if (isThumb !== undefined) { - localVarQueryParameter['isThumb'] = isThumb; - } - - if (isWeb !== undefined) { - localVarQueryParameter['isWeb'] = isWeb; - } - setSearchParams(localVarUrlObj, localVarQueryParameter); @@ -4489,13 +4479,11 @@ export const AssetApiFp = function(configuration?: Configuration) { /** * * @param {string} assetId - * @param {boolean} [isThumb] - * @param {boolean} [isWeb] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async downloadFile(assetId: string, isThumb?: boolean, isWeb?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.downloadFile(assetId, isThumb, isWeb, options); + async downloadFile(assetId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.downloadFile(assetId, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -4719,13 +4707,11 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath /** * * @param {string} assetId - * @param {boolean} [isThumb] - * @param {boolean} [isWeb] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - downloadFile(assetId: string, isThumb?: boolean, isWeb?: boolean, options?: any): AxiosPromise { - return localVarFp.downloadFile(assetId, isThumb, isWeb, options).then((request) => request(axios, basePath)); + downloadFile(assetId: string, options?: any): AxiosPromise { + return localVarFp.downloadFile(assetId, options).then((request) => request(axios, basePath)); }, /** * @@ -4939,14 +4925,12 @@ export class AssetApi extends BaseAPI { /** * * @param {string} assetId - * @param {boolean} [isThumb] - * @param {boolean} [isWeb] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AssetApi */ - public downloadFile(assetId: string, isThumb?: boolean, isWeb?: boolean, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).downloadFile(assetId, isThumb, isWeb, options).then((request) => request(this.axios, this.basePath)); + public downloadFile(assetId: string, options?: AxiosRequestConfig) { + return AssetApiFp(this.configuration).downloadFile(assetId, options).then((request) => request(this.axios, this.basePath)); } /** diff --git a/web/src/api/open-api/base.ts b/web/src/api/open-api/base.ts index 58f76152d..f022bd6e3 100644 --- a/web/src/api/open-api/base.ts +++ b/web/src/api/open-api/base.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.43.0 + * The version of the OpenAPI document: 1.43.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/web/src/api/open-api/common.ts b/web/src/api/open-api/common.ts index 2df54adf6..d41e01f32 100644 --- a/web/src/api/open-api/common.ts +++ b/web/src/api/open-api/common.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.43.0 + * The version of the OpenAPI document: 1.43.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/web/src/api/open-api/configuration.ts b/web/src/api/open-api/configuration.ts index bb49e3c84..b12628461 100644 --- a/web/src/api/open-api/configuration.ts +++ b/web/src/api/open-api/configuration.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.43.0 + * The version of the OpenAPI document: 1.43.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/web/src/api/open-api/index.ts b/web/src/api/open-api/index.ts index 9b6a04331..052d483f1 100644 --- a/web/src/api/open-api/index.ts +++ b/web/src/api/open-api/index.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.43.0 + * The version of the OpenAPI document: 1.43.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 36819c747..219935943 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -136,10 +136,8 @@ $downloadAssets[imageFileName] = 0; - const { data, status } = await api.assetApi.downloadFile(assetId, false, false, { - params: { - key - }, + const { data, status } = await api.assetApi.downloadFile(assetId, { + params: { key }, responseType: 'blob', onDownloadProgress: (progressEvent) => { if (progressEvent.lengthComputable) {