mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
refactor: server-info (#2038)
This commit is contained in:
parent
e10bbfa933
commit
b9bc621e2a
43 changed files with 632 additions and 420 deletions
|
|
@ -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,27 +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[] = [];
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IServerVersion } from 'apps/immich/src/constants/server_version.constant';
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
import { Controller, Get } from '@nestjs/common';
|
||||
import { ServerInfoService } from './server-info.service';
|
||||
import { serverVersion } from '../../constants/server_version.constant';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { ServerPingResponse } from './response-dto/server-ping-response.dto';
|
||||
import { ServerVersionReponseDto } from './response-dto/server-version-response.dto';
|
||||
import { ServerInfoResponseDto } from './response-dto/server-info-response.dto';
|
||||
import { ServerStatsResponseDto } from './response-dto/server-stats-response.dto';
|
||||
import { Authenticated } from '../../decorators/authenticated.decorator';
|
||||
|
||||
@ApiTags('Server Info')
|
||||
@Controller('server-info')
|
||||
export class ServerInfoController {
|
||||
constructor(private readonly serverInfoService: ServerInfoService) {}
|
||||
|
||||
@Get()
|
||||
async getServerInfo(): Promise<ServerInfoResponseDto> {
|
||||
return await this.serverInfoService.getServerInfo();
|
||||
}
|
||||
|
||||
@Get('/ping')
|
||||
async pingServer(): Promise<ServerPingResponse> {
|
||||
return new ServerPingResponse('pong');
|
||||
}
|
||||
|
||||
@Get('/version')
|
||||
async getServerVersion(): Promise<ServerVersionReponseDto> {
|
||||
return serverVersion;
|
||||
}
|
||||
|
||||
@Authenticated({ admin: true })
|
||||
@Get('/stats')
|
||||
async getStats(): Promise<ServerStatsResponseDto> {
|
||||
return await this.serverInfoService.getStats();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ServerInfoService } from './server-info.service';
|
||||
import { ServerInfoController } from './server-info.controller';
|
||||
import { UserEntity } from '@app/infra';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([UserEntity])],
|
||||
controllers: [ServerInfoController],
|
||||
providers: [ServerInfoService],
|
||||
})
|
||||
export class ServerInfoModule {}
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
import { APP_UPLOAD_LOCATION } from '@app/common/constants';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ServerInfoResponseDto } from './response-dto/server-info-response.dto';
|
||||
import diskusage from 'diskusage';
|
||||
import { ServerStatsResponseDto } from './response-dto/server-stats-response.dto';
|
||||
import { UsageByUserDto } from './response-dto/usage-by-user-response.dto';
|
||||
import { UserEntity } from '@app/infra';
|
||||
import { Repository } from 'typeorm';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { asHumanReadable } from '../../utils/human-readable.util';
|
||||
|
||||
@Injectable()
|
||||
export class ServerInfoService {
|
||||
constructor(
|
||||
@InjectRepository(UserEntity)
|
||||
private userRepository: Repository<UserEntity>,
|
||||
) {}
|
||||
|
||||
async getServerInfo(): Promise<ServerInfoResponseDto> {
|
||||
const diskInfo = await diskusage.check(APP_UPLOAD_LOCATION);
|
||||
|
||||
const usagePercentage = (((diskInfo.total - diskInfo.free) / diskInfo.total) * 100).toFixed(2);
|
||||
|
||||
const serverInfo = new ServerInfoResponseDto();
|
||||
serverInfo.diskAvailable = asHumanReadable(diskInfo.available);
|
||||
serverInfo.diskSize = asHumanReadable(diskInfo.total);
|
||||
serverInfo.diskUse = asHumanReadable(diskInfo.total - diskInfo.free);
|
||||
serverInfo.diskAvailableRaw = diskInfo.available;
|
||||
serverInfo.diskSizeRaw = diskInfo.total;
|
||||
serverInfo.diskUseRaw = diskInfo.total - diskInfo.free;
|
||||
serverInfo.diskUsagePercentage = parseFloat(usagePercentage);
|
||||
return serverInfo;
|
||||
}
|
||||
|
||||
async getStats(): Promise<ServerStatsResponseDto> {
|
||||
type UserStatsQueryResponse = {
|
||||
userId: string;
|
||||
userFirstName: string;
|
||||
userLastName: string;
|
||||
photos: string;
|
||||
videos: string;
|
||||
usage: string;
|
||||
};
|
||||
|
||||
const userStatsQueryResponse: UserStatsQueryResponse[] = await this.userRepository
|
||||
.createQueryBuilder('users')
|
||||
.select('users.id', 'userId')
|
||||
.addSelect('users.firstName', 'userFirstName')
|
||||
.addSelect('users.lastName', 'userLastName')
|
||||
.addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'IMAGE' AND assets.isVisible)`, 'photos')
|
||||
.addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'VIDEO' AND assets.isVisible)`, 'videos')
|
||||
.addSelect('COALESCE(SUM(exif.fileSizeInByte), 0)', 'usage')
|
||||
.leftJoin('users.assets', 'assets')
|
||||
.leftJoin('assets.exifInfo', 'exif')
|
||||
.groupBy('users.id')
|
||||
.orderBy('users.createdAt', 'ASC')
|
||||
.getRawMany();
|
||||
|
||||
const usageByUser = userStatsQueryResponse.map((userStats) => {
|
||||
const usage = new UsageByUserDto();
|
||||
usage.userId = userStats.userId;
|
||||
usage.userFirstName = userStats.userFirstName;
|
||||
usage.userLastName = userStats.userLastName;
|
||||
usage.photos = Number(userStats.photos);
|
||||
usage.videos = Number(userStats.videos);
|
||||
usage.usage = Number(userStats.usage);
|
||||
|
||||
return usage;
|
||||
});
|
||||
|
||||
const serverStats = new ServerStatsResponseDto();
|
||||
usageByUser.forEach((user) => {
|
||||
serverStats.photos += user.photos;
|
||||
serverStats.videos += user.videos;
|
||||
serverStats.usage += user.usage;
|
||||
});
|
||||
serverStats.usageByUser = usageByUser;
|
||||
|
||||
return serverStats;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ import { immichAppConfig } from '@app/common/config';
|
|||
import { Module, OnModuleInit } from '@nestjs/common';
|
||||
import { AssetModule } from './api-v1/asset/asset.module';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { ServerInfoModule } from './api-v1/server-info/server-info.module';
|
||||
import { AlbumModule } from './api-v1/album/album.module';
|
||||
import { AppController } from './app.controller';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
|
|
@ -17,6 +16,7 @@ import {
|
|||
JobController,
|
||||
OAuthController,
|
||||
SearchController,
|
||||
ServerInfoController,
|
||||
ShareController,
|
||||
SystemConfigController,
|
||||
UserController,
|
||||
|
|
@ -34,8 +34,6 @@ import { AuthGuard } from './middlewares/auth.guard';
|
|||
|
||||
AssetModule,
|
||||
|
||||
ServerInfoModule,
|
||||
|
||||
AlbumModule,
|
||||
|
||||
ScheduleModule.forRoot(),
|
||||
|
|
@ -52,6 +50,7 @@ import { AuthGuard } from './middlewares/auth.guard';
|
|||
JobController,
|
||||
OAuthController,
|
||||
SearchController,
|
||||
ServerInfoController,
|
||||
ShareController,
|
||||
SystemConfigController,
|
||||
UserController,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { APP_UPLOAD_LOCATION } from '@app/common/constants';
|
||||
import { APP_UPLOAD_LOCATION } from '@app/domain/domain.constant';
|
||||
import { BadRequestException, Logger, UnauthorizedException } from '@nestjs/common';
|
||||
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
|
||||
import { createHash, randomUUID } from 'crypto';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { APP_UPLOAD_LOCATION } from '@app/common/constants';
|
||||
import { APP_UPLOAD_LOCATION } from '@app/domain/domain.constant';
|
||||
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
|
||||
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
|
||||
import { Request } from 'express';
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
import pkg from 'package.json';
|
||||
|
||||
const [major, minor, patch] = pkg.version.split('.');
|
||||
|
||||
export interface IServerVersion {
|
||||
major: number;
|
||||
minor: number;
|
||||
patch: number;
|
||||
}
|
||||
|
||||
export const serverVersion: IServerVersion = {
|
||||
major: Number(major),
|
||||
minor: Number(minor),
|
||||
patch: Number(patch),
|
||||
};
|
||||
|
||||
export const SERVER_VERSION = `${serverVersion.major}.${serverVersion.minor}.${serverVersion.patch}`;
|
||||
|
|
@ -4,6 +4,7 @@ export * from './device-info.controller';
|
|||
export * from './job.controller';
|
||||
export * from './oauth.controller';
|
||||
export * from './search.controller';
|
||||
export * from './server-info.controller';
|
||||
export * from './share.controller';
|
||||
export * from './system-config.controller';
|
||||
export * from './user.controller';
|
||||
|
|
|
|||
37
server/apps/immich/src/controllers/server-info.controller.ts
Normal file
37
server/apps/immich/src/controllers/server-info.controller.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import {
|
||||
ServerInfoResponseDto,
|
||||
ServerInfoService,
|
||||
ServerPingResponse,
|
||||
ServerStatsResponseDto,
|
||||
ServerVersionReponseDto,
|
||||
} from '@app/domain';
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { Authenticated } from '../decorators/authenticated.decorator';
|
||||
|
||||
@ApiTags('Server Info')
|
||||
@Controller('server-info')
|
||||
export class ServerInfoController {
|
||||
constructor(private readonly service: ServerInfoService) {}
|
||||
|
||||
@Get()
|
||||
getServerInfo(): Promise<ServerInfoResponseDto> {
|
||||
return this.service.getInfo();
|
||||
}
|
||||
|
||||
@Get('/ping')
|
||||
pingServer(): ServerPingResponse {
|
||||
return this.service.ping();
|
||||
}
|
||||
|
||||
@Get('/version')
|
||||
getServerVersion(): ServerVersionReponseDto {
|
||||
return this.service.getVersion();
|
||||
}
|
||||
|
||||
@Authenticated({ admin: true })
|
||||
@Get('/stats')
|
||||
getStats(): Promise<ServerStatsResponseDto> {
|
||||
return this.service.getStats();
|
||||
}
|
||||
}
|
||||
|
|
@ -6,12 +6,11 @@ import cookieParser from 'cookie-parser';
|
|||
import { writeFileSync } from 'fs';
|
||||
import path from 'path';
|
||||
import { AppModule } from './app.module';
|
||||
import { SERVER_VERSION } from './constants/server_version.constant';
|
||||
import { RedisIoAdapter } from './middlewares/redis-io.adapter.middleware';
|
||||
import { json } from 'body-parser';
|
||||
import { patchOpenAPI } from './utils/patch-open-api.util';
|
||||
import { getLogLevels, MACHINE_LEARNING_ENABLED } from '@app/common';
|
||||
import { IMMICH_ACCESS_COOKIE, SearchService } from '@app/domain';
|
||||
import { SERVER_VERSION, IMMICH_ACCESS_COOKIE, SearchService } from '@app/domain';
|
||||
|
||||
const logger = new Logger('ImmichServer');
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { AssetEntity } from '@app/infra';
|
|||
import { BadRequestException, Injectable, InternalServerErrorException, Logger, StreamableFile } from '@nestjs/common';
|
||||
import archiver from 'archiver';
|
||||
import { extname } from 'path';
|
||||
import { asHumanReadable, HumanReadableSize } from '../../utils/human-readable.util';
|
||||
import { asHumanReadable, HumanReadableSize } from '@app/domain';
|
||||
|
||||
export interface DownloadArchive {
|
||||
stream: StreamableFile;
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
const KiB = Math.pow(1024, 1);
|
||||
const MiB = Math.pow(1024, 2);
|
||||
const GiB = Math.pow(1024, 3);
|
||||
const TiB = Math.pow(1024, 4);
|
||||
const PiB = Math.pow(1024, 5);
|
||||
|
||||
export const HumanReadableSize = { KiB, MiB, GiB, TiB, PiB };
|
||||
|
||||
export function asHumanReadable(bytes: number, precision = 1): string {
|
||||
const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB'];
|
||||
|
||||
let magnitude = 0;
|
||||
let remainder = bytes;
|
||||
while (remainder >= 1024) {
|
||||
if (magnitude + 1 < units.length) {
|
||||
magnitude++;
|
||||
remainder /= 1024;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return `${remainder.toFixed(magnitude == 0 ? 0 : precision)} ${units[magnitude]}`;
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { Logger } from '@nestjs/common';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { SERVER_VERSION } from 'apps/immich/src/constants/server_version.constant';
|
||||
import { SERVER_VERSION } from '@app/domain';
|
||||
import { getLogLevels } from '@app/common';
|
||||
import { RedisIoAdapter } from '../../immich/src/middlewares/redis-io.adapter.middleware';
|
||||
import { MicroservicesModule } from './microservices.module';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { APP_UPLOAD_LOCATION } from '@app/common/constants';
|
||||
import { AssetEntity, AssetType } from '@app/infra';
|
||||
import {
|
||||
APP_UPLOAD_LOCATION,
|
||||
IAssetJob,
|
||||
IAssetRepository,
|
||||
IBaseJob,
|
||||
|
|
@ -10,6 +9,7 @@ import {
|
|||
SystemConfigService,
|
||||
WithoutProperty,
|
||||
} from '@app/domain';
|
||||
import { AssetEntity, AssetType } from '@app/infra';
|
||||
import { Process, Processor } from '@nestjs/bull';
|
||||
import { Inject, Logger } from '@nestjs/common';
|
||||
import { Job } from 'bull';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue