feat(cli): Add --visibility flag to immich CLI upload subcommand (#29614)
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
Reference in New Issue
Block a user