mirror of
https://github.com/immich-app/immich
synced 2025-10-17 18:19:27 +00:00
chore: tree shake unused API methods from CLI (#6973)
This commit is contained in:
parent
954c1c2ef4
commit
aff71a10e5
200 changed files with 3337 additions and 22416 deletions
|
|
@ -1,10 +1,9 @@
|
|||
import { ServerVersionResponseDto, UserResponseDto } from '@immich/sdk';
|
||||
import { ImmichApi } from '../services/api.service';
|
||||
import { SessionService } from '../services/session.service';
|
||||
import { ImmichApi } from 'src/services/api.service';
|
||||
|
||||
export abstract class BaseCommand {
|
||||
protected sessionService!: SessionService;
|
||||
protected immichApi!: ImmichApi;
|
||||
protected user!: UserResponseDto;
|
||||
protected serverVersion!: ServerVersionResponseDto;
|
||||
|
||||
|
|
@ -15,7 +14,7 @@ export abstract class BaseCommand {
|
|||
this.sessionService = new SessionService(options.configDirectory);
|
||||
}
|
||||
|
||||
public async connect(): Promise<void> {
|
||||
this.immichApi = await this.sessionService.connect();
|
||||
public async connect(): Promise<ImmichApi> {
|
||||
return await this.sessionService.connect();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ import { BaseCommand } from './base-command';
|
|||
|
||||
export class ServerInfoCommand extends BaseCommand {
|
||||
public async run() {
|
||||
await this.connect();
|
||||
const versionInfo = await this.immichApi.serverInfoApi.getServerVersion();
|
||||
const mediaTypes = await this.immichApi.serverInfoApi.getSupportedMediaTypes();
|
||||
const statistics = await this.immichApi.assetApi.getAssetStatistics();
|
||||
const api = await this.connect();
|
||||
const versionInfo = await api.getServerVersion();
|
||||
const mediaTypes = await api.getSupportedMediaTypes();
|
||||
const statistics = await api.getAssetStatistics();
|
||||
|
||||
console.log(`Server Version: ${versionInfo.major}.${versionInfo.minor}.${versionInfo.patch}`);
|
||||
console.log(`Image Types: ${mediaTypes.image.map((extension) => extension.replace('.', ''))}`);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { basename } from 'node:path';
|
|||
import { access, constants, stat, unlink } from 'node:fs/promises';
|
||||
import { createHash } from 'node:crypto';
|
||||
import os from 'node:os';
|
||||
import { UploadFileRequest } from '@immich/sdk';
|
||||
import { ImmichApi } from 'src/services/api.service';
|
||||
|
||||
class Asset {
|
||||
readonly path: string;
|
||||
|
|
@ -33,7 +33,7 @@ class Asset {
|
|||
this.albumName = this.extractAlbumName();
|
||||
}
|
||||
|
||||
async getUploadFileRequest(): Promise<UploadFileRequest> {
|
||||
async getUploadFormData(): Promise<FormData> {
|
||||
if (!this.deviceAssetId) {
|
||||
throw new Error('Device asset id not set');
|
||||
}
|
||||
|
|
@ -52,15 +52,25 @@ class Asset {
|
|||
sidecarData = new File([await fs.openAsBlob(sideCarPath)], basename(sideCarPath));
|
||||
} catch {}
|
||||
|
||||
return {
|
||||
const data: any = {
|
||||
assetData: new File([await fs.openAsBlob(this.path)], basename(this.path)),
|
||||
deviceAssetId: this.deviceAssetId,
|
||||
deviceId: 'CLI',
|
||||
fileCreatedAt: this.fileCreatedAt,
|
||||
fileModifiedAt: this.fileModifiedAt,
|
||||
isFavorite: false,
|
||||
sidecarData,
|
||||
isFavorite: String(false),
|
||||
};
|
||||
const formData = new FormData();
|
||||
|
||||
for (const property in data) {
|
||||
formData.append(property, data[property]);
|
||||
}
|
||||
|
||||
if (sidecarData) {
|
||||
formData.append('sidecarData', sidecarData);
|
||||
}
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
async delete(): Promise<void> {
|
||||
|
|
@ -101,9 +111,9 @@ export class UploadCommand extends BaseCommand {
|
|||
uploadLength!: number;
|
||||
|
||||
public async run(paths: string[], options: UploadOptionsDto): Promise<void> {
|
||||
await this.connect();
|
||||
const api = await this.connect();
|
||||
|
||||
const formatResponse = await this.immichApi.serverInfoApi.getSupportedMediaTypes();
|
||||
const formatResponse = await api.getSupportedMediaTypes();
|
||||
const crawlService = new CrawlService(formatResponse.image, formatResponse.video);
|
||||
|
||||
const inputFiles: string[] = [];
|
||||
|
|
@ -153,7 +163,7 @@ export class UploadCommand extends BaseCommand {
|
|||
}
|
||||
}
|
||||
|
||||
const existingAlbums = await this.immichApi.albumApi.getAllAlbums();
|
||||
const existingAlbums = await api.getAllAlbums();
|
||||
|
||||
uploadProgress.start(totalSize, 0);
|
||||
uploadProgress.update({ value_formatted: 0, total_formatted: byteSize(totalSize) });
|
||||
|
|
@ -172,9 +182,7 @@ export class UploadCommand extends BaseCommand {
|
|||
if (!options.skipHash) {
|
||||
const assetBulkUploadCheckDto = { assets: [{ id: asset.path, checksum: await asset.hash() }] };
|
||||
|
||||
const checkResponse = await this.immichApi.assetApi.checkBulkUpload({
|
||||
assetBulkUploadCheckDto,
|
||||
});
|
||||
const checkResponse = await api.checkBulkUpload(assetBulkUploadCheckDto);
|
||||
|
||||
skipUpload = checkResponse.results[0].action === 'reject';
|
||||
|
||||
|
|
@ -188,9 +196,10 @@ export class UploadCommand extends BaseCommand {
|
|||
|
||||
if (!skipAsset && !options.dryRun) {
|
||||
if (!skipUpload) {
|
||||
const fileRequest = await asset.getUploadFileRequest();
|
||||
const response = await this.immichApi.assetApi.uploadFile(fileRequest);
|
||||
existingAssetId = response.id;
|
||||
const formData = await asset.getUploadFormData();
|
||||
const response = await this.uploadAsset(api, formData);
|
||||
const json = await response.json();
|
||||
existingAssetId = json.id;
|
||||
uploadCounter++;
|
||||
totalSizeUploaded += asset.fileSize;
|
||||
}
|
||||
|
|
@ -198,17 +207,14 @@ export class UploadCommand extends BaseCommand {
|
|||
if ((options.album || options.albumName) && asset.albumName !== undefined) {
|
||||
let album = existingAlbums.find((album) => album.albumName === asset.albumName);
|
||||
if (!album) {
|
||||
const response = await this.immichApi.albumApi.createAlbum({
|
||||
createAlbumDto: { albumName: asset.albumName },
|
||||
});
|
||||
const response = await api.createAlbum({ albumName: asset.albumName });
|
||||
album = response;
|
||||
existingAlbums.push(album);
|
||||
}
|
||||
|
||||
if (existingAssetId) {
|
||||
await this.immichApi.albumApi.addAssetsToAlbum({
|
||||
id: album.id,
|
||||
bulkIdsDto: { ids: [existingAssetId] },
|
||||
await api.addAssetsToAlbum(album.id, {
|
||||
ids: [existingAssetId],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -248,4 +254,21 @@ export class UploadCommand extends BaseCommand {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async uploadAsset(api: ImmichApi, data: FormData): Promise<Response> {
|
||||
const url = api.instanceUrl + '/asset/upload';
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'post',
|
||||
redirect: 'error',
|
||||
headers: {
|
||||
'x-api-key': api.apiKey,
|
||||
},
|
||||
body: data,
|
||||
});
|
||||
if (response.status !== 200 && response.status !== 201) {
|
||||
throw new Error(await response.text());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,56 +1,106 @@
|
|||
import {
|
||||
AlbumApi,
|
||||
APIKeyApi,
|
||||
AssetApi,
|
||||
AuthenticationApi,
|
||||
Configuration,
|
||||
JobApi,
|
||||
OAuthApi,
|
||||
ServerInfoApi,
|
||||
SystemConfigApi,
|
||||
UserApi,
|
||||
addAssetsToAlbum,
|
||||
checkBulkUpload,
|
||||
createAlbum,
|
||||
createApiKey,
|
||||
getAllAlbums,
|
||||
getAllAssets,
|
||||
getAssetStatistics,
|
||||
getMyUserInfo,
|
||||
getServerVersion,
|
||||
getSupportedMediaTypes,
|
||||
login,
|
||||
pingServer,
|
||||
signUpAdmin,
|
||||
uploadFile,
|
||||
ApiKeyCreateDto,
|
||||
AssetBulkUploadCheckDto,
|
||||
BulkIdsDto,
|
||||
CreateAlbumDto,
|
||||
CreateAssetDto,
|
||||
LoginCredentialDto,
|
||||
SignUpDto,
|
||||
} from '@immich/sdk';
|
||||
|
||||
/**
|
||||
* Wraps the underlying API to abstract away the options and make API calls mockable for testing.
|
||||
*/
|
||||
export class ImmichApi {
|
||||
public userApi: UserApi;
|
||||
public albumApi: AlbumApi;
|
||||
public assetApi: AssetApi;
|
||||
public authenticationApi: AuthenticationApi;
|
||||
public oauthApi: OAuthApi;
|
||||
public serverInfoApi: ServerInfoApi;
|
||||
public jobApi: JobApi;
|
||||
public keyApi: APIKeyApi;
|
||||
public systemConfigApi: SystemConfigApi;
|
||||
|
||||
private readonly config;
|
||||
private readonly options;
|
||||
|
||||
constructor(
|
||||
public instanceUrl: string,
|
||||
public apiKey: string,
|
||||
) {
|
||||
this.config = new Configuration({
|
||||
basePath: instanceUrl,
|
||||
this.options = {
|
||||
baseUrl: instanceUrl,
|
||||
headers: {
|
||||
'x-api-key': apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
this.userApi = new UserApi(this.config);
|
||||
this.albumApi = new AlbumApi(this.config);
|
||||
this.assetApi = new AssetApi(this.config);
|
||||
this.authenticationApi = new AuthenticationApi(this.config);
|
||||
this.oauthApi = new OAuthApi(this.config);
|
||||
this.serverInfoApi = new ServerInfoApi(this.config);
|
||||
this.jobApi = new JobApi(this.config);
|
||||
this.keyApi = new APIKeyApi(this.config);
|
||||
this.systemConfigApi = new SystemConfigApi(this.config);
|
||||
};
|
||||
}
|
||||
|
||||
setApiKey(apiKey: string) {
|
||||
this.apiKey = apiKey;
|
||||
if (!this.config.headers) {
|
||||
if (!this.options.headers) {
|
||||
throw new Error('missing headers');
|
||||
}
|
||||
this.config.headers['x-api-key'] = apiKey;
|
||||
this.options.headers['x-api-key'] = apiKey;
|
||||
}
|
||||
|
||||
async addAssetsToAlbum(id: string, bulkIdsDto: BulkIdsDto) {
|
||||
return await addAssetsToAlbum({ id, bulkIdsDto }, this.options);
|
||||
}
|
||||
|
||||
async checkBulkUpload(assetBulkUploadCheckDto: AssetBulkUploadCheckDto) {
|
||||
return await checkBulkUpload({ assetBulkUploadCheckDto }, this.options);
|
||||
}
|
||||
|
||||
async createAlbum(createAlbumDto: CreateAlbumDto) {
|
||||
return await createAlbum({ createAlbumDto }, this.options);
|
||||
}
|
||||
|
||||
async createApiKey(apiKeyCreateDto: ApiKeyCreateDto, options: { headers: { Authorization: string } }) {
|
||||
return await createApiKey({ apiKeyCreateDto }, { ...this.options, ...options });
|
||||
}
|
||||
|
||||
async getAllAlbums() {
|
||||
return await getAllAlbums({}, this.options);
|
||||
}
|
||||
|
||||
async getAllAssets() {
|
||||
return await getAllAssets({}, this.options);
|
||||
}
|
||||
|
||||
async getAssetStatistics() {
|
||||
return await getAssetStatistics({}, this.options);
|
||||
}
|
||||
|
||||
async getMyUserInfo() {
|
||||
return await getMyUserInfo(this.options);
|
||||
}
|
||||
|
||||
async getServerVersion() {
|
||||
return await getServerVersion(this.options);
|
||||
}
|
||||
|
||||
async getSupportedMediaTypes() {
|
||||
return await getSupportedMediaTypes(this.options);
|
||||
}
|
||||
|
||||
async login(loginCredentialDto: LoginCredentialDto) {
|
||||
return await login({ loginCredentialDto }, this.options);
|
||||
}
|
||||
|
||||
async pingServer() {
|
||||
return await pingServer(this.options);
|
||||
}
|
||||
|
||||
async signUpAdmin(signUpDto: SignUpDto) {
|
||||
return await signUpAdmin({ signUpDto }, this.options);
|
||||
}
|
||||
|
||||
async uploadFile(createAssetDto: CreateAssetDto) {
|
||||
return await uploadFile({ createAssetDto }, this.options);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,18 +12,20 @@ import {
|
|||
spyOnConsole,
|
||||
} from '../../test/cli-test-utils';
|
||||
|
||||
const mockPingServer = vi.fn(() => Promise.resolve({ res: 'pong' }));
|
||||
const mockUserInfo = vi.fn(() => Promise.resolve({ email: 'admin@example.com' }));
|
||||
const mocks = vi.hoisted(() => {
|
||||
return {
|
||||
getMyUserInfo: vi.fn(() => Promise.resolve({ email: 'admin@example.com' })),
|
||||
pingServer: vi.fn(() => Promise.resolve({ res: 'pong' })),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@immich/sdk', async () => ({
|
||||
...(await vi.importActual('@immich/sdk')),
|
||||
UserApi: vi.fn().mockImplementation(() => {
|
||||
return { getMyUserInfo: mockUserInfo };
|
||||
}),
|
||||
ServerInfoApi: vi.fn().mockImplementation(() => {
|
||||
return { pingServer: mockPingServer };
|
||||
}),
|
||||
}));
|
||||
vi.mock('./api.service', async (importOriginal) => {
|
||||
const module = await importOriginal<typeof import('./api.service')>();
|
||||
// @ts-expect-error this is only a partial implementation of the return value
|
||||
module.ImmichApi.prototype.getMyUserInfo = mocks.getMyUserInfo;
|
||||
module.ImmichApi.prototype.pingServer = mocks.pingServer;
|
||||
return module;
|
||||
});
|
||||
|
||||
describe('SessionService', () => {
|
||||
let sessionService: SessionService;
|
||||
|
|
@ -46,7 +48,7 @@ describe('SessionService', () => {
|
|||
);
|
||||
|
||||
await sessionService.connect();
|
||||
expect(mockPingServer).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.pingServer).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should error if no auth file exists', async () => {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { access, constants, mkdir, readFile, unlink, writeFile } from 'node:fs/p
|
|||
import path from 'node:path';
|
||||
import yaml from 'yaml';
|
||||
import { ImmichApi } from './api.service';
|
||||
|
||||
class LoginError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
|
|
@ -51,12 +50,12 @@ export class SessionService {
|
|||
|
||||
const api = new ImmichApi(instanceUrl, apiKey);
|
||||
|
||||
const pingResponse = await api.serverInfoApi.pingServer().catch((error) => {
|
||||
throw new Error(`Failed to connect to server ${api.instanceUrl}: ${error.message}`);
|
||||
const pingResponse = await api.pingServer().catch((error) => {
|
||||
throw new Error(`Failed to connect to server ${instanceUrl}: ${error.message}`, error);
|
||||
});
|
||||
|
||||
if (pingResponse.res !== 'pong') {
|
||||
throw new Error(`Could not parse response. Is Immich listening on ${api.instanceUrl}?`);
|
||||
throw new Error(`Could not parse response. Is Immich listening on ${instanceUrl}?`);
|
||||
}
|
||||
|
||||
return api;
|
||||
|
|
@ -68,7 +67,7 @@ export class SessionService {
|
|||
const api = new ImmichApi(instanceUrl, apiKey);
|
||||
|
||||
// Check if server and api key are valid
|
||||
const userInfo = await api.userApi.getMyUserInfo().catch((error) => {
|
||||
const userInfo = await api.getMyUserInfo().catch((error) => {
|
||||
throw new LoginError(`Failed to connect to server ${instanceUrl}: ${error.message}`);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue