mirror of
https://github.com/samsonjs/immich.git
synced 2026-03-25 09:15:56 +00: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
12 changed files with 46 additions and 74 deletions
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 a new issue