Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: tree shake unused API methods from CLI #6973

Merged
merged 1 commit into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion cli/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"mock-fs": "^5.2.0",
"typescript": "^5.3.3",
"vite": "^5.0.12",
"vitest": "^1.2.1",
"vitest": "^1.2.2",
"yaml": "^2.3.1"
},
"scripts": {
Expand Down
7 changes: 3 additions & 4 deletions cli/src/commands/base-command.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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();
}
}
8 changes: 4 additions & 4 deletions cli/src/commands/server-info.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('.', ''))}`);
Expand Down
63 changes: 43 additions & 20 deletions cli/src/commands/upload.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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');
}
Expand All @@ -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> {
Expand Down Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -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) });
Expand All @@ -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';

Expand All @@ -188,27 +196,25 @@ 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;
}

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],
});
}
}
Expand Down Expand Up @@ -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;
}
}
122 changes: 86 additions & 36 deletions cli/src/services/api.service.ts
Original file line number Diff line number Diff line change
@@ -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);
}
Comment on lines +51 to +53
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can remove async and await from all of these methods. I prefer not having any of those unless they're actually needed.


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);
}
}
28 changes: 15 additions & 13 deletions cli/src/services/session.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' }));

vi.mock('@immich/sdk', async () => ({
...(await vi.importActual('@immich/sdk')),
UserApi: vi.fn().mockImplementation(() => {
return { getMyUserInfo: mockUserInfo };
}),
ServerInfoApi: vi.fn().mockImplementation(() => {
return { pingServer: mockPingServer };
}),
}));
const mocks = vi.hoisted(() => {
return {
getMyUserInfo: vi.fn(() => Promise.resolve({ email: 'admin@example.com' })),
pingServer: vi.fn(() => Promise.resolve({ res: 'pong' })),
};
});

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;
Expand All @@ -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 () => {
Expand Down
Loading
Loading