This commit is contained in:
Lorenzo Montanari 2025-10-17 11:39:57 -04:00 committed by GitHub
commit 0956b2f745
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 459 additions and 0 deletions

282
cli/src/commands/jobs.ts Normal file
View file

@ -0,0 +1,282 @@
import {
AllJobStatusResponseDto,
getAllJobsStatus,
JobCommand,
JobCountsDto,
JobName,
JobStatusDto,
sendJobCommand,
} from '@immich/sdk';
import { authenticate, BaseOptions, logError, withError } from 'src/utils';
interface CommonJobStatusOptions {
jsonOutput?: boolean;
}
interface StartJobOptions extends CommonJobStatusOptions {
onlyMissing?: boolean;
refresh?: boolean;
all?: boolean;
wait?: boolean;
}
type GetJobStatusOptions = CommonJobStatusOptions;
type PauseJobOptions = GetJobStatusOptions;
type ResumeJobOptions = GetJobStatusOptions;
const JOB_STATUS_COLUMN_HEADERS: Readonly<Record<keyof JobCountsDto, string>> = {
active: 'Active Jobs',
delayed: 'Delayed Jobs',
paused: 'Paused Jobs',
waiting: 'Waiting Jobs',
completed: 'Completed Jobs',
failed: 'Failed Jobs',
};
/**
* Command entry point for getting the status of jobs.
* If jobName is provided, it will return the status of that specific job.
* If jobName is not provided, it will return the status of all jobs.
* @param jobName Name of the job to check status for, or undefined to get all jobs status.
* @param baseOptions Immich CLI base options.
* @param commandOptions Options for the command, such as whether to output in JSON format.
*/
export async function getJobsStatus(
jobName: JobName | undefined,
baseOptions: BaseOptions,
commandOptions: GetJobStatusOptions,
) {
await authenticate(baseOptions);
if (jobName) {
ensureJobNameIsValid(jobName);
return getStatusOfSingleJob(jobName, commandOptions);
} else {
return getStatusOfAllJobs(commandOptions);
}
}
/**
* Command entry point for starting a job.
* It will ask Immich to start the job with the given name.
* @param jobName Name of the job to start.
* @param baseOptions Immich CLI base options.
* @param commandOptions Options for the command, such as whether to output in JSON format.
*/
export async function startJob(jobName: JobName, baseOptions: BaseOptions, commandOptions: StartJobOptions) {
await authenticate(baseOptions);
ensureJobNameIsValid(jobName);
let force: boolean | undefined;
// Mimic the behavior of the Immich web UI, based on options provided.
if (commandOptions.all) {
force = true;
} else if (commandOptions.onlyMissing) {
force = false;
} else if (commandOptions.refresh) {
force = undefined;
}
const [error, response] = await withError(
sendJobCommand({
id: jobName,
jobCommandDto: {
command: JobCommand.Start,
force,
},
}),
);
if (error) {
logError(error, `Failed to start job: ${jobName}. Got error`);
process.exit(1);
}
let status = response;
if (commandOptions.wait && areJobsRunning(status.jobCounts)) {
status = await waitForJobCompletion(jobName);
}
if (commandOptions.jsonOutput) {
console.log(JSON.stringify(status, undefined, 2));
return;
}
const message = commandOptions.wait
? `Successfully executed job "${jobName}".`
: `Successfully queued execution of job "${jobName}".`;
console.log(message);
}
/**
* Command entry point for pausing executions of a job, given its name.
* @param jobName Name of the job to pause executions for.
* @param baseOptions Immich CLI base options.
* @param commandOptions Options for the command, such as whether to output in JSON format.
*/
export async function pauseJobExecutions(jobName: JobName, baseOptions: BaseOptions, commandOptions: PauseJobOptions) {
await authenticate(baseOptions);
ensureJobNameIsValid(jobName);
const [error, response] = await withError(
sendJobCommand({
id: jobName,
jobCommandDto: {
command: JobCommand.Pause,
},
}),
);
if (error) {
logError(error, `Failed to pause executions of job "${jobName}". Got error`);
process.exit(1);
}
if (commandOptions.jsonOutput) {
console.log(JSON.stringify(response.queueStatus, undefined, 2));
return;
}
console.log(`Successfully paused executions of job "${jobName}".`);
}
/**
* Command entry point for resuming executions of a job, given its name.
* @param jobName Job name to resume executions for.
* @param baseOptions Immich CLI base options.
* @param commandOptions Options for the command, such as whether to output in JSON format.
*/
export async function resumeJobExecutions(
jobName: JobName,
baseOptions: BaseOptions,
commandOptions: ResumeJobOptions,
) {
await authenticate(baseOptions);
ensureJobNameIsValid(jobName);
const [error, response] = await withError(
sendJobCommand({
id: jobName,
jobCommandDto: {
command: JobCommand.Resume,
},
}),
);
if (error) {
logError(error, `Failed to resume executions of job "${jobName}". Got error`);
process.exit(1);
}
if (commandOptions.jsonOutput) {
console.log(JSON.stringify(response.queueStatus, undefined, 2));
return;
}
console.log(`Successfully resumed executions of job "${jobName}".`);
}
/**
* An utility function to ensure that the provided job name is valid.
* If not, it will log an error and exit the process.
* @param jobName Name of the job to validate.
*/
function ensureJobNameIsValid(jobName: JobName): asserts jobName is JobName {
const validObjectNames = Object.values(JobName);
if (!validObjectNames.includes(jobName)) {
console.error(`Invalid job name: ${jobName}. Valid job names are: ${validObjectNames.join(', ')}`);
process.exit(1);
}
}
/**
* An helper function to get and print to standard output the status of a single job.
* @param jobName Name of the job to get the status for.
* @param commandOptions Command options, such as whether to output in JSON format.
*/
async function getStatusOfSingleJob(jobName: JobName, commandOptions: CommonJobStatusOptions) {
const status = await requestJobsStatus();
const jobStatus = status[jobName];
const outputPrefix = commandOptions.jsonOutput ? '' : `Status for requested job "${jobName}": `;
console.log(outputPrefix, JSON.stringify(jobStatus, undefined, 2));
}
/**
* An helper function to get and print to standard output the status of all known jobs.
* By default, it will print the status in a table format, with one row for each job.
* @param commandOptions Command options, such as whether to output in JSON format.
*/
async function getStatusOfAllJobs(commandOptions: CommonJobStatusOptions) {
const status = await requestJobsStatus();
if (commandOptions.jsonOutput) {
console.log(JSON.stringify(status, undefined, 2));
return;
}
console.table(
Object.entries(status).map(([name, status]) => {
const row: Record<string, string | number | boolean> = {
name,
isQueueActive: status.queueStatus.isActive,
isQueuePaused: status.queueStatus.isPaused,
};
for (const [key, value] of Object.entries(status.jobCounts)) {
row[JOB_STATUS_COLUMN_HEADERS[key as keyof JobCountsDto]] = value;
}
return row;
}),
);
}
/**
* An utility function to wait for a job to complete.
* It will poll the job status every second until the job is no longer running.
* @param jobName Name of the job to wait for completion.
* @returns A promise that resolves to the final status of the job.
*/
async function waitForJobCompletion(jobName: JobName): Promise<JobStatusDto> {
while (true) {
const status = await requestJobsStatus();
const jobStatus = status[jobName];
if (!areJobsRunning(jobStatus.jobCounts)) {
return jobStatus;
}
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
/**
* An helper function to determine if any jobs are currently running.
* @param jobCounts Counts of jobs in various states.
* @returns True if any jobs are running, false otherwise.
*/
function areJobsRunning(jobCounts: JobCountsDto): boolean {
return !!(jobCounts.active || jobCounts.delayed || jobCounts.paused || jobCounts.waiting);
}
/**
* An utility function to request the status of all jobs from the Immich server.
* It will log an error and exit the process if the request fails.
* @returns A promise that resolves to the status of all jobs.
*/
async function requestJobsStatus(): Promise<AllJobStatusResponseDto> {
const [error, status] = await withError(getAllJobsStatus());
if (error) {
logError(error, 'Failed to get job status. Error');
process.exit(1);
}
return status;
}

View file

@ -4,6 +4,7 @@ import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
import { upload } from 'src/commands/asset'; import { upload } from 'src/commands/asset';
import { login, logout } from 'src/commands/auth'; import { login, logout } from 'src/commands/auth';
import { getJobsStatus, pauseJobExecutions, resumeJobExecutions, startJob } from 'src/commands/jobs';
import { serverInfo } from 'src/commands/server-info'; import { serverInfo } from 'src/commands/server-info';
import { version } from '../package.json'; import { version } from '../package.json';
@ -34,6 +35,78 @@ program
.description('Remove stored credentials') .description('Remove stored credentials')
.action(() => logout(program.opts())); .action(() => logout(program.opts()));
const jobsCommand = program.createCommand('jobs').description('Manage background jobs');
jobsCommand
.command('pause')
.description('Pause executions of all instances of the given job')
.usage('<jobName> [options]')
.addOption(
new Option('-j, --json-output', 'Output detailed information in json format')
.env('IMMICH_JSON_OUTPUT')
.default(false),
)
.argument('<jobName>', 'Name of the job to pause executions for')
.action((jobName, options) => pauseJobExecutions(jobName, program.opts(), options));
jobsCommand
.command('resume')
.description('Resume executions of all instances of the given job')
.usage('<jobName> [options]')
.addOption(
new Option('-j, --json-output', 'Output detailed information in json format')
.env('IMMICH_JSON_OUTPUT')
.default(false),
)
.argument('<jobName>', 'Name of the job to resume executions for')
.action((jobName, options) => resumeJobExecutions(jobName, program.opts(), options));
jobsCommand
.command('run')
.description('Start a specific job')
.usage('<jobName> [options]')
.addOption(
new Option('-w, --wait', 'Wait for the job to complete before returning')
.env('IMMICH_WAIT_JOB_COMPLETION')
.default(false),
)
.addOption(
new Option('-j, --json-output', 'Output detailed information in json format')
.env('IMMICH_JSON_OUTPUT')
.default(false),
)
.addOption(
new Option(
'--all',
'Execute this job on all assets. Depending on the job, this may clear existing data previously created by the job',
)
.conflicts('refresh')
.conflicts('onlyMissing'),
)
.addOption(
new Option('--only-missing', 'Run this job only on assets that have not been processed yet')
.default(true)
.conflicts('all')
.conflicts('refresh'),
)
.addOption(new Option('--refresh', '(Re)run this job on all assets').conflicts('all').conflicts('onlyMissing'))
.argument('<jobName>', 'Name of the job to run')
.action((jobName, options) => startJob(jobName, program.opts(), options));
jobsCommand
.command('status')
.description('Get the status of all jobs or the status of a specific job')
.usage('[jobName] [options]')
.addOption(
new Option('-j, --json-output', 'Output detailed information in json format')
.env('IMMICH_JSON_OUTPUT')
.default(false),
)
.argument('[jobName]', 'Name of the job to check status for')
.action((jobName, options) => getJobsStatus(jobName, program.opts(), options));
program.addCommand(jobsCommand);
program program
.command('server-info') .command('server-info')
.description('Display server information') .description('Display server information')

View file

@ -46,6 +46,17 @@ Visit the page, open the "Create job" modal from the top right, select "Create D
A job will run and trigger a dump, you can verify this worked correctly by checking the logs or the `backups/` folder. A job will run and trigger a dump, you can verify this worked correctly by checking the logs or the `backups/` folder.
This dumps will count towards the last `X` dumps that will be kept based on your settings. This dumps will count towards the last `X` dumps that will be kept based on your settings.
Alternatively, you can trigger a database dump using the [Immich CLI](/docs/features/command-line-interface.md).
This can be done by running the following command in a terminal.
```bash
immich jobs run backupDatabase
```
Since this command is analogous to running the database dump via the Immich web interface, the resulting database dump
will be placed in the same `backups/` folder, and it will count towards the last `X` dumps that will be kept based
on your settings, just as if it was triggered via the web UI.
#### Restoring #### Restoring
We hope to make restoring simpler in future versions, for now you can find the database dumps in the `UPLOAD_LOCATION/backups` folder on your host. We hope to make restoring simpler in future versions, for now you can find the database dumps in the `UPLOAD_LOCATION/backups` folder on your host.

View file

@ -6,6 +6,7 @@ Immich has a command line interface (CLI) that allows you to perform certain act
- Upload photos and videos to Immich - Upload photos and videos to Immich
- Check server version - Check server version
- Manage execution of background jobs
More features are planned for the future. More features are planned for the future.
@ -70,6 +71,7 @@ Options:
Commands: Commands:
login|login-key <url> <key> Login using an API key login|login-key <url> <key> Login using an API key
logout Remove stored credentials logout Remove stored credentials
jobs Manage background jobs
server-info Display server information server-info Display server information
upload [options] [paths...] Upload assets upload [options] [paths...] Upload assets
help [command] display help for command help [command] display help for command
@ -79,6 +81,8 @@ Commands:
## Commands ## Commands
### Upload
The upload command supports the following options: The upload command supports the following options:
<details> <details>
@ -112,6 +116,31 @@ Options:
Note that the above options can read from environment variables as well. Note that the above options can read from environment variables as well.
### Jobs
The jobs command supports the following options:
<details>
<summary>Options</summary>
```
Usage: immich jobs [options] [command]
Manage background jobs
Options:
-h, --help display help for command
Commands:
pause [options] <jobName> Pause executions of all instances of the given job
resume [options] <jobName> Resume executions of all instances of the given job
run [options] <jobName> Start a specific job
status [options] [jobName] Get the status of all jobs or the status of a specific job
help [command] display help for command
```
</details>
## Quick Start ## Quick Start
You begin by authenticating to your Immich server. For instance: You begin by authenticating to your Immich server. For instance:
@ -190,3 +219,67 @@ immich upload --dry-run . | tail -n +6 | jq .newFiles[]
The API key can be obtained in the user setting panel on the web interface. The API key can be obtained in the user setting panel on the web interface.
![Obtain Api Key](./img/obtain-api-key.webp) ![Obtain Api Key](./img/obtain-api-key.webp)
### Manage jobs via the CLI
With the Immich CLI you can also manage background jobs on your server.
The `jobs` command allows you to pause, resume, run, or check the status of background jobs.
Once you are authenticated, you can use the `jobs` command to interact with your Immich server.
For example, you can run the following command to print out the status of all the background jobs Immich uses.
```bash
immich jobs status
```
Additionally, you can also query the status of a specific job by providing its' name.
For example, if you want to know the status of the thumbnail generation after having imported several
images, you can run this command:
```bash
immich jobs status thumbnailGeneration
```
You can also trigger execution of a specific job given its' name.
```bash
immich jobs run thumbnailGeneration
```
By default, this will run the thumbnail generation only on those assets that were not already processed.
If you want to run the specified job on all assets your Immich instance handles, you can use the `--refresh` option.
```bash
immich jobs run thumbnailGeneration --refresh
```
Alternatively, you can run the same command with the `--all` flag.
Depending on the job implementation, the `--all` flag may imply the deletion of the output of previous executions of
that job, before running it from scratch on all assets.
```bash
immich jobs run thumbnailGeneration --all
```
If you want to stop executions of a specific job, you can run the command `pause`, followed by the job name.
For example, to prevent generation of new thumbnails, you could run this command:
```bash
immich jobs pause thumbnailGeneration
```
Once executions of a specific job are paused, scheduled executions of that job will no longer occur until you
resume their execution. This can be done using the `resume` command.
For example, to resume the generation of new thumbnails, you can run:
```bash
immich jobs resume thumbnailGeneration
```
We can also use the `run` command to ask the Immich server to create a database dump.
This can be done with the following command:
```bash
immich jobs run backupDatabase
```