feat(cli): Add --visibility flag to immich CLI upload subcommand (#29614)

This commit is contained in:
Yuxin Wang
2026-07-15 14:11:03 -05:00
committed by GitHub
parent 834cc54abc
commit d8ed7d7bb9
4 changed files with 48 additions and 7 deletions
@@ -99,6 +99,7 @@ Options:
-H, --include-hidden Include hidden folders (default: false, env: IMMICH_INCLUDE_HIDDEN)
-a, --album Automatically create albums based on folder name (default: false, env: IMMICH_AUTO_CREATE_ALBUM)
-A, --album-name <name> Add all assets to specified album (env: IMMICH_ALBUM_NAME)
--visibility <visibility> Set the visibility of uploaded assets (choices: "archive", "timeline", "hidden", "locked", env: IMMICH_VISIBILITY)
-n, --dry-run Don't perform any actions, just show what will be done (default: false, env: IMMICH_DRY_RUN)
-c, --concurrency <number> Number of assets to upload at the same time (default: 4, env: IMMICH_UPLOAD_CONCURRENCY)
-j, --json-output Output detailed information in json format (default: false, env: IMMICH_JSON_OUTPUT)
@@ -176,6 +177,12 @@ By default, hidden files are skipped. If you want to include hidden files, use t
immich upload --include-hidden --recursive directory/
```
You can set the visibility of uploaded assets to `archive`, `timeline`, `hidden`, or `locked` with the `--visibility` option:
```bash
immich upload --visibility archive --recursive directory/
```
You can use the `--json-output` option to get a json printed which includes
three keys: `newFiles`, `duplicates` and `newAssets`. Due to some logging
output you will need to strip the first three lines of output to get the json.
+22 -1
View File
@@ -4,7 +4,14 @@ import path from 'node:path';
import { setTimeout as sleep } from 'node:timers/promises';
import { describe, expect, it, MockedFunction, vi } from 'vitest';
import { AssetRejectReason, AssetUploadAction, checkBulkUpload, defaults, getSupportedMediaTypes } from '@immich/sdk';
import {
AssetRejectReason,
AssetUploadAction,
AssetVisibility,
checkBulkUpload,
defaults,
getSupportedMediaTypes,
} from '@immich/sdk';
import createFetchMock from 'vitest-fetch-mock';
import {
@@ -102,6 +109,20 @@ describe('uploadFiles', () => {
await expect(uploadFiles([testFilePath], { concurrency: 1 })).resolves.toEqual([]);
});
it('uploads assets with the specified visibility', async () => {
fetchMocker.doMockIf(new RegExp(`${baseUrl}/assets$`), function () {
return {
status: 200,
body: JSON.stringify({ id: 'fc5621b1-86f6-44a1-9905-403e607df9f5', status: 'created' }),
};
});
await uploadFiles([testFilePath], { concurrency: 1, visibility: AssetVisibility.Hidden });
const formData = fetchMocker.mock.calls[0]?.[1]?.body as FormData;
expect(formData.get('visibility')).toBe('hidden');
});
});
describe('checkForDuplicates', () => {
+13 -6
View File
@@ -4,6 +4,7 @@ import {
AssetMediaResponseDto,
AssetMediaStatus,
AssetUploadAction,
AssetVisibility,
Permission,
addAssetsToAlbum,
checkBulkUpload,
@@ -39,6 +40,7 @@ export interface UploadOptionsDto {
deleteDuplicates?: boolean;
album?: boolean;
albumName?: string;
visibility?: AssetVisibility;
includeHidden?: boolean;
concurrency: number;
progress?: boolean;
@@ -306,10 +308,8 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas
return { newFiles, duplicates };
};
export const uploadFiles = async (
files: string[],
{ dryRun, concurrency, progress }: UploadOptionsDto,
): Promise<Asset[]> => {
export const uploadFiles = async (files: string[], options: UploadOptionsDto): Promise<Asset[]> => {
const { dryRun, concurrency, progress } = options;
if (files.length === 0) {
console.log('All assets were already uploaded, nothing to do.');
return [];
@@ -358,7 +358,7 @@ export const uploadFiles = async (
throw new Error(`Stats not found for ${filepath}`);
}
const response = await uploadFile(filepath, stats);
const response = await uploadFile(filepath, stats, options);
newAssets.push({ id: response.id, filepath });
if (response.status === AssetMediaStatus.Duplicate) {
duplicateCount++;
@@ -400,7 +400,11 @@ export const uploadFiles = async (
return newAssets;
};
const uploadFile = async (input: string, stats: Stats): Promise<AssetMediaResponseDto> => {
const uploadFile = async (
input: string,
stats: Stats,
{ visibility }: UploadOptionsDto,
): Promise<AssetMediaResponseDto> => {
const { baseUrl, headers } = defaults;
const formData = new FormData();
@@ -409,6 +413,9 @@ const uploadFile = async (input: string, stats: Stats): Promise<AssetMediaRespon
formData.append('fileSize', String(stats.size));
formData.append('isFavorite', 'false');
formData.append('assetData', new UploadFile(input, stats.size));
if (visibility) {
formData.append('visibility', visibility);
}
const sidecarPath = findSidecar(input);
if (sidecarPath) {
+6
View File
@@ -1,4 +1,5 @@
#! /usr/bin/env node
import { AssetVisibility } from '@immich/sdk';
import { Command, Option } from 'commander';
import os from 'node:os';
import path from 'node:path';
@@ -58,6 +59,11 @@ program
.env('IMMICH_ALBUM_NAME')
.conflicts('album'),
)
.addOption(
new Option('--visibility <visibility>', 'Set the visibility of uploaded assets')
.choices(Object.values(AssetVisibility))
.env('IMMICH_VISIBILITY'),
)
.addOption(
new Option('-n, --dry-run', "Don't perform any actions, just show what will be done")
.env('IMMICH_DRY_RUN')