1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +00:00

feat: adding photo & video storage space to server stats (#14125)

* expose detailed user storage stats + display them in the storage per user table

* chore: openapi & sql

* fix: fix test stubs

* fix: formatting errors, e2e test and server test

* fix: upper lower case typo in spec file

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
weathondev 2024-11-15 23:38:57 +01:00 committed by GitHub
parent 24ae4ecff1
commit f5c4af73aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 104 additions and 5 deletions

View file

@ -163,11 +163,15 @@ describe('/server', () => {
expect(body).toEqual({ expect(body).toEqual({
photos: 0, photos: 0,
usage: 0, usage: 0,
usagePhotos: 0,
usageVideos: 0,
usageByUser: [ usageByUser: [
{ {
quotaSizeInBytes: null, quotaSizeInBytes: null,
photos: 0, photos: 0,
usage: 0, usage: 0,
usagePhotos: 0,
usageVideos: 0,
userName: 'Immich Admin', userName: 'Immich Admin',
userId: admin.userId, userId: admin.userId,
videos: 0, videos: 0,
@ -176,6 +180,8 @@ describe('/server', () => {
quotaSizeInBytes: null, quotaSizeInBytes: null,
photos: 0, photos: 0,
usage: 0, usage: 0,
usagePhotos: 0,
usageVideos: 0,
userName: 'User 1', userName: 'User 1',
userId: nonAdmin.userId, userId: nonAdmin.userId,
videos: 0, videos: 0,

Binary file not shown.

View file

@ -10966,7 +10966,9 @@
{ {
"photos": 1, "photos": 1,
"videos": 1, "videos": 1,
"diskUsageRaw": 1 "diskUsageRaw": 2,
"usagePhotos": 1,
"usageVideos": 1
} }
], ],
"items": { "items": {
@ -10975,6 +10977,16 @@
"title": "Array of usage for each user", "title": "Array of usage for each user",
"type": "array" "type": "array"
}, },
"usagePhotos": {
"default": 0,
"format": "int64",
"type": "integer"
},
"usageVideos": {
"default": 0,
"format": "int64",
"type": "integer"
},
"videos": { "videos": {
"default": 0, "default": 0,
"type": "integer" "type": "integer"
@ -10984,6 +10996,8 @@
"photos", "photos",
"usage", "usage",
"usageByUser", "usageByUser",
"usagePhotos",
"usageVideos",
"videos" "videos"
], ],
"type": "object" "type": "object"
@ -12503,6 +12517,14 @@
"format": "int64", "format": "int64",
"type": "integer" "type": "integer"
}, },
"usagePhotos": {
"format": "int64",
"type": "integer"
},
"usageVideos": {
"format": "int64",
"type": "integer"
},
"userId": { "userId": {
"type": "string" "type": "string"
}, },
@ -12517,6 +12539,8 @@
"photos", "photos",
"quotaSizeInBytes", "quotaSizeInBytes",
"usage", "usage",
"usagePhotos",
"usageVideos",
"userId", "userId",
"userName", "userName",
"videos" "videos"

View file

@ -969,6 +969,8 @@ export type UsageByUserDto = {
photos: number; photos: number;
quotaSizeInBytes: number | null; quotaSizeInBytes: number | null;
usage: number; usage: number;
usagePhotos: number;
usageVideos: number;
userId: string; userId: string;
userName: string; userName: string;
videos: number; videos: number;
@ -977,6 +979,8 @@ export type ServerStatsResponseDto = {
photos: number; photos: number;
usage: number; usage: number;
usageByUser: UsageByUserDto[]; usageByUser: UsageByUserDto[];
usagePhotos: number;
usageVideos: number;
videos: number; videos: number;
}; };
export type ServerStorageResponseDto = { export type ServerStorageResponseDto = {

View file

@ -86,6 +86,10 @@ export class UsageByUserDto {
@ApiProperty({ type: 'integer', format: 'int64' }) @ApiProperty({ type: 'integer', format: 'int64' })
usage!: number; usage!: number;
@ApiProperty({ type: 'integer', format: 'int64' }) @ApiProperty({ type: 'integer', format: 'int64' })
usagePhotos!: number;
@ApiProperty({ type: 'integer', format: 'int64' })
usageVideos!: number;
@ApiProperty({ type: 'integer', format: 'int64' })
quotaSizeInBytes!: number | null; quotaSizeInBytes!: number | null;
} }
@ -99,6 +103,12 @@ export class ServerStatsResponseDto {
@ApiProperty({ type: 'integer', format: 'int64' }) @ApiProperty({ type: 'integer', format: 'int64' })
usage = 0; usage = 0;
@ApiProperty({ type: 'integer', format: 'int64' })
usagePhotos = 0;
@ApiProperty({ type: 'integer', format: 'int64' })
usageVideos = 0;
@ApiProperty({ @ApiProperty({
isArray: true, isArray: true,
type: UsageByUserDto, type: UsageByUserDto,
@ -107,7 +117,9 @@ export class ServerStatsResponseDto {
{ {
photos: 1, photos: 1,
videos: 1, videos: 1,
diskUsageRaw: 1, diskUsageRaw: 2,
usagePhotos: 1,
usageVideos: 1,
}, },
], ],
}) })

View file

@ -11,6 +11,8 @@ export interface UserStatsQueryResponse {
photos: number; photos: number;
videos: number; videos: number;
usage: number; usage: number;
usagePhotos: number;
usageVideos: number;
quotaSizeInBytes: number | null; quotaSizeInBytes: number | null;
} }

View file

@ -140,7 +140,23 @@ SELECT
"assets"."libraryId" IS NULL "assets"."libraryId" IS NULL
), ),
0 0
) AS "usage" ) AS "usage",
COALESCE(
SUM("exif"."fileSizeInByte") FILTER (
WHERE
"assets"."libraryId" IS NULL
AND "assets"."type" = 'IMAGE'
),
0
) AS "usagePhotos",
COALESCE(
SUM("exif"."fileSizeInByte") FILTER (
WHERE
"assets"."libraryId" IS NULL
AND "assets"."type" = 'VIDEO'
),
0
) AS "usageVideos"
FROM FROM
"users" "users" "users" "users"
LEFT JOIN "assets" "assets" ON "assets"."ownerId" = "users"."id" LEFT JOIN "assets" "assets" ON "assets"."ownerId" = "users"."id"

View file

@ -108,6 +108,14 @@ export class UserRepository implements IUserRepository {
.addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'IMAGE' AND assets.isVisible)`, 'photos') .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(`COUNT(assets.id) FILTER (WHERE assets.type = 'VIDEO' AND assets.isVisible)`, 'videos')
.addSelect('COALESCE(SUM(exif.fileSizeInByte) FILTER (WHERE assets.libraryId IS NULL), 0)', 'usage') .addSelect('COALESCE(SUM(exif.fileSizeInByte) FILTER (WHERE assets.libraryId IS NULL), 0)', 'usage')
.addSelect(
`COALESCE(SUM(exif.fileSizeInByte) FILTER (WHERE assets.libraryId IS NULL AND assets.type = 'IMAGE'), 0)`,
'usagePhotos',
)
.addSelect(
`COALESCE(SUM(exif.fileSizeInByte) FILTER (WHERE assets.libraryId IS NULL AND assets.type = 'VIDEO'), 0)`,
'usageVideos',
)
.addSelect('users.quotaSizeInBytes', 'quotaSizeInBytes') .addSelect('users.quotaSizeInBytes', 'quotaSizeInBytes')
.leftJoin('users.assets', 'assets') .leftJoin('users.assets', 'assets')
.leftJoin('assets.exifInfo', 'exif') .leftJoin('assets.exifInfo', 'exif')
@ -119,6 +127,8 @@ export class UserRepository implements IUserRepository {
stat.photos = Number(stat.photos); stat.photos = Number(stat.photos);
stat.videos = Number(stat.videos); stat.videos = Number(stat.videos);
stat.usage = Number(stat.usage); stat.usage = Number(stat.usage);
stat.usagePhotos = Number(stat.usagePhotos);
stat.usageVideos = Number(stat.usageVideos);
stat.quotaSizeInBytes = stat.quotaSizeInBytes; stat.quotaSizeInBytes = stat.quotaSizeInBytes;
} }

View file

@ -185,6 +185,8 @@ describe(ServerService.name, () => {
photos: 10, photos: 10,
videos: 11, videos: 11,
usage: 12_345, usage: 12_345,
usagePhotos: 1,
usageVideos: 11_345,
quotaSizeInBytes: 0, quotaSizeInBytes: 0,
}, },
{ {
@ -193,6 +195,8 @@ describe(ServerService.name, () => {
photos: 10, photos: 10,
videos: 20, videos: 20,
usage: 123_456, usage: 123_456,
usagePhotos: 100,
usageVideos: 23_456,
quotaSizeInBytes: 0, quotaSizeInBytes: 0,
}, },
{ {
@ -201,6 +205,8 @@ describe(ServerService.name, () => {
photos: 100, photos: 100,
videos: 0, videos: 0,
usage: 987_654, usage: 987_654,
usagePhotos: 900,
usageVideos: 87_654,
quotaSizeInBytes: 0, quotaSizeInBytes: 0,
}, },
]); ]);
@ -209,11 +215,15 @@ describe(ServerService.name, () => {
photos: 120, photos: 120,
videos: 31, videos: 31,
usage: 1_123_455, usage: 1_123_455,
usagePhotos: 1001,
usageVideos: 122_455,
usageByUser: [ usageByUser: [
{ {
photos: 10, photos: 10,
quotaSizeInBytes: 0, quotaSizeInBytes: 0,
usage: 12_345, usage: 12_345,
usagePhotos: 1,
usageVideos: 11_345,
userName: '1 User', userName: '1 User',
userId: 'user1', userId: 'user1',
videos: 11, videos: 11,
@ -222,6 +232,8 @@ describe(ServerService.name, () => {
photos: 10, photos: 10,
quotaSizeInBytes: 0, quotaSizeInBytes: 0,
usage: 123_456, usage: 123_456,
usagePhotos: 100,
usageVideos: 23_456,
userName: '2 User', userName: '2 User',
userId: 'user2', userId: 'user2',
videos: 20, videos: 20,
@ -230,6 +242,8 @@ describe(ServerService.name, () => {
photos: 100, photos: 100,
quotaSizeInBytes: 0, quotaSizeInBytes: 0,
usage: 987_654, usage: 987_654,
usagePhotos: 900,
usageVideos: 87_654,
userName: '3 User', userName: '3 User',
userId: 'user3', userId: 'user3',
videos: 0, videos: 0,

View file

@ -126,11 +126,16 @@ export class ServerService extends BaseService {
usage.photos = user.photos; usage.photos = user.photos;
usage.videos = user.videos; usage.videos = user.videos;
usage.usage = user.usage; usage.usage = user.usage;
usage.usagePhotos = user.usagePhotos;
usage.usageVideos = user.usageVideos;
usage.quotaSizeInBytes = user.quotaSizeInBytes; usage.quotaSizeInBytes = user.quotaSizeInBytes;
serverStats.photos += usage.photos; serverStats.photos += usage.photos;
serverStats.videos += usage.videos; serverStats.videos += usage.videos;
serverStats.usage += usage.usage; serverStats.usage += usage.usage;
serverStats.usagePhotos += usage.usagePhotos;
serverStats.usageVideos += usage.usageVideos;
serverStats.usageByUser.push(usage); serverStats.usageByUser.push(usage);
} }

View file

@ -16,6 +16,8 @@
photos: 0, photos: 0,
videos: 0, videos: 0,
usage: 0, usage: 0,
usagePhotos: 0,
usageVideos: 0,
usageByUser: [], usageByUser: [],
}, },
}: Props = $props(); }: Props = $props();
@ -105,8 +107,12 @@
class="flex h-[50px] w-full place-items-center text-center odd:bg-immich-gray even:bg-immich-bg odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50" class="flex h-[50px] w-full place-items-center text-center odd:bg-immich-gray even:bg-immich-bg odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50"
> >
<td class="w-1/4 text-ellipsis px-2 text-sm">{user.userName}</td> <td class="w-1/4 text-ellipsis px-2 text-sm">{user.userName}</td>
<td class="w-1/4 text-ellipsis px-2 text-sm">{user.photos.toLocaleString($locale)}</td> <td class="w-1/4 text-ellipsis px-2 text-sm"
<td class="w-1/4 text-ellipsis px-2 text-sm">{user.videos.toLocaleString($locale)}</td> >{user.photos.toLocaleString($locale)} ({getByteUnitString(user.usagePhotos, $locale, 0)})</td
>
<td class="w-1/4 text-ellipsis px-2 text-sm"
>{user.videos.toLocaleString($locale)} ({getByteUnitString(user.usageVideos, $locale, 0)})</td
>
<td class="w-1/4 text-ellipsis px-2 text-sm"> <td class="w-1/4 text-ellipsis px-2 text-sm">
{getByteUnitString(user.usage, $locale, 0)} {getByteUnitString(user.usage, $locale, 0)}
{#if user.quotaSizeInBytes} {#if user.quotaSizeInBytes}