mirror of
https://github.com/immich-app/immich.git
synced 2024-12-25 10:43:13 +02:00
fix(server): Server freezes when getting statistic (#994)
* fix(server): Server freezes when getting statistic * remove dead code
This commit is contained in:
parent
b3e51cc849
commit
41ffa0c015
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -5,7 +5,6 @@ export class ServerStatsResponseDto {
|
||||
constructor() {
|
||||
this.photos = 0;
|
||||
this.videos = 0;
|
||||
this.objects = 0;
|
||||
this.usageByUser = [];
|
||||
this.usageRaw = 0;
|
||||
this.usage = '';
|
||||
@ -34,7 +33,6 @@ export class ServerStatsResponseDto {
|
||||
{
|
||||
photos: 1,
|
||||
videos: 1,
|
||||
objects: 1,
|
||||
diskUsageRaw: 1,
|
||||
},
|
||||
],
|
||||
|
@ -3,16 +3,15 @@ import { ApiProperty } from '@nestjs/swagger';
|
||||
export class UsageByUserDto {
|
||||
constructor(userId: string) {
|
||||
this.userId = userId;
|
||||
this.objects = 0;
|
||||
this.videos = 0;
|
||||
this.photos = 0;
|
||||
this.usageRaw = 0;
|
||||
this.usage = '0B';
|
||||
}
|
||||
|
||||
@ApiProperty({ type: 'string' })
|
||||
userId: string;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
objects: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
videos: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
photos: number;
|
||||
|
@ -7,8 +7,6 @@ import { UsageByUserDto } from './response-dto/usage-by-user-response.dto';
|
||||
import { AssetEntity } from '@app/database/entities/asset.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import path from 'path';
|
||||
import { readdirSync, statSync } from 'fs';
|
||||
import { asHumanReadable } from '../../utils/human-readable.util';
|
||||
|
||||
@Injectable()
|
||||
@ -35,59 +33,46 @@ export class ServerInfoService {
|
||||
}
|
||||
|
||||
async getStats(): Promise<ServerStatsResponseDto> {
|
||||
const res = await this.assetRepository
|
||||
.createQueryBuilder('asset')
|
||||
.select(`COUNT(asset.id)`, 'count')
|
||||
.addSelect(`asset.type`, 'type')
|
||||
.addSelect(`asset.userId`, 'userId')
|
||||
.groupBy('asset.type, asset.userId')
|
||||
.addGroupBy('asset.type')
|
||||
const serverStats = new ServerStatsResponseDto();
|
||||
|
||||
type UserStatsQueryResponse = {
|
||||
assetType: string;
|
||||
assetCount: string;
|
||||
totalSizeInBytes: string;
|
||||
userId: string;
|
||||
};
|
||||
|
||||
const userStatsQueryResponse: UserStatsQueryResponse[] = await this.assetRepository
|
||||
.createQueryBuilder('a')
|
||||
.select('COUNT(a.id)', 'assetCount')
|
||||
.addSelect('SUM(ei.fileSizeInByte)', 'totalSizeInBytes')
|
||||
.addSelect('a."userId"')
|
||||
.addSelect('a.type', 'assetType')
|
||||
.where('a.isVisible = true')
|
||||
.leftJoin('a.exifInfo', 'ei')
|
||||
.groupBy('a."userId"')
|
||||
.addGroupBy('a.type')
|
||||
.getRawMany();
|
||||
|
||||
const serverStats = new ServerStatsResponseDto();
|
||||
const tmpMap = new Map<string, UsageByUserDto>();
|
||||
const getUsageByUser = (id: string) => tmpMap.get(id) || new UsageByUserDto(id);
|
||||
res.map((item) => {
|
||||
const usage: UsageByUserDto = getUsageByUser(item.userId);
|
||||
if (item.type === 'IMAGE') {
|
||||
usage.photos = parseInt(item.count);
|
||||
serverStats.photos += usage.photos;
|
||||
} else if (item.type === 'VIDEO') {
|
||||
usage.videos = parseInt(item.count);
|
||||
serverStats.videos += usage.videos;
|
||||
}
|
||||
tmpMap.set(item.userId, usage);
|
||||
|
||||
userStatsQueryResponse.forEach((r) => {
|
||||
const usageByUser = getUsageByUser(r.userId);
|
||||
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);
|
||||
|
||||
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.userId, usageByUser);
|
||||
});
|
||||
|
||||
for (const userId of tmpMap.keys()) {
|
||||
const usage = getUsageByUser(userId);
|
||||
const userDiskUsage = await ServerInfoService.getDirectoryStats(path.join(APP_UPLOAD_LOCATION, userId));
|
||||
usage.usageRaw = userDiskUsage.size;
|
||||
usage.objects = userDiskUsage.fileCount;
|
||||
usage.usage = asHumanReadable(usage.usageRaw);
|
||||
serverStats.usageRaw += usage.usageRaw;
|
||||
serverStats.objects += usage.objects;
|
||||
}
|
||||
serverStats.usage = asHumanReadable(serverStats.usageRaw);
|
||||
serverStats.usageByUser = Array.from(tmpMap.values());
|
||||
|
||||
return serverStats;
|
||||
}
|
||||
|
||||
private static async getDirectoryStats(dirPath: string) {
|
||||
let size = 0;
|
||||
let fileCount = 0;
|
||||
for (const filename of readdirSync(dirPath)) {
|
||||
const absFilename = path.join(dirPath, filename);
|
||||
const fileStat = statSync(absFilename);
|
||||
if (fileStat.isFile()) {
|
||||
size += fileStat.size;
|
||||
fileCount += 1;
|
||||
} else if (fileStat.isDirectory()) {
|
||||
const subDirStat = await ServerInfoService.getDirectoryStats(absFilename);
|
||||
size += subDirStat.size;
|
||||
fileCount += subDirStat.fileCount;
|
||||
}
|
||||
}
|
||||
return { size, fileCount };
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@ -1632,12 +1632,6 @@ export interface UsageByUserDto {
|
||||
* @memberof UsageByUserDto
|
||||
*/
|
||||
'userId': string;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof UsageByUserDto
|
||||
*/
|
||||
'objects': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
|
@ -2,7 +2,6 @@
|
||||
import { ServerStatsResponseDto, UserResponseDto } from '@api';
|
||||
import CameraIris from 'svelte-material-icons/CameraIris.svelte';
|
||||
import PlayCircle from 'svelte-material-icons/PlayCircle.svelte';
|
||||
import FileImageOutline from 'svelte-material-icons/FileImageOutline.svelte';
|
||||
import Memory from 'svelte-material-icons/Memory.svelte';
|
||||
import StatsCard from './stats-card.svelte';
|
||||
export let stats: ServerStatsResponseDto;
|
||||
@ -27,7 +26,6 @@
|
||||
<div class="flex mt-5 justify-between">
|
||||
<StatsCard logo={CameraIris} title={'PHOTOS'} value={stats.photos.toString()} />
|
||||
<StatsCard logo={PlayCircle} title={'VIDEOS'} value={stats.videos.toString()} />
|
||||
<StatsCard logo={FileImageOutline} title={'OBJECTS'} value={stats.objects.toString()} />
|
||||
<StatsCard logo={Memory} title={'STORAGE'} value={spaceUsage} unit={spaceUnit} />
|
||||
</div>
|
||||
</div>
|
||||
@ -39,11 +37,10 @@
|
||||
class="border rounded-md mb-4 bg-gray-50 dark:bg-immich-dark-gray dark:border-immich-dark-gray flex text-immich-primary dark:text-immich-dark-primary w-full h-12"
|
||||
>
|
||||
<tr class="flex w-full place-items-center">
|
||||
<th class="text-center w-1/5 font-medium text-sm">User</th>
|
||||
<th class="text-center w-1/5 font-medium text-sm">Photos</th>
|
||||
<th class="text-center w-1/5 font-medium text-sm">Videos</th>
|
||||
<th class="text-center w-1/5 font-medium text-sm">Objects</th>
|
||||
<th class="text-center w-1/5 font-medium text-sm">Size</th>
|
||||
<th class="text-center w-1/4 font-medium text-sm">User</th>
|
||||
<th class="text-center w-1/4 font-medium text-sm">Photos</th>
|
||||
<th class="text-center w-1/4 font-medium text-sm">Videos</th>
|
||||
<th class="text-center w-1/4 font-medium text-sm">Size</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
@ -57,11 +54,10 @@
|
||||
: 'bg-immich-bg dark:bg-immich-dark-gray/50'
|
||||
}`}
|
||||
>
|
||||
<td class="text-sm px-2 w-1/5 text-ellipsis">{getFullName(user.userId)}</td>
|
||||
<td class="text-sm px-2 w-1/5 text-ellipsis">{user.photos}</td>
|
||||
<td class="text-sm px-2 w-1/5 text-ellipsis">{user.videos}</td>
|
||||
<td class="text-sm px-2 w-1/5 text-ellipsis">{user.objects}</td>
|
||||
<td class="text-sm px-2 w-1/5 text-ellipsis">{user.usage}</td>
|
||||
<td class="text-sm px-2 w-1/4 text-ellipsis">{getFullName(user.userId)}</td>
|
||||
<td class="text-sm px-2 w-1/4 text-ellipsis">{user.photos}</td>
|
||||
<td class="text-sm px-2 w-1/4 text-ellipsis">{user.videos}</td>
|
||||
<td class="text-sm px-2 w-1/4 text-ellipsis">{user.usage}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
|
@ -7,7 +7,7 @@
|
||||
|
||||
$: zeros = () => {
|
||||
let result = '';
|
||||
const maxLength = 9;
|
||||
const maxLength = 13;
|
||||
const valueLength = parseInt(value).toString().length;
|
||||
const zeroLength = maxLength - valueLength;
|
||||
for (let i = 0; i < zeroLength; i++) {
|
||||
@ -18,7 +18,7 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="w-[180px] h-[140px] bg-immich-gray dark:bg-immich-dark-gray rounded-3xl p-5 flex flex-col justify-between"
|
||||
class="w-[250px] h-[140px] bg-immich-gray dark:bg-immich-dark-gray rounded-3xl p-5 flex flex-col justify-between"
|
||||
>
|
||||
<div class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary">
|
||||
<svelte:component this={logo} size="40" />
|
||||
|
Loading…
Reference in New Issue
Block a user