mirror of
https://github.com/immich-app/immich.git
synced 2025-05-30 19:54:52 -04:00
refactor: server-info (#2038)
This commit is contained in:
parent
e10bbfa933
commit
b9bc621e2a
@ -1,17 +0,0 @@
|
|||||||
# Deployment checklist for iOS/Android/Server
|
|
||||||
|
|
||||||
[ ] Up version in [mobile/pubspec.yml](/mobile/pubspec.yaml)
|
|
||||||
|
|
||||||
[ ] Up version in [docker/docker-compose.yml](/docker/docker-compose.yml) for `immich_server` service
|
|
||||||
|
|
||||||
[ ] Up version in [docker/docker-compose.gpu.yml](/docker/docker-compose.gpu.yml) for `immich_server` service
|
|
||||||
|
|
||||||
[ ] Up version in [docker/docker-compose.dev.yml](/docker/docker-compose.dev.yml) for `immich_server` service
|
|
||||||
|
|
||||||
[ ] Up version in [server/src/constants/server_version.constant.ts](/server/src/constants/server_version.constant.ts)
|
|
||||||
|
|
||||||
[ ] Up version in iOS Fastlane [/mobile/ios/fastlane/Fastfile](/mobile/ios/fastlane/Fastfile)
|
|
||||||
|
|
||||||
[ ] Add changelog to [Android Fastlane F-droid folder](/mobile/android/fastlane/metadata/android/en-US/changelogs)
|
|
||||||
|
|
||||||
All of the version should be the same.
|
|
@ -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 { Module, OnModuleInit } from '@nestjs/common';
|
||||||
import { AssetModule } from './api-v1/asset/asset.module';
|
import { AssetModule } from './api-v1/asset/asset.module';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { ServerInfoModule } from './api-v1/server-info/server-info.module';
|
|
||||||
import { AlbumModule } from './api-v1/album/album.module';
|
import { AlbumModule } from './api-v1/album/album.module';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { ScheduleModule } from '@nestjs/schedule';
|
import { ScheduleModule } from '@nestjs/schedule';
|
||||||
@ -17,6 +16,7 @@ import {
|
|||||||
JobController,
|
JobController,
|
||||||
OAuthController,
|
OAuthController,
|
||||||
SearchController,
|
SearchController,
|
||||||
|
ServerInfoController,
|
||||||
ShareController,
|
ShareController,
|
||||||
SystemConfigController,
|
SystemConfigController,
|
||||||
UserController,
|
UserController,
|
||||||
@ -34,8 +34,6 @@ import { AuthGuard } from './middlewares/auth.guard';
|
|||||||
|
|
||||||
AssetModule,
|
AssetModule,
|
||||||
|
|
||||||
ServerInfoModule,
|
|
||||||
|
|
||||||
AlbumModule,
|
AlbumModule,
|
||||||
|
|
||||||
ScheduleModule.forRoot(),
|
ScheduleModule.forRoot(),
|
||||||
@ -52,6 +50,7 @@ import { AuthGuard } from './middlewares/auth.guard';
|
|||||||
JobController,
|
JobController,
|
||||||
OAuthController,
|
OAuthController,
|
||||||
SearchController,
|
SearchController,
|
||||||
|
ServerInfoController,
|
||||||
ShareController,
|
ShareController,
|
||||||
SystemConfigController,
|
SystemConfigController,
|
||||||
UserController,
|
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 { BadRequestException, Logger, UnauthorizedException } from '@nestjs/common';
|
||||||
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
|
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
|
||||||
import { createHash, randomUUID } from 'crypto';
|
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 { BadRequestException, UnauthorizedException } from '@nestjs/common';
|
||||||
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
|
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
|
@ -4,6 +4,7 @@ export * from './device-info.controller';
|
|||||||
export * from './job.controller';
|
export * from './job.controller';
|
||||||
export * from './oauth.controller';
|
export * from './oauth.controller';
|
||||||
export * from './search.controller';
|
export * from './search.controller';
|
||||||
|
export * from './server-info.controller';
|
||||||
export * from './share.controller';
|
export * from './share.controller';
|
||||||
export * from './system-config.controller';
|
export * from './system-config.controller';
|
||||||
export * from './user.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 { writeFileSync } from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import { SERVER_VERSION } from './constants/server_version.constant';
|
|
||||||
import { RedisIoAdapter } from './middlewares/redis-io.adapter.middleware';
|
import { RedisIoAdapter } from './middlewares/redis-io.adapter.middleware';
|
||||||
import { json } from 'body-parser';
|
import { json } from 'body-parser';
|
||||||
import { patchOpenAPI } from './utils/patch-open-api.util';
|
import { patchOpenAPI } from './utils/patch-open-api.util';
|
||||||
import { getLogLevels, MACHINE_LEARNING_ENABLED } from '@app/common';
|
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');
|
const logger = new Logger('ImmichServer');
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { AssetEntity } from '@app/infra';
|
|||||||
import { BadRequestException, Injectable, InternalServerErrorException, Logger, StreamableFile } from '@nestjs/common';
|
import { BadRequestException, Injectable, InternalServerErrorException, Logger, StreamableFile } from '@nestjs/common';
|
||||||
import archiver from 'archiver';
|
import archiver from 'archiver';
|
||||||
import { extname } from 'path';
|
import { extname } from 'path';
|
||||||
import { asHumanReadable, HumanReadableSize } from '../../utils/human-readable.util';
|
import { asHumanReadable, HumanReadableSize } from '@app/domain';
|
||||||
|
|
||||||
export interface DownloadArchive {
|
export interface DownloadArchive {
|
||||||
stream: StreamableFile;
|
stream: StreamableFile;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
import { NestFactory } from '@nestjs/core';
|
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 { getLogLevels } from '@app/common';
|
||||||
import { RedisIoAdapter } from '../../immich/src/middlewares/redis-io.adapter.middleware';
|
import { RedisIoAdapter } from '../../immich/src/middlewares/redis-io.adapter.middleware';
|
||||||
import { MicroservicesModule } from './microservices.module';
|
import { MicroservicesModule } from './microservices.module';
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { APP_UPLOAD_LOCATION } from '@app/common/constants';
|
|
||||||
import { AssetEntity, AssetType } from '@app/infra';
|
|
||||||
import {
|
import {
|
||||||
|
APP_UPLOAD_LOCATION,
|
||||||
IAssetJob,
|
IAssetJob,
|
||||||
IAssetRepository,
|
IAssetRepository,
|
||||||
IBaseJob,
|
IBaseJob,
|
||||||
@ -10,6 +9,7 @@ import {
|
|||||||
SystemConfigService,
|
SystemConfigService,
|
||||||
WithoutProperty,
|
WithoutProperty,
|
||||||
} from '@app/domain';
|
} from '@app/domain';
|
||||||
|
import { AssetEntity, AssetType } from '@app/infra';
|
||||||
import { Process, Processor } from '@nestjs/bull';
|
import { Process, Processor } from '@nestjs/bull';
|
||||||
import { Inject, Logger } from '@nestjs/common';
|
import { Inject, Logger } from '@nestjs/common';
|
||||||
import { Job } from 'bull';
|
import { Job } from 'bull';
|
||||||
|
@ -834,6 +834,102 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/server-info": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "getServerInfo",
|
||||||
|
"description": "",
|
||||||
|
"parameters": [],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ServerInfoResponseDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Server Info"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/server-info/ping": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "pingServer",
|
||||||
|
"description": "",
|
||||||
|
"parameters": [],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ServerPingResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Server Info"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/server-info/version": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "getServerVersion",
|
||||||
|
"description": "",
|
||||||
|
"parameters": [],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ServerVersionReponseDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Server Info"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/server-info/stats": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "getStats",
|
||||||
|
"description": "",
|
||||||
|
"parameters": [],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ServerStatsResponseDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Server Info"
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/share": {
|
"/share": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getAllSharedLinks",
|
"operationId": "getAllSharedLinks",
|
||||||
@ -3270,102 +3366,6 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"/server-info": {
|
|
||||||
"get": {
|
|
||||||
"operationId": "getServerInfo",
|
|
||||||
"description": "",
|
|
||||||
"parameters": [],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/ServerInfoResponseDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"Server Info"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/server-info/ping": {
|
|
||||||
"get": {
|
|
||||||
"operationId": "pingServer",
|
|
||||||
"description": "",
|
|
||||||
"parameters": [],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/ServerPingResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"Server Info"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/server-info/version": {
|
|
||||||
"get": {
|
|
||||||
"operationId": "getServerVersion",
|
|
||||||
"description": "",
|
|
||||||
"parameters": [],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/ServerVersionReponseDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"Server Info"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/server-info/stats": {
|
|
||||||
"get": {
|
|
||||||
"operationId": "getStats",
|
|
||||||
"description": "",
|
|
||||||
"parameters": [],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/ServerStatsResponseDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"Server Info"
|
|
||||||
],
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearer": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
@ -4330,6 +4330,148 @@
|
|||||||
"items"
|
"items"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"ServerInfoResponseDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"diskSizeRaw": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"diskUseRaw": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"diskAvailableRaw": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"diskUsagePercentage": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float"
|
||||||
|
},
|
||||||
|
"diskSize": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"diskUse": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"diskAvailable": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"diskSizeRaw",
|
||||||
|
"diskUseRaw",
|
||||||
|
"diskAvailableRaw",
|
||||||
|
"diskUsagePercentage",
|
||||||
|
"diskSize",
|
||||||
|
"diskUse",
|
||||||
|
"diskAvailable"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ServerPingResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"res": {
|
||||||
|
"type": "string",
|
||||||
|
"readOnly": true,
|
||||||
|
"example": "pong"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"res"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ServerVersionReponseDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"major": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"minor": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"patch": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"major",
|
||||||
|
"minor",
|
||||||
|
"patch"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"UsageByUserDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"userId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"userFirstName": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"userLastName": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"photos": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"videos": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"usage": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"userId",
|
||||||
|
"userFirstName",
|
||||||
|
"userLastName",
|
||||||
|
"photos",
|
||||||
|
"videos",
|
||||||
|
"usage"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ServerStatsResponseDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"photos": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"videos": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"usage": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 0,
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"usageByUser": {
|
||||||
|
"default": [],
|
||||||
|
"title": "Array of usage for each user",
|
||||||
|
"example": [
|
||||||
|
{
|
||||||
|
"photos": 1,
|
||||||
|
"videos": 1,
|
||||||
|
"diskUsageRaw": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/UsageByUserDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"photos",
|
||||||
|
"videos",
|
||||||
|
"usage",
|
||||||
|
"usageByUser"
|
||||||
|
]
|
||||||
|
},
|
||||||
"SharedLinkType": {
|
"SharedLinkType": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
@ -5271,148 +5413,6 @@
|
|||||||
"required": [
|
"required": [
|
||||||
"albumId"
|
"albumId"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
"ServerInfoResponseDto": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"diskSizeRaw": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int64"
|
|
||||||
},
|
|
||||||
"diskUseRaw": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int64"
|
|
||||||
},
|
|
||||||
"diskAvailableRaw": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int64"
|
|
||||||
},
|
|
||||||
"diskUsagePercentage": {
|
|
||||||
"type": "number",
|
|
||||||
"format": "float"
|
|
||||||
},
|
|
||||||
"diskSize": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"diskUse": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"diskAvailable": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"diskSizeRaw",
|
|
||||||
"diskUseRaw",
|
|
||||||
"diskAvailableRaw",
|
|
||||||
"diskUsagePercentage",
|
|
||||||
"diskSize",
|
|
||||||
"diskUse",
|
|
||||||
"diskAvailable"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"ServerPingResponse": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"res": {
|
|
||||||
"type": "string",
|
|
||||||
"readOnly": true,
|
|
||||||
"example": "pong"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"res"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"ServerVersionReponseDto": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"major": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"minor": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"patch": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"major",
|
|
||||||
"minor",
|
|
||||||
"patch"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"UsageByUserDto": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"userId": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"userFirstName": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"userLastName": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"photos": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"videos": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"usage": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int64"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"userId",
|
|
||||||
"userFirstName",
|
|
||||||
"userLastName",
|
|
||||||
"photos",
|
|
||||||
"videos",
|
|
||||||
"usage"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"ServerStatsResponseDto": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"photos": {
|
|
||||||
"type": "integer",
|
|
||||||
"default": 0
|
|
||||||
},
|
|
||||||
"videos": {
|
|
||||||
"type": "integer",
|
|
||||||
"default": 0
|
|
||||||
},
|
|
||||||
"usage": {
|
|
||||||
"type": "integer",
|
|
||||||
"default": 0,
|
|
||||||
"format": "int64"
|
|
||||||
},
|
|
||||||
"usageByUser": {
|
|
||||||
"default": [],
|
|
||||||
"title": "Array of usage for each user",
|
|
||||||
"example": [
|
|
||||||
{
|
|
||||||
"photos": 1,
|
|
||||||
"videos": 1,
|
|
||||||
"diskUsageRaw": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/UsageByUserDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"photos",
|
|
||||||
"videos",
|
|
||||||
"usage",
|
|
||||||
"usageByUser"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import { BadRequestException } from '@nestjs/common';
|
import { BadRequestException } from '@nestjs/common';
|
||||||
|
|
||||||
export * from './upload_location.constant';
|
|
||||||
|
|
||||||
export const MACHINE_LEARNING_URL = process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003';
|
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';
|
export const MACHINE_LEARNING_ENABLED = MACHINE_LEARNING_URL !== 'false';
|
||||||
|
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export const APP_UPLOAD_LOCATION = './upload';
|
|
@ -1,4 +1,4 @@
|
|||||||
import pkg from 'package.json';
|
import pkg from '../../../package.json';
|
||||||
|
|
||||||
const [major, minor, patch] = pkg.version.split('.');
|
const [major, minor, patch] = pkg.version.split('.');
|
||||||
|
|
||||||
@ -15,3 +15,5 @@ export const serverVersion: IServerVersion = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SERVER_VERSION = `${serverVersion.major}.${serverVersion.minor}.${serverVersion.patch}`;
|
export const SERVER_VERSION = `${serverVersion.major}.${serverVersion.minor}.${serverVersion.patch}`;
|
||||||
|
|
||||||
|
export const APP_UPLOAD_LOCATION = './upload';
|
@ -7,6 +7,7 @@ import { JobService } from './job';
|
|||||||
import { MediaService } from './media';
|
import { MediaService } from './media';
|
||||||
import { OAuthService } from './oauth';
|
import { OAuthService } from './oauth';
|
||||||
import { SearchService } from './search';
|
import { SearchService } from './search';
|
||||||
|
import { ServerInfoService } from './server-info';
|
||||||
import { ShareService } from './share';
|
import { ShareService } from './share';
|
||||||
import { SmartInfoService } from './smart-info';
|
import { SmartInfoService } from './smart-info';
|
||||||
import { StorageService } from './storage';
|
import { StorageService } from './storage';
|
||||||
@ -22,6 +23,7 @@ const providers: Provider[] = [
|
|||||||
JobService,
|
JobService,
|
||||||
MediaService,
|
MediaService,
|
||||||
OAuthService,
|
OAuthService,
|
||||||
|
ServerInfoService,
|
||||||
SmartInfoService,
|
SmartInfoService,
|
||||||
StorageService,
|
StorageService,
|
||||||
StorageTemplateService,
|
StorageTemplateService,
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
|
import { basename, extname } from 'node:path';
|
||||||
|
|
||||||
|
export function getFileNameWithoutExtension(path: string): string {
|
||||||
|
return basename(path, extname(path));
|
||||||
|
}
|
||||||
|
|
||||||
const KiB = Math.pow(1024, 1);
|
const KiB = Math.pow(1024, 1);
|
||||||
const MiB = Math.pow(1024, 2);
|
const MiB = Math.pow(1024, 2);
|
||||||
const GiB = Math.pow(1024, 3);
|
const GiB = Math.pow(1024, 3);
|
@ -5,11 +5,14 @@ export * from './auth';
|
|||||||
export * from './communication';
|
export * from './communication';
|
||||||
export * from './crypto';
|
export * from './crypto';
|
||||||
export * from './device-info';
|
export * from './device-info';
|
||||||
|
export * from './domain.constant';
|
||||||
export * from './domain.module';
|
export * from './domain.module';
|
||||||
|
export * from './domain.util';
|
||||||
export * from './job';
|
export * from './job';
|
||||||
export * from './media';
|
export * from './media';
|
||||||
export * from './oauth';
|
export * from './oauth';
|
||||||
export * from './search';
|
export * from './search';
|
||||||
|
export * from './server-info';
|
||||||
export * from './share';
|
export * from './share';
|
||||||
export * from './smart-info';
|
export * from './smart-info';
|
||||||
export * from './storage';
|
export * from './storage';
|
||||||
@ -18,4 +21,3 @@ export * from './system-config';
|
|||||||
export * from './tag';
|
export * from './tag';
|
||||||
export * from './user';
|
export * from './user';
|
||||||
export * from './user-token';
|
export * from './user-token';
|
||||||
export * from './util';
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { APP_UPLOAD_LOCATION } from '@app/common';
|
|
||||||
import { AssetType } from '@app/infra/db/entities';
|
import { AssetType } from '@app/infra/db/entities';
|
||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import sanitize from 'sanitize-filename';
|
import sanitize from 'sanitize-filename';
|
||||||
import { IAssetRepository, mapAsset, WithoutProperty } from '../asset';
|
import { IAssetRepository, mapAsset, WithoutProperty } from '../asset';
|
||||||
import { CommunicationEvent, ICommunicationRepository } from '../communication';
|
import { CommunicationEvent, ICommunicationRepository } from '../communication';
|
||||||
|
import { APP_UPLOAD_LOCATION } from '../domain.constant';
|
||||||
import { IAssetJob, IBaseJob, IJobRepository, JobName } from '../job';
|
import { IAssetJob, IBaseJob, IJobRepository, JobName } from '../job';
|
||||||
import { IStorageRepository } from '../storage';
|
import { IStorageRepository } from '../storage';
|
||||||
import { IMediaRepository } from './media.repository';
|
import { IMediaRepository } from './media.repository';
|
||||||
|
2
server/libs/domain/src/server-info/index.ts
Normal file
2
server/libs/domain/src/server-info/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './response-dto';
|
||||||
|
export * from './server-info.service';
|
5
server/libs/domain/src/server-info/response-dto/index.ts
Normal file
5
server/libs/domain/src/server-info/response-dto/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
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,5 +1,5 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IServerVersion } from 'apps/immich/src/constants/server_version.constant';
|
import { IServerVersion } from '@app/domain';
|
||||||
|
|
||||||
export class ServerVersionReponseDto implements IServerVersion {
|
export class ServerVersionReponseDto implements IServerVersion {
|
||||||
@ApiProperty({ type: 'integer' })
|
@ApiProperty({ type: 'integer' })
|
209
server/libs/domain/src/server-info/server-info.service.spec.ts
Normal file
209
server/libs/domain/src/server-info/server-info.service.spec.ts
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
import { newStorageRepositoryMock, newUserRepositoryMock } from '../../test';
|
||||||
|
import { serverVersion } from '../domain.constant';
|
||||||
|
import { IStorageRepository } from '../storage';
|
||||||
|
import { IUserRepository } from '../user';
|
||||||
|
import { ServerInfoService } from './server-info.service';
|
||||||
|
|
||||||
|
describe(ServerInfoService.name, () => {
|
||||||
|
let sut: ServerInfoService;
|
||||||
|
let storageMock: jest.Mocked<IStorageRepository>;
|
||||||
|
let userMock: jest.Mocked<IUserRepository>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
storageMock = newStorageRepositoryMock();
|
||||||
|
userMock = newUserRepositoryMock();
|
||||||
|
|
||||||
|
sut = new ServerInfoService(userMock, storageMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work', () => {
|
||||||
|
expect(sut).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getInfo', () => {
|
||||||
|
it('should return the disk space as B', async () => {
|
||||||
|
storageMock.checkDiskUsage.mockResolvedValue({ free: 200, available: 300, total: 500 });
|
||||||
|
|
||||||
|
await expect(sut.getInfo()).resolves.toEqual({
|
||||||
|
diskAvailable: '300 B',
|
||||||
|
diskAvailableRaw: 300,
|
||||||
|
diskSize: '500 B',
|
||||||
|
diskSizeRaw: 500,
|
||||||
|
diskUsagePercentage: 60,
|
||||||
|
diskUse: '300 B',
|
||||||
|
diskUseRaw: 300,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('./upload');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the disk space as KiB', async () => {
|
||||||
|
storageMock.checkDiskUsage.mockResolvedValue({ free: 200_000, available: 300_000, total: 500_000 });
|
||||||
|
|
||||||
|
await expect(sut.getInfo()).resolves.toEqual({
|
||||||
|
diskAvailable: '293.0 KiB',
|
||||||
|
diskAvailableRaw: 300000,
|
||||||
|
diskSize: '488.3 KiB',
|
||||||
|
diskSizeRaw: 500000,
|
||||||
|
diskUsagePercentage: 60,
|
||||||
|
diskUse: '293.0 KiB',
|
||||||
|
diskUseRaw: 300000,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('./upload');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the disk space as MiB', async () => {
|
||||||
|
storageMock.checkDiskUsage.mockResolvedValue({ free: 200_000_000, available: 300_000_000, total: 500_000_000 });
|
||||||
|
|
||||||
|
await expect(sut.getInfo()).resolves.toEqual({
|
||||||
|
diskAvailable: '286.1 MiB',
|
||||||
|
diskAvailableRaw: 300000000,
|
||||||
|
diskSize: '476.8 MiB',
|
||||||
|
diskSizeRaw: 500000000,
|
||||||
|
diskUsagePercentage: 60,
|
||||||
|
diskUse: '286.1 MiB',
|
||||||
|
diskUseRaw: 300000000,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('./upload');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the disk space as GiB', async () => {
|
||||||
|
storageMock.checkDiskUsage.mockResolvedValue({
|
||||||
|
free: 200_000_000_000,
|
||||||
|
available: 300_000_000_000,
|
||||||
|
total: 500_000_000_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(sut.getInfo()).resolves.toEqual({
|
||||||
|
diskAvailable: '279.4 GiB',
|
||||||
|
diskAvailableRaw: 300000000000,
|
||||||
|
diskSize: '465.7 GiB',
|
||||||
|
diskSizeRaw: 500000000000,
|
||||||
|
diskUsagePercentage: 60,
|
||||||
|
diskUse: '279.4 GiB',
|
||||||
|
diskUseRaw: 300000000000,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('./upload');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the disk space as TiB', async () => {
|
||||||
|
storageMock.checkDiskUsage.mockResolvedValue({
|
||||||
|
free: 200_000_000_000_000,
|
||||||
|
available: 300_000_000_000_000,
|
||||||
|
total: 500_000_000_000_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(sut.getInfo()).resolves.toEqual({
|
||||||
|
diskAvailable: '272.8 TiB',
|
||||||
|
diskAvailableRaw: 300000000000000,
|
||||||
|
diskSize: '454.7 TiB',
|
||||||
|
diskSizeRaw: 500000000000000,
|
||||||
|
diskUsagePercentage: 60,
|
||||||
|
diskUse: '272.8 TiB',
|
||||||
|
diskUseRaw: 300000000000000,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('./upload');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the disk space as PiB', async () => {
|
||||||
|
storageMock.checkDiskUsage.mockResolvedValue({
|
||||||
|
free: 200_000_000_000_000_000,
|
||||||
|
available: 300_000_000_000_000_000,
|
||||||
|
total: 500_000_000_000_000_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(sut.getInfo()).resolves.toEqual({
|
||||||
|
diskAvailable: '266.5 PiB',
|
||||||
|
diskAvailableRaw: 300000000000000000,
|
||||||
|
diskSize: '444.1 PiB',
|
||||||
|
diskSizeRaw: 500000000000000000,
|
||||||
|
diskUsagePercentage: 60,
|
||||||
|
diskUse: '266.5 PiB',
|
||||||
|
diskUseRaw: 300000000000000000,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('./upload');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ping', () => {
|
||||||
|
it('should respond with pong', () => {
|
||||||
|
expect(sut.ping()).toEqual({ res: 'pong' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getVersion', () => {
|
||||||
|
it('should respond the server version', () => {
|
||||||
|
expect(sut.getVersion()).toEqual(serverVersion);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getStats', () => {
|
||||||
|
it('should total up usage by user', async () => {
|
||||||
|
userMock.getUserStats.mockResolvedValue([
|
||||||
|
{
|
||||||
|
userId: 'user1',
|
||||||
|
userFirstName: '1',
|
||||||
|
userLastName: 'User',
|
||||||
|
photos: 10,
|
||||||
|
videos: 11,
|
||||||
|
usage: 12345,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: 'user2',
|
||||||
|
userFirstName: '2',
|
||||||
|
userLastName: 'User',
|
||||||
|
photos: 10,
|
||||||
|
videos: 20,
|
||||||
|
usage: 123456,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: 'user3',
|
||||||
|
userFirstName: '3',
|
||||||
|
userLastName: 'User',
|
||||||
|
photos: 100,
|
||||||
|
videos: 0,
|
||||||
|
usage: 987654,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expect(sut.getStats()).resolves.toEqual({
|
||||||
|
photos: 120,
|
||||||
|
videos: 31,
|
||||||
|
usage: 1123455,
|
||||||
|
usageByUser: [
|
||||||
|
{
|
||||||
|
photos: 10,
|
||||||
|
usage: 12345,
|
||||||
|
userFirstName: '1',
|
||||||
|
userId: 'user1',
|
||||||
|
userLastName: 'User',
|
||||||
|
videos: 11,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
photos: 10,
|
||||||
|
usage: 123456,
|
||||||
|
userFirstName: '2',
|
||||||
|
userId: 'user2',
|
||||||
|
userLastName: 'User',
|
||||||
|
videos: 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
photos: 100,
|
||||||
|
usage: 987654,
|
||||||
|
userFirstName: '3',
|
||||||
|
userId: 'user3',
|
||||||
|
userLastName: 'User',
|
||||||
|
videos: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(userMock.getUserStats).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
60
server/libs/domain/src/server-info/server-info.service.ts
Normal file
60
server/libs/domain/src/server-info/server-info.service.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { APP_UPLOAD_LOCATION, serverVersion } from '../domain.constant';
|
||||||
|
import { asHumanReadable } from '../domain.util';
|
||||||
|
import { IStorageRepository } from '../storage';
|
||||||
|
import { IUserRepository, UserStatsQueryResponse } from '../user';
|
||||||
|
import { ServerInfoResponseDto, ServerPingResponse, ServerStatsResponseDto, UsageByUserDto } from './response-dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ServerInfoService {
|
||||||
|
constructor(
|
||||||
|
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||||
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getInfo(): Promise<ServerInfoResponseDto> {
|
||||||
|
const diskInfo = await this.storageRepository.checkDiskUsage(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
ping(): ServerPingResponse {
|
||||||
|
return new ServerPingResponse('pong');
|
||||||
|
}
|
||||||
|
|
||||||
|
getVersion() {
|
||||||
|
return serverVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStats(): Promise<ServerStatsResponseDto> {
|
||||||
|
const userStats: UserStatsQueryResponse[] = await this.userRepository.getUserStats();
|
||||||
|
const serverStats = new ServerStatsResponseDto();
|
||||||
|
|
||||||
|
for (const user of userStats) {
|
||||||
|
const usage = new UsageByUserDto();
|
||||||
|
usage.userId = user.userId;
|
||||||
|
usage.userFirstName = user.userFirstName;
|
||||||
|
usage.userLastName = user.userLastName;
|
||||||
|
usage.photos = user.photos;
|
||||||
|
usage.videos = user.videos;
|
||||||
|
usage.usage = user.usage;
|
||||||
|
|
||||||
|
serverStats.photos += usage.photos;
|
||||||
|
serverStats.videos += usage.videos;
|
||||||
|
serverStats.usage += usage.usage;
|
||||||
|
serverStats.usageByUser.push(usage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return serverStats;
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,3 @@
|
|||||||
import { APP_UPLOAD_LOCATION } from '@app/common';
|
|
||||||
import {
|
import {
|
||||||
IStorageRepository,
|
IStorageRepository,
|
||||||
ISystemConfigRepository,
|
ISystemConfigRepository,
|
||||||
@ -15,6 +14,7 @@ import handlebar from 'handlebars';
|
|||||||
import * as luxon from 'luxon';
|
import * as luxon from 'luxon';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import sanitize from 'sanitize-filename';
|
import sanitize from 'sanitize-filename';
|
||||||
|
import { APP_UPLOAD_LOCATION } from '../domain.constant';
|
||||||
import { SystemConfigCore } from '../system-config/system-config.core';
|
import { SystemConfigCore } from '../system-config/system-config.core';
|
||||||
|
|
||||||
export class StorageTemplateCore {
|
export class StorageTemplateCore {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { APP_UPLOAD_LOCATION } from '@app/common';
|
|
||||||
import { AssetEntity, SystemConfig } from '@app/infra/db/entities';
|
import { AssetEntity, SystemConfig } from '@app/infra/db/entities';
|
||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import { IAssetRepository } from '../asset/asset.repository';
|
import { IAssetRepository } from '../asset/asset.repository';
|
||||||
|
import { APP_UPLOAD_LOCATION } from '../domain.constant';
|
||||||
import { IStorageRepository } from '../storage/storage.repository';
|
import { IStorageRepository } from '../storage/storage.repository';
|
||||||
import { INITIAL_SYSTEM_CONFIG, ISystemConfigRepository } from '../system-config';
|
import { INITIAL_SYSTEM_CONFIG, ISystemConfigRepository } from '../system-config';
|
||||||
import { StorageTemplateCore } from './storage-template.core';
|
import { StorageTemplateCore } from './storage-template.core';
|
||||||
|
@ -6,6 +6,12 @@ export interface ImmichReadStream {
|
|||||||
length: number;
|
length: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DiskUsage {
|
||||||
|
available: number;
|
||||||
|
free: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
export const IStorageRepository = 'IStorageRepository';
|
export const IStorageRepository = 'IStorageRepository';
|
||||||
|
|
||||||
export interface IStorageRepository {
|
export interface IStorageRepository {
|
||||||
@ -16,4 +22,5 @@ export interface IStorageRepository {
|
|||||||
moveFile(source: string, target: string): Promise<void>;
|
moveFile(source: string, target: string): Promise<void>;
|
||||||
checkFileExists(filepath: string): Promise<boolean>;
|
checkFileExists(filepath: string): Promise<boolean>;
|
||||||
mkdirSync(filepath: string): void;
|
mkdirSync(filepath: string): void;
|
||||||
|
checkDiskUsage(folder: string): Promise<DiskUsage>;
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,15 @@ export interface UserListFilter {
|
|||||||
excludeId?: string;
|
excludeId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserStatsQueryResponse {
|
||||||
|
userId: string;
|
||||||
|
userFirstName: string;
|
||||||
|
userLastName: string;
|
||||||
|
photos: number;
|
||||||
|
videos: number;
|
||||||
|
usage: number;
|
||||||
|
}
|
||||||
|
|
||||||
export const IUserRepository = 'IUserRepository';
|
export const IUserRepository = 'IUserRepository';
|
||||||
|
|
||||||
export interface IUserRepository {
|
export interface IUserRepository {
|
||||||
@ -13,6 +22,7 @@ export interface IUserRepository {
|
|||||||
getByOAuthId(oauthId: string): Promise<UserEntity | null>;
|
getByOAuthId(oauthId: string): Promise<UserEntity | null>;
|
||||||
getDeletedUsers(): Promise<UserEntity[]>;
|
getDeletedUsers(): Promise<UserEntity[]>;
|
||||||
getList(filter?: UserListFilter): Promise<UserEntity[]>;
|
getList(filter?: UserListFilter): Promise<UserEntity[]>;
|
||||||
|
getUserStats(): Promise<UserStatsQueryResponse[]>;
|
||||||
create(user: Partial<UserEntity>): Promise<UserEntity>;
|
create(user: Partial<UserEntity>): Promise<UserEntity>;
|
||||||
update(id: string, user: Partial<UserEntity>): Promise<UserEntity>;
|
update(id: string, user: Partial<UserEntity>): Promise<UserEntity>;
|
||||||
delete(user: UserEntity, hard?: boolean): Promise<UserEntity>;
|
delete(user: UserEntity, hard?: boolean): Promise<UserEntity>;
|
||||||
|
@ -3,12 +3,12 @@ import { BadRequestException, Inject, Injectable, Logger, NotFoundException } fr
|
|||||||
import { randomBytes } from 'crypto';
|
import { randomBytes } from 'crypto';
|
||||||
import { ReadStream } from 'fs';
|
import { ReadStream } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { APP_UPLOAD_LOCATION } from '@app/common';
|
|
||||||
import { IAlbumRepository } from '../album/album.repository';
|
import { IAlbumRepository } from '../album/album.repository';
|
||||||
import { IKeyRepository } from '../api-key/api-key.repository';
|
import { IKeyRepository } from '../api-key/api-key.repository';
|
||||||
import { IAssetRepository } from '../asset/asset.repository';
|
import { IAssetRepository } from '../asset/asset.repository';
|
||||||
import { AuthUserDto } from '../auth';
|
import { AuthUserDto } from '../auth';
|
||||||
import { ICryptoRepository } from '../crypto/crypto.repository';
|
import { ICryptoRepository } from '../crypto/crypto.repository';
|
||||||
|
import { APP_UPLOAD_LOCATION } from '../domain.constant';
|
||||||
import { IJobRepository, IUserDeletionJob, JobName } from '../job';
|
import { IJobRepository, IUserDeletionJob, JobName } from '../job';
|
||||||
import { IStorageRepository } from '../storage/storage.repository';
|
import { IStorageRepository } from '../storage/storage.repository';
|
||||||
import { IUserTokenRepository } from '../user-token/user-token.repository';
|
import { IUserTokenRepository } from '../user-token/user-token.repository';
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
import { basename, extname } from 'node:path';
|
|
||||||
|
|
||||||
export function getFileNameWithoutExtension(path: string): string {
|
|
||||||
return basename(path, extname(path));
|
|
||||||
}
|
|
@ -9,5 +9,6 @@ export const newStorageRepositoryMock = (): jest.Mocked<IStorageRepository> => {
|
|||||||
moveFile: jest.fn(),
|
moveFile: jest.fn(),
|
||||||
checkFileExists: jest.fn(),
|
checkFileExists: jest.fn(),
|
||||||
mkdirSync: jest.fn(),
|
mkdirSync: jest.fn(),
|
||||||
|
checkDiskUsage: jest.fn(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -6,6 +6,7 @@ export const newUserRepositoryMock = (): jest.Mocked<IUserRepository> => {
|
|||||||
getAdmin: jest.fn(),
|
getAdmin: jest.fn(),
|
||||||
getByEmail: jest.fn(),
|
getByEmail: jest.fn(),
|
||||||
getByOAuthId: jest.fn(),
|
getByOAuthId: jest.fn(),
|
||||||
|
getUserStats: jest.fn(),
|
||||||
getList: jest.fn(),
|
getList: jest.fn(),
|
||||||
create: jest.fn(),
|
create: jest.fn(),
|
||||||
update: jest.fn(),
|
update: jest.fn(),
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { UserEntity } from '../entities';
|
import { UserEntity } from '../entities';
|
||||||
import { IUserRepository, UserListFilter } from '@app/domain';
|
import { IUserRepository, UserListFilter, UserStatsQueryResponse } from '@app/domain';
|
||||||
import { Injectable, InternalServerErrorException } from '@nestjs/common';
|
import { Injectable, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { IsNull, Not, Repository } from 'typeorm';
|
import { IsNull, Not, Repository } from 'typeorm';
|
||||||
@ -76,4 +76,28 @@ export class UserRepository implements IUserRepository {
|
|||||||
async restore(user: UserEntity): Promise<UserEntity> {
|
async restore(user: UserEntity): Promise<UserEntity> {
|
||||||
return this.userRepository.recover(user);
|
return this.userRepository.recover(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUserStats(): Promise<UserStatsQueryResponse[]> {
|
||||||
|
const stats = 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();
|
||||||
|
|
||||||
|
for (const stat of stats) {
|
||||||
|
stat.photos = Number(stat.photos);
|
||||||
|
stat.videos = Number(stat.videos);
|
||||||
|
stat.usage = Number(stat.usage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { ImmichReadStream, IStorageRepository } from '@app/domain';
|
import { DiskUsage, ImmichReadStream, IStorageRepository } from '@app/domain';
|
||||||
import { constants, createReadStream, existsSync, mkdirSync } from 'fs';
|
import { constants, createReadStream, existsSync, mkdirSync } from 'fs';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import mv from 'mv';
|
import mv from 'mv';
|
||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
|
import diskUsage from 'diskusage';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
const moveFile = promisify<string, string, mv.Options>(mv);
|
const moveFile = promisify<string, string, mv.Options>(mv);
|
||||||
@ -66,4 +67,8 @@ export class FilesystemProvider implements IStorageRepository {
|
|||||||
mkdirSync(filepath, { recursive: true });
|
mkdirSync(filepath, { recursive: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkDiskUsage(folder: string): Promise<DiskUsage> {
|
||||||
|
return diskUsage.check(folder);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
2
server/package-lock.json
generated
2
server/package-lock.json
generated
@ -6,7 +6,7 @@
|
|||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.50.1",
|
"version": "1.51.0",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
|
@ -129,7 +129,7 @@
|
|||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"testRegex": ".*\\.spec\\.ts$",
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
"transform": {
|
"transform": {
|
||||||
"^.+\\.(t|j)s$": "ts-jest"
|
"^.+\\.ts$": "ts-jest"
|
||||||
},
|
},
|
||||||
"collectCoverageFrom": [
|
"collectCoverageFrom": [
|
||||||
"**/*.(t|j)s",
|
"**/*.(t|j)s",
|
||||||
@ -137,10 +137,6 @@
|
|||||||
],
|
],
|
||||||
"coverageDirectory": "./coverage",
|
"coverageDirectory": "./coverage",
|
||||||
"coverageThreshold": {
|
"coverageThreshold": {
|
||||||
"global": {
|
|
||||||
"lines": 17,
|
|
||||||
"statements": 17
|
|
||||||
},
|
|
||||||
"./libs/domain/": {
|
"./libs/domain/": {
|
||||||
"branches": 80,
|
"branches": 80,
|
||||||
"functions": 85,
|
"functions": 85,
|
||||||
|
@ -18,8 +18,6 @@
|
|||||||
"paths": {
|
"paths": {
|
||||||
"@app/common": ["libs/common/src"],
|
"@app/common": ["libs/common/src"],
|
||||||
"@app/common/*": ["libs/common/src/*"],
|
"@app/common/*": ["libs/common/src/*"],
|
||||||
"@app/storage": ["libs/storage/src"],
|
|
||||||
"@app/storage/*": ["libs/storage/src/*"],
|
|
||||||
"@app/infra": ["libs/infra/src"],
|
"@app/infra": ["libs/infra/src"],
|
||||||
"@app/infra/*": ["libs/infra/src/*"],
|
"@app/infra/*": ["libs/infra/src/*"],
|
||||||
"@app/domain": ["libs/domain/src"],
|
"@app/domain": ["libs/domain/src"],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user