mirror of
https://github.com/immich-app/immich.git
synced 2025-03-11 02:23:09 -05:00
feat(server): library cleanup from ui (#16226)
* feat(server,web): scan all libraries from frontend * feat(server,web): scan all libraries from frontend * Add button text
This commit is contained in:
parent
8885e3105e
commit
869839f642
11 changed files with 37 additions and 24 deletions
|
@ -68,7 +68,7 @@ In rare cases, the library watcher can hang, preventing Immich from starting up.
|
||||||
|
|
||||||
### Nightly job
|
### Nightly job
|
||||||
|
|
||||||
There is an automatic scan job that is scheduled to run once a day. This job also cleans up any libraries stuck in deletion.
|
There is an automatic scan job that is scheduled to run once a day. This job also cleans up any libraries stuck in deletion. It is possible to trigger the cleanup by clicking "Scan all libraries" in the library managment page.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|
|
@ -96,7 +96,7 @@
|
||||||
"library_scanning_enable_description": "Enable periodic library scanning",
|
"library_scanning_enable_description": "Enable periodic library scanning",
|
||||||
"library_settings": "External Library",
|
"library_settings": "External Library",
|
||||||
"library_settings_description": "Manage external library settings",
|
"library_settings_description": "Manage external library settings",
|
||||||
"library_tasks_description": "Perform library tasks",
|
"library_tasks_description": "Scan external libraries for new and/or changed assets",
|
||||||
"library_watching_enable_description": "Watch external libraries for file changes",
|
"library_watching_enable_description": "Watch external libraries for file changes",
|
||||||
"library_watching_settings": "Library watching (EXPERIMENTAL)",
|
"library_watching_settings": "Library watching (EXPERIMENTAL)",
|
||||||
"library_watching_settings_description": "Automatically watch for changed files",
|
"library_watching_settings_description": "Automatically watch for changed files",
|
||||||
|
@ -336,6 +336,7 @@
|
||||||
"untracked_files": "Untracked Files",
|
"untracked_files": "Untracked Files",
|
||||||
"untracked_files_description": "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",
|
"untracked_files_description": "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",
|
||||||
"user_cleanup_job": "User cleanup",
|
"user_cleanup_job": "User cleanup",
|
||||||
|
"cleanup": "Cleanup",
|
||||||
"user_delete_delay": "<b>{user}</b>'s account and assets will be scheduled for permanent deletion in {delay, plural, one {# day} other {# days}}.",
|
"user_delete_delay": "<b>{user}</b>'s account and assets will be scheduled for permanent deletion in {delay, plural, one {# day} other {# days}}.",
|
||||||
"user_delete_delay_settings": "Delete delay",
|
"user_delete_delay_settings": "Delete delay",
|
||||||
"user_delete_delay_settings_description": "Number of days after removal to permanently delete a user's account and assets. The user deletion job runs at midnight to check for users that are ready for deletion. Changes to this setting will be evaluated at the next execution.",
|
"user_delete_delay_settings_description": "Number of days after removal to permanently delete a user's account and assets. The user deletion job runs at midnight to check for users that are ready for deletion. Changes to this setting will be evaluated at the next execution.",
|
||||||
|
@ -1114,6 +1115,7 @@
|
||||||
"say_something": "Say something",
|
"say_something": "Say something",
|
||||||
"scan_all_libraries": "Scan All Libraries",
|
"scan_all_libraries": "Scan All Libraries",
|
||||||
"scan_library": "Scan",
|
"scan_library": "Scan",
|
||||||
|
"rescan": "Rescan",
|
||||||
"scan_settings": "Scan Settings",
|
"scan_settings": "Scan Settings",
|
||||||
"scanning_for_album": "Scanning for album...",
|
"scanning_for_album": "Scanning for album...",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
|
|
|
@ -473,7 +473,7 @@ export enum JobName {
|
||||||
LIBRARY_SYNC_FILE = 'library-sync-file',
|
LIBRARY_SYNC_FILE = 'library-sync-file',
|
||||||
LIBRARY_SYNC_ASSET = 'library-sync-asset',
|
LIBRARY_SYNC_ASSET = 'library-sync-asset',
|
||||||
LIBRARY_DELETE = 'library-delete',
|
LIBRARY_DELETE = 'library-delete',
|
||||||
LIBRARY_QUEUE_SYNC_ALL = 'library-queue-sync-all',
|
LIBRARY_QUEUE_SCAN_ALL = 'library-queue-scan-all',
|
||||||
LIBRARY_QUEUE_CLEANUP = 'library-queue-cleanup',
|
LIBRARY_QUEUE_CLEANUP = 'library-queue-cleanup',
|
||||||
|
|
||||||
// cleanup
|
// cleanup
|
||||||
|
|
|
@ -170,7 +170,7 @@ export class JobService extends BaseService {
|
||||||
}
|
}
|
||||||
|
|
||||||
case QueueName.LIBRARY: {
|
case QueueName.LIBRARY: {
|
||||||
return this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SYNC_ALL, data: { force } });
|
return this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SCAN_ALL, data: { force } });
|
||||||
}
|
}
|
||||||
|
|
||||||
case QueueName.BACKUP_DATABASE: {
|
case QueueName.BACKUP_DATABASE: {
|
||||||
|
|
|
@ -1079,7 +1079,7 @@ describe(LibraryService.name, () => {
|
||||||
it('should queue the refresh job', async () => {
|
it('should queue the refresh job', async () => {
|
||||||
mocks.library.getAll.mockResolvedValue([libraryStub.externalLibrary1]);
|
mocks.library.getAll.mockResolvedValue([libraryStub.externalLibrary1]);
|
||||||
|
|
||||||
await expect(sut.handleQueueSyncAll()).resolves.toBe(JobStatus.SUCCESS);
|
await expect(sut.handleQueueScanAll()).resolves.toBe(JobStatus.SUCCESS);
|
||||||
|
|
||||||
expect(mocks.job.queue.mock.calls).toEqual([
|
expect(mocks.job.queue.mock.calls).toEqual([
|
||||||
[
|
[
|
||||||
|
|
|
@ -47,7 +47,7 @@ export class LibraryService extends BaseService {
|
||||||
name: 'libraryScan',
|
name: 'libraryScan',
|
||||||
expression: scan.cronExpression,
|
expression: scan.cronExpression,
|
||||||
onTick: () =>
|
onTick: () =>
|
||||||
handlePromiseError(this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SYNC_ALL }), this.logger),
|
handlePromiseError(this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SCAN_ALL }), this.logger),
|
||||||
start: scan.enabled,
|
start: scan.enabled,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -210,11 +210,17 @@ export class LibraryService extends BaseService {
|
||||||
|
|
||||||
@OnJob({ name: JobName.LIBRARY_QUEUE_CLEANUP, queue: QueueName.LIBRARY })
|
@OnJob({ name: JobName.LIBRARY_QUEUE_CLEANUP, queue: QueueName.LIBRARY })
|
||||||
async handleQueueCleanup(): Promise<JobStatus> {
|
async handleQueueCleanup(): Promise<JobStatus> {
|
||||||
this.logger.debug('Cleaning up any pending library deletions');
|
this.logger.log('Checking for any libraries pending deletion...');
|
||||||
const pendingDeletion = await this.libraryRepository.getAllDeleted();
|
const pendingDeletions = await this.libraryRepository.getAllDeleted();
|
||||||
await this.jobRepository.queueAll(
|
if (pendingDeletions.length > 0) {
|
||||||
pendingDeletion.map((libraryToDelete) => ({ name: JobName.LIBRARY_DELETE, data: { id: libraryToDelete.id } })),
|
const libraryString = pendingDeletions.length === 1 ? 'library' : 'libraries';
|
||||||
);
|
this.logger.log(`Found ${pendingDeletions.length} ${libraryString} pending deletion, cleaning up...`);
|
||||||
|
|
||||||
|
await this.jobRepository.queueAll(
|
||||||
|
pendingDeletions.map((libraryToDelete) => ({ name: JobName.LIBRARY_DELETE, data: { id: libraryToDelete.id } })),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -442,9 +448,13 @@ export class LibraryService extends BaseService {
|
||||||
await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SYNC_ASSETS, data: { id } });
|
await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SYNC_ASSETS, data: { id } });
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.LIBRARY_QUEUE_SYNC_ALL, queue: QueueName.LIBRARY })
|
async queueScanAll() {
|
||||||
async handleQueueSyncAll(): Promise<JobStatus> {
|
await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SCAN_ALL, data: {} });
|
||||||
this.logger.debug(`Refreshing all external libraries`);
|
}
|
||||||
|
|
||||||
|
@OnJob({ name: JobName.LIBRARY_QUEUE_SCAN_ALL, queue: QueueName.LIBRARY })
|
||||||
|
async handleQueueScanAll(): Promise<JobStatus> {
|
||||||
|
this.logger.log(`Refreshing all external libraries`);
|
||||||
|
|
||||||
await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_CLEANUP, data: {} });
|
await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_CLEANUP, data: {} });
|
||||||
|
|
||||||
|
|
|
@ -351,7 +351,7 @@ export type JobItem =
|
||||||
| { name: JobName.LIBRARY_QUEUE_SYNC_ASSETS; data: IEntityJob }
|
| { name: JobName.LIBRARY_QUEUE_SYNC_ASSETS; data: IEntityJob }
|
||||||
| { name: JobName.LIBRARY_SYNC_ASSET; data: ILibraryAssetJob }
|
| { name: JobName.LIBRARY_SYNC_ASSET; data: ILibraryAssetJob }
|
||||||
| { name: JobName.LIBRARY_DELETE; data: IEntityJob }
|
| { name: JobName.LIBRARY_DELETE; data: IEntityJob }
|
||||||
| { name: JobName.LIBRARY_QUEUE_SYNC_ALL; data?: IBaseJob }
|
| { name: JobName.LIBRARY_QUEUE_SCAN_ALL; data?: IBaseJob }
|
||||||
| { name: JobName.LIBRARY_QUEUE_CLEANUP; data: IBaseJob }
|
| { name: JobName.LIBRARY_QUEUE_CLEANUP; data: IBaseJob }
|
||||||
|
|
||||||
// Notification
|
// Notification
|
||||||
|
|
|
@ -185,7 +185,7 @@
|
||||||
{#if !disabled && !multipleButtons && isIdle}
|
{#if !disabled && !multipleButtons && isIdle}
|
||||||
<JobTileButton color="light-gray" onClick={() => onCommand({ command: JobCommand.Start, force: false })}>
|
<JobTileButton color="light-gray" onClick={() => onCommand({ command: JobCommand.Start, force: false })}>
|
||||||
<Icon path={mdiPlay} size="48" />
|
<Icon path={mdiPlay} size="48" />
|
||||||
{$t('start').toUpperCase()}
|
{missingText}
|
||||||
</JobTileButton>
|
</JobTileButton>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -79,8 +79,7 @@
|
||||||
icon: mdiLibraryShelves,
|
icon: mdiLibraryShelves,
|
||||||
title: $getJobName(JobName.Library),
|
title: $getJobName(JobName.Library),
|
||||||
subtitle: $t('admin.library_tasks_description'),
|
subtitle: $t('admin.library_tasks_description'),
|
||||||
allText: $t('all'),
|
missingText: $t('rescan'),
|
||||||
missingText: $t('refresh'),
|
|
||||||
},
|
},
|
||||||
[JobName.Sidecar]: {
|
[JobName.Sidecar]: {
|
||||||
title: $getJobName(JobName.Sidecar),
|
title: $getJobName(JobName.Sidecar),
|
||||||
|
@ -135,14 +134,14 @@
|
||||||
[JobName.StorageTemplateMigration]: {
|
[JobName.StorageTemplateMigration]: {
|
||||||
icon: mdiFolderMove,
|
icon: mdiFolderMove,
|
||||||
title: $getJobName(JobName.StorageTemplateMigration),
|
title: $getJobName(JobName.StorageTemplateMigration),
|
||||||
missingText: $t('missing'),
|
missingText: $t('start'),
|
||||||
description: StorageMigrationDescription,
|
description: StorageMigrationDescription,
|
||||||
},
|
},
|
||||||
[JobName.Migration]: {
|
[JobName.Migration]: {
|
||||||
icon: mdiFolderMove,
|
icon: mdiFolderMove,
|
||||||
title: $getJobName(JobName.Migration),
|
title: $getJobName(JobName.Migration),
|
||||||
subtitle: $t('admin.migration_job_description'),
|
subtitle: $t('admin.migration_job_description'),
|
||||||
missingText: $t('missing'),
|
missingText: $t('start'),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -146,7 +146,7 @@ export const getJobName = derived(t, ($t) => {
|
||||||
[JobName.Migration]: $t('admin.migration_job'),
|
[JobName.Migration]: $t('admin.migration_job'),
|
||||||
[JobName.BackgroundTask]: $t('admin.background_task_job'),
|
[JobName.BackgroundTask]: $t('admin.background_task_job'),
|
||||||
[JobName.Search]: $t('search'),
|
[JobName.Search]: $t('search'),
|
||||||
[JobName.Library]: $t('library'),
|
[JobName.Library]: $t('external_libraries'),
|
||||||
[JobName.Notifications]: $t('notifications'),
|
[JobName.Notifications]: $t('notifications'),
|
||||||
[JobName.BackupDatabase]: $t('admin.backup_database'),
|
[JobName.BackupDatabase]: $t('admin.backup_database'),
|
||||||
};
|
};
|
||||||
|
|
|
@ -22,7 +22,10 @@
|
||||||
getAllLibraries,
|
getAllLibraries,
|
||||||
getLibraryStatistics,
|
getLibraryStatistics,
|
||||||
getUserAdmin,
|
getUserAdmin,
|
||||||
|
JobCommand,
|
||||||
|
JobName,
|
||||||
scanLibrary,
|
scanLibrary,
|
||||||
|
sendJobCommand,
|
||||||
updateLibrary,
|
updateLibrary,
|
||||||
type LibraryResponseDto,
|
type LibraryResponseDto,
|
||||||
type LibraryStatsResponseDto,
|
type LibraryStatsResponseDto,
|
||||||
|
@ -151,9 +154,8 @@
|
||||||
|
|
||||||
const handleScanAll = async () => {
|
const handleScanAll = async () => {
|
||||||
try {
|
try {
|
||||||
for (const library of libraries) {
|
await sendJobCommand({ id: JobName.Library, jobCommandDto: { command: JobCommand.Start } });
|
||||||
await scanLibrary({ id: library.id });
|
|
||||||
}
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: $t('admin.refreshing_all_libraries'),
|
message: $t('admin.refreshing_all_libraries'),
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
|
|
Loading…
Add table
Reference in a new issue