feat: allow configurable config dir

This commit is contained in:
Jonathan Jogenfors 2023-11-21 10:00:51 +01:00
parent 19f83f3c79
commit 7c6ee25f89
9 changed files with 46 additions and 31 deletions

View file

@ -58,7 +58,7 @@
"format": "prettier --check .",
"format:fix": "prettier --write .",
"check": "tsc --noEmit",
"test:e2e": "NODE_OPTIONS='--experimental-vm-modules' jest --config test/e2e/jest-e2e.json --runInBand --verbose"
"test:e2e": "NODE_OPTIONS='--experimental-vm-modules' jest --config test/e2e/jest-e2e.json --runInBand"
},
"jest": {
"clearMocks": true,

View file

@ -3,6 +3,7 @@ import { SessionService } from '../services/session.service';
import { LoginError } from '../cores/errors/login-error';
import { exit } from 'node:process';
import { ServerVersionResponseDto, UserResponseDto } from 'src/api/open-api';
import { BaseOptionsDto } from 'src/cores/dto/base-options-dto';
export abstract class BaseCommand {
protected sessionService!: SessionService;
@ -10,8 +11,11 @@ export abstract class BaseCommand {
protected user!: UserResponseDto;
protected serverVersion!: ServerVersionResponseDto;
constructor() {
this.sessionService = new SessionService();
constructor(options: BaseOptionsDto) {
if (!options.config) {
throw new Error('Config directory is required');
}
this.sessionService = new SessionService(options.config);
}
public async connect(): Promise<void> {

View file

@ -0,0 +1,3 @@
export class BaseOptionsDto {
config?: string;
}

View file

@ -1,12 +1,20 @@
#! /usr/bin/env node
import { program, Option } from 'commander';
import { Option, Command } from 'commander';
import Upload from './commands/upload';
import ServerInfo from './commands/server-info';
import LoginKey from './commands/login/key';
import Logout from './commands/logout';
import path from 'node:path';
import os from 'os';
program.name('immich').description('Immich command line interface');
const userHomeDir = os.homedir();
const configDir = path.join(userHomeDir, '.config/immich/');
const program = new Command();
program.name('immich');
program.description('Immich command line interface');
program.addOption(new Option('-d, --config', 'Configuration directory').env('IMMICH_CONFIG_DIR').default(configDir));
program
.command('upload')
@ -29,14 +37,14 @@ program
.argument('[paths...]', 'One or more paths to assets to be uploaded')
.action(async (paths, options) => {
options.exclusionPatterns = options.ignore;
await new Upload().run(paths, options);
await new Upload(program.opts()).run(paths, options);
});
program
.command('server-info')
.description('Display server information')
.action(async () => {
await new ServerInfo().run();
await new ServerInfo(program.opts()).run();
});
program
@ -45,14 +53,14 @@ program
.argument('[instanceUrl]')
.argument('[apiKey]')
.action(async (paths, options) => {
await new LoginKey().run(paths, options);
await new LoginKey(program.opts()).run(paths, options);
});
program
.command('logout')
.description('Remove stored credentials')
.action(async () => {
await new Logout().run();
await new Logout(program.opts()).run();
});
program.parse(process.argv);

View file

@ -10,21 +10,9 @@ export class SessionService {
readonly authPath!: string;
private api!: ImmichApi;
constructor(configDir?: string) {
if (!configDir) {
configDir = process.env.IMMICH_CONFIG_DIR;
if (!configDir) {
const userHomeDir = os.homedir();
this.configDir = path.join(userHomeDir, '.config/immich/');
} else {
this.configDir = configDir;
}
} else {
this.configDir = configDir;
}
this.authPath = path.join(this.configDir, 'auth.yml');
constructor(configDir: string) {
this.configDir = configDir;
this.authPath = path.join(configDir, 'auth.yml');
}
public async connect(): Promise<ImmichApi> {

View file

@ -0,0 +1,5 @@
import { BaseOptionsDto } from 'src/cores/dto/base-options-dto';
export const CLI_BASE_OPTIONS: BaseOptionsDto = { config: '/tmp/immich/' };
export const spyOnConsole = () => jest.spyOn(console, 'log').mockImplementation();

View file

@ -4,13 +4,14 @@ import { LoginResponseDto } from 'src/api/open-api';
import { APIKeyCreateResponseDto } from '@app/domain';
import LoginKey from 'src/commands/login/key';
import { LoginError } from 'src/cores/errors/login-error';
import { CLI_BASE_OPTIONS, spyOnConsole } from 'test/cli-test-utils';
describe(`login-key (e2e)`, () => {
let server: any;
let admin: LoginResponseDto;
let apiKey: APIKeyCreateResponseDto;
let instanceUrl: string;
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
const consoleSpy = spyOnConsole();
beforeAll(async () => {
server = (await testApp.create({ jobs: true })).getHttpServer();
@ -36,12 +37,12 @@ describe(`login-key (e2e)`, () => {
});
it('should error when providing an invalid API key', async () => {
expect(async () => await new LoginKey().run(instanceUrl, 'invalid')).rejects.toThrow(
expect(async () => await new LoginKey(CLI_BASE_OPTIONS).run(instanceUrl, 'invalid')).rejects.toThrow(
new LoginError(`Failed to connect to server ${instanceUrl}: Request failed with status code 401`),
);
});
it('should log in when providing the correct API key', async () => {
await new LoginKey().run(instanceUrl, apiKey.secret);
await new LoginKey(CLI_BASE_OPTIONS).run(instanceUrl, apiKey.secret);
});
});

View file

@ -3,12 +3,13 @@ import { restoreTempFolder, testApp } from 'immich/test/test-utils';
import { LoginResponseDto } from 'src/api/open-api';
import ServerInfo from 'src/commands/server-info';
import { APIKeyCreateResponseDto } from '@app/domain';
import { CLI_BASE_OPTIONS, spyOnConsole } from 'test/cli-test-utils';
describe(`server-info (e2e)`, () => {
let server: any;
let admin: LoginResponseDto;
let apiKey: APIKeyCreateResponseDto;
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
const consoleSpy = spyOnConsole();
beforeAll(async () => {
server = (await testApp.create({ jobs: true })).getHttpServer();
@ -29,7 +30,7 @@ describe(`server-info (e2e)`, () => {
});
it('should show server version', async () => {
await new ServerInfo().run();
await new ServerInfo(CLI_BASE_OPTIONS).run();
expect(consoleSpy.mock.calls).toEqual([
[expect.stringMatching(new RegExp('Server is running version \\d+.\\d+.\\d+'))],

View file

@ -3,11 +3,13 @@ import { IMMICH_TEST_ASSET_PATH, restoreTempFolder, testApp } from 'immich/test/
import { LoginResponseDto } from 'src/api/open-api';
import Upload from 'src/commands/upload';
import { APIKeyCreateResponseDto } from '@app/domain';
import { CLI_BASE_OPTIONS, spyOnConsole } from 'test/cli-test-utils';
describe(`upload (e2e)`, () => {
let server: any;
let admin: LoginResponseDto;
let apiKey: APIKeyCreateResponseDto;
spyOnConsole();
beforeAll(async () => {
server = (await testApp.create({ jobs: true })).getHttpServer();
@ -28,13 +30,16 @@ describe(`upload (e2e)`, () => {
});
it('should upload a folder recursively', async () => {
await new Upload().run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], { recursive: true });
await new Upload(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], { recursive: true });
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
expect(assets.length).toBeGreaterThan(4);
});
it('should create album from folder name', async () => {
await new Upload().run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], { recursive: true, album: true });
await new Upload(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], {
recursive: true,
album: true,
});
const albums = await api.albumApi.getAllAlbums(server, admin.accessToken);
expect(albums.length).toEqual(1);
});