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:
Jason Rasmussen
2023-08-18 00:55:26 -04:00
committed by GitHub
parent 28d3d3e679
commit 2b839088c7
40 changed files with 805 additions and 187 deletions

View File

@@ -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"

View File

@@ -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';

View File

@@ -1,2 +1,2 @@
export * from './response-dto';
export * from './server-info.dto';
export * from './server-info.service';

View File

@@ -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';

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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[];
}

View File

@@ -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;
}

View File

@@ -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;
}

View 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;
}

View File

@@ -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', () => {

View File

@@ -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();

View File

@@ -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();
}