From d2807b8d6ab1a72f37a662423ddda54f41c742ce Mon Sep 17 00:00:00 2001
From: Jason Rasmussen <jrasm91@gmail.com>
Date: Sat, 14 Oct 2023 13:12:59 -0400
Subject: [PATCH] feat(web,server): offline/untracked files admin tool (#4447)

* feat: admin repair orphans tool

* chore: open api

* fix: include upload folder

* fix: bugs

* feat: empty placeholder

* fix: checks

* feat: move buttons to top of page

* feat: styling and clipboard

* styling

* better clicking hitbox

* fix: show title on hover

* feat: download report

* restrict file access to immich related files

* Add description

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
---
 cli/src/api/open-api/api.ts                   | 378 ++++++++++++++++++
 mobile/openapi/.openapi-generator/FILES       |  21 +
 mobile/openapi/README.md                      | Bin 21061 -> 21697 bytes
 mobile/openapi/doc/AuditApi.md                | Bin 2481 -> 8494 bytes
 mobile/openapi/doc/FileChecksumDto.md         | Bin 0 -> 441 bytes
 mobile/openapi/doc/FileChecksumResponseDto.md | Bin 0 -> 453 bytes
 mobile/openapi/doc/FileReportDto.md           | Bin 0 -> 529 bytes
 mobile/openapi/doc/FileReportFixDto.md        | Bin 0 -> 473 bytes
 mobile/openapi/doc/FileReportItemDto.md       | Bin 0 -> 603 bytes
 mobile/openapi/doc/PathEntityType.md          | Bin 0 -> 380 bytes
 mobile/openapi/doc/PathType.md                | Bin 0 -> 374 bytes
 mobile/openapi/lib/api.dart                   | Bin 6811 -> 7073 bytes
 mobile/openapi/lib/api/audit_api.dart         | Bin 2478 -> 6821 bytes
 mobile/openapi/lib/api_client.dart            | Bin 20580 -> 21186 bytes
 mobile/openapi/lib/api_helper.dart            | Bin 5128 -> 5328 bytes
 .../openapi/lib/model/file_checksum_dto.dart  | Bin 0 -> 2880 bytes
 .../lib/model/file_checksum_response_dto.dart | Bin 0 -> 3188 bytes
 mobile/openapi/lib/model/file_report_dto.dart | Bin 0 -> 3067 bytes
 .../lib/model/file_report_fix_dto.dart        | Bin 0 -> 2804 bytes
 .../lib/model/file_report_item_dto.dart       | Bin 0 -> 4300 bytes
 .../openapi/lib/model/path_entity_type.dart   | Bin 0 -> 2754 bytes
 mobile/openapi/lib/model/path_type.dart       | Bin 0 -> 3157 bytes
 mobile/openapi/test/audit_api_test.dart       | Bin 637 -> 1053 bytes
 .../openapi/test/file_checksum_dto_test.dart  | Bin 0 -> 603 bytes
 .../test/file_checksum_response_dto_test.dart | Bin 0 -> 694 bytes
 mobile/openapi/test/file_report_dto_test.dart | Bin 0 -> 733 bytes
 .../test/file_report_fix_dto_test.dart        | Bin 0 -> 609 bytes
 .../test/file_report_item_dto_test.dart       | Bin 0 -> 995 bytes
 .../openapi/test/path_entity_type_test.dart   | Bin 0 -> 425 bytes
 mobile/openapi/test/path_type_test.dart       | Bin 0 -> 413 bytes
 server/immich-openapi-specs.json              | 223 +++++++++++
 server/src/domain/audit/audit.dto.ts          |  55 ++-
 ....service.spec.ts => audit.service.spec.ts} |  34 +-
 server/src/domain/audit/audit.service.ts      | 189 ++++++++-
 .../src/domain/metadata/metadata.service.ts   |   5 +-
 .../domain/repositories/asset.repository.ts   |   1 +
 .../server-info/server-info.service.spec.ts   |  26 +-
 .../domain/server-info/server-info.service.ts |  17 +-
 .../storage-template.service.ts               |   2 +-
 server/src/domain/storage/storage.core.ts     |  16 +-
 .../domain/storage/storage.service.spec.ts    |  17 +-
 server/src/domain/storage/storage.service.ts  |  14 +-
 .../immich/controllers/audit.controller.ts    |  33 +-
 server/src/infra/entities/move.entity.ts      |   6 +-
 .../infra/repositories/asset.repository.ts    |   2 +-
 web/src/api/api.ts                            |   3 +
 web/src/api/open-api/api.ts                   | 378 ++++++++++++++++++
 web/src/lib/assets/empty-4.svg                |   1 +
 .../elements/buttons/link-button.svelte       |   3 +-
 .../side-bar/admin-side-bar.svelte            |   4 +
 web/src/lib/constants.ts                      |   1 +
 web/src/routes/admin/repair/+page.server.ts   |  26 ++
 web/src/routes/admin/repair/+page.svelte      | 336 ++++++++++++++++
 53 files changed, 1704 insertions(+), 87 deletions(-)
 create mode 100644 mobile/openapi/doc/FileChecksumDto.md
 create mode 100644 mobile/openapi/doc/FileChecksumResponseDto.md
 create mode 100644 mobile/openapi/doc/FileReportDto.md
 create mode 100644 mobile/openapi/doc/FileReportFixDto.md
 create mode 100644 mobile/openapi/doc/FileReportItemDto.md
 create mode 100644 mobile/openapi/doc/PathEntityType.md
 create mode 100644 mobile/openapi/doc/PathType.md
 create mode 100644 mobile/openapi/lib/model/file_checksum_dto.dart
 create mode 100644 mobile/openapi/lib/model/file_checksum_response_dto.dart
 create mode 100644 mobile/openapi/lib/model/file_report_dto.dart
 create mode 100644 mobile/openapi/lib/model/file_report_fix_dto.dart
 create mode 100644 mobile/openapi/lib/model/file_report_item_dto.dart
 create mode 100644 mobile/openapi/lib/model/path_entity_type.dart
 create mode 100644 mobile/openapi/lib/model/path_type.dart
 create mode 100644 mobile/openapi/test/file_checksum_dto_test.dart
 create mode 100644 mobile/openapi/test/file_checksum_response_dto_test.dart
 create mode 100644 mobile/openapi/test/file_report_dto_test.dart
 create mode 100644 mobile/openapi/test/file_report_fix_dto_test.dart
 create mode 100644 mobile/openapi/test/file_report_item_dto_test.dart
 create mode 100644 mobile/openapi/test/path_entity_type_test.dart
 create mode 100644 mobile/openapi/test/path_type_test.dart
 rename server/src/domain/audit/{audi.service.spec.ts => audit.service.spec.ts} (64%)
 create mode 100644 web/src/lib/assets/empty-4.svg
 create mode 100644 web/src/routes/admin/repair/+page.server.ts
 create mode 100644 web/src/routes/admin/repair/+page.svelte

diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts
index 0c8d2673cc..549cc59d0a 100644
--- a/cli/src/api/open-api/api.ts
+++ b/cli/src/api/open-api/api.ts
@@ -1604,6 +1604,109 @@ export interface ExifResponseDto {
      */
     'timeZone'?: string | null;
 }
+/**
+ * 
+ * @export
+ * @interface FileChecksumDto
+ */
+export interface FileChecksumDto {
+    /**
+     * 
+     * @type {Array<string>}
+     * @memberof FileChecksumDto
+     */
+    'filenames': Array<string>;
+}
+/**
+ * 
+ * @export
+ * @interface FileChecksumResponseDto
+ */
+export interface FileChecksumResponseDto {
+    /**
+     * 
+     * @type {string}
+     * @memberof FileChecksumResponseDto
+     */
+    'checksum': string;
+    /**
+     * 
+     * @type {string}
+     * @memberof FileChecksumResponseDto
+     */
+    'filename': string;
+}
+/**
+ * 
+ * @export
+ * @interface FileReportDto
+ */
+export interface FileReportDto {
+    /**
+     * 
+     * @type {Array<string>}
+     * @memberof FileReportDto
+     */
+    'extras': Array<string>;
+    /**
+     * 
+     * @type {Array<FileReportItemDto>}
+     * @memberof FileReportDto
+     */
+    'orphans': Array<FileReportItemDto>;
+}
+/**
+ * 
+ * @export
+ * @interface FileReportFixDto
+ */
+export interface FileReportFixDto {
+    /**
+     * 
+     * @type {Array<FileReportItemDto>}
+     * @memberof FileReportFixDto
+     */
+    'items': Array<FileReportItemDto>;
+}
+/**
+ * 
+ * @export
+ * @interface FileReportItemDto
+ */
+export interface FileReportItemDto {
+    /**
+     * 
+     * @type {string}
+     * @memberof FileReportItemDto
+     */
+    'checksum'?: string;
+    /**
+     * 
+     * @type {string}
+     * @memberof FileReportItemDto
+     */
+    'entityId': string;
+    /**
+     * 
+     * @type {PathEntityType}
+     * @memberof FileReportItemDto
+     */
+    'entityType': PathEntityType;
+    /**
+     * 
+     * @type {PathType}
+     * @memberof FileReportItemDto
+     */
+    'pathType': PathType;
+    /**
+     * 
+     * @type {string}
+     * @memberof FileReportItemDto
+     */
+    'pathValue': string;
+}
+
+
 /**
  * 
  * @export
@@ -2186,6 +2289,40 @@ export interface OAuthConfigResponseDto {
      */
     'url'?: string;
 }
+/**
+ * 
+ * @export
+ * @enum {string}
+ */
+
+export const PathEntityType = {
+    Asset: 'asset',
+    Person: 'person',
+    User: 'user'
+} as const;
+
+export type PathEntityType = typeof PathEntityType[keyof typeof PathEntityType];
+
+
+/**
+ * 
+ * @export
+ * @enum {string}
+ */
+
+export const PathType = {
+    Original: 'original',
+    JpegThumbnail: 'jpeg_thumbnail',
+    WebpThumbnail: 'webp_thumbnail',
+    EncodedVideo: 'encoded_video',
+    Sidecar: 'sidecar',
+    Face: 'face',
+    Profile: 'profile'
+} as const;
+
+export type PathType = typeof PathType[keyof typeof PathType];
+
+
 /**
  * 
  * @export
@@ -8821,6 +8958,50 @@ export class AssetApi extends BaseAPI {
  */
 export const AuditApiAxiosParamCreator = function (configuration?: Configuration) {
     return {
+        /**
+         * 
+         * @param {FileReportFixDto} fileReportFixDto 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        fixAuditFiles: async (fileReportFixDto: FileReportFixDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            // verify required parameter 'fileReportFixDto' is not null or undefined
+            assertParamExists('fixAuditFiles', 'fileReportFixDto', fileReportFixDto)
+            const localVarPath = `/audit/file-report/fix`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+
+            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication cookie required
+
+            // authentication api_key required
+            await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
+
+            // authentication bearer required
+            // http bearer authentication required
+            await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+
+    
+            localVarHeaderParameter['Content-Type'] = 'application/json';
+
+            setSearchParams(localVarUrlObj, localVarQueryParameter);
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+            localVarRequestOptions.data = serializeDataIfNeeded(fileReportFixDto, localVarRequestOptions, configuration)
+
+            return {
+                url: toPathString(localVarUrlObj),
+                options: localVarRequestOptions,
+            };
+        },
         /**
          * 
          * @param {EntityType} entityType 
@@ -8875,6 +9056,88 @@ export const AuditApiAxiosParamCreator = function (configuration?: Configuration
             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 
+            return {
+                url: toPathString(localVarUrlObj),
+                options: localVarRequestOptions,
+            };
+        },
+        /**
+         * 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        getAuditFiles: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/audit/file-report`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+
+            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication cookie required
+
+            // authentication api_key required
+            await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
+
+            // authentication bearer required
+            // http bearer authentication required
+            await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+
+    
+            setSearchParams(localVarUrlObj, localVarQueryParameter);
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+            return {
+                url: toPathString(localVarUrlObj),
+                options: localVarRequestOptions,
+            };
+        },
+        /**
+         * 
+         * @param {FileChecksumDto} fileChecksumDto 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        getFileChecksums: async (fileChecksumDto: FileChecksumDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            // verify required parameter 'fileChecksumDto' is not null or undefined
+            assertParamExists('getFileChecksums', 'fileChecksumDto', fileChecksumDto)
+            const localVarPath = `/audit/file-report/checksum`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+
+            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication cookie required
+
+            // authentication api_key required
+            await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
+
+            // authentication bearer required
+            // http bearer authentication required
+            await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+
+    
+            localVarHeaderParameter['Content-Type'] = 'application/json';
+
+            setSearchParams(localVarUrlObj, localVarQueryParameter);
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+            localVarRequestOptions.data = serializeDataIfNeeded(fileChecksumDto, localVarRequestOptions, configuration)
+
             return {
                 url: toPathString(localVarUrlObj),
                 options: localVarRequestOptions,
@@ -8890,6 +9153,16 @@ export const AuditApiAxiosParamCreator = function (configuration?: Configuration
 export const AuditApiFp = function(configuration?: Configuration) {
     const localVarAxiosParamCreator = AuditApiAxiosParamCreator(configuration)
     return {
+        /**
+         * 
+         * @param {FileReportFixDto} fileReportFixDto 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async fixAuditFiles(fileReportFixDto: FileReportFixDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.fixAuditFiles(fileReportFixDto, options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
         /**
          * 
          * @param {EntityType} entityType 
@@ -8902,6 +9175,25 @@ export const AuditApiFp = function(configuration?: Configuration) {
             const localVarAxiosArgs = await localVarAxiosParamCreator.getAuditDeletes(entityType, after, userId, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
+        /**
+         * 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async getAuditFiles(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<FileReportDto>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.getAuditFiles(options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
+        /**
+         * 
+         * @param {FileChecksumDto} fileChecksumDto 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async getFileChecksums(fileChecksumDto: FileChecksumDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<FileChecksumResponseDto>>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.getFileChecksums(fileChecksumDto, options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
     }
 };
 
@@ -8912,6 +9204,15 @@ export const AuditApiFp = function(configuration?: Configuration) {
 export const AuditApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
     const localVarFp = AuditApiFp(configuration)
     return {
+        /**
+         * 
+         * @param {AuditApiFixAuditFilesRequest} requestParameters Request parameters.
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        fixAuditFiles(requestParameters: AuditApiFixAuditFilesRequest, options?: AxiosRequestConfig): AxiosPromise<void> {
+            return localVarFp.fixAuditFiles(requestParameters.fileReportFixDto, options).then((request) => request(axios, basePath));
+        },
         /**
          * 
          * @param {AuditApiGetAuditDeletesRequest} requestParameters Request parameters.
@@ -8921,9 +9222,40 @@ export const AuditApiFactory = function (configuration?: Configuration, basePath
         getAuditDeletes(requestParameters: AuditApiGetAuditDeletesRequest, options?: AxiosRequestConfig): AxiosPromise<AuditDeletesResponseDto> {
             return localVarFp.getAuditDeletes(requestParameters.entityType, requestParameters.after, requestParameters.userId, options).then((request) => request(axios, basePath));
         },
+        /**
+         * 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        getAuditFiles(options?: AxiosRequestConfig): AxiosPromise<FileReportDto> {
+            return localVarFp.getAuditFiles(options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @param {AuditApiGetFileChecksumsRequest} requestParameters Request parameters.
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        getFileChecksums(requestParameters: AuditApiGetFileChecksumsRequest, options?: AxiosRequestConfig): AxiosPromise<Array<FileChecksumResponseDto>> {
+            return localVarFp.getFileChecksums(requestParameters.fileChecksumDto, options).then((request) => request(axios, basePath));
+        },
     };
 };
 
+/**
+ * Request parameters for fixAuditFiles operation in AuditApi.
+ * @export
+ * @interface AuditApiFixAuditFilesRequest
+ */
+export interface AuditApiFixAuditFilesRequest {
+    /**
+     * 
+     * @type {FileReportFixDto}
+     * @memberof AuditApiFixAuditFiles
+     */
+    readonly fileReportFixDto: FileReportFixDto
+}
+
 /**
  * Request parameters for getAuditDeletes operation in AuditApi.
  * @export
@@ -8952,6 +9284,20 @@ export interface AuditApiGetAuditDeletesRequest {
     readonly userId?: string
 }
 
+/**
+ * Request parameters for getFileChecksums operation in AuditApi.
+ * @export
+ * @interface AuditApiGetFileChecksumsRequest
+ */
+export interface AuditApiGetFileChecksumsRequest {
+    /**
+     * 
+     * @type {FileChecksumDto}
+     * @memberof AuditApiGetFileChecksums
+     */
+    readonly fileChecksumDto: FileChecksumDto
+}
+
 /**
  * AuditApi - object-oriented interface
  * @export
@@ -8959,6 +9305,17 @@ export interface AuditApiGetAuditDeletesRequest {
  * @extends {BaseAPI}
  */
 export class AuditApi extends BaseAPI {
+    /**
+     * 
+     * @param {AuditApiFixAuditFilesRequest} requestParameters Request parameters.
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof AuditApi
+     */
+    public fixAuditFiles(requestParameters: AuditApiFixAuditFilesRequest, options?: AxiosRequestConfig) {
+        return AuditApiFp(this.configuration).fixAuditFiles(requestParameters.fileReportFixDto, options).then((request) => request(this.axios, this.basePath));
+    }
+
     /**
      * 
      * @param {AuditApiGetAuditDeletesRequest} requestParameters Request parameters.
@@ -8969,6 +9326,27 @@ export class AuditApi extends BaseAPI {
     public getAuditDeletes(requestParameters: AuditApiGetAuditDeletesRequest, options?: AxiosRequestConfig) {
         return AuditApiFp(this.configuration).getAuditDeletes(requestParameters.entityType, requestParameters.after, requestParameters.userId, options).then((request) => request(this.axios, this.basePath));
     }
+
+    /**
+     * 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof AuditApi
+     */
+    public getAuditFiles(options?: AxiosRequestConfig) {
+        return AuditApiFp(this.configuration).getAuditFiles(options).then((request) => request(this.axios, this.basePath));
+    }
+
+    /**
+     * 
+     * @param {AuditApiGetFileChecksumsRequest} requestParameters Request parameters.
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof AuditApi
+     */
+    public getFileChecksums(requestParameters: AuditApiGetFileChecksumsRequest, options?: AxiosRequestConfig) {
+        return AuditApiFp(this.configuration).getFileChecksums(requestParameters.fileChecksumDto, options).then((request) => request(this.axios, this.basePath));
+    }
 }
 
 
diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES
index 494835c0a7..bf699a3133 100644
--- a/mobile/openapi/.openapi-generator/FILES
+++ b/mobile/openapi/.openapi-generator/FILES
@@ -59,6 +59,11 @@ doc/DownloadInfoDto.md
 doc/DownloadResponseDto.md
 doc/EntityType.md
 doc/ExifResponseDto.md
+doc/FileChecksumDto.md
+doc/FileChecksumResponseDto.md
+doc/FileReportDto.md
+doc/FileReportFixDto.md
+doc/FileReportItemDto.md
 doc/ImportAssetDto.md
 doc/JobApi.md
 doc/JobCommand.md
@@ -84,6 +89,8 @@ doc/OAuthCallbackDto.md
 doc/OAuthConfigDto.md
 doc/OAuthConfigResponseDto.md
 doc/PartnerApi.md
+doc/PathEntityType.md
+doc/PathType.md
 doc/PeopleResponseDto.md
 doc/PeopleUpdateDto.md
 doc/PeopleUpdateItem.md
@@ -227,6 +234,11 @@ lib/model/download_info_dto.dart
 lib/model/download_response_dto.dart
 lib/model/entity_type.dart
 lib/model/exif_response_dto.dart
+lib/model/file_checksum_dto.dart
+lib/model/file_checksum_response_dto.dart
+lib/model/file_report_dto.dart
+lib/model/file_report_fix_dto.dart
+lib/model/file_report_item_dto.dart
 lib/model/import_asset_dto.dart
 lib/model/job_command.dart
 lib/model/job_command_dto.dart
@@ -248,6 +260,8 @@ lib/model/o_auth_authorize_response_dto.dart
 lib/model/o_auth_callback_dto.dart
 lib/model/o_auth_config_dto.dart
 lib/model/o_auth_config_response_dto.dart
+lib/model/path_entity_type.dart
+lib/model/path_type.dart
 lib/model/people_response_dto.dart
 lib/model/people_update_dto.dart
 lib/model/people_update_item.dart
@@ -364,6 +378,11 @@ test/download_info_dto_test.dart
 test/download_response_dto_test.dart
 test/entity_type_test.dart
 test/exif_response_dto_test.dart
+test/file_checksum_dto_test.dart
+test/file_checksum_response_dto_test.dart
+test/file_report_dto_test.dart
+test/file_report_fix_dto_test.dart
+test/file_report_item_dto_test.dart
 test/import_asset_dto_test.dart
 test/job_api_test.dart
 test/job_command_dto_test.dart
@@ -389,6 +408,8 @@ test/o_auth_callback_dto_test.dart
 test/o_auth_config_dto_test.dart
 test/o_auth_config_response_dto_test.dart
 test/partner_api_test.dart
+test/path_entity_type_test.dart
+test/path_type_test.dart
 test/people_response_dto_test.dart
 test/people_update_dto_test.dart
 test/people_update_item_test.dart
diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md
index 2865b1308a8732c6013d9d85e9e2f7031e613e70..8217b2f292f93380dc8b3aa0c855c32ffca8da4c 100644
GIT binary patch
delta 582
zcmX@Qgz?}?#tmJPL1~#4j-@G?C2pBHsl{4au^K7)$@=<WA;*GDz1$RKpo&D0iZqZ4
zO@$f-Ev*3m;1De>1%0r%K1f2hD77HJs07Ham|P~AApukm)UE|o9j&F6o>~I4WAb!K
z`FbRi++DF71k}i-1#<<$7?3NSGg6bYi%WB{yB25{$oOQKA}kITMR74q<>Z4>vYWq1
zGOC9mTjElZ4-FDDQDDeuaw+I4L?ep_r4|?D=M|@7(>1x!O{^ZII0zh4s7l~sD8@oW
z-7+h%DTWARQSVujnu|>XP@FM$^Gk=9%ub0V8LoLHnI)AWl?AC#{{^53!p#K<qp1RM
KH=l5+<plr<3(c<p

delta 36
scmX@OlJV#g#tmJPlRrp?Om3GJ+pHtasJ>a%`7ZP126xuY!R~py02Mk7r~m)}

