diff --git a/e2e/src/api/specs/audit.e2e-spec.ts b/e2e/src/api/specs/audit.e2e-spec.ts deleted file mode 100644 index c6a2adbb0..000000000 --- a/e2e/src/api/specs/audit.e2e-spec.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { deleteAssets, getAuditFiles, updateAsset, type LoginResponseDto } from '@immich/sdk'; -import { asBearerAuth, utils } from 'src/utils'; -import { beforeAll, describe, expect, it } from 'vitest'; - -describe('/audits', () => { - let admin: LoginResponseDto; - - beforeAll(async () => { - await utils.resetDatabase(); - await utils.resetFilesystem(); - - admin = await utils.adminSetup(); - }); - - // TODO: Enable these tests again once #7436 is resolved as these were flaky - describe.skip('GET :/file-report', () => { - it('excludes assets without issues from report', async () => { - const [trashedAsset, archivedAsset] = await Promise.all([ - utils.createAsset(admin.accessToken), - utils.createAsset(admin.accessToken), - utils.createAsset(admin.accessToken), - ]); - - await Promise.all([ - deleteAssets({ assetBulkDeleteDto: { ids: [trashedAsset.id] } }, { headers: asBearerAuth(admin.accessToken) }), - updateAsset( - { - id: archivedAsset.id, - updateAssetDto: { isArchived: true }, - }, - { headers: asBearerAuth(admin.accessToken) }, - ), - ]); - - const body = await getAuditFiles({ - headers: asBearerAuth(admin.accessToken), - }); - - expect(body.orphans).toHaveLength(0); - expect(body.extras).toHaveLength(0); - }); - }); -}); diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index d46945f64..a4fbbf371 100644 Binary files a/mobile/openapi/README.md and b/mobile/openapi/README.md differ diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index ba64363c9..78fa31691 100644 Binary files a/mobile/openapi/lib/api.dart and b/mobile/openapi/lib/api.dart differ diff --git a/mobile/openapi/lib/api/file_reports_api.dart b/mobile/openapi/lib/api/file_reports_api.dart deleted file mode 100644 index 73b3feaed..000000000 Binary files a/mobile/openapi/lib/api/file_reports_api.dart and /dev/null differ diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 6abe576ac..9d0e80e58 100644 Binary files a/mobile/openapi/lib/api_client.dart and b/mobile/openapi/lib/api_client.dart differ diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index 5f9d15c08..eec991e90 100644 Binary files a/mobile/openapi/lib/api_helper.dart and b/mobile/openapi/lib/api_helper.dart differ diff --git a/mobile/openapi/lib/model/file_checksum_dto.dart b/mobile/openapi/lib/model/file_checksum_dto.dart deleted file mode 100644 index 7dc9ccdf2..000000000 Binary files a/mobile/openapi/lib/model/file_checksum_dto.dart and /dev/null differ diff --git a/mobile/openapi/lib/model/file_checksum_response_dto.dart b/mobile/openapi/lib/model/file_checksum_response_dto.dart deleted file mode 100644 index 7b963c8bd..000000000 Binary files a/mobile/openapi/lib/model/file_checksum_response_dto.dart and /dev/null differ diff --git a/mobile/openapi/lib/model/file_report_dto.dart b/mobile/openapi/lib/model/file_report_dto.dart deleted file mode 100644 index 3dc892e5e..000000000 Binary files a/mobile/openapi/lib/model/file_report_dto.dart and /dev/null differ diff --git a/mobile/openapi/lib/model/file_report_fix_dto.dart b/mobile/openapi/lib/model/file_report_fix_dto.dart deleted file mode 100644 index d46cdeb4b..000000000 Binary files a/mobile/openapi/lib/model/file_report_fix_dto.dart and /dev/null differ diff --git a/mobile/openapi/lib/model/file_report_item_dto.dart b/mobile/openapi/lib/model/file_report_item_dto.dart deleted file mode 100644 index 1ef08c2b4..000000000 Binary files a/mobile/openapi/lib/model/file_report_item_dto.dart and /dev/null differ diff --git a/mobile/openapi/lib/model/path_entity_type.dart b/mobile/openapi/lib/model/path_entity_type.dart deleted file mode 100644 index fdcdae4f1..000000000 Binary files a/mobile/openapi/lib/model/path_entity_type.dart and /dev/null differ diff --git a/mobile/openapi/lib/model/path_type.dart b/mobile/openapi/lib/model/path_type.dart deleted file mode 100644 index 55453ed1e..000000000 Binary files a/mobile/openapi/lib/model/path_type.dart and /dev/null differ diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 826af5a2e..adad1308c 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -4651,118 +4651,6 @@ ] } }, - "/reports": { - "get": { - "operationId": "getAuditFiles", - "parameters": [], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FileReportDto" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "File Reports" - ] - } - }, - "/reports/checksum": { - "post": { - "operationId": "getFileChecksums", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FileChecksumDto" - } - } - }, - "required": true - }, - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/FileChecksumResponseDto" - }, - "type": "array" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "File Reports" - ] - } - }, - "/reports/fix": { - "post": { - "operationId": "fixAuditFiles", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FileReportFixDto" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "File Reports" - ] - } - }, "/search/cities": { "get": { "operationId": "getAssetsByCity", @@ -9749,105 +9637,6 @@ ], "type": "object" }, - "FileChecksumDto": { - "properties": { - "filenames": { - "items": { - "type": "string" - }, - "type": "array" - } - }, - "required": [ - "filenames" - ], - "type": "object" - }, - "FileChecksumResponseDto": { - "properties": { - "checksum": { - "type": "string" - }, - "filename": { - "type": "string" - } - }, - "required": [ - "checksum", - "filename" - ], - "type": "object" - }, - "FileReportDto": { - "properties": { - "extras": { - "items": { - "type": "string" - }, - "type": "array" - }, - "orphans": { - "items": { - "$ref": "#/components/schemas/FileReportItemDto" - }, - "type": "array" - } - }, - "required": [ - "extras", - "orphans" - ], - "type": "object" - }, - "FileReportFixDto": { - "properties": { - "items": { - "items": { - "$ref": "#/components/schemas/FileReportItemDto" - }, - "type": "array" - } - }, - "required": [ - "items" - ], - "type": "object" - }, - "FileReportItemDto": { - "properties": { - "checksum": { - "type": "string" - }, - "entityId": { - "format": "uuid", - "type": "string" - }, - "entityType": { - "allOf": [ - { - "$ref": "#/components/schemas/PathEntityType" - } - ] - }, - "pathType": { - "allOf": [ - { - "$ref": "#/components/schemas/PathType" - } - ] - }, - "pathValue": { - "type": "string" - } - }, - "required": [ - "entityId", - "entityType", - "pathType", - "pathValue" - ], - "type": "object" - }, "FoldersResponse": { "properties": { "enabled": { @@ -10889,27 +10678,6 @@ ], "type": "object" }, - "PathEntityType": { - "enum": [ - "asset", - "person", - "user" - ], - "type": "string" - }, - "PathType": { - "enum": [ - "original", - "fullsize", - "preview", - "thumbnail", - "encoded_video", - "sidecar", - "face", - "profile" - ], - "type": "string" - }, "PeopleResponse": { "properties": { "enabled": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 743eeadf0..2684d2558 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -800,27 +800,6 @@ export type AssetFaceUpdateDto = { export type PersonStatisticsResponseDto = { assets: number; }; -export type FileReportItemDto = { - checksum?: string; - entityId: string; - entityType: PathEntityType; - pathType: PathType; - pathValue: string; -}; -export type FileReportDto = { - extras: string[]; - orphans: FileReportItemDto[]; -}; -export type FileChecksumDto = { - filenames: string[]; -}; -export type FileChecksumResponseDto = { - checksum: string; - filename: string; -}; -export type FileReportFixDto = { - items: FileReportItemDto[]; -}; export type SearchExploreItem = { data: AssetResponseDto; value: string; @@ -2663,35 +2642,6 @@ export function getPersonThumbnail({ id }: { ...opts })); } -export function getAuditFiles(opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: FileReportDto; - }>("/reports", { - ...opts - })); -} -export function getFileChecksums({ fileChecksumDto }: { - fileChecksumDto: FileChecksumDto; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 201; - data: FileChecksumResponseDto[]; - }>("/reports/checksum", oazapfts.json({ - ...opts, - method: "POST", - body: fileChecksumDto - }))); -} -export function fixAuditFiles({ fileReportFixDto }: { - fileReportFixDto: FileReportFixDto; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchText("/reports/fix", oazapfts.json({ - ...opts, - method: "POST", - body: fileReportFixDto - }))); -} export function getAssetsByCity(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; @@ -3751,21 +3701,6 @@ export enum PartnerDirection { SharedBy = "shared-by", SharedWith = "shared-with" } -export enum PathEntityType { - Asset = "asset", - Person = "person", - User = "user" -} -export enum PathType { - Original = "original", - Fullsize = "fullsize", - Preview = "preview", - Thumbnail = "thumbnail", - EncodedVideo = "encoded_video", - Sidecar = "sidecar", - Face = "face", - Profile = "profile" -} export enum SearchSuggestionType { Country = "country", State = "state", diff --git a/server/src/controllers/file-report.controller.ts b/server/src/controllers/file-report.controller.ts deleted file mode 100644 index a51a94a50..000000000 --- a/server/src/controllers/file-report.controller.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Body, Controller, Get, Post } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; -import { FileChecksumDto, FileChecksumResponseDto, FileReportDto, FileReportFixDto } from 'src/dtos/audit.dto'; -import { Authenticated } from 'src/middleware/auth.guard'; -import { AuditService } from 'src/services/audit.service'; - -@ApiTags('File Reports') -@Controller('reports') -export class ReportController { - constructor(private service: AuditService) {} - - @Get() - @Authenticated({ admin: true }) - getAuditFiles(): Promise { - return this.service.getFileReport(); - } - - @Post('checksum') - @Authenticated({ admin: true }) - getFileChecksums(@Body() dto: FileChecksumDto): Promise { - return this.service.getChecksums(dto); - } - - @Post('fix') - @Authenticated({ admin: true }) - fixAuditFiles(@Body() dto: FileReportFixDto): Promise { - return this.service.fixItems(dto.items); - } -} diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts index e36793b3d..9c39e580b 100644 --- a/server/src/controllers/index.ts +++ b/server/src/controllers/index.ts @@ -8,7 +8,6 @@ import { AuthController } from 'src/controllers/auth.controller'; import { DownloadController } from 'src/controllers/download.controller'; import { DuplicateController } from 'src/controllers/duplicate.controller'; import { FaceController } from 'src/controllers/face.controller'; -import { ReportController } from 'src/controllers/file-report.controller'; import { JobController } from 'src/controllers/job.controller'; import { LibraryController } from 'src/controllers/library.controller'; import { MapController } from 'src/controllers/map.controller'; @@ -53,7 +52,6 @@ export const controllers = [ OAuthController, PartnerController, PersonController, - ReportController, SearchController, ServerController, SessionController, diff --git a/server/src/dtos/audit.dto.ts b/server/src/dtos/audit.dto.ts deleted file mode 100644 index 434da46eb..000000000 --- a/server/src/dtos/audit.dto.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsArray, IsEnum, IsString, IsUUID, ValidateNested } from 'class-validator'; -import { AssetPathType, EntityType, PathType, PersonPathType, UserPathType } from 'src/enum'; -import { Optional, ValidateDate, ValidateUUID } from 'src/validation'; - -const PathEnum = Object.values({ ...AssetPathType, ...PersonPathType, ...UserPathType }); - -export class AuditDeletesDto { - @ValidateDate() - after!: Date; - - @ApiProperty({ enum: EntityType, enumName: 'EntityType' }) - @IsEnum(EntityType) - entityType!: EntityType; - - @Optional() - @IsUUID('4') - @ApiProperty({ format: 'uuid' }) - userId?: string; -} - -export enum PathEntityType { - ASSET = 'asset', - PERSON = 'person', - USER = 'user', -} - -export class AuditDeletesResponseDto { - needsFullSync!: boolean; - ids!: string[]; -} - -export class FileReportDto { - orphans!: FileReportItemDto[]; - extras!: string[]; -} - -export class FileChecksumDto { - @IsString({ each: true }) - filenames!: string[]; -} - -export class FileChecksumResponseDto { - filename!: string; - checksum!: string; -} - -export class FileReportFixDto { - @IsArray() - @ValidateNested({ each: true }) - @Type(() => FileReportItemDto) - items!: FileReportItemDto[]; -} - -// used both as request and response dto -export class FileReportItemDto { - @ValidateUUID() - entityId!: string; - - @ApiProperty({ enumName: 'PathEntityType', enum: PathEntityType }) - @IsEnum(PathEntityType) - entityType!: PathEntityType; - - @ApiProperty({ enumName: 'PathType', enum: PathEnum }) - @IsEnum(PathEnum) - pathType!: PathType; - - @IsString() - pathValue!: string; - - checksum?: string; -} diff --git a/server/src/services/audit.service.spec.ts b/server/src/services/audit.service.spec.ts index 6ef139f50..381b2ec7e 100644 --- a/server/src/services/audit.service.spec.ts +++ b/server/src/services/audit.service.spec.ts @@ -1,6 +1,4 @@ -import { BadRequestException } from '@nestjs/common'; -import { FileReportItemDto } from 'src/dtos/audit.dto'; -import { AssetFileType, AssetPathType, JobStatus, PersonPathType, UserPathType } from 'src/enum'; +import { JobStatus } from 'src/enum'; import { AuditService } from 'src/services/audit.service'; import { newTestService, ServiceMocks } from 'test/utils'; @@ -25,148 +23,4 @@ describe(AuditService.name, () => { expect(mocks.audit.removeBefore).toHaveBeenCalledWith(expect.any(Date)); }); }); - - describe('getChecksums', () => { - it('should fail if the file is not in the immich path', async () => { - await expect(sut.getChecksums({ filenames: ['foo/bar'] })).rejects.toBeInstanceOf(BadRequestException); - - expect(mocks.crypto.hashFile).not.toHaveBeenCalled(); - }); - - it('should get checksum for valid file', async () => { - await expect(sut.getChecksums({ filenames: ['./upload/my-file.jpg'] })).resolves.toEqual([ - { filename: './upload/my-file.jpg', checksum: expect.any(String) }, - ]); - - expect(mocks.crypto.hashFile).toHaveBeenCalledWith('./upload/my-file.jpg'); - }); - }); - - describe('fixItems', () => { - it('should fail if the file is not in the immich path', async () => { - await expect( - sut.fixItems([ - { entityId: 'my-id', pathType: AssetPathType.ORIGINAL, pathValue: 'foo/bar' } as FileReportItemDto, - ]), - ).rejects.toBeInstanceOf(BadRequestException); - - expect(mocks.asset.update).not.toHaveBeenCalled(); - expect(mocks.asset.upsertFile).not.toHaveBeenCalled(); - expect(mocks.person.update).not.toHaveBeenCalled(); - expect(mocks.user.update).not.toHaveBeenCalled(); - }); - - it('should update encoded video path', async () => { - await sut.fixItems([ - { - entityId: 'my-id', - pathType: AssetPathType.ENCODED_VIDEO, - pathValue: './upload/my-video.mp4', - } as FileReportItemDto, - ]); - - expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'my-id', encodedVideoPath: './upload/my-video.mp4' }); - expect(mocks.asset.upsertFile).not.toHaveBeenCalled(); - expect(mocks.person.update).not.toHaveBeenCalled(); - expect(mocks.user.update).not.toHaveBeenCalled(); - }); - - it('should update preview path', async () => { - await sut.fixItems([ - { - entityId: 'my-id', - pathType: AssetPathType.PREVIEW, - pathValue: './upload/my-preview.png', - } as FileReportItemDto, - ]); - - expect(mocks.asset.upsertFile).toHaveBeenCalledWith({ - assetId: 'my-id', - type: AssetFileType.PREVIEW, - path: './upload/my-preview.png', - }); - expect(mocks.asset.update).not.toHaveBeenCalled(); - expect(mocks.person.update).not.toHaveBeenCalled(); - expect(mocks.user.update).not.toHaveBeenCalled(); - }); - - it('should update thumbnail path', async () => { - await sut.fixItems([ - { - entityId: 'my-id', - pathType: AssetPathType.THUMBNAIL, - pathValue: './upload/my-thumbnail.webp', - } as FileReportItemDto, - ]); - - expect(mocks.asset.upsertFile).toHaveBeenCalledWith({ - assetId: 'my-id', - type: AssetFileType.THUMBNAIL, - path: './upload/my-thumbnail.webp', - }); - expect(mocks.asset.update).not.toHaveBeenCalled(); - expect(mocks.person.update).not.toHaveBeenCalled(); - expect(mocks.user.update).not.toHaveBeenCalled(); - }); - - it('should update original path', async () => { - await sut.fixItems([ - { - entityId: 'my-id', - pathType: AssetPathType.ORIGINAL, - pathValue: './upload/my-original.png', - } as FileReportItemDto, - ]); - - expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'my-id', originalPath: './upload/my-original.png' }); - expect(mocks.asset.upsertFile).not.toHaveBeenCalled(); - expect(mocks.person.update).not.toHaveBeenCalled(); - expect(mocks.user.update).not.toHaveBeenCalled(); - }); - - it('should update sidecar path', async () => { - await sut.fixItems([ - { - entityId: 'my-id', - pathType: AssetPathType.SIDECAR, - pathValue: './upload/my-sidecar.xmp', - } as FileReportItemDto, - ]); - - expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'my-id', sidecarPath: './upload/my-sidecar.xmp' }); - expect(mocks.asset.upsertFile).not.toHaveBeenCalled(); - expect(mocks.person.update).not.toHaveBeenCalled(); - expect(mocks.user.update).not.toHaveBeenCalled(); - }); - - it('should update face path', async () => { - await sut.fixItems([ - { - entityId: 'my-id', - pathType: PersonPathType.FACE, - pathValue: './upload/my-face.jpg', - } as FileReportItemDto, - ]); - - expect(mocks.person.update).toHaveBeenCalledWith({ id: 'my-id', thumbnailPath: './upload/my-face.jpg' }); - expect(mocks.asset.update).not.toHaveBeenCalled(); - expect(mocks.asset.upsertFile).not.toHaveBeenCalled(); - expect(mocks.user.update).not.toHaveBeenCalled(); - }); - - it('should update profile path', async () => { - await sut.fixItems([ - { - entityId: 'my-id', - pathType: UserPathType.PROFILE, - pathValue: './upload/my-profile-pic.jpg', - } as FileReportItemDto, - ]); - - expect(mocks.user.update).toHaveBeenCalledWith('my-id', { profileImagePath: './upload/my-profile-pic.jpg' }); - expect(mocks.asset.update).not.toHaveBeenCalled(); - expect(mocks.asset.upsertFile).not.toHaveBeenCalled(); - expect(mocks.person.update).not.toHaveBeenCalled(); - }); - }); }); diff --git a/server/src/services/audit.service.ts b/server/src/services/audit.service.ts index a049a9c64..7c9a070dd 100644 --- a/server/src/services/audit.service.ts +++ b/server/src/services/audit.service.ts @@ -1,23 +1,9 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { DateTime } from 'luxon'; -import { resolve } from 'node:path'; -import { AUDIT_LOG_MAX_DURATION, JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; -import { StorageCore } from 'src/cores/storage.core'; +import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; import { OnJob } from 'src/decorators'; -import { FileChecksumDto, FileChecksumResponseDto, FileReportItemDto, PathEntityType } from 'src/dtos/audit.dto'; -import { - AssetFileType, - AssetPathType, - JobName, - JobStatus, - PersonPathType, - QueueName, - StorageFolder, - UserPathType, -} from 'src/enum'; +import { JobName, JobStatus, QueueName } from 'src/enum'; import { BaseService } from 'src/services/base.service'; -import { getAssetFiles } from 'src/utils/asset.util'; -import { usePagination } from 'src/utils/pagination'; @Injectable() export class AuditService extends BaseService { @@ -26,187 +12,4 @@ export class AuditService extends BaseService { await this.auditRepository.removeBefore(DateTime.now().minus(AUDIT_LOG_MAX_DURATION).toJSDate()); return JobStatus.SUCCESS; } - - async getChecksums(dto: FileChecksumDto) { - const results: FileChecksumResponseDto[] = []; - for (const filename of dto.filenames) { - if (!StorageCore.isImmichPath(filename)) { - throw new BadRequestException( - `Could not get the checksum of ${filename} because the file isn't accessible by Immich`, - ); - } - - const checksum = await this.cryptoRepository.hashFile(filename); - results.push({ filename, checksum: checksum.toString('base64') }); - } - return results; - } - - async fixItems(items: FileReportItemDto[]) { - for (const { entityId: id, pathType, pathValue } of items) { - if (!StorageCore.isImmichPath(pathValue)) { - throw new BadRequestException( - `Could not fix item ${id} with path ${pathValue} because the file isn't accessible by Immich`, - ); - } - - switch (pathType) { - case AssetPathType.ENCODED_VIDEO: { - await this.assetRepository.update({ id, encodedVideoPath: pathValue }); - break; - } - - case AssetPathType.PREVIEW: { - await this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.PREVIEW, path: pathValue }); - break; - } - - case AssetPathType.THUMBNAIL: { - await this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.THUMBNAIL, path: pathValue }); - break; - } - - case AssetPathType.ORIGINAL: { - await this.assetRepository.update({ id, originalPath: pathValue }); - break; - } - - case AssetPathType.SIDECAR: { - await this.assetRepository.update({ id, sidecarPath: pathValue }); - break; - } - - case PersonPathType.FACE: { - await this.personRepository.update({ id, thumbnailPath: pathValue }); - break; - } - - case UserPathType.PROFILE: { - await this.userRepository.update(id, { profileImagePath: pathValue }); - break; - } - } - } - } - - private fullPath(filename: string) { - return resolve(filename); - } - - async getFileReport() { - const hasFile = (items: Set, filename: string) => items.has(filename) || items.has(this.fullPath(filename)); - const crawl = async (folder: StorageFolder) => - new Set( - await this.storageRepository.crawl({ - includeHidden: true, - pathsToCrawl: [StorageCore.getBaseFolder(folder)], - }), - ); - - const uploadFiles = await crawl(StorageFolder.UPLOAD); - const libraryFiles = await crawl(StorageFolder.LIBRARY); - const thumbFiles = await crawl(StorageFolder.THUMBNAILS); - const videoFiles = await crawl(StorageFolder.ENCODED_VIDEO); - const profileFiles = await crawl(StorageFolder.PROFILE); - const allFiles = new Set(); - for (const list of [libraryFiles, thumbFiles, videoFiles, profileFiles, uploadFiles]) { - for (const item of list) { - allFiles.add(item); - } - } - - const track = (filename: string | null | undefined) => { - if (!filename) { - return; - } - allFiles.delete(filename); - allFiles.delete(this.fullPath(filename)); - }; - - this.logger.log( - `Found ${libraryFiles.size} original files, ${thumbFiles.size} thumbnails, ${videoFiles.size} encoded videos, ${profileFiles.size} profile files`, - ); - const pagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (options) => - this.assetRepository.getAll(options, { withDeleted: true, withArchived: true }), - ); - - let assetCount = 0; - - const orphans: FileReportItemDto[] = []; - for await (const assets of pagination) { - assetCount += assets.length; - for (const { id, files, originalPath, encodedVideoPath, isExternal, checksum } of assets) { - const { fullsizeFile, previewFile, thumbnailFile } = getAssetFiles(files); - for (const file of [ - originalPath, - fullsizeFile?.path, - previewFile?.path, - encodedVideoPath, - thumbnailFile?.path, - ]) { - track(file); - } - - const entity = { entityId: id, entityType: PathEntityType.ASSET, checksum: checksum.toString('base64') }; - if ( - originalPath && - !hasFile(libraryFiles, originalPath) && - !hasFile(uploadFiles, originalPath) && - // Android motion assets - !hasFile(videoFiles, originalPath) && - // ignore external library assets - !isExternal - ) { - orphans.push({ ...entity, pathType: AssetPathType.ORIGINAL, pathValue: originalPath }); - } - if (previewFile && !hasFile(thumbFiles, previewFile.path)) { - orphans.push({ ...entity, pathType: AssetPathType.PREVIEW, pathValue: previewFile.path }); - } - if (thumbnailFile && !hasFile(thumbFiles, thumbnailFile.path)) { - orphans.push({ ...entity, pathType: AssetPathType.THUMBNAIL, pathValue: thumbnailFile.path }); - } - if (encodedVideoPath && !hasFile(videoFiles, encodedVideoPath)) { - orphans.push({ ...entity, pathType: AssetPathType.THUMBNAIL, pathValue: encodedVideoPath }); - } - } - } - - const users = await this.userRepository.getList(); - for (const { id, profileImagePath } of users) { - track(profileImagePath); - - const entity = { entityId: id, entityType: PathEntityType.USER }; - if (profileImagePath && !hasFile(profileFiles, profileImagePath)) { - orphans.push({ ...entity, pathType: UserPathType.PROFILE, pathValue: profileImagePath }); - } - } - - let peopleCount = 0; - for await (const { id, thumbnailPath } of this.personRepository.getAll()) { - track(thumbnailPath); - const entity = { entityId: id, entityType: PathEntityType.PERSON }; - if (thumbnailPath && !hasFile(thumbFiles, thumbnailPath)) { - orphans.push({ ...entity, pathType: PersonPathType.FACE, pathValue: thumbnailPath }); - } - - if (peopleCount === JOBS_ASSET_PAGINATION_SIZE) { - this.logger.log(`Found ${assetCount} assets, ${users.length} users, ${peopleCount} people`); - peopleCount = 0; - } - } - - this.logger.log(`Found ${assetCount} assets, ${users.length} users, ${peopleCount} people`); - - const extras: string[] = []; - for (const file of allFiles) { - extras.push(file); - } - - // send as absolute paths - for (const orphan of orphans) { - orphan.pathValue = this.fullPath(orphan.pathValue); - } - - return { orphans, extras }; - } } diff --git a/web/src/routes/admin/repair/+page.svelte b/web/src/routes/admin/repair/+page.svelte deleted file mode 100644 index 2d8ceca4d..000000000 --- a/web/src/routes/admin/repair/+page.svelte +++ /dev/null @@ -1,358 +0,0 @@ - - - - {#snippet buttons()} - - - - - - - {/snippet} -
-
- {#if matches.length + extras.length + orphans.length === 0} -
- -
- {:else} -
- - - - - - - - {#each matches as match (match.extra.filename)} - handleSplit(match)} - > - - - - {/each} - -
-
-

- {$t('matches').toUpperCase()} - {matches.length > 0 ? `(${matches.length.toLocaleString($locale)})` : ''} -

-

{$t('admin.these_files_matched_by_checksum')}

-
-
- {match.orphan.pathValue} => - {match.extra.filename} - - ({match.orphan.entityType}/{match.orphan.pathType}) -
- - - - - - - - - {#each orphans as orphan, index (index)} - - - - - - {/each} - -
-
-

- {$t('admin.offline_paths').toUpperCase()} - {orphans.length > 0 ? `(${orphans.length.toLocaleString($locale)})` : ''} -

-

- {$t('admin.offline_paths_description')} -

-
-
copyToClipboard(orphan.pathValue)}> - {}} /> - - {orphan.pathValue} - - ({orphan.entityType}) -
- - - - - - - - - {#each extras as extra (extra.filename)} - handleCheckOne(extra.filename)} - title={extra.filename} - > - - - - {/each} - -
-
-

- {$t('admin.untracked_files').toUpperCase()} - {extras.length > 0 ? `(${extras.length.toLocaleString($locale)})` : ''} -

-

- {$t('admin.untracked_files_description')} -

-
-
copyToClipboard(extra.filename)}> - {}} /> - - {extra.filename} - - {#if extra.checksum} - [sha1:{extra.checksum}] - {/if} - -
-
- {/if} -
-
-
diff --git a/web/src/routes/admin/repair/+page.ts b/web/src/routes/admin/repair/+page.ts deleted file mode 100644 index 9e52abb57..000000000 --- a/web/src/routes/admin/repair/+page.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { authenticate } from '$lib/utils/auth'; -import { getFormatter } from '$lib/utils/i18n'; -import { getAuditFiles } from '@immich/sdk'; -import type { PageLoad } from './$types'; - -export const load = (async () => { - await authenticate({ admin: true }); - const { orphans, extras } = await getAuditFiles(); - const $t = await getFormatter(); - - return { - orphans, - extras, - meta: { - title: $t('repair'), - }, - }; -}) satisfies PageLoad;