mirror of
https://github.com/immich-app/immich.git
synced 2025-12-06 09:13:13 +03:00
Compare commits
41 Commits
06f0f8dc48
...
feat/integ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e7854b5bb | ||
|
|
5d5d421201 | ||
|
|
7a215c16ab | ||
|
|
ae653f9bf5 | ||
|
|
73a17bb58e | ||
|
|
e1a1662225 | ||
|
|
6e752bed77 | ||
|
|
64cc64dd56 | ||
|
|
6cfd1994c4 | ||
|
|
806a2880ca | ||
|
|
042af30bef | ||
|
|
06fcd54b9f | ||
|
|
fec8923431 | ||
|
|
db690bcf63 | ||
|
|
1daf1b471f | ||
|
|
01f96de3e5 | ||
|
|
c4ac8d9f63 | ||
|
|
0362d21945 | ||
|
|
4d7f7b80da | ||
|
|
e447ba87c6 | ||
|
|
2779fce7d0 | ||
|
|
13e9cf0ed9 | ||
|
|
c50118e535 | ||
|
|
ca358f4dae | ||
|
|
d3abed3414 | ||
|
|
0fdc7b4448 | ||
|
|
8db6132669 | ||
|
|
03276de6b2 | ||
|
|
4462683739 | ||
|
|
919eb839ef | ||
|
|
251631948b | ||
|
|
93860238af | ||
|
|
1744237aeb | ||
|
|
ef7d8e94fa | ||
|
|
cc31b9c7f1 | ||
|
|
929ad529f4 | ||
|
|
1e941f3f88 | ||
|
|
15503b150a | ||
|
|
3414210450 | ||
|
|
4a7120cdeb | ||
|
|
f77f43a83d |
@@ -1,9 +1,12 @@
|
||||
import { LoginResponseDto } from '@immich/sdk';
|
||||
import { IntegrityReportType, LoginResponseDto, ManualJobName, QueueName } from '@immich/sdk';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { createUserDto } from 'src/fixtures';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { app, utils } from 'src/utils';
|
||||
import { app, testAssetDir, utils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
import { afterEach, beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
const assetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
|
||||
|
||||
describe('/admin/maintenance', () => {
|
||||
let cookie: string | undefined;
|
||||
@@ -34,6 +37,188 @@ describe('/admin/maintenance', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /integrity/summary (& jobs)', async () => {
|
||||
let baseline: Record<IntegrityReportType, number>;
|
||||
|
||||
beforeAll(async () => {
|
||||
await utils.createAsset(admin.accessToken, {
|
||||
assetData: {
|
||||
filename: 'asset.jpg',
|
||||
bytes: await readFile(assetFilepath),
|
||||
},
|
||||
});
|
||||
|
||||
await utils.copyFolder(`/data/upload/${admin.userId}`, `/data/upload/${admin.userId}-bak`);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await utils.deleteFolder(`/data/upload/${admin.userId}`);
|
||||
await utils.copyFolder(`/data/upload/${admin.userId}-bak`, `/data/upload/${admin.userId}`);
|
||||
});
|
||||
|
||||
it.sequential('may report issues', async () => {
|
||||
await utils.createJob(admin.accessToken, {
|
||||
name: ManualJobName.IntegrityOrphanFiles,
|
||||
});
|
||||
|
||||
await utils.createJob(admin.accessToken, {
|
||||
name: ManualJobName.IntegrityMissingFiles,
|
||||
});
|
||||
|
||||
await utils.createJob(admin.accessToken, {
|
||||
name: ManualJobName.IntegrityChecksumMismatch,
|
||||
});
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, QueueName.BackgroundTask);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.get('/admin/maintenance/integrity/summary')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
missing_file: 0,
|
||||
orphan_file: expect.any(Number),
|
||||
checksum_mismatch: 0,
|
||||
});
|
||||
|
||||
baseline = body;
|
||||
});
|
||||
|
||||
it.sequential('should detect an orphan file (job: check orphan files)', async () => {
|
||||
await utils.putTextFile('orphan', `/data/upload/${admin.userId}/orphan1.png`);
|
||||
|
||||
await utils.createJob(admin.accessToken, {
|
||||
name: ManualJobName.IntegrityOrphanFiles,
|
||||
});
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, QueueName.BackgroundTask);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.get('/admin/maintenance/integrity/summary')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({
|
||||
orphan_file: baseline.orphan_file + 1,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.sequential('should detect outdated orphan file reports (job: refresh orphan files)', async () => {
|
||||
// these should not be detected:
|
||||
await utils.putTextFile('orphan', `/data/upload/${admin.userId}/orphan2.png`);
|
||||
await utils.putTextFile('orphan', `/data/upload/${admin.userId}/orphan3.png`);
|
||||
|
||||
await utils.createJob(admin.accessToken, {
|
||||
name: ManualJobName.IntegrityOrphanFilesRefresh,
|
||||
});
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, QueueName.BackgroundTask);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.get('/admin/maintenance/integrity/summary')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({
|
||||
orphan_file: baseline.orphan_file,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.sequential('should detect a missing file and not a checksum mismatch (job: check missing files)', async () => {
|
||||
await utils.deleteFolder(`/data/upload/${admin.userId}`);
|
||||
|
||||
await utils.createJob(admin.accessToken, {
|
||||
name: ManualJobName.IntegrityMissingFiles,
|
||||
});
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, QueueName.BackgroundTask);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.get('/admin/maintenance/integrity/summary')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({
|
||||
missing_file: 1,
|
||||
checksum_mismatch: 0,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.sequential('should detect outdated missing file reports (job: refresh missing files)', async () => {
|
||||
await utils.createJob(admin.accessToken, {
|
||||
name: ManualJobName.IntegrityMissingFilesRefresh,
|
||||
});
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, QueueName.BackgroundTask);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.get('/admin/maintenance/integrity/summary')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({
|
||||
missing_file: 0,
|
||||
checksum_mismatch: 0,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.sequential('should detect a checksum mismatch (job: check file checksums)', async () => {
|
||||
await utils.truncateFolder(`/data/upload/${admin.userId}`);
|
||||
|
||||
await utils.createJob(admin.accessToken, {
|
||||
name: ManualJobName.IntegrityChecksumMismatch,
|
||||
});
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, QueueName.BackgroundTask);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.get('/admin/maintenance/integrity/summary')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({
|
||||
checksum_mismatch: 1,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.sequential('should detect outdated checksum mismatch reports (job: refresh file checksums)', async () => {
|
||||
await utils.createJob(admin.accessToken, {
|
||||
name: ManualJobName.IntegrityChecksumMismatchRefresh,
|
||||
});
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, QueueName.BackgroundTask);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.get('/admin/maintenance/integrity/summary')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({
|
||||
checksum_mismatch: 0,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// => enter maintenance mode
|
||||
|
||||
describe.sequential('POST /', () => {
|
||||
@@ -83,8 +268,8 @@ describe('/admin/maintenance', () => {
|
||||
return body.maintenanceMode;
|
||||
},
|
||||
{
|
||||
interval: 5e2,
|
||||
timeout: 1e4,
|
||||
interval: 500,
|
||||
timeout: 60_000,
|
||||
},
|
||||
)
|
||||
.toBeTruthy();
|
||||
@@ -162,8 +347,8 @@ describe('/admin/maintenance', () => {
|
||||
return body.maintenanceMode;
|
||||
},
|
||||
{
|
||||
interval: 5e2,
|
||||
timeout: 1e4,
|
||||
interval: 500,
|
||||
timeout: 60_000,
|
||||
},
|
||||
)
|
||||
.toBeFalsy();
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
CheckExistingAssetsDto,
|
||||
CreateAlbumDto,
|
||||
CreateLibraryDto,
|
||||
JobCreateDto,
|
||||
MaintenanceAction,
|
||||
MetadataSearchDto,
|
||||
Permission,
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
checkExistingAssets,
|
||||
createAlbum,
|
||||
createApiKey,
|
||||
createJob,
|
||||
createLibrary,
|
||||
createPartner,
|
||||
createPerson,
|
||||
@@ -52,9 +54,12 @@ import {
|
||||
import { BrowserContext } from '@playwright/test';
|
||||
import { exec, spawn } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { createWriteStream, existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { mkdtemp } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { Readable } from 'node:stream';
|
||||
import { pipeline } from 'node:stream/promises';
|
||||
import { setTimeout as setAsyncTimeout } from 'node:timers/promises';
|
||||
import { promisify } from 'node:util';
|
||||
import pg from 'pg';
|
||||
@@ -171,6 +176,7 @@ export const utils = {
|
||||
'user',
|
||||
'system_metadata',
|
||||
'tag',
|
||||
'integrity_report',
|
||||
];
|
||||
|
||||
const sql: string[] = [];
|
||||
@@ -481,6 +487,9 @@ export const utils = {
|
||||
tagAssets: (accessToken: string, tagId: string, assetIds: string[]) =>
|
||||
tagAssets({ id: tagId, bulkIdsDto: { ids: assetIds } }, { headers: asBearerAuth(accessToken) }),
|
||||
|
||||
createJob: async (accessToken: string, jobCreateDto: JobCreateDto) =>
|
||||
createJob({ jobCreateDto }, { headers: asBearerAuth(accessToken) }),
|
||||
|
||||
queueCommand: async (accessToken: string, name: QueueName, queueCommandDto: QueueCommandDto) =>
|
||||
runQueueCommandLegacy({ name, queueCommandDto }, { headers: asBearerAuth(accessToken) }),
|
||||
|
||||
@@ -559,6 +568,50 @@ export const utils = {
|
||||
mkdirSync(`${testAssetDir}/temp`, { recursive: true });
|
||||
},
|
||||
|
||||
putFile(source: string, dest: string) {
|
||||
return executeCommand('docker', ['cp', source, `immich-e2e-server:${dest}`]).promise;
|
||||
},
|
||||
|
||||
async putTextFile(contents: string, dest: string) {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'test-'));
|
||||
const fn = join(dir, 'file');
|
||||
await pipeline(Readable.from(contents), createWriteStream(fn));
|
||||
return executeCommand('docker', ['cp', fn, `immich-e2e-server:${dest}`]).promise;
|
||||
},
|
||||
|
||||
async move(source: string, dest: string) {
|
||||
return executeCommand('docker', ['exec', 'immich-e2e-server', 'mv', source, dest]).promise;
|
||||
},
|
||||
|
||||
async copyFolder(source: string, dest: string) {
|
||||
return executeCommand('docker', ['exec', 'immich-e2e-server', 'cp', '-r', source, dest]).promise;
|
||||
},
|
||||
|
||||
async deleteFile(path: string) {
|
||||
return executeCommand('docker', ['exec', 'immich-e2e-server', 'rm', path]).promise;
|
||||
},
|
||||
|
||||
async deleteFolder(path: string) {
|
||||
return executeCommand('docker', ['exec', 'immich-e2e-server', 'rm', '-r', path]).promise;
|
||||
},
|
||||
|
||||
async truncateFolder(path: string) {
|
||||
return executeCommand('docker', [
|
||||
'exec',
|
||||
'immich-e2e-server',
|
||||
'find',
|
||||
path,
|
||||
'-type',
|
||||
'f',
|
||||
'-exec',
|
||||
'truncate',
|
||||
'-s',
|
||||
'1',
|
||||
'{}',
|
||||
';',
|
||||
]).promise;
|
||||
},
|
||||
|
||||
resetAdminConfig: async (accessToken: string) => {
|
||||
const defaultConfig = await getConfigDefaults({ headers: asBearerAuth(accessToken) });
|
||||
await updateConfig({ systemConfigDto: defaultConfig }, { headers: asBearerAuth(accessToken) });
|
||||
|
||||
10
i18n/en.json
10
i18n/en.json
@@ -177,6 +177,16 @@
|
||||
"machine_learning_smart_search_enabled": "Enable smart search",
|
||||
"machine_learning_smart_search_enabled_description": "If disabled, images will not be encoded for smart search.",
|
||||
"machine_learning_url_description": "The URL of the machine learning server. If more than one URL is provided, each server will be attempted one-at-a-time until one responds successfully, in order from first to last. Servers that don't respond will be temporarily ignored until they come back online.",
|
||||
"maintenance_integrity_checksum_mismatch": "Checksum Mismatch",
|
||||
"maintenance_integrity_checksum_mismatch_job": "Check for checksum mismatches",
|
||||
"maintenance_integrity_checksum_mismatch_refresh_job": "Refresh checksum mismatch reports",
|
||||
"maintenance_integrity_missing_file": "Missing Files",
|
||||
"maintenance_integrity_missing_file_job": "Check for missing files",
|
||||
"maintenance_integrity_missing_file_refresh_job": "Refresh missing file reports",
|
||||
"maintenance_integrity_orphan_file": "Orphan Files",
|
||||
"maintenance_integrity_orphan_file_job": "Check for orphaned files",
|
||||
"maintenance_integrity_orphan_file_refresh_job": "Refresh orphan file reports",
|
||||
"maintenance_integrity_report": "Integrity Report",
|
||||
"maintenance_settings": "Maintenance",
|
||||
"maintenance_settings_description": "Put Immich into maintenance mode.",
|
||||
"maintenance_start": "Start maintenance mode",
|
||||
|
||||
13
mobile/openapi/README.md
generated
13
mobile/openapi/README.md
generated
@@ -161,6 +161,11 @@ Class | Method | HTTP request | Description
|
||||
*LibrariesApi* | [**scanLibrary**](doc//LibrariesApi.md#scanlibrary) | **POST** /libraries/{id}/scan | Scan a library
|
||||
*LibrariesApi* | [**updateLibrary**](doc//LibrariesApi.md#updatelibrary) | **PUT** /libraries/{id} | Update a library
|
||||
*LibrariesApi* | [**validate**](doc//LibrariesApi.md#validate) | **POST** /libraries/{id}/validate | Validate library settings
|
||||
*MaintenanceAdminApi* | [**deleteIntegrityReport**](doc//MaintenanceAdminApi.md#deleteintegrityreport) | **DELETE** /admin/maintenance/integrity/report/{id} | Delete report entry and perform corresponding deletion action
|
||||
*MaintenanceAdminApi* | [**getIntegrityReport**](doc//MaintenanceAdminApi.md#getintegrityreport) | **POST** /admin/maintenance/integrity/report | Get integrity report by type
|
||||
*MaintenanceAdminApi* | [**getIntegrityReportCsv**](doc//MaintenanceAdminApi.md#getintegrityreportcsv) | **GET** /admin/maintenance/integrity/report/{type}/csv | Export integrity report by type as CSV
|
||||
*MaintenanceAdminApi* | [**getIntegrityReportFile**](doc//MaintenanceAdminApi.md#getintegrityreportfile) | **GET** /admin/maintenance/integrity/report/{id}/file | Download the orphan/broken file if one exists
|
||||
*MaintenanceAdminApi* | [**getIntegrityReportSummary**](doc//MaintenanceAdminApi.md#getintegrityreportsummary) | **GET** /admin/maintenance/integrity/summary | Get integrity report summary
|
||||
*MaintenanceAdminApi* | [**maintenanceLogin**](doc//MaintenanceAdminApi.md#maintenancelogin) | **POST** /admin/maintenance/login | Log into maintenance mode
|
||||
*MaintenanceAdminApi* | [**setMaintenanceMode**](doc//MaintenanceAdminApi.md#setmaintenancemode) | **POST** /admin/maintenance | Set maintenance mode
|
||||
*MapApi* | [**getMapMarkers**](doc//MapApi.md#getmapmarkers) | **GET** /map/markers | Retrieve map markers
|
||||
@@ -402,6 +407,7 @@ Class | Method | HTTP request | Description
|
||||
- [FoldersResponse](doc//FoldersResponse.md)
|
||||
- [FoldersUpdate](doc//FoldersUpdate.md)
|
||||
- [ImageFormat](doc//ImageFormat.md)
|
||||
- [IntegrityReportType](doc//IntegrityReportType.md)
|
||||
- [JobCreateDto](doc//JobCreateDto.md)
|
||||
- [JobName](doc//JobName.md)
|
||||
- [JobSettingsDto](doc//JobSettingsDto.md)
|
||||
@@ -416,6 +422,10 @@ Class | Method | HTTP request | Description
|
||||
- [MachineLearningAvailabilityChecksDto](doc//MachineLearningAvailabilityChecksDto.md)
|
||||
- [MaintenanceAction](doc//MaintenanceAction.md)
|
||||
- [MaintenanceAuthDto](doc//MaintenanceAuthDto.md)
|
||||
- [MaintenanceGetIntegrityReportDto](doc//MaintenanceGetIntegrityReportDto.md)
|
||||
- [MaintenanceIntegrityReportDto](doc//MaintenanceIntegrityReportDto.md)
|
||||
- [MaintenanceIntegrityReportResponseDto](doc//MaintenanceIntegrityReportResponseDto.md)
|
||||
- [MaintenanceIntegrityReportSummaryResponseDto](doc//MaintenanceIntegrityReportSummaryResponseDto.md)
|
||||
- [MaintenanceLoginDto](doc//MaintenanceLoginDto.md)
|
||||
- [ManualJobName](doc//ManualJobName.md)
|
||||
- [MapMarkerResponseDto](doc//MapMarkerResponseDto.md)
|
||||
@@ -569,6 +579,9 @@ Class | Method | HTTP request | Description
|
||||
- [SystemConfigGeneratedFullsizeImageDto](doc//SystemConfigGeneratedFullsizeImageDto.md)
|
||||
- [SystemConfigGeneratedImageDto](doc//SystemConfigGeneratedImageDto.md)
|
||||
- [SystemConfigImageDto](doc//SystemConfigImageDto.md)
|
||||
- [SystemConfigIntegrityChecks](doc//SystemConfigIntegrityChecks.md)
|
||||
- [SystemConfigIntegrityChecksumJob](doc//SystemConfigIntegrityChecksumJob.md)
|
||||
- [SystemConfigIntegrityJob](doc//SystemConfigIntegrityJob.md)
|
||||
- [SystemConfigJobDto](doc//SystemConfigJobDto.md)
|
||||
- [SystemConfigLibraryDto](doc//SystemConfigLibraryDto.md)
|
||||
- [SystemConfigLibraryScanDto](doc//SystemConfigLibraryScanDto.md)
|
||||
|
||||
8
mobile/openapi/lib/api.dart
generated
8
mobile/openapi/lib/api.dart
generated
@@ -154,6 +154,7 @@ part 'model/facial_recognition_config.dart';
|
||||
part 'model/folders_response.dart';
|
||||
part 'model/folders_update.dart';
|
||||
part 'model/image_format.dart';
|
||||
part 'model/integrity_report_type.dart';
|
||||
part 'model/job_create_dto.dart';
|
||||
part 'model/job_name.dart';
|
||||
part 'model/job_settings_dto.dart';
|
||||
@@ -168,6 +169,10 @@ part 'model/logout_response_dto.dart';
|
||||
part 'model/machine_learning_availability_checks_dto.dart';
|
||||
part 'model/maintenance_action.dart';
|
||||
part 'model/maintenance_auth_dto.dart';
|
||||
part 'model/maintenance_get_integrity_report_dto.dart';
|
||||
part 'model/maintenance_integrity_report_dto.dart';
|
||||
part 'model/maintenance_integrity_report_response_dto.dart';
|
||||
part 'model/maintenance_integrity_report_summary_response_dto.dart';
|
||||
part 'model/maintenance_login_dto.dart';
|
||||
part 'model/manual_job_name.dart';
|
||||
part 'model/map_marker_response_dto.dart';
|
||||
@@ -321,6 +326,9 @@ part 'model/system_config_faces_dto.dart';
|
||||
part 'model/system_config_generated_fullsize_image_dto.dart';
|
||||
part 'model/system_config_generated_image_dto.dart';
|
||||
part 'model/system_config_image_dto.dart';
|
||||
part 'model/system_config_integrity_checks.dart';
|
||||
part 'model/system_config_integrity_checksum_job.dart';
|
||||
part 'model/system_config_integrity_job.dart';
|
||||
part 'model/system_config_job_dto.dart';
|
||||
part 'model/system_config_library_dto.dart';
|
||||
part 'model/system_config_library_scan_dto.dart';
|
||||
|
||||
267
mobile/openapi/lib/api/maintenance_admin_api.dart
generated
267
mobile/openapi/lib/api/maintenance_admin_api.dart
generated
@@ -16,6 +16,273 @@ class MaintenanceAdminApi {
|
||||
|
||||
final ApiClient apiClient;
|
||||
|
||||
/// Delete report entry and perform corresponding deletion action
|
||||
///
|
||||
/// ...
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<Response> deleteIntegrityReportWithHttpInfo(String id,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/admin/maintenance/integrity/report/{id}'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'DELETE',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Delete report entry and perform corresponding deletion action
|
||||
///
|
||||
/// ...
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<void> deleteIntegrityReport(String id,) async {
|
||||
final response = await deleteIntegrityReportWithHttpInfo(id,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
}
|
||||
|
||||
/// Get integrity report by type
|
||||
///
|
||||
/// ...
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [MaintenanceGetIntegrityReportDto] maintenanceGetIntegrityReportDto (required):
|
||||
Future<Response> getIntegrityReportWithHttpInfo(MaintenanceGetIntegrityReportDto maintenanceGetIntegrityReportDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/admin/maintenance/integrity/report';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = maintenanceGetIntegrityReportDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'POST',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Get integrity report by type
|
||||
///
|
||||
/// ...
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [MaintenanceGetIntegrityReportDto] maintenanceGetIntegrityReportDto (required):
|
||||
Future<MaintenanceIntegrityReportResponseDto?> getIntegrityReport(MaintenanceGetIntegrityReportDto maintenanceGetIntegrityReportDto,) async {
|
||||
final response = await getIntegrityReportWithHttpInfo(maintenanceGetIntegrityReportDto,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MaintenanceIntegrityReportResponseDto',) as MaintenanceIntegrityReportResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Export integrity report by type as CSV
|
||||
///
|
||||
/// ...
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [IntegrityReportType] type (required):
|
||||
Future<Response> getIntegrityReportCsvWithHttpInfo(IntegrityReportType type,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/admin/maintenance/integrity/report/{type}/csv'
|
||||
.replaceAll('{type}', type.toString());
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Export integrity report by type as CSV
|
||||
///
|
||||
/// ...
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [IntegrityReportType] type (required):
|
||||
Future<MultipartFile?> getIntegrityReportCsv(IntegrityReportType type,) async {
|
||||
final response = await getIntegrityReportCsvWithHttpInfo(type,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MultipartFile',) as MultipartFile;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Download the orphan/broken file if one exists
|
||||
///
|
||||
/// ...
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<Response> getIntegrityReportFileWithHttpInfo(String id,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/admin/maintenance/integrity/report/{id}/file'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Download the orphan/broken file if one exists
|
||||
///
|
||||
/// ...
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<MultipartFile?> getIntegrityReportFile(String id,) async {
|
||||
final response = await getIntegrityReportFileWithHttpInfo(id,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MultipartFile',) as MultipartFile;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Get integrity report summary
|
||||
///
|
||||
/// ...
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
Future<Response> getIntegrityReportSummaryWithHttpInfo() async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/admin/maintenance/integrity/summary';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Get integrity report summary
|
||||
///
|
||||
/// ...
|
||||
Future<MaintenanceIntegrityReportSummaryResponseDto?> getIntegrityReportSummary() async {
|
||||
final response = await getIntegrityReportSummaryWithHttpInfo();
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MaintenanceIntegrityReportSummaryResponseDto',) as MaintenanceIntegrityReportSummaryResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Log into maintenance mode
|
||||
///
|
||||
/// Login with maintenance token or cookie to receive current information and perform further actions.
|
||||
|
||||
16
mobile/openapi/lib/api_client.dart
generated
16
mobile/openapi/lib/api_client.dart
generated
@@ -356,6 +356,8 @@ class ApiClient {
|
||||
return FoldersUpdate.fromJson(value);
|
||||
case 'ImageFormat':
|
||||
return ImageFormatTypeTransformer().decode(value);
|
||||
case 'IntegrityReportType':
|
||||
return IntegrityReportTypeTypeTransformer().decode(value);
|
||||
case 'JobCreateDto':
|
||||
return JobCreateDto.fromJson(value);
|
||||
case 'JobName':
|
||||
@@ -384,6 +386,14 @@ class ApiClient {
|
||||
return MaintenanceActionTypeTransformer().decode(value);
|
||||
case 'MaintenanceAuthDto':
|
||||
return MaintenanceAuthDto.fromJson(value);
|
||||
case 'MaintenanceGetIntegrityReportDto':
|
||||
return MaintenanceGetIntegrityReportDto.fromJson(value);
|
||||
case 'MaintenanceIntegrityReportDto':
|
||||
return MaintenanceIntegrityReportDto.fromJson(value);
|
||||
case 'MaintenanceIntegrityReportResponseDto':
|
||||
return MaintenanceIntegrityReportResponseDto.fromJson(value);
|
||||
case 'MaintenanceIntegrityReportSummaryResponseDto':
|
||||
return MaintenanceIntegrityReportSummaryResponseDto.fromJson(value);
|
||||
case 'MaintenanceLoginDto':
|
||||
return MaintenanceLoginDto.fromJson(value);
|
||||
case 'ManualJobName':
|
||||
@@ -690,6 +700,12 @@ class ApiClient {
|
||||
return SystemConfigGeneratedImageDto.fromJson(value);
|
||||
case 'SystemConfigImageDto':
|
||||
return SystemConfigImageDto.fromJson(value);
|
||||
case 'SystemConfigIntegrityChecks':
|
||||
return SystemConfigIntegrityChecks.fromJson(value);
|
||||
case 'SystemConfigIntegrityChecksumJob':
|
||||
return SystemConfigIntegrityChecksumJob.fromJson(value);
|
||||
case 'SystemConfigIntegrityJob':
|
||||
return SystemConfigIntegrityJob.fromJson(value);
|
||||
case 'SystemConfigJobDto':
|
||||
return SystemConfigJobDto.fromJson(value);
|
||||
case 'SystemConfigLibraryDto':
|
||||
|
||||
3
mobile/openapi/lib/api_helper.dart
generated
3
mobile/openapi/lib/api_helper.dart
generated
@@ -94,6 +94,9 @@ String parameterToString(dynamic value) {
|
||||
if (value is ImageFormat) {
|
||||
return ImageFormatTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is IntegrityReportType) {
|
||||
return IntegrityReportTypeTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is JobName) {
|
||||
return JobNameTypeTransformer().encode(value).toString();
|
||||
}
|
||||
|
||||
88
mobile/openapi/lib/model/integrity_report_type.dart
generated
Normal file
88
mobile/openapi/lib/model/integrity_report_type.dart
generated
Normal file
@@ -0,0 +1,88 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
|
||||
class IntegrityReportType {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const IntegrityReportType._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const orphanFile = IntegrityReportType._(r'orphan_file');
|
||||
static const missingFile = IntegrityReportType._(r'missing_file');
|
||||
static const checksumMismatch = IntegrityReportType._(r'checksum_mismatch');
|
||||
|
||||
/// List of all possible values in this [enum][IntegrityReportType].
|
||||
static const values = <IntegrityReportType>[
|
||||
orphanFile,
|
||||
missingFile,
|
||||
checksumMismatch,
|
||||
];
|
||||
|
||||
static IntegrityReportType? fromJson(dynamic value) => IntegrityReportTypeTypeTransformer().decode(value);
|
||||
|
||||
static List<IntegrityReportType> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <IntegrityReportType>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = IntegrityReportType.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [IntegrityReportType] to String,
|
||||
/// and [decode] dynamic data back to [IntegrityReportType].
|
||||
class IntegrityReportTypeTypeTransformer {
|
||||
factory IntegrityReportTypeTypeTransformer() => _instance ??= const IntegrityReportTypeTypeTransformer._();
|
||||
|
||||
const IntegrityReportTypeTypeTransformer._();
|
||||
|
||||
String encode(IntegrityReportType data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a IntegrityReportType.
|
||||
///
|
||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||
///
|
||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||
/// and users are still using an old app with the old code.
|
||||
IntegrityReportType? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'orphan_file': return IntegrityReportType.orphanFile;
|
||||
case r'missing_file': return IntegrityReportType.missingFile;
|
||||
case r'checksum_mismatch': return IntegrityReportType.checksumMismatch;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [IntegrityReportTypeTypeTransformer] instance.
|
||||
static IntegrityReportTypeTypeTransformer? _instance;
|
||||
}
|
||||
|
||||
27
mobile/openapi/lib/model/job_name.dart
generated
27
mobile/openapi/lib/model/job_name.dart
generated
@@ -78,6 +78,15 @@ class JobName {
|
||||
static const ocrQueueAll = JobName._(r'OcrQueueAll');
|
||||
static const ocr = JobName._(r'Ocr');
|
||||
static const workflowRun = JobName._(r'WorkflowRun');
|
||||
static const integrityOrphanedFilesQueueAll = JobName._(r'IntegrityOrphanedFilesQueueAll');
|
||||
static const integrityOrphanedFiles = JobName._(r'IntegrityOrphanedFiles');
|
||||
static const integrityOrphanedRefresh = JobName._(r'IntegrityOrphanedRefresh');
|
||||
static const integrityMissingFilesQueueAll = JobName._(r'IntegrityMissingFilesQueueAll');
|
||||
static const integrityMissingFiles = JobName._(r'IntegrityMissingFiles');
|
||||
static const integrityMissingFilesRefresh = JobName._(r'IntegrityMissingFilesRefresh');
|
||||
static const integrityChecksumFiles = JobName._(r'IntegrityChecksumFiles');
|
||||
static const integrityChecksumFilesRefresh = JobName._(r'IntegrityChecksumFilesRefresh');
|
||||
static const integrityReportDelete = JobName._(r'IntegrityReportDelete');
|
||||
|
||||
/// List of all possible values in this [enum][JobName].
|
||||
static const values = <JobName>[
|
||||
@@ -136,6 +145,15 @@ class JobName {
|
||||
ocrQueueAll,
|
||||
ocr,
|
||||
workflowRun,
|
||||
integrityOrphanedFilesQueueAll,
|
||||
integrityOrphanedFiles,
|
||||
integrityOrphanedRefresh,
|
||||
integrityMissingFilesQueueAll,
|
||||
integrityMissingFiles,
|
||||
integrityMissingFilesRefresh,
|
||||
integrityChecksumFiles,
|
||||
integrityChecksumFilesRefresh,
|
||||
integrityReportDelete,
|
||||
];
|
||||
|
||||
static JobName? fromJson(dynamic value) => JobNameTypeTransformer().decode(value);
|
||||
@@ -229,6 +247,15 @@ class JobNameTypeTransformer {
|
||||
case r'OcrQueueAll': return JobName.ocrQueueAll;
|
||||
case r'Ocr': return JobName.ocr;
|
||||
case r'WorkflowRun': return JobName.workflowRun;
|
||||
case r'IntegrityOrphanedFilesQueueAll': return JobName.integrityOrphanedFilesQueueAll;
|
||||
case r'IntegrityOrphanedFiles': return JobName.integrityOrphanedFiles;
|
||||
case r'IntegrityOrphanedRefresh': return JobName.integrityOrphanedRefresh;
|
||||
case r'IntegrityMissingFilesQueueAll': return JobName.integrityMissingFilesQueueAll;
|
||||
case r'IntegrityMissingFiles': return JobName.integrityMissingFiles;
|
||||
case r'IntegrityMissingFilesRefresh': return JobName.integrityMissingFilesRefresh;
|
||||
case r'IntegrityChecksumFiles': return JobName.integrityChecksumFiles;
|
||||
case r'IntegrityChecksumFilesRefresh': return JobName.integrityChecksumFilesRefresh;
|
||||
case r'IntegrityReportDelete': return JobName.integrityReportDelete;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
|
||||
99
mobile/openapi/lib/model/maintenance_get_integrity_report_dto.dart
generated
Normal file
99
mobile/openapi/lib/model/maintenance_get_integrity_report_dto.dart
generated
Normal file
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class MaintenanceGetIntegrityReportDto {
|
||||
/// Returns a new [MaintenanceGetIntegrityReportDto] instance.
|
||||
MaintenanceGetIntegrityReportDto({
|
||||
required this.type,
|
||||
});
|
||||
|
||||
IntegrityReportType type;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is MaintenanceGetIntegrityReportDto &&
|
||||
other.type == type;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(type.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'MaintenanceGetIntegrityReportDto[type=$type]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'type'] = this.type;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [MaintenanceGetIntegrityReportDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static MaintenanceGetIntegrityReportDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "MaintenanceGetIntegrityReportDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return MaintenanceGetIntegrityReportDto(
|
||||
type: IntegrityReportType.fromJson(json[r'type'])!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<MaintenanceGetIntegrityReportDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <MaintenanceGetIntegrityReportDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = MaintenanceGetIntegrityReportDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, MaintenanceGetIntegrityReportDto> mapFromJson(dynamic json) {
|
||||
final map = <String, MaintenanceGetIntegrityReportDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = MaintenanceGetIntegrityReportDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of MaintenanceGetIntegrityReportDto-objects as value to a dart map
|
||||
static Map<String, List<MaintenanceGetIntegrityReportDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<MaintenanceGetIntegrityReportDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = MaintenanceGetIntegrityReportDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'type',
|
||||
};
|
||||
}
|
||||
|
||||
115
mobile/openapi/lib/model/maintenance_integrity_report_dto.dart
generated
Normal file
115
mobile/openapi/lib/model/maintenance_integrity_report_dto.dart
generated
Normal file
@@ -0,0 +1,115 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class MaintenanceIntegrityReportDto {
|
||||
/// Returns a new [MaintenanceIntegrityReportDto] instance.
|
||||
MaintenanceIntegrityReportDto({
|
||||
required this.id,
|
||||
required this.path,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
String id;
|
||||
|
||||
String path;
|
||||
|
||||
IntegrityReportType type;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is MaintenanceIntegrityReportDto &&
|
||||
other.id == id &&
|
||||
other.path == path &&
|
||||
other.type == type;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(id.hashCode) +
|
||||
(path.hashCode) +
|
||||
(type.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'MaintenanceIntegrityReportDto[id=$id, path=$path, type=$type]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'id'] = this.id;
|
||||
json[r'path'] = this.path;
|
||||
json[r'type'] = this.type;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [MaintenanceIntegrityReportDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static MaintenanceIntegrityReportDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "MaintenanceIntegrityReportDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return MaintenanceIntegrityReportDto(
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
path: mapValueOfType<String>(json, r'path')!,
|
||||
type: IntegrityReportType.fromJson(json[r'type'])!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<MaintenanceIntegrityReportDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <MaintenanceIntegrityReportDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = MaintenanceIntegrityReportDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, MaintenanceIntegrityReportDto> mapFromJson(dynamic json) {
|
||||
final map = <String, MaintenanceIntegrityReportDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = MaintenanceIntegrityReportDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of MaintenanceIntegrityReportDto-objects as value to a dart map
|
||||
static Map<String, List<MaintenanceIntegrityReportDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<MaintenanceIntegrityReportDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = MaintenanceIntegrityReportDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'id',
|
||||
'path',
|
||||
'type',
|
||||
};
|
||||
}
|
||||
|
||||
99
mobile/openapi/lib/model/maintenance_integrity_report_response_dto.dart
generated
Normal file
99
mobile/openapi/lib/model/maintenance_integrity_report_response_dto.dart
generated
Normal file
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class MaintenanceIntegrityReportResponseDto {
|
||||
/// Returns a new [MaintenanceIntegrityReportResponseDto] instance.
|
||||
MaintenanceIntegrityReportResponseDto({
|
||||
this.items = const [],
|
||||
});
|
||||
|
||||
List<MaintenanceIntegrityReportDto> items;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is MaintenanceIntegrityReportResponseDto &&
|
||||
_deepEquality.equals(other.items, items);
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(items.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'MaintenanceIntegrityReportResponseDto[items=$items]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'items'] = this.items;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [MaintenanceIntegrityReportResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static MaintenanceIntegrityReportResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "MaintenanceIntegrityReportResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return MaintenanceIntegrityReportResponseDto(
|
||||
items: MaintenanceIntegrityReportDto.listFromJson(json[r'items']),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<MaintenanceIntegrityReportResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <MaintenanceIntegrityReportResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = MaintenanceIntegrityReportResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, MaintenanceIntegrityReportResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, MaintenanceIntegrityReportResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = MaintenanceIntegrityReportResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of MaintenanceIntegrityReportResponseDto-objects as value to a dart map
|
||||
static Map<String, List<MaintenanceIntegrityReportResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<MaintenanceIntegrityReportResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = MaintenanceIntegrityReportResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'items',
|
||||
};
|
||||
}
|
||||
|
||||
115
mobile/openapi/lib/model/maintenance_integrity_report_summary_response_dto.dart
generated
Normal file
115
mobile/openapi/lib/model/maintenance_integrity_report_summary_response_dto.dart
generated
Normal file
@@ -0,0 +1,115 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class MaintenanceIntegrityReportSummaryResponseDto {
|
||||
/// Returns a new [MaintenanceIntegrityReportSummaryResponseDto] instance.
|
||||
MaintenanceIntegrityReportSummaryResponseDto({
|
||||
required this.checksumMismatch,
|
||||
required this.missingFile,
|
||||
required this.orphanFile,
|
||||
});
|
||||
|
||||
int checksumMismatch;
|
||||
|
||||
int missingFile;
|
||||
|
||||
int orphanFile;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is MaintenanceIntegrityReportSummaryResponseDto &&
|
||||
other.checksumMismatch == checksumMismatch &&
|
||||
other.missingFile == missingFile &&
|
||||
other.orphanFile == orphanFile;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(checksumMismatch.hashCode) +
|
||||
(missingFile.hashCode) +
|
||||
(orphanFile.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'MaintenanceIntegrityReportSummaryResponseDto[checksumMismatch=$checksumMismatch, missingFile=$missingFile, orphanFile=$orphanFile]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'checksum_mismatch'] = this.checksumMismatch;
|
||||
json[r'missing_file'] = this.missingFile;
|
||||
json[r'orphan_file'] = this.orphanFile;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [MaintenanceIntegrityReportSummaryResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static MaintenanceIntegrityReportSummaryResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "MaintenanceIntegrityReportSummaryResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return MaintenanceIntegrityReportSummaryResponseDto(
|
||||
checksumMismatch: mapValueOfType<int>(json, r'checksum_mismatch')!,
|
||||
missingFile: mapValueOfType<int>(json, r'missing_file')!,
|
||||
orphanFile: mapValueOfType<int>(json, r'orphan_file')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<MaintenanceIntegrityReportSummaryResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <MaintenanceIntegrityReportSummaryResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = MaintenanceIntegrityReportSummaryResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, MaintenanceIntegrityReportSummaryResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, MaintenanceIntegrityReportSummaryResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = MaintenanceIntegrityReportSummaryResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of MaintenanceIntegrityReportSummaryResponseDto-objects as value to a dart map
|
||||
static Map<String, List<MaintenanceIntegrityReportSummaryResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<MaintenanceIntegrityReportSummaryResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = MaintenanceIntegrityReportSummaryResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'checksum_mismatch',
|
||||
'missing_file',
|
||||
'orphan_file',
|
||||
};
|
||||
}
|
||||
|
||||
27
mobile/openapi/lib/model/manual_job_name.dart
generated
27
mobile/openapi/lib/model/manual_job_name.dart
generated
@@ -29,6 +29,15 @@ class ManualJobName {
|
||||
static const memoryCleanup = ManualJobName._(r'memory-cleanup');
|
||||
static const memoryCreate = ManualJobName._(r'memory-create');
|
||||
static const backupDatabase = ManualJobName._(r'backup-database');
|
||||
static const integrityMissingFiles = ManualJobName._(r'integrity-missing-files');
|
||||
static const integrityOrphanFiles = ManualJobName._(r'integrity-orphan-files');
|
||||
static const integrityChecksumMismatch = ManualJobName._(r'integrity-checksum-mismatch');
|
||||
static const integrityMissingFilesRefresh = ManualJobName._(r'integrity-missing-files-refresh');
|
||||
static const integrityOrphanFilesRefresh = ManualJobName._(r'integrity-orphan-files-refresh');
|
||||
static const integrityChecksumMismatchRefresh = ManualJobName._(r'integrity-checksum-mismatch-refresh');
|
||||
static const integrityMissingFilesDeleteAll = ManualJobName._(r'integrity-missing-files-delete-all');
|
||||
static const integrityOrphanFilesDeleteAll = ManualJobName._(r'integrity-orphan-files-delete-all');
|
||||
static const integrityChecksumMismatchDeleteAll = ManualJobName._(r'integrity-checksum-mismatch-delete-all');
|
||||
|
||||
/// List of all possible values in this [enum][ManualJobName].
|
||||
static const values = <ManualJobName>[
|
||||
@@ -38,6 +47,15 @@ class ManualJobName {
|
||||
memoryCleanup,
|
||||
memoryCreate,
|
||||
backupDatabase,
|
||||
integrityMissingFiles,
|
||||
integrityOrphanFiles,
|
||||
integrityChecksumMismatch,
|
||||
integrityMissingFilesRefresh,
|
||||
integrityOrphanFilesRefresh,
|
||||
integrityChecksumMismatchRefresh,
|
||||
integrityMissingFilesDeleteAll,
|
||||
integrityOrphanFilesDeleteAll,
|
||||
integrityChecksumMismatchDeleteAll,
|
||||
];
|
||||
|
||||
static ManualJobName? fromJson(dynamic value) => ManualJobNameTypeTransformer().decode(value);
|
||||
@@ -82,6 +100,15 @@ class ManualJobNameTypeTransformer {
|
||||
case r'memory-cleanup': return ManualJobName.memoryCleanup;
|
||||
case r'memory-create': return ManualJobName.memoryCreate;
|
||||
case r'backup-database': return ManualJobName.backupDatabase;
|
||||
case r'integrity-missing-files': return ManualJobName.integrityMissingFiles;
|
||||
case r'integrity-orphan-files': return ManualJobName.integrityOrphanFiles;
|
||||
case r'integrity-checksum-mismatch': return ManualJobName.integrityChecksumMismatch;
|
||||
case r'integrity-missing-files-refresh': return ManualJobName.integrityMissingFilesRefresh;
|
||||
case r'integrity-orphan-files-refresh': return ManualJobName.integrityOrphanFilesRefresh;
|
||||
case r'integrity-checksum-mismatch-refresh': return ManualJobName.integrityChecksumMismatchRefresh;
|
||||
case r'integrity-missing-files-delete-all': return ManualJobName.integrityMissingFilesDeleteAll;
|
||||
case r'integrity-orphan-files-delete-all': return ManualJobName.integrityOrphanFilesDeleteAll;
|
||||
case r'integrity-checksum-mismatch-delete-all': return ManualJobName.integrityChecksumMismatchDeleteAll;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
|
||||
10
mobile/openapi/lib/model/system_config_dto.dart
generated
10
mobile/openapi/lib/model/system_config_dto.dart
generated
@@ -16,6 +16,7 @@ class SystemConfigDto {
|
||||
required this.backup,
|
||||
required this.ffmpeg,
|
||||
required this.image,
|
||||
required this.integrityChecks,
|
||||
required this.job,
|
||||
required this.library_,
|
||||
required this.logging,
|
||||
@@ -42,6 +43,8 @@ class SystemConfigDto {
|
||||
|
||||
SystemConfigImageDto image;
|
||||
|
||||
SystemConfigIntegrityChecks integrityChecks;
|
||||
|
||||
SystemConfigJobDto job;
|
||||
|
||||
SystemConfigLibraryDto library_;
|
||||
@@ -83,6 +86,7 @@ class SystemConfigDto {
|
||||
other.backup == backup &&
|
||||
other.ffmpeg == ffmpeg &&
|
||||
other.image == image &&
|
||||
other.integrityChecks == integrityChecks &&
|
||||
other.job == job &&
|
||||
other.library_ == library_ &&
|
||||
other.logging == logging &&
|
||||
@@ -108,6 +112,7 @@ class SystemConfigDto {
|
||||
(backup.hashCode) +
|
||||
(ffmpeg.hashCode) +
|
||||
(image.hashCode) +
|
||||
(integrityChecks.hashCode) +
|
||||
(job.hashCode) +
|
||||
(library_.hashCode) +
|
||||
(logging.hashCode) +
|
||||
@@ -128,13 +133,14 @@ class SystemConfigDto {
|
||||
(user.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SystemConfigDto[backup=$backup, ffmpeg=$ffmpeg, image=$image, job=$job, library_=$library_, logging=$logging, machineLearning=$machineLearning, map=$map, metadata=$metadata, newVersionCheck=$newVersionCheck, nightlyTasks=$nightlyTasks, notifications=$notifications, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, server=$server, storageTemplate=$storageTemplate, templates=$templates, theme=$theme, trash=$trash, user=$user]';
|
||||
String toString() => 'SystemConfigDto[backup=$backup, ffmpeg=$ffmpeg, image=$image, integrityChecks=$integrityChecks, job=$job, library_=$library_, logging=$logging, machineLearning=$machineLearning, map=$map, metadata=$metadata, newVersionCheck=$newVersionCheck, nightlyTasks=$nightlyTasks, notifications=$notifications, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, server=$server, storageTemplate=$storageTemplate, templates=$templates, theme=$theme, trash=$trash, user=$user]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'backup'] = this.backup;
|
||||
json[r'ffmpeg'] = this.ffmpeg;
|
||||
json[r'image'] = this.image;
|
||||
json[r'integrityChecks'] = this.integrityChecks;
|
||||
json[r'job'] = this.job;
|
||||
json[r'library'] = this.library_;
|
||||
json[r'logging'] = this.logging;
|
||||
@@ -168,6 +174,7 @@ class SystemConfigDto {
|
||||
backup: SystemConfigBackupsDto.fromJson(json[r'backup'])!,
|
||||
ffmpeg: SystemConfigFFmpegDto.fromJson(json[r'ffmpeg'])!,
|
||||
image: SystemConfigImageDto.fromJson(json[r'image'])!,
|
||||
integrityChecks: SystemConfigIntegrityChecks.fromJson(json[r'integrityChecks'])!,
|
||||
job: SystemConfigJobDto.fromJson(json[r'job'])!,
|
||||
library_: SystemConfigLibraryDto.fromJson(json[r'library'])!,
|
||||
logging: SystemConfigLoggingDto.fromJson(json[r'logging'])!,
|
||||
@@ -236,6 +243,7 @@ class SystemConfigDto {
|
||||
'backup',
|
||||
'ffmpeg',
|
||||
'image',
|
||||
'integrityChecks',
|
||||
'job',
|
||||
'library',
|
||||
'logging',
|
||||
|
||||
115
mobile/openapi/lib/model/system_config_integrity_checks.dart
generated
Normal file
115
mobile/openapi/lib/model/system_config_integrity_checks.dart
generated
Normal file
@@ -0,0 +1,115 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class SystemConfigIntegrityChecks {
|
||||
/// Returns a new [SystemConfigIntegrityChecks] instance.
|
||||
SystemConfigIntegrityChecks({
|
||||
required this.checksumFiles,
|
||||
required this.missingFiles,
|
||||
required this.orphanedFiles,
|
||||
});
|
||||
|
||||
SystemConfigIntegrityChecksumJob checksumFiles;
|
||||
|
||||
SystemConfigIntegrityJob missingFiles;
|
||||
|
||||
SystemConfigIntegrityJob orphanedFiles;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SystemConfigIntegrityChecks &&
|
||||
other.checksumFiles == checksumFiles &&
|
||||
other.missingFiles == missingFiles &&
|
||||
other.orphanedFiles == orphanedFiles;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(checksumFiles.hashCode) +
|
||||
(missingFiles.hashCode) +
|
||||
(orphanedFiles.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SystemConfigIntegrityChecks[checksumFiles=$checksumFiles, missingFiles=$missingFiles, orphanedFiles=$orphanedFiles]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'checksumFiles'] = this.checksumFiles;
|
||||
json[r'missingFiles'] = this.missingFiles;
|
||||
json[r'orphanedFiles'] = this.orphanedFiles;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [SystemConfigIntegrityChecks] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static SystemConfigIntegrityChecks? fromJson(dynamic value) {
|
||||
upgradeDto(value, "SystemConfigIntegrityChecks");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return SystemConfigIntegrityChecks(
|
||||
checksumFiles: SystemConfigIntegrityChecksumJob.fromJson(json[r'checksumFiles'])!,
|
||||
missingFiles: SystemConfigIntegrityJob.fromJson(json[r'missingFiles'])!,
|
||||
orphanedFiles: SystemConfigIntegrityJob.fromJson(json[r'orphanedFiles'])!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<SystemConfigIntegrityChecks> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <SystemConfigIntegrityChecks>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = SystemConfigIntegrityChecks.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, SystemConfigIntegrityChecks> mapFromJson(dynamic json) {
|
||||
final map = <String, SystemConfigIntegrityChecks>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = SystemConfigIntegrityChecks.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of SystemConfigIntegrityChecks-objects as value to a dart map
|
||||
static Map<String, List<SystemConfigIntegrityChecks>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<SystemConfigIntegrityChecks>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = SystemConfigIntegrityChecks.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'checksumFiles',
|
||||
'missingFiles',
|
||||
'orphanedFiles',
|
||||
};
|
||||
}
|
||||
|
||||
123
mobile/openapi/lib/model/system_config_integrity_checksum_job.dart
generated
Normal file
123
mobile/openapi/lib/model/system_config_integrity_checksum_job.dart
generated
Normal file
@@ -0,0 +1,123 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class SystemConfigIntegrityChecksumJob {
|
||||
/// Returns a new [SystemConfigIntegrityChecksumJob] instance.
|
||||
SystemConfigIntegrityChecksumJob({
|
||||
required this.cronExpression,
|
||||
required this.enabled,
|
||||
required this.percentageLimit,
|
||||
required this.timeLimit,
|
||||
});
|
||||
|
||||
String cronExpression;
|
||||
|
||||
bool enabled;
|
||||
|
||||
num percentageLimit;
|
||||
|
||||
num timeLimit;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SystemConfigIntegrityChecksumJob &&
|
||||
other.cronExpression == cronExpression &&
|
||||
other.enabled == enabled &&
|
||||
other.percentageLimit == percentageLimit &&
|
||||
other.timeLimit == timeLimit;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(cronExpression.hashCode) +
|
||||
(enabled.hashCode) +
|
||||
(percentageLimit.hashCode) +
|
||||
(timeLimit.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SystemConfigIntegrityChecksumJob[cronExpression=$cronExpression, enabled=$enabled, percentageLimit=$percentageLimit, timeLimit=$timeLimit]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'cronExpression'] = this.cronExpression;
|
||||
json[r'enabled'] = this.enabled;
|
||||
json[r'percentageLimit'] = this.percentageLimit;
|
||||
json[r'timeLimit'] = this.timeLimit;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [SystemConfigIntegrityChecksumJob] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static SystemConfigIntegrityChecksumJob? fromJson(dynamic value) {
|
||||
upgradeDto(value, "SystemConfigIntegrityChecksumJob");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return SystemConfigIntegrityChecksumJob(
|
||||
cronExpression: mapValueOfType<String>(json, r'cronExpression')!,
|
||||
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||
percentageLimit: num.parse('${json[r'percentageLimit']}'),
|
||||
timeLimit: num.parse('${json[r'timeLimit']}'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<SystemConfigIntegrityChecksumJob> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <SystemConfigIntegrityChecksumJob>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = SystemConfigIntegrityChecksumJob.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, SystemConfigIntegrityChecksumJob> mapFromJson(dynamic json) {
|
||||
final map = <String, SystemConfigIntegrityChecksumJob>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = SystemConfigIntegrityChecksumJob.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of SystemConfigIntegrityChecksumJob-objects as value to a dart map
|
||||
static Map<String, List<SystemConfigIntegrityChecksumJob>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<SystemConfigIntegrityChecksumJob>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = SystemConfigIntegrityChecksumJob.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'cronExpression',
|
||||
'enabled',
|
||||
'percentageLimit',
|
||||
'timeLimit',
|
||||
};
|
||||
}
|
||||
|
||||
107
mobile/openapi/lib/model/system_config_integrity_job.dart
generated
Normal file
107
mobile/openapi/lib/model/system_config_integrity_job.dart
generated
Normal file
@@ -0,0 +1,107 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class SystemConfigIntegrityJob {
|
||||
/// Returns a new [SystemConfigIntegrityJob] instance.
|
||||
SystemConfigIntegrityJob({
|
||||
required this.cronExpression,
|
||||
required this.enabled,
|
||||
});
|
||||
|
||||
String cronExpression;
|
||||
|
||||
bool enabled;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SystemConfigIntegrityJob &&
|
||||
other.cronExpression == cronExpression &&
|
||||
other.enabled == enabled;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(cronExpression.hashCode) +
|
||||
(enabled.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SystemConfigIntegrityJob[cronExpression=$cronExpression, enabled=$enabled]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'cronExpression'] = this.cronExpression;
|
||||
json[r'enabled'] = this.enabled;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [SystemConfigIntegrityJob] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static SystemConfigIntegrityJob? fromJson(dynamic value) {
|
||||
upgradeDto(value, "SystemConfigIntegrityJob");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return SystemConfigIntegrityJob(
|
||||
cronExpression: mapValueOfType<String>(json, r'cronExpression')!,
|
||||
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<SystemConfigIntegrityJob> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <SystemConfigIntegrityJob>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = SystemConfigIntegrityJob.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, SystemConfigIntegrityJob> mapFromJson(dynamic json) {
|
||||
final map = <String, SystemConfigIntegrityJob>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = SystemConfigIntegrityJob.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of SystemConfigIntegrityJob-objects as value to a dart map
|
||||
static Map<String, List<SystemConfigIntegrityJob>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<SystemConfigIntegrityJob>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = SystemConfigIntegrityJob.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'cronExpression',
|
||||
'enabled',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -372,6 +372,275 @@
|
||||
"x-immich-state": "Alpha"
|
||||
}
|
||||
},
|
||||
"/admin/maintenance/integrity/report": {
|
||||
"post": {
|
||||
"description": "...",
|
||||
"operationId": "getIntegrityReport",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MaintenanceGetIntegrityReportDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MaintenanceIntegrityReportResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Get integrity report by type",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"/admin/maintenance/integrity/report/{id}": {
|
||||
"delete": {
|
||||
"description": "...",
|
||||
"operationId": "deleteIntegrityReport",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Delete report entry and perform corresponding deletion action",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"/admin/maintenance/integrity/report/{id}/file": {
|
||||
"get": {
|
||||
"description": "...",
|
||||
"operationId": "getIntegrityReportFile",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/octet-stream": {
|
||||
"schema": {
|
||||
"format": "binary",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Download the orphan/broken file if one exists",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"/admin/maintenance/integrity/report/{type}/csv": {
|
||||
"get": {
|
||||
"description": "...",
|
||||
"operationId": "getIntegrityReportCsv",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "type",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/IntegrityReportType"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/octet-stream": {
|
||||
"schema": {
|
||||
"format": "binary",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Export integrity report by type as CSV",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"/admin/maintenance/integrity/summary": {
|
||||
"get": {
|
||||
"description": "...",
|
||||
"operationId": "getIntegrityReportSummary",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MaintenanceIntegrityReportSummaryResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Get integrity report summary",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"/admin/maintenance/login": {
|
||||
"post": {
|
||||
"description": "Login with maintenance token or cookie to receive current information and perform further actions.",
|
||||
@@ -16589,6 +16858,14 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"IntegrityReportType": {
|
||||
"enum": [
|
||||
"orphan_file",
|
||||
"missing_file",
|
||||
"checksum_mismatch"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"JobCreateDto": {
|
||||
"properties": {
|
||||
"name": {
|
||||
@@ -16660,7 +16937,16 @@
|
||||
"VersionCheck",
|
||||
"OcrQueueAll",
|
||||
"Ocr",
|
||||
"WorkflowRun"
|
||||
"WorkflowRun",
|
||||
"IntegrityOrphanedFilesQueueAll",
|
||||
"IntegrityOrphanedFiles",
|
||||
"IntegrityOrphanedRefresh",
|
||||
"IntegrityMissingFilesQueueAll",
|
||||
"IntegrityMissingFiles",
|
||||
"IntegrityMissingFilesRefresh",
|
||||
"IntegrityChecksumFiles",
|
||||
"IntegrityChecksumFilesRefresh",
|
||||
"IntegrityReportDelete"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
@@ -16914,6 +17200,77 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"MaintenanceGetIntegrityReportDto": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrityReportType"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"MaintenanceIntegrityReportDto": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrityReportType"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"MaintenanceIntegrityReportResponseDto": {
|
||||
"properties": {
|
||||
"items": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MaintenanceIntegrityReportDto"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"items"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"MaintenanceIntegrityReportSummaryResponseDto": {
|
||||
"properties": {
|
||||
"checksum_mismatch": {
|
||||
"type": "integer"
|
||||
},
|
||||
"missing_file": {
|
||||
"type": "integer"
|
||||
},
|
||||
"orphan_file": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"checksum_mismatch",
|
||||
"missing_file",
|
||||
"orphan_file"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"MaintenanceLoginDto": {
|
||||
"properties": {
|
||||
"token": {
|
||||
@@ -16929,7 +17286,16 @@
|
||||
"user-cleanup",
|
||||
"memory-cleanup",
|
||||
"memory-create",
|
||||
"backup-database"
|
||||
"backup-database",
|
||||
"integrity-missing-files",
|
||||
"integrity-orphan-files",
|
||||
"integrity-checksum-mismatch",
|
||||
"integrity-missing-files-refresh",
|
||||
"integrity-orphan-files-refresh",
|
||||
"integrity-checksum-mismatch-refresh",
|
||||
"integrity-missing-files-delete-all",
|
||||
"integrity-orphan-files-delete-all",
|
||||
"integrity-checksum-mismatch-delete-all"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
@@ -21231,6 +21597,9 @@
|
||||
"image": {
|
||||
"$ref": "#/components/schemas/SystemConfigImageDto"
|
||||
},
|
||||
"integrityChecks": {
|
||||
"$ref": "#/components/schemas/SystemConfigIntegrityChecks"
|
||||
},
|
||||
"job": {
|
||||
"$ref": "#/components/schemas/SystemConfigJobDto"
|
||||
},
|
||||
@@ -21290,6 +21659,7 @@
|
||||
"backup",
|
||||
"ffmpeg",
|
||||
"image",
|
||||
"integrityChecks",
|
||||
"job",
|
||||
"library",
|
||||
"logging",
|
||||
@@ -21536,6 +21906,63 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SystemConfigIntegrityChecks": {
|
||||
"properties": {
|
||||
"checksumFiles": {
|
||||
"$ref": "#/components/schemas/SystemConfigIntegrityChecksumJob"
|
||||
},
|
||||
"missingFiles": {
|
||||
"$ref": "#/components/schemas/SystemConfigIntegrityJob"
|
||||
},
|
||||
"orphanedFiles": {
|
||||
"$ref": "#/components/schemas/SystemConfigIntegrityJob"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"checksumFiles",
|
||||
"missingFiles",
|
||||
"orphanedFiles"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SystemConfigIntegrityChecksumJob": {
|
||||
"properties": {
|
||||
"cronExpression": {
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"percentageLimit": {
|
||||
"type": "number"
|
||||
},
|
||||
"timeLimit": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"cronExpression",
|
||||
"enabled",
|
||||
"percentageLimit",
|
||||
"timeLimit"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SystemConfigIntegrityJob": {
|
||||
"properties": {
|
||||
"cronExpression": {
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"cronExpression",
|
||||
"enabled"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SystemConfigJobDto": {
|
||||
"properties": {
|
||||
"backgroundTask": {
|
||||
|
||||
@@ -43,6 +43,22 @@ export type ActivityStatisticsResponseDto = {
|
||||
export type SetMaintenanceModeDto = {
|
||||
action: MaintenanceAction;
|
||||
};
|
||||
export type MaintenanceGetIntegrityReportDto = {
|
||||
"type": IntegrityReportType;
|
||||
};
|
||||
export type MaintenanceIntegrityReportDto = {
|
||||
id: string;
|
||||
path: string;
|
||||
"type": IntegrityReportType;
|
||||
};
|
||||
export type MaintenanceIntegrityReportResponseDto = {
|
||||
items: MaintenanceIntegrityReportDto[];
|
||||
};
|
||||
export type MaintenanceIntegrityReportSummaryResponseDto = {
|
||||
checksum_mismatch: number;
|
||||
missing_file: number;
|
||||
orphan_file: number;
|
||||
};
|
||||
export type MaintenanceLoginDto = {
|
||||
token?: string;
|
||||
};
|
||||
@@ -1454,6 +1470,21 @@ export type SystemConfigImageDto = {
|
||||
preview: SystemConfigGeneratedImageDto;
|
||||
thumbnail: SystemConfigGeneratedImageDto;
|
||||
};
|
||||
export type SystemConfigIntegrityChecksumJob = {
|
||||
cronExpression: string;
|
||||
enabled: boolean;
|
||||
percentageLimit: number;
|
||||
timeLimit: number;
|
||||
};
|
||||
export type SystemConfigIntegrityJob = {
|
||||
cronExpression: string;
|
||||
enabled: boolean;
|
||||
};
|
||||
export type SystemConfigIntegrityChecks = {
|
||||
checksumFiles: SystemConfigIntegrityChecksumJob;
|
||||
missingFiles: SystemConfigIntegrityJob;
|
||||
orphanedFiles: SystemConfigIntegrityJob;
|
||||
};
|
||||
export type JobSettingsDto = {
|
||||
concurrency: number;
|
||||
};
|
||||
@@ -1606,6 +1637,7 @@ export type SystemConfigDto = {
|
||||
backup: SystemConfigBackupsDto;
|
||||
ffmpeg: SystemConfigFFmpegDto;
|
||||
image: SystemConfigImageDto;
|
||||
integrityChecks: SystemConfigIntegrityChecks;
|
||||
job: SystemConfigJobDto;
|
||||
library: SystemConfigLibraryDto;
|
||||
logging: SystemConfigLoggingDto;
|
||||
@@ -1862,6 +1894,69 @@ export function setMaintenanceMode({ setMaintenanceModeDto }: {
|
||||
body: setMaintenanceModeDto
|
||||
})));
|
||||
}
|
||||
/**
|
||||
* Get integrity report by type
|
||||
*/
|
||||
export function getIntegrityReport({ maintenanceGetIntegrityReportDto }: {
|
||||
maintenanceGetIntegrityReportDto: MaintenanceGetIntegrityReportDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 201;
|
||||
data: MaintenanceIntegrityReportResponseDto;
|
||||
}>("/admin/maintenance/integrity/report", oazapfts.json({
|
||||
...opts,
|
||||
method: "POST",
|
||||
body: maintenanceGetIntegrityReportDto
|
||||
})));
|
||||
}
|
||||
/**
|
||||
* Delete report entry and perform corresponding deletion action
|
||||
*/
|
||||
export function deleteIntegrityReport({ id }: {
|
||||
id: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText(`/admin/maintenance/integrity/report/${encodeURIComponent(id)}`, {
|
||||
...opts,
|
||||
method: "DELETE"
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Download the orphan/broken file if one exists
|
||||
*/
|
||||
export function getIntegrityReportFile({ id }: {
|
||||
id: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchBlob<{
|
||||
status: 200;
|
||||
data: Blob;
|
||||
}>(`/admin/maintenance/integrity/report/${encodeURIComponent(id)}/file`, {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Export integrity report by type as CSV
|
||||
*/
|
||||
export function getIntegrityReportCsv({ $type }: {
|
||||
$type: IntegrityReportType;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchBlob<{
|
||||
status: 200;
|
||||
data: Blob;
|
||||
}>(`/admin/maintenance/integrity/report/${encodeURIComponent($type)}/csv`, {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Get integrity report summary
|
||||
*/
|
||||
export function getIntegrityReportSummary(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: MaintenanceIntegrityReportSummaryResponseDto;
|
||||
}>("/admin/maintenance/integrity/summary", {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Log into maintenance mode
|
||||
*/
|
||||
@@ -5142,6 +5237,11 @@ export enum MaintenanceAction {
|
||||
Start = "start",
|
||||
End = "end"
|
||||
}
|
||||
export enum IntegrityReportType {
|
||||
OrphanFile = "orphan_file",
|
||||
MissingFile = "missing_file",
|
||||
ChecksumMismatch = "checksum_mismatch"
|
||||
}
|
||||
export enum NotificationLevel {
|
||||
Success = "success",
|
||||
Error = "error",
|
||||
@@ -5378,7 +5478,16 @@ export enum ManualJobName {
|
||||
UserCleanup = "user-cleanup",
|
||||
MemoryCleanup = "memory-cleanup",
|
||||
MemoryCreate = "memory-create",
|
||||
BackupDatabase = "backup-database"
|
||||
BackupDatabase = "backup-database",
|
||||
IntegrityMissingFiles = "integrity-missing-files",
|
||||
IntegrityOrphanFiles = "integrity-orphan-files",
|
||||
IntegrityChecksumMismatch = "integrity-checksum-mismatch",
|
||||
IntegrityMissingFilesRefresh = "integrity-missing-files-refresh",
|
||||
IntegrityOrphanFilesRefresh = "integrity-orphan-files-refresh",
|
||||
IntegrityChecksumMismatchRefresh = "integrity-checksum-mismatch-refresh",
|
||||
IntegrityMissingFilesDeleteAll = "integrity-missing-files-delete-all",
|
||||
IntegrityOrphanFilesDeleteAll = "integrity-orphan-files-delete-all",
|
||||
IntegrityChecksumMismatchDeleteAll = "integrity-checksum-mismatch-delete-all"
|
||||
}
|
||||
export enum QueueName {
|
||||
ThumbnailGeneration = "thumbnailGeneration",
|
||||
@@ -5486,7 +5595,16 @@ export enum JobName {
|
||||
VersionCheck = "VersionCheck",
|
||||
OcrQueueAll = "OcrQueueAll",
|
||||
Ocr = "Ocr",
|
||||
WorkflowRun = "WorkflowRun"
|
||||
WorkflowRun = "WorkflowRun",
|
||||
IntegrityOrphanedFilesQueueAll = "IntegrityOrphanedFilesQueueAll",
|
||||
IntegrityOrphanedFiles = "IntegrityOrphanedFiles",
|
||||
IntegrityOrphanedRefresh = "IntegrityOrphanedRefresh",
|
||||
IntegrityMissingFilesQueueAll = "IntegrityMissingFilesQueueAll",
|
||||
IntegrityMissingFiles = "IntegrityMissingFiles",
|
||||
IntegrityMissingFilesRefresh = "IntegrityMissingFilesRefresh",
|
||||
IntegrityChecksumFiles = "IntegrityChecksumFiles",
|
||||
IntegrityChecksumFilesRefresh = "IntegrityChecksumFilesRefresh",
|
||||
IntegrityReportDelete = "IntegrityReportDelete"
|
||||
}
|
||||
export enum SearchSuggestionType {
|
||||
Country = "country",
|
||||
|
||||
@@ -46,6 +46,22 @@ export interface SystemConfig {
|
||||
accelDecode: boolean;
|
||||
tonemap: ToneMapping;
|
||||
};
|
||||
integrityChecks: {
|
||||
missingFiles: {
|
||||
enabled: boolean;
|
||||
cronExpression: string;
|
||||
};
|
||||
orphanedFiles: {
|
||||
enabled: boolean;
|
||||
cronExpression: string;
|
||||
};
|
||||
checksumFiles: {
|
||||
enabled: boolean;
|
||||
cronExpression: string;
|
||||
timeLimit: number;
|
||||
percentageLimit: number;
|
||||
};
|
||||
};
|
||||
job: Record<ConcurrentQueueName, { concurrency: number }>;
|
||||
logging: {
|
||||
enabled: boolean;
|
||||
@@ -222,6 +238,22 @@ export const defaults = Object.freeze<SystemConfig>({
|
||||
accel: TranscodeHardwareAcceleration.Disabled,
|
||||
accelDecode: false,
|
||||
},
|
||||
integrityChecks: {
|
||||
missingFiles: {
|
||||
enabled: true,
|
||||
cronExpression: CronExpression.EVERY_DAY_AT_3AM,
|
||||
},
|
||||
orphanedFiles: {
|
||||
enabled: true,
|
||||
cronExpression: CronExpression.EVERY_DAY_AT_3AM,
|
||||
},
|
||||
checksumFiles: {
|
||||
enabled: true,
|
||||
cronExpression: CronExpression.EVERY_DAY_AT_3AM,
|
||||
timeLimit: 60 * 60 * 1000, // 1 hour
|
||||
percentageLimit: 1, // 100% of assets
|
||||
},
|
||||
},
|
||||
job: {
|
||||
[QueueName.BackgroundTask]: { concurrency: 5 },
|
||||
[QueueName.SmartSearch]: { concurrency: 2 },
|
||||
|
||||
@@ -1,19 +1,32 @@
|
||||
import { BadRequestException, Body, Controller, Post, Res } from '@nestjs/common';
|
||||
import { BadRequestException, Body, Controller, Delete, Get, Next, Param, Post, Res } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { Response } from 'express';
|
||||
import { NextFunction, Response } from 'express';
|
||||
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { MaintenanceAuthDto, MaintenanceLoginDto, SetMaintenanceModeDto } from 'src/dtos/maintenance.dto';
|
||||
import {
|
||||
MaintenanceAuthDto,
|
||||
MaintenanceGetIntegrityReportDto,
|
||||
MaintenanceIntegrityReportResponseDto,
|
||||
MaintenanceIntegrityReportSummaryResponseDto,
|
||||
MaintenanceLoginDto,
|
||||
SetMaintenanceModeDto,
|
||||
} from 'src/dtos/maintenance.dto';
|
||||
import { ApiTag, ImmichCookie, MaintenanceAction, Permission } from 'src/enum';
|
||||
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
|
||||
import { Auth, Authenticated, FileResponse, GetLoginDetails } from 'src/middleware/auth.guard';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { LoginDetails } from 'src/services/auth.service';
|
||||
import { MaintenanceService } from 'src/services/maintenance.service';
|
||||
import { sendFile } from 'src/utils/file';
|
||||
import { respondWithCookie } from 'src/utils/response';
|
||||
import { IntegrityReportTypeParamDto, UUIDParamDto } from 'src/validation';
|
||||
|
||||
@ApiTags(ApiTag.Maintenance)
|
||||
@Controller('admin/maintenance')
|
||||
export class MaintenanceController {
|
||||
constructor(private service: MaintenanceService) {}
|
||||
constructor(
|
||||
private logger: LoggingRepository,
|
||||
private service: MaintenanceService,
|
||||
) {}
|
||||
|
||||
@Post('login')
|
||||
@Endpoint({
|
||||
@@ -46,4 +59,69 @@ export class MaintenanceController {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Get('integrity/summary')
|
||||
@Endpoint({
|
||||
summary: 'Get integrity report summary',
|
||||
description: '...',
|
||||
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
|
||||
})
|
||||
@Authenticated({ permission: Permission.Maintenance, admin: true })
|
||||
getIntegrityReportSummary(): Promise<MaintenanceIntegrityReportSummaryResponseDto> {
|
||||
return this.service.getIntegrityReportSummary();
|
||||
}
|
||||
|
||||
@Post('integrity/report')
|
||||
@Endpoint({
|
||||
summary: 'Get integrity report by type',
|
||||
description: '...',
|
||||
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
|
||||
})
|
||||
@Authenticated({ permission: Permission.Maintenance, admin: true })
|
||||
getIntegrityReport(@Body() dto: MaintenanceGetIntegrityReportDto): Promise<MaintenanceIntegrityReportResponseDto> {
|
||||
return this.service.getIntegrityReport(dto);
|
||||
}
|
||||
|
||||
@Delete('integrity/report/:id')
|
||||
@Endpoint({
|
||||
summary: 'Delete report entry and perform corresponding deletion action',
|
||||
description: '...',
|
||||
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
|
||||
})
|
||||
@Authenticated({ permission: Permission.Maintenance, admin: true })
|
||||
async deleteIntegrityReport(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
await this.service.deleteIntegrityReport(auth, id);
|
||||
}
|
||||
|
||||
@Get('integrity/report/:type/csv')
|
||||
@Endpoint({
|
||||
summary: 'Export integrity report by type as CSV',
|
||||
description: '...',
|
||||
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
|
||||
})
|
||||
@FileResponse()
|
||||
@Authenticated({ permission: Permission.Maintenance, admin: true })
|
||||
getIntegrityReportCsv(@Param() { type }: IntegrityReportTypeParamDto, @Res() res: Response): void {
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
res.setHeader('Cache-Control', 'private, no-cache, no-transform');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(`${Date.now()}-${type}.csv`)}"`);
|
||||
|
||||
this.service.getIntegrityReportCsv(type).pipe(res);
|
||||
}
|
||||
|
||||
@Get('integrity/report/:id/file')
|
||||
@Endpoint({
|
||||
summary: 'Download the orphan/broken file if one exists',
|
||||
description: '...',
|
||||
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
|
||||
})
|
||||
@FileResponse()
|
||||
@Authenticated({ permission: Permission.Maintenance, admin: true })
|
||||
async getIntegrityReportFile(
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Res() res: Response,
|
||||
@Next() next: NextFunction,
|
||||
): Promise<void> {
|
||||
await sendFile(res, next, () => this.service.getIntegrityReportFile(id), this.logger);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { MaintenanceAction } from 'src/enum';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IntegrityReportType, MaintenanceAction } from 'src/enum';
|
||||
import { ValidateEnum, ValidateString } from 'src/validation';
|
||||
|
||||
export class SetMaintenanceModeDto {
|
||||
@@ -14,3 +15,40 @@ export class MaintenanceLoginDto {
|
||||
export class MaintenanceAuthDto {
|
||||
username!: string;
|
||||
}
|
||||
|
||||
export class MaintenanceIntegrityReportSummaryResponseDto {
|
||||
@ApiProperty({ type: 'integer' })
|
||||
[IntegrityReportType.ChecksumFail]!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
[IntegrityReportType.MissingFile]!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
[IntegrityReportType.OrphanFile]!: number;
|
||||
}
|
||||
|
||||
export class MaintenanceGetIntegrityReportDto {
|
||||
@ValidateEnum({ enum: IntegrityReportType, name: 'IntegrityReportType' })
|
||||
type!: IntegrityReportType;
|
||||
|
||||
// todo: paginate
|
||||
// @IsInt()
|
||||
// @Min(1)
|
||||
// @Type(() => Number)
|
||||
// @Optional()
|
||||
// page?: number;
|
||||
}
|
||||
|
||||
export class MaintenanceDeleteIntegrityReportDto {
|
||||
@ValidateEnum({ enum: IntegrityReportType, name: 'IntegrityReportType' })
|
||||
type!: IntegrityReportType;
|
||||
}
|
||||
|
||||
class MaintenanceIntegrityReportDto {
|
||||
id!: string;
|
||||
@ValidateEnum({ enum: IntegrityReportType, name: 'IntegrityReportType' })
|
||||
type!: IntegrityReportType;
|
||||
path!: string;
|
||||
}
|
||||
|
||||
export class MaintenanceIntegrityReportResponseDto {
|
||||
items!: MaintenanceIntegrityReportDto[];
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ const isOAuthEnabled = (config: SystemConfigOAuthDto) => config.enabled;
|
||||
const isOAuthOverrideEnabled = (config: SystemConfigOAuthDto) => config.mobileOverrideEnabled;
|
||||
const isEmailNotificationEnabled = (config: SystemConfigSmtpDto) => config.enabled;
|
||||
const isDatabaseBackupEnabled = (config: DatabaseBackupConfig) => config.enabled;
|
||||
const isEnabledProperty = (config: { enabled: boolean }) => config.enabled;
|
||||
|
||||
export class DatabaseBackupConfig {
|
||||
@ValidateBoolean()
|
||||
@@ -145,6 +146,42 @@ export class SystemConfigFFmpegDto {
|
||||
tonemap!: ToneMapping;
|
||||
}
|
||||
|
||||
class SystemConfigIntegrityJob {
|
||||
@ValidateBoolean()
|
||||
enabled!: boolean;
|
||||
|
||||
@ValidateIf(isEnabledProperty)
|
||||
@IsNotEmpty()
|
||||
@IsCronExpression()
|
||||
@IsString()
|
||||
cronExpression!: string;
|
||||
}
|
||||
|
||||
class SystemConfigIntegrityChecksumJob extends SystemConfigIntegrityJob {
|
||||
@IsInt()
|
||||
timeLimit!: number;
|
||||
|
||||
@IsNumber()
|
||||
percentageLimit!: number;
|
||||
}
|
||||
|
||||
class SystemConfigIntegrityChecks {
|
||||
@Type(() => SystemConfigIntegrityJob)
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
missingFiles!: SystemConfigIntegrityJob;
|
||||
|
||||
@Type(() => SystemConfigIntegrityJob)
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
orphanedFiles!: SystemConfigIntegrityJob;
|
||||
|
||||
@Type(() => SystemConfigIntegrityChecksumJob)
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
checksumFiles!: SystemConfigIntegrityChecksumJob;
|
||||
}
|
||||
|
||||
class JobSettingsDto {
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
@@ -649,6 +686,11 @@ export class SystemConfigDto implements SystemConfig {
|
||||
@IsObject()
|
||||
ffmpeg!: SystemConfigFFmpegDto;
|
||||
|
||||
@Type(() => SystemConfigIntegrityChecks)
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
integrityChecks!: SystemConfigIntegrityChecks;
|
||||
|
||||
@Type(() => SystemConfigLoggingDto)
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
|
||||
@@ -301,6 +301,7 @@ export enum SystemMetadataKey {
|
||||
SystemFlags = 'system-flags',
|
||||
VersionCheckState = 'version-check-state',
|
||||
License = 'license',
|
||||
IntegrityChecksumCheckpoint = 'integrity-checksum-checkpoint',
|
||||
}
|
||||
|
||||
export enum UserMetadataKey {
|
||||
@@ -344,6 +345,12 @@ export enum SourceType {
|
||||
Manual = 'manual',
|
||||
}
|
||||
|
||||
export enum IntegrityReportType {
|
||||
OrphanFile = 'orphan_file',
|
||||
MissingFile = 'missing_file',
|
||||
ChecksumFail = 'checksum_mismatch',
|
||||
}
|
||||
|
||||
export enum ManualJobName {
|
||||
PersonCleanup = 'person-cleanup',
|
||||
TagCleanup = 'tag-cleanup',
|
||||
@@ -351,6 +358,15 @@ export enum ManualJobName {
|
||||
MemoryCleanup = 'memory-cleanup',
|
||||
MemoryCreate = 'memory-create',
|
||||
BackupDatabase = 'backup-database',
|
||||
IntegrityMissingFiles = `integrity-missing-files`,
|
||||
IntegrityOrphanFiles = `integrity-orphan-files`,
|
||||
IntegrityChecksumFiles = `integrity-checksum-mismatch`,
|
||||
IntegrityMissingFilesRefresh = `integrity-missing-files-refresh`,
|
||||
IntegrityOrphanFilesRefresh = `integrity-orphan-files-refresh`,
|
||||
IntegrityChecksumFilesRefresh = `integrity-checksum-mismatch-refresh`,
|
||||
IntegrityMissingFilesDeleteAll = `integrity-missing-files-delete-all`,
|
||||
IntegrityOrphanFilesDeleteAll = `integrity-orphan-files-delete-all`,
|
||||
IntegrityChecksumFilesDeleteAll = `integrity-checksum-mismatch-delete-all`,
|
||||
}
|
||||
|
||||
export enum AssetPathType {
|
||||
@@ -637,6 +653,17 @@ export enum JobName {
|
||||
|
||||
// Workflow
|
||||
WorkflowRun = 'WorkflowRun',
|
||||
|
||||
// Integrity
|
||||
IntegrityOrphanedFilesQueueAll = 'IntegrityOrphanedFilesQueueAll',
|
||||
IntegrityOrphanedFiles = 'IntegrityOrphanedFiles',
|
||||
IntegrityOrphanedFilesRefresh = 'IntegrityOrphanedRefresh',
|
||||
IntegrityMissingFilesQueueAll = 'IntegrityMissingFilesQueueAll',
|
||||
IntegrityMissingFiles = 'IntegrityMissingFiles',
|
||||
IntegrityMissingFilesRefresh = 'IntegrityMissingFilesRefresh',
|
||||
IntegrityChecksumFiles = 'IntegrityChecksumFiles',
|
||||
IntegrityChecksumFilesRefresh = 'IntegrityChecksumFilesRefresh',
|
||||
IntegrityReportDelete = 'IntegrityReportDelete',
|
||||
}
|
||||
|
||||
export enum QueueCommand {
|
||||
@@ -679,6 +706,7 @@ export enum DatabaseLock {
|
||||
GetSystemConfig = 69,
|
||||
BackupDatabase = 42,
|
||||
MemoryCreation = 777,
|
||||
IntegrityCheck = 67,
|
||||
}
|
||||
|
||||
export enum MaintenanceAction {
|
||||
|
||||
179
server/src/queries/integrity.repository.sql
Normal file
179
server/src/queries/integrity.repository.sql
Normal file
@@ -0,0 +1,179 @@
|
||||
-- NOTE: This file is auto generated by ./sql-generator
|
||||
|
||||
-- IntegrityRepository.getById
|
||||
select
|
||||
"integrity_report".*
|
||||
from
|
||||
"integrity_report"
|
||||
where
|
||||
"id" = $1
|
||||
|
||||
-- IntegrityRepository.getIntegrityReportSummary
|
||||
select
|
||||
count(*) filter (
|
||||
where
|
||||
"type" = $1
|
||||
) as "checksum_mismatch",
|
||||
count(*) filter (
|
||||
where
|
||||
"type" = $2
|
||||
) as "missing_file",
|
||||
count(*) filter (
|
||||
where
|
||||
"type" = $3
|
||||
) as "orphan_file"
|
||||
from
|
||||
"integrity_report"
|
||||
|
||||
-- IntegrityRepository.getIntegrityReports
|
||||
select
|
||||
"id",
|
||||
"type",
|
||||
"path",
|
||||
"assetId",
|
||||
"fileAssetId"
|
||||
from
|
||||
"integrity_report"
|
||||
where
|
||||
"type" = $1
|
||||
order by
|
||||
"createdAt" desc
|
||||
|
||||
-- IntegrityRepository.getAssetPathsByPaths
|
||||
select
|
||||
"originalPath",
|
||||
"encodedVideoPath"
|
||||
from
|
||||
"asset"
|
||||
where
|
||||
(
|
||||
"originalPath" in $1
|
||||
or "encodedVideoPath" in $2
|
||||
)
|
||||
|
||||
-- IntegrityRepository.getAssetFilePathsByPaths
|
||||
select
|
||||
"path"
|
||||
from
|
||||
"asset_file"
|
||||
where
|
||||
"path" in $1
|
||||
|
||||
-- IntegrityRepository.getAssetCount
|
||||
select
|
||||
count(*) as "count"
|
||||
from
|
||||
"asset"
|
||||
|
||||
-- IntegrityRepository.streamAllAssetPaths
|
||||
select
|
||||
"id",
|
||||
"type",
|
||||
"path",
|
||||
"assetId",
|
||||
"fileAssetId"
|
||||
from
|
||||
"integrity_report"
|
||||
where
|
||||
"type" = $1
|
||||
order by
|
||||
"createdAt" desc
|
||||
select
|
||||
"originalPath",
|
||||
"encodedVideoPath"
|
||||
from
|
||||
"asset"
|
||||
|
||||
-- IntegrityRepository.streamAllAssetFilePaths
|
||||
select
|
||||
"path"
|
||||
from
|
||||
"asset_file"
|
||||
|
||||
-- IntegrityRepository.streamAssetPaths
|
||||
select
|
||||
"allPaths"."path" as "path",
|
||||
"allPaths"."assetId",
|
||||
"allPaths"."fileAssetId",
|
||||
"integrity_report"."id" as "reportId"
|
||||
from
|
||||
(
|
||||
select
|
||||
"asset"."originalPath" as "path",
|
||||
"asset"."id" as "assetId",
|
||||
null::uuid as "fileAssetId"
|
||||
from
|
||||
"asset"
|
||||
where
|
||||
"asset"."deletedAt" is null
|
||||
union all
|
||||
select
|
||||
"asset"."encodedVideoPath" as "path",
|
||||
"asset"."id" as "assetId",
|
||||
null::uuid as "fileAssetId"
|
||||
from
|
||||
"asset"
|
||||
where
|
||||
"asset"."deletedAt" is null
|
||||
and "asset"."encodedVideoPath" is not null
|
||||
and "asset"."encodedVideoPath" != ''
|
||||
union all
|
||||
select
|
||||
"path",
|
||||
null::uuid as "assetId",
|
||||
"asset_file"."id" as "fileAssetId"
|
||||
from
|
||||
"asset_file"
|
||||
) as "allPaths"
|
||||
left join "integrity_report" on "integrity_report"."type" = $1
|
||||
and (
|
||||
"integrity_report"."assetId" = "allPaths"."assetId"
|
||||
or "integrity_report"."fileAssetId" = "allPaths"."fileAssetId"
|
||||
)
|
||||
|
||||
-- IntegrityRepository.streamAssetChecksums
|
||||
select
|
||||
"asset"."originalPath",
|
||||
"asset"."checksum",
|
||||
"asset"."createdAt",
|
||||
"asset"."id" as "assetId",
|
||||
"integrity_report"."id" as "reportId"
|
||||
from
|
||||
"asset"
|
||||
left join "integrity_report" on "integrity_report"."assetId" = "asset"."id"
|
||||
and "integrity_report"."type" = $1
|
||||
where
|
||||
"createdAt" >= $2
|
||||
and "createdAt" <= $3
|
||||
order by
|
||||
"createdAt" asc
|
||||
|
||||
-- IntegrityRepository.streamIntegrityReports
|
||||
select
|
||||
"integrity_report"."id" as "reportId",
|
||||
"integrity_report"."path"
|
||||
from
|
||||
"integrity_report"
|
||||
where
|
||||
"integrity_report"."type" = $1
|
||||
|
||||
-- IntegrityRepository.streamIntegrityReportsByProperty
|
||||
select
|
||||
"id",
|
||||
"path",
|
||||
"assetId",
|
||||
"fileAssetId"
|
||||
from
|
||||
"integrity_report"
|
||||
where
|
||||
"abcdefghi" is not null
|
||||
|
||||
-- IntegrityRepository.deleteById
|
||||
delete from "integrity_report"
|
||||
where
|
||||
"id" = $1
|
||||
|
||||
-- IntegrityRepository.deleteByIds
|
||||
delete from "integrity_report"
|
||||
where
|
||||
"id" in $1
|
||||
@@ -15,6 +15,7 @@ import { DownloadRepository } from 'src/repositories/download.repository';
|
||||
import { DuplicateRepository } from 'src/repositories/duplicate.repository';
|
||||
import { EmailRepository } from 'src/repositories/email.repository';
|
||||
import { EventRepository } from 'src/repositories/event.repository';
|
||||
import { IntegrityRepository } from 'src/repositories/integrity.repository';
|
||||
import { JobRepository } from 'src/repositories/job.repository';
|
||||
import { LibraryRepository } from 'src/repositories/library.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
@@ -68,6 +69,7 @@ export const repositories = [
|
||||
DuplicateRepository,
|
||||
EmailRepository,
|
||||
EventRepository,
|
||||
IntegrityRepository,
|
||||
JobRepository,
|
||||
LibraryRepository,
|
||||
LoggingRepository,
|
||||
|
||||
234
server/src/repositories/integrity.repository.ts
Normal file
234
server/src/repositories/integrity.repository.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Insertable, Kysely, sql } from 'kysely';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { Readable } from 'node:stream';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { IntegrityReportType } from 'src/enum';
|
||||
import { DB } from 'src/schema';
|
||||
import { IntegrityReportTable } from 'src/schema/tables/integrity-report.table';
|
||||
|
||||
@Injectable()
|
||||
export class IntegrityRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
create(dto: Insertable<IntegrityReportTable> | Insertable<IntegrityReportTable>[]) {
|
||||
return this.db
|
||||
.insertInto('integrity_report')
|
||||
.values(dto)
|
||||
.onConflict((oc) =>
|
||||
oc.columns(['path', 'type']).doUpdateSet({
|
||||
assetId: (eb) => eb.ref('excluded.assetId'),
|
||||
fileAssetId: (eb) => eb.ref('excluded.fileAssetId'),
|
||||
}),
|
||||
)
|
||||
.returningAll()
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.STRING] })
|
||||
getById(id: string) {
|
||||
return this.db
|
||||
.selectFrom('integrity_report')
|
||||
.selectAll('integrity_report')
|
||||
.where('id', '=', id)
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [] })
|
||||
getIntegrityReportSummary() {
|
||||
return this.db
|
||||
.selectFrom('integrity_report')
|
||||
.select((eb) =>
|
||||
eb.fn
|
||||
.countAll<number>()
|
||||
.filterWhere('type', '=', IntegrityReportType.ChecksumFail)
|
||||
.as(IntegrityReportType.ChecksumFail),
|
||||
)
|
||||
.select((eb) =>
|
||||
eb.fn
|
||||
.countAll<number>()
|
||||
.filterWhere('type', '=', IntegrityReportType.MissingFile)
|
||||
.as(IntegrityReportType.MissingFile),
|
||||
)
|
||||
.select((eb) =>
|
||||
eb.fn
|
||||
.countAll<number>()
|
||||
.filterWhere('type', '=', IntegrityReportType.OrphanFile)
|
||||
.as(IntegrityReportType.OrphanFile),
|
||||
)
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.STRING] })
|
||||
getIntegrityReports(type: IntegrityReportType) {
|
||||
return this.db
|
||||
.selectFrom('integrity_report')
|
||||
.select(['id', 'type', 'path', 'assetId', 'fileAssetId'])
|
||||
.where('type', '=', type)
|
||||
.orderBy('createdAt', 'desc')
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.STRING] })
|
||||
getAssetPathsByPaths(paths: string[]) {
|
||||
return this.db
|
||||
.selectFrom('asset')
|
||||
.select(['originalPath', 'encodedVideoPath'])
|
||||
.where((eb) => eb.or([eb('originalPath', 'in', paths), eb('encodedVideoPath', 'in', paths)]))
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.STRING] })
|
||||
getAssetFilePathsByPaths(paths: string[]) {
|
||||
return this.db.selectFrom('asset_file').select(['path']).where('path', 'in', paths).execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [] })
|
||||
getAssetCount() {
|
||||
return this.db
|
||||
.selectFrom('asset')
|
||||
.select((eb) => eb.fn.countAll<number>().as('count'))
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.STRING], stream: true })
|
||||
streamIntegrityReportsCSV(type: IntegrityReportType): Readable {
|
||||
const items = this.db
|
||||
.selectFrom('integrity_report')
|
||||
.select(['id', 'type', 'path', 'assetId', 'fileAssetId'])
|
||||
.where('type', '=', type)
|
||||
.orderBy('createdAt', 'desc')
|
||||
.stream();
|
||||
|
||||
// very rudimentary csv serialiser
|
||||
async function* generator() {
|
||||
yield 'id,type,assetId,fileAssetId,path\n';
|
||||
|
||||
for await (const item of items) {
|
||||
// no expectation of particularly bad filenames
|
||||
// but they could potentially have a newline or quote character
|
||||
yield `${item.id},${item.type},${item.assetId},${item.fileAssetId},"${item.path.replaceAll('"', '""')}"\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return Readable.from(generator());
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [], stream: true })
|
||||
streamAllAssetPaths() {
|
||||
return this.db.selectFrom('asset').select(['originalPath', 'encodedVideoPath']).stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [], stream: true })
|
||||
streamAllAssetFilePaths() {
|
||||
return this.db.selectFrom('asset_file').select(['path']).stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [], stream: true })
|
||||
streamAssetPaths() {
|
||||
return this.db
|
||||
.selectFrom((eb) =>
|
||||
eb
|
||||
.selectFrom('asset')
|
||||
.where('asset.deletedAt', 'is', null)
|
||||
.select(['asset.originalPath as path'])
|
||||
.select((eb) => [
|
||||
eb.ref('asset.id').$castTo<string | null>().as('assetId'),
|
||||
sql<string | null>`null::uuid`.as('fileAssetId'),
|
||||
])
|
||||
.unionAll(
|
||||
eb
|
||||
.selectFrom('asset')
|
||||
.where('asset.deletedAt', 'is', null)
|
||||
.select((eb) => [
|
||||
eb.ref('asset.encodedVideoPath').$castTo<string>().as('path'),
|
||||
eb.ref('asset.id').$castTo<string | null>().as('assetId'),
|
||||
sql<string | null>`null::uuid`.as('fileAssetId'),
|
||||
])
|
||||
.where('asset.encodedVideoPath', 'is not', null)
|
||||
.where('asset.encodedVideoPath', '!=', sql<string>`''`),
|
||||
)
|
||||
.unionAll(
|
||||
eb
|
||||
.selectFrom('asset_file')
|
||||
.select(['path'])
|
||||
.select((eb) => [
|
||||
sql<string | null>`null::uuid`.as('assetId'),
|
||||
eb.ref('asset_file.id').$castTo<string | null>().as('fileAssetId'),
|
||||
]),
|
||||
)
|
||||
.as('allPaths'),
|
||||
)
|
||||
.leftJoin(
|
||||
'integrity_report',
|
||||
(join) =>
|
||||
join
|
||||
.on('integrity_report.type', '=', IntegrityReportType.OrphanFile)
|
||||
.on((eb) =>
|
||||
eb.or([
|
||||
eb('integrity_report.assetId', '=', eb.ref('allPaths.assetId')),
|
||||
eb('integrity_report.fileAssetId', '=', eb.ref('allPaths.fileAssetId')),
|
||||
]),
|
||||
),
|
||||
// .onRef('integrity_report.path', '=', 'allPaths.path')
|
||||
)
|
||||
.select(['allPaths.path as path', 'allPaths.assetId', 'allPaths.fileAssetId', 'integrity_report.id as reportId'])
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.DATE, DummyValue.DATE], stream: true })
|
||||
streamAssetChecksums(startMarker?: Date, endMarker?: Date) {
|
||||
return this.db
|
||||
.selectFrom('asset')
|
||||
.leftJoin('integrity_report', (join) =>
|
||||
join
|
||||
.onRef('integrity_report.assetId', '=', 'asset.id')
|
||||
// .onRef('integrity_report.path', '=', 'asset.originalPath')
|
||||
.on('integrity_report.type', '=', IntegrityReportType.ChecksumFail),
|
||||
)
|
||||
.select([
|
||||
'asset.originalPath',
|
||||
'asset.checksum',
|
||||
'asset.createdAt',
|
||||
'asset.id as assetId',
|
||||
'integrity_report.id as reportId',
|
||||
])
|
||||
.$if(startMarker !== undefined, (qb) => qb.where('createdAt', '>=', startMarker!))
|
||||
.$if(endMarker !== undefined, (qb) => qb.where('createdAt', '<=', endMarker!))
|
||||
.orderBy('createdAt', 'asc')
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.STRING], stream: true })
|
||||
streamIntegrityReports(type: IntegrityReportType) {
|
||||
return this.db
|
||||
.selectFrom('integrity_report')
|
||||
.select(['integrity_report.id as reportId', 'integrity_report.path'])
|
||||
.where('integrity_report.type', '=', type)
|
||||
.$if(type === IntegrityReportType.ChecksumFail, (eb) =>
|
||||
eb.leftJoin('asset', 'integrity_report.path', 'asset.originalPath').select('asset.checksum'),
|
||||
)
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.STRING], stream: true })
|
||||
streamIntegrityReportsByProperty(property?: 'assetId' | 'fileAssetId', filterType?: IntegrityReportType) {
|
||||
return this.db
|
||||
.selectFrom('integrity_report')
|
||||
.select(['id', 'path', 'assetId', 'fileAssetId'])
|
||||
.$if(filterType !== undefined, (eb) => eb.where('type', '=', filterType!))
|
||||
.$if(property === undefined, (eb) => eb.where('assetId', 'is', null).where('fileAssetId', 'is', null))
|
||||
.$if(property !== undefined, (eb) => eb.where(property!, 'is not', null))
|
||||
.stream();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.STRING] })
|
||||
deleteById(id: string) {
|
||||
return this.db.deleteFrom('integrity_report').where('id', '=', id).execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.STRING] })
|
||||
deleteByIds(ids: string[]) {
|
||||
return this.db.deleteFrom('integrity_report').where('id', 'in', ids).execute();
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ import { AssetTable } from 'src/schema/tables/asset.table';
|
||||
import { AuditTable } from 'src/schema/tables/audit.table';
|
||||
import { FaceSearchTable } from 'src/schema/tables/face-search.table';
|
||||
import { GeodataPlacesTable } from 'src/schema/tables/geodata-places.table';
|
||||
import { IntegrityReportTable } from 'src/schema/tables/integrity-report.table';
|
||||
import { LibraryTable } from 'src/schema/tables/library.table';
|
||||
import { MemoryAssetAuditTable } from 'src/schema/tables/memory-asset-audit.table';
|
||||
import { MemoryAssetTable } from 'src/schema/tables/memory-asset.table';
|
||||
@@ -98,6 +99,7 @@ export class ImmichDatabase {
|
||||
AssetExifTable,
|
||||
FaceSearchTable,
|
||||
GeodataPlacesTable,
|
||||
IntegrityReportTable,
|
||||
LibraryTable,
|
||||
MemoryTable,
|
||||
MemoryAuditTable,
|
||||
@@ -195,6 +197,8 @@ export interface DB {
|
||||
|
||||
geodata_places: GeodataPlacesTable;
|
||||
|
||||
integrity_report: IntegrityReportTable;
|
||||
|
||||
library: LibraryTable;
|
||||
|
||||
memory: MemoryTable;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`CREATE TABLE "integrity_report" (
|
||||
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"type" character varying NOT NULL,
|
||||
"path" character varying NOT NULL,
|
||||
"createdAt" timestamp with time zone NOT NULL DEFAULT now(),
|
||||
"assetId" uuid,
|
||||
"fileAssetId" uuid,
|
||||
CONSTRAINT "integrity_report_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "asset" ("id") ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT "integrity_report_fileAssetId_fkey" FOREIGN KEY ("fileAssetId") REFERENCES "asset_file" ("id") ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT "integrity_report_type_path_uq" UNIQUE ("type", "path"),
|
||||
CONSTRAINT "integrity_report_pkey" PRIMARY KEY ("id")
|
||||
);`.execute(db);
|
||||
await sql`CREATE INDEX "integrity_report_assetId_idx" ON "integrity_report" ("assetId");`.execute(db);
|
||||
await sql`CREATE INDEX "integrity_report_fileAssetId_idx" ON "integrity_report" ("fileAssetId");`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`DROP TABLE "integrity_report";`.execute(db);
|
||||
}
|
||||
35
server/src/schema/tables/integrity-report.table.ts
Normal file
35
server/src/schema/tables/integrity-report.table.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { IntegrityReportType } from 'src/enum';
|
||||
import { AssetFileTable } from 'src/schema/tables/asset-file.table';
|
||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
ForeignKeyColumn,
|
||||
Generated,
|
||||
PrimaryGeneratedColumn,
|
||||
Table,
|
||||
Timestamp,
|
||||
Unique,
|
||||
} from 'src/sql-tools';
|
||||
|
||||
@Table('integrity_report')
|
||||
@Unique({ columns: ['type', 'path'] })
|
||||
export class IntegrityReportTable {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: Generated<string>;
|
||||
|
||||
@Column()
|
||||
type!: IntegrityReportType;
|
||||
|
||||
@Column()
|
||||
path!: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Generated<Timestamp>;
|
||||
|
||||
@ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: true })
|
||||
assetId!: string | null;
|
||||
|
||||
@ForeignKeyColumn(() => AssetFileTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: true })
|
||||
fileAssetId!: string | null;
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import { DownloadRepository } from 'src/repositories/download.repository';
|
||||
import { DuplicateRepository } from 'src/repositories/duplicate.repository';
|
||||
import { EmailRepository } from 'src/repositories/email.repository';
|
||||
import { EventRepository } from 'src/repositories/event.repository';
|
||||
import { IntegrityRepository } from 'src/repositories/integrity.repository';
|
||||
import { JobRepository } from 'src/repositories/job.repository';
|
||||
import { LibraryRepository } from 'src/repositories/library.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
@@ -79,6 +80,7 @@ export const BASE_SERVICE_DEPENDENCIES = [
|
||||
DuplicateRepository,
|
||||
EmailRepository,
|
||||
EventRepository,
|
||||
IntegrityRepository,
|
||||
JobRepository,
|
||||
LibraryRepository,
|
||||
MachineLearningRepository,
|
||||
@@ -137,6 +139,7 @@ export class BaseService {
|
||||
protected duplicateRepository: DuplicateRepository,
|
||||
protected emailRepository: EmailRepository,
|
||||
protected eventRepository: EventRepository,
|
||||
protected integrityRepository: IntegrityRepository,
|
||||
protected jobRepository: JobRepository,
|
||||
protected libraryRepository: LibraryRepository,
|
||||
protected machineLearningRepository: MachineLearningRepository,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { CliService } from 'src/services/cli.service';
|
||||
import { DatabaseService } from 'src/services/database.service';
|
||||
import { DownloadService } from 'src/services/download.service';
|
||||
import { DuplicateService } from 'src/services/duplicate.service';
|
||||
import { IntegrityService } from 'src/services/integrity.service';
|
||||
import { JobService } from 'src/services/job.service';
|
||||
import { LibraryService } from 'src/services/library.service';
|
||||
import { MaintenanceService } from 'src/services/maintenance.service';
|
||||
@@ -62,6 +63,7 @@ export const services = [
|
||||
DatabaseService,
|
||||
DownloadService,
|
||||
DuplicateService,
|
||||
IntegrityService,
|
||||
JobService,
|
||||
LibraryService,
|
||||
MaintenanceService,
|
||||
|
||||
631
server/src/services/integrity.service.ts
Normal file
631
server/src/services/integrity.service.ts
Normal file
@@ -0,0 +1,631 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { createReadStream } from 'node:fs';
|
||||
import { stat } from 'node:fs/promises';
|
||||
import { Writable } from 'node:stream';
|
||||
import { pipeline } from 'node:stream/promises';
|
||||
import { JOBS_LIBRARY_PAGINATION_SIZE } from 'src/constants';
|
||||
import { StorageCore } from 'src/cores/storage.core';
|
||||
import { OnEvent, OnJob } from 'src/decorators';
|
||||
import {
|
||||
AssetStatus,
|
||||
DatabaseLock,
|
||||
ImmichWorker,
|
||||
IntegrityReportType,
|
||||
JobName,
|
||||
JobStatus,
|
||||
QueueName,
|
||||
StorageFolder,
|
||||
SystemMetadataKey,
|
||||
} from 'src/enum';
|
||||
import { ArgOf } from 'src/repositories/event.repository';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import {
|
||||
IIntegrityDeleteReportJob,
|
||||
IIntegrityJob,
|
||||
IIntegrityMissingFilesJob,
|
||||
IIntegrityOrphanedFilesJob,
|
||||
IIntegrityPathWithChecksumJob,
|
||||
IIntegrityPathWithReportJob,
|
||||
} from 'src/types';
|
||||
import { handlePromiseError } from 'src/utils/misc';
|
||||
|
||||
/**
|
||||
* Orphan Files:
|
||||
* Files are detected in /data/encoded-video, /data/library, /data/upload
|
||||
* Checked against the asset table
|
||||
* Files are detected in /data/thumbs
|
||||
* Checked against the asset_file table
|
||||
*
|
||||
* * Can perform download or delete of files
|
||||
*
|
||||
* Missing Files:
|
||||
* Paths are queried from asset(originalPath, encodedVideoPath), asset_file(path)
|
||||
* Check whether files exist on disk
|
||||
*
|
||||
* * Reports must include origin (asset or asset_file) & ID for further action
|
||||
* * Can perform trash (asset) or delete (asset_file)
|
||||
*
|
||||
* Checksum Mismatch:
|
||||
* Paths & checksums are queried from asset(originalPath, checksum)
|
||||
* Check whether files match checksum, missing files ignored
|
||||
*
|
||||
* * Reports must include origin (as above) for further action
|
||||
* * Can perform download or trash (asset)
|
||||
*/
|
||||
|
||||
@Injectable()
|
||||
export class IntegrityService extends BaseService {
|
||||
private integrityLock = false;
|
||||
|
||||
@OnEvent({ name: 'ConfigInit', workers: [ImmichWorker.Microservices] })
|
||||
async onConfigInit({
|
||||
newConfig: {
|
||||
integrityChecks: { orphanedFiles, missingFiles, checksumFiles },
|
||||
},
|
||||
}: ArgOf<'ConfigInit'>) {
|
||||
this.integrityLock = await this.databaseRepository.tryLock(DatabaseLock.IntegrityCheck);
|
||||
if (this.integrityLock) {
|
||||
this.cronRepository.create({
|
||||
name: 'integrityOrphanedFiles',
|
||||
expression: orphanedFiles.cronExpression,
|
||||
onTick: () =>
|
||||
handlePromiseError(
|
||||
this.jobRepository.queue({ name: JobName.IntegrityOrphanedFilesQueueAll, data: {} }),
|
||||
this.logger,
|
||||
),
|
||||
start: orphanedFiles.enabled,
|
||||
});
|
||||
|
||||
this.cronRepository.create({
|
||||
name: 'integrityMissingFiles',
|
||||
expression: missingFiles.cronExpression,
|
||||
onTick: () =>
|
||||
handlePromiseError(
|
||||
this.jobRepository.queue({ name: JobName.IntegrityMissingFilesQueueAll, data: {} }),
|
||||
this.logger,
|
||||
),
|
||||
start: missingFiles.enabled,
|
||||
});
|
||||
|
||||
this.cronRepository.create({
|
||||
name: 'integrityChecksumFiles',
|
||||
expression: checksumFiles.cronExpression,
|
||||
onTick: () =>
|
||||
handlePromiseError(this.jobRepository.queue({ name: JobName.IntegrityChecksumFiles, data: {} }), this.logger),
|
||||
start: checksumFiles.enabled,
|
||||
});
|
||||
}
|
||||
|
||||
// debug: run on boot
|
||||
setTimeout(() => {
|
||||
void this.jobRepository.queue({
|
||||
name: JobName.IntegrityOrphanedFilesQueueAll,
|
||||
data: {},
|
||||
});
|
||||
|
||||
void this.jobRepository.queue({
|
||||
name: JobName.IntegrityMissingFilesQueueAll,
|
||||
data: {},
|
||||
});
|
||||
|
||||
void this.jobRepository.queue({
|
||||
name: JobName.IntegrityChecksumFiles,
|
||||
data: {},
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'ConfigUpdate', server: true })
|
||||
onConfigUpdate({
|
||||
newConfig: {
|
||||
integrityChecks: { orphanedFiles, missingFiles, checksumFiles },
|
||||
},
|
||||
}: ArgOf<'ConfigUpdate'>) {
|
||||
if (!this.integrityLock) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cronRepository.update({
|
||||
name: 'integrityOrphanedFiles',
|
||||
expression: orphanedFiles.cronExpression,
|
||||
start: orphanedFiles.enabled,
|
||||
});
|
||||
|
||||
this.cronRepository.update({
|
||||
name: 'integrityMissingFiles',
|
||||
expression: missingFiles.cronExpression,
|
||||
start: missingFiles.enabled,
|
||||
});
|
||||
|
||||
this.cronRepository.update({
|
||||
name: 'integrityChecksumFiles',
|
||||
expression: checksumFiles.cronExpression,
|
||||
start: checksumFiles.enabled,
|
||||
});
|
||||
}
|
||||
|
||||
@OnJob({ name: JobName.IntegrityOrphanedFilesQueueAll, queue: QueueName.BackgroundTask })
|
||||
async handleOrphanedFilesQueueAll({ refreshOnly }: IIntegrityJob = {}): Promise<JobStatus> {
|
||||
this.logger.log(`Checking for out of date orphaned file reports...`);
|
||||
|
||||
const reports = this.integrityRepository.streamIntegrityReports(IntegrityReportType.OrphanFile);
|
||||
|
||||
let total = 0;
|
||||
for await (const batchReports of chunk(reports, JOBS_LIBRARY_PAGINATION_SIZE)) {
|
||||
await this.jobRepository.queue({
|
||||
name: JobName.IntegrityOrphanedFilesRefresh,
|
||||
data: {
|
||||
items: batchReports,
|
||||
},
|
||||
});
|
||||
|
||||
total += batchReports.length;
|
||||
this.logger.log(`Queued report check of ${batchReports.length} report(s) (${total} so far)`);
|
||||
}
|
||||
|
||||
if (refreshOnly) {
|
||||
this.logger.log('Refresh complete.');
|
||||
return JobStatus.Success;
|
||||
}
|
||||
|
||||
this.logger.log(`Scanning for orphaned files...`);
|
||||
|
||||
const assetPaths = this.storageRepository.walk({
|
||||
pathsToCrawl: [StorageFolder.EncodedVideo, StorageFolder.Library, StorageFolder.Upload].map((folder) =>
|
||||
StorageCore.getBaseFolder(folder),
|
||||
),
|
||||
includeHidden: false,
|
||||
take: JOBS_LIBRARY_PAGINATION_SIZE,
|
||||
});
|
||||
|
||||
const assetFilePaths = this.storageRepository.walk({
|
||||
pathsToCrawl: [StorageCore.getBaseFolder(StorageFolder.Thumbnails)],
|
||||
includeHidden: false,
|
||||
take: JOBS_LIBRARY_PAGINATION_SIZE,
|
||||
});
|
||||
|
||||
async function* paths() {
|
||||
for await (const batch of assetPaths) {
|
||||
yield ['asset', batch] as const;
|
||||
}
|
||||
|
||||
for await (const batch of assetFilePaths) {
|
||||
yield ['asset_file', batch] as const;
|
||||
}
|
||||
}
|
||||
|
||||
total = 0;
|
||||
for await (const [batchType, batchPaths] of paths()) {
|
||||
await this.jobRepository.queue({
|
||||
name: JobName.IntegrityOrphanedFiles,
|
||||
data: {
|
||||
type: batchType,
|
||||
paths: batchPaths,
|
||||
},
|
||||
});
|
||||
|
||||
const count = batchPaths.length;
|
||||
total += count;
|
||||
|
||||
this.logger.log(`Queued orphan check of ${count} file(s) (${total} so far)`);
|
||||
}
|
||||
|
||||
return JobStatus.Success;
|
||||
}
|
||||
|
||||
@OnJob({ name: JobName.IntegrityOrphanedFiles, queue: QueueName.BackgroundTask })
|
||||
async handleOrphanedFiles({ type, paths }: IIntegrityOrphanedFilesJob): Promise<JobStatus> {
|
||||
this.logger.log(`Processing batch of ${paths.length} files to check if they are orphaned.`);
|
||||
|
||||
const orphanedFiles = new Set<string>(paths);
|
||||
if (type === 'asset') {
|
||||
const assets = await this.integrityRepository.getAssetPathsByPaths(paths);
|
||||
for (const { originalPath, encodedVideoPath } of assets) {
|
||||
orphanedFiles.delete(originalPath);
|
||||
|
||||
if (encodedVideoPath) {
|
||||
orphanedFiles.delete(encodedVideoPath);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const assets = await this.integrityRepository.getAssetFilePathsByPaths(paths);
|
||||
for (const { path } of assets) {
|
||||
orphanedFiles.delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
if (orphanedFiles.size > 0) {
|
||||
await this.integrityRepository.create(
|
||||
[...orphanedFiles].map((path) => ({
|
||||
type: IntegrityReportType.OrphanFile,
|
||||
path,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(`Processed ${paths.length} and found ${orphanedFiles.size} orphaned file(s).`);
|
||||
return JobStatus.Success;
|
||||
}
|
||||
|
||||
@OnJob({ name: JobName.IntegrityOrphanedFilesRefresh, queue: QueueName.BackgroundTask })
|
||||
async handleOrphanedRefresh({ items }: IIntegrityPathWithReportJob): Promise<JobStatus> {
|
||||
this.logger.log(`Processing batch of ${items.length} reports to check if they are out of date.`);
|
||||
|
||||
const results = await Promise.all(
|
||||
items.map(({ reportId, path }) =>
|
||||
stat(path)
|
||||
.then(() => void 0)
|
||||
.catch(() => reportId),
|
||||
),
|
||||
);
|
||||
|
||||
const reportIds = results.filter(Boolean) as string[];
|
||||
|
||||
if (reportIds.length > 0) {
|
||||
await this.integrityRepository.deleteByIds(reportIds);
|
||||
}
|
||||
|
||||
this.logger.log(`Processed ${items.length} paths and found ${reportIds.length} report(s) out of date.`);
|
||||
return JobStatus.Success;
|
||||
}
|
||||
|
||||
@OnJob({ name: JobName.IntegrityMissingFilesQueueAll, queue: QueueName.BackgroundTask })
|
||||
async handleMissingFilesQueueAll({ refreshOnly }: IIntegrityJob = {}): Promise<JobStatus> {
|
||||
if (refreshOnly) {
|
||||
this.logger.log(`Checking for out of date missing file reports...`);
|
||||
|
||||
const reports = this.integrityRepository.streamIntegrityReports(IntegrityReportType.MissingFile);
|
||||
|
||||
let total = 0;
|
||||
for await (const batchReports of chunk(reports, JOBS_LIBRARY_PAGINATION_SIZE)) {
|
||||
await this.jobRepository.queue({
|
||||
name: JobName.IntegrityMissingFilesRefresh,
|
||||
data: {
|
||||
items: batchReports,
|
||||
},
|
||||
});
|
||||
|
||||
total += batchReports.length;
|
||||
this.logger.log(`Queued report check of ${batchReports.length} report(s) (${total} so far)`);
|
||||
}
|
||||
|
||||
this.logger.log('Refresh complete.');
|
||||
return JobStatus.Success;
|
||||
}
|
||||
|
||||
this.logger.log(`Scanning for missing files...`);
|
||||
|
||||
const assetPaths = this.integrityRepository.streamAssetPaths();
|
||||
|
||||
let total = 0;
|
||||
for await (const batchPaths of chunk(assetPaths, JOBS_LIBRARY_PAGINATION_SIZE)) {
|
||||
await this.jobRepository.queue({
|
||||
name: JobName.IntegrityMissingFiles,
|
||||
data: {
|
||||
items: batchPaths,
|
||||
},
|
||||
});
|
||||
|
||||
total += batchPaths.length;
|
||||
this.logger.log(`Queued missing check of ${batchPaths.length} file(s) (${total} so far)`);
|
||||
}
|
||||
|
||||
return JobStatus.Success;
|
||||
}
|
||||
|
||||
@OnJob({ name: JobName.IntegrityMissingFiles, queue: QueueName.BackgroundTask })
|
||||
async handleMissingFiles({ items }: IIntegrityMissingFilesJob): Promise<JobStatus> {
|
||||
this.logger.log(`Processing batch of ${items.length} files to check if they are missing.`);
|
||||
|
||||
const results = await Promise.all(
|
||||
items.map((item) =>
|
||||
stat(item.path)
|
||||
.then(() => ({ ...item, exists: true }))
|
||||
.catch(() => ({ ...item, exists: false })),
|
||||
),
|
||||
);
|
||||
|
||||
const outdatedReports = results
|
||||
.filter(({ exists, reportId }) => exists && reportId)
|
||||
.map(({ reportId }) => reportId!);
|
||||
|
||||
if (outdatedReports.length > 0) {
|
||||
await this.integrityRepository.deleteByIds(outdatedReports);
|
||||
}
|
||||
|
||||
const missingFiles = results.filter(({ exists }) => !exists);
|
||||
if (missingFiles.length > 0) {
|
||||
await this.integrityRepository.create(
|
||||
missingFiles.map(({ path, assetId, fileAssetId }) => ({
|
||||
type: IntegrityReportType.MissingFile,
|
||||
path,
|
||||
assetId,
|
||||
fileAssetId,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(`Processed ${items.length} and found ${missingFiles.length} missing file(s).`);
|
||||
return JobStatus.Success;
|
||||
}
|
||||
|
||||
@OnJob({ name: JobName.IntegrityMissingFilesRefresh, queue: QueueName.BackgroundTask })
|
||||
async handleMissingRefresh({ items: paths }: IIntegrityPathWithReportJob): Promise<JobStatus> {
|
||||
this.logger.log(`Processing batch of ${paths.length} reports to check if they are out of date.`);
|
||||
|
||||
const results = await Promise.all(
|
||||
paths.map(({ reportId, path }) =>
|
||||
stat(path)
|
||||
.then(() => reportId)
|
||||
.catch(() => void 0),
|
||||
),
|
||||
);
|
||||
|
||||
const reportIds = results.filter(Boolean) as string[];
|
||||
|
||||
if (reportIds.length > 0) {
|
||||
await this.integrityRepository.deleteByIds(reportIds);
|
||||
}
|
||||
|
||||
this.logger.log(`Processed ${paths.length} paths and found ${reportIds.length} report(s) out of date.`);
|
||||
return JobStatus.Success;
|
||||
}
|
||||
|
||||
@OnJob({ name: JobName.IntegrityChecksumFiles, queue: QueueName.BackgroundTask })
|
||||
async handleChecksumFiles({ refreshOnly }: IIntegrityJob = {}): Promise<JobStatus> {
|
||||
if (refreshOnly) {
|
||||
this.logger.log(`Checking for out of date checksum file reports...`);
|
||||
|
||||
const reports = this.integrityRepository.streamIntegrityReports(IntegrityReportType.ChecksumFail);
|
||||
|
||||
let total = 0;
|
||||
for await (const batchReports of chunk(reports, JOBS_LIBRARY_PAGINATION_SIZE)) {
|
||||
await this.jobRepository.queue({
|
||||
name: JobName.IntegrityChecksumFilesRefresh,
|
||||
data: {
|
||||
items: batchReports.map(({ path, reportId, checksum }) => ({
|
||||
path,
|
||||
reportId,
|
||||
checksum: checksum?.toString('hex'),
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
total += batchReports.length;
|
||||
this.logger.log(`Queued report check of ${batchReports.length} report(s) (${total} so far)`);
|
||||
}
|
||||
|
||||
this.logger.log('Refresh complete.');
|
||||
return JobStatus.Success;
|
||||
}
|
||||
|
||||
const {
|
||||
integrityChecks: {
|
||||
checksumFiles: { timeLimit, percentageLimit },
|
||||
},
|
||||
} = await this.getConfig({
|
||||
withCache: true,
|
||||
});
|
||||
|
||||
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)`,
|
||||
);
|
||||
|
||||
let processed = 0;
|
||||
const startedAt = Date.now();
|
||||
const { count } = await this.integrityRepository.getAssetCount();
|
||||
const checkpoint = await this.systemMetadataRepository.get(SystemMetadataKey.IntegrityChecksumCheckpoint);
|
||||
|
||||
let startMarker: Date | undefined = checkpoint?.date ? new Date(checkpoint.date) : undefined;
|
||||
let endMarker: Date | undefined;
|
||||
|
||||
const printStats = () => {
|
||||
const averageTime = ((Date.now() - startedAt) / processed).toFixed(2);
|
||||
const completionProgress = ((processed / count) * 100).toFixed(2);
|
||||
|
||||
this.logger.log(
|
||||
`Processed ${processed} files so far... (avg. ${averageTime} ms/asset, ${completionProgress}% of all assets)`,
|
||||
);
|
||||
};
|
||||
|
||||
let lastCreatedAt: Date | undefined;
|
||||
|
||||
finishEarly: do {
|
||||
this.logger.log(
|
||||
`Processing assets in range [${startMarker?.toISOString() ?? 'beginning'}, ${endMarker?.toISOString() ?? 'end'}]`,
|
||||
);
|
||||
|
||||
const assets = this.integrityRepository.streamAssetChecksums(startMarker, endMarker);
|
||||
endMarker = startMarker;
|
||||
startMarker = undefined;
|
||||
|
||||
for await (const { originalPath, checksum, createdAt, assetId, reportId } of assets) {
|
||||
processed++;
|
||||
|
||||
try {
|
||||
const hash = createHash('sha1');
|
||||
|
||||
await pipeline([
|
||||
createReadStream(originalPath),
|
||||
new Writable({
|
||||
write(chunk, _encoding, callback) {
|
||||
hash.update(chunk);
|
||||
callback();
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
if (checksum.equals(hash.digest())) {
|
||||
if (reportId) {
|
||||
await this.integrityRepository.deleteById(reportId);
|
||||
}
|
||||
} else {
|
||||
throw new Error('File failed checksum');
|
||||
}
|
||||
} catch (error) {
|
||||
if ((error as { code?: string }).code === 'ENOENT') {
|
||||
if (reportId) {
|
||||
await this.integrityRepository.deleteById(reportId);
|
||||
}
|
||||
// missing file; handled by the missing files job
|
||||
continue;
|
||||
}
|
||||
|
||||
this.logger.warn('Failed to process a file: ' + error);
|
||||
await this.integrityRepository.create({
|
||||
path: originalPath,
|
||||
type: IntegrityReportType.ChecksumFail,
|
||||
assetId,
|
||||
});
|
||||
}
|
||||
|
||||
if (processed % 100 === 0) {
|
||||
printStats();
|
||||
}
|
||||
|
||||
if (Date.now() > startedAt + timeLimit || processed > count * percentageLimit) {
|
||||
this.logger.log('Reached stop criteria.');
|
||||
lastCreatedAt = createdAt;
|
||||
break finishEarly;
|
||||
}
|
||||
}
|
||||
} while (endMarker);
|
||||
|
||||
await this.systemMetadataRepository.set(SystemMetadataKey.IntegrityChecksumCheckpoint, {
|
||||
date: lastCreatedAt?.toISOString(),
|
||||
});
|
||||
|
||||
printStats();
|
||||
|
||||
if (lastCreatedAt) {
|
||||
this.logger.log(`Finished checksum job, will continue from ${lastCreatedAt.toISOString()}.`);
|
||||
} else {
|
||||
this.logger.log(`Finished checksum job, covered all assets.`);
|
||||
}
|
||||
|
||||
return JobStatus.Success;
|
||||
}
|
||||
|
||||
@OnJob({ name: JobName.IntegrityChecksumFilesRefresh, queue: QueueName.BackgroundTask })
|
||||
async handleChecksumRefresh({ items: paths }: IIntegrityPathWithChecksumJob): Promise<JobStatus> {
|
||||
this.logger.log(`Processing batch of ${paths.length} reports to check if they are out of date.`);
|
||||
|
||||
const results = await Promise.all(
|
||||
paths.map(async ({ reportId, path, checksum }) => {
|
||||
if (!checksum) {
|
||||
return reportId;
|
||||
}
|
||||
|
||||
try {
|
||||
const hash = createHash('sha1');
|
||||
|
||||
await pipeline([
|
||||
createReadStream(path),
|
||||
new Writable({
|
||||
write(chunk, _encoding, callback) {
|
||||
hash.update(chunk);
|
||||
callback();
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
if (Buffer.from(checksum, 'hex').equals(hash.digest())) {
|
||||
return reportId;
|
||||
}
|
||||
} catch (error) {
|
||||
if ((error as { code?: string }).code === 'ENOENT') {
|
||||
return reportId;
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const reportIds = results.filter(Boolean) as string[];
|
||||
|
||||
if (reportIds.length > 0) {
|
||||
await this.integrityRepository.deleteByIds(reportIds);
|
||||
}
|
||||
|
||||
this.logger.log(`Processed ${paths.length} paths and found ${reportIds.length} report(s) out of date.`);
|
||||
return JobStatus.Success;
|
||||
}
|
||||
|
||||
@OnJob({ name: JobName.IntegrityReportDelete, queue: QueueName.BackgroundTask })
|
||||
async handleDeleteIntegrityReport({ type }: IIntegrityDeleteReportJob): Promise<JobStatus> {
|
||||
this.logger.log(`Deleting all entries for ${type ?? 'all types of'} integrity report`);
|
||||
|
||||
let properties;
|
||||
switch (type) {
|
||||
case IntegrityReportType.ChecksumFail: {
|
||||
properties = ['assetId'] as const;
|
||||
break;
|
||||
}
|
||||
case IntegrityReportType.MissingFile: {
|
||||
properties = ['assetId', 'fileAssetId'] as const;
|
||||
break;
|
||||
}
|
||||
case IntegrityReportType.OrphanFile: {
|
||||
properties = [void 0] as const;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
properties = [void 0, 'assetId', 'fileAssetId'] as const;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const property of properties) {
|
||||
const reports = this.integrityRepository.streamIntegrityReportsByProperty(property, type);
|
||||
for await (const report of chunk(reports, JOBS_LIBRARY_PAGINATION_SIZE)) {
|
||||
// todo: queue sub-job here instead?
|
||||
|
||||
switch (property) {
|
||||
case 'assetId': {
|
||||
const ids = report.map(({ assetId }) => assetId!);
|
||||
await this.assetRepository.updateAll(ids, {
|
||||
deletedAt: new Date(),
|
||||
status: AssetStatus.Trashed,
|
||||
});
|
||||
|
||||
await this.eventRepository.emit('AssetTrashAll', {
|
||||
assetIds: ids,
|
||||
userId: '', // ???
|
||||
});
|
||||
|
||||
await this.integrityRepository.deleteByIds(report.map(({ id }) => id));
|
||||
break;
|
||||
}
|
||||
case 'fileAssetId': {
|
||||
await this.assetRepository.deleteFiles(report.map(({ fileAssetId }) => ({ id: fileAssetId! })));
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
await Promise.all(report.map(({ path }) => this.storageRepository.unlink(path).catch(() => void 0)));
|
||||
await this.integrityRepository.deleteByIds(report.map(({ id }) => id));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log('Finished deleting integrity report.');
|
||||
return JobStatus.Success;
|
||||
}
|
||||
}
|
||||
|
||||
async function* chunk<T>(generator: AsyncIterableIterator<T>, n: number) {
|
||||
let chunk: T[] = [];
|
||||
for await (const item of generator) {
|
||||
chunk.push(item);
|
||||
|
||||
if (chunk.length === n) {
|
||||
yield chunk;
|
||||
chunk = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.length > 0) {
|
||||
yield chunk;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from 'src/decorators';
|
||||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { JobCreateDto } from 'src/dtos/job.dto';
|
||||
import { AssetType, AssetVisibility, JobName, JobStatus, ManualJobName } from 'src/enum';
|
||||
import { AssetType, AssetVisibility, IntegrityReportType, JobName, JobStatus, ManualJobName } from 'src/enum';
|
||||
import { ArgsOf } from 'src/repositories/event.repository';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { JobItem } from 'src/types';
|
||||
@@ -34,6 +34,42 @@ const asJobItem = (dto: JobCreateDto): JobItem => {
|
||||
return { name: JobName.DatabaseBackup };
|
||||
}
|
||||
|
||||
case ManualJobName.IntegrityMissingFiles: {
|
||||
return { name: JobName.IntegrityMissingFilesQueueAll };
|
||||
}
|
||||
|
||||
case ManualJobName.IntegrityOrphanFiles: {
|
||||
return { name: JobName.IntegrityOrphanedFilesQueueAll };
|
||||
}
|
||||
|
||||
case ManualJobName.IntegrityChecksumFiles: {
|
||||
return { name: JobName.IntegrityChecksumFiles };
|
||||
}
|
||||
|
||||
case ManualJobName.IntegrityMissingFilesRefresh: {
|
||||
return { name: JobName.IntegrityMissingFilesQueueAll, data: { refreshOnly: true } };
|
||||
}
|
||||
|
||||
case ManualJobName.IntegrityOrphanFilesRefresh: {
|
||||
return { name: JobName.IntegrityOrphanedFilesQueueAll, data: { refreshOnly: true } };
|
||||
}
|
||||
|
||||
case ManualJobName.IntegrityChecksumFilesRefresh: {
|
||||
return { name: JobName.IntegrityChecksumFiles, data: { refreshOnly: true } };
|
||||
}
|
||||
|
||||
case ManualJobName.IntegrityMissingFilesDeleteAll: {
|
||||
return { name: JobName.IntegrityReportDelete, data: { type: IntegrityReportType.MissingFile } };
|
||||
}
|
||||
|
||||
case ManualJobName.IntegrityOrphanFilesDeleteAll: {
|
||||
return { name: JobName.IntegrityReportDelete, data: { type: IntegrityReportType.OrphanFile } };
|
||||
}
|
||||
|
||||
case ManualJobName.IntegrityChecksumFilesDeleteAll: {
|
||||
return { name: JobName.IntegrityReportDelete, data: { type: IntegrityReportType.ChecksumFail } };
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new BadRequestException('Invalid job name');
|
||||
}
|
||||
|
||||
@@ -106,4 +106,7 @@ describe(MaintenanceService.name, () => {
|
||||
expect(mocks.systemMetadata.get).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip('getIntegrityReportSummary'); // just calls repository
|
||||
describe.skip('getIntegrityReport'); // just calls repository
|
||||
});
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { basename } from 'node:path';
|
||||
import { Readable } from 'node:stream';
|
||||
import { OnEvent } from 'src/decorators';
|
||||
import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto';
|
||||
import { SystemMetadataKey } from 'src/enum';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import {
|
||||
MaintenanceAuthDto,
|
||||
MaintenanceGetIntegrityReportDto,
|
||||
MaintenanceIntegrityReportResponseDto,
|
||||
MaintenanceIntegrityReportSummaryResponseDto,
|
||||
} from 'src/dtos/maintenance.dto';
|
||||
import { AssetStatus, CacheControl, IntegrityReportType, SystemMetadataKey } from 'src/enum';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { MaintenanceModeState } from 'src/types';
|
||||
import { ImmichFileResponse } from 'src/utils/file';
|
||||
import { createMaintenanceLoginUrl, generateMaintenanceSecret, signMaintenanceJwt } from 'src/utils/maintenance';
|
||||
import { getExternalDomain } from 'src/utils/misc';
|
||||
|
||||
@@ -50,4 +59,52 @@ export class MaintenanceService extends BaseService {
|
||||
|
||||
return await createMaintenanceLoginUrl(baseUrl, auth, secret);
|
||||
}
|
||||
|
||||
getIntegrityReportSummary(): Promise<MaintenanceIntegrityReportSummaryResponseDto> {
|
||||
return this.integrityRepository.getIntegrityReportSummary();
|
||||
}
|
||||
|
||||
async getIntegrityReport(dto: MaintenanceGetIntegrityReportDto): Promise<MaintenanceIntegrityReportResponseDto> {
|
||||
return {
|
||||
items: await this.integrityRepository.getIntegrityReports(dto.type),
|
||||
};
|
||||
}
|
||||
|
||||
getIntegrityReportCsv(type: IntegrityReportType): Readable {
|
||||
return this.integrityRepository.streamIntegrityReportsCSV(type);
|
||||
}
|
||||
|
||||
async getIntegrityReportFile(id: string): Promise<ImmichFileResponse> {
|
||||
const { path } = await this.integrityRepository.getById(id);
|
||||
|
||||
return new ImmichFileResponse({
|
||||
path,
|
||||
fileName: basename(path),
|
||||
contentType: 'application/octet-stream',
|
||||
cacheControl: CacheControl.PrivateWithoutCache,
|
||||
});
|
||||
}
|
||||
|
||||
async deleteIntegrityReport(auth: AuthDto, id: string): Promise<void> {
|
||||
const { path, assetId, fileAssetId } = await this.integrityRepository.getById(id);
|
||||
|
||||
if (assetId) {
|
||||
await this.assetRepository.updateAll([assetId], {
|
||||
deletedAt: new Date(),
|
||||
status: AssetStatus.Trashed,
|
||||
});
|
||||
|
||||
await this.eventRepository.emit('AssetTrashAll', {
|
||||
assetIds: [assetId],
|
||||
userId: auth.user.id,
|
||||
});
|
||||
|
||||
await this.integrityRepository.deleteById(id);
|
||||
} else if (fileAssetId) {
|
||||
await this.assetRepository.deleteFiles([{ id: fileAssetId }]);
|
||||
} else {
|
||||
await this.storageRepository.unlink(path);
|
||||
await this.integrityRepository.deleteById(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,22 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
||||
accelDecode: false,
|
||||
tonemap: ToneMapping.Hable,
|
||||
},
|
||||
integrityChecks: {
|
||||
orphanedFiles: {
|
||||
enabled: true,
|
||||
cronExpression: '0 03 * * *',
|
||||
},
|
||||
missingFiles: {
|
||||
enabled: true,
|
||||
cronExpression: '0 03 * * *',
|
||||
},
|
||||
checksumFiles: {
|
||||
enabled: true,
|
||||
cronExpression: '0 03 * * *',
|
||||
timeLimit: 60 * 60 * 1000,
|
||||
percentageLimit: 1,
|
||||
},
|
||||
},
|
||||
logging: {
|
||||
enabled: true,
|
||||
level: LogLevel.Log,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
DatabaseSslMode,
|
||||
ExifOrientation,
|
||||
ImageFormat,
|
||||
IntegrityReportType,
|
||||
JobName,
|
||||
MemoryType,
|
||||
PluginTriggerType,
|
||||
@@ -282,6 +283,36 @@ export interface IWorkflowJob<T extends PluginTriggerType = PluginTriggerType> {
|
||||
event: WorkflowData[T];
|
||||
}
|
||||
|
||||
export interface IIntegrityJob {
|
||||
refreshOnly?: boolean;
|
||||
}
|
||||
|
||||
export interface IIntegrityDeleteReportJob {
|
||||
type?: IntegrityReportType;
|
||||
}
|
||||
|
||||
export interface IIntegrityOrphanedFilesJob {
|
||||
type: 'asset' | 'asset_file';
|
||||
paths: string[];
|
||||
}
|
||||
|
||||
export interface IIntegrityMissingFilesJob {
|
||||
items: {
|
||||
path: string;
|
||||
reportId: string | null;
|
||||
assetId: string | null;
|
||||
fileAssetId: string | null;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface IIntegrityPathWithReportJob {
|
||||
items: { path: string; reportId: string | null }[];
|
||||
}
|
||||
|
||||
export interface IIntegrityPathWithChecksumJob {
|
||||
items: { path: string; reportId: string | null; checksum?: string | null }[];
|
||||
}
|
||||
|
||||
export interface JobCounts {
|
||||
active: number;
|
||||
completed: number;
|
||||
@@ -391,7 +422,18 @@ export type JobItem =
|
||||
| { name: JobName.Ocr; data: IEntityJob }
|
||||
|
||||
// Workflow
|
||||
| { name: JobName.WorkflowRun; data: IWorkflowJob };
|
||||
| { name: JobName.WorkflowRun; data: IWorkflowJob }
|
||||
|
||||
// Integrity
|
||||
| { name: JobName.IntegrityOrphanedFilesQueueAll; data?: IIntegrityJob }
|
||||
| { name: JobName.IntegrityOrphanedFiles; data: IIntegrityOrphanedFilesJob }
|
||||
| { name: JobName.IntegrityOrphanedFilesRefresh; data: IIntegrityPathWithReportJob }
|
||||
| { name: JobName.IntegrityMissingFilesQueueAll; data?: IIntegrityJob }
|
||||
| { name: JobName.IntegrityMissingFiles; data: IIntegrityPathWithReportJob }
|
||||
| { name: JobName.IntegrityMissingFilesRefresh; data: IIntegrityPathWithReportJob }
|
||||
| { name: JobName.IntegrityChecksumFiles; data?: IIntegrityJob }
|
||||
| { name: JobName.IntegrityChecksumFilesRefresh; data?: IIntegrityPathWithChecksumJob }
|
||||
| { name: JobName.IntegrityReportDelete; data: IIntegrityDeleteReportJob };
|
||||
|
||||
export type VectorExtension = (typeof VECTOR_EXTENSIONS)[number];
|
||||
|
||||
@@ -506,6 +548,7 @@ export interface SystemMetadata extends Record<SystemMetadataKey, Record<string,
|
||||
[SystemMetadataKey.SystemFlags]: DeepPartial<SystemFlags>;
|
||||
[SystemMetadataKey.VersionCheckState]: VersionCheckMetadata;
|
||||
[SystemMetadataKey.MemoriesState]: MemoriesState;
|
||||
[SystemMetadataKey.IntegrityChecksumCheckpoint]: { date?: string };
|
||||
}
|
||||
|
||||
export interface UserPreferences {
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
import { CronJob } from 'cron';
|
||||
import { DateTime } from 'luxon';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { IntegrityReportType } from 'src/enum';
|
||||
import { isIP, isIPRange } from 'validator';
|
||||
|
||||
@Injectable()
|
||||
@@ -96,6 +97,12 @@ export class UUIDAssetIDParamDto {
|
||||
assetId!: string;
|
||||
}
|
||||
|
||||
export class IntegrityReportTypeParamDto {
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ enum: IntegrityReportType, enumName: 'IntegrityReportType' })
|
||||
type!: IntegrityReportType;
|
||||
}
|
||||
|
||||
type PinCodeOptions = { optional?: boolean } & OptionalOptions;
|
||||
export const PinCode = (options?: PinCodeOptions & ApiPropertyOptions) => {
|
||||
const { optional, nullable, emptyToNull, ...apiPropertyOptions } = {
|
||||
|
||||
@@ -31,6 +31,7 @@ import { DownloadRepository } from 'src/repositories/download.repository';
|
||||
import { DuplicateRepository } from 'src/repositories/duplicate.repository';
|
||||
import { EmailRepository } from 'src/repositories/email.repository';
|
||||
import { EventRepository } from 'src/repositories/event.repository';
|
||||
import { IntegrityRepository } from 'src/repositories/integrity.repository';
|
||||
import { JobRepository } from 'src/repositories/job.repository';
|
||||
import { LibraryRepository } from 'src/repositories/library.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
@@ -225,6 +226,7 @@ export type ServiceOverrides = {
|
||||
duplicateRepository: DuplicateRepository;
|
||||
email: EmailRepository;
|
||||
event: EventRepository;
|
||||
integrityReport: IntegrityRepository;
|
||||
job: JobRepository;
|
||||
library: LibraryRepository;
|
||||
logger: LoggingRepository;
|
||||
@@ -298,6 +300,7 @@ export const getMocks = () => {
|
||||
email: automock(EmailRepository, { args: [loggerMock] }),
|
||||
// eslint-disable-next-line no-sparse-arrays
|
||||
event: automock(EventRepository, { args: [, , loggerMock], strict: false }),
|
||||
integrityReport: automock(IntegrityRepository, { strict: false }),
|
||||
job: newJobRepositoryMock(),
|
||||
apiKey: automock(ApiKeyRepository),
|
||||
library: automock(LibraryRepository, { strict: false }),
|
||||
@@ -366,6 +369,7 @@ export const newTestService = <T extends BaseService>(
|
||||
overrides.duplicateRepository || (mocks.duplicateRepository as As<DuplicateRepository>),
|
||||
overrides.email || (mocks.email as As<EmailRepository>),
|
||||
overrides.event || (mocks.event as As<EventRepository>),
|
||||
overrides.integrityReport || (mocks.integrityReport as As<IntegrityRepository>),
|
||||
overrides.job || (mocks.job as As<JobRepository>),
|
||||
overrides.library || (mocks.library as As<LibraryRepository>),
|
||||
overrides.machineLearning || (mocks.machineLearning as As<MachineLearningRepository>),
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { ByteUnit } from '$lib/utils/byte-units';
|
||||
import { Code, Icon, Text } from '@immich/ui';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
icon: string;
|
||||
icon?: string;
|
||||
title: string;
|
||||
value: number;
|
||||
unit?: ByteUnit | undefined;
|
||||
footer?: Snippet<[]>;
|
||||
}
|
||||
|
||||
let { icon, title, value, unit = undefined }: Props = $props();
|
||||
let { icon, title, value, unit = undefined, footer }: Props = $props();
|
||||
|
||||
const zeros = $derived(() => {
|
||||
const maxLength = 13;
|
||||
@@ -22,7 +24,9 @@
|
||||
|
||||
<div class="flex h-35 w-full flex-col justify-between rounded-3xl bg-subtle text-primary p-5">
|
||||
<div class="flex place-items-center gap-4">
|
||||
<Icon {icon} size="40" />
|
||||
{#if icon}
|
||||
<Icon {icon} size="40" />
|
||||
{/if}
|
||||
<Text size="large" fontWeight="bold" class="uppercase">{title}</Text>
|
||||
</div>
|
||||
|
||||
@@ -32,4 +36,6 @@
|
||||
<Code color="muted" class="absolute -top-5 end-1 font-light p-0">{unit}</Code>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{@render footer?.()}
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,8 @@ export enum AppRoute {
|
||||
ADMIN_USERS = '/admin/users',
|
||||
ADMIN_LIBRARY_MANAGEMENT = '/admin/library-management',
|
||||
ADMIN_SETTINGS = '/admin/system-settings',
|
||||
ADMIN_MAINTENANCE_SETTINGS = '/admin/maintenance',
|
||||
ADMIN_MAINTENANCE_INTEGRITY_REPORT = '/admin/maintenance/integrity-report/',
|
||||
ADMIN_STATS = '/admin/server-status',
|
||||
ADMIN_JOBS = '/admin/jobs-status',
|
||||
ADMIN_REPAIR = '/admin/repair',
|
||||
|
||||
@@ -16,6 +16,30 @@
|
||||
{ title: $t('admin.memory_cleanup_job'), value: ManualJobName.MemoryCleanup },
|
||||
{ title: $t('admin.memory_generate_job'), value: ManualJobName.MemoryCreate },
|
||||
{ title: $t('admin.backup_database'), value: ManualJobName.BackupDatabase },
|
||||
{
|
||||
title: $t('admin.maintenance_integrity_missing_file_job'),
|
||||
value: ManualJobName.IntegrityMissingFiles,
|
||||
},
|
||||
{
|
||||
title: $t('admin.maintenance_integrity_orphan_file_job'),
|
||||
value: ManualJobName.IntegrityOrphanFiles,
|
||||
},
|
||||
{
|
||||
title: $t('admin.maintenance_integrity_checksum_mismatch_job'),
|
||||
value: ManualJobName.IntegrityChecksumMismatch,
|
||||
},
|
||||
{
|
||||
title: $t('admin.maintenance_integrity_missing_file_refresh_job'),
|
||||
value: ManualJobName.IntegrityMissingFilesRefresh,
|
||||
},
|
||||
{
|
||||
title: $t('admin.maintenance_integrity_orphan_file_refresh_job'),
|
||||
value: ManualJobName.IntegrityOrphanFilesRefresh,
|
||||
},
|
||||
{
|
||||
title: $t('admin.maintenance_integrity_checksum_mismatch_refresh_job'),
|
||||
value: ManualJobName.IntegrityChecksumMismatchRefresh,
|
||||
},
|
||||
].map(({ value, title }) => ({ id: value, label: title, value }));
|
||||
|
||||
let selectedJob: ComboBoxOption | undefined = $state(undefined);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import BottomInfo from '$lib/components/shared-components/side-bar/bottom-info.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { NavbarItem } from '@immich/ui';
|
||||
import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiSync } from '@mdi/js';
|
||||
import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiSync, mdiWrench } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<NavbarItem title={$t('users')} href={AppRoute.ADMIN_USERS} icon={mdiAccountMultipleOutline} />
|
||||
<NavbarItem title={$t('jobs')} href={AppRoute.ADMIN_JOBS} icon={mdiSync} />
|
||||
<NavbarItem title={$t('settings')} href={AppRoute.ADMIN_SETTINGS} icon={mdiCog} />
|
||||
<NavbarItem title={$t('admin.maintenance_settings')} href={AppRoute.ADMIN_MAINTENANCE_SETTINGS} icon={mdiWrench} />
|
||||
<NavbarItem title={$t('external_libraries')} href={AppRoute.ADMIN_LIBRARY_MANAGEMENT} icon={mdiBookshelf} />
|
||||
<NavbarItem title={$t('server_stats')} href={AppRoute.ADMIN_STATS} icon={mdiServer} />
|
||||
</div>
|
||||
|
||||
154
web/src/routes/admin/maintenance/+page.svelte
Normal file
154
web/src/routes/admin/maintenance/+page.svelte
Normal file
@@ -0,0 +1,154 @@
|
||||
<script lang="ts">
|
||||
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
||||
import ServerStatisticsCard from '$lib/components/server-statistics/ServerStatisticsCard.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { asyncTimeout } from '$lib/utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import {
|
||||
createJob,
|
||||
getIntegrityReportSummary,
|
||||
getQueuesLegacy,
|
||||
IntegrityReportType,
|
||||
MaintenanceAction,
|
||||
ManualJobName,
|
||||
setMaintenanceMode,
|
||||
type MaintenanceIntegrityReportSummaryResponseDto,
|
||||
type QueuesResponseLegacyDto,
|
||||
} from '@immich/sdk';
|
||||
import { Button, HStack, Text, toastManager } from '@immich/ui';
|
||||
import { mdiProgressWrench } from '@mdi/js';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
interface Props {
|
||||
data: PageData;
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
|
||||
let integrityReport: MaintenanceIntegrityReportSummaryResponseDto = $state(data.integrityReport);
|
||||
|
||||
const TYPES: IntegrityReportType[] = [
|
||||
IntegrityReportType.OrphanFile,
|
||||
IntegrityReportType.MissingFile,
|
||||
IntegrityReportType.ChecksumMismatch,
|
||||
];
|
||||
|
||||
async function switchToMaintenance() {
|
||||
try {
|
||||
await setMaintenanceMode({
|
||||
setMaintenanceModeDto: {
|
||||
action: MaintenanceAction.Start,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, $t('admin.maintenance_start_error'));
|
||||
}
|
||||
}
|
||||
|
||||
let jobs: QueuesResponseLegacyDto | undefined = $state();
|
||||
let expectingUpdate: boolean = $state(false);
|
||||
|
||||
async function runJob(reportType: IntegrityReportType, refreshOnly?: boolean) {
|
||||
let name: ManualJobName;
|
||||
switch (reportType) {
|
||||
case IntegrityReportType.OrphanFile: {
|
||||
name = refreshOnly ? ManualJobName.IntegrityOrphanFilesRefresh : ManualJobName.IntegrityOrphanFiles;
|
||||
break;
|
||||
}
|
||||
case IntegrityReportType.MissingFile: {
|
||||
name = refreshOnly ? ManualJobName.IntegrityMissingFilesRefresh : ManualJobName.IntegrityMissingFiles;
|
||||
break;
|
||||
}
|
||||
case IntegrityReportType.ChecksumMismatch: {
|
||||
name = refreshOnly ? ManualJobName.IntegrityChecksumMismatchRefresh : ManualJobName.IntegrityChecksumMismatch;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await createJob({ jobCreateDto: { name } });
|
||||
if (jobs) {
|
||||
expectingUpdate = true;
|
||||
jobs.backgroundTask.queueStatus.isActive = true;
|
||||
}
|
||||
toastManager.success($t('admin.job_created'));
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_submit_job'));
|
||||
}
|
||||
}
|
||||
|
||||
let running = true;
|
||||
|
||||
onMount(async () => {
|
||||
while (running) {
|
||||
jobs = await getQueuesLegacy();
|
||||
if (jobs.backgroundTask.queueStatus.isActive) {
|
||||
expectingUpdate = true;
|
||||
} else if (expectingUpdate) {
|
||||
integrityReport = await getIntegrityReportSummary();
|
||||
}
|
||||
await asyncTimeout(5000);
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
running = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}>
|
||||
{#snippet buttons()}
|
||||
<HStack gap={1}>
|
||||
<Button
|
||||
leadingIcon={mdiProgressWrench}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
onclick={switchToMaintenance}
|
||||
>
|
||||
<Text class="hidden md:block">{$t('admin.maintenance_start')}</Text>
|
||||
</Button>
|
||||
</HStack>
|
||||
{/snippet}
|
||||
|
||||
<section id="setting-content" class="flex place-content-center sm:mx-4">
|
||||
<section class="w-full pb-28 sm:w-5/6 md:w-[850px]">
|
||||
<p class="text-sm dark:text-immich-dark-fg uppercase">{$t('admin.maintenance_integrity_report')}</p>
|
||||
|
||||
<div class="mt-5 hidden justify-between lg:flex gap-4">
|
||||
{#each TYPES as reportType (reportType)}
|
||||
<ServerStatisticsCard
|
||||
title={$t(`admin.maintenance_integrity_${reportType}`)}
|
||||
value={integrityReport[reportType]}
|
||||
>
|
||||
{#snippet footer()}
|
||||
<HStack gap={1} class="justify-end">
|
||||
<Button
|
||||
onclick={() => runJob(reportType)}
|
||||
size="tiny"
|
||||
variant="ghost"
|
||||
class="self-end mt-1"
|
||||
disabled={jobs?.backgroundTask.queueStatus.isActive}>Check All</Button
|
||||
>
|
||||
<Button
|
||||
onclick={() => runJob(reportType, true)}
|
||||
size="tiny"
|
||||
variant="ghost"
|
||||
class="self-end mt-1"
|
||||
disabled={jobs?.backgroundTask.queueStatus.isActive}>Refresh</Button
|
||||
>
|
||||
<Button
|
||||
href={`${AppRoute.ADMIN_MAINTENANCE_INTEGRITY_REPORT + reportType}`}
|
||||
size="tiny"
|
||||
class="self-end mt-1">View</Button
|
||||
>
|
||||
</HStack>
|
||||
{/snippet}
|
||||
</ServerStatisticsCard>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</AdminPageLayout>
|
||||
17
web/src/routes/admin/maintenance/+page.ts
Normal file
17
web/src/routes/admin/maintenance/+page.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { authenticate } from '$lib/utils/auth';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import { getIntegrityReportSummary } from '@immich/sdk';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async ({ url }) => {
|
||||
await authenticate(url, { admin: true });
|
||||
const integrityReport = await getIntegrityReportSummary();
|
||||
const $t = await getFormatter();
|
||||
|
||||
return {
|
||||
integrityReport,
|
||||
meta: {
|
||||
title: $t('admin.maintenance_settings'),
|
||||
},
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
@@ -0,0 +1,177 @@
|
||||
<script lang="ts">
|
||||
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { createJob, deleteIntegrityReport, getBaseUrl, IntegrityReportType, ManualJobName } from '@immich/sdk';
|
||||
import {
|
||||
Button,
|
||||
HStack,
|
||||
IconButton,
|
||||
menuManager,
|
||||
modalManager,
|
||||
Text,
|
||||
toastManager,
|
||||
type ContextMenuBaseProps,
|
||||
type MenuItems,
|
||||
} from '@immich/ui';
|
||||
import { mdiDotsVertical, mdiDownload, mdiTrashCanOutline } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
interface Props {
|
||||
data: PageData;
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
|
||||
let deleting = new SvelteSet();
|
||||
let integrityReport = $state(data.integrityReport.items);
|
||||
|
||||
async function removeAll() {
|
||||
const confirm = await modalManager.showDialog({
|
||||
confirmText: $t('delete'),
|
||||
});
|
||||
|
||||
if (confirm) {
|
||||
let name: ManualJobName;
|
||||
switch (data.type) {
|
||||
case IntegrityReportType.OrphanFile: {
|
||||
name = ManualJobName.IntegrityOrphanFilesDeleteAll;
|
||||
break;
|
||||
}
|
||||
case IntegrityReportType.MissingFile: {
|
||||
name = ManualJobName.IntegrityMissingFilesDeleteAll;
|
||||
break;
|
||||
}
|
||||
case IntegrityReportType.ChecksumMismatch: {
|
||||
name = ManualJobName.IntegrityChecksumMismatchDeleteAll;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
deleting.add('all');
|
||||
await createJob({ jobCreateDto: { name } });
|
||||
toastManager.success($t('admin.job_created'));
|
||||
} catch (error) {
|
||||
handleError(error, 'Failed to delete file!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(id: string) {
|
||||
const confirm = await modalManager.showDialog({
|
||||
confirmText: $t('delete'),
|
||||
});
|
||||
|
||||
if (confirm) {
|
||||
try {
|
||||
deleting.add(id);
|
||||
await deleteIntegrityReport({
|
||||
id,
|
||||
});
|
||||
integrityReport = integrityReport.filter((report) => report.id !== id);
|
||||
} catch (error) {
|
||||
handleError(error, 'Failed to delete file!');
|
||||
} finally {
|
||||
deleting.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function download(reportId: string) {
|
||||
location.href = `${getBaseUrl()}/admin/maintenance/integrity/report/${reportId}/file`;
|
||||
}
|
||||
|
||||
const handleOpen = async (event: Event, props: Partial<ContextMenuBaseProps>, reportId: string) => {
|
||||
const items: MenuItems = [];
|
||||
|
||||
if (data.type === IntegrityReportType.OrphanFile || data.type === IntegrityReportType.ChecksumMismatch) {
|
||||
items.push({
|
||||
title: $t('download'),
|
||||
icon: mdiDownload,
|
||||
onAction() {
|
||||
void download(reportId);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await menuManager.show({
|
||||
...props,
|
||||
target: event.currentTarget as HTMLElement,
|
||||
items: [
|
||||
...items,
|
||||
{
|
||||
title: $t('delete'),
|
||||
icon: mdiTrashCanOutline,
|
||||
color: 'danger',
|
||||
onAction() {
|
||||
void remove(reportId);
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<AdminPageLayout
|
||||
breadcrumbs={[
|
||||
{ title: $t('admin.maintenance_settings'), href: AppRoute.ADMIN_MAINTENANCE_SETTINGS },
|
||||
{ title: $t('admin.maintenance_integrity_report') },
|
||||
{ title: data.meta.title },
|
||||
]}
|
||||
>
|
||||
{#snippet buttons()}
|
||||
<HStack gap={1}>
|
||||
<Button
|
||||
size="small"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
leadingIcon={mdiDownload}
|
||||
href={`${getBaseUrl()}/admin/maintenance/integrity/report/${data.type}/csv`}
|
||||
>
|
||||
<Text class="hidden md:block">Download CSV</Text>
|
||||
</Button>
|
||||
<Button size="small" variant="ghost" color="danger" leadingIcon={mdiTrashCanOutline} onclick={removeAll}>
|
||||
<Text class="hidden md:block">Delete All</Text>
|
||||
</Button>
|
||||
</HStack>
|
||||
{/snippet}
|
||||
|
||||
<section id="setting-content" class="flex place-content-center sm:mx-4">
|
||||
<section class="w-full pb-28 sm:w-5/6 md:w-[850px]">
|
||||
<table class="mt-5 w-full text-start">
|
||||
<thead
|
||||
class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray"
|
||||
>
|
||||
<tr class="flex w-full place-items-center">
|
||||
<th class="w-7/8 text-left px-2 text-sm font-medium">{$t('filename')}</th>
|
||||
<th class="w-1/8"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
class="block max-h-80 w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray dark:text-immich-dark-fg"
|
||||
>
|
||||
{#each integrityReport as { id, path } (id)}
|
||||
<tr
|
||||
class={`flex py-1 w-full place-items-center even:bg-subtle/20 odd:bg-subtle/80 ${deleting.has(id) || deleting.has('all') ? 'text-gray-500' : ''}`}
|
||||
>
|
||||
<td class="w-7/8 text-ellipsis text-left px-2 text-sm select-all">{path}</td>
|
||||
<td class="w-1/8 text-ellipsis text-right flex justify-end px-2">
|
||||
<IconButton
|
||||
color="secondary"
|
||||
icon={mdiDotsVertical}
|
||||
variant="ghost"
|
||||
onclick={(event: Event) => handleOpen(event, { position: 'top-right' }, id)}
|
||||
aria-label={$t('open')}
|
||||
disabled={deleting.has(id) || deleting.has('all')}
|
||||
/></td
|
||||
>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</section>
|
||||
</AdminPageLayout>
|
||||
@@ -0,0 +1,24 @@
|
||||
import { authenticate } from '$lib/utils/auth';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import { getIntegrityReport, IntegrityReportType } from '@immich/sdk';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async ({ params, url }) => {
|
||||
const type = params.type as IntegrityReportType;
|
||||
|
||||
await authenticate(url, { admin: true });
|
||||
const integrityReport = await getIntegrityReport({
|
||||
maintenanceGetIntegrityReportDto: {
|
||||
type,
|
||||
},
|
||||
});
|
||||
const $t = await getFormatter();
|
||||
|
||||
return {
|
||||
type,
|
||||
integrityReport,
|
||||
meta: {
|
||||
title: $t(`admin.maintenance_integrity_${type}`),
|
||||
},
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
Reference in New Issue
Block a user