diff --git a/mobile/openapi/doc/AuditApi.md b/mobile/openapi/doc/AuditApi.md
index 63a1c97a3bb5088631cca0c12aaa2a58e65b6260..8fbca70bc704951dc3d8499927ccfde01cbc7ddb 100644
GIT binary patch
delta 1162
zcmah{&x_MQ6sEXjL-ZgBdeDW3*+V+g2KJz`s|)qVdJ!$%1qBg_$<&T+W`@jETI{kn
z1z|xM@Ne)WXfOU99z-vC)PKR7Z_+JJQ>{5A?|u2+_rCYNKk@F$tIzE&8?^cv(_3tp
zDnjl@<yT9xW}b!EOl-4jn{^lgA?@4SJA^=7)??df6jX{r<NE*+uRpc-?-BBd>VMN>
zvr2p8`rlcRF__R<fmeGpeWLpLoKR%6Qqr<d5%52;6J$TP{6P@nVihBTO_-@fmcLVO
zy2S?9v>5Nz!lnIc6)uAn>H?$s2E_4Pxty$vAv_BJpG2|;j4Pe+lr|Snu5Sc;kS03q
zfqCn#9d*#=hxcMm6P{}XXkt~Ey>_*RSFewt)E0&zbg1qZ9Q07qAb2WR1`*Ar9zsEt
z6kJhwD6(N49Yk=euY00k=A!@RTq5NzLyb)E_^}c^fChTpN>eH|A?t8UXvk=nahjQ5
z{>SlZ?Z98NXQ%`R&+tbgx(+k_2#>I`WY)ddvnUmrm)_kY_=-N9B|AN)fbvWV#&MdU
z3OJ7ClxC3dA;_dia;j<3G~bs`o}QUA>&<x8Kd9X~lxJ~H5K-Yxrt~GZ+&Ody1#eo#
zZcmj-ni5QdbfN38+C3*Za$ru3oT@qRHCGnWqb1wNT!i`QJ#SUUxe;;Lr!F|9X+X@k
X<x`D?i&uX(XZ#PxtGO^={V%@(h*y`H

