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,7 +1,5 @@
|
|||
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_ENABLED = MACHINE_LEARNING_URL !== 'false';
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export const APP_UPLOAD_LOCATION = './upload';
|
||||
19
server/libs/domain/src/domain.constant.ts
Normal file
19
server/libs/domain/src/domain.constant.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
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}`;
|
||||
|
||||
export const APP_UPLOAD_LOCATION = './upload';
|
||||
|
|
@ -7,6 +7,7 @@ import { JobService } from './job';
|
|||
import { MediaService } from './media';
|
||||
import { OAuthService } from './oauth';
|
||||
import { SearchService } from './search';
|
||||
import { ServerInfoService } from './server-info';
|
||||
import { ShareService } from './share';
|
||||
import { SmartInfoService } from './smart-info';
|
||||
import { StorageService } from './storage';
|
||||
|
|
@ -22,6 +23,7 @@ const providers: Provider[] = [
|
|||
JobService,
|
||||
MediaService,
|
||||
OAuthService,
|
||||
ServerInfoService,
|
||||
SmartInfoService,
|
||||
StorageService,
|
||||
StorageTemplateService,
|
||||
|
|
|
|||
30
server/libs/domain/src/domain.util.ts
Normal file
30
server/libs/domain/src/domain.util.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { basename, extname } from 'node:path';
|
||||
|
||||
export function getFileNameWithoutExtension(path: string): string {
|
||||
return basename(path, extname(path));
|
||||
}
|
||||
|
||||
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]}`;
|
||||
}
|
||||
|
|
@ -5,11 +5,14 @@ export * from './auth';
|
|||
export * from './communication';
|
||||
export * from './crypto';
|
||||
export * from './device-info';
|
||||
export * from './domain.constant';
|
||||
export * from './domain.module';
|
||||
export * from './domain.util';
|
||||
export * from './job';
|
||||
export * from './media';
|
||||
export * from './oauth';
|
||||
export * from './search';
|
||||
export * from './server-info';
|
||||
export * from './share';
|
||||
export * from './smart-info';
|
||||
export * from './storage';
|
||||
|
|
@ -18,4 +21,3 @@ export * from './system-config';
|
|||
export * from './tag';
|
||||
export * from './user';
|
||||
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 { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { join } from 'path';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { IAssetRepository, mapAsset, WithoutProperty } from '../asset';
|
||||
import { CommunicationEvent, ICommunicationRepository } from '../communication';
|
||||
import { APP_UPLOAD_LOCATION } from '../domain.constant';
|
||||
import { IAssetJob, IBaseJob, IJobRepository, JobName } from '../job';
|
||||
import { IStorageRepository } from '../storage';
|
||||
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';
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { ApiResponseProperty } from '@nestjs/swagger';
|
||||
|
||||
export class ServerPingResponse {
|
||||
constructor(res: string) {
|
||||
this.res = res;
|
||||
}
|
||||
|
||||
@ApiResponseProperty({ type: String, example: 'pong' })
|
||||
res!: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
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[] = [];
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IServerVersion } from '@app/domain';
|
||||
|
||||
export class ServerVersionReponseDto implements IServerVersion {
|
||||
@ApiProperty({ type: 'integer' })
|
||||
major!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
minor!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
patch!: number;
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
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;
|
||||
}
|
||||
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 {
|
||||
IStorageRepository,
|
||||
ISystemConfigRepository,
|
||||
|
|
@ -15,6 +14,7 @@ import handlebar from 'handlebars';
|
|||
import * as luxon from 'luxon';
|
||||
import path from 'node:path';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { APP_UPLOAD_LOCATION } from '../domain.constant';
|
||||
import { SystemConfigCore } from '../system-config/system-config.core';
|
||||
|
||||
export class StorageTemplateCore {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { APP_UPLOAD_LOCATION } from '@app/common';
|
||||
import { AssetEntity, SystemConfig } from '@app/infra/db/entities';
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { IAssetRepository } from '../asset/asset.repository';
|
||||
import { APP_UPLOAD_LOCATION } from '../domain.constant';
|
||||
import { IStorageRepository } from '../storage/storage.repository';
|
||||
import { INITIAL_SYSTEM_CONFIG, ISystemConfigRepository } from '../system-config';
|
||||
import { StorageTemplateCore } from './storage-template.core';
|
||||
|
|
|
|||
|
|
@ -6,6 +6,12 @@ export interface ImmichReadStream {
|
|||
length: number;
|
||||
}
|
||||
|
||||
export interface DiskUsage {
|
||||
available: number;
|
||||
free: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export const IStorageRepository = 'IStorageRepository';
|
||||
|
||||
export interface IStorageRepository {
|
||||
|
|
@ -16,4 +22,5 @@ export interface IStorageRepository {
|
|||
moveFile(source: string, target: string): Promise<void>;
|
||||
checkFileExists(filepath: string): Promise<boolean>;
|
||||
mkdirSync(filepath: string): void;
|
||||
checkDiskUsage(folder: string): Promise<DiskUsage>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,15 @@ export interface UserListFilter {
|
|||
excludeId?: string;
|
||||
}
|
||||
|
||||
export interface UserStatsQueryResponse {
|
||||
userId: string;
|
||||
userFirstName: string;
|
||||
userLastName: string;
|
||||
photos: number;
|
||||
videos: number;
|
||||
usage: number;
|
||||
}
|
||||
|
||||
export const IUserRepository = 'IUserRepository';
|
||||
|
||||
export interface IUserRepository {
|
||||
|
|
@ -13,6 +22,7 @@ export interface IUserRepository {
|
|||
getByOAuthId(oauthId: string): Promise<UserEntity | null>;
|
||||
getDeletedUsers(): Promise<UserEntity[]>;
|
||||
getList(filter?: UserListFilter): Promise<UserEntity[]>;
|
||||
getUserStats(): Promise<UserStatsQueryResponse[]>;
|
||||
create(user: Partial<UserEntity>): Promise<UserEntity>;
|
||||
update(id: string, user: Partial<UserEntity>): 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 { ReadStream } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { APP_UPLOAD_LOCATION } from '@app/common';
|
||||
import { IAlbumRepository } from '../album/album.repository';
|
||||
import { IKeyRepository } from '../api-key/api-key.repository';
|
||||
import { IAssetRepository } from '../asset/asset.repository';
|
||||
import { AuthUserDto } from '../auth';
|
||||
import { ICryptoRepository } from '../crypto/crypto.repository';
|
||||
import { APP_UPLOAD_LOCATION } from '../domain.constant';
|
||||
import { IJobRepository, IUserDeletionJob, JobName } from '../job';
|
||||
import { IStorageRepository } from '../storage/storage.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(),
|
||||
checkFileExists: jest.fn(),
|
||||
mkdirSync: jest.fn(),
|
||||
checkDiskUsage: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export const newUserRepositoryMock = (): jest.Mocked<IUserRepository> => {
|
|||
getAdmin: jest.fn(),
|
||||
getByEmail: jest.fn(),
|
||||
getByOAuthId: jest.fn(),
|
||||
getUserStats: jest.fn(),
|
||||
getList: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { UserEntity } from '../entities';
|
||||
import { IUserRepository, UserListFilter } from '@app/domain';
|
||||
import { IUserRepository, UserListFilter, UserStatsQueryResponse } from '@app/domain';
|
||||
import { Injectable, InternalServerErrorException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { IsNull, Not, Repository } from 'typeorm';
|
||||
|
|
@ -76,4 +76,28 @@ export class UserRepository implements IUserRepository {
|
|||
async restore(user: UserEntity): Promise<UserEntity> {
|
||||
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 fs from 'fs/promises';
|
||||
import mv from 'mv';
|
||||
import { promisify } from 'node:util';
|
||||
import diskUsage from 'diskusage';
|
||||
import path from 'path';
|
||||
|
||||
const moveFile = promisify<string, string, mv.Options>(mv);
|
||||
|
|
@ -66,4 +67,8 @@ export class FilesystemProvider implements IStorageRepository {
|
|||
mkdirSync(filepath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
checkDiskUsage(folder: string): Promise<DiskUsage> {
|
||||
return diskUsage.check(folder);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue