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) -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 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) -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) -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) -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) -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/ 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 You can use the `--json-output` option to get a json printed which includes
three keys: `newFiles`, `duplicates` and `newAssets`. Due to some logging 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. 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 { setTimeout as sleep } from 'node:timers/promises';
import { describe, expect, it, MockedFunction, vi } from 'vitest'; 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 createFetchMock from 'vitest-fetch-mock';
import { import {
@@ -102,6 +109,20 @@ describe('uploadFiles', () => {
await expect(uploadFiles([testFilePath], { concurrency: 1 })).resolves.toEqual([]); 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', () => { describe('checkForDuplicates', () => {
+13 -6
View File
@@ -4,6 +4,7 @@ import {
AssetMediaResponseDto, AssetMediaResponseDto,
AssetMediaStatus, AssetMediaStatus,
AssetUploadAction, AssetUploadAction,
AssetVisibility,
Permission, Permission,
addAssetsToAlbum, addAssetsToAlbum,
checkBulkUpload, checkBulkUpload,
@@ -39,6 +40,7 @@ export interface UploadOptionsDto {
deleteDuplicates?: boolean; deleteDuplicates?: boolean;
album?: boolean; album?: boolean;
albumName?: string; albumName?: string;
visibility?: AssetVisibility;
includeHidden?: boolean; includeHidden?: boolean;
concurrency: number; concurrency: number;
progress?: boolean; progress?: boolean;
@@ -306,10 +308,8 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas
return { newFiles, duplicates }; return { newFiles, duplicates };
}; };
export const uploadFiles = async ( export const uploadFiles = async (files: string[], options: UploadOptionsDto): Promise<Asset[]> => {
files: string[], const { dryRun, concurrency, progress } = options;
{ dryRun, concurrency, progress }: UploadOptionsDto,
): Promise<Asset[]> => {
if (files.length === 0) { if (files.length === 0) {
console.log('All assets were already uploaded, nothing to do.'); console.log('All assets were already uploaded, nothing to do.');
return []; return [];
@@ -358,7 +358,7 @@ export const uploadFiles = async (
throw new Error(`Stats not found for ${filepath}`); 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 }); newAssets.push({ id: response.id, filepath });
if (response.status === AssetMediaStatus.Duplicate) { if (response.status === AssetMediaStatus.Duplicate) {
duplicateCount++; duplicateCount++;
@@ -400,7 +400,11 @@ export const uploadFiles = async (
return newAssets; 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 { baseUrl, headers } = defaults;
const formData = new FormData(); 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('fileSize', String(stats.size));
formData.append('isFavorite', 'false'); formData.append('isFavorite', 'false');
formData.append('assetData', new UploadFile(input, stats.size)); formData.append('assetData', new UploadFile(input, stats.size));
if (visibility) {
formData.append('visibility', visibility);
}
const sidecarPath = findSidecar(input); const sidecarPath = findSidecar(input);
if (sidecarPath) { if (sidecarPath) {
+6
View File
@@ -1,4 +1,5 @@
#! /usr/bin/env node #! /usr/bin/env node
import { AssetVisibility } from '@immich/sdk';
import { Command, Option } from 'commander'; import { Command, Option } from 'commander';
import os from 'node:os'; import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
@@ -58,6 +59,11 @@ program
.env('IMMICH_ALBUM_NAME') .env('IMMICH_ALBUM_NAME')
.conflicts('album'), .conflicts('album'),
) )
.addOption(
new Option('--visibility <visibility>', 'Set the visibility of uploaded assets')
.choices(Object.values(AssetVisibility))
.env('IMMICH_VISIBILITY'),
)
.addOption( .addOption(
new Option('-n, --dry-run', "Don't perform any actions, just show what will be done") new Option('-n, --dry-run', "Don't perform any actions, just show what will be done")
.env('IMMICH_DRY_RUN') .env('IMMICH_DRY_RUN')