mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
feat(web,server): server features (#3756)
* feat: server features * chore: open api * icon size --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
@@ -3248,6 +3248,27 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/server-info/features": {
|
||||
"get": {
|
||||
"operationId": "getServerFeatures",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ServerFeaturesDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Server Info"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/server-info/media-types": {
|
||||
"get": {
|
||||
"operationId": "getSupportedMediaTypes",
|
||||
@@ -3331,7 +3352,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ServerVersionReponseDto"
|
||||
"$ref": "#/components/schemas/ServerVersionResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -6331,6 +6352,33 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ServerFeaturesDto": {
|
||||
"properties": {
|
||||
"machineLearning": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"oauth": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"oauthAutoLaunch": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"passwordLogin": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"search": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"machineLearning",
|
||||
"search",
|
||||
"oauth",
|
||||
"oauthAutoLaunch",
|
||||
"passwordLogin"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ServerInfoResponseDto": {
|
||||
"properties": {
|
||||
"diskAvailable": {
|
||||
@@ -6450,7 +6498,7 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ServerVersionReponseDto": {
|
||||
"ServerVersionResponseDto": {
|
||||
"properties": {
|
||||
"major": {
|
||||
"type": "integer"
|
||||
|
||||
@@ -21,6 +21,8 @@ export const SERVER_VERSION = `${serverVersion.major}.${serverVersion.minor}.${s
|
||||
|
||||
export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload';
|
||||
|
||||
export const SEARCH_ENABLED = process.env.TYPESENSE_ENABLED !== 'false';
|
||||
|
||||
export const MACHINE_LEARNING_URL = process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003';
|
||||
export const MACHINE_LEARNING_ENABLED = MACHINE_LEARNING_URL !== 'false';
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './response-dto';
|
||||
export * from './server-info.dto';
|
||||
export * from './server-info.service';
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export * from './server-info-response.dto';
|
||||
export * from './server-ping-response.dto';
|
||||
export * from './server-stats-response.dto';
|
||||
export * from './server-version-response.dto';
|
||||
export * from './usage-by-user-response.dto';
|
||||
@@ -1,19 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class ServerInfoResponseDto {
|
||||
diskSize!: string;
|
||||
diskUse!: string;
|
||||
diskAvailable!: string;
|
||||
|
||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||
diskSizeRaw!: number;
|
||||
|
||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||
diskUseRaw!: number;
|
||||
|
||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||
diskAvailableRaw!: number;
|
||||
|
||||
@ApiProperty({ type: 'number', format: 'float' })
|
||||
diskUsagePercentage!: number;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { ApiResponseProperty } from '@nestjs/swagger';
|
||||
|
||||
export class ServerPingResponse {
|
||||
constructor(res: string) {
|
||||
this.res = res;
|
||||
}
|
||||
|
||||
@ApiResponseProperty({ type: String, example: 'pong' })
|
||||
res!: string;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { UsageByUserDto } from './usage-by-user-response.dto';
|
||||
|
||||
export class ServerStatsResponseDto {
|
||||
@ApiProperty({ type: 'integer' })
|
||||
photos = 0;
|
||||
|
||||
@ApiProperty({ type: 'integer' })
|
||||
videos = 0;
|
||||
|
||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||
usage = 0;
|
||||
|
||||
@ApiProperty({
|
||||
isArray: true,
|
||||
type: UsageByUserDto,
|
||||
title: 'Array of usage for each user',
|
||||
example: [
|
||||
{
|
||||
photos: 1,
|
||||
videos: 1,
|
||||
diskUsageRaw: 1,
|
||||
},
|
||||
],
|
||||
})
|
||||
usageByUser: UsageByUserDto[] = [];
|
||||
}
|
||||
|
||||
export class ServerMediaTypesResponseDto {
|
||||
video!: string[];
|
||||
image!: string[];
|
||||
sidecar!: string[];
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { IServerVersion } from '@app/domain';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class ServerVersionReponseDto implements IServerVersion {
|
||||
@ApiProperty({ type: 'integer' })
|
||||
major!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
minor!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
patch!: number;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class UsageByUserDto {
|
||||
@ApiProperty({ type: 'string' })
|
||||
userId!: string;
|
||||
@ApiProperty({ type: 'string' })
|
||||
userFirstName!: string;
|
||||
@ApiProperty({ type: 'string' })
|
||||
userLastName!: string;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
photos!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
videos!: number;
|
||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||
usage!: number;
|
||||
}
|
||||
89
server/src/domain/server-info/server-info.dto.ts
Normal file
89
server/src/domain/server-info/server-info.dto.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { IServerVersion } from '@app/domain';
|
||||
import { ApiProperty, ApiResponseProperty } from '@nestjs/swagger';
|
||||
|
||||
export class ServerPingResponse {
|
||||
@ApiResponseProperty({ type: String, example: 'pong' })
|
||||
res!: string;
|
||||
}
|
||||
|
||||
export class ServerInfoResponseDto {
|
||||
diskSize!: string;
|
||||
diskUse!: string;
|
||||
diskAvailable!: string;
|
||||
|
||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||
diskSizeRaw!: number;
|
||||
|
||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||
diskUseRaw!: number;
|
||||
|
||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||
diskAvailableRaw!: number;
|
||||
|
||||
@ApiProperty({ type: 'number', format: 'float' })
|
||||
diskUsagePercentage!: number;
|
||||
}
|
||||
|
||||
export class ServerVersionResponseDto implements IServerVersion {
|
||||
@ApiProperty({ type: 'integer' })
|
||||
major!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
minor!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
patch!: number;
|
||||
}
|
||||
|
||||
export class UsageByUserDto {
|
||||
@ApiProperty({ type: 'string' })
|
||||
userId!: string;
|
||||
@ApiProperty({ type: 'string' })
|
||||
userFirstName!: string;
|
||||
@ApiProperty({ type: 'string' })
|
||||
userLastName!: string;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
photos!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
videos!: number;
|
||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||
usage!: number;
|
||||
}
|
||||
|
||||
export class ServerStatsResponseDto {
|
||||
@ApiProperty({ type: 'integer' })
|
||||
photos = 0;
|
||||
|
||||
@ApiProperty({ type: 'integer' })
|
||||
videos = 0;
|
||||
|
||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||
usage = 0;
|
||||
|
||||
@ApiProperty({
|
||||
isArray: true,
|
||||
type: UsageByUserDto,
|
||||
title: 'Array of usage for each user',
|
||||
example: [
|
||||
{
|
||||
photos: 1,
|
||||
videos: 1,
|
||||
diskUsageRaw: 1,
|
||||
},
|
||||
],
|
||||
})
|
||||
usageByUser: UsageByUserDto[] = [];
|
||||
}
|
||||
|
||||
export class ServerMediaTypesResponseDto {
|
||||
video!: string[];
|
||||
image!: string[];
|
||||
sidecar!: string[];
|
||||
}
|
||||
|
||||
export class ServerFeaturesDto {
|
||||
machineLearning!: boolean;
|
||||
search!: boolean;
|
||||
|
||||
oauth!: boolean;
|
||||
oauthAutoLaunch!: boolean;
|
||||
passwordLogin!: boolean;
|
||||
}
|
||||
@@ -1,19 +1,22 @@
|
||||
import { newStorageRepositoryMock, newUserRepositoryMock } from '@test';
|
||||
import { newStorageRepositoryMock, newSystemConfigRepositoryMock, newUserRepositoryMock } from '@test';
|
||||
import { serverVersion } from '../domain.constant';
|
||||
import { ISystemConfigRepository } from '../index';
|
||||
import { IStorageRepository } from '../storage';
|
||||
import { IUserRepository } from '../user';
|
||||
import { ServerInfoService } from './server-info.service';
|
||||
|
||||
describe(ServerInfoService.name, () => {
|
||||
let sut: ServerInfoService;
|
||||
let configMock: jest.Mocked<ISystemConfigRepository>;
|
||||
let storageMock: jest.Mocked<IStorageRepository>;
|
||||
let userMock: jest.Mocked<IUserRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
configMock = newSystemConfigRepositoryMock();
|
||||
storageMock = newStorageRepositoryMock();
|
||||
userMock = newUserRepositoryMock();
|
||||
|
||||
sut = new ServerInfoService(userMock, storageMock);
|
||||
sut = new ServerInfoService(configMock, userMock, storageMock);
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
@@ -140,6 +143,19 @@ describe(ServerInfoService.name, () => {
|
||||
it('should respond the server version', () => {
|
||||
expect(sut.getVersion()).toEqual(serverVersion);
|
||||
});
|
||||
|
||||
describe('getFeatures', () => {
|
||||
it('should respond the server features', async () => {
|
||||
await expect(sut.getFeatures()).resolves.toEqual({
|
||||
machineLearning: true,
|
||||
oauth: false,
|
||||
oauthAutoLaunch: false,
|
||||
passwordLogin: true,
|
||||
search: true,
|
||||
});
|
||||
expect(configMock.load).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStats', () => {
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { mimeTypes, serverVersion } from '../domain.constant';
|
||||
import { MACHINE_LEARNING_ENABLED, mimeTypes, SEARCH_ENABLED, serverVersion } from '../domain.constant';
|
||||
import { asHumanReadable } from '../domain.util';
|
||||
import { IStorageRepository, StorageCore, StorageFolder } from '../storage';
|
||||
import { ISystemConfigRepository } from '../system-config';
|
||||
import { SystemConfigCore } from '../system-config/system-config.core';
|
||||
import { IUserRepository, UserStatsQueryResponse } from '../user';
|
||||
import {
|
||||
ServerFeaturesDto,
|
||||
ServerInfoResponseDto,
|
||||
ServerMediaTypesResponseDto,
|
||||
ServerPingResponse,
|
||||
ServerStatsResponseDto,
|
||||
UsageByUserDto,
|
||||
} from './response-dto';
|
||||
} from './server-info.dto';
|
||||
|
||||
@Injectable()
|
||||
export class ServerInfoService {
|
||||
private storageCore = new StorageCore();
|
||||
private configCore: SystemConfigCore;
|
||||
|
||||
constructor(
|
||||
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||
) {}
|
||||
) {
|
||||
this.configCore = new SystemConfigCore(configRepository);
|
||||
}
|
||||
|
||||
async getInfo(): Promise<ServerInfoResponseDto> {
|
||||
const libraryBase = this.storageCore.getBaseFolder(StorageFolder.LIBRARY);
|
||||
@@ -38,13 +45,27 @@ export class ServerInfoService {
|
||||
}
|
||||
|
||||
ping(): ServerPingResponse {
|
||||
return new ServerPingResponse('pong');
|
||||
return { res: 'pong' };
|
||||
}
|
||||
|
||||
getVersion() {
|
||||
return serverVersion;
|
||||
}
|
||||
|
||||
async getFeatures(): Promise<ServerFeaturesDto> {
|
||||
const config = await this.configCore.getConfig();
|
||||
|
||||
return {
|
||||
machineLearning: MACHINE_LEARNING_ENABLED,
|
||||
search: SEARCH_ENABLED,
|
||||
|
||||
// TODO: use these instead of `POST oauth/config`
|
||||
oauth: config.oauth.enabled,
|
||||
oauthAutoLaunch: config.oauth.autoLaunch,
|
||||
passwordLogin: config.passwordLogin.enabled,
|
||||
};
|
||||
}
|
||||
|
||||
async getStats(): Promise<ServerStatsResponseDto> {
|
||||
const userStats: UserStatsQueryResponse[] = await this.userRepository.getUserStats();
|
||||
const serverStats = new ServerStatsResponseDto();
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import {
|
||||
ServerFeaturesDto,
|
||||
ServerInfoResponseDto,
|
||||
ServerInfoService,
|
||||
ServerMediaTypesResponseDto,
|
||||
ServerPingResponse,
|
||||
ServerStatsResponseDto,
|
||||
ServerVersionReponseDto,
|
||||
ServerVersionResponseDto,
|
||||
} from '@app/domain';
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
@@ -24,25 +25,31 @@ export class ServerInfoController {
|
||||
}
|
||||
|
||||
@PublicRoute()
|
||||
@Get('/ping')
|
||||
@Get('ping')
|
||||
pingServer(): ServerPingResponse {
|
||||
return this.service.ping();
|
||||
}
|
||||
|
||||
@PublicRoute()
|
||||
@Get('/version')
|
||||
getServerVersion(): ServerVersionReponseDto {
|
||||
@Get('version')
|
||||
getServerVersion(): ServerVersionResponseDto {
|
||||
return this.service.getVersion();
|
||||
}
|
||||
|
||||
@PublicRoute()
|
||||
@Get('features')
|
||||
getServerFeatures(): Promise<ServerFeaturesDto> {
|
||||
return this.service.getFeatures();
|
||||
}
|
||||
|
||||
@AdminRoute()
|
||||
@Get('/stats')
|
||||
@Get('stats')
|
||||
getStats(): Promise<ServerStatsResponseDto> {
|
||||
return this.service.getStats();
|
||||
}
|
||||
|
||||
@PublicRoute()
|
||||
@Get('/media-types')
|
||||
@Get('media-types')
|
||||
getSupportedMediaTypes(): ServerMediaTypesResponseDto {
|
||||
return this.service.getSupportedMediaTypes();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user