mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
feat(web): improved server stats (#1870)
* feat(web): improved server stats * fix(web): don't log unauthorized errors * Revert "fix(web): don't log unauthorized errors" This reverts commit 7fc2987a77ae8bf3a7381ed3156a7a0c16f27564.
This commit is contained in:
@@ -37,6 +37,7 @@ describe('Album service', () => {
|
||||
shouldChangePassword: false,
|
||||
oauthId: '',
|
||||
tags: [],
|
||||
assets: [],
|
||||
});
|
||||
const albumId = 'f19ab956-4761-41ea-a5d6-bae948308d58';
|
||||
const sharedAlbumOwnerId = '2222';
|
||||
|
||||
@@ -2,28 +2,14 @@ import { ApiProperty } from '@nestjs/swagger';
|
||||
import { UsageByUserDto } from './usage-by-user-response.dto';
|
||||
|
||||
export class ServerStatsResponseDto {
|
||||
constructor() {
|
||||
this.photos = 0;
|
||||
this.videos = 0;
|
||||
this.usageByUser = [];
|
||||
this.usageRaw = 0;
|
||||
this.usage = '';
|
||||
}
|
||||
@ApiProperty({ type: 'integer' })
|
||||
photos = 0;
|
||||
|
||||
@ApiProperty({ type: 'integer' })
|
||||
photos!: number;
|
||||
|
||||
@ApiProperty({ type: 'integer' })
|
||||
videos!: number;
|
||||
|
||||
@ApiProperty({ type: 'integer' })
|
||||
objects!: number;
|
||||
videos = 0;
|
||||
|
||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||
usageRaw!: number;
|
||||
|
||||
@ApiProperty({ type: 'string' })
|
||||
usage!: string;
|
||||
usage = 0;
|
||||
|
||||
@ApiProperty({
|
||||
isArray: true,
|
||||
@@ -37,5 +23,5 @@ export class ServerStatsResponseDto {
|
||||
},
|
||||
],
|
||||
})
|
||||
usageByUser!: UsageByUserDto[];
|
||||
usageByUser: UsageByUserDto[] = [];
|
||||
}
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class UsageByUserDto {
|
||||
constructor(userId: string) {
|
||||
this.userId = userId;
|
||||
this.videos = 0;
|
||||
this.photos = 0;
|
||||
this.usageRaw = 0;
|
||||
this.usage = '0B';
|
||||
}
|
||||
|
||||
@ApiProperty({ type: 'string' })
|
||||
userId: string;
|
||||
userId!: string;
|
||||
@ApiProperty({ type: 'string' })
|
||||
userFirstName!: string;
|
||||
@ApiProperty({ type: 'string' })
|
||||
userLastName!: string;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
videos: number;
|
||||
photos!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
photos: number;
|
||||
videos!: number;
|
||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||
usageRaw!: number;
|
||||
@ApiProperty({ type: 'string' })
|
||||
usage!: string;
|
||||
usage!: number;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ServerInfoService } from './server-info.service';
|
||||
import { ServerInfoController } from './server-info.controller';
|
||||
import { AssetEntity } from '@app/infra';
|
||||
import { UserEntity } from '@app/infra';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([AssetEntity])],
|
||||
imports: [TypeOrmModule.forFeature([UserEntity])],
|
||||
controllers: [ServerInfoController],
|
||||
providers: [ServerInfoService],
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 { AssetEntity } from '@app/infra';
|
||||
import { UserEntity } from '@app/infra';
|
||||
import { Repository } from 'typeorm';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { asHumanReadable } from '../../utils/human-readable.util';
|
||||
@@ -12,8 +12,8 @@ import { asHumanReadable } from '../../utils/human-readable.util';
|
||||
@Injectable()
|
||||
export class ServerInfoService {
|
||||
constructor(
|
||||
@InjectRepository(AssetEntity)
|
||||
private assetRepository: Repository<AssetEntity>,
|
||||
@InjectRepository(UserEntity)
|
||||
private userRepository: Repository<UserEntity>,
|
||||
) {}
|
||||
|
||||
async getServerInfo(): Promise<ServerInfoResponseDto> {
|
||||
@@ -33,44 +33,48 @@ export class ServerInfoService {
|
||||
}
|
||||
|
||||
async getStats(): Promise<ServerStatsResponseDto> {
|
||||
const serverStats = new ServerStatsResponseDto();
|
||||
|
||||
type UserStatsQueryResponse = {
|
||||
assetType: string;
|
||||
assetCount: string;
|
||||
totalSizeInBytes: string;
|
||||
ownerId: string;
|
||||
userId: string;
|
||||
userFirstName: string;
|
||||
userLastName: string;
|
||||
photos: string;
|
||||
videos: string;
|
||||
usage: string;
|
||||
};
|
||||
|
||||
const userStatsQueryResponse: UserStatsQueryResponse[] = await this.assetRepository
|
||||
.createQueryBuilder('a')
|
||||
.select('COUNT(a.id)', 'assetCount')
|
||||
.addSelect('SUM(ei.fileSizeInByte)', 'totalSizeInBytes')
|
||||
.addSelect('a."ownerId"')
|
||||
.addSelect('a.type', 'assetType')
|
||||
.where('a.isVisible = true')
|
||||
.leftJoin('a.exifInfo', 'ei')
|
||||
.groupBy('a."ownerId"')
|
||||
.addGroupBy('a.type')
|
||||
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 tmpMap = new Map<string, UsageByUserDto>();
|
||||
const getUsageByUser = (id: string) => tmpMap.get(id) || new UsageByUserDto(id);
|
||||
userStatsQueryResponse.forEach((r) => {
|
||||
const usageByUser = getUsageByUser(r.ownerId);
|
||||
usageByUser.photos += r.assetType === 'IMAGE' ? parseInt(r.assetCount) : 0;
|
||||
usageByUser.videos += r.assetType === 'VIDEO' ? parseInt(r.assetCount) : 0;
|
||||
usageByUser.usageRaw += parseInt(r.totalSizeInBytes);
|
||||
usageByUser.usage = asHumanReadable(usageByUser.usageRaw);
|
||||
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);
|
||||
|
||||
serverStats.photos += r.assetType === 'IMAGE' ? parseInt(r.assetCount) : 0;
|
||||
serverStats.videos += r.assetType === 'VIDEO' ? parseInt(r.assetCount) : 0;
|
||||
serverStats.usageRaw += parseInt(r.totalSizeInBytes);
|
||||
serverStats.usage = asHumanReadable(serverStats.usageRaw);
|
||||
tmpMap.set(r.ownerId, usageByUser);
|
||||
return usage;
|
||||
});
|
||||
|
||||
serverStats.usageByUser = Array.from(tmpMap.values());
|
||||
const serverStats = new ServerStatsResponseDto();
|
||||
usageByUser.forEach((user) => {
|
||||
serverStats.photos += user.photos;
|
||||
serverStats.videos += user.videos;
|
||||
serverStats.usage += user.usage;
|
||||
});
|
||||
serverStats.usageByUser = usageByUser;
|
||||
|
||||
return serverStats;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ describe('TagService', () => {
|
||||
deletedAt: undefined,
|
||||
updatedAt: '2022-12-02T19:29:23.603Z',
|
||||
tags: [],
|
||||
assets: [],
|
||||
oauthId: 'oauth-id-1',
|
||||
});
|
||||
|
||||
|
||||
@@ -4883,25 +4883,29 @@
|
||||
"userId": {
|
||||
"type": "string"
|
||||
},
|
||||
"videos": {
|
||||
"type": "integer"
|
||||
"userFirstName": {
|
||||
"type": "string"
|
||||
},
|
||||
"userLastName": {
|
||||
"type": "string"
|
||||
},
|
||||
"photos": {
|
||||
"type": "integer"
|
||||
},
|
||||
"usageRaw": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
"videos": {
|
||||
"type": "integer"
|
||||
},
|
||||
"usage": {
|
||||
"type": "string"
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"userId",
|
||||
"videos",
|
||||
"userFirstName",
|
||||
"userLastName",
|
||||
"photos",
|
||||
"usageRaw",
|
||||
"videos",
|
||||
"usage"
|
||||
]
|
||||
},
|
||||
@@ -4909,22 +4913,20 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"photos": {
|
||||
"type": "integer"
|
||||
"type": "integer",
|
||||
"default": 0
|
||||
},
|
||||
"videos": {
|
||||
"type": "integer"
|
||||
},
|
||||
"objects": {
|
||||
"type": "integer"
|
||||
},
|
||||
"usageRaw": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
"default": 0
|
||||
},
|
||||
"usage": {
|
||||
"type": "string"
|
||||
"type": "integer",
|
||||
"default": 0,
|
||||
"format": "int64"
|
||||
},
|
||||
"usageByUser": {
|
||||
"default": [],
|
||||
"title": "Array of usage for each user",
|
||||
"example": [
|
||||
{
|
||||
@@ -4942,8 +4944,6 @@
|
||||
"required": [
|
||||
"photos",
|
||||
"videos",
|
||||
"objects",
|
||||
"usageRaw",
|
||||
"usage",
|
||||
"usageByUser"
|
||||
]
|
||||
|
||||
@@ -54,6 +54,7 @@ const adminUser: UserEntity = Object.freeze({
|
||||
createdAt: '2021-01-01',
|
||||
updatedAt: '2021-01-01',
|
||||
tags: [],
|
||||
assets: [],
|
||||
});
|
||||
|
||||
const immichUser: UserEntity = Object.freeze({
|
||||
@@ -69,6 +70,7 @@ const immichUser: UserEntity = Object.freeze({
|
||||
createdAt: '2021-01-01',
|
||||
updatedAt: '2021-01-01',
|
||||
tags: [],
|
||||
assets: [],
|
||||
});
|
||||
|
||||
const updatedImmichUser: UserEntity = Object.freeze({
|
||||
@@ -84,6 +86,7 @@ const updatedImmichUser: UserEntity = Object.freeze({
|
||||
createdAt: '2021-01-01',
|
||||
updatedAt: '2021-01-01',
|
||||
tags: [],
|
||||
assets: [],
|
||||
});
|
||||
|
||||
const adminUserResponse = Object.freeze({
|
||||
|
||||
@@ -76,6 +76,7 @@ export const userEntityStub = {
|
||||
createdAt: '2021-01-01',
|
||||
updatedAt: '2021-01-01',
|
||||
tags: [],
|
||||
assets: [],
|
||||
}),
|
||||
user1: Object.freeze<UserEntity>({
|
||||
...authStub.user1,
|
||||
@@ -88,6 +89,7 @@ export const userEntityStub = {
|
||||
createdAt: '2021-01-01',
|
||||
updatedAt: '2021-01-01',
|
||||
tags: [],
|
||||
assets: [],
|
||||
}),
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { AssetEntity } from './asset.entity';
|
||||
import { TagEntity } from './tag.entity';
|
||||
|
||||
@Entity('users')
|
||||
@@ -49,4 +50,7 @@ export class UserEntity {
|
||||
|
||||
@OneToMany(() => TagEntity, (tag) => tag.user)
|
||||
tags!: TagEntity[];
|
||||
|
||||
@OneToMany(() => AssetEntity, (asset) => asset.owner)
|
||||
assets!: AssetEntity[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user