mirror of
https://github.com/immich-app/immich.git
synced 2025-12-20 17:25:35 +03:00
feat: draft controller entry
chore: lint & format
This commit is contained in:
@@ -323,6 +323,51 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/admin/maintenance": {
|
"/admin/maintenance": {
|
||||||
|
"get": {
|
||||||
|
"description": "...",
|
||||||
|
"operationId": "getIntegrityReport",
|
||||||
|
"parameters": [],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/MaintenanceIntegrityReportResponseDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"summary": "Get integrity report",
|
||||||
|
"tags": [
|
||||||
|
"Maintenance (admin)"
|
||||||
|
],
|
||||||
|
"x-immich-admin-only": true,
|
||||||
|
"x-immich-history": [
|
||||||
|
{
|
||||||
|
"version": "v9.9.9",
|
||||||
|
"state": "Added"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "v9.9.9",
|
||||||
|
"state": "Alpha"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-immich-permission": "maintenance",
|
||||||
|
"x-immich-state": "Alpha"
|
||||||
|
},
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Put Immich into or take it out of maintenance mode",
|
"description": "Put Immich into or take it out of maintenance mode",
|
||||||
"operationId": "setMaintenanceMode",
|
"operationId": "setMaintenanceMode",
|
||||||
@@ -16920,6 +16965,40 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"MaintenanceIntegrityReportDto": {
|
||||||
|
"properties": {
|
||||||
|
"path": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"enum": [
|
||||||
|
"orphan_file",
|
||||||
|
"missing_file",
|
||||||
|
"checksum_mismatch"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"path",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"MaintenanceIntegrityReportResponseDto": {
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/MaintenanceIntegrityReportDto"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"items"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"MaintenanceLoginDto": {
|
"MaintenanceLoginDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"token": {
|
"token": {
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ export const defaults = Object.freeze<SystemConfig>({
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
cronExpression: CronExpression.EVERY_DAY_AT_3AM,
|
cronExpression: CronExpression.EVERY_DAY_AT_3AM,
|
||||||
timeLimit: 60 * 60 * 1000, // 1 hour
|
timeLimit: 60 * 60 * 1000, // 1 hour
|
||||||
percentageLimit: 1.0, // 100% of assets
|
percentageLimit: 1, // 100% of assets
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
job: {
|
job: {
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { BadRequestException, Body, Controller, Post, Res } from '@nestjs/common';
|
import { BadRequestException, Body, Controller, Get, Post, Res } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { MaintenanceAuthDto, MaintenanceLoginDto, SetMaintenanceModeDto } from 'src/dtos/maintenance.dto';
|
import {
|
||||||
|
MaintenanceAuthDto,
|
||||||
|
MaintenanceGetIntegrityReportDto,
|
||||||
|
MaintenanceIntegrityReportResponseDto,
|
||||||
|
MaintenanceLoginDto,
|
||||||
|
SetMaintenanceModeDto,
|
||||||
|
} from 'src/dtos/maintenance.dto';
|
||||||
import { ApiTag, ImmichCookie, MaintenanceAction, Permission } from 'src/enum';
|
import { ApiTag, ImmichCookie, MaintenanceAction, Permission } from 'src/enum';
|
||||||
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
|
||||||
import { LoginDetails } from 'src/services/auth.service';
|
import { LoginDetails } from 'src/services/auth.service';
|
||||||
@@ -46,4 +52,15 @@ export class MaintenanceController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@Endpoint({
|
||||||
|
summary: 'Get integrity report',
|
||||||
|
description: '...',
|
||||||
|
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
|
||||||
|
})
|
||||||
|
@Authenticated({ permission: Permission.Maintenance, admin: true })
|
||||||
|
getIntegrityReport(dto: MaintenanceGetIntegrityReportDto): Promise<MaintenanceIntegrityReportResponseDto> {
|
||||||
|
return this.service.getIntegrityReport(dto);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { MaintenanceAction } from 'src/enum';
|
import { IsEnum } from 'class-validator';
|
||||||
|
import { IntegrityReportType, MaintenanceAction } from 'src/enum';
|
||||||
import { ValidateEnum, ValidateString } from 'src/validation';
|
import { ValidateEnum, ValidateString } from 'src/validation';
|
||||||
|
|
||||||
export class SetMaintenanceModeDto {
|
export class SetMaintenanceModeDto {
|
||||||
@@ -14,3 +15,22 @@ export class MaintenanceLoginDto {
|
|||||||
export class MaintenanceAuthDto {
|
export class MaintenanceAuthDto {
|
||||||
username!: string;
|
username!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class MaintenanceGetIntegrityReportDto {
|
||||||
|
// todo: paginate
|
||||||
|
// @IsInt()
|
||||||
|
// @Min(1)
|
||||||
|
// @Type(() => Number)
|
||||||
|
// @Optional()
|
||||||
|
// page?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MaintenanceIntegrityReportDto {
|
||||||
|
@IsEnum(IntegrityReportType)
|
||||||
|
type!: IntegrityReportType;
|
||||||
|
path!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MaintenanceIntegrityReportResponseDto {
|
||||||
|
items!: MaintenanceIntegrityReportDto[];
|
||||||
|
}
|
||||||
|
|||||||
@@ -382,11 +382,11 @@ export class AssetRepository {
|
|||||||
return items.map((asset) => asset.deviceAssetId);
|
return items.map((asset) => asset.deviceAssetId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllAssetPaths() {
|
getAllAssetPaths() {
|
||||||
return this.db.selectFrom('asset').select(['originalPath', 'encodedVideoPath']).stream();
|
return this.db.selectFrom('asset').select(['originalPath', 'encodedVideoPath']).stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllAssetFilePaths() {
|
getAllAssetFilePaths() {
|
||||||
return this.db.selectFrom('asset_file').select(['path']).stream();
|
return this.db.selectFrom('asset_file').select(['path']).stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Insertable, Kysely } from 'kysely';
|
import { Insertable, Kysely } from 'kysely';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
|
import { MaintenanceGetIntegrityReportDto, MaintenanceIntegrityReportResponseDto } from 'src/dtos/maintenance.dto';
|
||||||
import { DB } from 'src/schema';
|
import { DB } from 'src/schema';
|
||||||
import { IntegrityReportTable } from 'src/schema/tables/integrity-report.table';
|
import { IntegrityReportTable } from 'src/schema/tables/integrity-report.table';
|
||||||
|
|
||||||
@@ -17,6 +18,16 @@ export class IntegrityReportRepository {
|
|||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getIntegrityReport(_dto: MaintenanceGetIntegrityReportDto): Promise<MaintenanceIntegrityReportResponseDto> {
|
||||||
|
return {
|
||||||
|
items: await this.db
|
||||||
|
.selectFrom('integrity_report')
|
||||||
|
.select(['type', 'path'])
|
||||||
|
.orderBy('createdAt', 'desc')
|
||||||
|
.execute(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
deleteById(id: string) {
|
deleteById(id: string) {
|
||||||
return this.db.deleteFrom('integrity_report').where('id', '=', id).execute();
|
return this.db.deleteFrom('integrity_report').where('id', '=', id).execute();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ async function* chunk<T>(generator: AsyncIterableIterator<T>, n: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chunk.length) {
|
if (chunk.length > 0) {
|
||||||
yield chunk;
|
yield chunk;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,17 +83,17 @@ export class IntegrityService extends BaseService {
|
|||||||
|
|
||||||
// debug: run on boot
|
// debug: run on boot
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
this.jobRepository.queue({
|
void this.jobRepository.queue({
|
||||||
name: JobName.IntegrityOrphanedFilesQueueAll,
|
name: JobName.IntegrityOrphanedFilesQueueAll,
|
||||||
data: {},
|
data: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.jobRepository.queue({
|
void this.jobRepository.queue({
|
||||||
name: JobName.IntegrityMissingFilesQueueAll,
|
name: JobName.IntegrityMissingFilesQueueAll,
|
||||||
data: {},
|
data: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.jobRepository.queue({
|
void this.jobRepository.queue({
|
||||||
name: JobName.IntegrityChecksumFiles,
|
name: JobName.IntegrityChecksumFiles,
|
||||||
data: {},
|
data: {},
|
||||||
});
|
});
|
||||||
@@ -101,7 +101,7 @@ export class IntegrityService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent({ name: 'ConfigUpdate', server: true })
|
@OnEvent({ name: 'ConfigUpdate', server: true })
|
||||||
async onConfigUpdate({
|
onConfigUpdate({
|
||||||
newConfig: {
|
newConfig: {
|
||||||
integrityChecks: { orphanedFiles, missingFiles, checksumFiles },
|
integrityChecks: { orphanedFiles, missingFiles, checksumFiles },
|
||||||
},
|
},
|
||||||
@@ -237,9 +237,9 @@ export class IntegrityService extends BaseService {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const reportIds = results.filter((reportId) => reportId) as string[];
|
const reportIds = results.filter(Boolean) as string[];
|
||||||
|
|
||||||
if (reportIds.length) {
|
if (reportIds.length > 0) {
|
||||||
await this.integrityReportRepository.deleteByIds(reportIds);
|
await this.integrityReportRepository.deleteByIds(reportIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,12 +285,12 @@ export class IntegrityService extends BaseService {
|
|||||||
.filter(({ exists, reportId }) => exists && reportId)
|
.filter(({ exists, reportId }) => exists && reportId)
|
||||||
.map(({ reportId }) => reportId!);
|
.map(({ reportId }) => reportId!);
|
||||||
|
|
||||||
if (outdatedReports.length) {
|
if (outdatedReports.length > 0) {
|
||||||
await this.integrityReportRepository.deleteByIds(outdatedReports);
|
await this.integrityReportRepository.deleteByIds(outdatedReports);
|
||||||
}
|
}
|
||||||
|
|
||||||
const missingFiles = results.filter(({ exists }) => !exists);
|
const missingFiles = results.filter(({ exists }) => !exists);
|
||||||
if (missingFiles.length) {
|
if (missingFiles.length > 0) {
|
||||||
await this.integrityReportRepository.create(
|
await this.integrityReportRepository.create(
|
||||||
missingFiles.map(({ path }) => ({
|
missingFiles.map(({ path }) => ({
|
||||||
type: IntegrityReportType.MissingFile,
|
type: IntegrityReportType.MissingFile,
|
||||||
@@ -306,7 +306,7 @@ export class IntegrityService extends BaseService {
|
|||||||
@OnJob({ name: JobName.IntegrityChecksumFiles, queue: QueueName.BackgroundTask })
|
@OnJob({ name: JobName.IntegrityChecksumFiles, queue: QueueName.BackgroundTask })
|
||||||
async handleChecksumFiles(): Promise<JobStatus> {
|
async handleChecksumFiles(): Promise<JobStatus> {
|
||||||
const timeLimit = 60 * 60 * 1000; // 1000;
|
const timeLimit = 60 * 60 * 1000; // 1000;
|
||||||
const percentageLimit = 1.0; // 0.25;
|
const percentageLimit = 1; // 0.25;
|
||||||
|
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Checking file checksums... (will run for up to ${(timeLimit / (60 * 60 * 1000)).toFixed(2)} hours or until ${(percentageLimit * 100).toFixed(2)}% of assets are processed)`,
|
`Checking file checksums... (will run for up to ${(timeLimit / (60 * 60 * 1000)).toFixed(2)} hours or until ${(percentageLimit * 100).toFixed(2)}% of assets are processed)`,
|
||||||
@@ -390,7 +390,7 @@ export class IntegrityService extends BaseService {
|
|||||||
}
|
}
|
||||||
} while (endMarker);
|
} while (endMarker);
|
||||||
|
|
||||||
this.systemMetadataRepository.set(SystemMetadataKey.IntegrityChecksumCheckpoint, {
|
await this.systemMetadataRepository.set(SystemMetadataKey.IntegrityChecksumCheckpoint, {
|
||||||
date: lastCreatedAt?.toISOString(),
|
date: lastCreatedAt?.toISOString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { OnEvent } from 'src/decorators';
|
import { OnEvent } from 'src/decorators';
|
||||||
import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto';
|
import {
|
||||||
|
MaintenanceAuthDto,
|
||||||
|
MaintenanceGetIntegrityReportDto,
|
||||||
|
MaintenanceIntegrityReportResponseDto,
|
||||||
|
} from 'src/dtos/maintenance.dto';
|
||||||
import { SystemMetadataKey } from 'src/enum';
|
import { SystemMetadataKey } from 'src/enum';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { MaintenanceModeState } from 'src/types';
|
import { MaintenanceModeState } from 'src/types';
|
||||||
@@ -50,4 +54,8 @@ export class MaintenanceService extends BaseService {
|
|||||||
|
|
||||||
return await createMaintenanceLoginUrl(baseUrl, auth, secret);
|
return await createMaintenanceLoginUrl(baseUrl, auth, secret);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getIntegrityReport(dto: MaintenanceGetIntegrityReportDto): Promise<MaintenanceIntegrityReportResponseDto> {
|
||||||
|
return this.integrityReportRepository.getIntegrityReport(dto);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
cronExpression: '0 03 * * *',
|
cronExpression: '0 03 * * *',
|
||||||
timeLimit: 60 * 60 * 1000,
|
timeLimit: 60 * 60 * 1000,
|
||||||
percentageLimit: 1.0,
|
percentageLimit: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
logging: {
|
logging: {
|
||||||
|
|||||||
Reference in New Issue
Block a user