mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05:00 
			
		
		
		
	refactor: asset v1, app.utils (#8152)
This commit is contained in:
		
							parent
							
								
									87ccba7f9d
								
							
						
					
					
						commit
						382b63954c
					
				@ -156,10 +156,10 @@
 | 
				
			|||||||
    "coverageDirectory": "./coverage",
 | 
					    "coverageDirectory": "./coverage",
 | 
				
			||||||
    "coverageThreshold": {
 | 
					    "coverageThreshold": {
 | 
				
			||||||
      "./src/": {
 | 
					      "./src/": {
 | 
				
			||||||
        "branches": 75,
 | 
					        "branches": 70,
 | 
				
			||||||
        "functions": 80,
 | 
					        "functions": 75,
 | 
				
			||||||
        "lines": 85,
 | 
					        "lines": 80,
 | 
				
			||||||
        "statements": 85
 | 
					        "statements": 80
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "testEnvironment": "node",
 | 
					    "testEnvironment": "node",
 | 
				
			||||||
 | 
				
			|||||||
@ -6,12 +6,11 @@ import { existsSync } from 'node:fs';
 | 
				
			|||||||
import sirv from 'sirv';
 | 
					import sirv from 'sirv';
 | 
				
			||||||
import { ApiModule } from 'src/apps/api.module';
 | 
					import { ApiModule } from 'src/apps/api.module';
 | 
				
			||||||
import { ApiService } from 'src/apps/api.service';
 | 
					import { ApiService } from 'src/apps/api.service';
 | 
				
			||||||
import { excludePaths } from 'src/config';
 | 
					import { WEB_ROOT, envName, excludePaths, isDev, serverVersion } from 'src/constants';
 | 
				
			||||||
import { WEB_ROOT, envName, isDev, serverVersion } from 'src/constants';
 | 
					 | 
				
			||||||
import { useSwagger } from 'src/immich/app.utils';
 | 
					 | 
				
			||||||
import { WebSocketAdapter } from 'src/middleware/websocket.adapter';
 | 
					import { WebSocketAdapter } from 'src/middleware/websocket.adapter';
 | 
				
			||||||
import { otelSDK } from 'src/utils/instrumentation';
 | 
					import { otelSDK } from 'src/utils/instrumentation';
 | 
				
			||||||
import { ImmichLogger } from 'src/utils/logger';
 | 
					import { ImmichLogger } from 'src/utils/logger';
 | 
				
			||||||
 | 
					import { useSwagger } from 'src/utils/misc';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const logger = new ImmichLogger('ImmichServer');
 | 
					const logger = new ImmichLogger('ImmichServer');
 | 
				
			||||||
const port = Number(process.env.SERVER_PORT) || 3001;
 | 
					const port = Number(process.env.SERVER_PORT) || 3001;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,13 +1,13 @@
 | 
				
			|||||||
import { Module, OnModuleInit, ValidationPipe } from '@nestjs/common';
 | 
					import { Module, OnModuleInit, ValidationPipe } from '@nestjs/common';
 | 
				
			||||||
import { APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
 | 
					import { APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
 | 
				
			||||||
import { ScheduleModule } from '@nestjs/schedule';
 | 
					import { ScheduleModule } from '@nestjs/schedule';
 | 
				
			||||||
import { TypeOrmModule } from '@nestjs/typeorm';
 | 
					 | 
				
			||||||
import { ApiService } from 'src/apps/api.service';
 | 
					import { ApiService } from 'src/apps/api.service';
 | 
				
			||||||
import { AppModule } from 'src/apps/app.module';
 | 
					import { AppModule } from 'src/apps/app.module';
 | 
				
			||||||
import { ActivityController } from 'src/controllers/activity.controller';
 | 
					import { ActivityController } from 'src/controllers/activity.controller';
 | 
				
			||||||
import { AlbumController } from 'src/controllers/album.controller';
 | 
					import { AlbumController } from 'src/controllers/album.controller';
 | 
				
			||||||
import { APIKeyController } from 'src/controllers/api-key.controller';
 | 
					import { APIKeyController } from 'src/controllers/api-key.controller';
 | 
				
			||||||
import { AppController } from 'src/controllers/app.controller';
 | 
					import { AppController } from 'src/controllers/app.controller';
 | 
				
			||||||
 | 
					import { AssetControllerV1 } from 'src/controllers/asset-v1.controller';
 | 
				
			||||||
import { AssetController, AssetsController } from 'src/controllers/asset.controller';
 | 
					import { AssetController, AssetsController } from 'src/controllers/asset.controller';
 | 
				
			||||||
import { AuditController } from 'src/controllers/audit.controller';
 | 
					import { AuditController } from 'src/controllers/audit.controller';
 | 
				
			||||||
import { AuthController } from 'src/controllers/auth.controller';
 | 
					import { AuthController } from 'src/controllers/auth.controller';
 | 
				
			||||||
@ -25,11 +25,6 @@ import { SystemConfigController } from 'src/controllers/system-config.controller
 | 
				
			|||||||
import { TagController } from 'src/controllers/tag.controller';
 | 
					import { TagController } from 'src/controllers/tag.controller';
 | 
				
			||||||
import { TrashController } from 'src/controllers/trash.controller';
 | 
					import { TrashController } from 'src/controllers/trash.controller';
 | 
				
			||||||
import { UserController } from 'src/controllers/user.controller';
 | 
					import { UserController } from 'src/controllers/user.controller';
 | 
				
			||||||
import { AssetEntity } from 'src/entities/asset.entity';
 | 
					 | 
				
			||||||
import { ExifEntity } from 'src/entities/exif.entity';
 | 
					 | 
				
			||||||
import { AssetRepositoryV1, IAssetRepositoryV1 } from 'src/immich/api-v1/asset/asset-repository';
 | 
					 | 
				
			||||||
import { AssetController as AssetControllerV1 } from 'src/immich/api-v1/asset/asset.controller';
 | 
					 | 
				
			||||||
import { AssetService as AssetServiceV1 } from 'src/immich/api-v1/asset/asset.service';
 | 
					 | 
				
			||||||
import { AuthGuard } from 'src/middleware/auth.guard';
 | 
					import { AuthGuard } from 'src/middleware/auth.guard';
 | 
				
			||||||
import { ErrorInterceptor } from 'src/middleware/error.interceptor';
 | 
					import { ErrorInterceptor } from 'src/middleware/error.interceptor';
 | 
				
			||||||
import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor';
 | 
					import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor';
 | 
				
			||||||
@ -39,7 +34,6 @@ import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor';
 | 
				
			|||||||
    //
 | 
					    //
 | 
				
			||||||
    AppModule,
 | 
					    AppModule,
 | 
				
			||||||
    ScheduleModule.forRoot(),
 | 
					    ScheduleModule.forRoot(),
 | 
				
			||||||
    TypeOrmModule.forFeature([AssetEntity, ExifEntity]),
 | 
					 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  controllers: [
 | 
					  controllers: [
 | 
				
			||||||
    ActivityController,
 | 
					    ActivityController,
 | 
				
			||||||
@ -67,19 +61,17 @@ import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor';
 | 
				
			|||||||
    PersonController,
 | 
					    PersonController,
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  providers: [
 | 
					  providers: [
 | 
				
			||||||
 | 
					    ApiService,
 | 
				
			||||||
 | 
					    FileUploadInterceptor,
 | 
				
			||||||
    { provide: APP_PIPE, useValue: new ValidationPipe({ transform: true, whitelist: true }) },
 | 
					    { provide: APP_PIPE, useValue: new ValidationPipe({ transform: true, whitelist: true }) },
 | 
				
			||||||
    { provide: APP_INTERCEPTOR, useClass: ErrorInterceptor },
 | 
					    { provide: APP_INTERCEPTOR, useClass: ErrorInterceptor },
 | 
				
			||||||
    { provide: APP_GUARD, useClass: AuthGuard },
 | 
					    { provide: APP_GUARD, useClass: AuthGuard },
 | 
				
			||||||
    { provide: IAssetRepositoryV1, useClass: AssetRepositoryV1 },
 | 
					 | 
				
			||||||
    ApiService,
 | 
					 | 
				
			||||||
    AssetServiceV1,
 | 
					 | 
				
			||||||
    FileUploadInterceptor,
 | 
					 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class ApiModule implements OnModuleInit {
 | 
					export class ApiModule implements OnModuleInit {
 | 
				
			||||||
  constructor(private appService: ApiService) {}
 | 
					  constructor(private apiService: ApiService) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async onModuleInit() {
 | 
					  async onModuleInit() {
 | 
				
			||||||
    await this.appService.init();
 | 
					    await this.apiService.init();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -13,6 +13,7 @@ import { IActivityRepository } from 'src/interfaces/activity.interface';
 | 
				
			|||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
 | 
					import { IAlbumRepository } from 'src/interfaces/album.interface';
 | 
				
			||||||
import { IKeyRepository } from 'src/interfaces/api-key.interface';
 | 
					import { IKeyRepository } from 'src/interfaces/api-key.interface';
 | 
				
			||||||
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
 | 
					import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
 | 
				
			||||||
 | 
					import { IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface';
 | 
				
			||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
 | 
					import { IAssetRepository } from 'src/interfaces/asset.interface';
 | 
				
			||||||
import { IAuditRepository } from 'src/interfaces/audit.interface';
 | 
					import { IAuditRepository } from 'src/interfaces/audit.interface';
 | 
				
			||||||
import { ICommunicationRepository } from 'src/interfaces/communication.interface';
 | 
					import { ICommunicationRepository } from 'src/interfaces/communication.interface';
 | 
				
			||||||
@ -40,6 +41,7 @@ import { ActivityRepository } from 'src/repositories/activity.repository';
 | 
				
			|||||||
import { AlbumRepository } from 'src/repositories/album.repository';
 | 
					import { AlbumRepository } from 'src/repositories/album.repository';
 | 
				
			||||||
import { ApiKeyRepository } from 'src/repositories/api-key.repository';
 | 
					import { ApiKeyRepository } from 'src/repositories/api-key.repository';
 | 
				
			||||||
import { AssetStackRepository } from 'src/repositories/asset-stack.repository';
 | 
					import { AssetStackRepository } from 'src/repositories/asset-stack.repository';
 | 
				
			||||||
 | 
					import { AssetRepositoryV1 } from 'src/repositories/asset-v1.repository';
 | 
				
			||||||
import { AssetRepository } from 'src/repositories/asset.repository';
 | 
					import { AssetRepository } from 'src/repositories/asset.repository';
 | 
				
			||||||
import { AuditRepository } from 'src/repositories/audit.repository';
 | 
					import { AuditRepository } from 'src/repositories/audit.repository';
 | 
				
			||||||
import { CommunicationRepository } from 'src/repositories/communication.repository';
 | 
					import { CommunicationRepository } from 'src/repositories/communication.repository';
 | 
				
			||||||
@ -65,6 +67,7 @@ import { UserRepository } from 'src/repositories/user.repository';
 | 
				
			|||||||
import { ActivityService } from 'src/services/activity.service';
 | 
					import { ActivityService } from 'src/services/activity.service';
 | 
				
			||||||
import { AlbumService } from 'src/services/album.service';
 | 
					import { AlbumService } from 'src/services/album.service';
 | 
				
			||||||
import { APIKeyService } from 'src/services/api-key.service';
 | 
					import { APIKeyService } from 'src/services/api-key.service';
 | 
				
			||||||
 | 
					import { AssetServiceV1 } from 'src/services/asset-v1.service';
 | 
				
			||||||
import { AssetService } from 'src/services/asset.service';
 | 
					import { AssetService } from 'src/services/asset.service';
 | 
				
			||||||
import { AuditService } from 'src/services/audit.service';
 | 
					import { AuditService } from 'src/services/audit.service';
 | 
				
			||||||
import { AuthService } from 'src/services/auth.service';
 | 
					import { AuthService } from 'src/services/auth.service';
 | 
				
			||||||
@ -94,6 +97,7 @@ const services: Provider[] = [
 | 
				
			|||||||
  ActivityService,
 | 
					  ActivityService,
 | 
				
			||||||
  AlbumService,
 | 
					  AlbumService,
 | 
				
			||||||
  AssetService,
 | 
					  AssetService,
 | 
				
			||||||
 | 
					  AssetServiceV1,
 | 
				
			||||||
  AuditService,
 | 
					  AuditService,
 | 
				
			||||||
  AuthService,
 | 
					  AuthService,
 | 
				
			||||||
  DatabaseService,
 | 
					  DatabaseService,
 | 
				
			||||||
@ -122,6 +126,7 @@ const repositories: Provider[] = [
 | 
				
			|||||||
  { provide: IAccessRepository, useClass: AccessRepository },
 | 
					  { provide: IAccessRepository, useClass: AccessRepository },
 | 
				
			||||||
  { provide: IAlbumRepository, useClass: AlbumRepository },
 | 
					  { provide: IAlbumRepository, useClass: AlbumRepository },
 | 
				
			||||||
  { provide: IAssetRepository, useClass: AssetRepository },
 | 
					  { provide: IAssetRepository, useClass: AssetRepository },
 | 
				
			||||||
 | 
					  { provide: IAssetRepositoryV1, useClass: AssetRepositoryV1 },
 | 
				
			||||||
  { provide: IAssetStackRepository, useClass: AssetStackRepository },
 | 
					  { provide: IAssetStackRepository, useClass: AssetStackRepository },
 | 
				
			||||||
  { provide: IAuditRepository, useClass: AuditRepository },
 | 
					  { provide: IAuditRepository, useClass: AuditRepository },
 | 
				
			||||||
  { provide: ICommunicationRepository, useClass: CommunicationRepository },
 | 
					  { provide: ICommunicationRepository, useClass: CommunicationRepository },
 | 
				
			||||||
 | 
				
			|||||||
@ -69,5 +69,3 @@ export const bullConfig: QueueOptions = {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const bullQueues: RegisterQueueOptions[] = Object.values(QueueName).map((name) => ({ name }));
 | 
					export const bullQueues: RegisterQueueOptions[] = Object.values(QueueName).map((name) => ({ name }));
 | 
				
			||||||
 | 
					 | 
				
			||||||
export const excludePaths = ['/.well-known/immich', '/custom.css', '/favicon.ico'];
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -35,6 +35,8 @@ export enum AuthType {
 | 
				
			|||||||
  OAUTH = 'oauth',
 | 
					  OAUTH = 'oauth',
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const excludePaths = ['/.well-known/immich', '/custom.css', '/favicon.ico'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const FACE_THUMBNAIL_SIZE = 250;
 | 
					export const FACE_THUMBNAIL_SIZE = 250;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const supportedYearTokens = ['y', 'yy'];
 | 
					export const supportedYearTokens = ['y', 'yy'];
 | 
				
			||||||
 | 
				
			|||||||
@ -16,22 +16,26 @@ import {
 | 
				
			|||||||
import { ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger';
 | 
					import { ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger';
 | 
				
			||||||
import { NextFunction, Response } from 'express';
 | 
					import { NextFunction, Response } from 'express';
 | 
				
			||||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
 | 
					import { AssetResponseDto } from 'src/dtos/asset-response.dto';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  AssetBulkUploadCheckResponseDto,
 | 
				
			||||||
 | 
					  AssetFileUploadResponseDto,
 | 
				
			||||||
 | 
					  CheckExistingAssetsResponseDto,
 | 
				
			||||||
 | 
					  CuratedLocationsResponseDto,
 | 
				
			||||||
 | 
					  CuratedObjectsResponseDto,
 | 
				
			||||||
 | 
					} from 'src/dtos/asset-v1-response.dto';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  AssetBulkUploadCheckDto,
 | 
				
			||||||
 | 
					  AssetSearchDto,
 | 
				
			||||||
 | 
					  CheckExistingAssetsDto,
 | 
				
			||||||
 | 
					  CreateAssetDto,
 | 
				
			||||||
 | 
					  GetAssetThumbnailDto,
 | 
				
			||||||
 | 
					  ServeFileDto,
 | 
				
			||||||
 | 
					} from 'src/dtos/asset-v1.dto';
 | 
				
			||||||
import { AuthDto } from 'src/dtos/auth.dto';
 | 
					import { AuthDto } from 'src/dtos/auth.dto';
 | 
				
			||||||
import { AssetService as AssetServiceV1 } from 'src/immich/api-v1/asset/asset.service';
 | 
					 | 
				
			||||||
import { AssetBulkUploadCheckDto } from 'src/immich/api-v1/asset/dto/asset-check.dto';
 | 
					 | 
				
			||||||
import { AssetSearchDto } from 'src/immich/api-v1/asset/dto/asset-search.dto';
 | 
					 | 
				
			||||||
import { CheckExistingAssetsDto } from 'src/immich/api-v1/asset/dto/check-existing-assets.dto';
 | 
					 | 
				
			||||||
import { CreateAssetDto } from 'src/immich/api-v1/asset/dto/create-asset.dto';
 | 
					 | 
				
			||||||
import { GetAssetThumbnailDto } from 'src/immich/api-v1/asset/dto/get-asset-thumbnail.dto';
 | 
					 | 
				
			||||||
import { ServeFileDto } from 'src/immich/api-v1/asset/dto/serve-file.dto';
 | 
					 | 
				
			||||||
import { AssetBulkUploadCheckResponseDto } from 'src/immich/api-v1/asset/response-dto/asset-check-response.dto';
 | 
					 | 
				
			||||||
import { AssetFileUploadResponseDto } from 'src/immich/api-v1/asset/response-dto/asset-file-upload-response.dto';
 | 
					 | 
				
			||||||
import { CheckExistingAssetsResponseDto } from 'src/immich/api-v1/asset/response-dto/check-existing-assets-response.dto';
 | 
					 | 
				
			||||||
import { CuratedLocationsResponseDto } from 'src/immich/api-v1/asset/response-dto/curated-locations-response.dto';
 | 
					 | 
				
			||||||
import { CuratedObjectsResponseDto } from 'src/immich/api-v1/asset/response-dto/curated-objects-response.dto';
 | 
					 | 
				
			||||||
import { sendFile } from 'src/immich/app.utils';
 | 
					 | 
				
			||||||
import { Auth, Authenticated, FileResponse, SharedLinkRoute } from 'src/middleware/auth.guard';
 | 
					import { Auth, Authenticated, FileResponse, SharedLinkRoute } from 'src/middleware/auth.guard';
 | 
				
			||||||
import { FileUploadInterceptor, ImmichFile, Route, mapToUploadFile } from 'src/middleware/file-upload.interceptor';
 | 
					import { FileUploadInterceptor, ImmichFile, Route, mapToUploadFile } from 'src/middleware/file-upload.interceptor';
 | 
				
			||||||
 | 
					import { AssetServiceV1 } from 'src/services/asset-v1.service';
 | 
				
			||||||
 | 
					import { sendFile } from 'src/utils/file';
 | 
				
			||||||
import { FileNotEmptyValidator, UUIDParamDto } from 'src/validation';
 | 
					import { FileNotEmptyValidator, UUIDParamDto } from 'src/validation';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface UploadFiles {
 | 
					interface UploadFiles {
 | 
				
			||||||
@ -43,8 +47,8 @@ interface UploadFiles {
 | 
				
			|||||||
@ApiTags('Asset')
 | 
					@ApiTags('Asset')
 | 
				
			||||||
@Controller(Route.ASSET)
 | 
					@Controller(Route.ASSET)
 | 
				
			||||||
@Authenticated()
 | 
					@Authenticated()
 | 
				
			||||||
export class AssetController {
 | 
					export class AssetControllerV1 {
 | 
				
			||||||
  constructor(private serviceV1: AssetServiceV1) {}
 | 
					  constructor(private service: AssetServiceV1) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @SharedLinkRoute()
 | 
					  @SharedLinkRoute()
 | 
				
			||||||
  @Post('upload')
 | 
					  @Post('upload')
 | 
				
			||||||
@ -73,7 +77,7 @@ export class AssetController {
 | 
				
			|||||||
      sidecarFile = mapToUploadFile(_sidecarFile);
 | 
					      sidecarFile = mapToUploadFile(_sidecarFile);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const responseDto = await this.serviceV1.uploadFile(auth, dto, file, livePhotoFile, sidecarFile);
 | 
					    const responseDto = await this.service.uploadFile(auth, dto, file, livePhotoFile, sidecarFile);
 | 
				
			||||||
    if (responseDto.duplicate) {
 | 
					    if (responseDto.duplicate) {
 | 
				
			||||||
      res.status(HttpStatus.OK);
 | 
					      res.status(HttpStatus.OK);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -91,7 +95,7 @@ export class AssetController {
 | 
				
			|||||||
    @Param() { id }: UUIDParamDto,
 | 
					    @Param() { id }: UUIDParamDto,
 | 
				
			||||||
    @Query() dto: ServeFileDto,
 | 
					    @Query() dto: ServeFileDto,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    await sendFile(res, next, () => this.serviceV1.serveFile(auth, id, dto));
 | 
					    await sendFile(res, next, () => this.service.serveFile(auth, id, dto));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @SharedLinkRoute()
 | 
					  @SharedLinkRoute()
 | 
				
			||||||
@ -104,22 +108,22 @@ export class AssetController {
 | 
				
			|||||||
    @Param() { id }: UUIDParamDto,
 | 
					    @Param() { id }: UUIDParamDto,
 | 
				
			||||||
    @Query() dto: GetAssetThumbnailDto,
 | 
					    @Query() dto: GetAssetThumbnailDto,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    await sendFile(res, next, () => this.serviceV1.serveThumbnail(auth, id, dto));
 | 
					    await sendFile(res, next, () => this.service.serveThumbnail(auth, id, dto));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Get('/curated-objects')
 | 
					  @Get('/curated-objects')
 | 
				
			||||||
  getCuratedObjects(@Auth() auth: AuthDto): Promise<CuratedObjectsResponseDto[]> {
 | 
					  getCuratedObjects(@Auth() auth: AuthDto): Promise<CuratedObjectsResponseDto[]> {
 | 
				
			||||||
    return this.serviceV1.getCuratedObject(auth);
 | 
					    return this.service.getCuratedObject(auth);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Get('/curated-locations')
 | 
					  @Get('/curated-locations')
 | 
				
			||||||
  getCuratedLocations(@Auth() auth: AuthDto): Promise<CuratedLocationsResponseDto[]> {
 | 
					  getCuratedLocations(@Auth() auth: AuthDto): Promise<CuratedLocationsResponseDto[]> {
 | 
				
			||||||
    return this.serviceV1.getCuratedLocation(auth);
 | 
					    return this.service.getCuratedLocation(auth);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Get('/search-terms')
 | 
					  @Get('/search-terms')
 | 
				
			||||||
  getAssetSearchTerms(@Auth() auth: AuthDto): Promise<string[]> {
 | 
					  getAssetSearchTerms(@Auth() auth: AuthDto): Promise<string[]> {
 | 
				
			||||||
    return this.serviceV1.getAssetSearchTerm(auth);
 | 
					    return this.service.getAssetSearchTerm(auth);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
@ -133,7 +137,7 @@ export class AssetController {
 | 
				
			|||||||
    schema: { type: 'string' },
 | 
					    schema: { type: 'string' },
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
  getAllAssets(@Auth() auth: AuthDto, @Query() dto: AssetSearchDto): Promise<AssetResponseDto[]> {
 | 
					  getAllAssets(@Auth() auth: AuthDto, @Query() dto: AssetSearchDto): Promise<AssetResponseDto[]> {
 | 
				
			||||||
    return this.serviceV1.getAllAssets(auth, dto);
 | 
					    return this.service.getAllAssets(auth, dto);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
@ -145,7 +149,7 @@ export class AssetController {
 | 
				
			|||||||
    @Auth() auth: AuthDto,
 | 
					    @Auth() auth: AuthDto,
 | 
				
			||||||
    @Body() dto: CheckExistingAssetsDto,
 | 
					    @Body() dto: CheckExistingAssetsDto,
 | 
				
			||||||
  ): Promise<CheckExistingAssetsResponseDto> {
 | 
					  ): Promise<CheckExistingAssetsResponseDto> {
 | 
				
			||||||
    return this.serviceV1.checkExistingAssets(auth, dto);
 | 
					    return this.service.checkExistingAssets(auth, dto);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
@ -157,6 +161,6 @@ export class AssetController {
 | 
				
			|||||||
    @Auth() auth: AuthDto,
 | 
					    @Auth() auth: AuthDto,
 | 
				
			||||||
    @Body() dto: AssetBulkUploadCheckDto,
 | 
					    @Body() dto: AssetBulkUploadCheckDto,
 | 
				
			||||||
  ): Promise<AssetBulkUploadCheckResponseDto> {
 | 
					  ): Promise<AssetBulkUploadCheckResponseDto> {
 | 
				
			||||||
    return this.serviceV1.bulkUploadCheck(auth, dto);
 | 
					    return this.service.bulkUploadCheck(auth, dto);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -4,9 +4,9 @@ import { NextFunction, Response } from 'express';
 | 
				
			|||||||
import { AssetIdsDto } from 'src/dtos/asset.dto';
 | 
					import { AssetIdsDto } from 'src/dtos/asset.dto';
 | 
				
			||||||
import { AuthDto } from 'src/dtos/auth.dto';
 | 
					import { AuthDto } from 'src/dtos/auth.dto';
 | 
				
			||||||
import { DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto';
 | 
					import { DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto';
 | 
				
			||||||
import { asStreamableFile, sendFile } from 'src/immich/app.utils';
 | 
					 | 
				
			||||||
import { Auth, Authenticated, FileResponse, SharedLinkRoute } from 'src/middleware/auth.guard';
 | 
					import { Auth, Authenticated, FileResponse, SharedLinkRoute } from 'src/middleware/auth.guard';
 | 
				
			||||||
import { DownloadService } from 'src/services/download.service';
 | 
					import { DownloadService } from 'src/services/download.service';
 | 
				
			||||||
 | 
					import { asStreamableFile, sendFile } from 'src/utils/file';
 | 
				
			||||||
import { UUIDParamDto } from 'src/validation';
 | 
					import { UUIDParamDto } from 'src/validation';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ApiTags('Download')
 | 
					@ApiTags('Download')
 | 
				
			||||||
 | 
				
			|||||||
@ -15,9 +15,9 @@ import {
 | 
				
			|||||||
  PersonStatisticsResponseDto,
 | 
					  PersonStatisticsResponseDto,
 | 
				
			||||||
  PersonUpdateDto,
 | 
					  PersonUpdateDto,
 | 
				
			||||||
} from 'src/dtos/person.dto';
 | 
					} from 'src/dtos/person.dto';
 | 
				
			||||||
import { sendFile } from 'src/immich/app.utils';
 | 
					 | 
				
			||||||
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
 | 
					import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
 | 
				
			||||||
import { PersonService } from 'src/services/person.service';
 | 
					import { PersonService } from 'src/services/person.service';
 | 
				
			||||||
 | 
					import { sendFile } from 'src/utils/file';
 | 
				
			||||||
import { UUIDParamDto } from 'src/validation';
 | 
					import { UUIDParamDto } from 'src/validation';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ApiTags('Person')
 | 
					@ApiTags('Person')
 | 
				
			||||||
 | 
				
			|||||||
@ -19,10 +19,10 @@ import { NextFunction, Response } from 'express';
 | 
				
			|||||||
import { AuthDto } from 'src/dtos/auth.dto';
 | 
					import { AuthDto } from 'src/dtos/auth.dto';
 | 
				
			||||||
import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
 | 
					import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
 | 
				
			||||||
import { CreateUserDto, DeleteUserDto, UpdateUserDto, UserResponseDto } from 'src/dtos/user.dto';
 | 
					import { CreateUserDto, DeleteUserDto, UpdateUserDto, UserResponseDto } from 'src/dtos/user.dto';
 | 
				
			||||||
import { sendFile } from 'src/immich/app.utils';
 | 
					 | 
				
			||||||
import { AdminRoute, Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
 | 
					import { AdminRoute, Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
 | 
				
			||||||
import { FileUploadInterceptor, Route } from 'src/middleware/file-upload.interceptor';
 | 
					import { FileUploadInterceptor, Route } from 'src/middleware/file-upload.interceptor';
 | 
				
			||||||
import { UserService } from 'src/services/user.service';
 | 
					import { UserService } from 'src/services/user.service';
 | 
				
			||||||
 | 
					import { sendFile } from 'src/utils/file';
 | 
				
			||||||
import { UUIDParamDto } from 'src/validation';
 | 
					import { UUIDParamDto } from 'src/validation';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ApiTags('User')
 | 
					@ApiTags('User')
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										45
									
								
								server/src/dtos/asset-v1-response.dto.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								server/src/dtos/asset-v1-response.dto.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					export class AssetBulkUploadCheckResult {
 | 
				
			||||||
 | 
					  id!: string;
 | 
				
			||||||
 | 
					  action!: AssetUploadAction;
 | 
				
			||||||
 | 
					  reason?: AssetRejectReason;
 | 
				
			||||||
 | 
					  assetId?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class AssetBulkUploadCheckResponseDto {
 | 
				
			||||||
 | 
					  results!: AssetBulkUploadCheckResult[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export enum AssetUploadAction {
 | 
				
			||||||
 | 
					  ACCEPT = 'accept',
 | 
				
			||||||
 | 
					  REJECT = 'reject',
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export enum AssetRejectReason {
 | 
				
			||||||
 | 
					  DUPLICATE = 'duplicate',
 | 
				
			||||||
 | 
					  UNSUPPORTED_FORMAT = 'unsupported-format',
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class AssetFileUploadResponseDto {
 | 
				
			||||||
 | 
					  id!: string;
 | 
				
			||||||
 | 
					  duplicate!: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class CheckExistingAssetsResponseDto {
 | 
				
			||||||
 | 
					  existingIds!: string[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class CuratedLocationsResponseDto {
 | 
				
			||||||
 | 
					  id!: string;
 | 
				
			||||||
 | 
					  city!: string;
 | 
				
			||||||
 | 
					  resizePath!: string;
 | 
				
			||||||
 | 
					  deviceAssetId!: string;
 | 
				
			||||||
 | 
					  deviceId!: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class CuratedObjectsResponseDto {
 | 
				
			||||||
 | 
					  id!: string;
 | 
				
			||||||
 | 
					  object!: string;
 | 
				
			||||||
 | 
					  resizePath!: string;
 | 
				
			||||||
 | 
					  deviceAssetId!: string;
 | 
				
			||||||
 | 
					  deviceId!: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										154
									
								
								server/src/dtos/asset-v1.dto.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								server/src/dtos/asset-v1.dto.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,154 @@
 | 
				
			|||||||
 | 
					import { ApiProperty } from '@nestjs/swagger';
 | 
				
			||||||
 | 
					import { Type } from 'class-transformer';
 | 
				
			||||||
 | 
					import { ArrayNotEmpty, IsArray, IsEnum, IsInt, IsNotEmpty, IsString, IsUUID, ValidateNested } from 'class-validator';
 | 
				
			||||||
 | 
					import { UploadFieldName } from 'src/dtos/asset.dto';
 | 
				
			||||||
 | 
					import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class AssetBulkUploadCheckItem {
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  @IsNotEmpty()
 | 
				
			||||||
 | 
					  id!: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /** base64 or hex encoded sha1 hash */
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  @IsNotEmpty()
 | 
				
			||||||
 | 
					  checksum!: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class AssetBulkUploadCheckDto {
 | 
				
			||||||
 | 
					  @IsArray()
 | 
				
			||||||
 | 
					  @ValidateNested({ each: true })
 | 
				
			||||||
 | 
					  @Type(() => AssetBulkUploadCheckItem)
 | 
				
			||||||
 | 
					  assets!: AssetBulkUploadCheckItem[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class AssetSearchDto {
 | 
				
			||||||
 | 
					  @ValidateBoolean({ optional: true })
 | 
				
			||||||
 | 
					  isFavorite?: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ValidateBoolean({ optional: true })
 | 
				
			||||||
 | 
					  isArchived?: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Optional()
 | 
				
			||||||
 | 
					  @IsInt()
 | 
				
			||||||
 | 
					  @Type(() => Number)
 | 
				
			||||||
 | 
					  @ApiProperty({ type: 'integer' })
 | 
				
			||||||
 | 
					  skip?: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Optional()
 | 
				
			||||||
 | 
					  @IsInt()
 | 
				
			||||||
 | 
					  @Type(() => Number)
 | 
				
			||||||
 | 
					  @ApiProperty({ type: 'integer' })
 | 
				
			||||||
 | 
					  take?: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Optional()
 | 
				
			||||||
 | 
					  @IsUUID('4')
 | 
				
			||||||
 | 
					  @ApiProperty({ format: 'uuid' })
 | 
				
			||||||
 | 
					  userId?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ValidateDate({ optional: true })
 | 
				
			||||||
 | 
					  updatedAfter?: Date;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ValidateDate({ optional: true })
 | 
				
			||||||
 | 
					  updatedBefore?: Date;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class CheckExistingAssetsDto {
 | 
				
			||||||
 | 
					  @ArrayNotEmpty()
 | 
				
			||||||
 | 
					  @IsString({ each: true })
 | 
				
			||||||
 | 
					  @IsNotEmpty({ each: true })
 | 
				
			||||||
 | 
					  deviceAssetIds!: string[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsNotEmpty()
 | 
				
			||||||
 | 
					  deviceId!: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class CreateAssetDto {
 | 
				
			||||||
 | 
					  @ValidateUUID({ optional: true })
 | 
				
			||||||
 | 
					  libraryId?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsNotEmpty()
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  deviceAssetId!: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsNotEmpty()
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  deviceId!: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ValidateDate()
 | 
				
			||||||
 | 
					  fileCreatedAt!: Date;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ValidateDate()
 | 
				
			||||||
 | 
					  fileModifiedAt!: Date;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Optional()
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  duration?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ValidateBoolean({ optional: true })
 | 
				
			||||||
 | 
					  isFavorite?: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ValidateBoolean({ optional: true })
 | 
				
			||||||
 | 
					  isArchived?: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ValidateBoolean({ optional: true })
 | 
				
			||||||
 | 
					  isVisible?: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ValidateBoolean({ optional: true })
 | 
				
			||||||
 | 
					  isOffline?: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ValidateBoolean({ optional: true })
 | 
				
			||||||
 | 
					  isReadOnly?: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // The properties below are added to correctly generate the API docs
 | 
				
			||||||
 | 
					  // and client SDKs. Validation should be handled in the controller.
 | 
				
			||||||
 | 
					  @ApiProperty({ type: 'string', format: 'binary' })
 | 
				
			||||||
 | 
					  [UploadFieldName.ASSET_DATA]!: any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ApiProperty({ type: 'string', format: 'binary', required: false })
 | 
				
			||||||
 | 
					  [UploadFieldName.LIVE_PHOTO_DATA]?: any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ApiProperty({ type: 'string', format: 'binary', required: false })
 | 
				
			||||||
 | 
					  [UploadFieldName.SIDECAR_DATA]?: any;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export enum GetAssetThumbnailFormatEnum {
 | 
				
			||||||
 | 
					  JPEG = 'JPEG',
 | 
				
			||||||
 | 
					  WEBP = 'WEBP',
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class GetAssetThumbnailDto {
 | 
				
			||||||
 | 
					  @Optional()
 | 
				
			||||||
 | 
					  @IsEnum(GetAssetThumbnailFormatEnum)
 | 
				
			||||||
 | 
					  @ApiProperty({
 | 
				
			||||||
 | 
					    type: String,
 | 
				
			||||||
 | 
					    enum: GetAssetThumbnailFormatEnum,
 | 
				
			||||||
 | 
					    default: GetAssetThumbnailFormatEnum.WEBP,
 | 
				
			||||||
 | 
					    required: false,
 | 
				
			||||||
 | 
					    enumName: 'ThumbnailFormat',
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  format: GetAssetThumbnailFormatEnum = GetAssetThumbnailFormatEnum.WEBP;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class SearchPropertiesDto {
 | 
				
			||||||
 | 
					  tags?: string[];
 | 
				
			||||||
 | 
					  objects?: string[];
 | 
				
			||||||
 | 
					  assetType?: string;
 | 
				
			||||||
 | 
					  orientation?: string;
 | 
				
			||||||
 | 
					  lensModel?: string;
 | 
				
			||||||
 | 
					  make?: string;
 | 
				
			||||||
 | 
					  model?: string;
 | 
				
			||||||
 | 
					  city?: string;
 | 
				
			||||||
 | 
					  state?: string;
 | 
				
			||||||
 | 
					  country?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class ServeFileDto {
 | 
				
			||||||
 | 
					  @ValidateBoolean({ optional: true })
 | 
				
			||||||
 | 
					  @ApiProperty({ title: 'Is serve thumbnail (resize) file' })
 | 
				
			||||||
 | 
					  isThumb?: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ValidateBoolean({ optional: true })
 | 
				
			||||||
 | 
					  @ApiProperty({ title: 'Is request made from web' })
 | 
				
			||||||
 | 
					  isWeb?: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,20 +0,0 @@
 | 
				
			|||||||
import { Type } from 'class-transformer';
 | 
					 | 
				
			||||||
import { IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class AssetBulkUploadCheckItem {
 | 
					 | 
				
			||||||
  @IsString()
 | 
					 | 
				
			||||||
  @IsNotEmpty()
 | 
					 | 
				
			||||||
  id!: string;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /** base64 or hex encoded sha1 hash */
 | 
					 | 
				
			||||||
  @IsString()
 | 
					 | 
				
			||||||
  @IsNotEmpty()
 | 
					 | 
				
			||||||
  checksum!: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class AssetBulkUploadCheckDto {
 | 
					 | 
				
			||||||
  @IsArray()
 | 
					 | 
				
			||||||
  @ValidateNested({ each: true })
 | 
					 | 
				
			||||||
  @Type(() => AssetBulkUploadCheckItem)
 | 
					 | 
				
			||||||
  assets!: AssetBulkUploadCheckItem[];
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,35 +0,0 @@
 | 
				
			|||||||
import { ApiProperty } from '@nestjs/swagger';
 | 
					 | 
				
			||||||
import { Type } from 'class-transformer';
 | 
					 | 
				
			||||||
import { IsInt, IsUUID } from 'class-validator';
 | 
					 | 
				
			||||||
import { Optional, ValidateBoolean, ValidateDate } from 'src/validation';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class AssetSearchDto {
 | 
					 | 
				
			||||||
  @ValidateBoolean({ optional: true })
 | 
					 | 
				
			||||||
  isFavorite?: boolean;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @ValidateBoolean({ optional: true })
 | 
					 | 
				
			||||||
  isArchived?: boolean;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @Optional()
 | 
					 | 
				
			||||||
  @IsInt()
 | 
					 | 
				
			||||||
  @Type(() => Number)
 | 
					 | 
				
			||||||
  @ApiProperty({ type: 'integer' })
 | 
					 | 
				
			||||||
  skip?: number;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @Optional()
 | 
					 | 
				
			||||||
  @IsInt()
 | 
					 | 
				
			||||||
  @Type(() => Number)
 | 
					 | 
				
			||||||
  @ApiProperty({ type: 'integer' })
 | 
					 | 
				
			||||||
  take?: number;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @Optional()
 | 
					 | 
				
			||||||
  @IsUUID('4')
 | 
					 | 
				
			||||||
  @ApiProperty({ format: 'uuid' })
 | 
					 | 
				
			||||||
  userId?: string;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @ValidateDate({ optional: true })
 | 
					 | 
				
			||||||
  updatedAfter?: Date;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @ValidateDate({ optional: true })
 | 
					 | 
				
			||||||
  updatedBefore?: Date;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,28 +0,0 @@
 | 
				
			|||||||
import { plainToInstance } from 'class-transformer';
 | 
					 | 
				
			||||||
import { validateSync } from 'class-validator';
 | 
					 | 
				
			||||||
import { CheckExistingAssetsDto } from 'src/immich/api-v1/asset/dto/check-existing-assets.dto';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
describe('CheckExistingAssetsDto', () => {
 | 
					 | 
				
			||||||
  it('should fail with an empty list', () => {
 | 
					 | 
				
			||||||
    const dto = plainToInstance(CheckExistingAssetsDto, { deviceAssetIds: [], deviceId: 'test-device' });
 | 
					 | 
				
			||||||
    const errors = validateSync(dto);
 | 
					 | 
				
			||||||
    expect(errors).toHaveLength(1);
 | 
					 | 
				
			||||||
    expect(errors[0].property).toEqual('deviceAssetIds');
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  it('should fail with an empty string', () => {
 | 
					 | 
				
			||||||
    const dto = plainToInstance(CheckExistingAssetsDto, { deviceAssetIds: [''], deviceId: 'test-device' });
 | 
					 | 
				
			||||||
    const errors = validateSync(dto);
 | 
					 | 
				
			||||||
    expect(errors).toHaveLength(1);
 | 
					 | 
				
			||||||
    expect(errors[0].property).toEqual('deviceAssetIds');
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  it('should work with valid asset ids', () => {
 | 
					 | 
				
			||||||
    const dto = plainToInstance(CheckExistingAssetsDto, {
 | 
					 | 
				
			||||||
      deviceAssetIds: ['asset-1', 'asset-2'],
 | 
					 | 
				
			||||||
      deviceId: 'test-device',
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    const errors = validateSync(dto);
 | 
					 | 
				
			||||||
    expect(errors).toHaveLength(0);
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
@ -1,11 +0,0 @@
 | 
				
			|||||||
import { ArrayNotEmpty, IsNotEmpty, IsString } from 'class-validator';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class CheckExistingAssetsDto {
 | 
					 | 
				
			||||||
  @ArrayNotEmpty()
 | 
					 | 
				
			||||||
  @IsString({ each: true })
 | 
					 | 
				
			||||||
  @IsNotEmpty({ each: true })
 | 
					 | 
				
			||||||
  deviceAssetIds!: string[];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @IsNotEmpty()
 | 
					 | 
				
			||||||
  deviceId!: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,53 +0,0 @@
 | 
				
			|||||||
import { ApiProperty } from '@nestjs/swagger';
 | 
					 | 
				
			||||||
import { IsNotEmpty, IsString } from 'class-validator';
 | 
					 | 
				
			||||||
import { UploadFieldName } from 'src/dtos/asset.dto';
 | 
					 | 
				
			||||||
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class CreateAssetDto {
 | 
					 | 
				
			||||||
  @ValidateUUID({ optional: true })
 | 
					 | 
				
			||||||
  libraryId?: string;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @IsNotEmpty()
 | 
					 | 
				
			||||||
  @IsString()
 | 
					 | 
				
			||||||
  deviceAssetId!: string;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @IsNotEmpty()
 | 
					 | 
				
			||||||
  @IsString()
 | 
					 | 
				
			||||||
  deviceId!: string;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @ValidateDate()
 | 
					 | 
				
			||||||
  fileCreatedAt!: Date;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @ValidateDate()
 | 
					 | 
				
			||||||
  fileModifiedAt!: Date;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @Optional()
 | 
					 | 
				
			||||||
  @IsString()
 | 
					 | 
				
			||||||
  duration?: string;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @ValidateBoolean({ optional: true })
 | 
					 | 
				
			||||||
  isFavorite?: boolean;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @ValidateBoolean({ optional: true })
 | 
					 | 
				
			||||||
  isArchived?: boolean;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @ValidateBoolean({ optional: true })
 | 
					 | 
				
			||||||
  isVisible?: boolean;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @ValidateBoolean({ optional: true })
 | 
					 | 
				
			||||||
  isOffline?: boolean;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @ValidateBoolean({ optional: true })
 | 
					 | 
				
			||||||
  isReadOnly?: boolean;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // The properties below are added to correctly generate the API docs
 | 
					 | 
				
			||||||
  // and client SDKs. Validation should be handled in the controller.
 | 
					 | 
				
			||||||
  @ApiProperty({ type: 'string', format: 'binary' })
 | 
					 | 
				
			||||||
  [UploadFieldName.ASSET_DATA]!: any;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @ApiProperty({ type: 'string', format: 'binary', required: false })
 | 
					 | 
				
			||||||
  [UploadFieldName.LIVE_PHOTO_DATA]?: any;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @ApiProperty({ type: 'string', format: 'binary', required: false })
 | 
					 | 
				
			||||||
  [UploadFieldName.SIDECAR_DATA]?: any;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,21 +0,0 @@
 | 
				
			|||||||
import { ApiProperty } from '@nestjs/swagger';
 | 
					 | 
				
			||||||
import { IsEnum } from 'class-validator';
 | 
					 | 
				
			||||||
import { Optional } from 'src/validation';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export enum GetAssetThumbnailFormatEnum {
 | 
					 | 
				
			||||||
  JPEG = 'JPEG',
 | 
					 | 
				
			||||||
  WEBP = 'WEBP',
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class GetAssetThumbnailDto {
 | 
					 | 
				
			||||||
  @Optional()
 | 
					 | 
				
			||||||
  @IsEnum(GetAssetThumbnailFormatEnum)
 | 
					 | 
				
			||||||
  @ApiProperty({
 | 
					 | 
				
			||||||
    type: String,
 | 
					 | 
				
			||||||
    enum: GetAssetThumbnailFormatEnum,
 | 
					 | 
				
			||||||
    default: GetAssetThumbnailFormatEnum.WEBP,
 | 
					 | 
				
			||||||
    required: false,
 | 
					 | 
				
			||||||
    enumName: 'ThumbnailFormat',
 | 
					 | 
				
			||||||
  })
 | 
					 | 
				
			||||||
  format: GetAssetThumbnailFormatEnum = GetAssetThumbnailFormatEnum.WEBP;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,12 +0,0 @@
 | 
				
			|||||||
export class SearchPropertiesDto {
 | 
					 | 
				
			||||||
  tags?: string[];
 | 
					 | 
				
			||||||
  objects?: string[];
 | 
					 | 
				
			||||||
  assetType?: string;
 | 
					 | 
				
			||||||
  orientation?: string;
 | 
					 | 
				
			||||||
  lensModel?: string;
 | 
					 | 
				
			||||||
  make?: string;
 | 
					 | 
				
			||||||
  model?: string;
 | 
					 | 
				
			||||||
  city?: string;
 | 
					 | 
				
			||||||
  state?: string;
 | 
					 | 
				
			||||||
  country?: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,12 +0,0 @@
 | 
				
			|||||||
import { ApiProperty } from '@nestjs/swagger';
 | 
					 | 
				
			||||||
import { ValidateBoolean } from 'src/validation';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class ServeFileDto {
 | 
					 | 
				
			||||||
  @ValidateBoolean({ optional: true })
 | 
					 | 
				
			||||||
  @ApiProperty({ title: 'Is serve thumbnail (resize) file' })
 | 
					 | 
				
			||||||
  isThumb?: boolean;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @ValidateBoolean({ optional: true })
 | 
					 | 
				
			||||||
  @ApiProperty({ title: 'Is request made from web' })
 | 
					 | 
				
			||||||
  isWeb?: boolean;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,20 +0,0 @@
 | 
				
			|||||||
export class AssetBulkUploadCheckResult {
 | 
					 | 
				
			||||||
  id!: string;
 | 
					 | 
				
			||||||
  action!: AssetUploadAction;
 | 
					 | 
				
			||||||
  reason?: AssetRejectReason;
 | 
					 | 
				
			||||||
  assetId?: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export class AssetBulkUploadCheckResponseDto {
 | 
					 | 
				
			||||||
  results!: AssetBulkUploadCheckResult[];
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export enum AssetUploadAction {
 | 
					 | 
				
			||||||
  ACCEPT = 'accept',
 | 
					 | 
				
			||||||
  REJECT = 'reject',
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export enum AssetRejectReason {
 | 
					 | 
				
			||||||
  DUPLICATE = 'duplicate',
 | 
					 | 
				
			||||||
  UNSUPPORTED_FORMAT = 'unsupported-format',
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,4 +0,0 @@
 | 
				
			|||||||
export class AssetFileUploadResponseDto {
 | 
					 | 
				
			||||||
  id!: string;
 | 
					 | 
				
			||||||
  duplicate!: boolean;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,3 +0,0 @@
 | 
				
			|||||||
export class CheckExistingAssetsResponseDto {
 | 
					 | 
				
			||||||
  existingIds!: string[];
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,7 +0,0 @@
 | 
				
			|||||||
export class CuratedLocationsResponseDto {
 | 
					 | 
				
			||||||
  id!: string;
 | 
					 | 
				
			||||||
  city!: string;
 | 
					 | 
				
			||||||
  resizePath!: string;
 | 
					 | 
				
			||||||
  deviceAssetId!: string;
 | 
					 | 
				
			||||||
  deviceId!: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,7 +0,0 @@
 | 
				
			|||||||
export class CuratedObjectsResponseDto {
 | 
					 | 
				
			||||||
  id!: string;
 | 
					 | 
				
			||||||
  object!: string;
 | 
					 | 
				
			||||||
  resizePath!: string;
 | 
					 | 
				
			||||||
  deviceAssetId!: string;
 | 
					 | 
				
			||||||
  deviceId!: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,205 +0,0 @@
 | 
				
			|||||||
import { HttpException, INestApplication, StreamableFile } from '@nestjs/common';
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  DocumentBuilder,
 | 
					 | 
				
			||||||
  OpenAPIObject,
 | 
					 | 
				
			||||||
  SwaggerCustomOptions,
 | 
					 | 
				
			||||||
  SwaggerDocumentOptions,
 | 
					 | 
				
			||||||
  SwaggerModule,
 | 
					 | 
				
			||||||
} from '@nestjs/swagger';
 | 
					 | 
				
			||||||
import { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface';
 | 
					 | 
				
			||||||
import { NextFunction, Response } from 'express';
 | 
					 | 
				
			||||||
import _ from 'lodash';
 | 
					 | 
				
			||||||
import { writeFileSync } from 'node:fs';
 | 
					 | 
				
			||||||
import { access, constants } from 'node:fs/promises';
 | 
					 | 
				
			||||||
import path, { isAbsolute } from 'node:path';
 | 
					 | 
				
			||||||
import { promisify } from 'node:util';
 | 
					 | 
				
			||||||
import { IMMICH_ACCESS_COOKIE, IMMICH_API_KEY_HEADER, IMMICH_API_KEY_NAME, serverVersion } from 'src/constants';
 | 
					 | 
				
			||||||
import { ImmichReadStream } from 'src/interfaces/storage.interface';
 | 
					 | 
				
			||||||
import { Metadata } from 'src/middleware/auth.guard';
 | 
					 | 
				
			||||||
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
 | 
					 | 
				
			||||||
import { ImmichLogger } from 'src/utils/logger';
 | 
					 | 
				
			||||||
import { isConnectionAborted } from 'src/utils/misc';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type SendFile = Parameters<Response['sendFile']>;
 | 
					 | 
				
			||||||
type SendFileOptions = SendFile[1];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const logger = new ImmichLogger('SendFile');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const sendFile = async (
 | 
					 | 
				
			||||||
  res: Response,
 | 
					 | 
				
			||||||
  next: NextFunction,
 | 
					 | 
				
			||||||
  handler: () => Promise<ImmichFileResponse>,
 | 
					 | 
				
			||||||
): Promise<void> => {
 | 
					 | 
				
			||||||
  const _sendFile = (path: string, options: SendFileOptions) =>
 | 
					 | 
				
			||||||
    promisify<string, SendFileOptions>(res.sendFile).bind(res)(path, options);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const file = await handler();
 | 
					 | 
				
			||||||
    switch (file.cacheControl) {
 | 
					 | 
				
			||||||
      case CacheControl.PRIVATE_WITH_CACHE: {
 | 
					 | 
				
			||||||
        res.set('Cache-Control', 'private, max-age=86400, no-transform');
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      case CacheControl.PRIVATE_WITHOUT_CACHE: {
 | 
					 | 
				
			||||||
        res.set('Cache-Control', 'private, no-cache, no-transform');
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    res.header('Content-Type', file.contentType);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const options: SendFileOptions = { dotfiles: 'allow' };
 | 
					 | 
				
			||||||
    if (!isAbsolute(file.path)) {
 | 
					 | 
				
			||||||
      options.root = process.cwd();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    await access(file.path, constants.R_OK);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return _sendFile(file.path, options);
 | 
					 | 
				
			||||||
  } catch (error: Error | any) {
 | 
					 | 
				
			||||||
    // ignore client-closed connection
 | 
					 | 
				
			||||||
    if (isConnectionAborted(error)) {
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // log non-http errors
 | 
					 | 
				
			||||||
    if (error instanceof HttpException === false) {
 | 
					 | 
				
			||||||
      logger.error(`Unable to send file: ${error.name}`, error.stack);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    res.header('Cache-Control', 'none');
 | 
					 | 
				
			||||||
    next(error);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const asStreamableFile = ({ stream, type, length }: ImmichReadStream) => {
 | 
					 | 
				
			||||||
  return new StreamableFile(stream, { type, length });
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function sortKeys<T>(target: T): T {
 | 
					 | 
				
			||||||
  if (!target || typeof target !== 'object' || Array.isArray(target)) {
 | 
					 | 
				
			||||||
    return target;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const result: Partial<T> = {};
 | 
					 | 
				
			||||||
  const keys = Object.keys(target).sort() as Array<keyof T>;
 | 
					 | 
				
			||||||
  for (const key of keys) {
 | 
					 | 
				
			||||||
    result[key] = sortKeys(target[key]);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  return result as T;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const routeToErrorMessage = (methodName: string) =>
 | 
					 | 
				
			||||||
  'Failed to ' + methodName.replaceAll(/[A-Z]+/g, (letter) => ` ${letter.toLowerCase()}`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const patchOpenAPI = (document: OpenAPIObject) => {
 | 
					 | 
				
			||||||
  document.paths = sortKeys(document.paths);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (document.components?.schemas) {
 | 
					 | 
				
			||||||
    const schemas = document.components.schemas as Record<string, SchemaObject>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    document.components.schemas = sortKeys(schemas);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (const schema of Object.values(schemas)) {
 | 
					 | 
				
			||||||
      if (schema.properties) {
 | 
					 | 
				
			||||||
        schema.properties = sortKeys(schema.properties);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (schema.required) {
 | 
					 | 
				
			||||||
        schema.required = schema.required.sort();
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  for (const [key, value] of Object.entries(document.paths)) {
 | 
					 | 
				
			||||||
    const newKey = key.replace('/api/', '/');
 | 
					 | 
				
			||||||
    delete document.paths[key];
 | 
					 | 
				
			||||||
    document.paths[newKey] = value;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  for (const path of Object.values(document.paths)) {
 | 
					 | 
				
			||||||
    const operations = {
 | 
					 | 
				
			||||||
      get: path.get,
 | 
					 | 
				
			||||||
      put: path.put,
 | 
					 | 
				
			||||||
      post: path.post,
 | 
					 | 
				
			||||||
      delete: path.delete,
 | 
					 | 
				
			||||||
      options: path.options,
 | 
					 | 
				
			||||||
      head: path.head,
 | 
					 | 
				
			||||||
      patch: path.patch,
 | 
					 | 
				
			||||||
      trace: path.trace,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (const operation of Object.values(operations)) {
 | 
					 | 
				
			||||||
      if (!operation) {
 | 
					 | 
				
			||||||
        continue;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if ((operation.security || []).some((item) => !!item[Metadata.PUBLIC_SECURITY])) {
 | 
					 | 
				
			||||||
        delete operation.security;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (operation.summary === '') {
 | 
					 | 
				
			||||||
        delete operation.summary;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (operation.operationId) {
 | 
					 | 
				
			||||||
        // console.log(`${routeToErrorMessage(operation.operationId).padEnd(40)} (${operation.operationId})`);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (operation.description === '') {
 | 
					 | 
				
			||||||
        delete operation.description;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (operation.parameters) {
 | 
					 | 
				
			||||||
        operation.parameters = _.orderBy(operation.parameters, 'name');
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return document;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const useSwagger = (app: INestApplication, isDevelopment: boolean) => {
 | 
					 | 
				
			||||||
  const config = new DocumentBuilder()
 | 
					 | 
				
			||||||
    .setTitle('Immich')
 | 
					 | 
				
			||||||
    .setDescription('Immich API')
 | 
					 | 
				
			||||||
    .setVersion(serverVersion.toString())
 | 
					 | 
				
			||||||
    .addBearerAuth({
 | 
					 | 
				
			||||||
      type: 'http',
 | 
					 | 
				
			||||||
      scheme: 'Bearer',
 | 
					 | 
				
			||||||
      in: 'header',
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
    .addCookieAuth(IMMICH_ACCESS_COOKIE)
 | 
					 | 
				
			||||||
    .addApiKey(
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        type: 'apiKey',
 | 
					 | 
				
			||||||
        in: 'header',
 | 
					 | 
				
			||||||
        name: IMMICH_API_KEY_HEADER,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      IMMICH_API_KEY_NAME,
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    .addServer('/api')
 | 
					 | 
				
			||||||
    .build();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const options: SwaggerDocumentOptions = {
 | 
					 | 
				
			||||||
    operationIdFactory: (controllerKey: string, methodKey: string) => methodKey,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const specification = SwaggerModule.createDocument(app, config, options);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const customOptions: SwaggerCustomOptions = {
 | 
					 | 
				
			||||||
    swaggerOptions: {
 | 
					 | 
				
			||||||
      persistAuthorization: true,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    customSiteTitle: 'Immich API Documentation',
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  SwaggerModule.setup('doc', app, specification, customOptions);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (isDevelopment) {
 | 
					 | 
				
			||||||
    // Generate API Documentation only in development mode
 | 
					 | 
				
			||||||
    const outputPath = path.resolve(process.cwd(), '../open-api/immich-openapi-specs.json');
 | 
					 | 
				
			||||||
    writeFileSync(outputPath, JSON.stringify(patchOpenAPI(specification), null, 2), { encoding: 'utf8' });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
							
								
								
									
										25
									
								
								server/src/interfaces/asset-v1.interface.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								server/src/interfaces/asset-v1.interface.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					import { CuratedLocationsResponseDto, CuratedObjectsResponseDto } from 'src/dtos/asset-v1-response.dto';
 | 
				
			||||||
 | 
					import { AssetSearchDto, CheckExistingAssetsDto, SearchPropertiesDto } from 'src/dtos/asset-v1.dto';
 | 
				
			||||||
 | 
					import { AssetEntity } from 'src/entities/asset.entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface AssetCheck {
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  checksum: Buffer;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface AssetOwnerCheck extends AssetCheck {
 | 
				
			||||||
 | 
					  ownerId: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface IAssetRepositoryV1 {
 | 
				
			||||||
 | 
					  get(id: string): Promise<AssetEntity | null>;
 | 
				
			||||||
 | 
					  getLocationsByUserId(userId: string): Promise<CuratedLocationsResponseDto[]>;
 | 
				
			||||||
 | 
					  getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]>;
 | 
				
			||||||
 | 
					  getAllByUserId(userId: string, dto: AssetSearchDto): Promise<AssetEntity[]>;
 | 
				
			||||||
 | 
					  getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]>;
 | 
				
			||||||
 | 
					  getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise<AssetCheck[]>;
 | 
				
			||||||
 | 
					  getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<string[]>;
 | 
				
			||||||
 | 
					  getByOriginalPath(originalPath: string): Promise<AssetOwnerCheck | null>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const IAssetRepositoryV1 = 'IAssetRepositoryV1';
 | 
				
			||||||
@ -7,9 +7,8 @@ import {
 | 
				
			|||||||
  NestInterceptor,
 | 
					  NestInterceptor,
 | 
				
			||||||
} from '@nestjs/common';
 | 
					} from '@nestjs/common';
 | 
				
			||||||
import { Observable, catchError, throwError } from 'rxjs';
 | 
					import { Observable, catchError, throwError } from 'rxjs';
 | 
				
			||||||
import { routeToErrorMessage } from 'src/immich/app.utils';
 | 
					 | 
				
			||||||
import { ImmichLogger } from 'src/utils/logger';
 | 
					import { ImmichLogger } from 'src/utils/logger';
 | 
				
			||||||
import { isConnectionAborted } from 'src/utils/misc';
 | 
					import { isConnectionAborted, routeToErrorMessage } from 'src/utils/misc';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class ErrorInterceptor implements NestInterceptor {
 | 
					export class ErrorInterceptor implements NestInterceptor {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,43 +1,16 @@
 | 
				
			|||||||
import { Injectable } from '@nestjs/common';
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
import { InjectRepository } from '@nestjs/typeorm';
 | 
					import { InjectRepository } from '@nestjs/typeorm';
 | 
				
			||||||
 | 
					import { CuratedLocationsResponseDto, CuratedObjectsResponseDto } from 'src/dtos/asset-v1-response.dto';
 | 
				
			||||||
 | 
					import { AssetSearchDto, CheckExistingAssetsDto, SearchPropertiesDto } from 'src/dtos/asset-v1.dto';
 | 
				
			||||||
import { AssetEntity } from 'src/entities/asset.entity';
 | 
					import { AssetEntity } from 'src/entities/asset.entity';
 | 
				
			||||||
import { ExifEntity } from 'src/entities/exif.entity';
 | 
					import { AssetCheck, AssetOwnerCheck, IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface';
 | 
				
			||||||
import { AssetSearchDto } from 'src/immich/api-v1/asset/dto/asset-search.dto';
 | 
					 | 
				
			||||||
import { CheckExistingAssetsDto } from 'src/immich/api-v1/asset/dto/check-existing-assets.dto';
 | 
					 | 
				
			||||||
import { SearchPropertiesDto } from 'src/immich/api-v1/asset/dto/search-properties.dto';
 | 
					 | 
				
			||||||
import { CuratedLocationsResponseDto } from 'src/immich/api-v1/asset/response-dto/curated-locations-response.dto';
 | 
					 | 
				
			||||||
import { CuratedObjectsResponseDto } from 'src/immich/api-v1/asset/response-dto/curated-objects-response.dto';
 | 
					 | 
				
			||||||
import { OptionalBetween } from 'src/utils/database';
 | 
					import { OptionalBetween } from 'src/utils/database';
 | 
				
			||||||
import { In } from 'typeorm/find-options/operator/In.js';
 | 
					import { In } from 'typeorm/find-options/operator/In.js';
 | 
				
			||||||
import { Repository } from 'typeorm/repository/Repository.js';
 | 
					import { Repository } from 'typeorm/repository/Repository.js';
 | 
				
			||||||
export interface AssetCheck {
 | 
					 | 
				
			||||||
  id: string;
 | 
					 | 
				
			||||||
  checksum: Buffer;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface AssetOwnerCheck extends AssetCheck {
 | 
					 | 
				
			||||||
  ownerId: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface IAssetRepositoryV1 {
 | 
					 | 
				
			||||||
  get(id: string): Promise<AssetEntity | null>;
 | 
					 | 
				
			||||||
  getLocationsByUserId(userId: string): Promise<CuratedLocationsResponseDto[]>;
 | 
					 | 
				
			||||||
  getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]>;
 | 
					 | 
				
			||||||
  getAllByUserId(userId: string, dto: AssetSearchDto): Promise<AssetEntity[]>;
 | 
					 | 
				
			||||||
  getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]>;
 | 
					 | 
				
			||||||
  getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise<AssetCheck[]>;
 | 
					 | 
				
			||||||
  getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<string[]>;
 | 
					 | 
				
			||||||
  getByOriginalPath(originalPath: string): Promise<AssetOwnerCheck | null>;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const IAssetRepositoryV1 = 'IAssetRepositoryV1';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class AssetRepositoryV1 implements IAssetRepositoryV1 {
 | 
					export class AssetRepositoryV1 implements IAssetRepositoryV1 {
 | 
				
			||||||
  constructor(
 | 
					  constructor(@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>) {}
 | 
				
			||||||
    @InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
 | 
					 | 
				
			||||||
    @InjectRepository(ExifEntity) private exifRepository: Repository<ExifEntity>,
 | 
					 | 
				
			||||||
  ) {}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Retrieves all assets by user ID.
 | 
					   * Retrieves all assets by user ID.
 | 
				
			||||||
@ -1,15 +1,15 @@
 | 
				
			|||||||
import { when } from 'jest-when';
 | 
					import { when } from 'jest-when';
 | 
				
			||||||
 | 
					import { AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-v1-response.dto';
 | 
				
			||||||
 | 
					import { CreateAssetDto } from 'src/dtos/asset-v1.dto';
 | 
				
			||||||
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity';
 | 
					import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity';
 | 
				
			||||||
import { ExifEntity } from 'src/entities/exif.entity';
 | 
					import { ExifEntity } from 'src/entities/exif.entity';
 | 
				
			||||||
import { IAssetRepositoryV1 } from 'src/immich/api-v1/asset/asset-repository';
 | 
					import { IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface';
 | 
				
			||||||
import { AssetService } from 'src/immich/api-v1/asset/asset.service';
 | 
					 | 
				
			||||||
import { CreateAssetDto } from 'src/immich/api-v1/asset/dto/create-asset.dto';
 | 
					 | 
				
			||||||
import { AssetRejectReason, AssetUploadAction } from 'src/immich/api-v1/asset/response-dto/asset-check-response.dto';
 | 
					 | 
				
			||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
 | 
					import { IAssetRepository } from 'src/interfaces/asset.interface';
 | 
				
			||||||
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
 | 
					import { IJobRepository, JobName } from 'src/interfaces/job.interface';
 | 
				
			||||||
import { ILibraryRepository } from 'src/interfaces/library.interface';
 | 
					import { ILibraryRepository } from 'src/interfaces/library.interface';
 | 
				
			||||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
 | 
					import { IStorageRepository } from 'src/interfaces/storage.interface';
 | 
				
			||||||
import { IUserRepository } from 'src/interfaces/user.interface';
 | 
					import { IUserRepository } from 'src/interfaces/user.interface';
 | 
				
			||||||
 | 
					import { AssetServiceV1 } from 'src/services/asset-v1.service';
 | 
				
			||||||
import { assetStub } from 'test/fixtures/asset.stub';
 | 
					import { assetStub } from 'test/fixtures/asset.stub';
 | 
				
			||||||
import { authStub } from 'test/fixtures/auth.stub';
 | 
					import { authStub } from 'test/fixtures/auth.stub';
 | 
				
			||||||
import { fileStub } from 'test/fixtures/file.stub';
 | 
					import { fileStub } from 'test/fixtures/file.stub';
 | 
				
			||||||
@ -60,7 +60,7 @@ const _getAsset_1 = () => {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe('AssetService', () => {
 | 
					describe('AssetService', () => {
 | 
				
			||||||
  let sut: AssetService;
 | 
					  let sut: AssetServiceV1;
 | 
				
			||||||
  let accessMock: IAccessRepositoryMock;
 | 
					  let accessMock: IAccessRepositoryMock;
 | 
				
			||||||
  let assetRepositoryMockV1: jest.Mocked<IAssetRepositoryV1>;
 | 
					  let assetRepositoryMockV1: jest.Mocked<IAssetRepositoryV1>;
 | 
				
			||||||
  let assetMock: jest.Mocked<IAssetRepository>;
 | 
					  let assetMock: jest.Mocked<IAssetRepository>;
 | 
				
			||||||
@ -88,7 +88,7 @@ describe('AssetService', () => {
 | 
				
			|||||||
    storageMock = newStorageRepositoryMock();
 | 
					    storageMock = newStorageRepositoryMock();
 | 
				
			||||||
    userMock = newUserRepositoryMock();
 | 
					    userMock = newUserRepositoryMock();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sut = new AssetService(accessMock, assetRepositoryMockV1, assetMock, jobMock, libraryMock, storageMock, userMock);
 | 
					    sut = new AssetServiceV1(accessMock, assetRepositoryMockV1, assetMock, jobMock, libraryMock, storageMock, userMock);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    when(assetRepositoryMockV1.get)
 | 
					    when(assetRepositoryMockV1.get)
 | 
				
			||||||
      .calledWith(assetStub.livePhotoStillAsset.id)
 | 
					      .calledWith(assetStub.livePhotoStillAsset.id)
 | 
				
			||||||
@ -7,26 +7,29 @@ import {
 | 
				
			|||||||
} from '@nestjs/common';
 | 
					} from '@nestjs/common';
 | 
				
			||||||
import { AccessCore, Permission } from 'src/cores/access.core';
 | 
					import { AccessCore, Permission } from 'src/cores/access.core';
 | 
				
			||||||
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
 | 
					import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  AssetBulkUploadCheckResponseDto,
 | 
				
			||||||
 | 
					  AssetFileUploadResponseDto,
 | 
				
			||||||
 | 
					  AssetRejectReason,
 | 
				
			||||||
 | 
					  AssetUploadAction,
 | 
				
			||||||
 | 
					  CheckExistingAssetsResponseDto,
 | 
				
			||||||
 | 
					  CuratedLocationsResponseDto,
 | 
				
			||||||
 | 
					  CuratedObjectsResponseDto,
 | 
				
			||||||
 | 
					} from 'src/dtos/asset-v1-response.dto';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  AssetBulkUploadCheckDto,
 | 
				
			||||||
 | 
					  AssetSearchDto,
 | 
				
			||||||
 | 
					  CheckExistingAssetsDto,
 | 
				
			||||||
 | 
					  CreateAssetDto,
 | 
				
			||||||
 | 
					  GetAssetThumbnailDto,
 | 
				
			||||||
 | 
					  GetAssetThumbnailFormatEnum,
 | 
				
			||||||
 | 
					  ServeFileDto,
 | 
				
			||||||
 | 
					} from 'src/dtos/asset-v1.dto';
 | 
				
			||||||
import { AuthDto } from 'src/dtos/auth.dto';
 | 
					import { AuthDto } from 'src/dtos/auth.dto';
 | 
				
			||||||
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity';
 | 
					import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity';
 | 
				
			||||||
import { LibraryType } from 'src/entities/library.entity';
 | 
					import { LibraryType } from 'src/entities/library.entity';
 | 
				
			||||||
import { IAssetRepositoryV1 } from 'src/immich/api-v1/asset/asset-repository';
 | 
					 | 
				
			||||||
import { AssetBulkUploadCheckDto } from 'src/immich/api-v1/asset/dto/asset-check.dto';
 | 
					 | 
				
			||||||
import { AssetSearchDto } from 'src/immich/api-v1/asset/dto/asset-search.dto';
 | 
					 | 
				
			||||||
import { CheckExistingAssetsDto } from 'src/immich/api-v1/asset/dto/check-existing-assets.dto';
 | 
					 | 
				
			||||||
import { CreateAssetDto } from 'src/immich/api-v1/asset/dto/create-asset.dto';
 | 
					 | 
				
			||||||
import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from 'src/immich/api-v1/asset/dto/get-asset-thumbnail.dto';
 | 
					 | 
				
			||||||
import { ServeFileDto } from 'src/immich/api-v1/asset/dto/serve-file.dto';
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  AssetBulkUploadCheckResponseDto,
 | 
					 | 
				
			||||||
  AssetRejectReason,
 | 
					 | 
				
			||||||
  AssetUploadAction,
 | 
					 | 
				
			||||||
} from 'src/immich/api-v1/asset/response-dto/asset-check-response.dto';
 | 
					 | 
				
			||||||
import { AssetFileUploadResponseDto } from 'src/immich/api-v1/asset/response-dto/asset-file-upload-response.dto';
 | 
					 | 
				
			||||||
import { CheckExistingAssetsResponseDto } from 'src/immich/api-v1/asset/response-dto/check-existing-assets-response.dto';
 | 
					 | 
				
			||||||
import { CuratedLocationsResponseDto } from 'src/immich/api-v1/asset/response-dto/curated-locations-response.dto';
 | 
					 | 
				
			||||||
import { CuratedObjectsResponseDto } from 'src/immich/api-v1/asset/response-dto/curated-objects-response.dto';
 | 
					 | 
				
			||||||
import { IAccessRepository } from 'src/interfaces/access.interface';
 | 
					import { IAccessRepository } from 'src/interfaces/access.interface';
 | 
				
			||||||
 | 
					import { IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface';
 | 
				
			||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
 | 
					import { IAssetRepository } from 'src/interfaces/asset.interface';
 | 
				
			||||||
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
 | 
					import { IJobRepository, JobName } from 'src/interfaces/job.interface';
 | 
				
			||||||
import { ILibraryRepository } from 'src/interfaces/library.interface';
 | 
					import { ILibraryRepository } from 'src/interfaces/library.interface';
 | 
				
			||||||
@ -39,8 +42,9 @@ import { mimeTypes } from 'src/utils/mime-types';
 | 
				
			|||||||
import { QueryFailedError } from 'typeorm';
 | 
					import { QueryFailedError } from 'typeorm';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class AssetService {
 | 
					/** @deprecated */
 | 
				
			||||||
  readonly logger = new ImmichLogger(AssetService.name);
 | 
					export class AssetServiceV1 {
 | 
				
			||||||
 | 
					  readonly logger = new ImmichLogger(AssetServiceV1.name);
 | 
				
			||||||
  private access: AccessCore;
 | 
					  private access: AccessCore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
@ -1,4 +1,11 @@
 | 
				
			|||||||
import { basename, extname } from 'node:path';
 | 
					import { HttpException, StreamableFile } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { NextFunction, Response } from 'express';
 | 
				
			||||||
 | 
					import { access, constants } from 'node:fs/promises';
 | 
				
			||||||
 | 
					import { basename, extname, isAbsolute } from 'node:path';
 | 
				
			||||||
 | 
					import { promisify } from 'node:util';
 | 
				
			||||||
 | 
					import { ImmichReadStream } from 'src/interfaces/storage.interface';
 | 
				
			||||||
 | 
					import { ImmichLogger } from 'src/utils/logger';
 | 
				
			||||||
 | 
					import { isConnectionAborted } from 'src/utils/misc';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getFileNameWithoutExtension(path: string): string {
 | 
					export function getFileNameWithoutExtension(path: string): string {
 | 
				
			||||||
  return basename(path, extname(path));
 | 
					  return basename(path, extname(path));
 | 
				
			||||||
@ -23,3 +30,59 @@ export class ImmichFileResponse {
 | 
				
			|||||||
    Object.assign(this, response);
 | 
					    Object.assign(this, response);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					type SendFile = Parameters<Response['sendFile']>;
 | 
				
			||||||
 | 
					type SendFileOptions = SendFile[1];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const logger = new ImmichLogger('SendFile');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const sendFile = async (
 | 
				
			||||||
 | 
					  res: Response,
 | 
				
			||||||
 | 
					  next: NextFunction,
 | 
				
			||||||
 | 
					  handler: () => Promise<ImmichFileResponse>,
 | 
				
			||||||
 | 
					): Promise<void> => {
 | 
				
			||||||
 | 
					  const _sendFile = (path: string, options: SendFileOptions) =>
 | 
				
			||||||
 | 
					    promisify<string, SendFileOptions>(res.sendFile).bind(res)(path, options);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const file = await handler();
 | 
				
			||||||
 | 
					    switch (file.cacheControl) {
 | 
				
			||||||
 | 
					      case CacheControl.PRIVATE_WITH_CACHE: {
 | 
				
			||||||
 | 
					        res.set('Cache-Control', 'private, max-age=86400, no-transform');
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      case CacheControl.PRIVATE_WITHOUT_CACHE: {
 | 
				
			||||||
 | 
					        res.set('Cache-Control', 'private, no-cache, no-transform');
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.header('Content-Type', file.contentType);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const options: SendFileOptions = { dotfiles: 'allow' };
 | 
				
			||||||
 | 
					    if (!isAbsolute(file.path)) {
 | 
				
			||||||
 | 
					      options.root = process.cwd();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await access(file.path, constants.R_OK);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return _sendFile(file.path, options);
 | 
				
			||||||
 | 
					  } catch (error: Error | any) {
 | 
				
			||||||
 | 
					    // ignore client-closed connection
 | 
				
			||||||
 | 
					    if (isConnectionAborted(error)) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // log non-http errors
 | 
				
			||||||
 | 
					    if (error instanceof HttpException === false) {
 | 
				
			||||||
 | 
					      logger.error(`Unable to send file: ${error.name}`, error.stack);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.header('Cache-Control', 'none');
 | 
				
			||||||
 | 
					    next(error);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const asStreamableFile = ({ stream, type, length }: ImmichReadStream) => {
 | 
				
			||||||
 | 
					  return new StreamableFile(stream, { type, length });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -13,8 +13,7 @@ import { snakeCase, startCase } from 'lodash';
 | 
				
			|||||||
import { OpenTelemetryModuleOptions } from 'nestjs-otel/lib/interfaces';
 | 
					import { OpenTelemetryModuleOptions } from 'nestjs-otel/lib/interfaces';
 | 
				
			||||||
import { copyMetadataFromFunctionToFunction } from 'nestjs-otel/lib/opentelemetry.utils';
 | 
					import { copyMetadataFromFunctionToFunction } from 'nestjs-otel/lib/opentelemetry.utils';
 | 
				
			||||||
import { performance } from 'node:perf_hooks';
 | 
					import { performance } from 'node:perf_hooks';
 | 
				
			||||||
import { excludePaths } from 'src/config';
 | 
					import { excludePaths, serverVersion } from 'src/constants';
 | 
				
			||||||
import { serverVersion } from 'src/constants';
 | 
					 | 
				
			||||||
import { DecorateAll } from 'src/decorators';
 | 
					import { DecorateAll } from 'src/decorators';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let metricsEnabled = process.env.IMMICH_METRICS === 'true';
 | 
					let metricsEnabled = process.env.IMMICH_METRICS === 'true';
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,23 @@
 | 
				
			|||||||
import { CLIP_MODEL_INFO } from 'src/constants';
 | 
					import { INestApplication } from '@nestjs/common';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  DocumentBuilder,
 | 
				
			||||||
 | 
					  OpenAPIObject,
 | 
				
			||||||
 | 
					  SwaggerCustomOptions,
 | 
				
			||||||
 | 
					  SwaggerDocumentOptions,
 | 
				
			||||||
 | 
					  SwaggerModule,
 | 
				
			||||||
 | 
					} from '@nestjs/swagger';
 | 
				
			||||||
 | 
					import { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface';
 | 
				
			||||||
 | 
					import _ from 'lodash';
 | 
				
			||||||
 | 
					import { writeFileSync } from 'node:fs';
 | 
				
			||||||
 | 
					import path from 'node:path';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  CLIP_MODEL_INFO,
 | 
				
			||||||
 | 
					  IMMICH_ACCESS_COOKIE,
 | 
				
			||||||
 | 
					  IMMICH_API_KEY_HEADER,
 | 
				
			||||||
 | 
					  IMMICH_API_KEY_NAME,
 | 
				
			||||||
 | 
					  serverVersion,
 | 
				
			||||||
 | 
					} from 'src/constants';
 | 
				
			||||||
 | 
					import { Metadata } from 'src/middleware/auth.guard';
 | 
				
			||||||
import { ImmichLogger } from 'src/utils/logger';
 | 
					import { ImmichLogger } from 'src/utils/logger';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const isConnectionAborted = (error: Error | any) => error.code === 'ECONNABORTED';
 | 
					export const isConnectionAborted = (error: Error | any) => error.code === 'ECONNABORTED';
 | 
				
			||||||
@ -30,3 +49,130 @@ export function getCLIPModelInfo(modelName: string) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  return modelInfo;
 | 
					  return modelInfo;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function sortKeys<T>(target: T): T {
 | 
				
			||||||
 | 
					  if (!target || typeof target !== 'object' || Array.isArray(target)) {
 | 
				
			||||||
 | 
					    return target;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const result: Partial<T> = {};
 | 
				
			||||||
 | 
					  const keys = Object.keys(target).sort() as Array<keyof T>;
 | 
				
			||||||
 | 
					  for (const key of keys) {
 | 
				
			||||||
 | 
					    result[key] = sortKeys(target[key]);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return result as T;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const routeToErrorMessage = (methodName: string) =>
 | 
				
			||||||
 | 
					  'Failed to ' + methodName.replaceAll(/[A-Z]+/g, (letter) => ` ${letter.toLowerCase()}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const patchOpenAPI = (document: OpenAPIObject) => {
 | 
				
			||||||
 | 
					  document.paths = sortKeys(document.paths);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (document.components?.schemas) {
 | 
				
			||||||
 | 
					    const schemas = document.components.schemas as Record<string, SchemaObject>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    document.components.schemas = sortKeys(schemas);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const schema of Object.values(schemas)) {
 | 
				
			||||||
 | 
					      if (schema.properties) {
 | 
				
			||||||
 | 
					        schema.properties = sortKeys(schema.properties);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (schema.required) {
 | 
				
			||||||
 | 
					        schema.required = schema.required.sort();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  for (const [key, value] of Object.entries(document.paths)) {
 | 
				
			||||||
 | 
					    const newKey = key.replace('/api/', '/');
 | 
				
			||||||
 | 
					    delete document.paths[key];
 | 
				
			||||||
 | 
					    document.paths[newKey] = value;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  for (const path of Object.values(document.paths)) {
 | 
				
			||||||
 | 
					    const operations = {
 | 
				
			||||||
 | 
					      get: path.get,
 | 
				
			||||||
 | 
					      put: path.put,
 | 
				
			||||||
 | 
					      post: path.post,
 | 
				
			||||||
 | 
					      delete: path.delete,
 | 
				
			||||||
 | 
					      options: path.options,
 | 
				
			||||||
 | 
					      head: path.head,
 | 
				
			||||||
 | 
					      patch: path.patch,
 | 
				
			||||||
 | 
					      trace: path.trace,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const operation of Object.values(operations)) {
 | 
				
			||||||
 | 
					      if (!operation) {
 | 
				
			||||||
 | 
					        continue;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if ((operation.security || []).some((item) => !!item[Metadata.PUBLIC_SECURITY])) {
 | 
				
			||||||
 | 
					        delete operation.security;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (operation.summary === '') {
 | 
				
			||||||
 | 
					        delete operation.summary;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (operation.operationId) {
 | 
				
			||||||
 | 
					        // console.log(`${routeToErrorMessage(operation.operationId).padEnd(40)} (${operation.operationId})`);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (operation.description === '') {
 | 
				
			||||||
 | 
					        delete operation.description;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (operation.parameters) {
 | 
				
			||||||
 | 
					        operation.parameters = _.orderBy(operation.parameters, 'name');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return document;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useSwagger = (app: INestApplication, isDevelopment: boolean) => {
 | 
				
			||||||
 | 
					  const config = new DocumentBuilder()
 | 
				
			||||||
 | 
					    .setTitle('Immich')
 | 
				
			||||||
 | 
					    .setDescription('Immich API')
 | 
				
			||||||
 | 
					    .setVersion(serverVersion.toString())
 | 
				
			||||||
 | 
					    .addBearerAuth({
 | 
				
			||||||
 | 
					      type: 'http',
 | 
				
			||||||
 | 
					      scheme: 'Bearer',
 | 
				
			||||||
 | 
					      in: 'header',
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .addCookieAuth(IMMICH_ACCESS_COOKIE)
 | 
				
			||||||
 | 
					    .addApiKey(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        type: 'apiKey',
 | 
				
			||||||
 | 
					        in: 'header',
 | 
				
			||||||
 | 
					        name: IMMICH_API_KEY_HEADER,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      IMMICH_API_KEY_NAME,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .addServer('/api')
 | 
				
			||||||
 | 
					    .build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const options: SwaggerDocumentOptions = {
 | 
				
			||||||
 | 
					    operationIdFactory: (controllerKey: string, methodKey: string) => methodKey,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const specification = SwaggerModule.createDocument(app, config, options);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const customOptions: SwaggerCustomOptions = {
 | 
				
			||||||
 | 
					    swaggerOptions: {
 | 
				
			||||||
 | 
					      persistAuthorization: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    customSiteTitle: 'Immich API Documentation',
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  SwaggerModule.setup('doc', app, specification, customOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (isDevelopment) {
 | 
				
			||||||
 | 
					    // Generate API Documentation only in development mode
 | 
				
			||||||
 | 
					    const outputPath = path.resolve(process.cwd(), '../open-api/immich-openapi-specs.json');
 | 
				
			||||||
 | 
					    writeFileSync(outputPath, JSON.stringify(patchOpenAPI(specification), null, 2), { encoding: 'utf8' });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user