delta 15
XcmZ4Iv{87%waHqHKAX33_HzILHY^4%

diff --git a/mobile/openapi/doc/FileChecksumDto.md b/mobile/openapi/doc/FileChecksumDto.md
new file mode 100644
index 0000000000000000000000000000000000000000..b7070431fc0bddf441c810c8b4c7aa2ee2627aa0
GIT binary patch
literal 441
zcma)2O-lnY5WVMD4D6vcknLSXXx$1zTZ(#H78*9QZRl<$B(n#>A8)c2tX?#iFfZ>T
z^9mr3PJ3GpWUw!vY>$s8Y|n8VDkdR{0yd-#@Cjj}fY<F=A5k0*2crWEJ9r8VvkQKE
zF<fNROqkte>g2Q!=`payD0X^4_<*-}pRWpI8?0wbE=@b)#86i!fmf4Kf6YS5E^Xpc
zNR%?MrQF!a_pcnRJ3dTMLTiJa9(x8RXvsx}x@m-{>s8t@gWFsR^hw$@H)XkaSud;Y
ux*SYls2vA%tn;5$M?p>P#5sJcm;2{`YExGVo$+KH|FHN9{1rYILVN;vc!^B_

literal 0
HcmV?d00001

diff --git a/mobile/openapi/doc/FileChecksumResponseDto.md b/mobile/openapi/doc/FileChecksumResponseDto.md
new file mode 100644
index 0000000000000000000000000000000000000000..9cdea7280acd1df09db76bcdd128b3c5c7299e19
GIT binary patch
literal 453
zcma)2!D<3A5WVLs2KHbLWWC#y?OG_Pi0w^;4VzJen@mW?L!lqvWV=vX3T-ao&3p4^
z-c&#Vy$-f^WU#NFY>$s;>@Ml*c9;VBgbkArRRt^32KbCHNFnI%qE9H=wlzAkur5$!
zn2h!LXgn+CMKHN9(!psPGa1=p5*zIh-r@Bvl&`_mMH|?X&uBxO73${Y;tf*jFD#_&
z#xzo9v6L?ytv?MQ<IEp(VxO1zofrH4L(U0Iur3FJ-X=aA?&^BBTP~XQvUa92)Lk8V
m)@5&Xq^O1qr73)MFSgJB?9KO5=uIH&`J2V(z#qe>LWmE?9*e91

literal 0
HcmV?d00001

diff --git a/mobile/openapi/doc/FileReportDto.md b/mobile/openapi/doc/FileReportDto.md
new file mode 100644
index 0000000000000000000000000000000000000000..c6fd73e2d01be98df05d313fc71bc3f935d4addf
GIT binary patch
literal 529
zcma)(L2JT55QXpiD+Y4V1QPFUDOj{1sEF+?1hQmDU7GAHn-N;*kMC?0v@ON%CCtlv
z-z;wvAdgOaTXbZwEuL+QTl5ro#-vn|z>>59J|GMR@Va@^dsKB@8y#5LE}CI<*6+@^
zNkm4&=ssEp(k95yz!rP8)*ZqpyngujnPJ}q>)Dc%X-zDS{OX8!HRbuOS!Ef%bI^U3
zi8afXw&#Zz4%Y1-2T9b@;7%V~1}138^$cZINsxj+Xm?7M=jf%B1!E`02&EeTQj5-{
zLjDu3%JNCtiyK$5Y-r_PRW&Y($#y=St>ywq3^m6N9qZUezN3&2z4s|x($mf2pILc@
T9G&rG9e+cd7XDbCDy6;vpg*TW

literal 0
HcmV?d00001

diff --git a/mobile/openapi/doc/FileReportFixDto.md b/mobile/openapi/doc/FileReportFixDto.md
new file mode 100644
index 0000000000000000000000000000000000000000..58135dcb03e1b2ecede590f1df5e3a9f2107599e
GIT binary patch
literal 473
zcma)2!D<3A5WVLs2KJyE$a-%}L0JVsMWnZ7VZ&x@LpPbQ8KH%Kd=o2Z+e4d6m^bge
z;k^PVpx41xjtmaf$_{wIKqIg0Tg@ayQNV_@0e(R^b0FycRX?NXy3Xj4g>{(^lM8-#
zahzq>Oqkqf>e*>Orsc>Mqu6PO@BvTnA%8W-ezbut`84f_6GPp+1l~+a{WS|IEu)K4
zCT1h$#zua)hFhmyO7|#bJN<=L&Xh@&g$9rMG%zqhPd+j<Z7W36EK&s-+~(3?Nak%j
zt*Y5!Ij^_NWKbCDPYyln97!E%ROj+y30L)ezy7B-RjJUMK-Tj&i?4$}j+a7+Pnm3$
AAOHXW

literal 0
HcmV?d00001

diff --git a/mobile/openapi/doc/FileReportItemDto.md b/mobile/openapi/doc/FileReportItemDto.md
new file mode 100644
index 0000000000000000000000000000000000000000..8ba2b0a3438665a82a93188089ffa89870034678
GIT binary patch
literal 603
zcma)3O=|)%5WVMD4D3M-?0Roc7A*)>7F&8*77UxI1~;3KojnNs@ts6z)fQ?9ByaLQ
zl1v89W2d|+tkc*Q&!)u<dKYL(Xy@dF$TE0wN(1c?5(&I&4r+&DyWMIPNEjOhFgn*C
z&X&_?8wsPwNKH;<;KM)$d$Cp);S=7DJ}z_Yn_xT{*D+c<Vn%*3xOjo`{68$DZ1&h3
z`oT)cWGP=Mm~NK<<I1Hh)z-jN?8wk@ssE&YpU0~vMkQsbXun9RQ?(vnR|=~q9K)04
zp2@f3OIh6i61=N+IK|!GU5KiB;vN7wSjDzMYkI2daZyY+i`jg&V6#lSX0ZG)75_B<
ia3N3K>nHG|o|VfxwcN`bJMEq6=*r?0coRMmLVN*T2)mmA

literal 0
HcmV?d00001

diff --git a/mobile/openapi/doc/PathEntityType.md b/mobile/openapi/doc/PathEntityType.md
new file mode 100644
index 0000000000000000000000000000000000000000..12783a48f0fd041ede4fd761b6d21a84c8af289f
GIT binary patch
literal 380
zcma)1!HNPg487+o0&`Fgt@quN;v58(VeuxylxcP?+NN~kLGa_Z9ffthXfEL;FYo15
z$dQ7HPJ6a=(v6Wr9mL6Hdx->4Rk&hfQ4WLy1EcAV=7*qdTWgYl^O2L_=sy3r>rIPh
zA&fo?bx>+jUM6wW!^U{RD;AU77SwmiMR6Q5+OTAXdZ;{nKuW#B0K45h|B1r5F1hgJ
zdfD&CT2H^~*<xL5Z!62)*;5cxeAPE6H6-jO_;)>PmT%tXyo!R2?1KE0xEVf~PXOQ-
DG&OaR

literal 0
HcmV?d00001

diff --git a/mobile/openapi/doc/PathType.md b/mobile/openapi/doc/PathType.md
new file mode 100644
index 0000000000000000000000000000000000000000..48e944368845519625680d34e36c0dd55ef8e033
GIT binary patch
literal 374
zcma)1!HNPg487+o0&{RZwBFs5q6a}`5N{$(nMS8*o6?B~!H?f|R#?}|ZZ6>^FYo15
z$dQ7HPJ6a=)wPjse~$!ERk&tjQ98n5htaebb0%n-#+oGHeB>lJdd{a$rAbjMgwbcA
z4oY>(%Os9^*cwmxz+#fyg8DYOD2_u$Tb9gFFO`QcNU6Utz~L~@f1)sMN+P_vUXI5P
zttY$XY_VBtZ!62~%~KFleAS7Q8WQ$n{JEahtAF0+yo!R2?1KD~_%?hq9|OP_kMnaB

literal 0
HcmV?d00001

diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart
index 2c6837cb7189d76a1af9c180f7a69837c2b5494f..48745a162c93ef63fe69f0e8bab9e05617840d3a 100644
GIT binary patch
delta 131
zcmbPjy3l;X5gy*O%$(Ht<c!qh?Bdeg$%(8IXd;5Fid<kdMX3e(MUx!`CHWEDlK8aD
u3XmoNWU<VWRIo{#kMKw_vm};e#7}PEk>@A?u}UfnCMSxEZobREj|%`?4KI=a

delta 21
dcmZ2zKHGG|5uVMRyi&}Q4FuUXzY^Ha1pr(j2iE`q

diff --git a/mobile/openapi/lib/api/audit_api.dart b/mobile/openapi/lib/api/audit_api.dart
index 4eabd17c9c0fd681e82a5132a10f47fe027f60fc..24b93f178fe8594e2c2690e6cc3de92e3dd6f6e0 100644
GIT binary patch
delta 1190
zcmb7CK~ED=5YDzjDVV4&CK3}eB#_;Otv#6-TG}Y4kpzMP5>FmB+gEs6_pQ5c4QL1+
zO!WS^dC`McW8~)50~dY(CoUZM6U<vSbe3wPmu5R}zHjFH=KEg$cK^pO<L=Yd^$n=H
zahuC(hj&>eWPu3v_qIARmMSf*ttF)v-+nB`bI@J10#?Xg$2??hTfNWSt|RiHrqjsi
z8JAm6qzsn)j<BwB^-($MEVpt)51?x_^R+T8^fjxu`A_Qf+GNRegP_YjS90N3Uqr$;
zl`-@nkIGbS=;z+msRI?vj{>w}8HcR?M(}n6rxG>)puh$<+VMSjheUvAUmXvR&t+IL
zo@lYuO-DI}ePj3Fi*bEKeKn@Xh#Pp2S@j_|QPPsK)@tK2t<C&zH0Nw471J{TZ`f?W
zCX>A*S?X}O@(0obr(owMGf>mf0-;w^AB!`%=LGyMNZuCnUPkdNou)aBTTBa=VIqZW
zmhQ~c{;Xj7f=o#|2vL%x@eWDS<sKNPQW_@$_@6Wp*hQE~fhZtZTIP|g)0Nj)6bL_J
zxO(H_9P~9xf<{@j68(QfTZteCPiD<#FnzHicn42;VxS+s+l4Tl{YbQTU_tE{rf$}f
zv;sb1^nI|@t>V-k?Q_5!sKHEPp<C>f3gDjS4p428MkzGXvTLIWcsKb~pNr*lDTJs0

delta 15
WcmZ2#x=wh*F2>CfY@gVfYPkS2I|Z5m

diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart
index 1866c468834715b7082152e871bf1f93f95cf27f..9a98b4997ab8eaf952fccb0f7380a85315e66851 100644
GIT binary patch
delta 229
zcmaE|fbq~$#tp}`dE7E{Qk^qWle3FUb0<5>h@%Q5N-3iXG)l{(3MlGnbAikVN-fAQ
zDgl{*%mwP_Maa5kR)7?si2&8`A=G%5q=ManE(BDy`It5@J9A=5#$;J-IhFtr!&=*t
Y9mEK!ESMaqAPNz5)HdFH$!L-+0L%MOD*ylh

delta 19
bcmX@Kl<~;|#tp}`Hy7#hvTrstohS<cSn&tE

diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart
index ee254516df0aa8f5dae0fffb6c3cb3bd8445f003..417a282e0988c5252cbd99cdef19fe4a64f8a31f 100644
GIT binary patch
delta 63
zcmeCsxS+XVBNt0RVoAp2vmBC~AckvRNoL7pUM?4D5HF;%AXQVLno9u)ic(8Ti}Dme
L!khEBW^n=lz}OXH

delta 12
Tcmcbh*`cvvBiH6i?r=^3BNYU!

diff --git a/mobile/openapi/lib/model/file_checksum_dto.dart b/mobile/openapi/lib/model/file_checksum_dto.dart
new file mode 100644
index 0000000000000000000000000000000000000000..e587586ccaab2504f7dce6bb3693e99350f248a3
GIT binary patch
literal 2880
zcmbVOZExE+4E~;9!2v_#0-kfXPeXdnHmvDkZw;_@ABG?>R9lX<Ig(G38w6ec`;n6E
zI&RkXE<h4nCMoiH9#Wo6Mw1D={dTo@@%Q|4{^jk}{2VUcf0~DIzJSZc6?|TtzrXnY
z49&>$O~$m1Uq*kv9MP><OQm^|E1l$`<Ws0+ZFrXOl5e=QVS6t&mD2X82P?O1XOgP6
ziRS;*LZiE68~m-9#(&GT!Qk2)cTbfxmPwn43^x?H;M%#HqnnjNa+8!wuF=dEOeU}X
zNYa97GaBJ`7E}(Z;*wP&!tdE=l$OjGxWMAx6+HcE>dm=Ta61B+VEtdXt+h13K=Lgt
z2WBe}F1eIPXg_!uPz(TD2on)@RI`B?TtTp0h4^@XiWvZW62|`Z)oLNvGw5vHqc`e?
zYmK#|*|kz7Wv`i48sa!uT>s!H)?^E=Pa&Rx$0%iGK--4xlV8956^Q$lo}9Qey+J?G
z_<>A>w6NwD3TBFTD&x4fi_$5Egd%y$jbVC6L?d?vH^MlxpsO)zBX}VC)p|U@Dt{5U
z0>=Z8mtCazd3U)QJLo>MssS~GY=`iRbcW$R7$q^7XD1humGA?XkePedxBD{>j<Z<m
zaks^Ih4zQkdQ<HL<Lqf+PxSs5Apw&a_--;FtbrR=)~K6Yt4$MX>6TD?IU0;fn@cQp
z6@siw)CY-SD~yw5kB>_89DBm4PuD3b$Rwn#1iQ<MG{HtJR7U*xg`u2ZyA&LF4VXT)
z7D>z|aEXz~FrRiEP)0qBok^6kPVjf!1YQHwG!2RavI9)ce>xtf$C$wcsG~Y-@_7vb
zu{_|AdCSv&LbKrXz;k-gu@npQq`$rUZBixdJHi_TU0V-om_doDT_BqWP*QFo+`0~5
zYqe$9B{mi0tTa5_w^({4H8*u>Nw34$%azZJxS)tc1tkF|Cl04bm`i2no67FmkVp^C
z5^PA~2sg%w=1lLNw*-1}<Do7hQl_f~y&Gf*ojW}|i=8t25s6_cB4SzQRnznIxVHEn
zpxk&_MO6hYP*Z5HTol)H4F&xfFhmPBtU5|Wo2dr{t@^WJ7^R&Neavy!2O)Rd#GaDh
zwE|9!=JcIHcTgsHR^gFMN3Wlx2lU~p-n9fqbSJoJiy=;YL5~vY;zZ7@N6RCGB1}u~
z_>=EwIy!%^N80-VR?)R<9jqE}X{b4nlt1CN!WOthtwSgHfP)v_3Z0s9iBle}=}m@$
zVVfRN11acU1I@^uo*dQ1QyUax^c8b_r~jjyCraWe@@ht0ulXNU50-Vv@PLMh=;?s#
zi74#)^R83FPFY?U2W^f!^#n12tAcmjx9^aEGBbDtGvv5NSzL3#BhKKq)By}Wd|h19
fTUkTBf9q<UKay{E;LmOy8okrqy_=nlaFYBBCJ&#a

literal 0
HcmV?d00001

diff --git a/mobile/openapi/lib/model/file_checksum_response_dto.dart b/mobile/openapi/lib/model/file_checksum_response_dto.dart
new file mode 100644
index 0000000000000000000000000000000000000000..2cfd87b4b90c6990f75bb85123c268364788b803
GIT binary patch
literal 3188
zcmbVOZExE)5dQ98aRG{2!Blzcry;4l7L7f$YhoaE2MmTGFfwJal|_xDY8a{i`|e0l
zmKQ4t8X$>8-W#9ixf4gD!Ds~UzRYK@{+!%QZr{x(S1|qXX%fQK3~pv~xSm~onErEy
zW@Pzx#gvI(N58!u(5qONQt>2{D#=8_FQ6`K%~uI8_=cAz^zUL*No7uYuwu)0I;m=t
zDE@CP6uOsegMTZg@W0_oV{oNUyQi|$hLt7}D@-Ud!IgE_CzBPT<T@#2xkfXSvoiVp
z*CfrE(t`n}GaxgN6)#yOBK*A^4AO#W4O2Ya`<$oWbiKLdy27OV%1F2$0E`faTW)Gq
zYG6?EEi8J>mmqAhDUZ;v$0(qf03Hi4xzJJC<PJ~l5%2^U?d{L8WPr~`iE<6ztGni{
z?b=)UohyaNq0gO^1)W_nBNfDPFuVK8Q$)n%T%AKa28VFUihybg{mzdc?g>OcPA^~D
z!kuAz8qCtx2*)PAOfvD$4@wrLfi*XfGo8PeD~?6lG_5d6k0npJ)=ce4PbiU`YoV=K
z;PHukfOGi8?f8_3od?MqjE!^;frV%|JuQ@lPZhuLPhoqm_=Qim9NIM3ta{_3hOpWp
zw;~;bksq}zF^$tlCQ4SoS6qS^9$4S+FCF04VxfjU>u`zYN0R!Vp)ciN=vSPfvP!pM
zX<<)t`<V^^R<6K}1r5Sz_|A%&Ysi$`G$9vu37aNEgCRB70+CsUP}T)Xm2|lk+V)b1
z@k;Rwo7>I>Ri`Lsosi5E91}*Q2~Lz;u1EoHp|21Bu%%ec8xCBFo&c;WumzItu`a(3
z$ZLlhYZ9eQn+GHXu325T)BNZwCBS0ckEihhHmv%a5SwN5UB#Q&<A4MaLWl43GdDBH
z-!u87w>KOfys=gavA2I45fBfp2pTj$2|VfabQg@Fz}+b_d<3hj`Q8qowUS$QS70kc
z#tO~Dy~ov|s<^HTLz3^yu~@oFi64qg^iWYaV618*p>L#_Y$~(!p%EDzc|nswZgEUo
z)5E?e)e#=|5fzbo?b+}c4Pu1aK`UpmRc|*k&3Zai#8xY(sE((@+v9$Oe&b~%oi6ae
zU4VbGQEY!U)O5#6A9|##lLYyS9Y|=^9Xox|{21zTfmJ+8h2<=ERQ0tDPMqfIaRKcu
zA;Awd-bd)(;pY1ht+=Xop4f=q1lRsh#F0DLvkA8GB5U5E>j|O}wuN*2#&<M{t-sSN
z)$j=0=-Rd#o*rLmAX>1b`%rI%$#IEVjIJ>~oV;?@=-iB4{IH-GUF#_5VJu#EK0>6h
z-i8~KA6cB#O$g6xY>d%W)Z-a{Mpw_2%yZ<|jN{JGcv4+>ykq8j=!uBF7RZjs!nUsu
z9UivIbHX_q516qdm=Vl#-fE-0T%rs$UI!R*U!!d9IN;|~V_NDI1|L7qZs@Ao3=sQG
iZEvQ3k#BqV7~fk|4i^pg*%@Zd?R03br<Vg9NdEx`x)UD&

literal 0
HcmV?d00001

