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:
Michel Heusschen
2023-02-26 20:57:34 +01:00
committed by GitHub
parent 7d45ae68a6
commit 368142e79b
25 changed files with 199 additions and 260 deletions

View File

@@ -37,6 +37,7 @@ describe('Album service', () => {
shouldChangePassword: false,
oauthId: '',
tags: [],
assets: [],
});
const albumId = 'f19ab956-4761-41ea-a5d6-bae948308d58';
const sharedAlbumOwnerId = '2222';

View File

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

View File

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

View File

@@ -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],
})

View File

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

View File

@@ -25,6 +25,7 @@ describe('TagService', () => {
deletedAt: undefined,
updatedAt: '2022-12-02T19:29:23.603Z',
tags: [],
assets: [],
oauthId: 'oauth-id-1',
});

View File

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

View File

@@ -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({

View File

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

View File

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