mirror of
https://github.com/immich-app/immich.git
synced 2025-06-22 17:04:00 +02:00
chore: remove audit file report (#17994)
This commit is contained in:
parent
ebad6a008f
commit
094a41ac9a
22 changed files with 4 additions and 2100 deletions
e2e/src/api/specs
mobile/openapi
README.md
lib
open-api
server/src
controllers
dtos
services
web/src/routes/admin/repair
|
@ -1,43 +0,0 @@
|
||||||
import { deleteAssets, getAuditFiles, updateAsset, type LoginResponseDto } from '@immich/sdk';
|
|
||||||
import { asBearerAuth, utils } from 'src/utils';
|
|
||||||
import { beforeAll, describe, expect, it } from 'vitest';
|
|
||||||
|
|
||||||
describe('/audits', () => {
|
|
||||||
let admin: LoginResponseDto;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await utils.resetDatabase();
|
|
||||||
await utils.resetFilesystem();
|
|
||||||
|
|
||||||
admin = await utils.adminSetup();
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Enable these tests again once #7436 is resolved as these were flaky
|
|
||||||
describe.skip('GET :/file-report', () => {
|
|
||||||
it('excludes assets without issues from report', async () => {
|
|
||||||
const [trashedAsset, archivedAsset] = await Promise.all([
|
|
||||||
utils.createAsset(admin.accessToken),
|
|
||||||
utils.createAsset(admin.accessToken),
|
|
||||||
utils.createAsset(admin.accessToken),
|
|
||||||
]);
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
deleteAssets({ assetBulkDeleteDto: { ids: [trashedAsset.id] } }, { headers: asBearerAuth(admin.accessToken) }),
|
|
||||||
updateAsset(
|
|
||||||
{
|
|
||||||
id: archivedAsset.id,
|
|
||||||
updateAssetDto: { isArchived: true },
|
|
||||||
},
|
|
||||||
{ headers: asBearerAuth(admin.accessToken) },
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const body = await getAuditFiles({
|
|
||||||
headers: asBearerAuth(admin.accessToken),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(body.orphans).toHaveLength(0);
|
|
||||||
expect(body.extras).toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
10
mobile/openapi/README.md
generated
10
mobile/openapi/README.md
generated
|
@ -122,9 +122,6 @@ Class | Method | HTTP request | Description
|
||||||
*FacesApi* | [**deleteFace**](doc//FacesApi.md#deleteface) | **DELETE** /faces/{id} |
|
*FacesApi* | [**deleteFace**](doc//FacesApi.md#deleteface) | **DELETE** /faces/{id} |
|
||||||
*FacesApi* | [**getFaces**](doc//FacesApi.md#getfaces) | **GET** /faces |
|
*FacesApi* | [**getFaces**](doc//FacesApi.md#getfaces) | **GET** /faces |
|
||||||
*FacesApi* | [**reassignFacesById**](doc//FacesApi.md#reassignfacesbyid) | **PUT** /faces/{id} |
|
*FacesApi* | [**reassignFacesById**](doc//FacesApi.md#reassignfacesbyid) | **PUT** /faces/{id} |
|
||||||
*FileReportsApi* | [**fixAuditFiles**](doc//FileReportsApi.md#fixauditfiles) | **POST** /reports/fix |
|
|
||||||
*FileReportsApi* | [**getAuditFiles**](doc//FileReportsApi.md#getauditfiles) | **GET** /reports |
|
|
||||||
*FileReportsApi* | [**getFileChecksums**](doc//FileReportsApi.md#getfilechecksums) | **POST** /reports/checksum |
|
|
||||||
*JobsApi* | [**createJob**](doc//JobsApi.md#createjob) | **POST** /jobs |
|
*JobsApi* | [**createJob**](doc//JobsApi.md#createjob) | **POST** /jobs |
|
||||||
*JobsApi* | [**getAllJobsStatus**](doc//JobsApi.md#getalljobsstatus) | **GET** /jobs |
|
*JobsApi* | [**getAllJobsStatus**](doc//JobsApi.md#getalljobsstatus) | **GET** /jobs |
|
||||||
*JobsApi* | [**sendJobCommand**](doc//JobsApi.md#sendjobcommand) | **PUT** /jobs/{id} |
|
*JobsApi* | [**sendJobCommand**](doc//JobsApi.md#sendjobcommand) | **PUT** /jobs/{id} |
|
||||||
|
@ -332,11 +329,6 @@ Class | Method | HTTP request | Description
|
||||||
- [ExifResponseDto](doc//ExifResponseDto.md)
|
- [ExifResponseDto](doc//ExifResponseDto.md)
|
||||||
- [FaceDto](doc//FaceDto.md)
|
- [FaceDto](doc//FaceDto.md)
|
||||||
- [FacialRecognitionConfig](doc//FacialRecognitionConfig.md)
|
- [FacialRecognitionConfig](doc//FacialRecognitionConfig.md)
|
||||||
- [FileChecksumDto](doc//FileChecksumDto.md)
|
|
||||||
- [FileChecksumResponseDto](doc//FileChecksumResponseDto.md)
|
|
||||||
- [FileReportDto](doc//FileReportDto.md)
|
|
||||||
- [FileReportFixDto](doc//FileReportFixDto.md)
|
|
||||||
- [FileReportItemDto](doc//FileReportItemDto.md)
|
|
||||||
- [FoldersResponse](doc//FoldersResponse.md)
|
- [FoldersResponse](doc//FoldersResponse.md)
|
||||||
- [FoldersUpdate](doc//FoldersUpdate.md)
|
- [FoldersUpdate](doc//FoldersUpdate.md)
|
||||||
- [ImageFormat](doc//ImageFormat.md)
|
- [ImageFormat](doc//ImageFormat.md)
|
||||||
|
@ -381,8 +373,6 @@ Class | Method | HTTP request | Description
|
||||||
- [OnThisDayDto](doc//OnThisDayDto.md)
|
- [OnThisDayDto](doc//OnThisDayDto.md)
|
||||||
- [PartnerDirection](doc//PartnerDirection.md)
|
- [PartnerDirection](doc//PartnerDirection.md)
|
||||||
- [PartnerResponseDto](doc//PartnerResponseDto.md)
|
- [PartnerResponseDto](doc//PartnerResponseDto.md)
|
||||||
- [PathEntityType](doc//PathEntityType.md)
|
|
||||||
- [PathType](doc//PathType.md)
|
|
||||||
- [PeopleResponse](doc//PeopleResponse.md)
|
- [PeopleResponse](doc//PeopleResponse.md)
|
||||||
- [PeopleResponseDto](doc//PeopleResponseDto.md)
|
- [PeopleResponseDto](doc//PeopleResponseDto.md)
|
||||||
- [PeopleUpdate](doc//PeopleUpdate.md)
|
- [PeopleUpdate](doc//PeopleUpdate.md)
|
||||||
|
|
8
mobile/openapi/lib/api.dart
generated
8
mobile/openapi/lib/api.dart
generated
|
@ -39,7 +39,6 @@ part 'api/deprecated_api.dart';
|
||||||
part 'api/download_api.dart';
|
part 'api/download_api.dart';
|
||||||
part 'api/duplicates_api.dart';
|
part 'api/duplicates_api.dart';
|
||||||
part 'api/faces_api.dart';
|
part 'api/faces_api.dart';
|
||||||
part 'api/file_reports_api.dart';
|
|
||||||
part 'api/jobs_api.dart';
|
part 'api/jobs_api.dart';
|
||||||
part 'api/libraries_api.dart';
|
part 'api/libraries_api.dart';
|
||||||
part 'api/map_api.dart';
|
part 'api/map_api.dart';
|
||||||
|
@ -133,11 +132,6 @@ part 'model/email_notifications_update.dart';
|
||||||
part 'model/exif_response_dto.dart';
|
part 'model/exif_response_dto.dart';
|
||||||
part 'model/face_dto.dart';
|
part 'model/face_dto.dart';
|
||||||
part 'model/facial_recognition_config.dart';
|
part 'model/facial_recognition_config.dart';
|
||||||
part 'model/file_checksum_dto.dart';
|
|
||||||
part 'model/file_checksum_response_dto.dart';
|
|
||||||
part 'model/file_report_dto.dart';
|
|
||||||
part 'model/file_report_fix_dto.dart';
|
|
||||||
part 'model/file_report_item_dto.dart';
|
|
||||||
part 'model/folders_response.dart';
|
part 'model/folders_response.dart';
|
||||||
part 'model/folders_update.dart';
|
part 'model/folders_update.dart';
|
||||||
part 'model/image_format.dart';
|
part 'model/image_format.dart';
|
||||||
|
@ -182,8 +176,6 @@ part 'model/o_auth_token_endpoint_auth_method.dart';
|
||||||
part 'model/on_this_day_dto.dart';
|
part 'model/on_this_day_dto.dart';
|
||||||
part 'model/partner_direction.dart';
|
part 'model/partner_direction.dart';
|
||||||
part 'model/partner_response_dto.dart';
|
part 'model/partner_response_dto.dart';
|
||||||
part 'model/path_entity_type.dart';
|
|
||||||
part 'model/path_type.dart';
|
|
||||||
part 'model/people_response.dart';
|
part 'model/people_response.dart';
|
||||||
part 'model/people_response_dto.dart';
|
part 'model/people_response_dto.dart';
|
||||||
part 'model/people_update.dart';
|
part 'model/people_update.dart';
|
||||||
|
|
148
mobile/openapi/lib/api/file_reports_api.dart
generated
148
mobile/openapi/lib/api/file_reports_api.dart
generated
|
@ -1,148 +0,0 @@
|
||||||
//
|
|
||||||
// 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 FileReportsApi {
|
|
||||||
FileReportsApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
|
|
||||||
|
|
||||||
final ApiClient apiClient;
|
|
||||||
|
|
||||||
/// Performs an HTTP 'POST /reports/fix' operation and returns the [Response].
|
|
||||||
/// Parameters:
|
|
||||||
///
|
|
||||||
/// * [FileReportFixDto] fileReportFixDto (required):
|
|
||||||
Future<Response> fixAuditFilesWithHttpInfo(FileReportFixDto fileReportFixDto,) async {
|
|
||||||
// ignore: prefer_const_declarations
|
|
||||||
final apiPath = r'/reports/fix';
|
|
||||||
|
|
||||||
// ignore: prefer_final_locals
|
|
||||||
Object? postBody = fileReportFixDto;
|
|
||||||
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parameters:
|
|
||||||
///
|
|
||||||
/// * [FileReportFixDto] fileReportFixDto (required):
|
|
||||||
Future<void> fixAuditFiles(FileReportFixDto fileReportFixDto,) async {
|
|
||||||
final response = await fixAuditFilesWithHttpInfo(fileReportFixDto,);
|
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Performs an HTTP 'GET /reports' operation and returns the [Response].
|
|
||||||
Future<Response> getAuditFilesWithHttpInfo() async {
|
|
||||||
// ignore: prefer_const_declarations
|
|
||||||
final apiPath = r'/reports';
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<FileReportDto?> getAuditFiles() async {
|
|
||||||
final response = await getAuditFilesWithHttpInfo();
|
|
||||||
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), 'FileReportDto',) as FileReportDto;
|
|
||||||
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Performs an HTTP 'POST /reports/checksum' operation and returns the [Response].
|
|
||||||
/// Parameters:
|
|
||||||
///
|
|
||||||
/// * [FileChecksumDto] fileChecksumDto (required):
|
|
||||||
Future<Response> getFileChecksumsWithHttpInfo(FileChecksumDto fileChecksumDto,) async {
|
|
||||||
// ignore: prefer_const_declarations
|
|
||||||
final apiPath = r'/reports/checksum';
|
|
||||||
|
|
||||||
// ignore: prefer_final_locals
|
|
||||||
Object? postBody = fileChecksumDto;
|
|
||||||
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parameters:
|
|
||||||
///
|
|
||||||
/// * [FileChecksumDto] fileChecksumDto (required):
|
|
||||||
Future<List<FileChecksumResponseDto>?> getFileChecksums(FileChecksumDto fileChecksumDto,) async {
|
|
||||||
final response = await getFileChecksumsWithHttpInfo(fileChecksumDto,);
|
|
||||||
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) {
|
|
||||||
final responseBody = await _decodeBodyBytes(response);
|
|
||||||
return (await apiClient.deserializeAsync(responseBody, 'List<FileChecksumResponseDto>') as List)
|
|
||||||
.cast<FileChecksumResponseDto>()
|
|
||||||
.toList(growable: false);
|
|
||||||
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
14
mobile/openapi/lib/api_client.dart
generated
14
mobile/openapi/lib/api_client.dart
generated
|
@ -320,16 +320,6 @@ class ApiClient {
|
||||||
return FaceDto.fromJson(value);
|
return FaceDto.fromJson(value);
|
||||||
case 'FacialRecognitionConfig':
|
case 'FacialRecognitionConfig':
|
||||||
return FacialRecognitionConfig.fromJson(value);
|
return FacialRecognitionConfig.fromJson(value);
|
||||||
case 'FileChecksumDto':
|
|
||||||
return FileChecksumDto.fromJson(value);
|
|
||||||
case 'FileChecksumResponseDto':
|
|
||||||
return FileChecksumResponseDto.fromJson(value);
|
|
||||||
case 'FileReportDto':
|
|
||||||
return FileReportDto.fromJson(value);
|
|
||||||
case 'FileReportFixDto':
|
|
||||||
return FileReportFixDto.fromJson(value);
|
|
||||||
case 'FileReportItemDto':
|
|
||||||
return FileReportItemDto.fromJson(value);
|
|
||||||
case 'FoldersResponse':
|
case 'FoldersResponse':
|
||||||
return FoldersResponse.fromJson(value);
|
return FoldersResponse.fromJson(value);
|
||||||
case 'FoldersUpdate':
|
case 'FoldersUpdate':
|
||||||
|
@ -418,10 +408,6 @@ class ApiClient {
|
||||||
return PartnerDirectionTypeTransformer().decode(value);
|
return PartnerDirectionTypeTransformer().decode(value);
|
||||||
case 'PartnerResponseDto':
|
case 'PartnerResponseDto':
|
||||||
return PartnerResponseDto.fromJson(value);
|
return PartnerResponseDto.fromJson(value);
|
||||||
case 'PathEntityType':
|
|
||||||
return PathEntityTypeTypeTransformer().decode(value);
|
|
||||||
case 'PathType':
|
|
||||||
return PathTypeTypeTransformer().decode(value);
|
|
||||||
case 'PeopleResponse':
|
case 'PeopleResponse':
|
||||||
return PeopleResponse.fromJson(value);
|
return PeopleResponse.fromJson(value);
|
||||||
case 'PeopleResponseDto':
|
case 'PeopleResponseDto':
|
||||||
|
|
6
mobile/openapi/lib/api_helper.dart
generated
6
mobile/openapi/lib/api_helper.dart
generated
|
@ -112,12 +112,6 @@ String parameterToString(dynamic value) {
|
||||||
if (value is PartnerDirection) {
|
if (value is PartnerDirection) {
|
||||||
return PartnerDirectionTypeTransformer().encode(value).toString();
|
return PartnerDirectionTypeTransformer().encode(value).toString();
|
||||||
}
|
}
|
||||||
if (value is PathEntityType) {
|
|
||||||
return PathEntityTypeTypeTransformer().encode(value).toString();
|
|
||||||
}
|
|
||||||
if (value is PathType) {
|
|
||||||
return PathTypeTypeTransformer().encode(value).toString();
|
|
||||||
}
|
|
||||||
if (value is Permission) {
|
if (value is Permission) {
|
||||||
return PermissionTypeTransformer().encode(value).toString();
|
return PermissionTypeTransformer().encode(value).toString();
|
||||||
}
|
}
|
||||||
|
|
101
mobile/openapi/lib/model/file_checksum_dto.dart
generated
101
mobile/openapi/lib/model/file_checksum_dto.dart
generated
|
@ -1,101 +0,0 @@
|
||||||
//
|
|
||||||
// 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 FileChecksumDto {
|
|
||||||
/// Returns a new [FileChecksumDto] instance.
|
|
||||||
FileChecksumDto({
|
|
||||||
this.filenames = const [],
|
|
||||||
});
|
|
||||||
|
|
||||||
List<String> filenames;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) => identical(this, other) || other is FileChecksumDto &&
|
|
||||||
_deepEquality.equals(other.filenames, filenames);
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode =>
|
|
||||||
// ignore: unnecessary_parenthesis
|
|
||||||
(filenames.hashCode);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'FileChecksumDto[filenames=$filenames]';
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
final json = <String, dynamic>{};
|
|
||||||
json[r'filenames'] = this.filenames;
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a new [FileChecksumDto] instance and imports its values from
|
|
||||||
/// [value] if it's a [Map], null otherwise.
|
|
||||||
// ignore: prefer_constructors_over_static_methods
|
|
||||||
static FileChecksumDto? fromJson(dynamic value) {
|
|
||||||
upgradeDto(value, "FileChecksumDto");
|
|
||||||
if (value is Map) {
|
|
||||||
final json = value.cast<String, dynamic>();
|
|
||||||
|
|
||||||
return FileChecksumDto(
|
|
||||||
filenames: json[r'filenames'] is Iterable
|
|
||||||
? (json[r'filenames'] as Iterable).cast<String>().toList(growable: false)
|
|
||||||
: const [],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<FileChecksumDto> listFromJson(dynamic json, {bool growable = false,}) {
|
|
||||||
final result = <FileChecksumDto>[];
|
|
||||||
if (json is List && json.isNotEmpty) {
|
|
||||||
for (final row in json) {
|
|
||||||
final value = FileChecksumDto.fromJson(row);
|
|
||||||
if (value != null) {
|
|
||||||
result.add(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result.toList(growable: growable);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Map<String, FileChecksumDto> mapFromJson(dynamic json) {
|
|
||||||
final map = <String, FileChecksumDto>{};
|
|
||||||
if (json is Map && json.isNotEmpty) {
|
|
||||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
|
||||||
for (final entry in json.entries) {
|
|
||||||
final value = FileChecksumDto.fromJson(entry.value);
|
|
||||||
if (value != null) {
|
|
||||||
map[entry.key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
// maps a json object with a list of FileChecksumDto-objects as value to a dart map
|
|
||||||
static Map<String, List<FileChecksumDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
|
||||||
final map = <String, List<FileChecksumDto>>{};
|
|
||||||
if (json is Map && json.isNotEmpty) {
|
|
||||||
// ignore: parameter_assignments
|
|
||||||
json = json.cast<String, dynamic>();
|
|
||||||
for (final entry in json.entries) {
|
|
||||||
map[entry.key] = FileChecksumDto.listFromJson(entry.value, growable: growable,);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The list of required keys that must be present in a JSON.
|
|
||||||
static const requiredKeys = <String>{
|
|
||||||
'filenames',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
107
mobile/openapi/lib/model/file_checksum_response_dto.dart
generated
107
mobile/openapi/lib/model/file_checksum_response_dto.dart
generated
|
@ -1,107 +0,0 @@
|
||||||
//
|
|
||||||
// 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 FileChecksumResponseDto {
|
|
||||||
/// Returns a new [FileChecksumResponseDto] instance.
|
|
||||||
FileChecksumResponseDto({
|
|
||||||
required this.checksum,
|
|
||||||
required this.filename,
|
|
||||||
});
|
|
||||||
|
|
||||||
String checksum;
|
|
||||||
|
|
||||||
String filename;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) => identical(this, other) || other is FileChecksumResponseDto &&
|
|
||||||
other.checksum == checksum &&
|
|
||||||
other.filename == filename;
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode =>
|
|
||||||
// ignore: unnecessary_parenthesis
|
|
||||||
(checksum.hashCode) +
|
|
||||||
(filename.hashCode);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'FileChecksumResponseDto[checksum=$checksum, filename=$filename]';
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
final json = <String, dynamic>{};
|
|
||||||
json[r'checksum'] = this.checksum;
|
|
||||||
json[r'filename'] = this.filename;
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a new [FileChecksumResponseDto] instance and imports its values from
|
|
||||||
/// [value] if it's a [Map], null otherwise.
|
|
||||||
// ignore: prefer_constructors_over_static_methods
|
|
||||||
static FileChecksumResponseDto? fromJson(dynamic value) {
|
|
||||||
upgradeDto(value, "FileChecksumResponseDto");
|
|
||||||
if (value is Map) {
|
|
||||||
final json = value.cast<String, dynamic>();
|
|
||||||
|
|
||||||
return FileChecksumResponseDto(
|
|
||||||
checksum: mapValueOfType<String>(json, r'checksum')!,
|
|
||||||
filename: mapValueOfType<String>(json, r'filename')!,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<FileChecksumResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
|
||||||
final result = <FileChecksumResponseDto>[];
|
|
||||||
if (json is List && json.isNotEmpty) {
|
|
||||||
for (final row in json) {
|
|
||||||
final value = FileChecksumResponseDto.fromJson(row);
|
|
||||||
if (value != null) {
|
|
||||||
result.add(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result.toList(growable: growable);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Map<String, FileChecksumResponseDto> mapFromJson(dynamic json) {
|
|
||||||
final map = <String, FileChecksumResponseDto>{};
|
|
||||||
if (json is Map && json.isNotEmpty) {
|
|
||||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
|
||||||
for (final entry in json.entries) {
|
|
||||||
final value = FileChecksumResponseDto.fromJson(entry.value);
|
|
||||||
if (value != null) {
|
|
||||||
map[entry.key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
// maps a json object with a list of FileChecksumResponseDto-objects as value to a dart map
|
|
||||||
static Map<String, List<FileChecksumResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
|
||||||
final map = <String, List<FileChecksumResponseDto>>{};
|
|
||||||
if (json is Map && json.isNotEmpty) {
|
|
||||||
// ignore: parameter_assignments
|
|
||||||
json = json.cast<String, dynamic>();
|
|
||||||
for (final entry in json.entries) {
|
|
||||||
map[entry.key] = FileChecksumResponseDto.listFromJson(entry.value, growable: growable,);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The list of required keys that must be present in a JSON.
|
|
||||||
static const requiredKeys = <String>{
|
|
||||||
'checksum',
|
|
||||||
'filename',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
109
mobile/openapi/lib/model/file_report_dto.dart
generated
109
mobile/openapi/lib/model/file_report_dto.dart
generated
|
@ -1,109 +0,0 @@
|
||||||
//
|
|
||||||
// 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 FileReportDto {
|
|
||||||
/// Returns a new [FileReportDto] instance.
|
|
||||||
FileReportDto({
|
|
||||||
this.extras = const [],
|
|
||||||
this.orphans = const [],
|
|
||||||
});
|
|
||||||
|
|
||||||
List<String> extras;
|
|
||||||
|
|
||||||
List<FileReportItemDto> orphans;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) => identical(this, other) || other is FileReportDto &&
|
|
||||||
_deepEquality.equals(other.extras, extras) &&
|
|
||||||
_deepEquality.equals(other.orphans, orphans);
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode =>
|
|
||||||
// ignore: unnecessary_parenthesis
|
|
||||||
(extras.hashCode) +
|
|
||||||
(orphans.hashCode);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'FileReportDto[extras=$extras, orphans=$orphans]';
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
final json = <String, dynamic>{};
|
|
||||||
json[r'extras'] = this.extras;
|
|
||||||
json[r'orphans'] = this.orphans;
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a new [FileReportDto] instance and imports its values from
|
|
||||||
/// [value] if it's a [Map], null otherwise.
|
|
||||||
// ignore: prefer_constructors_over_static_methods
|
|
||||||
static FileReportDto? fromJson(dynamic value) {
|
|
||||||
upgradeDto(value, "FileReportDto");
|
|
||||||
if (value is Map) {
|
|
||||||
final json = value.cast<String, dynamic>();
|
|
||||||
|
|
||||||
return FileReportDto(
|
|
||||||
extras: json[r'extras'] is Iterable
|
|
||||||
? (json[r'extras'] as Iterable).cast<String>().toList(growable: false)
|
|
||||||
: const [],
|
|
||||||
orphans: FileReportItemDto.listFromJson(json[r'orphans']),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<FileReportDto> listFromJson(dynamic json, {bool growable = false,}) {
|
|
||||||
final result = <FileReportDto>[];
|
|
||||||
if (json is List && json.isNotEmpty) {
|
|
||||||
for (final row in json) {
|
|
||||||
final value = FileReportDto.fromJson(row);
|
|
||||||
if (value != null) {
|
|
||||||
result.add(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result.toList(growable: growable);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Map<String, FileReportDto> mapFromJson(dynamic json) {
|
|
||||||
final map = <String, FileReportDto>{};
|
|
||||||
if (json is Map && json.isNotEmpty) {
|
|
||||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
|
||||||
for (final entry in json.entries) {
|
|
||||||
final value = FileReportDto.fromJson(entry.value);
|
|
||||||
if (value != null) {
|
|
||||||
map[entry.key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
// maps a json object with a list of FileReportDto-objects as value to a dart map
|
|
||||||
static Map<String, List<FileReportDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
|
||||||
final map = <String, List<FileReportDto>>{};
|
|
||||||
if (json is Map && json.isNotEmpty) {
|
|
||||||
// ignore: parameter_assignments
|
|
||||||
json = json.cast<String, dynamic>();
|
|
||||||
for (final entry in json.entries) {
|
|
||||||
map[entry.key] = FileReportDto.listFromJson(entry.value, growable: growable,);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The list of required keys that must be present in a JSON.
|
|
||||||
static const requiredKeys = <String>{
|
|
||||||
'extras',
|
|
||||||
'orphans',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
99
mobile/openapi/lib/model/file_report_fix_dto.dart
generated
99
mobile/openapi/lib/model/file_report_fix_dto.dart
generated
|
@ -1,99 +0,0 @@
|
||||||
//
|
|
||||||
// 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 FileReportFixDto {
|
|
||||||
/// Returns a new [FileReportFixDto] instance.
|
|
||||||
FileReportFixDto({
|
|
||||||
this.items = const [],
|
|
||||||
});
|
|
||||||
|
|
||||||
List<FileReportItemDto> items;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) => identical(this, other) || other is FileReportFixDto &&
|
|
||||||
_deepEquality.equals(other.items, items);
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode =>
|
|
||||||
// ignore: unnecessary_parenthesis
|
|
||||||
(items.hashCode);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'FileReportFixDto[items=$items]';
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
final json = <String, dynamic>{};
|
|
||||||
json[r'items'] = this.items;
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a new [FileReportFixDto] instance and imports its values from
|
|
||||||
/// [value] if it's a [Map], null otherwise.
|
|
||||||
// ignore: prefer_constructors_over_static_methods
|
|
||||||
static FileReportFixDto? fromJson(dynamic value) {
|
|
||||||
upgradeDto(value, "FileReportFixDto");
|
|
||||||
if (value is Map) {
|
|
||||||
final json = value.cast<String, dynamic>();
|
|
||||||
|
|
||||||
return FileReportFixDto(
|
|
||||||
items: FileReportItemDto.listFromJson(json[r'items']),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<FileReportFixDto> listFromJson(dynamic json, {bool growable = false,}) {
|
|
||||||
final result = <FileReportFixDto>[];
|
|
||||||
if (json is List && json.isNotEmpty) {
|
|
||||||
for (final row in json) {
|
|
||||||
final value = FileReportFixDto.fromJson(row);
|
|
||||||
if (value != null) {
|
|
||||||
result.add(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result.toList(growable: growable);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Map<String, FileReportFixDto> mapFromJson(dynamic json) {
|
|
||||||
final map = <String, FileReportFixDto>{};
|
|
||||||
if (json is Map && json.isNotEmpty) {
|
|
||||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
|
||||||
for (final entry in json.entries) {
|
|
||||||
final value = FileReportFixDto.fromJson(entry.value);
|
|
||||||
if (value != null) {
|
|
||||||
map[entry.key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
// maps a json object with a list of FileReportFixDto-objects as value to a dart map
|
|
||||||
static Map<String, List<FileReportFixDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
|
||||||
final map = <String, List<FileReportFixDto>>{};
|
|
||||||
if (json is Map && json.isNotEmpty) {
|
|
||||||
// ignore: parameter_assignments
|
|
||||||
json = json.cast<String, dynamic>();
|
|
||||||
for (final entry in json.entries) {
|
|
||||||
map[entry.key] = FileReportFixDto.listFromJson(entry.value, growable: growable,);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The list of required keys that must be present in a JSON.
|
|
||||||
static const requiredKeys = <String>{
|
|
||||||
'items',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
140
mobile/openapi/lib/model/file_report_item_dto.dart
generated
140
mobile/openapi/lib/model/file_report_item_dto.dart
generated
|
@ -1,140 +0,0 @@
|
||||||
//
|
|
||||||
// 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 FileReportItemDto {
|
|
||||||
/// Returns a new [FileReportItemDto] instance.
|
|
||||||
FileReportItemDto({
|
|
||||||
this.checksum,
|
|
||||||
required this.entityId,
|
|
||||||
required this.entityType,
|
|
||||||
required this.pathType,
|
|
||||||
required this.pathValue,
|
|
||||||
});
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
|
||||||
/// does not include a default value (using the "default:" property), however, the generated
|
|
||||||
/// source code must fall back to having a nullable type.
|
|
||||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
|
||||||
///
|
|
||||||
String? checksum;
|
|
||||||
|
|
||||||
String entityId;
|
|
||||||
|
|
||||||
PathEntityType entityType;
|
|
||||||
|
|
||||||
PathType pathType;
|
|
||||||
|
|
||||||
String pathValue;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) => identical(this, other) || other is FileReportItemDto &&
|
|
||||||
other.checksum == checksum &&
|
|
||||||
other.entityId == entityId &&
|
|
||||||
other.entityType == entityType &&
|
|
||||||
other.pathType == pathType &&
|
|
||||||
other.pathValue == pathValue;
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode =>
|
|
||||||
// ignore: unnecessary_parenthesis
|
|
||||||
(checksum == null ? 0 : checksum!.hashCode) +
|
|
||||||
(entityId.hashCode) +
|
|
||||||
(entityType.hashCode) +
|
|
||||||
(pathType.hashCode) +
|
|
||||||
(pathValue.hashCode);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'FileReportItemDto[checksum=$checksum, entityId=$entityId, entityType=$entityType, pathType=$pathType, pathValue=$pathValue]';
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
final json = <String, dynamic>{};
|
|
||||||
if (this.checksum != null) {
|
|
||||||
json[r'checksum'] = this.checksum;
|
|
||||||
} else {
|
|
||||||
// json[r'checksum'] = null;
|
|
||||||
}
|
|
||||||
json[r'entityId'] = this.entityId;
|
|
||||||
json[r'entityType'] = this.entityType;
|
|
||||||
json[r'pathType'] = this.pathType;
|
|
||||||
json[r'pathValue'] = this.pathValue;
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a new [FileReportItemDto] instance and imports its values from
|
|
||||||
/// [value] if it's a [Map], null otherwise.
|
|
||||||
// ignore: prefer_constructors_over_static_methods
|
|
||||||
static FileReportItemDto? fromJson(dynamic value) {
|
|
||||||
upgradeDto(value, "FileReportItemDto");
|
|
||||||
if (value is Map) {
|
|
||||||
final json = value.cast<String, dynamic>();
|
|
||||||
|
|
||||||
return FileReportItemDto(
|
|
||||||
checksum: mapValueOfType<String>(json, r'checksum'),
|
|
||||||
entityId: mapValueOfType<String>(json, r'entityId')!,
|
|
||||||
entityType: PathEntityType.fromJson(json[r'entityType'])!,
|
|
||||||
pathType: PathType.fromJson(json[r'pathType'])!,
|
|
||||||
pathValue: mapValueOfType<String>(json, r'pathValue')!,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<FileReportItemDto> listFromJson(dynamic json, {bool growable = false,}) {
|
|
||||||
final result = <FileReportItemDto>[];
|
|
||||||
if (json is List && json.isNotEmpty) {
|
|
||||||
for (final row in json) {
|
|
||||||
final value = FileReportItemDto.fromJson(row);
|
|
||||||
if (value != null) {
|
|
||||||
result.add(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result.toList(growable: growable);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Map<String, FileReportItemDto> mapFromJson(dynamic json) {
|
|
||||||
final map = <String, FileReportItemDto>{};
|
|
||||||
if (json is Map && json.isNotEmpty) {
|
|
||||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
|
||||||
for (final entry in json.entries) {
|
|
||||||
final value = FileReportItemDto.fromJson(entry.value);
|
|
||||||
if (value != null) {
|
|
||||||
map[entry.key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
// maps a json object with a list of FileReportItemDto-objects as value to a dart map
|
|
||||||
static Map<String, List<FileReportItemDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
|
||||||
final map = <String, List<FileReportItemDto>>{};
|
|
||||||
if (json is Map && json.isNotEmpty) {
|
|
||||||
// ignore: parameter_assignments
|
|
||||||
json = json.cast<String, dynamic>();
|
|
||||||
for (final entry in json.entries) {
|
|
||||||
map[entry.key] = FileReportItemDto.listFromJson(entry.value, growable: growable,);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The list of required keys that must be present in a JSON.
|
|
||||||
static const requiredKeys = <String>{
|
|
||||||
'entityId',
|
|
||||||
'entityType',
|
|
||||||
'pathType',
|
|
||||||
'pathValue',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
88
mobile/openapi/lib/model/path_entity_type.dart
generated
88
mobile/openapi/lib/model/path_entity_type.dart
generated
|
@ -1,88 +0,0 @@
|
||||||
//
|
|
||||||
// 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 PathEntityType {
|
|
||||||
/// Instantiate a new enum with the provided [value].
|
|
||||||
const PathEntityType._(this.value);
|
|
||||||
|
|
||||||
/// The underlying value of this enum member.
|
|
||||||
final String value;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => value;
|
|
||||||
|
|
||||||
String toJson() => value;
|
|
||||||
|
|
||||||
static const asset = PathEntityType._(r'asset');
|
|
||||||
static const person = PathEntityType._(r'person');
|
|
||||||
static const user = PathEntityType._(r'user');
|
|
||||||
|
|
||||||
/// List of all possible values in this [enum][PathEntityType].
|
|
||||||
static const values = <PathEntityType>[
|
|
||||||
asset,
|
|
||||||
person,
|
|
||||||
user,
|
|
||||||
];
|
|
||||||
|
|
||||||
static PathEntityType? fromJson(dynamic value) => PathEntityTypeTypeTransformer().decode(value);
|
|
||||||
|
|
||||||
static List<PathEntityType> listFromJson(dynamic json, {bool growable = false,}) {
|
|
||||||
final result = <PathEntityType>[];
|
|
||||||
if (json is List && json.isNotEmpty) {
|
|
||||||
for (final row in json) {
|
|
||||||
final value = PathEntityType.fromJson(row);
|
|
||||||
if (value != null) {
|
|
||||||
result.add(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result.toList(growable: growable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Transformation class that can [encode] an instance of [PathEntityType] to String,
|
|
||||||
/// and [decode] dynamic data back to [PathEntityType].
|
|
||||||
class PathEntityTypeTypeTransformer {
|
|
||||||
factory PathEntityTypeTypeTransformer() => _instance ??= const PathEntityTypeTypeTransformer._();
|
|
||||||
|
|
||||||
const PathEntityTypeTypeTransformer._();
|
|
||||||
|
|
||||||
String encode(PathEntityType data) => data.value;
|
|
||||||
|
|
||||||
/// Decodes a [dynamic value][data] to a PathEntityType.
|
|
||||||
///
|
|
||||||
/// 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.
|
|
||||||
PathEntityType? decode(dynamic data, {bool allowNull = true}) {
|
|
||||||
if (data != null) {
|
|
||||||
switch (data) {
|
|
||||||
case r'asset': return PathEntityType.asset;
|
|
||||||
case r'person': return PathEntityType.person;
|
|
||||||
case r'user': return PathEntityType.user;
|
|
||||||
default:
|
|
||||||
if (!allowNull) {
|
|
||||||
throw ArgumentError('Unknown enum value to decode: $data');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Singleton [PathEntityTypeTypeTransformer] instance.
|
|
||||||
static PathEntityTypeTypeTransformer? _instance;
|
|
||||||
}
|
|
||||||
|
|
103
mobile/openapi/lib/model/path_type.dart
generated
103
mobile/openapi/lib/model/path_type.dart
generated
|
@ -1,103 +0,0 @@
|
||||||
//
|
|
||||||
// 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 PathType {
|
|
||||||
/// Instantiate a new enum with the provided [value].
|
|
||||||
const PathType._(this.value);
|
|
||||||
|
|
||||||
/// The underlying value of this enum member.
|
|
||||||
final String value;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => value;
|
|
||||||
|
|
||||||
String toJson() => value;
|
|
||||||
|
|
||||||
static const original = PathType._(r'original');
|
|
||||||
static const fullsize = PathType._(r'fullsize');
|
|
||||||
static const preview = PathType._(r'preview');
|
|
||||||
static const thumbnail = PathType._(r'thumbnail');
|
|
||||||
static const encodedVideo = PathType._(r'encoded_video');
|
|
||||||
static const sidecar = PathType._(r'sidecar');
|
|
||||||
static const face = PathType._(r'face');
|
|
||||||
static const profile = PathType._(r'profile');
|
|
||||||
|
|
||||||
/// List of all possible values in this [enum][PathType].
|
|
||||||
static const values = <PathType>[
|
|
||||||
original,
|
|
||||||
fullsize,
|
|
||||||
preview,
|
|
||||||
thumbnail,
|
|
||||||
encodedVideo,
|
|
||||||
sidecar,
|
|
||||||
face,
|
|
||||||
profile,
|
|
||||||
];
|
|
||||||
|
|
||||||
static PathType? fromJson(dynamic value) => PathTypeTypeTransformer().decode(value);
|
|
||||||
|
|
||||||
static List<PathType> listFromJson(dynamic json, {bool growable = false,}) {
|
|
||||||
final result = <PathType>[];
|
|
||||||
if (json is List && json.isNotEmpty) {
|
|
||||||
for (final row in json) {
|
|
||||||
final value = PathType.fromJson(row);
|
|
||||||
if (value != null) {
|
|
||||||
result.add(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result.toList(growable: growable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Transformation class that can [encode] an instance of [PathType] to String,
|
|
||||||
/// and [decode] dynamic data back to [PathType].
|
|
||||||
class PathTypeTypeTransformer {
|
|
||||||
factory PathTypeTypeTransformer() => _instance ??= const PathTypeTypeTransformer._();
|
|
||||||
|
|
||||||
const PathTypeTypeTransformer._();
|
|
||||||
|
|
||||||
String encode(PathType data) => data.value;
|
|
||||||
|
|
||||||
/// Decodes a [dynamic value][data] to a PathType.
|
|
||||||
///
|
|
||||||
/// 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.
|
|
||||||
PathType? decode(dynamic data, {bool allowNull = true}) {
|
|
||||||
if (data != null) {
|
|
||||||
switch (data) {
|
|
||||||
case r'original': return PathType.original;
|
|
||||||
case r'fullsize': return PathType.fullsize;
|
|
||||||
case r'preview': return PathType.preview;
|
|
||||||
case r'thumbnail': return PathType.thumbnail;
|
|
||||||
case r'encoded_video': return PathType.encodedVideo;
|
|
||||||
case r'sidecar': return PathType.sidecar;
|
|
||||||
case r'face': return PathType.face;
|
|
||||||
case r'profile': return PathType.profile;
|
|
||||||
default:
|
|
||||||
if (!allowNull) {
|
|
||||||
throw ArgumentError('Unknown enum value to decode: $data');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Singleton [PathTypeTypeTransformer] instance.
|
|
||||||
static PathTypeTypeTransformer? _instance;
|
|
||||||
}
|
|
||||||
|
|
|
@ -4651,118 +4651,6 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/reports": {
|
|
||||||
"get": {
|
|
||||||
"operationId": "getAuditFiles",
|
|
||||||
"parameters": [],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/FileReportDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearer": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"api_key": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"File Reports"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/reports/checksum": {
|
|
||||||
"post": {
|
|
||||||
"operationId": "getFileChecksums",
|
|
||||||
"parameters": [],
|
|
||||||
"requestBody": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/FileChecksumDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"201": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/FileChecksumResponseDto"
|
|
||||||
},
|
|
||||||
"type": "array"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearer": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"api_key": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"File Reports"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/reports/fix": {
|
|
||||||
"post": {
|
|
||||||
"operationId": "fixAuditFiles",
|
|
||||||
"parameters": [],
|
|
||||||
"requestBody": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/FileReportFixDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"201": {
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearer": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"api_key": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"File Reports"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/search/cities": {
|
"/search/cities": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getAssetsByCity",
|
"operationId": "getAssetsByCity",
|
||||||
|
@ -9749,105 +9637,6 @@
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"FileChecksumDto": {
|
|
||||||
"properties": {
|
|
||||||
"filenames": {
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"type": "array"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"filenames"
|
|
||||||
],
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"FileChecksumResponseDto": {
|
|
||||||
"properties": {
|
|
||||||
"checksum": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"filename": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"checksum",
|
|
||||||
"filename"
|
|
||||||
],
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"FileReportDto": {
|
|
||||||
"properties": {
|
|
||||||
"extras": {
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"type": "array"
|
|
||||||
},
|
|
||||||
"orphans": {
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/FileReportItemDto"
|
|
||||||
},
|
|
||||||
"type": "array"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"extras",
|
|
||||||
"orphans"
|
|
||||||
],
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"FileReportFixDto": {
|
|
||||||
"properties": {
|
|
||||||
"items": {
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/FileReportItemDto"
|
|
||||||
},
|
|
||||||
"type": "array"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"items"
|
|
||||||
],
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"FileReportItemDto": {
|
|
||||||
"properties": {
|
|
||||||
"checksum": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"entityId": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"entityType": {
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/PathEntityType"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"pathType": {
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/PathType"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"pathValue": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"entityId",
|
|
||||||
"entityType",
|
|
||||||
"pathType",
|
|
||||||
"pathValue"
|
|
||||||
],
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"FoldersResponse": {
|
"FoldersResponse": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"enabled": {
|
"enabled": {
|
||||||
|
@ -10889,27 +10678,6 @@
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"PathEntityType": {
|
|
||||||
"enum": [
|
|
||||||
"asset",
|
|
||||||
"person",
|
|
||||||
"user"
|
|
||||||
],
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"PathType": {
|
|
||||||
"enum": [
|
|
||||||
"original",
|
|
||||||
"fullsize",
|
|
||||||
"preview",
|
|
||||||
"thumbnail",
|
|
||||||
"encoded_video",
|
|
||||||
"sidecar",
|
|
||||||
"face",
|
|
||||||
"profile"
|
|
||||||
],
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"PeopleResponse": {
|
"PeopleResponse": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"enabled": {
|
"enabled": {
|
||||||
|
|
|
@ -800,27 +800,6 @@ export type AssetFaceUpdateDto = {
|
||||||
export type PersonStatisticsResponseDto = {
|
export type PersonStatisticsResponseDto = {
|
||||||
assets: number;
|
assets: number;
|
||||||
};
|
};
|
||||||
export type FileReportItemDto = {
|
|
||||||
checksum?: string;
|
|
||||||
entityId: string;
|
|
||||||
entityType: PathEntityType;
|
|
||||||
pathType: PathType;
|
|
||||||
pathValue: string;
|
|
||||||
};
|
|
||||||
export type FileReportDto = {
|
|
||||||
extras: string[];
|
|
||||||
orphans: FileReportItemDto[];
|
|
||||||
};
|
|
||||||
export type FileChecksumDto = {
|
|
||||||
filenames: string[];
|
|
||||||
};
|
|
||||||
export type FileChecksumResponseDto = {
|
|
||||||
checksum: string;
|
|
||||||
filename: string;
|
|
||||||
};
|
|
||||||
export type FileReportFixDto = {
|
|
||||||
items: FileReportItemDto[];
|
|
||||||
};
|
|
||||||
export type SearchExploreItem = {
|
export type SearchExploreItem = {
|
||||||
data: AssetResponseDto;
|
data: AssetResponseDto;
|
||||||
value: string;
|
value: string;
|
||||||
|
@ -2663,35 +2642,6 @@ export function getPersonThumbnail({ id }: {
|
||||||
...opts
|
...opts
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
export function getAuditFiles(opts?: Oazapfts.RequestOpts) {
|
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
|
||||||
status: 200;
|
|
||||||
data: FileReportDto;
|
|
||||||
}>("/reports", {
|
|
||||||
...opts
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
export function getFileChecksums({ fileChecksumDto }: {
|
|
||||||
fileChecksumDto: FileChecksumDto;
|
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
|
||||||
status: 201;
|
|
||||||
data: FileChecksumResponseDto[];
|
|
||||||
}>("/reports/checksum", oazapfts.json({
|
|
||||||
...opts,
|
|
||||||
method: "POST",
|
|
||||||
body: fileChecksumDto
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
export function fixAuditFiles({ fileReportFixDto }: {
|
|
||||||
fileReportFixDto: FileReportFixDto;
|
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
|
||||||
return oazapfts.ok(oazapfts.fetchText("/reports/fix", oazapfts.json({
|
|
||||||
...opts,
|
|
||||||
method: "POST",
|
|
||||||
body: fileReportFixDto
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
export function getAssetsByCity(opts?: Oazapfts.RequestOpts) {
|
export function getAssetsByCity(opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
|
@ -3751,21 +3701,6 @@ export enum PartnerDirection {
|
||||||
SharedBy = "shared-by",
|
SharedBy = "shared-by",
|
||||||
SharedWith = "shared-with"
|
SharedWith = "shared-with"
|
||||||
}
|
}
|
||||||
export enum PathEntityType {
|
|
||||||
Asset = "asset",
|
|
||||||
Person = "person",
|
|
||||||
User = "user"
|
|
||||||
}
|
|
||||||
export enum PathType {
|
|
||||||
Original = "original",
|
|
||||||
Fullsize = "fullsize",
|
|
||||||
Preview = "preview",
|
|
||||||
Thumbnail = "thumbnail",
|
|
||||||
EncodedVideo = "encoded_video",
|
|
||||||
Sidecar = "sidecar",
|
|
||||||
Face = "face",
|
|
||||||
Profile = "profile"
|
|
||||||
}
|
|
||||||
export enum SearchSuggestionType {
|
export enum SearchSuggestionType {
|
||||||
Country = "country",
|
Country = "country",
|
||||||
State = "state",
|
State = "state",
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
import { Body, Controller, Get, Post } from '@nestjs/common';
|
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
|
||||||
import { FileChecksumDto, FileChecksumResponseDto, FileReportDto, FileReportFixDto } from 'src/dtos/audit.dto';
|
|
||||||
import { Authenticated } from 'src/middleware/auth.guard';
|
|
||||||
import { AuditService } from 'src/services/audit.service';
|
|
||||||
|
|
||||||
@ApiTags('File Reports')
|
|
||||||
@Controller('reports')
|
|
||||||
export class ReportController {
|
|
||||||
constructor(private service: AuditService) {}
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
@Authenticated({ admin: true })
|
|
||||||
getAuditFiles(): Promise<FileReportDto> {
|
|
||||||
return this.service.getFileReport();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('checksum')
|
|
||||||
@Authenticated({ admin: true })
|
|
||||||
getFileChecksums(@Body() dto: FileChecksumDto): Promise<FileChecksumResponseDto[]> {
|
|
||||||
return this.service.getChecksums(dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('fix')
|
|
||||||
@Authenticated({ admin: true })
|
|
||||||
fixAuditFiles(@Body() dto: FileReportFixDto): Promise<void> {
|
|
||||||
return this.service.fixItems(dto.items);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -8,7 +8,6 @@ import { AuthController } from 'src/controllers/auth.controller';
|
||||||
import { DownloadController } from 'src/controllers/download.controller';
|
import { DownloadController } from 'src/controllers/download.controller';
|
||||||
import { DuplicateController } from 'src/controllers/duplicate.controller';
|
import { DuplicateController } from 'src/controllers/duplicate.controller';
|
||||||
import { FaceController } from 'src/controllers/face.controller';
|
import { FaceController } from 'src/controllers/face.controller';
|
||||||
import { ReportController } from 'src/controllers/file-report.controller';
|
|
||||||
import { JobController } from 'src/controllers/job.controller';
|
import { JobController } from 'src/controllers/job.controller';
|
||||||
import { LibraryController } from 'src/controllers/library.controller';
|
import { LibraryController } from 'src/controllers/library.controller';
|
||||||
import { MapController } from 'src/controllers/map.controller';
|
import { MapController } from 'src/controllers/map.controller';
|
||||||
|
@ -53,7 +52,6 @@ export const controllers = [
|
||||||
OAuthController,
|
OAuthController,
|
||||||
PartnerController,
|
PartnerController,
|
||||||
PersonController,
|
PersonController,
|
||||||
ReportController,
|
|
||||||
SearchController,
|
SearchController,
|
||||||
ServerController,
|
ServerController,
|
||||||
SessionController,
|
SessionController,
|
||||||
|
|
|
@ -1,73 +0,0 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
|
||||||
import { Type } from 'class-transformer';
|
|
||||||
import { IsArray, IsEnum, IsString, IsUUID, ValidateNested } from 'class-validator';
|
|
||||||
import { AssetPathType, EntityType, PathType, PersonPathType, UserPathType } from 'src/enum';
|
|
||||||
import { Optional, ValidateDate, ValidateUUID } from 'src/validation';
|
|
||||||
|
|
||||||
const PathEnum = Object.values({ ...AssetPathType, ...PersonPathType, ...UserPathType });
|
|
||||||
|
|
||||||
export class AuditDeletesDto {
|
|
||||||
@ValidateDate()
|
|
||||||
after!: Date;
|
|
||||||
|
|
||||||
@ApiProperty({ enum: EntityType, enumName: 'EntityType' })
|
|
||||||
@IsEnum(EntityType)
|
|
||||||
entityType!: EntityType;
|
|
||||||
|
|
||||||
@Optional()
|
|
||||||
@IsUUID('4')
|
|
||||||
@ApiProperty({ format: 'uuid' })
|
|
||||||
userId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum PathEntityType {
|
|
||||||
ASSET = 'asset',
|
|
||||||
PERSON = 'person',
|
|
||||||
USER = 'user',
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AuditDeletesResponseDto {
|
|
||||||
needsFullSync!: boolean;
|
|
||||||
ids!: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class FileReportDto {
|
|
||||||
orphans!: FileReportItemDto[];
|
|
||||||
extras!: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class FileChecksumDto {
|
|
||||||
@IsString({ each: true })
|
|
||||||
filenames!: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class FileChecksumResponseDto {
|
|
||||||
filename!: string;
|
|
||||||
checksum!: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class FileReportFixDto {
|
|
||||||
@IsArray()
|
|
||||||
@ValidateNested({ each: true })
|
|
||||||
@Type(() => FileReportItemDto)
|
|
||||||
items!: FileReportItemDto[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// used both as request and response dto
|
|
||||||
export class FileReportItemDto {
|
|
||||||
@ValidateUUID()
|
|
||||||
entityId!: string;
|
|
||||||
|
|
||||||
@ApiProperty({ enumName: 'PathEntityType', enum: PathEntityType })
|
|
||||||
@IsEnum(PathEntityType)
|
|
||||||
entityType!: PathEntityType;
|
|
||||||
|
|
||||||
@ApiProperty({ enumName: 'PathType', enum: PathEnum })
|
|
||||||
@IsEnum(PathEnum)
|
|
||||||
pathType!: PathType;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
pathValue!: string;
|
|
||||||
|
|
||||||
checksum?: string;
|
|
||||||
}
|
|
|
@ -1,6 +1,4 @@
|
||||||
import { BadRequestException } from '@nestjs/common';
|
import { JobStatus } from 'src/enum';
|
||||||
import { FileReportItemDto } from 'src/dtos/audit.dto';
|
|
||||||
import { AssetFileType, AssetPathType, JobStatus, PersonPathType, UserPathType } from 'src/enum';
|
|
||||||
import { AuditService } from 'src/services/audit.service';
|
import { AuditService } from 'src/services/audit.service';
|
||||||
import { newTestService, ServiceMocks } from 'test/utils';
|
import { newTestService, ServiceMocks } from 'test/utils';
|
||||||
|
|
||||||
|
@ -25,148 +23,4 @@ describe(AuditService.name, () => {
|
||||||
expect(mocks.audit.removeBefore).toHaveBeenCalledWith(expect.any(Date));
|
expect(mocks.audit.removeBefore).toHaveBeenCalledWith(expect.any(Date));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getChecksums', () => {
|
|
||||||
it('should fail if the file is not in the immich path', async () => {
|
|
||||||
await expect(sut.getChecksums({ filenames: ['foo/bar'] })).rejects.toBeInstanceOf(BadRequestException);
|
|
||||||
|
|
||||||
expect(mocks.crypto.hashFile).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get checksum for valid file', async () => {
|
|
||||||
await expect(sut.getChecksums({ filenames: ['./upload/my-file.jpg'] })).resolves.toEqual([
|
|
||||||
{ filename: './upload/my-file.jpg', checksum: expect.any(String) },
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(mocks.crypto.hashFile).toHaveBeenCalledWith('./upload/my-file.jpg');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('fixItems', () => {
|
|
||||||
it('should fail if the file is not in the immich path', async () => {
|
|
||||||
await expect(
|
|
||||||
sut.fixItems([
|
|
||||||
{ entityId: 'my-id', pathType: AssetPathType.ORIGINAL, pathValue: 'foo/bar' } as FileReportItemDto,
|
|
||||||
]),
|
|
||||||
).rejects.toBeInstanceOf(BadRequestException);
|
|
||||||
|
|
||||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
|
||||||
expect(mocks.asset.upsertFile).not.toHaveBeenCalled();
|
|
||||||
expect(mocks.person.update).not.toHaveBeenCalled();
|
|
||||||
expect(mocks.user.update).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update encoded video path', async () => {
|
|
||||||
await sut.fixItems([
|
|
||||||
{
|
|
||||||
entityId: 'my-id',
|
|
||||||
pathType: AssetPathType.ENCODED_VIDEO,
|
|
||||||
pathValue: './upload/my-video.mp4',
|
|
||||||
} as FileReportItemDto,
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'my-id', encodedVideoPath: './upload/my-video.mp4' });
|
|
||||||
expect(mocks.asset.upsertFile).not.toHaveBeenCalled();
|
|
||||||
expect(mocks.person.update).not.toHaveBeenCalled();
|
|
||||||
expect(mocks.user.update).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update preview path', async () => {
|
|
||||||
await sut.fixItems([
|
|
||||||
{
|
|
||||||
entityId: 'my-id',
|
|
||||||
pathType: AssetPathType.PREVIEW,
|
|
||||||
pathValue: './upload/my-preview.png',
|
|
||||||
} as FileReportItemDto,
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(mocks.asset.upsertFile).toHaveBeenCalledWith({
|
|
||||||
assetId: 'my-id',
|
|
||||||
type: AssetFileType.PREVIEW,
|
|
||||||
path: './upload/my-preview.png',
|
|
||||||
});
|
|
||||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
|
||||||
expect(mocks.person.update).not.toHaveBeenCalled();
|
|
||||||
expect(mocks.user.update).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update thumbnail path', async () => {
|
|
||||||
await sut.fixItems([
|
|
||||||
{
|
|
||||||
entityId: 'my-id',
|
|
||||||
pathType: AssetPathType.THUMBNAIL,
|
|
||||||
pathValue: './upload/my-thumbnail.webp',
|
|
||||||
} as FileReportItemDto,
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(mocks.asset.upsertFile).toHaveBeenCalledWith({
|
|
||||||
assetId: 'my-id',
|
|
||||||
type: AssetFileType.THUMBNAIL,
|
|
||||||
path: './upload/my-thumbnail.webp',
|
|
||||||
});
|
|
||||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
|
||||||
expect(mocks.person.update).not.toHaveBeenCalled();
|
|
||||||
expect(mocks.user.update).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update original path', async () => {
|
|
||||||
await sut.fixItems([
|
|
||||||
{
|
|
||||||
entityId: 'my-id',
|
|
||||||
pathType: AssetPathType.ORIGINAL,
|
|
||||||
pathValue: './upload/my-original.png',
|
|
||||||
} as FileReportItemDto,
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'my-id', originalPath: './upload/my-original.png' });
|
|
||||||
expect(mocks.asset.upsertFile).not.toHaveBeenCalled();
|
|
||||||
expect(mocks.person.update).not.toHaveBeenCalled();
|
|
||||||
expect(mocks.user.update).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update sidecar path', async () => {
|
|
||||||
await sut.fixItems([
|
|
||||||
{
|
|
||||||
entityId: 'my-id',
|
|
||||||
pathType: AssetPathType.SIDECAR,
|
|
||||||
pathValue: './upload/my-sidecar.xmp',
|
|
||||||
} as FileReportItemDto,
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'my-id', sidecarPath: './upload/my-sidecar.xmp' });
|
|
||||||
expect(mocks.asset.upsertFile).not.toHaveBeenCalled();
|
|
||||||
expect(mocks.person.update).not.toHaveBeenCalled();
|
|
||||||
expect(mocks.user.update).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update face path', async () => {
|
|
||||||
await sut.fixItems([
|
|
||||||
{
|
|
||||||
entityId: 'my-id',
|
|
||||||
pathType: PersonPathType.FACE,
|
|
||||||
pathValue: './upload/my-face.jpg',
|
|
||||||
} as FileReportItemDto,
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(mocks.person.update).toHaveBeenCalledWith({ id: 'my-id', thumbnailPath: './upload/my-face.jpg' });
|
|
||||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
|
||||||
expect(mocks.asset.upsertFile).not.toHaveBeenCalled();
|
|
||||||
expect(mocks.user.update).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update profile path', async () => {
|
|
||||||
await sut.fixItems([
|
|
||||||
{
|
|
||||||
entityId: 'my-id',
|
|
||||||
pathType: UserPathType.PROFILE,
|
|
||||||
pathValue: './upload/my-profile-pic.jpg',
|
|
||||||
} as FileReportItemDto,
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(mocks.user.update).toHaveBeenCalledWith('my-id', { profileImagePath: './upload/my-profile-pic.jpg' });
|
|
||||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
|
||||||
expect(mocks.asset.upsertFile).not.toHaveBeenCalled();
|
|
||||||
expect(mocks.person.update).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,23 +1,9 @@
|
||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { resolve } from 'node:path';
|
import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
|
||||||
import { AUDIT_LOG_MAX_DURATION, JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
|
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
|
||||||
import { OnJob } from 'src/decorators';
|
import { OnJob } from 'src/decorators';
|
||||||
import { FileChecksumDto, FileChecksumResponseDto, FileReportItemDto, PathEntityType } from 'src/dtos/audit.dto';
|
import { JobName, JobStatus, QueueName } from 'src/enum';
|
||||||
import {
|
|
||||||
AssetFileType,
|
|
||||||
AssetPathType,
|
|
||||||
JobName,
|
|
||||||
JobStatus,
|
|
||||||
PersonPathType,
|
|
||||||
QueueName,
|
|
||||||
StorageFolder,
|
|
||||||
UserPathType,
|
|
||||||
} from 'src/enum';
|
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { getAssetFiles } from 'src/utils/asset.util';
|
|
||||||
import { usePagination } from 'src/utils/pagination';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuditService extends BaseService {
|
export class AuditService extends BaseService {
|
||||||
|
@ -26,187 +12,4 @@ export class AuditService extends BaseService {
|
||||||
await this.auditRepository.removeBefore(DateTime.now().minus(AUDIT_LOG_MAX_DURATION).toJSDate());
|
await this.auditRepository.removeBefore(DateTime.now().minus(AUDIT_LOG_MAX_DURATION).toJSDate());
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getChecksums(dto: FileChecksumDto) {
|
|
||||||
const results: FileChecksumResponseDto[] = [];
|
|
||||||
for (const filename of dto.filenames) {
|
|
||||||
if (!StorageCore.isImmichPath(filename)) {
|
|
||||||
throw new BadRequestException(
|
|
||||||
`Could not get the checksum of ${filename} because the file isn't accessible by Immich`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const checksum = await this.cryptoRepository.hashFile(filename);
|
|
||||||
results.push({ filename, checksum: checksum.toString('base64') });
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fixItems(items: FileReportItemDto[]) {
|
|
||||||
for (const { entityId: id, pathType, pathValue } of items) {
|
|
||||||
if (!StorageCore.isImmichPath(pathValue)) {
|
|
||||||
throw new BadRequestException(
|
|
||||||
`Could not fix item ${id} with path ${pathValue} because the file isn't accessible by Immich`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (pathType) {
|
|
||||||
case AssetPathType.ENCODED_VIDEO: {
|
|
||||||
await this.assetRepository.update({ id, encodedVideoPath: pathValue });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case AssetPathType.PREVIEW: {
|
|
||||||
await this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.PREVIEW, path: pathValue });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case AssetPathType.THUMBNAIL: {
|
|
||||||
await this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.THUMBNAIL, path: pathValue });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case AssetPathType.ORIGINAL: {
|
|
||||||
await this.assetRepository.update({ id, originalPath: pathValue });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case AssetPathType.SIDECAR: {
|
|
||||||
await this.assetRepository.update({ id, sidecarPath: pathValue });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case PersonPathType.FACE: {
|
|
||||||
await this.personRepository.update({ id, thumbnailPath: pathValue });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case UserPathType.PROFILE: {
|
|
||||||
await this.userRepository.update(id, { profileImagePath: pathValue });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fullPath(filename: string) {
|
|
||||||
return resolve(filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getFileReport() {
|
|
||||||
const hasFile = (items: Set<string>, filename: string) => items.has(filename) || items.has(this.fullPath(filename));
|
|
||||||
const crawl = async (folder: StorageFolder) =>
|
|
||||||
new Set(
|
|
||||||
await this.storageRepository.crawl({
|
|
||||||
includeHidden: true,
|
|
||||||
pathsToCrawl: [StorageCore.getBaseFolder(folder)],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const uploadFiles = await crawl(StorageFolder.UPLOAD);
|
|
||||||
const libraryFiles = await crawl(StorageFolder.LIBRARY);
|
|
||||||
const thumbFiles = await crawl(StorageFolder.THUMBNAILS);
|
|
||||||
const videoFiles = await crawl(StorageFolder.ENCODED_VIDEO);
|
|
||||||
const profileFiles = await crawl(StorageFolder.PROFILE);
|
|
||||||
const allFiles = new Set<string>();
|
|
||||||
for (const list of [libraryFiles, thumbFiles, videoFiles, profileFiles, uploadFiles]) {
|
|
||||||
for (const item of list) {
|
|
||||||
allFiles.add(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const track = (filename: string | null | undefined) => {
|
|
||||||
if (!filename) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
allFiles.delete(filename);
|
|
||||||
allFiles.delete(this.fullPath(filename));
|
|
||||||
};
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Found ${libraryFiles.size} original files, ${thumbFiles.size} thumbnails, ${videoFiles.size} encoded videos, ${profileFiles.size} profile files`,
|
|
||||||
);
|
|
||||||
const pagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (options) =>
|
|
||||||
this.assetRepository.getAll(options, { withDeleted: true, withArchived: true }),
|
|
||||||
);
|
|
||||||
|
|
||||||
let assetCount = 0;
|
|
||||||
|
|
||||||
const orphans: FileReportItemDto[] = [];
|
|
||||||
for await (const assets of pagination) {
|
|
||||||
assetCount += assets.length;
|
|
||||||
for (const { id, files, originalPath, encodedVideoPath, isExternal, checksum } of assets) {
|
|
||||||
const { fullsizeFile, previewFile, thumbnailFile } = getAssetFiles(files);
|
|
||||||
for (const file of [
|
|
||||||
originalPath,
|
|
||||||
fullsizeFile?.path,
|
|
||||||
previewFile?.path,
|
|
||||||
encodedVideoPath,
|
|
||||||
thumbnailFile?.path,
|
|
||||||
]) {
|
|
||||||
track(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
const entity = { entityId: id, entityType: PathEntityType.ASSET, checksum: checksum.toString('base64') };
|
|
||||||
if (
|
|
||||||
originalPath &&
|
|
||||||
!hasFile(libraryFiles, originalPath) &&
|
|
||||||
!hasFile(uploadFiles, originalPath) &&
|
|
||||||
// Android motion assets
|
|
||||||
!hasFile(videoFiles, originalPath) &&
|
|
||||||
// ignore external library assets
|
|
||||||
!isExternal
|
|
||||||
) {
|
|
||||||
orphans.push({ ...entity, pathType: AssetPathType.ORIGINAL, pathValue: originalPath });
|
|
||||||
}
|
|
||||||
if (previewFile && !hasFile(thumbFiles, previewFile.path)) {
|
|
||||||
orphans.push({ ...entity, pathType: AssetPathType.PREVIEW, pathValue: previewFile.path });
|
|
||||||
}
|
|
||||||
if (thumbnailFile && !hasFile(thumbFiles, thumbnailFile.path)) {
|
|
||||||
orphans.push({ ...entity, pathType: AssetPathType.THUMBNAIL, pathValue: thumbnailFile.path });
|
|
||||||
}
|
|
||||||
if (encodedVideoPath && !hasFile(videoFiles, encodedVideoPath)) {
|
|
||||||
orphans.push({ ...entity, pathType: AssetPathType.THUMBNAIL, pathValue: encodedVideoPath });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const users = await this.userRepository.getList();
|
|
||||||
for (const { id, profileImagePath } of users) {
|
|
||||||
track(profileImagePath);
|
|
||||||
|
|
||||||
const entity = { entityId: id, entityType: PathEntityType.USER };
|
|
||||||
if (profileImagePath && !hasFile(profileFiles, profileImagePath)) {
|
|
||||||
orphans.push({ ...entity, pathType: UserPathType.PROFILE, pathValue: profileImagePath });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let peopleCount = 0;
|
|
||||||
for await (const { id, thumbnailPath } of this.personRepository.getAll()) {
|
|
||||||
track(thumbnailPath);
|
|
||||||
const entity = { entityId: id, entityType: PathEntityType.PERSON };
|
|
||||||
if (thumbnailPath && !hasFile(thumbFiles, thumbnailPath)) {
|
|
||||||
orphans.push({ ...entity, pathType: PersonPathType.FACE, pathValue: thumbnailPath });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (peopleCount === JOBS_ASSET_PAGINATION_SIZE) {
|
|
||||||
this.logger.log(`Found ${assetCount} assets, ${users.length} users, ${peopleCount} people`);
|
|
||||||
peopleCount = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(`Found ${assetCount} assets, ${users.length} users, ${peopleCount} people`);
|
|
||||||
|
|
||||||
const extras: string[] = [];
|
|
||||||
for (const file of allFiles) {
|
|
||||||
extras.push(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
// send as absolute paths
|
|
||||||
for (const orphan of orphans) {
|
|
||||||
orphan.pathValue = this.fullPath(orphan.pathValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { orphans, extras };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,358 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import empty4Url from '$lib/assets/empty-4.svg';
|
|
||||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
|
||||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
|
||||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
|
||||||
import {
|
|
||||||
NotificationType,
|
|
||||||
notificationController,
|
|
||||||
} from '$lib/components/shared-components/notification/notification';
|
|
||||||
import { downloadManager } from '$lib/managers/download-manager.svelte';
|
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
|
||||||
import { copyToClipboard } from '$lib/utils';
|
|
||||||
import { downloadBlob } from '$lib/utils/asset-utils';
|
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
|
||||||
import { fixAuditFiles, getAuditFiles, getFileChecksums, type FileReportItemDto } from '@immich/sdk';
|
|
||||||
import { Button, HStack, Text } from '@immich/ui';
|
|
||||||
import { mdiCheckAll, mdiContentCopy, mdiDownload, mdiRefresh, mdiWrench } from '@mdi/js';
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
import type { PageData } from './$types';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
data: PageData;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { data }: Props = $props();
|
|
||||||
|
|
||||||
interface UntrackedFile {
|
|
||||||
filename: string;
|
|
||||||
checksum: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Match {
|
|
||||||
orphan: FileReportItemDto;
|
|
||||||
extra: UntrackedFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalize = (filenames: string[]) => filenames.map((filename) => ({ filename, checksum: null }));
|
|
||||||
|
|
||||||
let checking = $state(false);
|
|
||||||
let repairing = $state(false);
|
|
||||||
|
|
||||||
let orphans: FileReportItemDto[] = $state(data.orphans);
|
|
||||||
let extras: UntrackedFile[] = $state(normalize(data.extras));
|
|
||||||
let matches: Match[] = $state([]);
|
|
||||||
|
|
||||||
const handleDownload = () => {
|
|
||||||
if (extras.length > 0) {
|
|
||||||
const blob = new Blob([extras.map(({ filename }) => filename).join('\n')], { type: 'text/plain' });
|
|
||||||
const downloadKey = 'untracked.txt';
|
|
||||||
downloadManager.add(downloadKey, blob.size);
|
|
||||||
downloadManager.update(downloadKey, blob.size);
|
|
||||||
downloadBlob(blob, downloadKey);
|
|
||||||
setTimeout(() => downloadManager.clear(downloadKey), 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (orphans.length > 0) {
|
|
||||||
const blob = new Blob([JSON.stringify(orphans, null, 4)], { type: 'application/json' });
|
|
||||||
const downloadKey = 'orphans.json';
|
|
||||||
downloadManager.add(downloadKey, blob.size);
|
|
||||||
downloadManager.update(downloadKey, blob.size);
|
|
||||||
downloadBlob(blob, downloadKey);
|
|
||||||
setTimeout(() => downloadManager.clear(downloadKey), 5000);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRepair = async () => {
|
|
||||||
if (matches.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
repairing = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fixAuditFiles({
|
|
||||||
fileReportFixDto: {
|
|
||||||
items: matches.map(({ orphan, extra }) => ({
|
|
||||||
entityId: orphan.entityId,
|
|
||||||
entityType: orphan.entityType,
|
|
||||||
pathType: orphan.pathType,
|
|
||||||
pathValue: extra.filename,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
notificationController.show({
|
|
||||||
type: NotificationType.Info,
|
|
||||||
message: $t('admin.repaired_items', { values: { count: matches.length } }),
|
|
||||||
});
|
|
||||||
|
|
||||||
matches = [];
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, $t('errors.unable_to_repair_items'));
|
|
||||||
} finally {
|
|
||||||
repairing = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSplit = (match: Match) => {
|
|
||||||
matches = matches.filter((_match) => _match !== match);
|
|
||||||
orphans = [match.orphan, ...orphans];
|
|
||||||
extras = [match.extra, ...extras];
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
|
||||||
matches = [];
|
|
||||||
orphans = [];
|
|
||||||
extras = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const report = await getAuditFiles();
|
|
||||||
|
|
||||||
orphans = report.orphans;
|
|
||||||
extras = normalize(report.extras);
|
|
||||||
|
|
||||||
notificationController.show({ message: $t('refreshed'), type: NotificationType.Info });
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, $t('errors.unable_to_load_items'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCheckOne = async (filename: string) => {
|
|
||||||
try {
|
|
||||||
const matched = await loadAndMatch([filename]);
|
|
||||||
if (matched) {
|
|
||||||
notificationController.show({
|
|
||||||
message: $t('admin.repair_matched_items', { values: { count: 1 } }),
|
|
||||||
type: NotificationType.Info,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, $t('errors.repair_unable_to_check_items', { values: { count: 'one' } }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCheckAll = async () => {
|
|
||||||
checking = true;
|
|
||||||
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const chunkSize = 10;
|
|
||||||
const filenames = extras.filter(({ checksum }) => !checksum).map(({ filename }) => filename);
|
|
||||||
for (let index = 0; index < filenames.length; index += chunkSize) {
|
|
||||||
count += await loadAndMatch(filenames.slice(index, index + chunkSize));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, $t('errors.repair_unable_to_check_items', { values: { count: 'other' } }));
|
|
||||||
} finally {
|
|
||||||
checking = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationController.show({
|
|
||||||
message: $t('admin.repair_matched_items', { values: { count } }),
|
|
||||||
type: NotificationType.Info,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadAndMatch = async (filenames: string[]) => {
|
|
||||||
const items = await getFileChecksums({
|
|
||||||
fileChecksumDto: { filenames },
|
|
||||||
});
|
|
||||||
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
for (const { checksum, filename } of items) {
|
|
||||||
const extra = extras.find((extra) => extra.filename === filename);
|
|
||||||
if (extra) {
|
|
||||||
extra.checksum = checksum;
|
|
||||||
extras = [...extras];
|
|
||||||
}
|
|
||||||
|
|
||||||
const orphan = orphans.find((orphan) => orphan.checksum === checksum);
|
|
||||||
if (orphan) {
|
|
||||||
count++;
|
|
||||||
matches = [...matches, { orphan, extra: { filename, checksum } }];
|
|
||||||
orphans = orphans.filter((_orphan) => _orphan !== orphan);
|
|
||||||
extras = extras.filter((extra) => extra.filename !== filename);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return count;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<UserPageLayout title={data.meta.title} admin>
|
|
||||||
{#snippet buttons()}
|
|
||||||
<HStack gap={0}>
|
|
||||||
<Button
|
|
||||||
leadingIcon={mdiWrench}
|
|
||||||
onclick={() => handleRepair()}
|
|
||||||
disabled={matches.length === 0 || repairing}
|
|
||||||
size="small"
|
|
||||||
variant="ghost"
|
|
||||||
color="secondary"
|
|
||||||
>
|
|
||||||
<Text class="hidden md:block">{$t('admin.repair_all')}</Text>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
leadingIcon={mdiCheckAll}
|
|
||||||
onclick={() => handleCheckAll()}
|
|
||||||
disabled={extras.length === 0 || checking}
|
|
||||||
size="small"
|
|
||||||
variant="ghost"
|
|
||||||
color="secondary"
|
|
||||||
>
|
|
||||||
<Text class="hidden md:block">{$t('admin.check_all')}</Text>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
leadingIcon={mdiDownload}
|
|
||||||
onclick={() => handleDownload()}
|
|
||||||
disabled={extras.length + orphans.length === 0}
|
|
||||||
size="small"
|
|
||||||
variant="ghost"
|
|
||||||
color="secondary"
|
|
||||||
>
|
|
||||||
<Text class="hidden md:block">{$t('export')}</Text>
|
|
||||||
</Button>
|
|
||||||
<Button leadingIcon={mdiRefresh} onclick={() => handleRefresh()} size="small" variant="ghost" color="secondary">
|
|
||||||
<Text class="hidden md:block">{$t('refresh')}</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]">
|
|
||||||
{#if matches.length + extras.length + orphans.length === 0}
|
|
||||||
<div class="w-full">
|
|
||||||
<EmptyPlaceholder fullWidth text={$t('repair_no_results_message')} src={empty4Url} />
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="gap-2">
|
|
||||||
<table class="table-fixed mt-5 w-full text-start">
|
|
||||||
<thead
|
|
||||||
class="mb-4 flex w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary"
|
|
||||||
>
|
|
||||||
<tr class="flex w-full place-items-center p-2 md:p-5">
|
|
||||||
<th class="w-full text-sm place-items-center font-medium flex justify-between" colspan="2">
|
|
||||||
<div class="px-3">
|
|
||||||
<p>
|
|
||||||
{$t('matches').toUpperCase()}
|
|
||||||
{matches.length > 0 ? `(${matches.length.toLocaleString($locale)})` : ''}
|
|
||||||
</p>
|
|
||||||
<p class="text-gray-600 dark:text-gray-300 mt-1">{$t('admin.these_files_matched_by_checksum')}</p>
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody
|
|
||||||
class="w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray dark:text-immich-dark-fg max-h-[500px] block overflow-x-hidden"
|
|
||||||
>
|
|
||||||
{#each matches as match (match.extra.filename)}
|
|
||||||
<tr
|
|
||||||
class="w-full h-[75px] place-items-center border-[3px] border-transparent p-2 odd:bg-immich-gray even:bg-immich-bg hover:cursor-pointer hover:border-immich-primary/75 odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50 dark:hover:border-immich-dark-primary/75 md:p-5 flex justify-between"
|
|
||||||
tabindex="0"
|
|
||||||
onclick={() => handleSplit(match)}
|
|
||||||
>
|
|
||||||
<td class="text-sm text-ellipsis flex flex-col gap-1 font-mono">
|
|
||||||
<span>{match.orphan.pathValue} =></span>
|
|
||||||
<span>{match.extra.filename}</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-sm text-ellipsis d-flex font-mono">
|
|
||||||
<span>({match.orphan.entityType}/{match.orphan.pathType})</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<table class="table-fixed mt-5 w-full text-start">
|
|
||||||
<thead
|
|
||||||
class="mb-4 flex w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary"
|
|
||||||
>
|
|
||||||
<tr class="flex w-full place-items-center p-1 md:p-5">
|
|
||||||
<th class="w-full text-sm font-medium justify-between place-items-center flex" colspan="2">
|
|
||||||
<div class="px-3">
|
|
||||||
<p>
|
|
||||||
{$t('admin.offline_paths').toUpperCase()}
|
|
||||||
{orphans.length > 0 ? `(${orphans.length.toLocaleString($locale)})` : ''}
|
|
||||||
</p>
|
|
||||||
<p class="text-gray-600 dark:text-gray-300 mt-1">
|
|
||||||
{$t('admin.offline_paths_description')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody
|
|
||||||
class="w-full rounded-md border dark:border-immich-dark-gray dark:text-immich-dark-fg overflow-y-auto max-h-[500px] block overflow-x-hidden"
|
|
||||||
>
|
|
||||||
{#each orphans as orphan, index (index)}
|
|
||||||
<tr
|
|
||||||
class="w-full h-[50px] place-items-center border-[3px] border-transparent odd:bg-immich-gray even:bg-immich-bg hover:cursor-pointer hover:border-immich-primary/75 odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50 dark:hover:border-immich-dark-primary/75 md:p-5 flex justify-between"
|
|
||||||
tabindex="0"
|
|
||||||
title={orphan.pathValue}
|
|
||||||
>
|
|
||||||
<td onclick={() => copyToClipboard(orphan.pathValue)}>
|
|
||||||
<CircleIconButton title={$t('copy_file_path')} icon={mdiContentCopy} size="18" onclick={() => {}} />
|
|
||||||
</td>
|
|
||||||
<td class="truncate text-sm font-mono text-start" title={orphan.pathValue}>
|
|
||||||
{orphan.pathValue}
|
|
||||||
</td>
|
|
||||||
<td class="text-sm font-mono">
|
|
||||||
<span>({orphan.entityType})</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<table class="table-fixed mt-5 w-full text-start max-h-[300px]">
|
|
||||||
<thead
|
|
||||||
class="mb-4 flex w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary"
|
|
||||||
>
|
|
||||||
<tr class="flex w-full place-items-center p-2 md:p-5">
|
|
||||||
<th class="w-full text-sm font-medium place-items-center flex justify-between" colspan="2">
|
|
||||||
<div class="px-3">
|
|
||||||
<p>
|
|
||||||
{$t('admin.untracked_files').toUpperCase()}
|
|
||||||
{extras.length > 0 ? `(${extras.length.toLocaleString($locale)})` : ''}
|
|
||||||
</p>
|
|
||||||
<p class="text-gray-600 dark:text-gray-300 mt-1">
|
|
||||||
{$t('admin.untracked_files_description')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody
|
|
||||||
class="w-full rounded-md border-2 dark:border-immich-dark-gray dark:text-immich-dark-fg overflow-y-auto max-h-[500px] block overflow-x-hidden"
|
|
||||||
>
|
|
||||||
{#each extras as extra (extra.filename)}
|
|
||||||
<tr
|
|
||||||
class="flex h-[50px] w-full place-items-center border-[3px] border-transparent p-1 odd:bg-immich-gray even:bg-immich-bg hover:cursor-pointer hover:border-immich-primary/75 odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50 dark:hover:border-immich-dark-primary/75 md:p-5 justify-between"
|
|
||||||
tabindex="0"
|
|
||||||
onclick={() => handleCheckOne(extra.filename)}
|
|
||||||
title={extra.filename}
|
|
||||||
>
|
|
||||||
<td onclick={() => copyToClipboard(extra.filename)}>
|
|
||||||
<CircleIconButton title={$t('copy_file_path')} icon={mdiContentCopy} size="18" onclick={() => {}} />
|
|
||||||
</td>
|
|
||||||
<td class="w-full text-md text-ellipsis flex justify-between pe-5">
|
|
||||||
<span class="text-ellipsis grow truncate font-mono text-sm pe-5" title={extra.filename}
|
|
||||||
>{extra.filename}</span
|
|
||||||
>
|
|
||||||
<span class="text-sm font-mono dark:text-immich-dark-primary text-immich-primary pes-5">
|
|
||||||
{#if extra.checksum}
|
|
||||||
[sha1:{extra.checksum}]
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
</UserPageLayout>
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { authenticate } from '$lib/utils/auth';
|
|
||||||
import { getFormatter } from '$lib/utils/i18n';
|
|
||||||
import { getAuditFiles } from '@immich/sdk';
|
|
||||||
import type { PageLoad } from './$types';
|
|
||||||
|
|
||||||
export const load = (async () => {
|
|
||||||
await authenticate({ admin: true });
|
|
||||||
const { orphans, extras } = await getAuditFiles();
|
|
||||||
const $t = await getFormatter();
|
|
||||||
|
|
||||||
return {
|
|
||||||
orphans,
|
|
||||||
extras,
|
|
||||||
meta: {
|
|
||||||
title: $t('repair'),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}) satisfies PageLoad;
|
|
Loading…
Add table
Add a link
Reference in a new issue