diff --git a/mobile/openapi/lib/model/file_report_dto.dart b/mobile/openapi/lib/model/file_report_dto.dart
new file mode 100644
index 0000000000000000000000000000000000000000..0d00938b8a2cb945d644fbf0ca70d1033e0177dc
GIT binary patch
literal 3067
zcmbVO+iu%N5PjEIOn@MwP=;#rR4ApApq2~gVgZi(PzVCCrdHCX6qnsyMim_WduR5-
zi>$;(4OkL)XXkRxnc<GdgYg*N{dqfo`N!;fcJuCbb^(`HA7?RK%;9=|3!ml}SC@Z1
zLo>2`yJE_uuaaM04d_v9N~w6ZmMU9|f={3>Yt2^~FZhm^CJxVHS4m~go?ykE9duUJ
zCR6;MS|~hTvK@X_OyRfTN~3e7&mYfasSPVlCRP|wtOZy0xIP=K5GB`HA<GS#nJp``
zUw_W>EmL|hz;FiS8f3*wR*3}vUk?U(!L)`;EbfL=4Hrhj{QzK$)!%Sat5O4llJ8;B
zGg^YMiRC;&>)uvG=K;)C=!E}k6w{D`tpgUzxZRVg!VFJ0k0Z<q;G@vyx6ek2ax(?z
zrCo2sT^YW^l&9dGS(~@=D_05uLqB&?7F4ogMk+|tXnyyF=Lm$^ay5c<3J%zu6%l0|
zhn;WV+%FKP@Vt0oYjE35g{jXH`rA#B>335UrGX7Muw{DtyIgTh$_8lkN3tt<&b4Oh
zK(aw4Y`GTNnnezsBpe^XPj20ZZZ{r)<M0I|-H*uPF>KLAWEakR`rLgZJdU;Wx&N^o
zTDPBA<>`uHbwGiOe2R|WYgwXOr--#ESpi>g2?g=M`u_OZ;c6`wYUm;kmuTG4(utAJ
zqP6l1&P-VaTZgo8BxOBa*}%#bxYpJnjE1kQsJVu<k~{BcVVAHO)@U%KR#_kt%NWYK
zK;4nZ_Cnhp>CjmzzQzW%J)!Cxm8mn*Y=*6FM4sVP*vb{r>lXU`^~NS(k@ZNs!~)Ei
zOJvg`>0&lv9iT^mHA!;T^jS+><a&_0*dy4}M1+aCKS$Bz0b>}L8o03|>o*V)^<83d
zGLYG*o5eI5^^Q$WMhCg_98Ws9_(6~q=<gEwvGDkKjYdXp`T$^k<mYHVeTl?aBNC6h
zokG3Zr_hTK2%tzY+}mlhQF71j3hZN8vqJOu7#MK)E3WIpPz&@uFP1JV;*b0heUt!B
z6bo=7^tCjzU1bizAySG{E%JWl9*43uZTAD|P7rAaF>LOHlx&g0lszVdORJz~sa0~<
zA8CRLNZ4xSbk;fP5Dm!o@b#yRq?)1t(geaU>&14c2fsT~`bfc!RSn5q3PPR^5VYzJ
zn!b-PZn|h=#2<ofSxFrk-)kJ4FaFvvf#waL;Y$|p5_D&9L%T;DuBt<zE}<vEbtr}S
zaOFHgq4knjqYfdDP>B#N+`+&3fhMgz?{r9!f50amZQ2EE#zz`|79#21v3p^*xI_&@
z7mptPFP#k@@naQV7HCV?FLH&Ac}4}KoRg-Rk~OWL)x%R;5`A=)axTvQ(aRI1@Dy43
znb>LfM^%GG^||d4(4UkY@x)C>w!IlQigJSJH8#fS2w@DjTi$RUuDpQq()b!@$Zm~t
yxZ{8?L5*Qa_YB^Do?p{N(v$9Pi{bDGI@{bBruP<$a6ND%eHdmuJ>o?I=g5EX>e*WW

literal 0
HcmV?d00001

diff --git a/mobile/openapi/lib/model/file_report_fix_dto.dart b/mobile/openapi/lib/model/file_report_fix_dto.dart
new file mode 100644
index 0000000000000000000000000000000000000000..8bf8062d305f10fcfab3fa41d47683eb65278644
GIT binary patch
literal 2804
zcmbVOU2oeq6n*!vxCMq-0aSVI(~wkNi^d+(wK0%p4})O{j7(c>Wzi+6dPeI1zI!i4
zTmDFs&44Y7x}WEqORCrF^m_2_ue;I9KL$60+jn<^D;QpX9K>)nf}7DDd>UO{5C49F
zW@PzxDvV2CC$C<2=vK`uZDcmnCYz~JUO-*dR!%cn%7v_49PZU(sf|17A<C6l+iY38
z%*b!GGU#531^zCD!K;(TVsK+myXU&HPE;;aQ%tC4N*Z>zCzF+`k~S-KHAgd7h${Q-
z*DNoDv7HX4bD(FSm$DK|mEiZX)5%L=Eex@_TS+|()jwBG!$Sw4hXCA4SDVU$fJ&}l
zd}ucT#pUHPL5IVq5k&!Tg|dlqa$z9_?gz$`7$0rUu>gRN%DOi@-)pqQ68gX{Z1Glq
zm&PChXnn7BNj(_hw1G5@M)zN2j-9zensZ3ekCDnn8ByjqJo)j%{{wZR-?K9=)mwyO
z(??2Bm4mr-PzYPR*Hei}*?TJ((n%$AX{|78;u)zcq*a#9B5o`R-X4T@i=UlQ)f}Am
ze<JRxyF<uxH0ChrFMOGF8K_TU`NliNFkPd}RNlupAFQr0il<|yDpA50TtYgItRFU)
z9tpM>n=Xg%PSAG8qc<_E4Ys3&4RQIOGy|fVf*&1=P+Rye%34~O8NFy?jd=-WHba9h
z>2QoaPGYF)5@kS~uasr=wa{N0Il}>9t(iJUq1cQhl;J2jm1j70g`N@{e&I;DzjF-?
zTLYu7EntY{L|hq(2@4AA0DI`NNs<fH_aQ5hr=8}b!SRsa2+Q*yrr@Gg<h37>q{PV?
zB3N7~F}maHoCb_On1?PK{%ukbcw5RE2o9#uYpf4@DFPOhDuxFh#ktWdabMyPz)X}@
z#+v|!hr&o(myV=%*kFG=@wE|}6q3NCGT`itRh1}vqupS!bn6fk0miA1n$UWM`wyG$
zo(Bp%J!uOy-%>*AV!*KQ1{?xtN9l`{)zuD3BvBiQm`*+MEl&?^fOrq+#>;7HD+*9u
zK={W|cyJmP+5>!q7c9hbOLv=#P^yOot=f})6ef&#8+Oe5Q`DKB)U)!Ko^kk_1M>p5
zXIqBH3!b8MTKe5@k3n2D>%d_`cS_pO4YA@odYVfPl(1cknI||!pvK<ut6b9#$NpZE
zl>8^eqU&~mux-4hy@^rM{ybVKSKt!O51qP)EWGq)=+$g{xJ{!uy?#(MoYoVHAQc^t
zQJ);>(MfGQcTO=zTQ!fQ`ajxvrYxQ#v1U(e?Ez0J2>aS6`H+eIU9=^ncsr)MfsME7
z^5WQ=c1+b0MGx)@x#d5+`vFSL;!!G)<{G7OF9A<0i)m>L1bp~Bx}kTehJF9K6rBGg
V-|eB@e_%M86PjPBE<3oH{0nfNjC=q9

literal 0
HcmV?d00001

diff --git a/mobile/openapi/lib/model/file_report_item_dto.dart b/mobile/openapi/lib/model/file_report_item_dto.dart
new file mode 100644
index 0000000000000000000000000000000000000000..77b3f72505daeadfa7b11141ee9934105e9a964d
GIT binary patch
literal 4300
zcmbVPZExE+4E~;9!4Ace;@ot%PlxQ1ZdltxcUv>Gy#a@yFjQM<v^iEzk{bkF|ND`Y
z<T!QuayMY9O_3DI=MgC%9CQv2;KN^+<NZI5&yLSOTpl06$?50g1dhgVHok-}<D=7)
zzjsiLG~egKxZ&IWuWvi_s%}bcWIEL*ovK0(peidX^HdgcE=!lhcQs#V<F;CeVkuTO
zT~sbL@}Ei>G%v**e;2~wUnh-4=f-X~&va>>C|#;@+)z!GG;D6SZdRyL+O*K+4XU}B
zDAV8Gq}fau+v(tT4)hfCLY88o`uP7{r;`=JS~$Vv&L!n=>g4>$X}IeE9AE{`rK?P7
zK|m>&Fli}Xf#Lu&*++}ka*zA~xS6v3Y$mfGwwfm_ipB72l$%?f=8vykt`>6J>O#2L
zQ)}NuQL*9u9;VAl`Cs^2NMR+E+99x)7$GbS7RR_1*qN@19A@HH!nKqoDwX?XRTSd7
zkUMap%1mP566``|2udcL(j^c_g7aKUORljzSyAN@3z*BPs0s(S6cO|)tI8X8@+zzh
zUeyuoB{0)Vc`HrADmSu}MmU)V_f}U1L&!9Go>$hvR1^hVi|hv=co;YNM$AIU;IMiG
z5JCD8DTQn@AoAR+Z3zJ5ek4KO0PS%ZOGecTj?w*vGwAUUBGg=QFN6p&%f2FN$Ba2)
z7uM*pUS)X%x9Nvu3@QuXYpjvMaB%l)tqY<z7D^i!4twM4?=nNuBT{A$h9mHYC=*4G
zf=}Yg&!7GYFrgNO;q`0ujsMt9&0_Rlr};o7W=bmc^QIRs<{WD!e%n-w`ID#mxvj$G
zh&tPAqe=%i(jhhM>?5&eIO448CP%%kl$o?vm=*OV3~?r{vaHsNh|(^^Dma8UFsLiA
z(~pk#;1_QZGk<J_(rvHBEVr+4ZtJBM0|M2Y_76P<z1xCx60;fZ#;2r4YPcICk1~oh
zO6fVNu^8^gc)g6{^Fn^S>T-x*#Nu}!Q3Cl2yT8gtc#J+-U1Es7MNU;I3iys97(z??
z?*1KDRZW4ZyQble$0ND{b}=#Cm{|7;{cZGL@97?7f!)t<jx5+}M@R>C-oDm~C0cur
z60iP1k*fAkm1GZ;V=>pNQL;sm3%sexBCl$AZgEm1xDT!WKg=@_We$F@TTssWzHMP@
z^c+1>C!O3<E5{KyMTIU6$q7PzmG~3F{bH$X=#SwtF~A-#r#PN@vYRTyUS(72Vkr)K
zr?M391T&q}x$}Z{OFHBrm_C6{KDbB%VhM*v4GklTa=;1IU>U3QGhtneZ#_ROsqKYI
zwhmD*Faqi(Fo3yOFb3l(p&Pu8dc^7k8l2qolbqZ~-aKm@6}x*o53Oml0esz<DSg6;
zOGLZlf$;X8iA@)gV%`C*w@jl-_v)b`XZvjEWY3z7*s;Kr{yqPO8pq?tYz^RlRtQh4
z_#-G#eiBoREbxWC<3V_1^pdVCc>M8lU}bV2Y2Zm<q@~*kHk-Eh$(4_eh(tb#M-&JS
zIzCc;WzV!bo-f=gIz?{b(Cr1^dPx@uGF`7n_A%{n-`2M4x?|{}K2)PMom<V0mVF|9
zyv=yhH!OX1W0f~*9ju(Dyn3-%1Bg%Tr3pl{iMK)@5%ssi5%c$jK`p9y90qMa%|;(9
zk2^nEc-9+wU>-Zo*ss<ND1LaPc=yLo9Qu^;V}1ifl$uq9u}^PG+L#U%-ly~eIN4L5
z6&v6@11pwl;thYu6%8@A_q?RtpTHJP8%>{}q2A18(*8=gRBnbMIz;qM)WTuk>!Dfb
z`*iEYD?EKep}Tc&{?Zuj#H1~UiMc^*TltZ|%Z@AV+GNgSd;K5&ypS3%p;?#-jaKk1
zi!iZGoLgY@38uPu4KgM5X<eV&q-K~W$i|_^y&A+gfJ^*@Bj?0{`hyhdX7R=(5NZ0d
zx|TGtEpAI^LBOYr@fm&bg#OH9yf*%mZ0iF)y5o9;(Q}%2{orbjq9Ca!Oym6<T`t%J
JFMrsH{spLgbyffX

literal 0
HcmV?d00001

diff --git a/mobile/openapi/lib/model/path_entity_type.dart b/mobile/openapi/lib/model/path_entity_type.dart
new file mode 100644
index 0000000000000000000000000000000000000000..bbc8d51586c9b99515bcb31dc09d4d694e3a7a89
GIT binary patch
literal 2754
zcmai0U2oeq6n*!vxB-U70X%u@)8Ni^1H{=fv`Mi#4})O{j6}s;Wl|%l7)Fu*zH>=Q
zi4~<aV4E`U_dWMqPp6~ll+JHgH$VTeyk7o&zFJ<;<<-aKgf4FA`esF+ZZ57a|2cse
zOMb|O_4BvMuWv{EsP46~GA)cv3suS))w*^vPh}~0Qu|5!tag>Leo#Y{dvS1SRr}P+
z|7vBSuEh?Ym9Y5s(mEKoZdg4t+IgXUs&Y6~g_1U?yFq8A($b}+(f1JZTcOk6eo3>f
zux>PhJ5Qz{Q%Nl<mEiquG#X{4aE?9;zg@z<KdcUwq;DglDe}LH&Xw?z1ZlY^sp}o>
zmEV%zN~)}RKwvqoA4FNpO#;0TanE3qj{R1-BuJgXZ8X1viCX8<mItNpDco|1EE_%T
z<nB&dHd`nyO8Vk$CmGCqFb~pN!~l`z(wle~pV9n%G#W&^>%W}Q{Yw<ttE>qg6_K9i
zFO#sR;p!AA5FM#hXrs}?I`M8m30ShjDtyBVO`$$27kVekk}Bhzx+`VO%#qTuUTbd6
zW_{er(2=7+Ly;Z4{(da^ehptlA-_pD#<1g=<KfdLgwz;3Hn5<=nq8=EegMKz&Dd`4
z!m%i0Y-NRZ7_6PN<Fh1}naSn&89o{xoOwUe1(o>qvhVF*G+;vC?u;qv-kQDOBF(7~
zrIVA#vjFNAM^?JJ<QDgBuaP0wK@~J+g9Ido>c4qI{G2FvZTxaq`9o_FHjSlm1StG+
zM_BafzV#Xb{sLEB-Jl$h#R0?+qQ=RbbX}Iu4HCr^5|QU!*`glNV@RR>j!#zl+Ui!z
z8%}E6mTA@%EckyMJu>}GscRC31GR`;0bcQx2~9v8cWpyBtH2bQ<nireki2PTa}uly
z4Z?^W-%yK{TzEluBKykHeGADTP4!?D8eE9X8+-T>;24u>7xQ8<AEd3G1F%>e$svk+
zO}1g3SgP^y91tfRSc(sk9lM#o2qWRZyX&?_0S%iq3vakgqL(J~B-$1~=?a|yVVM0j
zu(M$xds~CYQGMp_VXNq?HlFSz#Rlc%>MWDa6)<vO3SeA7Uk6CC)rc&aC-kS;OVIIz
zr>EySOyT!^H@1HeLG;b&yVY2`aS_XWX|1s_$t}i6AD4m`O)ntCc_3`a{6Kro1{Tjh
zUlEoimM5x?Dm~kHaSo9z#trKZ^KRu1t0!+O!tzX#kgyV~L4m1j$GZxKO$lRF^|Uwn
z2Ob@c@)#0}rtagm=iKa;)w*tixVV=csWDrzAiZD;hh3d*<Lz_)$b^&BuCrOw!(J>&
zSO$>~nOYj1W}UsJ6t7=sg^l)_4qrZL<gyT8=NXFAzBs{?F8$|;u!QH9)49E``CnOp
z!ua%7f7Jj&{2vkfhmnb>p3zTSuDFt0AKzh3W?zmEv=|5A3p!g$4^He+RgVUM$c7lX
R)q{Jpr?GhArFTrj{{xFLlVAV<

literal 0
HcmV?d00001

diff --git a/mobile/openapi/lib/model/path_type.dart b/mobile/openapi/lib/model/path_type.dart
new file mode 100644
index 0000000000000000000000000000000000000000..9cb02e0c9e5eb9c4b067b3f04f486962765ab5aa
GIT binary patch
literal 3157
zcmai0ZEqqs5dO}un3JjtktnqHsmhh(B89Z6uF0uKDpd%r%&vjGTkNgv4ML~<_da8<
z7uY3fqyV<(#WVBFGaik4qY+&{&K`dJZF)cb>v}f5q1(Gp(*fN)(EY=V{(QK(yZz@4
z#8~oUCX5@u8~*&R$IE=Jw2{e5n`D(2@`@@|S(zoWkXxzTAg=Rmsf|0UA&Q;Y+oY^q
zV&uP7ZlJEj7N4as_;%7*7&rF3da9LmLb)W*;4ok1(s*@y)>)A&X_G>$HN@OTsN|QQ
zl5``C?e*Z!kzSE5r4nU6#Q*zVua_3WTKX*9X0|USeeV&C5cyqjn+qpNkdix+s@l>n
zcN=mWNu|+GxKc**Cs9;#F@&DKYh!Ad^xY=6Lw|k=SHbWM%9YBbDfYQqlRxEav25_M
zmD{B>Y;ct;QP3A>8cA>FqkfXcz&k|hL#M-!{w0k+1fyQGIsU_H)jmXGU7pt0)+S$j
zcjMy-%thS4KnO%9Dql;vo^7h_Qi;5vmL4CX1oi0|?VVhfFKYAQtJ+ef8tMNVS<r3Q
z^;yCN(q}wd?5D!C)#~jt3M-MywlwdZ5d|)|Q`*w?HY?OP_>@}@M^O}1YHRZ)v^*Lt
z<to5o&X`-wBRD<CPAX8#kLlMa^I;DAM6qfEzd9*Y*gDC6*g8&W*r_uT794Q+RR_b1
zg^#DcQ@q23Rz`0<;<7z@9T*Euz}SpJ+-)->lm)qLrRiS|Q52N4A4HGsa;gpiPN=~8
zw(a3706w7aORWo98@&^p&oQk;VddcY(l>VOC?joEIGi4=&JhVGE?-ffO%b}$m+0+V
z;`K1M_u5UjrQ1hau%-?5LzBWImyJaa=h2@J=@=4CexT<2id_fiLH5m<R8<tmrij9|
zha$_GBn3U9XMg$l9acuV%BZO2G>6lVskv&B<o!SQo|*7R)RY9&fGh-d1d}>Sg(3iw
zYqTKz&OJk-F+g4ih?3JaCk?zkp+J$LvI~l6$b=KL6zMmXKIjfxWZZ&?FCkFqwAsH5
zBgR<L+-))$w`jR#6f7Nj@|3(^lB)SMq@f?NOdODBUj7jnziIRv4{M7lGLP~58ZYK7
zyx<gwCLCsclp%c59Wo1k(7StJVZo4erouczPI(5Mw}(0@?Pw_}q&*{BrKz-b1tWWI
zTCOwnm4_Hx4M>sMkbc)YiQzKfK4`i2o%Fiz+V=n4Aoyl9Jt{mL!c#(KQ)9FVL2l3%
z>NFKRHd`0MJ@<qepY3VK(ZJ&M=R3l<$M{FSk)@-_9r_BvqJ1#7(YZ!$F-WtR5thf}
z#z4YI47wb|Tv?vHFsut0E6c<D=M8QJzGZ~Lq|SIh)|m?&IjN~5cz~<eP!cl^3sR@&
zEnXYxCY&BKK`N}Iqse?#7qGd=Fw%eEG$x5!r!|$knkS>8quv_JUG<Z((osJh%w6r{
z3Di-J<GHI=pL8_*i0+E>c1Jc2=&pEua?r@+N??3kq4M#MYxt(A@NunJ!ZDud+N>*n
zG<%%)FCNu51(b&;UjQeVAhhfi{lJI_&x`2e6^iFk!S@4)hwlCY*cQ@(z#DdLF&Y}J
T=5I!DYk;?8CkH}(&vW8GDi98W

literal 0
HcmV?d00001

diff --git a/mobile/openapi/test/audit_api_test.dart b/mobile/openapi/test/audit_api_test.dart
index 68ffede19c056ff71bb12146dffb13c14309f26c..2ce8d3d8600947320c428974e63572bba8c6482f 100644
GIT binary patch
delta 263
zcmey%GM8h+3C19Ww9E>}(v-{+x6GW>Vhs=(lv<Eq1mst^l;i`YQ3W&=5{oPIlDQOs
zpd_`pL_-}!BAYVVk4ba#KE|CI3JUuAZlxuqMX5FjV}WMcDWs>CAlW%ti%Fgtr~;%h
z3Q3bsW^oBvr*lSXa&~cPZcu7*L4ICwD##45As}O6k`TAUcrd3U@qyMz@gVD%?9ZeJ
E0Ka%$-v9sr

delta 18
ZcmbQs@t0-83C793OzYWdHLba7xd22|1}Ojl

diff --git a/mobile/openapi/test/file_checksum_dto_test.dart b/mobile/openapi/test/file_checksum_dto_test.dart
new file mode 100644
index 0000000000000000000000000000000000000000..6eb3a39023071ba9978bd2b0f50873e775b28306
GIT binary patch
literal 603
zcmZvZ(NDrK5XRs4SDa5kf=qcbAV%WAkRVGGAB-`j*>0nuYiHYGh~a<tTEqwBVQnw@
zzTe$<i{mJcVf?yGFCQn1$@6$QNnkpgPr8t#ut=BilqR$3+XczI@;>LrjrzUoe&kD0
zDs7Mz+OR@M96+NQi#bDy6)M*~)}pGlac3P|e({}Ub>o=9&qf%sD_+rAb3?a7V+n5T
zd3&anbzC_na%w0FfhO4P*=ABGv`lJMlFY5SVmDVTTXSQh=rAL+YMyO)i344u;<bqB
z>-p5J;XjE(>tcT#1-_}TEzn9}N-KF-W45tPl{g)3waB63LbW^a9RZNI5Q<9>Awz~R
zI`y<W!-$ln(M{cMdGx8G)rDVN0M3PVw=d2JRo)R@QicjGv~w(YBOPqHZ0J9QwZZ#G
vhu{!KCb|v}aBGCxP<-Qdu=-=H4xPB&sVt1|ofKi2CTXDT{jBr~_dfao44}ro

literal 0
HcmV?d00001

diff --git a/mobile/openapi/test/file_checksum_response_dto_test.dart b/mobile/openapi/test/file_checksum_response_dto_test.dart
new file mode 100644
index 0000000000000000000000000000000000000000..a90fc616496e702cc11f0dfc81aaf7d99b1344a8
GIT binary patch
literal 694
zcma)4Piw+36u<XVyq#2VcJnk8VY2EFHVdvh>=Z+NZG&x+CFzC?zx!UI!eGjF$V(pl
z{=Fn|9K|tAU$gZ3akiYTrr9ik`QmBTha`n%n!$6LEaq=l1oO!I4L5Ew9NZ2g--=Qy
zgRD@76{5xww6e9hVOV2>()CZhXtXl!EQ8mdd}o<%9W(ge3PW_s8>*Tc+8r88;>Mn*
z=So_~rDI}4hN2K?0^Oc9tA#|%Y9&j8xr$46cf<0E8yiK3jL^|M-|`YiN~7dj#Iy!}
z>dyEV#G!SuFDHOE^}PkU5}4DKJXDx(ZQHESYKn@9Q_+VK8)&$Y-5z{J07NQ;<TZ#e
zOO7zP#O?OR5iv@mTHWn<t4qjEACB_D;KdmsOURFLNCQGr5>ep*u0qg;rZ#Q|>l1K&
n(BXC`^AOe(DR7o1X%N{T68$vsU%@}&pC%{Z|4c|OnC+u)zp~-l

literal 0
HcmV?d00001

diff --git a/mobile/openapi/test/file_report_dto_test.dart b/mobile/openapi/test/file_report_dto_test.dart
new file mode 100644
index 0000000000000000000000000000000000000000..a843046683e41cb2902506e9f848d1a9c6036781
GIT binary patch
literal 733
zcmbtRO-my|5WUZ@C{IZsI;*EeS6N{a3Ahfp*~22z7Be-I#+mN6yG9M-f491$h?s-m
zp+Aa#@6~(Nd7kAtoc>#uyJxeD+2!eSR=|95KAS>O!bQ1+KV`9)|KAZ<ME<VP^YOvn
z{y`Q?-5TpjHP)*}cXSAY83NUcI_k;r^sUytv!1syuzSFDP;Ox5>2A<o=mvY)9eO#L
zd=POTw$m$X0;6H2YiXz(O+L}X)@GeH6jWzTD==Q6Q9pmEYK1;z*~TM`99Fm3(xG)^
z(CJ*(UQ9hY`UGJLoX71A5Szw2fQ$s@GLlP*$qTmGz121JsLgl+PZ@wf8*Q)yof1?8
z#~&Hv$x$Y_*4x32M^X0U`)CTUQc3Jw2mbY!y*BL$&?9>cFs`Y=p<}qmZjkRu(}Jt(
ziReOdD#A8hfL8?0OKQ(+xY-EYY<gf=n<|-(G%>y`i!w<(Z#=#mc+b3G>LvFHSnp)g
R-!b#5@HIE$E4g~kUI6Ns?gIb-

literal 0
HcmV?d00001

diff --git a/mobile/openapi/test/file_report_fix_dto_test.dart b/mobile/openapi/test/file_report_fix_dto_test.dart
new file mode 100644
index 0000000000000000000000000000000000000000..44e7344295fdb0182a237744340ce621fcf0dc6b
GIT binary patch
literal 609
zcmZuu-)q7!5Pt7paeHb7XE&dQ&M~3w5S#^Z4`YlN+N*}ONtdK<GW_3niIY9(Ly|lA
ze%yD7;~<V<`m)NepVCFToUYOYve`WCLz2THU%_*p%(B-jf;r@4$+evf2e-q(wPGWc
zMph}!DpBJII@uX48P?dKwEa^rnpSChk-_UPzBjDxEYtYe2~Bj#8@gMr>9wd0iEDG2
zUMOiSmzIf=3`HeSd%C%3Rtt%S)k<y%X183jyBk()xi&#?oDss77ax3sBh{kht%&Ii
zT<UOq2C-;t?5-2QnY!KptpqY!$r4@iOnfI+1v@25Xt<D34}JmwLRUia8iWr~AWY6k
zQEwa&bfZ<*MxhfwgM@wf6ZDdEVeI|C_sn8L<_{pKFc6hk@vgS8<8?<r<`)QW?>#aB
sKRmf8Ux3{rw3-&7?H<;izdo9AvzLX>;8>Jnl_$AZIJkMK^6w$|1v?taB>(^b

literal 0
HcmV?d00001

diff --git a/mobile/openapi/test/file_report_item_dto_test.dart b/mobile/openapi/test/file_report_item_dto_test.dart
new file mode 100644
index 0000000000000000000000000000000000000000..7e90322f70c6de4ddb347e70635b8dfa0c84e7f6
GIT binary patch
literal 995
zcmb7>(M#ko5XRs8SBy`)!fHLA4n<ICR~GK<q4mJ07~0u3*e1DTdPfoecPCjZ2fN_z
zL(>d=-}jrDq-m0-aPwFeC%5@set%Qu87x;n@&#lC+!ZDKEV9+|>4;@Pd0k6KSLdhS
z&XZ88MjMA>ZJb!E4liKPgU4E6hdpXqOttE*akSGRyMJ=@!VV-H{uz{GyOuqlRyzI@
zI?v(G@3vP)dy<-jsu`%(3SG4Oov==6^rADmVHveji|=QmYNhi@vgr|KR#q>v!3$$i
zORG};PQ%o*%Pk3^r!*XsAV5RyfmZ@cUdcU%$ybDZMke`dR1LjUdVUPA2>{F2O3MyZ
zoT5Uw+IG#4FB8@_&J1=w3-Q~U*#bTyW9S1pr5mVPtX}-kvm0VxY&Y=&)FN2NGdddK
zc@jQva-<)16&nmjZn7-0B8I$edifs`)=uN9{t_|?4}$!X)aH>;juv5)5w3gUg7jcu
hL|G>91)78hVLo0ccz?@o_<EdA<$f4mPkgJl<Q+gRI7R>f

literal 0
HcmV?d00001

diff --git a/mobile/openapi/test/path_entity_type_test.dart b/mobile/openapi/test/path_entity_type_test.dart
new file mode 100644
index 0000000000000000000000000000000000000000..7a9c9a714ecb1b3b89906823b3aeef9cc416ee34
GIT binary patch
literal 425
zcmZvYK}*Ci5QXpg72~P9P&c|K*+p=nU0KkrNIiH8LpyDQZ4#4IDYF0FMA?h>kO>dI
zH*aP*XPm?Cr7SM*^JD(JEAtHYhlf0ctbk)t!efyg_OBNKi{xF6*59sI*J~D~Y?Zbo
z8f`@*d)mOLMn|=vo(57rJ!?4_ZT+kR`%j#lFryci-bZPJUEvU_K^uNg)`jNQ&D$%j
zoJZw_tV5t|B-tOkn+@xwA}4yS+JN~EmAJVQRfpCw_B$ga2CFx0X`>A(G?It6id!e!
ze~2gNc`RojhDPlmoCKcGcX=4fPvvBYeP~(3fKn}%@WmKFYxQUrNi_UnlN8R-X}M+W
G#J&Ob>5yFj

literal 0
HcmV?d00001

diff --git a/mobile/openapi/test/path_type_test.dart b/mobile/openapi/test/path_type_test.dart
new file mode 100644
index 0000000000000000000000000000000000000000..20862a0ef5b6489d28c5f6ed0ceb39030544d46a
GIT binary patch
literal 413
zcmZvYK}*Ci5QXpg72~P9P&c|K*+p=nU0KkrNIiH8LpyDQZ4#4IS!DmaiBd22kO`0Z
z-n?WuXPm?Cr7SM*^JD(JEAtHYhlf0ctbk)t!efyg_OBNKi{xF6*59sI*J~D~Y?Zbo
z8f`@*d)mOLMn|=vo(57rowOW`wtm)u{U@HBFryci-bZPJUEvU_K^tyQ)`j8L&D$%j
zoJZw_tV5t|B-tOkn+@xwA}4yS+JN~EmAJVQRfpCw_WMRi3|4R0(ncFnXe1A36@Q&<
z{~?~7=dnyc42{}BSP49#@5+-Q_Mv4B14^}6!WUxzt<|GhBoY0ik`$))dAVim%)S8y
CCyQeM

literal 0
HcmV?d00001

diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json
index 6392669b15..cc649d784d 100644
--- a/server/immich-openapi-specs.json
+++ b/server/immich-openapi-specs.json
@@ -2286,6 +2286,118 @@
         ]
       }
     },
+    "/audit/file-report": {
+      "get": {
+        "operationId": "getAuditFiles",
+        "parameters": [],
+        "responses": {
+          "200": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/FileReportDto"
+                }
+              }
+            },
+            "description": ""
+          }
+        },
+        "security": [
+          {
+            "bearer": []
+          },
+          {
+            "cookie": []
+          },
+          {
+            "api_key": []
+          }
+        ],
+        "tags": [
+          "Audit"
+        ]
+      }
+    },
+    "/audit/file-report/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": [
+          "Audit"
+        ]
+      }
+    },
+    "/audit/file-report/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": [
+          "Audit"
+        ]
+      }
+    },
     "/auth/admin-sign-up": {
       "post": {
         "operationId": "adminSignUp",
@@ -6580,6 +6692,97 @@
         },
         "type": "object"
       },
+      "FileChecksumDto": {
+        "properties": {
+          "filenames": {
+            "items": {
+              "type": "string"
+            },
+            "type": "array"
+          }
+        },
+        "required": [
+          "filenames"
+        ],
+        "type": "object"
+      },
+      "FileChecksumResponseDto": {
+        "properties": {
+          "checksum": {
+            "type": "string"
+          },
+          "filename": {
+            "type": "string"
+          }
+        },
+        "required": [
+          "filename",
+          "checksum"
+        ],
+        "type": "object"
+      },
+      "FileReportDto": {
+        "properties": {
+          "extras": {
+            "items": {
+              "type": "string"
+            },
+            "type": "array"
+          },
+          "orphans": {
+            "items": {
+              "$ref": "#/components/schemas/FileReportItemDto"
+            },
+            "type": "array"
+          }
+        },
+        "required": [
+          "orphans",
+          "extras"
+        ],
+        "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": {
+            "$ref": "#/components/schemas/PathEntityType"
+          },
+          "pathType": {
+            "$ref": "#/components/schemas/PathType"
+          },
+          "pathValue": {
+            "type": "string"
+          }
+        },
+        "required": [
+          "entityId",
+          "entityType",
+          "pathType",
+          "pathValue"
+        ],
+        "type": "object"
+      },
       "ImportAssetDto": {
         "properties": {
           "assetPath": {
@@ -7027,6 +7230,26 @@
         ],
         "type": "object"
       },
+      "PathEntityType": {
+        "enum": [
+          "asset",
+          "person",
+          "user"
+        ],
+        "type": "string"
+      },
+      "PathType": {
+        "enum": [
+          "original",
+          "jpeg_thumbnail",
+          "webp_thumbnail",
+          "encoded_video",
+          "sidecar",
+          "face",
+          "profile"
+        ],
+        "type": "string"
+      },
       "PeopleResponseDto": {
         "properties": {
           "people": {
diff --git a/server/src/domain/audit/audit.dto.ts b/server/src/domain/audit/audit.dto.ts
index b437ed5b7f..d941f9a1df 100644
--- a/server/src/domain/audit/audit.dto.ts
+++ b/server/src/domain/audit/audit.dto.ts
@@ -1,8 +1,10 @@
-import { EntityType } from '@app/infra/entities';
+import { AssetPathType, EntityType, PathType, PersonPathType, UserPathType } from '@app/infra/entities';
 import { ApiProperty } from '@nestjs/swagger';
 import { Type } from 'class-transformer';
-import { IsDate, IsEnum, IsUUID } from 'class-validator';
-import { Optional } from '../domain.util';
+import { IsArray, IsDate, IsEnum, IsString, IsUUID, ValidateNested } from 'class-validator';
+import { Optional, ValidateUUID } from '../domain.util';
+
+const PathEnum = Object.values({ ...AssetPathType, ...PersonPathType, ...UserPathType });
 
 export class AuditDeletesDto {
   @IsDate()
@@ -19,7 +21,54 @@ export class AuditDeletesDto {
   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;
+}
diff --git a/server/src/domain/audit/audi.service.spec.ts b/server/src/domain/audit/audit.service.spec.ts
similarity index 64%
rename from server/src/domain/audit/audi.service.spec.ts
rename to server/src/domain/audit/audit.service.spec.ts
index 39b447330c..5e68250fab 100644
--- a/server/src/domain/audit/audi.service.spec.ts
+++ b/server/src/domain/audit/audit.service.spec.ts
@@ -1,17 +1,45 @@
 import { DatabaseAction, EntityType } from '@app/infra/entities';
-import { IAccessRepositoryMock, auditStub, authStub, newAccessRepositoryMock, newAuditRepositoryMock } from '@test';
-import { IAuditRepository } from '../repositories';
+import {
+  IAccessRepositoryMock,
+  auditStub,
+  authStub,
+  newAccessRepositoryMock,
+  newAssetRepositoryMock,
+  newAuditRepositoryMock,
+  newCryptoRepositoryMock,
+  newPersonRepositoryMock,
+  newStorageRepositoryMock,
+  newUserRepositoryMock,
+} from '@test';
+import {
+  IAssetRepository,
+  IAuditRepository,
+  ICryptoRepository,
+  IPersonRepository,
+  IStorageRepository,
+  IUserRepository,
+} from '../repositories';
 import { AuditService } from './audit.service';
 
 describe(AuditService.name, () => {
   let sut: AuditService;
   let accessMock: IAccessRepositoryMock;
+  let assetMock: jest.Mocked<IAssetRepository>;
   let auditMock: jest.Mocked<IAuditRepository>;
+  let cryptoMock: jest.Mocked<ICryptoRepository>;
+  let personMock: jest.Mocked<IPersonRepository>;
+  let storageMock: jest.Mocked<IStorageRepository>;
+  let userMock: jest.Mocked<IUserRepository>;
 
   beforeEach(async () => {
     accessMock = newAccessRepositoryMock();
+    assetMock = newAssetRepositoryMock();
+    cryptoMock = newCryptoRepositoryMock();
     auditMock = newAuditRepositoryMock();
-    sut = new AuditService(accessMock, auditMock);
+    personMock = newPersonRepositoryMock();
+    storageMock = newStorageRepositoryMock();
+    userMock = newUserRepositoryMock();
+    sut = new AuditService(accessMock, assetMock, cryptoMock, personMock, auditMock, storageMock, userMock);
   });
 
   it('should work', () => {
diff --git a/server/src/domain/audit/audit.service.ts b/server/src/domain/audit/audit.service.ts
index 7e1574d485..dc3d65b653 100644
--- a/server/src/domain/audit/audit.service.ts
+++ b/server/src/domain/audit/audit.service.ts
@@ -1,19 +1,44 @@
-import { DatabaseAction } from '@app/infra/entities';
-import { Inject, Injectable } from '@nestjs/common';
+import { AssetPathType, DatabaseAction, PersonPathType, UserPathType } from '@app/infra/entities';
+import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common';
 import { DateTime } from 'luxon';
+import { resolve } from 'node:path';
 import { AccessCore, Permission } from '../access';
 import { AuthUserDto } from '../auth';
 import { AUDIT_LOG_MAX_DURATION } from '../domain.constant';
-import { IAccessRepository, IAuditRepository } from '../repositories';
-import { AuditDeletesDto, AuditDeletesResponseDto } from './audit.dto';
+import { usePagination } from '../domain.util';
+import { JOBS_ASSET_PAGINATION_SIZE } from '../job';
+import {
+  IAccessRepository,
+  IAssetRepository,
+  IAuditRepository,
+  ICryptoRepository,
+  IPersonRepository,
+  IStorageRepository,
+  IUserRepository,
+} from '../repositories';
+import { StorageCore, StorageFolder } from '../storage';
+import {
+  AuditDeletesDto,
+  AuditDeletesResponseDto,
+  FileChecksumDto,
+  FileChecksumResponseDto,
+  FileReportItemDto,
+  PathEntityType,
+} from './audit.dto';
 
 @Injectable()
 export class AuditService {
   private access: AccessCore;
+  private logger = new Logger(AuditService.name);
 
   constructor(
     @Inject(IAccessRepository) accessRepository: IAccessRepository,
+    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
+    @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
+    @Inject(IPersonRepository) private personRepository: IPersonRepository,
     @Inject(IAuditRepository) private repository: IAuditRepository,
+    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
+    @Inject(IUserRepository) private userRepository: IUserRepository,
   ) {
     this.access = new AccessCore(accessRepository);
   }
@@ -40,4 +65,160 @@ export class AuditService {
       ids: audits.map(({ entityId }) => entityId),
     };
   }
+
+  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.save({ id, encodedVideoPath: pathValue });
+          break;
+
+        case AssetPathType.JPEG_THUMBNAIL:
+          await this.assetRepository.save({ id, resizePath: pathValue });
+          break;
+
+        case AssetPathType.WEBP_THUMBNAIL:
+          await this.assetRepository.save({ id, webpPath: pathValue });
+          break;
+
+        case AssetPathType.ORIGINAL:
+          await this.assetRepository.save({ id, originalPath: pathValue });
+          break;
+
+        case AssetPathType.SIDECAR:
+          await this.assetRepository.save({ 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;
+      }
+    }
+  }
+
+  async getFileReport() {
+    const fullPath = (filename: string) => resolve(filename);
+    const hasFile = (items: Set<string>, filename: string) => items.has(filename) || items.has(fullPath(filename));
+    const crawl = async (folder: StorageFolder) =>
+      new Set(await this.storageRepository.crawl({ 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) => {
+      if (!filename) {
+        return;
+      }
+      allFiles.delete(filename);
+      allFiles.delete(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 }),
+    );
+
+    let assetCount = 0;
+
+    const orphans: FileReportItemDto[] = [];
+    for await (const assets of pagination) {
+      assetCount += assets.length;
+      for (const { id, originalPath, resizePath, encodedVideoPath, webpPath, isExternal, checksum } of assets) {
+        for (const file of [originalPath, resizePath, encodedVideoPath, webpPath]) {
+          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 (resizePath && !hasFile(thumbFiles, resizePath)) {
+          orphans.push({ ...entity, pathType: AssetPathType.JPEG_THUMBNAIL, pathValue: resizePath });
+        }
+        if (webpPath && !hasFile(thumbFiles, webpPath)) {
+          orphans.push({ ...entity, pathType: AssetPathType.WEBP_THUMBNAIL, pathValue: webpPath });
+        }
+        if (encodedVideoPath && !hasFile(videoFiles, encodedVideoPath)) {
+          orphans.push({ ...entity, pathType: AssetPathType.WEBP_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 });
+      }
+    }
+
+    const people = await this.personRepository.getAll();
+    for (const { id, thumbnailPath } of people) {
+      track(thumbnailPath);
+      const entity = { entityId: id, entityType: PathEntityType.PERSON };
+      if (thumbnailPath && !hasFile(thumbFiles, thumbnailPath)) {
+        orphans.push({ ...entity, pathType: PersonPathType.FACE, pathValue: thumbnailPath });
+      }
+    }
+
+    this.logger.log(`Found ${assetCount} assets, ${users.length} users, ${people.length} people`);
+
+    const extras: string[] = [];
+    for (const file of allFiles) {
+      extras.push(file);
+    }
+
+    // send as absolute paths
+    for (const orphan of orphans) {
+      orphan.pathValue = fullPath(orphan.pathValue);
+    }
+
+    return { orphans, extras };
+  }
 }
diff --git a/server/src/domain/metadata/metadata.service.ts b/server/src/domain/metadata/metadata.service.ts
index cc03537f53..2779df54c6 100644
--- a/server/src/domain/metadata/metadata.service.ts
+++ b/server/src/domain/metadata/metadata.service.ts
@@ -289,6 +289,9 @@ export class MetadataService {
       });
       const checksum = this.cryptoRepository.hashSha1(video);
 
+      const motionPath = this.storageCore.getAndroidMotionPath(asset);
+      this.storageCore.ensureFolders(motionPath);
+
       let motionAsset = await this.assetRepository.getByChecksum(asset.ownerId, checksum);
       if (!motionAsset) {
         const createdAt = asset.fileCreatedAt ?? asset.createdAt;
@@ -300,7 +303,7 @@ export class MetadataService {
           localDateTime: createdAt,
           checksum,
           ownerId: asset.ownerId,
-          originalPath: this.storageCore.getAndroidMotionPath(asset),
+          originalPath: motionPath,
           originalFileName: asset.originalFileName,
           isVisible: false,
           isReadOnly: false,
diff --git a/server/src/domain/repositories/asset.repository.ts b/server/src/domain/repositories/asset.repository.ts
index 89a4afbf17..5266c98ae7 100644
--- a/server/src/domain/repositories/asset.repository.ts
+++ b/server/src/domain/repositories/asset.repository.ts
@@ -14,6 +14,7 @@ export interface AssetSearchOptions {
   trashedBefore?: Date;
   type?: AssetType;
   order?: 'ASC' | 'DESC';
+  withDeleted?: boolean;
 }
 
 export interface LivePhotoSearchOptions {
diff --git a/server/src/domain/server-info/server-info.service.spec.ts b/server/src/domain/server-info/server-info.service.spec.ts
index 8655f7cc52..53115594c9 100644
--- a/server/src/domain/server-info/server-info.service.spec.ts
+++ b/server/src/domain/server-info/server-info.service.spec.ts
@@ -1,40 +1,20 @@
-import {
-  newAssetRepositoryMock,
-  newMoveRepositoryMock,
-  newPersonRepositoryMock,
-  newStorageRepositoryMock,
-  newSystemConfigRepositoryMock,
-  newUserRepositoryMock,
-} from '@test';
+import { newStorageRepositoryMock, newSystemConfigRepositoryMock, newUserRepositoryMock } from '@test';
 import { serverVersion } from '../domain.constant';
-import {
-  IAssetRepository,
-  IMoveRepository,
-  IPersonRepository,
-  IStorageRepository,
-  ISystemConfigRepository,
-  IUserRepository,
-} from '../repositories';
+import { IStorageRepository, ISystemConfigRepository, IUserRepository } from '../repositories';
 import { ServerInfoService } from './server-info.service';
 
 describe(ServerInfoService.name, () => {
   let sut: ServerInfoService;
-  let assetMock: jest.Mocked<IAssetRepository>;
   let configMock: jest.Mocked<ISystemConfigRepository>;
-  let moveMock: jest.Mocked<IMoveRepository>;
-  let personMock: jest.Mocked<IPersonRepository>;
   let storageMock: jest.Mocked<IStorageRepository>;
   let userMock: jest.Mocked<IUserRepository>;
 
   beforeEach(() => {
-    assetMock = newAssetRepositoryMock();
     configMock = newSystemConfigRepositoryMock();
-    moveMock = newMoveRepositoryMock();
-    personMock = newPersonRepositoryMock();
     storageMock = newStorageRepositoryMock();
     userMock = newUserRepositoryMock();
 
-    sut = new ServerInfoService(assetMock, configMock, moveMock, personMock, userMock, storageMock);
+    sut = new ServerInfoService(configMock, userMock, storageMock);
   });
 
   it('should work', () => {
diff --git a/server/src/domain/server-info/server-info.service.ts b/server/src/domain/server-info/server-info.service.ts
index d68b484735..1406423abe 100644
--- a/server/src/domain/server-info/server-info.service.ts
+++ b/server/src/domain/server-info/server-info.service.ts
@@ -1,15 +1,7 @@
 import { Inject, Injectable } from '@nestjs/common';
 import { mimeTypes, serverVersion } from '../domain.constant';
 import { asHumanReadable } from '../domain.util';
-import {
-  IAssetRepository,
-  IMoveRepository,
-  IPersonRepository,
-  IStorageRepository,
-  ISystemConfigRepository,
-  IUserRepository,
-  UserStatsQueryResponse,
-} from '../repositories';
+import { IStorageRepository, ISystemConfigRepository, IUserRepository, UserStatsQueryResponse } from '../repositories';
 import { StorageCore, StorageFolder } from '../storage';
 import { SystemConfigCore } from '../system-config';
 import {
@@ -25,22 +17,17 @@ import {
 @Injectable()
 export class ServerInfoService {
   private configCore: SystemConfigCore;
-  private storageCore: StorageCore;
 
   constructor(
-    @Inject(IAssetRepository) assetRepository: IAssetRepository,
     @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
-    @Inject(IMoveRepository) moveRepository: IMoveRepository,
-    @Inject(IPersonRepository) personRepository: IPersonRepository,
     @Inject(IUserRepository) private userRepository: IUserRepository,
     @Inject(IStorageRepository) private storageRepository: IStorageRepository,
   ) {
     this.configCore = SystemConfigCore.create(configRepository);
-    this.storageCore = new StorageCore(storageRepository, assetRepository, moveRepository, personRepository);
   }
 
   async getInfo(): Promise<ServerInfoResponseDto> {
-    const libraryBase = this.storageCore.getBaseFolder(StorageFolder.LIBRARY);
+    const libraryBase = StorageCore.getBaseFolder(StorageFolder.LIBRARY);
     const diskInfo = await this.storageRepository.checkDiskUsage(libraryBase);
 
     const usagePercentage = (((diskInfo.total - diskInfo.free) / diskInfo.total) * 100).toFixed(2);
diff --git a/server/src/domain/storage-template/storage-template.service.ts b/server/src/domain/storage-template/storage-template.service.ts
index 6681e60625..b04ffc89a0 100644
--- a/server/src/domain/storage-template/storage-template.service.ts
+++ b/server/src/domain/storage-template/storage-template.service.ts
@@ -90,7 +90,7 @@ export class StorageTemplateService {
     }
 
     this.logger.debug('Cleaning up empty directories...');
-    const libraryFolder = this.storageCore.getBaseFolder(StorageFolder.LIBRARY);
+    const libraryFolder = StorageCore.getBaseFolder(StorageFolder.LIBRARY);
     await this.storageRepository.removeEmptyDirs(libraryFolder);
 
     this.logger.log('Finished storage template migration');
diff --git a/server/src/domain/storage/storage.core.ts b/server/src/domain/storage/storage.core.ts
index 249b2857f1..69e2bd7995 100644
--- a/server/src/domain/storage/storage.core.ts
+++ b/server/src/domain/storage/storage.core.ts
@@ -1,6 +1,6 @@
 import { AssetEntity, AssetPathType, PathType, PersonEntity, PersonPathType } from '@app/infra/entities';
 import { Logger } from '@nestjs/common';
-import { dirname, join } from 'node:path';
+import { dirname, join, resolve } from 'node:path';
 import { APP_MEDIA_LOCATION } from '../domain.constant';
 import { IAssetRepository, IMoveRepository, IPersonRepository, IStorageRepository } from '../repositories';
 
@@ -32,14 +32,14 @@ export class StorageCore {
   ) {}
 
   getFolderLocation(folder: StorageFolder, userId: string) {
-    return join(this.getBaseFolder(folder), userId);
+    return join(StorageCore.getBaseFolder(folder), userId);
   }
 
   getLibraryFolder(user: { storageLabel: string | null; id: string }) {
-    return join(this.getBaseFolder(StorageFolder.LIBRARY), user.storageLabel || user.id);
+    return join(StorageCore.getBaseFolder(StorageFolder.LIBRARY), user.storageLabel || user.id);
   }
 
-  getBaseFolder(folder: StorageFolder) {
+  static getBaseFolder(folder: StorageFolder) {
     return join(APP_MEDIA_LOCATION, folder);
   }
 
@@ -64,7 +64,11 @@ export class StorageCore {
   }
 
   isAndroidMotionPath(originalPath: string) {
-    return originalPath.startsWith(this.getBaseFolder(StorageFolder.ENCODED_VIDEO));
+    return originalPath.startsWith(StorageCore.getBaseFolder(StorageFolder.ENCODED_VIDEO));
+  }
+
+  static isImmichPath(path: string) {
+    return resolve(path).startsWith(resolve(APP_MEDIA_LOCATION));
   }
 
   async moveAssetFile(asset: AssetEntity, pathType: GeneratedAssetPath) {
@@ -135,7 +139,7 @@ export class StorageCore {
   }
 
   removeEmptyDirs(folder: StorageFolder) {
-    return this.repository.removeEmptyDirs(this.getBaseFolder(folder));
+    return this.repository.removeEmptyDirs(StorageCore.getBaseFolder(folder));
   }
 
   private savePath(pathType: PathType, id: string, newPath: string) {
diff --git a/server/src/domain/storage/storage.service.spec.ts b/server/src/domain/storage/storage.service.spec.ts
index e197dee4af..0c5531e5f6 100644
--- a/server/src/domain/storage/storage.service.spec.ts
+++ b/server/src/domain/storage/storage.service.spec.ts
@@ -1,25 +1,14 @@
-import {
-  newAssetRepositoryMock,
-  newMoveRepositoryMock,
-  newPersonRepositoryMock,
-  newStorageRepositoryMock,
-} from '@test';
-import { IAssetRepository, IMoveRepository, IPersonRepository, IStorageRepository } from '../repositories';
+import { newStorageRepositoryMock } from '@test';
+import { IStorageRepository } from '../repositories';
 import { StorageService } from './storage.service';
 
 describe(StorageService.name, () => {
   let sut: StorageService;
-  let assetMock: jest.Mocked<IAssetRepository>;
-  let moveMock: jest.Mocked<IMoveRepository>;
-  let personMock: jest.Mocked<IPersonRepository>;
   let storageMock: jest.Mocked<IStorageRepository>;
 
   beforeEach(async () => {
-    assetMock = newAssetRepositoryMock();
-    moveMock = newMoveRepositoryMock();
-    personMock = newPersonRepositoryMock();
     storageMock = newStorageRepositoryMock();
-    sut = new StorageService(assetMock, moveMock, personMock, storageMock);
+    sut = new StorageService(storageMock);
   });
 
   it('should work', () => {
diff --git a/server/src/domain/storage/storage.service.ts b/server/src/domain/storage/storage.service.ts
index 6298113130..0d7c9432e1 100644
--- a/server/src/domain/storage/storage.service.ts
+++ b/server/src/domain/storage/storage.service.ts
@@ -1,24 +1,16 @@
 import { Inject, Injectable, Logger } from '@nestjs/common';
 import { IDeleteFilesJob } from '../job';
-import { IAssetRepository, IMoveRepository, IPersonRepository, IStorageRepository } from '../repositories';
+import { IStorageRepository } from '../repositories';
 import { StorageCore, StorageFolder } from './storage.core';
 
 @Injectable()
 export class StorageService {
   private logger = new Logger(StorageService.name);
-  private storageCore: StorageCore;
 
-  constructor(
-    @Inject(IAssetRepository) assetRepository: IAssetRepository,
-    @Inject(IMoveRepository) private moveRepository: IMoveRepository,
-    @Inject(IPersonRepository) personRepository: IPersonRepository,
-    @Inject(IStorageRepository) private storageRepository: IStorageRepository,
-  ) {
-    this.storageCore = new StorageCore(storageRepository, assetRepository, moveRepository, personRepository);
-  }
+  constructor(@Inject(IStorageRepository) private storageRepository: IStorageRepository) {}
 
   init() {
-    const libraryBase = this.storageCore.getBaseFolder(StorageFolder.LIBRARY);
+    const libraryBase = StorageCore.getBaseFolder(StorageFolder.LIBRARY);
     this.storageRepository.mkdirSync(libraryBase);
   }
 
diff --git a/server/src/immich/controllers/audit.controller.ts b/server/src/immich/controllers/audit.controller.ts
index bb720323fd..a50d33741d 100644
--- a/server/src/immich/controllers/audit.controller.ts
+++ b/server/src/immich/controllers/audit.controller.ts
@@ -1,7 +1,16 @@
-import { AuditDeletesDto, AuditDeletesResponseDto, AuditService, AuthUserDto } from '@app/domain';
-import { Controller, Get, Query } from '@nestjs/common';
+import {
+  AuditDeletesDto,
+  AuditDeletesResponseDto,
+  AuditService,
+  AuthUserDto,
+  FileChecksumDto,
+  FileChecksumResponseDto,
+  FileReportDto,
+  FileReportFixDto,
+} from '@app/domain';
+import { Body, Controller, Get, Post, Query } from '@nestjs/common';
 import { ApiTags } from '@nestjs/swagger';
-import { AuthUser, Authenticated } from '../app.guard';
+import { AdminRoute, AuthUser, Authenticated } from '../app.guard';
 import { UseValidation } from '../app.utils';
 
 @ApiTags('Audit')
@@ -15,4 +24,22 @@ export class AuditController {
   getAuditDeletes(@AuthUser() authUser: AuthUserDto, @Query() dto: AuditDeletesDto): Promise<AuditDeletesResponseDto> {
     return this.service.getDeletes(authUser, dto);
   }
+
+  @AdminRoute()
+  @Get('file-report')
+  getAuditFiles(): Promise<FileReportDto> {
+    return this.service.getFileReport();
+  }
+
+  @AdminRoute()
+  @Post('file-report/checksum')
+  getFileChecksums(@Body() dto: FileChecksumDto): Promise<FileChecksumResponseDto[]> {
+    return this.service.getChecksums(dto);
+  }
+
+  @AdminRoute()
+  @Post('file-report/fix')
+  fixAuditFiles(@Body() dto: FileReportFixDto): Promise<void> {
+    return this.service.fixItems(dto.items);
+  }
 }
diff --git a/server/src/infra/entities/move.entity.ts b/server/src/infra/entities/move.entity.ts
index daeb7f4b40..de20cb9737 100644
--- a/server/src/infra/entities/move.entity.ts
+++ b/server/src/infra/entities/move.entity.ts
@@ -34,4 +34,8 @@ export enum PersonPathType {
   FACE = 'face',
 }
 
-export type PathType = AssetPathType | PersonPathType;
+export enum UserPathType {
+  PROFILE = 'profile',
+}
+
+export type PathType = AssetPathType | PersonPathType | UserPathType;
diff --git a/server/src/infra/repositories/asset.repository.ts b/server/src/infra/repositories/asset.repository.ts
index 37153d86fb..362b00d52b 100644
--- a/server/src/infra/repositories/asset.repository.ts
+++ b/server/src/infra/repositories/asset.repository.ts
@@ -174,7 +174,7 @@ export class AssetRepository implements IAssetRepository {
           person: true,
         },
       },
-      withDeleted: !!options.trashedBefore,
+      withDeleted: options.withDeleted ?? !!options.trashedBefore,
       order: {
         // Ensures correct order when paginating
         createdAt: options.order ?? 'ASC',
diff --git a/web/src/api/api.ts b/web/src/api/api.ts
index 00b60dfca8..9beb370d35 100644
--- a/web/src/api/api.ts
+++ b/web/src/api/api.ts
@@ -19,6 +19,7 @@ import {
   SystemConfigApi,
   UserApi,
   UserApiFp,
+  AuditApi,
 } from './open-api';
 import { BASE_PATH } from './open-api/base';
 import { DUMMY_BASE_URL, toPathString } from './open-api/common';
@@ -28,6 +29,7 @@ export class ImmichApi {
   public albumApi: AlbumApi;
   public libraryApi: LibraryApi;
   public assetApi: AssetApi;
+  public auditApi: AuditApi;
   public authenticationApi: AuthenticationApi;
   public jobApi: JobApi;
   public keyApi: APIKeyApi;
@@ -51,6 +53,7 @@ export class ImmichApi {
     this.config = new Configuration(params);
 
     this.albumApi = new AlbumApi(this.config);
+    this.auditApi = new AuditApi(this.config);
     this.libraryApi = new LibraryApi(this.config);
     this.assetApi = new AssetApi(this.config);
     this.authenticationApi = new AuthenticationApi(this.config);
diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts
index 0c8d2673cc..549cc59d0a 100644
--- a/web/src/api/open-api/api.ts
+++ b/web/src/api/open-api/api.ts
@@ -1604,6 +1604,109 @@ export interface ExifResponseDto {
      */
     'timeZone'?: string | null;
 }
+/**
+ * 
+ * @export
+ * @interface FileChecksumDto
+ */
+export interface FileChecksumDto {
+    /**
+     * 
+     * @type {Array<string>}
+     * @memberof FileChecksumDto
+     */
+    'filenames': Array<string>;
+}
+/**
+ * 
+ * @export
+ * @interface FileChecksumResponseDto
+ */
+export interface FileChecksumResponseDto {
+    /**
+     * 
+     * @type {string}
+     * @memberof FileChecksumResponseDto
+     */
+    'checksum': string;
+    /**
+     * 
+     * @type {string}
+     * @memberof FileChecksumResponseDto
+     */
+    'filename': string;
+}
+/**
+ * 
+ * @export
+ * @interface FileReportDto
+ */
+export interface FileReportDto {
+    /**
+     * 
+     * @type {Array<string>}
+     * @memberof FileReportDto
+     */
+    'extras': Array<string>;
+    /**
+     * 
+     * @type {Array<FileReportItemDto>}
+     * @memberof FileReportDto
+     */
+    'orphans': Array<FileReportItemDto>;
+}
+/**
+ * 
+ * @export
+ * @interface FileReportFixDto
+ */
+export interface FileReportFixDto {
+    /**
+     * 
+     * @type {Array<FileReportItemDto>}
+     * @memberof FileReportFixDto
+     */
+    'items': Array<FileReportItemDto>;
+}
+/**
+ * 
+ * @export
+ * @interface FileReportItemDto
+ */
+export interface FileReportItemDto {
+    /**
+     * 
+     * @type {string}
+     * @memberof FileReportItemDto
+     */
+    'checksum'?: string;
+    /**
+     * 
+     * @type {string}
+     * @memberof FileReportItemDto
+     */
+    'entityId': string;
+    /**
+     * 
+     * @type {PathEntityType}
+     * @memberof FileReportItemDto
+     */
+    'entityType': PathEntityType;
+    /**
+     * 
+     * @type {PathType}
+     * @memberof FileReportItemDto
+     */
+    'pathType': PathType;
+    /**
+     * 
+     * @type {string}
+     * @memberof FileReportItemDto
+     */
+    'pathValue': string;
+}
+
+
 /**
  * 
  * @export
@@ -2186,6 +2289,40 @@ export interface OAuthConfigResponseDto {
      */
     'url'?: string;
 }
+/**
+ * 
+ * @export
+ * @enum {string}
+ */
+
+export const PathEntityType = {
+    Asset: 'asset',
+    Person: 'person',
+    User: 'user'
+} as const;
+
+export type PathEntityType = typeof PathEntityType[keyof typeof PathEntityType];
+
+
+/**
+ * 
+ * @export
+ * @enum {string}
+ */
+
+export const PathType = {
+    Original: 'original',
+    JpegThumbnail: 'jpeg_thumbnail',
+    WebpThumbnail: 'webp_thumbnail',
+    EncodedVideo: 'encoded_video',
+    Sidecar: 'sidecar',
+    Face: 'face',
+    Profile: 'profile'
+} as const;
+
+export type PathType = typeof PathType[keyof typeof PathType];
+
+
 /**
  * 
  * @export
@@ -8821,6 +8958,50 @@ export class AssetApi extends BaseAPI {
  */
 export const AuditApiAxiosParamCreator = function (configuration?: Configuration) {
     return {
+        /**
+         * 
+         * @param {FileReportFixDto} fileReportFixDto 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        fixAuditFiles: async (fileReportFixDto: FileReportFixDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            // verify required parameter 'fileReportFixDto' is not null or undefined
+            assertParamExists('fixAuditFiles', 'fileReportFixDto', fileReportFixDto)
+            const localVarPath = `/audit/file-report/fix`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+
+            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication cookie required
+
+            // authentication api_key required
+            await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
+
+            // authentication bearer required
+            // http bearer authentication required
+            await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+
+    
+            localVarHeaderParameter['Content-Type'] = 'application/json';
+
+            setSearchParams(localVarUrlObj, localVarQueryParameter);
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+            localVarRequestOptions.data = serializeDataIfNeeded(fileReportFixDto, localVarRequestOptions, configuration)
+
+            return {
+                url: toPathString(localVarUrlObj),
+                options: localVarRequestOptions,
+            };
+        },
         /**
          * 
          * @param {EntityType} entityType 
@@ -8875,6 +9056,88 @@ export const AuditApiAxiosParamCreator = function (configuration?: Configuration
             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 
+            return {
+                url: toPathString(localVarUrlObj),
+                options: localVarRequestOptions,
+            };
+        },
+        /**
+         * 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        getAuditFiles: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/audit/file-report`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+
+            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication cookie required
+
+            // authentication api_key required
+            await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
+
+            // authentication bearer required
+            // http bearer authentication required
+            await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+
+    
+            setSearchParams(localVarUrlObj, localVarQueryParameter);
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+            return {
+                url: toPathString(localVarUrlObj),
+                options: localVarRequestOptions,
+            };
+        },
+        /**
+         * 
+         * @param {FileChecksumDto} fileChecksumDto 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        getFileChecksums: async (fileChecksumDto: FileChecksumDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            // verify required parameter 'fileChecksumDto' is not null or undefined
+            assertParamExists('getFileChecksums', 'fileChecksumDto', fileChecksumDto)
+            const localVarPath = `/audit/file-report/checksum`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+
+            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication cookie required
+
+            // authentication api_key required
+            await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
+
+            // authentication bearer required
+            // http bearer authentication required
+            await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+
+    
+            localVarHeaderParameter['Content-Type'] = 'application/json';
+
+            setSearchParams(localVarUrlObj, localVarQueryParameter);
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+            localVarRequestOptions.data = serializeDataIfNeeded(fileChecksumDto, localVarRequestOptions, configuration)
+
             return {
                 url: toPathString(localVarUrlObj),
                 options: localVarRequestOptions,
@@ -8890,6 +9153,16 @@ export const AuditApiAxiosParamCreator = function (configuration?: Configuration
 export const AuditApiFp = function(configuration?: Configuration) {
     const localVarAxiosParamCreator = AuditApiAxiosParamCreator(configuration)
     return {
+        /**
+         * 
+         * @param {FileReportFixDto} fileReportFixDto 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async fixAuditFiles(fileReportFixDto: FileReportFixDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.fixAuditFiles(fileReportFixDto, options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
         /**
          * 
          * @param {EntityType} entityType 
@@ -8902,6 +9175,25 @@ export const AuditApiFp = function(configuration?: Configuration) {
             const localVarAxiosArgs = await localVarAxiosParamCreator.getAuditDeletes(entityType, after, userId, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
+        /**
+         * 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async getAuditFiles(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<FileReportDto>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.getAuditFiles(options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
+        /**
+         * 
+         * @param {FileChecksumDto} fileChecksumDto 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async getFileChecksums(fileChecksumDto: FileChecksumDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<FileChecksumResponseDto>>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.getFileChecksums(fileChecksumDto, options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
     }
 };
 
@@ -8912,6 +9204,15 @@ export const AuditApiFp = function(configuration?: Configuration) {
 export const AuditApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
     const localVarFp = AuditApiFp(configuration)
     return {
+        /**
+         * 
+         * @param {AuditApiFixAuditFilesRequest} requestParameters Request parameters.
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        fixAuditFiles(requestParameters: AuditApiFixAuditFilesRequest, options?: AxiosRequestConfig): AxiosPromise<void> {
+            return localVarFp.fixAuditFiles(requestParameters.fileReportFixDto, options).then((request) => request(axios, basePath));
+        },
         /**
          * 
          * @param {AuditApiGetAuditDeletesRequest} requestParameters Request parameters.
@@ -8921,9 +9222,40 @@ export const AuditApiFactory = function (configuration?: Configuration, basePath
         getAuditDeletes(requestParameters: AuditApiGetAuditDeletesRequest, options?: AxiosRequestConfig): AxiosPromise<AuditDeletesResponseDto> {
             return localVarFp.getAuditDeletes(requestParameters.entityType, requestParameters.after, requestParameters.userId, options).then((request) => request(axios, basePath));
         },
+        /**
+         * 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        getAuditFiles(options?: AxiosRequestConfig): AxiosPromise<FileReportDto> {
+            return localVarFp.getAuditFiles(options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @param {AuditApiGetFileChecksumsRequest} requestParameters Request parameters.
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        getFileChecksums(requestParameters: AuditApiGetFileChecksumsRequest, options?: AxiosRequestConfig): AxiosPromise<Array<FileChecksumResponseDto>> {
+            return localVarFp.getFileChecksums(requestParameters.fileChecksumDto, options).then((request) => request(axios, basePath));
+        },
     };
 };
 
+/**
+ * Request parameters for fixAuditFiles operation in AuditApi.
+ * @export
+ * @interface AuditApiFixAuditFilesRequest
+ */
+export interface AuditApiFixAuditFilesRequest {
+    /**
+     * 
+     * @type {FileReportFixDto}
+     * @memberof AuditApiFixAuditFiles
+     */
+    readonly fileReportFixDto: FileReportFixDto
+}
+
 /**
  * Request parameters for getAuditDeletes operation in AuditApi.
  * @export
@@ -8952,6 +9284,20 @@ export interface AuditApiGetAuditDeletesRequest {
     readonly userId?: string
 }
 
+/**
+ * Request parameters for getFileChecksums operation in AuditApi.
+ * @export
+ * @interface AuditApiGetFileChecksumsRequest
+ */
+export interface AuditApiGetFileChecksumsRequest {
+    /**
+     * 
+     * @type {FileChecksumDto}
+     * @memberof AuditApiGetFileChecksums
+     */
+    readonly fileChecksumDto: FileChecksumDto
+}
+
 /**
  * AuditApi - object-oriented interface
  * @export
@@ -8959,6 +9305,17 @@ export interface AuditApiGetAuditDeletesRequest {
  * @extends {BaseAPI}
  */
 export class AuditApi extends BaseAPI {
+    /**
+     * 
+     * @param {AuditApiFixAuditFilesRequest} requestParameters Request parameters.
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof AuditApi
+     */
+    public fixAuditFiles(requestParameters: AuditApiFixAuditFilesRequest, options?: AxiosRequestConfig) {
+        return AuditApiFp(this.configuration).fixAuditFiles(requestParameters.fileReportFixDto, options).then((request) => request(this.axios, this.basePath));
+    }
+
     /**
      * 
      * @param {AuditApiGetAuditDeletesRequest} requestParameters Request parameters.
@@ -8969,6 +9326,27 @@ export class AuditApi extends BaseAPI {
     public getAuditDeletes(requestParameters: AuditApiGetAuditDeletesRequest, options?: AxiosRequestConfig) {
         return AuditApiFp(this.configuration).getAuditDeletes(requestParameters.entityType, requestParameters.after, requestParameters.userId, options).then((request) => request(this.axios, this.basePath));
     }
+
+    /**
+     * 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof AuditApi
+     */
+    public getAuditFiles(options?: AxiosRequestConfig) {
+        return AuditApiFp(this.configuration).getAuditFiles(options).then((request) => request(this.axios, this.basePath));
+    }
+
+    /**
+     * 
+     * @param {AuditApiGetFileChecksumsRequest} requestParameters Request parameters.
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof AuditApi
+     */
+    public getFileChecksums(requestParameters: AuditApiGetFileChecksumsRequest, options?: AxiosRequestConfig) {
+        return AuditApiFp(this.configuration).getFileChecksums(requestParameters.fileChecksumDto, options).then((request) => request(this.axios, this.basePath));
+    }
 }
 
 
diff --git a/web/src/lib/assets/empty-4.svg b/web/src/lib/assets/empty-4.svg
new file mode 100644
index 0000000000..05aeb2b2af
--- /dev/null
+++ b/web/src/lib/assets/empty-4.svg
@@ -0,0 +1 @@
+<svg width="900" height="600" viewBox="0 0 900 600" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill="transparent" d="M0 0h900v600H0z"/><path d="M214.359 475.389c16.42 16.712 47.124 13.189 47.124 13.189s4.064-30.62-12.372-47.322c-16.419-16.712-47.109-13.198-47.109-13.198s-4.063 30.619 12.357 47.331z" fill="url(#a)"/><path d="M639.439 125.517c-17.194 9.808-41.345-.121-41.345-.121s3.743-25.827 20.946-35.623c17.194-9.808 41.335.11 41.335.11s-3.743 25.827-20.936 35.634z" fill="url(#b)"/><path d="M324.812 156.133c-17.672 17.987-50.72 14.194-50.72 14.194s-4.373-32.955 13.316-50.931c17.673-17.987 50.704-14.206 50.704-14.206s4.373 32.956-13.3 50.943z" fill="url(#c)"/><ellipse rx="15.17" ry="15.928" transform="matrix(1 0 0 -1 228.07 341.957)" fill="#E1E4E5"/><circle r="8.5" transform="matrix(1 0 0 -1 478.5 509.5)" fill="#9d9ea3"/><circle r="17.518" transform="matrix(1 0 0 -1 693.518 420.518)" fill="#9d9ea3"/><circle cx="708.183" cy="266.183" r="14.183" fill="#4F4F51"/><circle cx="247.603" cy="225.621" r="12.136" fill="#F8AE9D"/><ellipse cx="316.324" cy="510.867" rx="7.324" ry="6.867" fill="#E1E4E5"/><ellipse cx="664.796" cy="371.388" rx="9.796" ry="9.388" fill="#E1E4E5"/><circle cx="625.378" cy="479.378" r="11.377" fill="#E1E4E5"/><ellipse cx="401.025" cy="114.39" rx="5.309" ry="6.068" fill="#E1E4E5"/><circle cx="661.834" cy="300.834" r="5.58" transform="rotate(105 661.834 300.834)" fill="#E1E4E5"/><circle cx="654.769" cy="226.082" r="7.585" fill="#E1E4E5"/><ellipse cx="254.159" cy="284.946" rx="5.309" ry="4.551" fill="#E1E4E5"/><circle cx="521.363" cy="106.27" r="11.613" transform="rotate(105 521.363 106.27)" fill="#E1E4E5"/><path d="M162.314 308.103h-.149C161.284 320.589 152 320.781 152 320.781s10.238.2 10.238 14.628c0-14.428 10.238-14.628 10.238-14.628s-9.281-.192-10.162-12.678zm531.83-158.512h-.256c-1.518 21.504-17.507 21.835-17.507 21.835s17.632.345 17.632 25.192c0-24.847 17.632-25.192 17.632-25.192s-15.983-.331-17.501-21.835z" fill="#E1E4E5"/><path fill-rule="evenodd" clip-rule="evenodd" d="M553.714 397.505v56.123c0 20.672-16.743 37.416-37.415 37.416H329.22c-20.672 0-37.415-16.744-37.415-37.416V266.55c0-20.672 16.743-37.416 37.415-37.416h56.124" fill="url(#d)"/><path fill-rule="evenodd" clip-rule="evenodd" d="M363.07 155.431h214.049c26.28 0 47.566 21.286 47.566 47.566v214.049c0 26.28-21.286 47.566-47.566 47.566H363.07c-26.28 0-47.566-21.286-47.566-47.566V202.997c0-26.28 21.286-47.566 47.566-47.566z" fill="#9d9ea3"/><path d="m425.113 307.765 33.925 33.924 74.038-74.059" stroke="#fff" stroke-width="32.125" stroke-linecap="round" stroke-linejoin="round"/><defs><linearGradient id="a" x1="279.871" y1="532.474" x2="161.165" y2="346.391" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"/><stop offset="1" stop-color="#EEE"/></linearGradient><linearGradient id="b" x1="573.046" y1="156.85" x2="712.364" y2="32.889" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"/><stop offset="1" stop-color="#EEE"/></linearGradient><linearGradient id="c" x1="254.302" y1="217.573" x2="382.065" y2="17.293" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"/><stop offset="1" stop-color="#EEE"/></linearGradient><linearGradient id="d" x1="417.175" y1="82.293" x2="425.251" y2="775.957" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"/><stop offset="1" stop-color="#EEE"/></linearGradient></defs></svg>
\ No newline at end of file
diff --git a/web/src/lib/components/elements/buttons/link-button.svelte b/web/src/lib/components/elements/buttons/link-button.svelte
index 0d0d369077..d5fe8b29b5 100644
--- a/web/src/lib/components/elements/buttons/link-button.svelte
+++ b/web/src/lib/components/elements/buttons/link-button.svelte
@@ -6,8 +6,9 @@
   import Button from './button.svelte';
 
   export let color: Color = 'transparent-gray';
+  export let disabled = false;
 </script>
 
-<Button size="link" {color} shadow={false} rounded="lg" on:click>
+<Button size="link" {color} shadow={false} rounded="lg" {disabled} on:click>
   <slot />
 </Button>
diff --git a/web/src/lib/components/shared-components/side-bar/admin-side-bar.svelte b/web/src/lib/components/shared-components/side-bar/admin-side-bar.svelte
index 6a88602d6b..76f87a3d92 100644
--- a/web/src/lib/components/shared-components/side-bar/admin-side-bar.svelte
+++ b/web/src/lib/components/shared-components/side-bar/admin-side-bar.svelte
@@ -7,6 +7,7 @@
   import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
   import Cog from 'svelte-material-icons/Cog.svelte';
   import Server from 'svelte-material-icons/Server.svelte';
+  import Tools from 'svelte-material-icons/Tools.svelte';
   import Sync from 'svelte-material-icons/Sync.svelte';
 </script>
 
@@ -27,6 +28,9 @@
   <a data-sveltekit-preload-data="hover" href={AppRoute.ADMIN_STATS} draggable="false">
     <SideBarButton title="Server Stats" logo={Server} isSelected={$page.route.id === AppRoute.ADMIN_STATS} />
   </a>
+  <a data-sveltekit-preload-data="hover" href={AppRoute.ADMIN_REPAIR} draggable="false">
+    <SideBarButton title="Repair" logo={Tools} isSelected={$page.route.id === AppRoute.ADMIN_REPAIR} />
+  </a>
   <div class="mb-6 mt-auto">
     <StatusBox />
   </div>
diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts
index 33d311ed08..71572ef7c3 100644
--- a/web/src/lib/constants.ts
+++ b/web/src/lib/constants.ts
@@ -12,6 +12,7 @@ export enum AppRoute {
   ADMIN_SETTINGS = '/admin/system-settings',
   ADMIN_STATS = '/admin/server-status',
   ADMIN_JOBS = '/admin/jobs-status',
+  ADMIN_REPAIR = '/admin/repair',
 
   ALBUMS = '/albums',
   LIBRARIES = '/libraries',
diff --git a/web/src/routes/admin/repair/+page.server.ts b/web/src/routes/admin/repair/+page.server.ts
new file mode 100644
index 0000000000..9f04e013ca
--- /dev/null
+++ b/web/src/routes/admin/repair/+page.server.ts
@@ -0,0 +1,26 @@
+import { AppRoute } from '$lib/constants';
+import { redirect } from '@sveltejs/kit';
+import type { PageServerLoad } from './$types';
+
+export const load = (async ({ parent, locals: { api } }) => {
+  const { user } = await parent();
+
+  if (!user) {
+    throw redirect(302, AppRoute.AUTH_LOGIN);
+  } else if (!user.isAdmin) {
+    throw redirect(302, AppRoute.PHOTOS);
+  }
+
+  const {
+    data: { orphans, extras },
+  } = await api.auditApi.getAuditFiles();
+
+  return {
+    user,
+    orphans,
+    extras,
+    meta: {
+      title: 'Repair',
+    },
+  };
+}) satisfies PageServerLoad;
diff --git a/web/src/routes/admin/repair/+page.svelte b/web/src/routes/admin/repair/+page.svelte
new file mode 100644
index 0000000000..57b203df85
--- /dev/null
+++ b/web/src/routes/admin/repair/+page.svelte
@@ -0,0 +1,336 @@
+<script lang="ts">
+  import empty4Url from '$lib/assets/empty-4.svg';
+  import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
+  import LinkButton from '$lib/components/elements/buttons/link-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/stores/download';
+  import { downloadBlob } from '$lib/utils/asset-utils';
+  import { handleError } from '$lib/utils/handle-error';
+  import { FileReportItemDto, api, copyToClipboard } from '@api';
+  import CheckAll from 'svelte-material-icons/CheckAll.svelte';
+  import ContentCopy from 'svelte-material-icons/ContentCopy.svelte';
+  import Download from 'svelte-material-icons/Download.svelte';
+  import Refresh from 'svelte-material-icons/Refresh.svelte';
+  import Wrench from 'svelte-material-icons/Wrench.svelte';
+  import type { PageData } from './$types';
+
+  export let data: PageData;
+
+  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 = false;
+  let repairing = false;
+
+  let orphans: FileReportItemDto[] = data.orphans;
+  let extras: UntrackedFile[] = normalize(data.extras);
+  let matches: Match[] = [];
+
+  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), 5_000);
+    }
+
+    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), 5_000);
+    }
+  };
+
+  const handleRepair = async () => {
+    if (matches.length === 0) {
+      return;
+    }
+
+    repairing = true;
+
+    try {
+      await api.auditApi.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: `Repaired ${matches.length} items`,
+      });
+
+      matches = [];
+    } catch (error) {
+      handleError(error, '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 { data: report } = await api.auditApi.getAuditFiles();
+
+      orphans = report.orphans;
+      extras = normalize(report.extras);
+
+      notificationController.show({ message: 'Refreshed', type: NotificationType.Info });
+    } catch (error) {
+      handleError(error, 'Unable to load items');
+    }
+  };
+
+  const handleCheckOne = async (filename: string) => {
+    try {
+      const matched = await loadAndMatch([filename]);
+      if (matched) {
+        notificationController.show({ message: `Matched 1 item`, type: NotificationType.Info });
+      }
+    } catch (error) {
+      handleError(error, 'Unable to check item');
+    }
+  };
+
+  const handleCheckAll = async () => {
+    checking = true;
+
+    let count = 0;
+
+    try {
+      const chunkSize = 10;
+      const filenames = [...extras.filter(({ checksum }) => !checksum).map(({ filename }) => filename)];
+      for (let i = 0; i < filenames.length; i += chunkSize) {
+        count += await loadAndMatch(filenames.slice(i, i + chunkSize));
+      }
+    } catch (error) {
+      handleError(error, 'Unable to check items');
+    } finally {
+      checking = false;
+    }
+
+    notificationController.show({ message: `Matched ${count} items`, type: NotificationType.Info });
+  };
+
+  const loadAndMatch = async (filenames: string[]) => {
+    const { data: items } = await api.auditApi.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 user={data.user} title={data.meta.title} admin>
+  <svelte:fragment slot="sidebar" />
+  <div class="flex justify-end gap-2" slot="buttons">
+    <LinkButton on:click={() => handleRepair()} disabled={matches.length === 0 || repairing}>
+      <div class="flex place-items-center gap-2 text-sm">
+        <Wrench size="18" />
+        Repair All
+      </div>
+    </LinkButton>
+    <LinkButton on:click={() => handleCheckAll()} disabled={extras.length === 0 || checking}>
+      <div class="flex place-items-center gap-2 text-sm">
+        <CheckAll size="18" />
+        Check All
+      </div>
+    </LinkButton>
+    <LinkButton on:click={() => handleDownload()} disabled={extras.length + orphans.length === 0}>
+      <div class="flex place-items-center gap-2 text-sm">
+        <Download size="18" />
+        Export
+      </div>
+    </LinkButton>
+    <LinkButton on:click={() => handleRefresh()}>
+      <div class="flex place-items-center gap-2 text-sm">
+        <Refresh size="18" />
+        Refresh
+      </div>
+    </LinkButton>
+  </div>
+  <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="Untracked and missing files will show up here"
+            alt="Empty report"
+            src={empty4Url}
+          />
+        </div>
+      {:else}
+        <div class="gap-2">
+          <table class="table-fixed mt-5 w-full text-left">
+            <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>MATCHES {matches.length ? `(${matches.length})` : ''}</p>
+                    <p class="text-gray-600 dark:text-gray-300 mt-1">These files are matched by their checksums</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"
+                  on:click={() => 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-left">
+            <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>OFFLINE PATHS {orphans.length ? `(${orphans.length})` : ''}</p>
+                    <p class="text-gray-600 dark:text-gray-300 mt-1">
+                      These files are the results of manually deletion of the default upload library
+                    </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 on:click={() => copyToClipboard(orphan.pathValue)}>
+                    <CircleIconButton logo={ContentCopy} size="18" />
+                  </td>
+                  <td class="truncate text-sm font-mono text-left" 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-left 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>UNTRACKS FILES {extras.length ? `(${extras.length})` : ''}</p>
+                    <p class="text-gray-600 dark:text-gray-300 mt-1">
+                      These files are not tracked by the application. They can be the results of failed moves,
+                      interrupted uploads, or left behind due to a bug
+                    </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"
+                  on:click={() => handleCheckOne(extra.filename)}
+                  title={extra.filename}
+                >
+                  <td on:click={() => copyToClipboard(extra.filename)}>
+                    <CircleIconButton logo={ContentCopy} size="18" />
+                  </td>
+                  <td class="w-full text-md text-ellipsis flex justify-between pr-5">
+                    <span class="text-ellipsis grow truncate font-mono text-sm pr-5" title={extra.filename}
+                      >{extra.filename}</span
+                    >
+                    <span class="text-sm font-mono dark:text-immich-dark-primary text-immich-primary pr-5">
+                      {#if extra.checksum}
+                        [sha1:{extra.checksum}]
+                      {/if}
+                    </span>
+                  </td>
+                </tr>
+              {/each}
+            </tbody>
+          </table>
+        </div>
+      {/if}
+    </section>
+  </section>
+</UserPageLayout>