diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index d8ff4d30f..3709c9353 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -139,6 +139,7 @@ Class | Method | HTTP request | Description *LibraryApi* | [**getLibrary**](doc//LibraryApi.md#getlibrary) | **GET** /library/{id} | *LibraryApi* | [**getLibraryStatistics**](doc//LibraryApi.md#getlibrarystatistics) | **GET** /library/{id}/statistics | *LibraryApi* | [**removeOfflineFiles**](doc//LibraryApi.md#removeofflinefiles) | **POST** /library/{id}/removeOffline | +*LibraryApi* | [**scanDeletedFiles**](doc//LibraryApi.md#scandeletedfiles) | **POST** /library/{id}/scanDeleted | *LibraryApi* | [**scanLibrary**](doc//LibraryApi.md#scanlibrary) | **POST** /library/{id}/scan | *LibraryApi* | [**updateLibrary**](doc//LibraryApi.md#updatelibrary) | **PUT** /library/{id} | *LibraryApi* | [**validate**](doc//LibraryApi.md#validate) | **POST** /library/{id}/validate | diff --git a/mobile/openapi/doc/LibraryApi.md b/mobile/openapi/doc/LibraryApi.md index 8a204788b..acb35c067 100644 --- a/mobile/openapi/doc/LibraryApi.md +++ b/mobile/openapi/doc/LibraryApi.md @@ -15,6 +15,7 @@ Method | HTTP request | Description [**getLibrary**](LibraryApi.md#getlibrary) | **GET** /library/{id} | [**getLibraryStatistics**](LibraryApi.md#getlibrarystatistics) | **GET** /library/{id}/statistics | [**removeOfflineFiles**](LibraryApi.md#removeofflinefiles) | **POST** /library/{id}/removeOffline | +[**scanDeletedFiles**](LibraryApi.md#scandeletedfiles) | **POST** /library/{id}/scanDeleted | [**scanLibrary**](LibraryApi.md#scanlibrary) | **POST** /library/{id}/scan | [**updateLibrary**](LibraryApi.md#updatelibrary) | **PUT** /library/{id} | [**validate**](LibraryApi.md#validate) | **POST** /library/{id}/validate | @@ -348,6 +349,60 @@ void (empty response body) [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **scanDeletedFiles** +> scanDeletedFiles(id) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = LibraryApi(); +final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | + +try { + api_instance.scanDeletedFiles(id); +} catch (e) { + print('Exception when calling LibraryApi->scanDeletedFiles: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **id** | **String**| | + +### Return type + +void (empty response body) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: Not defined + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **scanLibrary** > scanLibrary(id, scanLibraryDto) diff --git a/mobile/openapi/doc/ScanLibraryDto.md b/mobile/openapi/doc/ScanLibraryDto.md index cb105c5f8..e2c489d85 100644 --- a/mobile/openapi/doc/ScanLibraryDto.md +++ b/mobile/openapi/doc/ScanLibraryDto.md @@ -8,7 +8,6 @@ import 'package:openapi/api.dart'; ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**checkForOffline** | **bool** | | [optional] **refreshAllFiles** | **bool** | | [optional] **refreshModifiedFiles** | **bool** | | [optional] diff --git a/mobile/openapi/lib/api/library_api.dart b/mobile/openapi/lib/api/library_api.dart index befd0aeef..e8667726b 100644 --- a/mobile/openapi/lib/api/library_api.dart +++ b/mobile/openapi/lib/api/library_api.dart @@ -293,6 +293,46 @@ class LibraryApi { } } + /// Performs an HTTP 'POST /library/{id}/scanDeleted' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + Future scanDeletedFilesWithHttpInfo(String id,) async { + // ignore: prefer_const_declarations + final path = r'/library/{id}/scanDeleted' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + Future scanDeletedFiles(String id,) async { + final response = await scanDeletedFilesWithHttpInfo(id,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + /// Performs an HTTP 'POST /library/{id}/scan' operation and returns the [Response]. /// Parameters: /// diff --git a/mobile/openapi/lib/model/scan_library_dto.dart b/mobile/openapi/lib/model/scan_library_dto.dart index 6a857dad1..0f5dedf64 100644 --- a/mobile/openapi/lib/model/scan_library_dto.dart +++ b/mobile/openapi/lib/model/scan_library_dto.dart @@ -13,19 +13,10 @@ part of openapi.api; class ScanLibraryDto { /// Returns a new [ScanLibraryDto] instance. ScanLibraryDto({ - this.checkForOffline, this.refreshAllFiles, this.refreshModifiedFiles, }); - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - bool? checkForOffline; - /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -44,27 +35,20 @@ class ScanLibraryDto { @override bool operator ==(Object other) => identical(this, other) || other is ScanLibraryDto && - other.checkForOffline == checkForOffline && other.refreshAllFiles == refreshAllFiles && other.refreshModifiedFiles == refreshModifiedFiles; @override int get hashCode => // ignore: unnecessary_parenthesis - (checkForOffline == null ? 0 : checkForOffline!.hashCode) + (refreshAllFiles == null ? 0 : refreshAllFiles!.hashCode) + (refreshModifiedFiles == null ? 0 : refreshModifiedFiles!.hashCode); @override - String toString() => 'ScanLibraryDto[checkForOffline=$checkForOffline, refreshAllFiles=$refreshAllFiles, refreshModifiedFiles=$refreshModifiedFiles]'; + String toString() => 'ScanLibraryDto[refreshAllFiles=$refreshAllFiles, refreshModifiedFiles=$refreshModifiedFiles]'; Map toJson() { final json = {}; - if (this.checkForOffline != null) { - json[r'checkForOffline'] = this.checkForOffline; - } else { - // json[r'checkForOffline'] = null; - } if (this.refreshAllFiles != null) { json[r'refreshAllFiles'] = this.refreshAllFiles; } else { @@ -86,7 +70,6 @@ class ScanLibraryDto { final json = value.cast(); return ScanLibraryDto( - checkForOffline: mapValueOfType(json, r'checkForOffline'), refreshAllFiles: mapValueOfType(json, r'refreshAllFiles'), refreshModifiedFiles: mapValueOfType(json, r'refreshModifiedFiles'), ); diff --git a/mobile/openapi/test/library_api_test.dart b/mobile/openapi/test/library_api_test.dart index 21afeff54..4f664fa99 100644 --- a/mobile/openapi/test/library_api_test.dart +++ b/mobile/openapi/test/library_api_test.dart @@ -47,6 +47,11 @@ void main() { // TODO }); + //Future scanDeletedFiles(String id) async + test('test scanDeletedFiles', () async { + // TODO + }); + //Future scanLibrary(String id, ScanLibraryDto scanLibraryDto) async test('test scanLibrary', () async { // TODO diff --git a/mobile/openapi/test/scan_library_dto_test.dart b/mobile/openapi/test/scan_library_dto_test.dart index 8482dcd2b..2b3c75867 100644 --- a/mobile/openapi/test/scan_library_dto_test.dart +++ b/mobile/openapi/test/scan_library_dto_test.dart @@ -16,11 +16,6 @@ void main() { // final instance = ScanLibraryDto(); group('test ScanLibraryDto', () { - // bool checkForOffline - test('to test the property `checkForOffline`', () async { - // TODO - }); - // bool refreshAllFiles test('to test the property `refreshAllFiles`', () async { // TODO diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index ca87f8dd3..b64d0a5f2 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -3603,6 +3603,41 @@ ] } }, + "/library/{id}/scanDeleted": { + "post": { + "operationId": "scanDeletedFiles", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Library" + ] + } + }, "/library/{id}/statistics": { "get": { "operationId": "getLibraryStatistics", @@ -8978,9 +9013,6 @@ }, "ScanLibraryDto": { "properties": { - "checkForOffline": { - "type": "boolean" - }, "refreshAllFiles": { "type": "boolean" }, diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 2a2fbab66..2fcc7011f 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -476,7 +476,6 @@ export type UpdateLibraryDto = { name?: string; }; export type ScanLibraryDto = { - checkForOffline?: boolean; refreshAllFiles?: boolean; refreshModifiedFiles?: boolean; }; @@ -1943,6 +1942,14 @@ export function scanLibrary({ id, scanLibraryDto }: { body: scanLibraryDto }))); } +export function scanDeletedFiles({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/library/${encodeURIComponent(id)}/scanDeleted`, { + ...opts, + method: "POST" + })); +} export function getLibraryStatistics({ id }: { id: string; }, opts?: Oazapfts.RequestOpts) { diff --git a/server/src/domain/job/job.constants.ts b/server/src/domain/job/job.constants.ts index f050aec8c..162fc1dbe 100644 --- a/server/src/domain/job/job.constants.ts +++ b/server/src/domain/job/job.constants.ts @@ -74,7 +74,7 @@ export enum JobName { LIBRARY_DELETE = 'library-delete', LIBRARY_QUEUE_SCAN_ALL = 'library-queue-all-refresh', LIBRARY_QUEUE_CLEANUP = 'library-queue-cleanup', - LIBRARY_SCAN_OFFLINE = 'library-scan-offline', + LIBRARY_SCAN_DELETED = 'library-scan-deleted', LIBRARY_CHECK_OFFLINE = 'library-check-if-online', LIBRARY_REMOVE_OFFLINE = 'library-remove-offline', @@ -151,7 +151,7 @@ export const JOBS_TO_QUEUE: Record = { [JobName.LIBRARY_SCAN_ASSET]: QueueName.LIBRARY, [JobName.LIBRARY_SCAN]: QueueName.LIBRARY, [JobName.LIBRARY_DELETE]: QueueName.LIBRARY, - [JobName.LIBRARY_SCAN_OFFLINE]: QueueName.LIBRARY, + [JobName.LIBRARY_SCAN_DELETED]: QueueName.LIBRARY, [JobName.LIBRARY_REMOVE_OFFLINE]: QueueName.LIBRARY, [JobName.LIBRARY_CHECK_OFFLINE]: QueueName.LIBRARY, [JobName.LIBRARY_QUEUE_SCAN_ALL]: QueueName.LIBRARY, diff --git a/server/src/domain/library/library.dto.ts b/server/src/domain/library/library.dto.ts index 22c70ac99..5e4bb4ec6 100644 --- a/server/src/domain/library/library.dto.ts +++ b/server/src/domain/library/library.dto.ts @@ -104,9 +104,6 @@ export class ScanLibraryDto { @ValidateBoolean({ optional: true }) refreshAllFiles?: boolean; - - @ValidateBoolean({ optional: true }) - checkForOffline?: boolean; } export class SearchLibraryDto { diff --git a/server/src/domain/library/library.service.spec.ts b/server/src/domain/library/library.service.spec.ts index ba7121837..16d35ed0b 100644 --- a/server/src/domain/library/library.service.spec.ts +++ b/server/src/domain/library/library.service.spec.ts @@ -1440,16 +1440,28 @@ describe(LibraryService.name, () => { ], ]); }); + }); - it('should queue an offline file scan', async () => { + describe('queueDeletedScan', () => { + it('should not queue a deleted scan of upload library', async () => { + libraryMock.get.mockResolvedValue(libraryStub.uploadLibrary1); + + await expect(sut.queueDeletedScan(authStub.admin, libraryStub.uploadLibrary1.id)).rejects.toBeInstanceOf( + BadRequestException, + ); + + expect(jobMock.queue).not.toBeCalled(); + }); + + it('should queue a deleted file scan', async () => { libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); - await sut.queueScan(authStub.admin, libraryStub.externalLibrary1.id, { checkForOffline: true }); + await sut.queueDeletedScan(authStub.admin, libraryStub.externalLibrary1.id); expect(jobMock.queue.mock.calls).toEqual([ [ { - name: JobName.LIBRARY_SCAN_OFFLINE, + name: JobName.LIBRARY_SCAN_DELETED, data: { id: libraryStub.externalLibrary1.id, }, @@ -1457,28 +1469,6 @@ describe(LibraryService.name, () => { ], ]); }); - - it('should error when queuing a scan with checkOffline and refreshAll', async () => { - libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); - - await expect( - sut.queueScan(authStub.admin, libraryStub.externalLibrary1.id, { - refreshAllFiles: true, - checkForOffline: true, - }), - ).rejects.toBeInstanceOf(BadRequestException); - }); - - it('should error when queuing a scan with checkOffline and refreshModified', async () => { - libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); - - await expect( - sut.queueScan(authStub.admin, libraryStub.externalLibrary1.id, { - refreshModifiedFiles: true, - checkForOffline: true, - }), - ).rejects.toBeInstanceOf(BadRequestException); - }); }); describe('queueEmptyTrash', () => { diff --git a/server/src/domain/library/library.service.ts b/server/src/domain/library/library.service.ts index 165fe6a9b..089cdd6ae 100644 --- a/server/src/domain/library/library.service.ts +++ b/server/src/domain/library/library.service.ts @@ -559,11 +559,7 @@ export class LibraryService extends EventEmitter { const library = await this.repository.get(id); if (!library || library.type !== LibraryType.EXTERNAL) { - throw new BadRequestException('Can only refresh external libraries'); - } - - if (dto.checkForOffline) { - await this.jobRepository.queue({ name: JobName.LIBRARY_SCAN_OFFLINE, data: { id } }); + throw new BadRequestException('Can only scan external libraries'); } await this.jobRepository.queue({ @@ -576,6 +572,17 @@ export class LibraryService extends EventEmitter { }); } + async queueDeletedScan(auth: AuthDto, id: string) { + await this.access.requirePermission(auth, Permission.LIBRARY_UPDATE, id); + + const library = await this.repository.get(id); + if (!library || library.type !== LibraryType.EXTERNAL) { + throw new BadRequestException('Can only scan external libraries'); + } + + await this.jobRepository.queue({ name: JobName.LIBRARY_SCAN_DELETED, data: { id } }); + } + async queueRemoveOffline(auth: AuthDto, id: string) { this.logger.verbose(`Removing offline files from library: ${id}`); await this.access.requirePermission(auth, Permission.LIBRARY_UPDATE, id); diff --git a/server/src/domain/repositories/job.repository.ts b/server/src/domain/repositories/job.repository.ts index fa1258ac8..500f87c15 100644 --- a/server/src/domain/repositories/job.repository.ts +++ b/server/src/domain/repositories/job.repository.ts @@ -92,7 +92,7 @@ export type JobItem = | { name: JobName.LIBRARY_REMOVE_OFFLINE; data: IEntityJob } | { name: JobName.LIBRARY_DELETE; data: IEntityJob } | { name: JobName.LIBRARY_QUEUE_SCAN_ALL; data: IBaseJob } - | { name: JobName.LIBRARY_SCAN_OFFLINE; data: IEntityJob } + | { name: JobName.LIBRARY_SCAN_DELETED; data: IEntityJob } | { name: JobName.LIBRARY_CHECK_OFFLINE; data: IEntityJob } | { name: JobName.LIBRARY_QUEUE_CLEANUP; data: IBaseJob }; diff --git a/server/src/immich/controllers/library.controller.ts b/server/src/immich/controllers/library.controller.ts index 801dc173d..bcb0c04af 100644 --- a/server/src/immich/controllers/library.controller.ts +++ b/server/src/immich/controllers/library.controller.ts @@ -69,6 +69,12 @@ export class LibraryController { return this.service.queueScan(auth, id, dto); } + @Post(':id/scanDeleted') + @HttpCode(HttpStatus.NO_CONTENT) + scanDeletedFiles(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) { + return this.service.queueDeletedScan(auth, id); + } + @Post(':id/removeOffline') @HttpCode(HttpStatus.NO_CONTENT) removeOfflineFiles(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) { diff --git a/server/src/microservices/app.service.ts b/server/src/microservices/app.service.ts index b70e48fcb..d18ac8d08 100644 --- a/server/src/microservices/app.service.ts +++ b/server/src/microservices/app.service.ts @@ -77,7 +77,7 @@ export class AppService { [JobName.LIBRARY_SCAN_ASSET]: (data) => this.libraryService.handleAssetRefresh(data), [JobName.LIBRARY_SCAN]: (data) => this.libraryService.handleQueueAssetRefresh(data), [JobName.LIBRARY_DELETE]: (data) => this.libraryService.handleDeleteLibrary(data), - [JobName.LIBRARY_SCAN_OFFLINE]: (data) => this.libraryService.handleQueueOfflineCheck(data), + [JobName.LIBRARY_SCAN_DELETED]: (data) => this.libraryService.handleQueueOfflineCheck(data), [JobName.LIBRARY_CHECK_OFFLINE]: (data) => this.libraryService.handleOfflineCheck(data), [JobName.LIBRARY_REMOVE_OFFLINE]: (data) => this.libraryService.handleOfflineRemoval(data), [JobName.LIBRARY_QUEUE_SCAN_ALL]: (data) => this.libraryService.handleQueueAllScan(data), diff --git a/web/src/routes/admin/library-management/+page.svelte b/web/src/routes/admin/library-management/+page.svelte index 92051696c..6fd3fcece 100644 --- a/web/src/routes/admin/library-management/+page.svelte +++ b/web/src/routes/admin/library-management/+page.svelte @@ -32,6 +32,7 @@ type LibraryResponseDto, type LibraryStatsResponseDto, type UserResponseDto, + scanDeletedFiles, } from '@immich/sdk'; import { mdiDatabase, mdiDotsVertical, mdiPlusBoxOutline, mdiSync, mdiUpload } from '@mdi/js'; import { onMount } from 'svelte'; @@ -207,7 +208,7 @@ const handleScanDeleted = async (libraryId: string) => { try { - await scanLibrary({ id: libraryId, scanLibraryDto: { checkForOffline: true } }); + await scanDeletedFiles({ id: libraryId }); notificationController.show({ message: `Scanning library for deleted files`, type: NotificationType.Info,