mirror of
https://github.com/immich-app/immich.git
synced 2024-12-29 15:11:58 +00:00
feat(cli) Add new CLI (#3066)
* Add new cli * Remove old readme * Add documentation to readme file * Add github workflow tests for cli * Fix typo in docs * Add usage info to readme * Add package-lock.json * Fix tsconfig * Cleanup * Fix lint * Cleanup package.json * Fix accidental server change * Remove rootdir from cli * Remove tsbuildinfo * Add prettierignore * Make CLI use internal openapi specs * Add ignore and dry-run features * Sort paths alphabetically * Don't remove substring * Remove shorthand for delete * Remove unused import * Remove chokidar * Set correct openapi cli generator script * Add progress bar * Rename target to asset * Add deletion progress bar * Ignore require statement * Use read streams instead of readfile * Fix github feedback * Fix upload requires * More github comments * Cleanup messages * Cleaner pattern concats * Github comments --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
37edef834e
commit
6f4449d5e9
53 changed files with 21349 additions and 0 deletions
26
.github/workflows/test.yml
vendored
26
.github/workflows/test.yml
vendored
|
@ -73,6 +73,32 @@ jobs:
|
||||||
run: npm run test:cov
|
run: npm run test:cov
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
|
cli-unit-tests:
|
||||||
|
name: Run cli test suites
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./cli
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Run npm install
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run linter
|
||||||
|
run: npm run lint
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
|
- name: Run formatter
|
||||||
|
run: npm run format
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
|
- name: Run unit tests & coverage
|
||||||
|
run: npm run test:cov
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
web-unit-tests:
|
web-unit-tests:
|
||||||
name: Run web unit test suites and checks
|
name: Run web unit test suites and checks
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
20
cli/.editorconfig
Normal file
20
cli/.editorconfig
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# Editor configuration, see https://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.{ts,js}]
|
||||||
|
quote_type = single
|
||||||
|
|
||||||
|
[*.{md,mdx}]
|
||||||
|
max_line_length = off
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
quote_type = double
|
1
cli/.eslintignore
Normal file
1
cli/.eslintignore
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/dist
|
23
cli/.eslintrc.js
Normal file
23
cli/.eslintrc.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
module.exports = {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
project: 'tsconfig.json',
|
||||||
|
sourceType: 'module',
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
},
|
||||||
|
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||||
|
extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
jest: true,
|
||||||
|
},
|
||||||
|
ignorePatterns: ['.eslintrc.js'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/interface-name-prefix': 'off',
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'prettier/prettier': 0,
|
||||||
|
},
|
||||||
|
};
|
13
cli/.gitignore
vendored
Normal file
13
cli/.gitignore
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
*-debug.log
|
||||||
|
*-error.log
|
||||||
|
/.nyc_output
|
||||||
|
/dist
|
||||||
|
/lib
|
||||||
|
/tmp
|
||||||
|
/yarn.lock
|
||||||
|
node_modules
|
||||||
|
oclif.manifest.json
|
||||||
|
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
/coverage/
|
18
cli/.prettierignore
Normal file
18
cli/.prettierignore
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/build
|
||||||
|
/package
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
src/api/open-api
|
||||||
|
*.md
|
||||||
|
*.json
|
||||||
|
coverage
|
||||||
|
dist
|
||||||
|
**/migrations/**
|
||||||
|
|
||||||
|
# Ignore files for PNPM, NPM and YARN
|
||||||
|
pnpm-lock.yaml
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
6
cli/.prettierrc
Normal file
6
cli/.prettierrc
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 120,
|
||||||
|
"semi": true
|
||||||
|
}
|
46
cli/README.md
Normal file
46
cli/README.md
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
A command-line interface for interfacing with Immich
|
||||||
|
|
||||||
|
# Getting started
|
||||||
|
|
||||||
|
$ ts-node cli/src
|
||||||
|
|
||||||
|
To start using the CLI, you need to login with an API key first:
|
||||||
|
|
||||||
|
$ ts-node cli/src login-key https://your-immich-instance/api your-api-key
|
||||||
|
|
||||||
|
NOTE: This will store your api key under ~/.config/immich/auth.yml
|
||||||
|
|
||||||
|
Next, you can run commands:
|
||||||
|
|
||||||
|
$ ts-node cli/src server-info
|
||||||
|
|
||||||
|
When you're done, log out to remove the credentials from your filesystem
|
||||||
|
|
||||||
|
$ ts-node cli/src logout
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage: immich [options] [command]
|
||||||
|
|
||||||
|
Immich command line interface
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h, --help display help for command
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
upload [options] [paths...] Upload assets
|
||||||
|
import [options] [paths...] Import existing assets
|
||||||
|
server-info Display server information
|
||||||
|
login-key [instanceUrl] [apiKey] Login using an API key
|
||||||
|
help [command] display help for command
|
||||||
|
```
|
||||||
|
|
||||||
|
# Todo
|
||||||
|
|
||||||
|
- Sidecar should check both .jpg.xmp and .xmp
|
||||||
|
- Sidecar check could be case-insensitive
|
||||||
|
|
||||||
|
# Known issues
|
||||||
|
|
||||||
|
- Upload can't use sdk due to multiple issues
|
0
cli/asdf
Normal file
0
cli/asdf
Normal file
8
cli/jest.config.ts
Normal file
8
cli/jest.config.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import type { Config } from 'jest';
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
setupFilesAfterEnv: ['jest-extended/all'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
6261
cli/package-lock.json
generated
Normal file
6261
cli/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
49
cli/package.json
Normal file
49
cli/package.json
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
{
|
||||||
|
"name": "immich-cli",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.4.0",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"mime-types": "^2.1.35",
|
||||||
|
"systeminformation": "^5.18.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/byte-size": "^8.1.0",
|
||||||
|
"@types/chai": "^4.3.5",
|
||||||
|
"@types/cli-progress": "^3.11.0",
|
||||||
|
"@types/jest": "^29.5.2",
|
||||||
|
"@types/js-yaml": "^4.0.5",
|
||||||
|
"@types/mime-types": "^2.1.1",
|
||||||
|
"@types/mock-fs": "^4.13.1",
|
||||||
|
"@types/node": "^20.3.1",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.60.1",
|
||||||
|
"byte-size": "^8.1.1",
|
||||||
|
"chai": "^4.3.7",
|
||||||
|
"cli-progress": "^3.12.0",
|
||||||
|
"commander": "^11.0.0",
|
||||||
|
"eslint": "^8.43.0",
|
||||||
|
"eslint-config-prettier": "^8.8.0",
|
||||||
|
"eslint-plugin-jest": "^27.2.2",
|
||||||
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
|
"eslint-plugin-unicorn": "^47.0.0",
|
||||||
|
"glob": "^10.3.1",
|
||||||
|
"jest": "^29.5.0",
|
||||||
|
"jest-extended": "^4.0.0",
|
||||||
|
"jest-message-util": "^29.5.0",
|
||||||
|
"jest-mock-axios": "^4.7.2",
|
||||||
|
"jest-when": "^3.5.2",
|
||||||
|
"mock-fs": "^5.2.0",
|
||||||
|
"picomatch": "^2.3.1",
|
||||||
|
"ts-jest": "^29.1.0",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"tslib": "^2.5.3",
|
||||||
|
"typescript": "^4.9.4",
|
||||||
|
"yaml": "^2.3.1"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"lint": "eslint \"src/**/*.ts\" --max-warnings 0",
|
||||||
|
"prepack": "yarn build ",
|
||||||
|
"test": "jest",
|
||||||
|
"test:cov": "jest --coverage",
|
||||||
|
"format": "prettier --check ."
|
||||||
|
}
|
||||||
|
}
|
3
cli/src/__mocks__/axios.ts
Normal file
3
cli/src/__mocks__/axios.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// ./__mocks__/axios.js
|
||||||
|
import mockAxios from 'jest-mock-axios';
|
||||||
|
export default mockAxios;
|
50
cli/src/api/client.ts
Normal file
50
cli/src/api/client.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import {
|
||||||
|
AlbumApi,
|
||||||
|
APIKeyApi,
|
||||||
|
AssetApi,
|
||||||
|
AuthenticationApi,
|
||||||
|
Configuration,
|
||||||
|
JobApi,
|
||||||
|
OAuthApi,
|
||||||
|
ServerInfoApi,
|
||||||
|
SystemConfigApi,
|
||||||
|
UserApi,
|
||||||
|
} from './open-api';
|
||||||
|
import { ApiConfiguration } from '../cores/api-configuration';
|
||||||
|
|
||||||
|
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;
|
||||||
|
public readonly apiConfiguration: ApiConfiguration;
|
||||||
|
|
||||||
|
constructor(instanceUrl: string, apiKey: string) {
|
||||||
|
this.apiConfiguration = new ApiConfiguration(instanceUrl, apiKey);
|
||||||
|
this.config = new Configuration({
|
||||||
|
basePath: instanceUrl,
|
||||||
|
baseOptions: {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
4
cli/src/api/open-api/.gitignore
vendored
Normal file
4
cli/src/api/open-api/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
wwwroot/*.js
|
||||||
|
node_modules
|
||||||
|
typings
|
||||||
|
dist
|
1
cli/src/api/open-api/.npmignore
Normal file
1
cli/src/api/open-api/.npmignore
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm
|
23
cli/src/api/open-api/.openapi-generator-ignore
Normal file
23
cli/src/api/open-api/.openapi-generator-ignore
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# OpenAPI Generator Ignore
|
||||||
|
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
|
||||||
|
|
||||||
|
# Use this file to prevent files from being overwritten by the generator.
|
||||||
|
# The patterns follow closely to .gitignore or .dockerignore.
|
||||||
|
|
||||||
|
# As an example, the C# client generator defines ApiClient.cs.
|
||||||
|
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
|
||||||
|
#ApiClient.cs
|
||||||
|
|
||||||
|
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
|
||||||
|
#foo/*/qux
|
||||||
|
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
|
||||||
|
|
||||||
|
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
|
||||||
|
#foo/**/qux
|
||||||
|
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
|
||||||
|
|
||||||
|
# You can also negate patterns with an exclamation (!).
|
||||||
|
# For example, you can ignore all files in a docs folder with the file extension .md:
|
||||||
|
#docs/*.md
|
||||||
|
# Then explicitly reverse the ignore rule for a single file:
|
||||||
|
#!docs/README.md
|
9
cli/src/api/open-api/.openapi-generator/FILES
Normal file
9
cli/src/api/open-api/.openapi-generator/FILES
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
.gitignore
|
||||||
|
.npmignore
|
||||||
|
.openapi-generator-ignore
|
||||||
|
api.ts
|
||||||
|
base.ts
|
||||||
|
common.ts
|
||||||
|
configuration.ts
|
||||||
|
git_push.sh
|
||||||
|
index.ts
|
1
cli/src/api/open-api/.openapi-generator/VERSION
Normal file
1
cli/src/api/open-api/.openapi-generator/VERSION
Normal file
|
@ -0,0 +1 @@
|
||||||
|
6.5.0
|
12508
cli/src/api/open-api/api.ts
Normal file
12508
cli/src/api/open-api/api.ts
Normal file
File diff suppressed because it is too large
Load diff
72
cli/src/api/open-api/base.ts
Normal file
72
cli/src/api/open-api/base.ts
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Immich
|
||||||
|
* Immich API
|
||||||
|
*
|
||||||
|
* The version of the OpenAPI document: 1.65.0
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
* https://openapi-generator.tech
|
||||||
|
* Do not edit the class manually.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import type { Configuration } from './configuration';
|
||||||
|
// Some imports not used depending on template conditions
|
||||||
|
// @ts-ignore
|
||||||
|
import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||||
|
import globalAxios from 'axios';
|
||||||
|
|
||||||
|
export const BASE_PATH = "/api".replace(/\/+$/, "");
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
*/
|
||||||
|
export const COLLECTION_FORMATS = {
|
||||||
|
csv: ",",
|
||||||
|
ssv: " ",
|
||||||
|
tsv: "\t",
|
||||||
|
pipes: "|",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface RequestArgs
|
||||||
|
*/
|
||||||
|
export interface RequestArgs {
|
||||||
|
url: string;
|
||||||
|
options: AxiosRequestConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @class BaseAPI
|
||||||
|
*/
|
||||||
|
export class BaseAPI {
|
||||||
|
protected configuration: Configuration | undefined;
|
||||||
|
|
||||||
|
constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected axios: AxiosInstance = globalAxios) {
|
||||||
|
if (configuration) {
|
||||||
|
this.configuration = configuration;
|
||||||
|
this.basePath = configuration.basePath || this.basePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @class RequiredError
|
||||||
|
* @extends {Error}
|
||||||
|
*/
|
||||||
|
export class RequiredError extends Error {
|
||||||
|
constructor(public field: string, msg?: string) {
|
||||||
|
super(msg);
|
||||||
|
this.name = "RequiredError"
|
||||||
|
}
|
||||||
|
}
|
150
cli/src/api/open-api/common.ts
Normal file
150
cli/src/api/open-api/common.ts
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Immich
|
||||||
|
* Immich API
|
||||||
|
*
|
||||||
|
* The version of the OpenAPI document: 1.65.0
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
* https://openapi-generator.tech
|
||||||
|
* Do not edit the class manually.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import type { Configuration } from "./configuration";
|
||||||
|
import type { RequestArgs } from "./base";
|
||||||
|
import type { AxiosInstance, AxiosResponse } from 'axios';
|
||||||
|
import { RequiredError } from "./base";
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
*/
|
||||||
|
export const DUMMY_BASE_URL = 'https://example.com'
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @throws {RequiredError}
|
||||||
|
* @export
|
||||||
|
*/
|
||||||
|
export const assertParamExists = function (functionName: string, paramName: string, paramValue: unknown) {
|
||||||
|
if (paramValue === null || paramValue === undefined) {
|
||||||
|
throw new RequiredError(paramName, `Required parameter ${paramName} was null or undefined when calling ${functionName}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
*/
|
||||||
|
export const setApiKeyToObject = async function (object: any, keyParamName: string, configuration?: Configuration) {
|
||||||
|
if (configuration && configuration.apiKey) {
|
||||||
|
const localVarApiKeyValue = typeof configuration.apiKey === 'function'
|
||||||
|
? await configuration.apiKey(keyParamName)
|
||||||
|
: await configuration.apiKey;
|
||||||
|
object[keyParamName] = localVarApiKeyValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
*/
|
||||||
|
export const setBasicAuthToObject = function (object: any, configuration?: Configuration) {
|
||||||
|
if (configuration && (configuration.username || configuration.password)) {
|
||||||
|
object["auth"] = { username: configuration.username, password: configuration.password };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
*/
|
||||||
|
export const setBearerAuthToObject = async function (object: any, configuration?: Configuration) {
|
||||||
|
if (configuration && configuration.accessToken) {
|
||||||
|
const accessToken = typeof configuration.accessToken === 'function'
|
||||||
|
? await configuration.accessToken()
|
||||||
|
: await configuration.accessToken;
|
||||||
|
object["Authorization"] = "Bearer " + accessToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
*/
|
||||||
|
export const setOAuthToObject = async function (object: any, name: string, scopes: string[], configuration?: Configuration) {
|
||||||
|
if (configuration && configuration.accessToken) {
|
||||||
|
const localVarAccessTokenValue = typeof configuration.accessToken === 'function'
|
||||||
|
? await configuration.accessToken(name, scopes)
|
||||||
|
: await configuration.accessToken;
|
||||||
|
object["Authorization"] = "Bearer " + localVarAccessTokenValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFlattenedQueryParams(urlSearchParams: URLSearchParams, parameter: any, key: string = ""): void {
|
||||||
|
if (parameter == null) return;
|
||||||
|
if (typeof parameter === "object") {
|
||||||
|
if (Array.isArray(parameter)) {
|
||||||
|
(parameter as any[]).forEach(item => setFlattenedQueryParams(urlSearchParams, item, key));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Object.keys(parameter).forEach(currentKey =>
|
||||||
|
setFlattenedQueryParams(urlSearchParams, parameter[currentKey], `${key}${key !== '' ? '.' : ''}${currentKey}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (urlSearchParams.has(key)) {
|
||||||
|
urlSearchParams.append(key, parameter);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
urlSearchParams.set(key, parameter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
*/
|
||||||
|
export const setSearchParams = function (url: URL, ...objects: any[]) {
|
||||||
|
const searchParams = new URLSearchParams(url.search);
|
||||||
|
setFlattenedQueryParams(searchParams, objects);
|
||||||
|
url.search = searchParams.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
*/
|
||||||
|
export const serializeDataIfNeeded = function (value: any, requestOptions: any, configuration?: Configuration) {
|
||||||
|
const nonString = typeof value !== 'string';
|
||||||
|
const needsSerialization = nonString && configuration && configuration.isJsonMime
|
||||||
|
? configuration.isJsonMime(requestOptions.headers['Content-Type'])
|
||||||
|
: nonString;
|
||||||
|
return needsSerialization
|
||||||
|
? JSON.stringify(value !== undefined ? value : {})
|
||||||
|
: (value || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
*/
|
||||||
|
export const toPathString = function (url: URL) {
|
||||||
|
return url.pathname + url.search + url.hash
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
*/
|
||||||
|
export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) {
|
||||||
|
return <T = unknown, R = AxiosResponse<T>>(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
|
||||||
|
const axiosRequestArgs = {...axiosArgs.options, url: (configuration?.basePath || basePath) + axiosArgs.url};
|
||||||
|
return axios.request<T, R>(axiosRequestArgs);
|
||||||
|
};
|
||||||
|
}
|
101
cli/src/api/open-api/configuration.ts
Normal file
101
cli/src/api/open-api/configuration.ts
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Immich
|
||||||
|
* Immich API
|
||||||
|
*
|
||||||
|
* The version of the OpenAPI document: 1.65.0
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
* https://openapi-generator.tech
|
||||||
|
* Do not edit the class manually.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
export interface ConfigurationParameters {
|
||||||
|
apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>);
|
||||||
|
username?: string;
|
||||||
|
password?: string;
|
||||||
|
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
|
||||||
|
basePath?: string;
|
||||||
|
baseOptions?: any;
|
||||||
|
formDataCtor?: new () => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Configuration {
|
||||||
|
/**
|
||||||
|
* parameter for apiKey security
|
||||||
|
* @param name security name
|
||||||
|
* @memberof Configuration
|
||||||
|
*/
|
||||||
|
apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>);
|
||||||
|
/**
|
||||||
|
* parameter for basic security
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof Configuration
|
||||||
|
*/
|
||||||
|
username?: string;
|
||||||
|
/**
|
||||||
|
* parameter for basic security
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof Configuration
|
||||||
|
*/
|
||||||
|
password?: string;
|
||||||
|
/**
|
||||||
|
* parameter for oauth2 security
|
||||||
|
* @param name security name
|
||||||
|
* @param scopes oauth2 scope
|
||||||
|
* @memberof Configuration
|
||||||
|
*/
|
||||||
|
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
|
||||||
|
/**
|
||||||
|
* override base path
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof Configuration
|
||||||
|
*/
|
||||||
|
basePath?: string;
|
||||||
|
/**
|
||||||
|
* base options for axios calls
|
||||||
|
*
|
||||||
|
* @type {any}
|
||||||
|
* @memberof Configuration
|
||||||
|
*/
|
||||||
|
baseOptions?: any;
|
||||||
|
/**
|
||||||
|
* The FormData constructor that will be used to create multipart form data
|
||||||
|
* requests. You can inject this here so that execution environments that
|
||||||
|
* do not support the FormData class can still run the generated client.
|
||||||
|
*
|
||||||
|
* @type {new () => FormData}
|
||||||
|
*/
|
||||||
|
formDataCtor?: new () => any;
|
||||||
|
|
||||||
|
constructor(param: ConfigurationParameters = {}) {
|
||||||
|
this.apiKey = param.apiKey;
|
||||||
|
this.username = param.username;
|
||||||
|
this.password = param.password;
|
||||||
|
this.accessToken = param.accessToken;
|
||||||
|
this.basePath = param.basePath;
|
||||||
|
this.baseOptions = param.baseOptions;
|
||||||
|
this.formDataCtor = param.formDataCtor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given MIME is a JSON MIME.
|
||||||
|
* JSON MIME examples:
|
||||||
|
* application/json
|
||||||
|
* application/json; charset=UTF8
|
||||||
|
* APPLICATION/JSON
|
||||||
|
* application/vnd.company+json
|
||||||
|
* @param mime - MIME (Multipurpose Internet Mail Extensions)
|
||||||
|
* @return True if the given MIME is JSON, false otherwise.
|
||||||
|
*/
|
||||||
|
public isJsonMime(mime: string): boolean {
|
||||||
|
const jsonMime: RegExp = new RegExp('^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i');
|
||||||
|
return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json');
|
||||||
|
}
|
||||||
|
}
|
57
cli/src/api/open-api/git_push.sh
Normal file
57
cli/src/api/open-api/git_push.sh
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/
|
||||||
|
#
|
||||||
|
# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com"
|
||||||
|
|
||||||
|
git_user_id=$1
|
||||||
|
git_repo_id=$2
|
||||||
|
release_note=$3
|
||||||
|
git_host=$4
|
||||||
|
|
||||||
|
if [ "$git_host" = "" ]; then
|
||||||
|
git_host="github.com"
|
||||||
|
echo "[INFO] No command line input provided. Set \$git_host to $git_host"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$git_user_id" = "" ]; then
|
||||||
|
git_user_id="GIT_USER_ID"
|
||||||
|
echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$git_repo_id" = "" ]; then
|
||||||
|
git_repo_id="GIT_REPO_ID"
|
||||||
|
echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$release_note" = "" ]; then
|
||||||
|
release_note="Minor update"
|
||||||
|
echo "[INFO] No command line input provided. Set \$release_note to $release_note"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Initialize the local directory as a Git repository
|
||||||
|
git init
|
||||||
|
|
||||||
|
# Adds the files in the local repository and stages them for commit.
|
||||||
|
git add .
|
||||||
|
|
||||||
|
# Commits the tracked changes and prepares them to be pushed to a remote repository.
|
||||||
|
git commit -m "$release_note"
|
||||||
|
|
||||||
|
# Sets the new remote
|
||||||
|
git_remote=$(git remote)
|
||||||
|
if [ "$git_remote" = "" ]; then # git remote not defined
|
||||||
|
|
||||||
|
if [ "$GIT_TOKEN" = "" ]; then
|
||||||
|
echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment."
|
||||||
|
git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git
|
||||||
|
else
|
||||||
|
git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git
|
||||||
|
fi
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
git pull origin master
|
||||||
|
|
||||||
|
# Pushes (Forces) the changes in the local repository up to the remote repository
|
||||||
|
echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git"
|
||||||
|
git push origin master 2>&1 | grep -v 'To https'
|
18
cli/src/api/open-api/index.ts
Normal file
18
cli/src/api/open-api/index.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Immich
|
||||||
|
* Immich API
|
||||||
|
*
|
||||||
|
* The version of the OpenAPI document: 1.65.0
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
* https://openapi-generator.tech
|
||||||
|
* Do not edit the class manually.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
export * from "./api";
|
||||||
|
export * from "./configuration";
|
||||||
|
|
38
cli/src/cli/base-command.ts
Normal file
38
cli/src/cli/base-command.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { ImmichApi } from '../api/client';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { SessionService } from '../services/session.service';
|
||||||
|
import { LoginError } from '../cores/errors/login-error';
|
||||||
|
import { exit } from 'node:process';
|
||||||
|
import os from 'os';
|
||||||
|
import { ServerVersionReponseDto, UserResponseDto } from 'src/api/open-api';
|
||||||
|
|
||||||
|
export abstract class BaseCommand {
|
||||||
|
protected sessionService!: SessionService;
|
||||||
|
protected immichApi!: ImmichApi;
|
||||||
|
protected deviceId!: string;
|
||||||
|
protected user!: UserResponseDto;
|
||||||
|
protected serverVersion!: ServerVersionReponseDto;
|
||||||
|
|
||||||
|
protected configDir;
|
||||||
|
protected authPath;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
const userHomeDir = os.homedir();
|
||||||
|
this.configDir = path.join(userHomeDir, '.config/immich/');
|
||||||
|
this.sessionService = new SessionService(this.configDir);
|
||||||
|
this.authPath = path.join(this.configDir, 'auth.yml');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async connect(): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.immichApi = await this.sessionService.connect();
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof LoginError) {
|
||||||
|
console.log(error.message);
|
||||||
|
exit(1);
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
cli/src/commands/login/key.ts
Normal file
9
cli/src/commands/login/key.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { BaseCommand } from '../../cli/base-command';
|
||||||
|
|
||||||
|
export default class LoginKey extends BaseCommand {
|
||||||
|
public async run(instanceUrl: string, apiKey: string): Promise<void> {
|
||||||
|
console.log('Executing API key auth flow...');
|
||||||
|
|
||||||
|
await this.sessionService.keyLogin(instanceUrl, apiKey);
|
||||||
|
}
|
||||||
|
}
|
13
cli/src/commands/logout.ts
Normal file
13
cli/src/commands/logout.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { BaseCommand } from '../cli/base-command';
|
||||||
|
|
||||||
|
export default class Logout extends BaseCommand {
|
||||||
|
public static readonly description = 'Logout and remove persisted credentials';
|
||||||
|
|
||||||
|
public async run(): Promise<void> {
|
||||||
|
console.log('Executing logout flow...');
|
||||||
|
|
||||||
|
await this.sessionService.logout();
|
||||||
|
|
||||||
|
console.log('Successfully logged out');
|
||||||
|
}
|
||||||
|
}
|
15
cli/src/commands/server-info.ts
Normal file
15
cli/src/commands/server-info.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { BaseCommand } from '../cli/base-command';
|
||||||
|
|
||||||
|
export default class ServerInfo extends BaseCommand {
|
||||||
|
static description = 'Display server information';
|
||||||
|
static enableJsonFlag = true;
|
||||||
|
|
||||||
|
public async run() {
|
||||||
|
console.log('Getting server information');
|
||||||
|
|
||||||
|
await this.connect();
|
||||||
|
const { data: versionInfo } = await this.immichApi.serverInfoApi.getServerVersion();
|
||||||
|
|
||||||
|
console.log(versionInfo);
|
||||||
|
}
|
||||||
|
}
|
176
cli/src/commands/upload.ts
Normal file
176
cli/src/commands/upload.ts
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
import { BaseCommand } from '../cli/base-command';
|
||||||
|
import { CrawledAsset } from '../cores/models/crawled-asset';
|
||||||
|
import { CrawlService, UploadService } from '../services';
|
||||||
|
import * as si from 'systeminformation';
|
||||||
|
import FormData from 'form-data';
|
||||||
|
import { UploadOptionsDto } from '../cores/dto/upload-options-dto';
|
||||||
|
import { CrawlOptionsDto } from '../cores/dto/crawl-options-dto';
|
||||||
|
|
||||||
|
import cliProgress from 'cli-progress';
|
||||||
|
import byteSize from 'byte-size';
|
||||||
|
|
||||||
|
export default class Upload extends BaseCommand {
|
||||||
|
private crawlService = new CrawlService();
|
||||||
|
private uploadService!: UploadService;
|
||||||
|
deviceId!: string;
|
||||||
|
uploadLength!: number;
|
||||||
|
dryRun = false;
|
||||||
|
|
||||||
|
public async run(paths: string[], options: UploadOptionsDto): Promise<void> {
|
||||||
|
await this.connect();
|
||||||
|
|
||||||
|
const uuid = await si.uuid();
|
||||||
|
this.deviceId = uuid.os || 'CLI';
|
||||||
|
this.uploadService = new UploadService(this.immichApi.apiConfiguration);
|
||||||
|
|
||||||
|
this.dryRun = options.dryRun;
|
||||||
|
|
||||||
|
const crawlOptions = new CrawlOptionsDto();
|
||||||
|
crawlOptions.pathsToCrawl = paths;
|
||||||
|
crawlOptions.recursive = options.recursive;
|
||||||
|
crawlOptions.excludePatterns = options.excludePatterns;
|
||||||
|
|
||||||
|
const crawledFiles: string[] = await this.crawlService.crawl(crawlOptions);
|
||||||
|
|
||||||
|
if (crawledFiles.length === 0) {
|
||||||
|
console.log('No assets found, exiting');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const assetsToUpload = crawledFiles.map((path) => new CrawledAsset(path));
|
||||||
|
|
||||||
|
const uploadProgress = new cliProgress.SingleBar(
|
||||||
|
{
|
||||||
|
format: '{bar} | {percentage}% | ETA: {eta_formatted} | {value_formatted}/{total_formatted}: {filename}',
|
||||||
|
},
|
||||||
|
cliProgress.Presets.shades_classic,
|
||||||
|
);
|
||||||
|
|
||||||
|
let totalSize = 0;
|
||||||
|
let sizeSoFar = 0;
|
||||||
|
|
||||||
|
let totalSizeUploaded = 0;
|
||||||
|
let uploadCounter = 0;
|
||||||
|
|
||||||
|
for (const asset of assetsToUpload) {
|
||||||
|
// Compute total size first
|
||||||
|
await asset.process();
|
||||||
|
totalSize += asset.fileSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadProgress.start(totalSize, 0);
|
||||||
|
uploadProgress.update({ value_formatted: 0, total_formatted: byteSize(totalSize) });
|
||||||
|
|
||||||
|
for (const asset of assetsToUpload) {
|
||||||
|
uploadProgress.update({
|
||||||
|
filename: asset.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (options.import) {
|
||||||
|
const importData = {
|
||||||
|
assetPath: asset.path,
|
||||||
|
deviceAssetId: asset.deviceAssetId,
|
||||||
|
assetType: asset.assetType,
|
||||||
|
deviceId: this.deviceId,
|
||||||
|
fileCreatedAt: asset.fileCreatedAt,
|
||||||
|
fileModifiedAt: asset.fileModifiedAt,
|
||||||
|
isFavorite: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!this.dryRun) {
|
||||||
|
await this.uploadService.import(importData);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await this.uploadAsset(asset, options.skipHash);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
uploadProgress.stop();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
sizeSoFar += asset.fileSize;
|
||||||
|
if (!asset.skipped) {
|
||||||
|
totalSizeUploaded += asset.fileSize;
|
||||||
|
uploadCounter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadProgress.update(sizeSoFar, { value_formatted: byteSize(sizeSoFar) });
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadProgress.stop();
|
||||||
|
|
||||||
|
let messageStart;
|
||||||
|
if (this.dryRun) {
|
||||||
|
messageStart = 'Would have ';
|
||||||
|
} else {
|
||||||
|
messageStart = 'Successfully ';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.import) {
|
||||||
|
console.log(`${messageStart} imported ${uploadCounter} assets (${byteSize(totalSizeUploaded)})`);
|
||||||
|
} else {
|
||||||
|
if (uploadCounter === 0) {
|
||||||
|
console.log('All assets were already uploaded, nothing to do.');
|
||||||
|
} else {
|
||||||
|
console.log(`${messageStart} uploaded ${uploadCounter} assets (${byteSize(totalSizeUploaded)})`);
|
||||||
|
}
|
||||||
|
if (options.delete) {
|
||||||
|
if (this.dryRun) {
|
||||||
|
console.log(`Would now have deleted assets, but skipped due to dry run`);
|
||||||
|
} else {
|
||||||
|
console.log('Deleting assets that have been uploaded...');
|
||||||
|
const deletionProgress = new cliProgress.SingleBar(cliProgress.Presets.shades_classic);
|
||||||
|
deletionProgress.start(crawledFiles.length, 0);
|
||||||
|
|
||||||
|
for (const asset of assetsToUpload) {
|
||||||
|
if (!this.dryRun) {
|
||||||
|
await asset.delete();
|
||||||
|
}
|
||||||
|
deletionProgress.increment();
|
||||||
|
}
|
||||||
|
deletionProgress.stop();
|
||||||
|
console.log('Deletion complete');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async uploadAsset(asset: CrawledAsset, skipHash = false) {
|
||||||
|
await asset.readData();
|
||||||
|
|
||||||
|
let skipUpload = false;
|
||||||
|
if (!skipHash) {
|
||||||
|
const checksum = await asset.hash();
|
||||||
|
|
||||||
|
const checkResponse = await this.uploadService.checkIfAssetAlreadyExists(asset.path, checksum);
|
||||||
|
skipUpload = checkResponse.data.results[0].action === 'reject';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skipUpload) {
|
||||||
|
asset.skipped = true;
|
||||||
|
} else {
|
||||||
|
const uploadFormData = new FormData();
|
||||||
|
|
||||||
|
uploadFormData.append('deviceAssetId', asset.deviceAssetId);
|
||||||
|
uploadFormData.append('deviceId', this.deviceId);
|
||||||
|
uploadFormData.append('fileCreatedAt', asset.fileCreatedAt);
|
||||||
|
uploadFormData.append('fileModifiedAt', asset.fileModifiedAt);
|
||||||
|
uploadFormData.append('isFavorite', String(false));
|
||||||
|
uploadFormData.append('fileExtension', asset.fileExtension);
|
||||||
|
uploadFormData.append('assetType', asset.assetType);
|
||||||
|
uploadFormData.append('assetData', asset.assetData, { filename: asset.path });
|
||||||
|
|
||||||
|
if (asset.sidecarData) {
|
||||||
|
uploadFormData.append('sidecarData', asset.sidecarData, {
|
||||||
|
filename: asset.sidecarPath,
|
||||||
|
contentType: 'application/xml',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.dryRun) {
|
||||||
|
await this.uploadService.upload(uploadFormData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
cli/src/cores/api-configuration.ts
Normal file
9
cli/src/cores/api-configuration.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
export class ApiConfiguration {
|
||||||
|
public readonly instanceUrl!: string;
|
||||||
|
public readonly apiKey!: string;
|
||||||
|
|
||||||
|
constructor(instanceUrl: string, apiKey: string) {
|
||||||
|
this.instanceUrl = instanceUrl;
|
||||||
|
this.apiKey = apiKey;
|
||||||
|
}
|
||||||
|
}
|
58
cli/src/cores/constants.ts
Normal file
58
cli/src/cores/constants.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
// Check asset-upload.config.spec.ts for complete list
|
||||||
|
// TODO: we should get this list from the server via API in the future
|
||||||
|
|
||||||
|
// Videos
|
||||||
|
const videos = ['mp4', 'webm', 'mov', '3gp', 'avi', 'm2ts', 'mts', 'mpg', 'flv', 'mkv', 'wmv'];
|
||||||
|
|
||||||
|
// Images
|
||||||
|
const heic = ['heic', 'heif'];
|
||||||
|
const jpeg = ['jpg', 'jpeg'];
|
||||||
|
const png = ['png'];
|
||||||
|
const gif = ['gif'];
|
||||||
|
const tiff = ['tif', 'tiff'];
|
||||||
|
const webp = ['webp'];
|
||||||
|
const dng = ['dng'];
|
||||||
|
const other = [
|
||||||
|
'3fr',
|
||||||
|
'ari',
|
||||||
|
'arw',
|
||||||
|
'avif',
|
||||||
|
'cap',
|
||||||
|
'cin',
|
||||||
|
'cr2',
|
||||||
|
'cr3',
|
||||||
|
'crw',
|
||||||
|
'dcr',
|
||||||
|
'nef',
|
||||||
|
'erf',
|
||||||
|
'fff',
|
||||||
|
'iiq',
|
||||||
|
'jxl',
|
||||||
|
'k25',
|
||||||
|
'kdc',
|
||||||
|
'mrw',
|
||||||
|
'orf',
|
||||||
|
'ori',
|
||||||
|
'pef',
|
||||||
|
'raf',
|
||||||
|
'raw',
|
||||||
|
'rwl',
|
||||||
|
'sr2',
|
||||||
|
'srf',
|
||||||
|
'srw',
|
||||||
|
'orf',
|
||||||
|
'ori',
|
||||||
|
'x3f',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ACCEPTED_FILE_EXTENSIONS = [
|
||||||
|
...videos,
|
||||||
|
...jpeg,
|
||||||
|
...png,
|
||||||
|
...heic,
|
||||||
|
...gif,
|
||||||
|
...tiff,
|
||||||
|
...webp,
|
||||||
|
...dng,
|
||||||
|
...other,
|
||||||
|
];
|
6
cli/src/cores/dto/crawl-options-dto.ts
Normal file
6
cli/src/cores/dto/crawl-options-dto.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export class CrawlOptionsDto {
|
||||||
|
pathsToCrawl!: string[];
|
||||||
|
recursive = false;
|
||||||
|
includeHidden = false;
|
||||||
|
excludePatterns!: string[];
|
||||||
|
}
|
8
cli/src/cores/dto/upload-options-dto.ts
Normal file
8
cli/src/cores/dto/upload-options-dto.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export class UploadOptionsDto {
|
||||||
|
recursive = false;
|
||||||
|
excludePatterns!: string[];
|
||||||
|
dryRun = false;
|
||||||
|
skipHash = false;
|
||||||
|
delete = false;
|
||||||
|
import = false;
|
||||||
|
}
|
11
cli/src/cores/errors/login-error.ts
Normal file
11
cli/src/cores/errors/login-error.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
export class LoginError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
|
||||||
|
// assign the error class name in your custom error (as a shortcut)
|
||||||
|
this.name = this.constructor.name;
|
||||||
|
|
||||||
|
// capturing the stack trace keeps the reference to your error class
|
||||||
|
Error.captureStackTrace(this, this.constructor);
|
||||||
|
}
|
||||||
|
}
|
2
cli/src/cores/index.ts
Normal file
2
cli/src/cores/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './constants';
|
||||||
|
export * from './models';
|
71
cli/src/cores/models/crawled-asset.ts
Normal file
71
cli/src/cores/models/crawled-asset.ts
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as mime from 'mime-types';
|
||||||
|
import { basename } from 'node:path';
|
||||||
|
import * as path from 'path';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import { AssetTypeEnum } from 'src/api/open-api';
|
||||||
|
|
||||||
|
export class CrawledAsset {
|
||||||
|
public path: string;
|
||||||
|
|
||||||
|
public assetType?: AssetTypeEnum;
|
||||||
|
public assetData?: fs.ReadStream;
|
||||||
|
public deviceAssetId?: string;
|
||||||
|
public fileCreatedAt?: string;
|
||||||
|
public fileModifiedAt?: string;
|
||||||
|
public fileExtension?: string;
|
||||||
|
public sidecarData?: Buffer;
|
||||||
|
public sidecarPath?: string;
|
||||||
|
public fileSize!: number;
|
||||||
|
public skipped = false;
|
||||||
|
|
||||||
|
constructor(path: string) {
|
||||||
|
this.path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
async readData() {
|
||||||
|
this.assetData = fs.createReadStream(this.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
async process() {
|
||||||
|
const stats = await fs.promises.stat(this.path);
|
||||||
|
this.deviceAssetId = `${basename(this.path)}-${stats.size}`.replace(/\s+/g, '');
|
||||||
|
|
||||||
|
// TODO: Determine file type from extension only
|
||||||
|
const mimeType = mime.lookup(this.path);
|
||||||
|
if (!mimeType) {
|
||||||
|
throw Error('Cannot determine mime type of asset: ' + this.path);
|
||||||
|
}
|
||||||
|
this.assetType = mimeType.split('/')[0].toUpperCase() as AssetTypeEnum;
|
||||||
|
this.fileCreatedAt = stats.ctime.toISOString();
|
||||||
|
this.fileModifiedAt = stats.mtime.toISOString();
|
||||||
|
this.fileExtension = path.extname(this.path);
|
||||||
|
this.fileSize = stats.size;
|
||||||
|
|
||||||
|
// TODO: doesn't xmp replace the file extension? Will need investigation
|
||||||
|
const sideCarPath = `${this.path}.xmp`;
|
||||||
|
try {
|
||||||
|
fs.accessSync(sideCarPath, fs.constants.R_OK);
|
||||||
|
this.sidecarData = await fs.promises.readFile(sideCarPath);
|
||||||
|
this.sidecarPath = sideCarPath;
|
||||||
|
} catch (error) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(): Promise<void> {
|
||||||
|
return fs.promises.unlink(this.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async hash(): Promise<string> {
|
||||||
|
const sha1 = (filePath: string) => {
|
||||||
|
const hash = crypto.createHash('sha1');
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
const rs = fs.createReadStream(filePath);
|
||||||
|
rs.on('error', reject);
|
||||||
|
rs.on('data', (chunk) => hash.update(chunk));
|
||||||
|
rs.on('end', () => resolve(hash.digest('hex')));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return await sha1(this.path);
|
||||||
|
}
|
||||||
|
}
|
1
cli/src/cores/models/index.ts
Normal file
1
cli/src/cores/models/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './crawled-asset';
|
61
cli/src/index.ts
Normal file
61
cli/src/index.ts
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import { program, Option } from 'commander';
|
||||||
|
import Upload from './commands/upload';
|
||||||
|
import ServerInfo from './commands/server-info';
|
||||||
|
import LoginKey from './commands/login/key';
|
||||||
|
|
||||||
|
program.name('immich').description('Immich command line interface');
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('upload')
|
||||||
|
.description('Upload assets')
|
||||||
|
.usage('[options] [paths...]')
|
||||||
|
.addOption(new Option('-r, --recursive', 'Recursive').env('IMMICH_RECURSIVE').default(false))
|
||||||
|
.addOption(new Option('-i, --ignore [paths...]', 'Paths to ignore').env('IMMICH_IGNORE_PATHS'))
|
||||||
|
.addOption(new Option('-h, --skip-hash', "Don't hash files before upload").env('IMMICH_SKIP_HASH').default(false))
|
||||||
|
.addOption(
|
||||||
|
new Option('-n, --dry-run', "Don't perform any actions, just show what will be done")
|
||||||
|
.env('IMMICH_DRY_RUN')
|
||||||
|
.default(false),
|
||||||
|
)
|
||||||
|
.addOption(new Option('--delete', 'Delete local assets after upload').env('IMMICH_DELETE_ASSETS'))
|
||||||
|
.argument('[paths...]', 'One or more paths to assets to be uploaded')
|
||||||
|
.action((paths, options) => {
|
||||||
|
options.excludePatterns = options.ignore;
|
||||||
|
new Upload().run(paths, options);
|
||||||
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('import')
|
||||||
|
.description('Import existing assets')
|
||||||
|
.usage('[options] [paths...]')
|
||||||
|
.addOption(new Option('-r, --recursive', 'Recursive').env('IMMICH_RECURSIVE').default(false))
|
||||||
|
.addOption(
|
||||||
|
new Option('-n, --dry-run', "Don't perform any actions, just show what will be done")
|
||||||
|
.env('IMMICH_DRY_RUN')
|
||||||
|
.default(false),
|
||||||
|
)
|
||||||
|
.addOption(new Option('-i, --ignore [paths...]', 'Paths to ignore').env('IMMICH_IGNORE_PATHS').default(false))
|
||||||
|
.argument('[paths...]', 'One or more paths to assets to be uploaded')
|
||||||
|
.action((paths, options) => {
|
||||||
|
options.import = true;
|
||||||
|
new Upload().run(paths, options);
|
||||||
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('server-info')
|
||||||
|
.description('Display server information')
|
||||||
|
|
||||||
|
.action(() => {
|
||||||
|
new ServerInfo().run();
|
||||||
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('login-key')
|
||||||
|
.description('Login using an API key')
|
||||||
|
.argument('[instanceUrl]')
|
||||||
|
.argument('[apiKey]')
|
||||||
|
.action((paths, options) => {
|
||||||
|
new LoginKey().run(paths, options);
|
||||||
|
});
|
||||||
|
|
||||||
|
program.parse(process.argv);
|
235
cli/src/services/crawl.service.spec.ts
Normal file
235
cli/src/services/crawl.service.spec.ts
Normal file
|
@ -0,0 +1,235 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
import { CrawlService } from './crawl.service';
|
||||||
|
import mockfs from 'mock-fs';
|
||||||
|
import { toIncludeSameMembers } from 'jest-extended';
|
||||||
|
import { CrawlOptionsDto } from '../cores/dto/crawl-options-dto';
|
||||||
|
|
||||||
|
const matchers = require('jest-extended');
|
||||||
|
expect.extend(matchers);
|
||||||
|
|
||||||
|
const crawlService = new CrawlService();
|
||||||
|
|
||||||
|
describe('CrawlService', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
// Write a dummy output before mock-fs to prevent some annoying errors
|
||||||
|
console.log();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should crawl a single directory', async () => {
|
||||||
|
mockfs({
|
||||||
|
'/photos/image.jpg': '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = new CrawlOptionsDto();
|
||||||
|
options.pathsToCrawl = ['/photos/'];
|
||||||
|
const paths: string[] = await crawlService.crawl(options);
|
||||||
|
expect(paths).toIncludeSameMembers(['/photos/image.jpg']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should crawl a single file', async () => {
|
||||||
|
mockfs({
|
||||||
|
'/photos/image.jpg': '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = new CrawlOptionsDto();
|
||||||
|
options.pathsToCrawl = ['/photos/image.jpg'];
|
||||||
|
const paths: string[] = await crawlService.crawl(options);
|
||||||
|
expect(paths).toIncludeSameMembers(['/photos/image.jpg']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should crawl a file and a directory', async () => {
|
||||||
|
mockfs({
|
||||||
|
'/photos/image.jpg': '',
|
||||||
|
'/images/photo.jpg': '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = new CrawlOptionsDto();
|
||||||
|
options.pathsToCrawl = ['/photos/image.jpg', '/images/'];
|
||||||
|
const paths: string[] = await crawlService.crawl(options);
|
||||||
|
expect(paths).toIncludeSameMembers(['/photos/image.jpg', '/images/photo.jpg']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exclude by file extension', async () => {
|
||||||
|
mockfs({
|
||||||
|
'/photos/image.jpg': '',
|
||||||
|
'/photos/image.tif': '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = new CrawlOptionsDto();
|
||||||
|
options.pathsToCrawl = ['/photos/'];
|
||||||
|
options.excludePatterns = ['**/*.tif'];
|
||||||
|
const paths: string[] = await crawlService.crawl(options);
|
||||||
|
expect(paths).toIncludeSameMembers(['/photos/image.jpg']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exclude by file extension without case sensitivity', async () => {
|
||||||
|
mockfs({
|
||||||
|
'/photos/image.jpg': '',
|
||||||
|
'/photos/image.tif': '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = new CrawlOptionsDto();
|
||||||
|
options.pathsToCrawl = ['/photos/'];
|
||||||
|
options.excludePatterns = ['**/*.TIF'];
|
||||||
|
const paths: string[] = await crawlService.crawl(options);
|
||||||
|
expect(paths).toIncludeSameMembers(['/photos/image.jpg']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exclude by folder', async () => {
|
||||||
|
mockfs({
|
||||||
|
'/photos/image.jpg': '',
|
||||||
|
'/photos/raw/image.jpg': '',
|
||||||
|
'/photos/raw2/image.jpg': '',
|
||||||
|
'/photos/folder/raw/image.jpg': '',
|
||||||
|
'/photos/crawl/image.jpg': '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = new CrawlOptionsDto();
|
||||||
|
options.pathsToCrawl = ['/photos/'];
|
||||||
|
options.excludePatterns = ['**/raw/**'];
|
||||||
|
options.recursive = true;
|
||||||
|
const paths: string[] = await crawlService.crawl(options);
|
||||||
|
expect(paths).toIncludeSameMembers(['/photos/image.jpg', '/photos/raw2/image.jpg', '/photos/crawl/image.jpg']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should crawl multiple paths', async () => {
|
||||||
|
mockfs({
|
||||||
|
'/photos/image1.jpg': '',
|
||||||
|
'/images/image2.jpg': '',
|
||||||
|
'/albums/image3.jpg': '',
|
||||||
|
});
|
||||||
|
const options = new CrawlOptionsDto();
|
||||||
|
options.pathsToCrawl = ['/photos/', '/images/', '/albums/'];
|
||||||
|
options.recursive = false;
|
||||||
|
const paths: string[] = await crawlService.crawl(options);
|
||||||
|
expect(paths).toIncludeSameMembers(['/photos/image1.jpg', '/images/image2.jpg', '/albums/image3.jpg']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should crawl a single path without trailing slash', async () => {
|
||||||
|
mockfs({
|
||||||
|
'/photos/image.jpg': '',
|
||||||
|
});
|
||||||
|
const options = new CrawlOptionsDto();
|
||||||
|
options.pathsToCrawl = ['/photos'];
|
||||||
|
const paths: string[] = await crawlService.crawl(options);
|
||||||
|
expect(paths).toIncludeSameMembers(['/photos/image.jpg']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should crawl a single path without recursion', async () => {
|
||||||
|
mockfs({
|
||||||
|
'/photos/image.jpg': '',
|
||||||
|
'/photos/subfolder/image1.jpg': '',
|
||||||
|
'/photos/subfolder/image2.jpg': '',
|
||||||
|
'/image1.jpg': '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = new CrawlOptionsDto();
|
||||||
|
options.pathsToCrawl = ['/photos/'];
|
||||||
|
const paths: string[] = await crawlService.crawl(options);
|
||||||
|
expect(paths).toIncludeSameMembers(['/photos/image.jpg']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should crawl a single path with recursion', async () => {
|
||||||
|
mockfs({
|
||||||
|
'/photos/image.jpg': '',
|
||||||
|
'/photos/subfolder/image1.jpg': '',
|
||||||
|
'/photos/subfolder/image2.jpg': '',
|
||||||
|
'/image1.jpg': '',
|
||||||
|
});
|
||||||
|
const options = new CrawlOptionsDto();
|
||||||
|
options.pathsToCrawl = ['/photos/'];
|
||||||
|
options.recursive = true;
|
||||||
|
const paths: string[] = await crawlService.crawl(options);
|
||||||
|
expect(paths).toIncludeSameMembers([
|
||||||
|
'/photos/image.jpg',
|
||||||
|
'/photos/subfolder/image1.jpg',
|
||||||
|
'/photos/subfolder/image2.jpg',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter file extensions', async () => {
|
||||||
|
mockfs({
|
||||||
|
'/photos/image.jpg': '',
|
||||||
|
'/photos/image.txt': '',
|
||||||
|
'/photos/1': '',
|
||||||
|
});
|
||||||
|
const options = new CrawlOptionsDto();
|
||||||
|
options.pathsToCrawl = ['/photos/'];
|
||||||
|
const paths: string[] = await crawlService.crawl(options);
|
||||||
|
expect(paths).toIncludeSameMembers(['/photos/image.jpg']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include photo and video extensions', async () => {
|
||||||
|
mockfs({
|
||||||
|
'/photos/image.jpg': '',
|
||||||
|
'/photos/image.jpeg': '',
|
||||||
|
'/photos/image.heic': '',
|
||||||
|
'/photos/image.heif': '',
|
||||||
|
'/photos/image.png': '',
|
||||||
|
'/photos/image.gif': '',
|
||||||
|
'/photos/image.tif': '',
|
||||||
|
'/photos/image.tiff': '',
|
||||||
|
'/photos/image.webp': '',
|
||||||
|
'/photos/image.dng': '',
|
||||||
|
'/photos/image.nef': '',
|
||||||
|
'/videos/video.mp4': '',
|
||||||
|
'/videos/video.mov': '',
|
||||||
|
'/videos/video.webm': '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = new CrawlOptionsDto();
|
||||||
|
options.pathsToCrawl = ['/photos/', '/videos/'];
|
||||||
|
const paths: string[] = await crawlService.crawl(options);
|
||||||
|
|
||||||
|
expect(paths).toIncludeSameMembers([
|
||||||
|
'/photos/image.jpg',
|
||||||
|
'/photos/image.jpeg',
|
||||||
|
'/photos/image.heic',
|
||||||
|
'/photos/image.heif',
|
||||||
|
'/photos/image.png',
|
||||||
|
'/photos/image.gif',
|
||||||
|
'/photos/image.tif',
|
||||||
|
'/photos/image.tiff',
|
||||||
|
'/photos/image.webp',
|
||||||
|
'/photos/image.dng',
|
||||||
|
'/photos/image.nef',
|
||||||
|
'/videos/video.mp4',
|
||||||
|
'/videos/video.mov',
|
||||||
|
'/videos/video.webm',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check file extensions without case sensitivity', async () => {
|
||||||
|
mockfs({
|
||||||
|
'/photos/image.jpg': '',
|
||||||
|
'/photos/image.Jpg': '',
|
||||||
|
'/photos/image.jpG': '',
|
||||||
|
'/photos/image.JPG': '',
|
||||||
|
'/photos/image.jpEg': '',
|
||||||
|
'/photos/image.TIFF': '',
|
||||||
|
'/photos/image.tif': '',
|
||||||
|
'/photos/image.dng': '',
|
||||||
|
'/photos/image.NEF': '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = new CrawlOptionsDto();
|
||||||
|
options.pathsToCrawl = ['/photos/'];
|
||||||
|
const paths: string[] = await crawlService.crawl(options);
|
||||||
|
expect(paths).toIncludeSameMembers([
|
||||||
|
'/photos/image.jpg',
|
||||||
|
'/photos/image.Jpg',
|
||||||
|
'/photos/image.jpG',
|
||||||
|
'/photos/image.JPG',
|
||||||
|
'/photos/image.jpEg',
|
||||||
|
'/photos/image.TIFF',
|
||||||
|
'/photos/image.tif',
|
||||||
|
'/photos/image.dng',
|
||||||
|
'/photos/image.NEF',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mockfs.restore();
|
||||||
|
});
|
||||||
|
});
|
47
cli/src/services/crawl.service.ts
Normal file
47
cli/src/services/crawl.service.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import { CrawlOptionsDto } from 'src/cores/dto/crawl-options-dto';
|
||||||
|
import { ACCEPTED_FILE_EXTENSIONS } from '../cores';
|
||||||
|
import { glob } from 'glob';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
export class CrawlService {
|
||||||
|
public async crawl(crawlOptions: CrawlOptionsDto): Promise<string[]> {
|
||||||
|
const pathsToCrawl: string[] = crawlOptions.pathsToCrawl;
|
||||||
|
|
||||||
|
const directories: string[] = [];
|
||||||
|
const crawledFiles: string[] = [];
|
||||||
|
|
||||||
|
for await (const currentPath of pathsToCrawl) {
|
||||||
|
const stats = await fs.promises.stat(currentPath);
|
||||||
|
if (stats.isFile() || stats.isSymbolicLink()) {
|
||||||
|
crawledFiles.push(currentPath);
|
||||||
|
} else {
|
||||||
|
directories.push(currentPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let searchPattern: string;
|
||||||
|
if (directories.length === 1) {
|
||||||
|
searchPattern = directories[0];
|
||||||
|
} else if (directories.length === 0) {
|
||||||
|
return crawledFiles;
|
||||||
|
} else {
|
||||||
|
searchPattern = '{' + directories.join(',') + '}';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (crawlOptions.recursive) {
|
||||||
|
searchPattern = searchPattern + '/**/';
|
||||||
|
}
|
||||||
|
|
||||||
|
searchPattern = `${searchPattern}/*.{${ACCEPTED_FILE_EXTENSIONS.join(',')}}`;
|
||||||
|
|
||||||
|
const globbedFiles = await glob(searchPattern, {
|
||||||
|
nocase: true,
|
||||||
|
nodir: true,
|
||||||
|
ignore: crawlOptions.excludePatterns,
|
||||||
|
});
|
||||||
|
|
||||||
|
const returnedFiles = crawledFiles.concat(globbedFiles);
|
||||||
|
returnedFiles.sort();
|
||||||
|
return returnedFiles;
|
||||||
|
}
|
||||||
|
}
|
2
cli/src/services/index.ts
Normal file
2
cli/src/services/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './upload.service';
|
||||||
|
export * from './crawl.service';
|
95
cli/src/services/session.service.spec.ts
Normal file
95
cli/src/services/session.service.spec.ts
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
import { SessionService } from './session.service';
|
||||||
|
import mockfs from 'mock-fs';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import yaml from 'yaml';
|
||||||
|
import { LoginError } from '../cores/errors/login-error';
|
||||||
|
|
||||||
|
const mockPingServer = jest.fn(() => Promise.resolve({ data: { res: 'pong' } }));
|
||||||
|
const mockUserInfo = jest.fn(() => Promise.resolve({ data: { email: 'admin@example.com' } }));
|
||||||
|
|
||||||
|
jest.mock('../api/open-api', () => {
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
...jest.requireActual('../api/open-api'),
|
||||||
|
UserApi: jest.fn().mockImplementation(() => {
|
||||||
|
return { getMyUserInfo: mockUserInfo };
|
||||||
|
}),
|
||||||
|
ServerInfoApi: jest.fn().mockImplementation(() => {
|
||||||
|
return { pingServer: mockPingServer };
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('SessionService', () => {
|
||||||
|
let sessionService: SessionService;
|
||||||
|
beforeAll(() => {
|
||||||
|
// Write a dummy output before mock-fs to prevent some annoying errors
|
||||||
|
console.log();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const configDir = '/config';
|
||||||
|
sessionService = new SessionService(configDir);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should connect to immich', async () => {
|
||||||
|
mockfs({
|
||||||
|
'/config/auth.yml': 'apiKey: pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg\ninstanceUrl: https://test/api',
|
||||||
|
});
|
||||||
|
await sessionService.connect();
|
||||||
|
expect(mockPingServer).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should error if no auth file exists', async () => {
|
||||||
|
mockfs();
|
||||||
|
await sessionService.connect().catch((error) => {
|
||||||
|
expect(error.message).toEqual('No auth file exist. Please login first');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should error if auth file is missing instance URl', async () => {
|
||||||
|
mockfs({
|
||||||
|
'/config/auth.yml': 'foo: pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg\napiKey: https://test/api',
|
||||||
|
});
|
||||||
|
await sessionService.connect().catch((error) => {
|
||||||
|
expect(error).toBeInstanceOf(LoginError);
|
||||||
|
expect(error.message).toEqual('Instance URL missing in auth config file /config/auth.yml');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should error if auth file is missing api key', async () => {
|
||||||
|
mockfs({
|
||||||
|
'/config/auth.yml': 'instanceUrl: pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg\nbar: https://test/api',
|
||||||
|
});
|
||||||
|
await sessionService.connect().catch((error) => {
|
||||||
|
expect(error).toBeInstanceOf(LoginError);
|
||||||
|
expect(error.message).toEqual('API key missing in auth config file /config/auth.yml');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create auth file when logged in', async () => {
|
||||||
|
mockfs();
|
||||||
|
|
||||||
|
await sessionService.keyLogin('https://test/api', 'pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg');
|
||||||
|
|
||||||
|
const data: string = await fs.promises.readFile('/config/auth.yml', 'utf8');
|
||||||
|
const authConfig = yaml.parse(data);
|
||||||
|
expect(authConfig.instanceUrl).toBe('https://test/api');
|
||||||
|
expect(authConfig.apiKey).toBe('pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete auth file when logging out', async () => {
|
||||||
|
mockfs({
|
||||||
|
'/config/auth.yml': 'apiKey: pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg\ninstanceUrl: https://test/api',
|
||||||
|
});
|
||||||
|
await sessionService.logout();
|
||||||
|
|
||||||
|
await fs.promises.access('/auth.yml', fs.constants.F_OK).catch((error) => {
|
||||||
|
expect(error.message).toContain('ENOENT');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mockfs.restore();
|
||||||
|
});
|
||||||
|
});
|
81
cli/src/services/session.service.ts
Normal file
81
cli/src/services/session.service.ts
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import yaml from 'yaml';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { ImmichApi } from '../api/client';
|
||||||
|
import { LoginError } from '../cores/errors/login-error';
|
||||||
|
|
||||||
|
export class SessionService {
|
||||||
|
readonly configDir: string;
|
||||||
|
readonly authPath!: string;
|
||||||
|
private api!: ImmichApi;
|
||||||
|
|
||||||
|
constructor(configDir: string) {
|
||||||
|
this.configDir = configDir;
|
||||||
|
this.authPath = path.join(this.configDir, 'auth.yml');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async connect(): Promise<ImmichApi> {
|
||||||
|
await fs.promises.access(this.authPath, fs.constants.F_OK).catch((error) => {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
throw new LoginError('No auth file exist. Please login first');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const data: string = await fs.promises.readFile(this.authPath, 'utf8');
|
||||||
|
const parsedConfig = yaml.parse(data);
|
||||||
|
const instanceUrl: string = parsedConfig.instanceUrl;
|
||||||
|
const apiKey: string = parsedConfig.apiKey;
|
||||||
|
|
||||||
|
if (!instanceUrl) {
|
||||||
|
throw new LoginError('Instance URL missing in auth config file ' + this.authPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new LoginError('API key missing in auth config file ' + this.authPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.api = new ImmichApi(instanceUrl, apiKey);
|
||||||
|
|
||||||
|
await this.ping();
|
||||||
|
|
||||||
|
return this.api;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async keyLogin(instanceUrl: string, apiKey: string): Promise<ImmichApi> {
|
||||||
|
this.api = new ImmichApi(instanceUrl, apiKey);
|
||||||
|
|
||||||
|
// Check if server and api key are valid
|
||||||
|
const { data: userInfo } = await this.api.userApi.getMyUserInfo().catch((error) => {
|
||||||
|
throw new LoginError(`Failed to connect to the server: ${error.message}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Logged in as ${userInfo.email}`);
|
||||||
|
|
||||||
|
if (!fs.existsSync(this.configDir)) {
|
||||||
|
// Create config folder if it doesn't exist
|
||||||
|
fs.mkdirSync(this.configDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(this.authPath, yaml.stringify({ instanceUrl, apiKey }));
|
||||||
|
|
||||||
|
console.log('Wrote auth info to ' + this.authPath);
|
||||||
|
return this.api;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async logout(): Promise<void> {
|
||||||
|
if (fs.existsSync(this.authPath)) {
|
||||||
|
fs.unlinkSync(this.authPath);
|
||||||
|
console.log('Removed auth file ' + this.authPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ping(): Promise<void> {
|
||||||
|
const { data: pingResponse } = await this.api.serverInfoApi.pingServer().catch((error) => {
|
||||||
|
throw new Error(`Failed to connect to the server: ${error.message}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (pingResponse.res !== 'pong') {
|
||||||
|
throw new Error('Unexpected ping reply');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
cli/src/services/upload.service.spec.ts
Normal file
36
cli/src/services/upload.service.spec.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import { UploadService } from './upload.service';
|
||||||
|
import mockfs from 'mock-fs';
|
||||||
|
import axios from 'axios';
|
||||||
|
import mockAxios from 'jest-mock-axios';
|
||||||
|
import FormData from 'form-data';
|
||||||
|
import { ApiConfiguration } from '../cores/api-configuration';
|
||||||
|
|
||||||
|
describe('UploadService', () => {
|
||||||
|
let uploadService: UploadService;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
// Write a dummy output before mock-fs to prevent some annoying errors
|
||||||
|
console.log();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const apiConfiguration = new ApiConfiguration('https://example.com/api', 'key');
|
||||||
|
|
||||||
|
uploadService = new UploadService(apiConfiguration);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should upload a single file', async () => {
|
||||||
|
const data = new FormData();
|
||||||
|
data.append('assetType', 'image');
|
||||||
|
|
||||||
|
uploadService.upload(data);
|
||||||
|
|
||||||
|
mockAxios.mockResponse();
|
||||||
|
expect(axios).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mockfs.restore();
|
||||||
|
mockAxios.reset();
|
||||||
|
});
|
||||||
|
});
|
65
cli/src/services/upload.service.ts
Normal file
65
cli/src/services/upload.service.ts
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import axios, { AxiosRequestConfig } from 'axios';
|
||||||
|
import FormData from 'form-data';
|
||||||
|
import { ApiConfiguration } from '../cores/api-configuration';
|
||||||
|
|
||||||
|
export class UploadService {
|
||||||
|
private readonly uploadConfig: AxiosRequestConfig<any>;
|
||||||
|
private readonly checkAssetExistenceConfig: AxiosRequestConfig<any>;
|
||||||
|
private readonly importConfig: AxiosRequestConfig<any>;
|
||||||
|
|
||||||
|
constructor(apiConfiguration: ApiConfiguration) {
|
||||||
|
this.uploadConfig = {
|
||||||
|
method: 'post',
|
||||||
|
maxRedirects: 0,
|
||||||
|
url: `${apiConfiguration.instanceUrl}/asset/upload`,
|
||||||
|
headers: {
|
||||||
|
'x-api-key': apiConfiguration.apiKey,
|
||||||
|
},
|
||||||
|
maxContentLength: Number.POSITIVE_INFINITY,
|
||||||
|
maxBodyLength: Number.POSITIVE_INFINITY,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.importConfig = {
|
||||||
|
method: 'post',
|
||||||
|
maxRedirects: 0,
|
||||||
|
url: `${apiConfiguration.instanceUrl}/asset/import`,
|
||||||
|
headers: {
|
||||||
|
'x-api-key': apiConfiguration.apiKey,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
maxContentLength: Number.POSITIVE_INFINITY,
|
||||||
|
maxBodyLength: Number.POSITIVE_INFINITY,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.checkAssetExistenceConfig = {
|
||||||
|
method: 'post',
|
||||||
|
maxRedirects: 0,
|
||||||
|
url: `${apiConfiguration.instanceUrl}/asset/bulk-upload-check`,
|
||||||
|
headers: {
|
||||||
|
'x-api-key': apiConfiguration.apiKey,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public checkIfAssetAlreadyExists(path: string, checksum: string): Promise<any> {
|
||||||
|
this.checkAssetExistenceConfig.data = JSON.stringify({ assets: [{ id: path, checksum: checksum }] });
|
||||||
|
|
||||||
|
// TODO: retry on 500 errors?
|
||||||
|
return axios(this.checkAssetExistenceConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
public upload(data: FormData): Promise<any> {
|
||||||
|
this.uploadConfig.data = data;
|
||||||
|
|
||||||
|
// TODO: retry on 500 errors?
|
||||||
|
return axios(this.uploadConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
public import(data: any): Promise<any> {
|
||||||
|
this.importConfig.data = data;
|
||||||
|
|
||||||
|
// TODO: retry on 500 errors?
|
||||||
|
return axios(this.importConfig);
|
||||||
|
}
|
||||||
|
}
|
7
cli/test/tsconfig.json
Normal file
7
cli/test/tsconfig.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"extends": "../tsconfig",
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"references": [{ "path": ".." }]
|
||||||
|
}
|
3
cli/testSetup.js
Normal file
3
cli/testSetup.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// add all jest-extended matchers
|
||||||
|
import * as matchers from 'jest-extended';
|
||||||
|
expect.extend(matchers);
|
25
cli/tsconfig.json
Normal file
25
cli/tsconfig.json
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"strict": true,
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"target": "es2017",
|
||||||
|
"moduleResolution": "node16",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"incremental": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"baseUrl": "./",
|
||||||
|
"paths": {
|
||||||
|
"@test": ["test"],
|
||||||
|
"@test/*": ["test/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": ["dist", "node_modules", "upload"]
|
||||||
|
}
|
|
@ -23,11 +23,23 @@ function web {
|
||||||
npx --yes @openapitools/openapi-generator-cli generate -g typescript-axios -i ./immich-openapi-specs.json -o ../web/src/api/open-api -t ./openapi-generator/templates/web --additional-properties=useSingleRequestParameter=true
|
npx --yes @openapitools/openapi-generator-cli generate -g typescript-axios -i ./immich-openapi-specs.json -o ../web/src/api/open-api -t ./openapi-generator/templates/web --additional-properties=useSingleRequestParameter=true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cli {
|
||||||
|
rm -rf ../cli/src/api/open-api
|
||||||
|
cd ./openapi-generator/templates/cli
|
||||||
|
wget -O apiInner.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/v6.6.0/modules/openapi-generator/src/main/resources/typescript-axios/apiInner.mustache
|
||||||
|
patch -u apiInner.mustache < apiInner.mustache.patch
|
||||||
|
cd ../../..
|
||||||
|
npx --yes @openapitools/openapi-generator-cli generate -g typescript-axios -i ./immich-openapi-specs.json -o ../cli/src/api/open-api -t ./openapi-generator/templates/cli --additional-properties=useSingleRequestParameter=true
|
||||||
|
}
|
||||||
|
|
||||||
if [[ $1 == 'mobile' ]]; then
|
if [[ $1 == 'mobile' ]]; then
|
||||||
mobile
|
mobile
|
||||||
elif [[ $1 == 'web' ]]; then
|
elif [[ $1 == 'web' ]]; then
|
||||||
web
|
web
|
||||||
|
elif [[ $1 == 'cli' ]]; then
|
||||||
|
cli
|
||||||
else
|
else
|
||||||
mobile
|
mobile
|
||||||
web
|
web
|
||||||
|
cli
|
||||||
fi
|
fi
|
||||||
|
|
391
server/openapi-generator/templates/cli/apiInner.mustache
Normal file
391
server/openapi-generator/templates/cli/apiInner.mustache
Normal file
|
@ -0,0 +1,391 @@
|
||||||
|
{{#withSeparateModelsAndApi}}
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
{{>licenseInfo}}
|
||||||
|
|
||||||
|
import type { Configuration } from '{{apiRelativeToRoot}}configuration';
|
||||||
|
import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||||
|
import globalAxios from 'axios';
|
||||||
|
{{#withNodeImports}}
|
||||||
|
// URLSearchParams not necessarily used
|
||||||
|
// @ts-ignore
|
||||||
|
import { URL, URLSearchParams } from 'url';
|
||||||
|
{{#multipartFormData}}
|
||||||
|
import FormData from 'form-data'
|
||||||
|
{{/multipartFormData}}
|
||||||
|
{{/withNodeImports}}
|
||||||
|
// Some imports not used depending on template conditions
|
||||||
|
// @ts-ignore
|
||||||
|
import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '{{apiRelativeToRoot}}common';
|
||||||
|
// @ts-ignore
|
||||||
|
import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError } from '{{apiRelativeToRoot}}base';
|
||||||
|
{{#imports}}
|
||||||
|
// @ts-ignore
|
||||||
|
import { {{classname}} } from '{{apiRelativeToRoot}}{{tsModelPackage}}';
|
||||||
|
{{/imports}}
|
||||||
|
{{/withSeparateModelsAndApi}}
|
||||||
|
{{^withSeparateModelsAndApi}}
|
||||||
|
{{/withSeparateModelsAndApi}}
|
||||||
|
{{#operations}}
|
||||||
|
/**
|
||||||
|
* {{classname}} - axios parameter creator{{#description}}
|
||||||
|
* {{&description}}{{/description}}
|
||||||
|
* @export
|
||||||
|
*/
|
||||||
|
export const {{classname}}AxiosParamCreator = function (configuration?: Configuration) {
|
||||||
|
return {
|
||||||
|
{{#operation}}
|
||||||
|
/**
|
||||||
|
* {{¬es}}
|
||||||
|
{{#summary}}
|
||||||
|
* @summary {{&summary}}
|
||||||
|
{{/summary}}
|
||||||
|
{{#allParams}}
|
||||||
|
* @param {{=<% %>=}}{<%&dataType%>}<%={{ }}=%> {{^required}}[{{/required}}{{paramName}}{{^required}}]{{/required}} {{description}}
|
||||||
|
{{/allParams}}
|
||||||
|
* @param {*} [options] Override http request option.{{#isDeprecated}}
|
||||||
|
* @deprecated{{/isDeprecated}}
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
{{nickname}}: async ({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
|
{{#allParams}}
|
||||||
|
{{#required}}
|
||||||
|
// verify required parameter '{{paramName}}' is not null or undefined
|
||||||
|
assertParamExists('{{nickname}}', '{{paramName}}', {{paramName}})
|
||||||
|
{{/required}}
|
||||||
|
{{/allParams}}
|
||||||
|
const localVarPath = `{{{path}}}`{{#pathParams}}
|
||||||
|
.replace(`{${"{{baseName}}"}}`, encodeURIComponent(String({{paramName}}))){{/pathParams}};
|
||||||
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
|
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||||
|
let baseOptions;
|
||||||
|
if (configuration) {
|
||||||
|
baseOptions = configuration.baseOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localVarRequestOptions = { method: '{{httpMethod}}', ...baseOptions, ...options};
|
||||||
|
const localVarHeaderParameter = {} as any;
|
||||||
|
const localVarQueryParameter = {} as any;{{#vendorExtensions}}{{#hasFormParams}}
|
||||||
|
const localVarFormParams = new {{^multipartFormData}}URLSearchParams(){{/multipartFormData}}{{#multipartFormData}}((configuration && configuration.formDataCtor) || FormData)(){{/multipartFormData}};{{/hasFormParams}}{{/vendorExtensions}}
|
||||||
|
|
||||||
|
{{#authMethods}}
|
||||||
|
// authentication {{name}} required
|
||||||
|
{{#isApiKey}}
|
||||||
|
{{#isKeyInHeader}}
|
||||||
|
await setApiKeyToObject(localVarHeaderParameter, "{{keyParamName}}", configuration)
|
||||||
|
{{/isKeyInHeader}}
|
||||||
|
{{#isKeyInQuery}}
|
||||||
|
await setApiKeyToObject(localVarQueryParameter, "{{keyParamName}}", configuration)
|
||||||
|
{{/isKeyInQuery}}
|
||||||
|
{{/isApiKey}}
|
||||||
|
{{#isBasicBasic}}
|
||||||
|
// http basic authentication required
|
||||||
|
setBasicAuthToObject(localVarRequestOptions, configuration)
|
||||||
|
{{/isBasicBasic}}
|
||||||
|
{{#isBasicBearer}}
|
||||||
|
// http bearer authentication required
|
||||||
|
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||||
|
{{/isBasicBearer}}
|
||||||
|
{{#isOAuth}}
|
||||||
|
// oauth required
|
||||||
|
await setOAuthToObject(localVarHeaderParameter, "{{name}}", [{{#scopes}}"{{{scope}}}"{{^-last}}, {{/-last}}{{/scopes}}], configuration)
|
||||||
|
{{/isOAuth}}
|
||||||
|
|
||||||
|
{{/authMethods}}
|
||||||
|
{{#queryParams}}
|
||||||
|
{{#isArray}}
|
||||||
|
if ({{paramName}}) {
|
||||||
|
{{#isCollectionFormatMulti}}
|
||||||
|
{{#uniqueItems}}
|
||||||
|
localVarQueryParameter['{{baseName}}'] = Array.from({{paramName}});
|
||||||
|
{{/uniqueItems}}
|
||||||
|
{{^uniqueItems}}
|
||||||
|
localVarQueryParameter['{{baseName}}'] = {{paramName}};
|
||||||
|
{{/uniqueItems}}
|
||||||
|
{{/isCollectionFormatMulti}}
|
||||||
|
{{^isCollectionFormatMulti}}
|
||||||
|
{{#uniqueItems}}
|
||||||
|
localVarQueryParameter['{{baseName}}'] = Array.from({{paramName}}).join(COLLECTION_FORMATS.{{collectionFormat}});
|
||||||
|
{{/uniqueItems}}
|
||||||
|
{{^uniqueItems}}
|
||||||
|
localVarQueryParameter['{{baseName}}'] = {{paramName}}.join(COLLECTION_FORMATS.{{collectionFormat}});
|
||||||
|
{{/uniqueItems}}
|
||||||
|
{{/isCollectionFormatMulti}}
|
||||||
|
}
|
||||||
|
{{/isArray}}
|
||||||
|
{{^isArray}}
|
||||||
|
if ({{paramName}} !== undefined) {
|
||||||
|
{{#isDateTime}}
|
||||||
|
localVarQueryParameter['{{baseName}}'] = ({{paramName}} as any instanceof Date) ?
|
||||||
|
({{paramName}} as any).toISOString() :
|
||||||
|
{{paramName}};
|
||||||
|
{{/isDateTime}}
|
||||||
|
{{^isDateTime}}
|
||||||
|
{{#isDate}}
|
||||||
|
localVarQueryParameter['{{baseName}}'] = ({{paramName}} as any instanceof Date) ?
|
||||||
|
({{paramName}} as any).toISOString().substr(0,10) :
|
||||||
|
{{paramName}};
|
||||||
|
{{/isDate}}
|
||||||
|
{{^isDate}}
|
||||||
|
localVarQueryParameter['{{baseName}}'] = {{paramName}};
|
||||||
|
{{/isDate}}
|
||||||
|
{{/isDateTime}}
|
||||||
|
}
|
||||||
|
{{/isArray}}
|
||||||
|
|
||||||
|
{{/queryParams}}
|
||||||
|
{{#headerParams}}
|
||||||
|
{{#isArray}}
|
||||||
|
if ({{paramName}}) {
|
||||||
|
{{#uniqueItems}}
|
||||||
|
let mapped = Array.from({{paramName}}).map(value => (<any>"{{{dataType}}}" !== "Set<string>") ? JSON.stringify(value) : (value || ""));
|
||||||
|
{{/uniqueItems}}
|
||||||
|
{{^uniqueItems}}
|
||||||
|
let mapped = {{paramName}}.map(value => (<any>"{{{dataType}}}" !== "Array<string>") ? JSON.stringify(value) : (value || ""));
|
||||||
|
{{/uniqueItems}}
|
||||||
|
localVarHeaderParameter['{{baseName}}'] = mapped.join(COLLECTION_FORMATS["{{collectionFormat}}"]);
|
||||||
|
}
|
||||||
|
{{/isArray}}
|
||||||
|
{{^isArray}}
|
||||||
|
{{! `val == null` covers for both `null` and `undefined`}}
|
||||||
|
if ({{paramName}} != null) {
|
||||||
|
{{#isString}}
|
||||||
|
localVarHeaderParameter['{{baseName}}'] = String({{paramName}});
|
||||||
|
{{/isString}}
|
||||||
|
{{^isString}}
|
||||||
|
{{! isString is falsy also for $ref that defines a string or enum type}}
|
||||||
|
localVarHeaderParameter['{{baseName}}'] = typeof {{paramName}} === 'string'
|
||||||
|
? {{paramName}}
|
||||||
|
: JSON.stringify({{paramName}});
|
||||||
|
{{/isString}}
|
||||||
|
}
|
||||||
|
{{/isArray}}
|
||||||
|
|
||||||
|
{{/headerParams}}
|
||||||
|
{{#vendorExtensions}}
|
||||||
|
{{#formParams}}
|
||||||
|
{{#isArray}}
|
||||||
|
if ({{paramName}}) {
|
||||||
|
{{#isCollectionFormatMulti}}
|
||||||
|
{{paramName}}.forEach((element) => {
|
||||||
|
localVarFormParams.{{#multipartFormData}}append{{/multipartFormData}}{{^multipartFormData}}set{{/multipartFormData}}('{{baseName}}', element as any);
|
||||||
|
})
|
||||||
|
{{/isCollectionFormatMulti}}
|
||||||
|
{{^isCollectionFormatMulti}}
|
||||||
|
localVarFormParams.{{#multipartFormData}}append{{/multipartFormData}}{{^multipartFormData}}set{{/multipartFormData}}('{{baseName}}', {{paramName}}.join(COLLECTION_FORMATS.{{collectionFormat}}));
|
||||||
|
{{/isCollectionFormatMulti}}
|
||||||
|
}{{/isArray}}
|
||||||
|
{{^isArray}}
|
||||||
|
if ({{paramName}} !== undefined) { {{^multipartFormData}}
|
||||||
|
localVarFormParams.set('{{baseName}}', {{paramName}} as any);{{/multipartFormData}}{{#multipartFormData}}{{#isPrimitiveType}}
|
||||||
|
localVarFormParams.append('{{baseName}}', {{paramName}} as any);{{/isPrimitiveType}}{{^isPrimitiveType}}{{#isEnum}}
|
||||||
|
localVarFormParams.append('{{baseName}}', {{paramName}} as any);{{/isEnum}}{{^isEnum}}
|
||||||
|
localVarFormParams.append('{{baseName}}', new Blob([JSON.stringify({{paramName}})], { type: "application/json", }));{{/isEnum}}{{/isPrimitiveType}}{{/multipartFormData}}
|
||||||
|
}{{/isArray}}
|
||||||
|
{{/formParams}}{{/vendorExtensions}}
|
||||||
|
{{#vendorExtensions}}{{#hasFormParams}}{{^multipartFormData}}
|
||||||
|
localVarHeaderParameter['Content-Type'] = 'application/x-www-form-urlencoded';{{/multipartFormData}}{{#multipartFormData}}
|
||||||
|
localVarHeaderParameter['Content-Type'] = 'multipart/form-data';{{/multipartFormData}}
|
||||||
|
{{/hasFormParams}}{{/vendorExtensions}}
|
||||||
|
{{#bodyParam}}
|
||||||
|
{{^consumes}}
|
||||||
|
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||||
|
{{/consumes}}
|
||||||
|
{{#consumes.0}}
|
||||||
|
localVarHeaderParameter['Content-Type'] = '{{{mediaType}}}';
|
||||||
|
{{/consumes.0}}
|
||||||
|
|
||||||
|
{{/bodyParam}}
|
||||||
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions,{{#hasFormParams}}{{#multipartFormData}} ...(localVarFormParams as any).getHeaders?.(),{{/multipartFormData}}{{/hasFormParams}} ...options.headers};
|
||||||
|
{{#hasFormParams}}
|
||||||
|
localVarRequestOptions.data = localVarFormParams{{#vendorExtensions}}{{^multipartFormData}}.toString(){{/multipartFormData}}{{/vendorExtensions}};
|
||||||
|
{{/hasFormParams}}
|
||||||
|
{{#bodyParam}}
|
||||||
|
localVarRequestOptions.data = serializeDataIfNeeded({{paramName}}, localVarRequestOptions, configuration)
|
||||||
|
{{/bodyParam}}
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: toPathString(localVarUrlObj),
|
||||||
|
options: localVarRequestOptions,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{{/operation}}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {{classname}} - functional programming interface{{#description}}
|
||||||
|
* {{{.}}}{{/description}}
|
||||||
|
* @export
|
||||||
|
*/
|
||||||
|
export const {{classname}}Fp = function(configuration?: Configuration) {
|
||||||
|
const localVarAxiosParamCreator = {{classname}}AxiosParamCreator(configuration)
|
||||||
|
return {
|
||||||
|
{{#operation}}
|
||||||
|
/**
|
||||||
|
* {{¬es}}
|
||||||
|
{{#summary}}
|
||||||
|
* @summary {{&summary}}
|
||||||
|
{{/summary}}
|
||||||
|
{{#allParams}}
|
||||||
|
* @param {{=<% %>=}}{<%&dataType%>}<%={{ }}=%> {{^required}}[{{/required}}{{paramName}}{{^required}}]{{/required}} {{description}}
|
||||||
|
{{/allParams}}
|
||||||
|
* @param {*} [options] Override http request option.{{#isDeprecated}}
|
||||||
|
* @deprecated{{/isDeprecated}}
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
async {{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<{{{returnType}}}{{^returnType}}void{{/returnType}}>> {
|
||||||
|
const localVarAxiosArgs = await localVarAxiosParamCreator.{{nickname}}({{#allParams}}{{paramName}}, {{/allParams}}options);
|
||||||
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
|
},
|
||||||
|
{{/operation}}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {{classname}} - factory interface{{#description}}
|
||||||
|
* {{&description}}{{/description}}
|
||||||
|
* @export
|
||||||
|
*/
|
||||||
|
export const {{classname}}Factory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||||
|
const localVarFp = {{classname}}Fp(configuration)
|
||||||
|
return {
|
||||||
|
{{#operation}}
|
||||||
|
/**
|
||||||
|
* {{¬es}}
|
||||||
|
{{#summary}}
|
||||||
|
* @summary {{&summary}}
|
||||||
|
{{/summary}}
|
||||||
|
{{#useSingleRequestParameter}}
|
||||||
|
{{#allParams.0}}
|
||||||
|
* @param {{=<% %>=}}{<%& classname %><%& operationIdCamelCase %>Request}<%={{ }}=%> requestParameters Request parameters.
|
||||||
|
{{/allParams.0}}
|
||||||
|
{{/useSingleRequestParameter}}
|
||||||
|
{{^useSingleRequestParameter}}
|
||||||
|
{{#allParams}}
|
||||||
|
* @param {{=<% %>=}}{<%&dataType%>}<%={{ }}=%> {{^required}}[{{/required}}{{paramName}}{{^required}}]{{/required}} {{description}}
|
||||||
|
{{/allParams}}
|
||||||
|
{{/useSingleRequestParameter}}
|
||||||
|
* @param {*} [options] Override http request option.{{#isDeprecated}}
|
||||||
|
* @deprecated{{/isDeprecated}}
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
{{#useSingleRequestParameter}}
|
||||||
|
{{nickname}}({{#allParams.0}}requestParameters: {{classname}}{{operationIdCamelCase}}Request{{^hasRequiredParams}} = {}{{/hasRequiredParams}}, {{/allParams.0}}options?: AxiosRequestConfig): AxiosPromise<{{{returnType}}}{{^returnType}}void{{/returnType}}> {
|
||||||
|
return localVarFp.{{nickname}}({{#allParams.0}}{{#allParams}}requestParameters.{{paramName}}, {{/allParams}}{{/allParams.0}}options).then((request) => request(axios, basePath));
|
||||||
|
},
|
||||||
|
{{/useSingleRequestParameter}}
|
||||||
|
{{^useSingleRequestParameter}}
|
||||||
|
{{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}options?: any): AxiosPromise<{{{returnType}}}{{^returnType}}void{{/returnType}}> {
|
||||||
|
return localVarFp.{{nickname}}({{#allParams}}{{paramName}}, {{/allParams}}options).then((request) => request(axios, basePath));
|
||||||
|
},
|
||||||
|
{{/useSingleRequestParameter}}
|
||||||
|
{{/operation}}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
{{#withInterfaces}}
|
||||||
|
/**
|
||||||
|
* {{classname}} - interface{{#description}}
|
||||||
|
* {{&description}}{{/description}}
|
||||||
|
* @export
|
||||||
|
* @interface {{classname}}
|
||||||
|
*/
|
||||||
|
export interface {{classname}}Interface {
|
||||||
|
{{#operation}}
|
||||||
|
/**
|
||||||
|
* {{¬es}}
|
||||||
|
{{#summary}}
|
||||||
|
* @summary {{&summary}}
|
||||||
|
{{/summary}}
|
||||||
|
{{#allParams}}
|
||||||
|
* @param {{=<% %>=}}{<%&dataType%>}<%={{ }}=%> {{^required}}[{{/required}}{{paramName}}{{^required}}]{{/required}} {{description}}
|
||||||
|
{{/allParams}}
|
||||||
|
* @param {*} [options] Override http request option.{{#isDeprecated}}
|
||||||
|
* @deprecated{{/isDeprecated}}
|
||||||
|
* @throws {RequiredError}
|
||||||
|
* @memberof {{classname}}Interface
|
||||||
|
*/
|
||||||
|
{{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}options?: AxiosRequestConfig): AxiosPromise<{{{returnType}}}{{^returnType}}void{{/returnType}}>;
|
||||||
|
|
||||||
|
{{/operation}}
|
||||||
|
}
|
||||||
|
|
||||||
|
{{/withInterfaces}}
|
||||||
|
{{#useSingleRequestParameter}}
|
||||||
|
{{#operation}}
|
||||||
|
{{#allParams.0}}
|
||||||
|
/**
|
||||||
|
* Request parameters for {{nickname}} operation in {{classname}}.
|
||||||
|
* @export
|
||||||
|
* @interface {{classname}}{{operationIdCamelCase}}Request
|
||||||
|
*/
|
||||||
|
export interface {{classname}}{{operationIdCamelCase}}Request {
|
||||||
|
{{#allParams}}
|
||||||
|
/**
|
||||||
|
* {{description}}
|
||||||
|
* @type {{=<% %>=}}{<%&dataType%>}<%={{ }}=%>
|
||||||
|
* @memberof {{classname}}{{operationIdCamelCase}}
|
||||||
|
*/
|
||||||
|
readonly {{paramName}}{{^required}}?{{/required}}: {{{dataType}}}
|
||||||
|
{{^-last}}
|
||||||
|
|
||||||
|
{{/-last}}
|
||||||
|
{{/allParams}}
|
||||||
|
}
|
||||||
|
|
||||||
|
{{/allParams.0}}
|
||||||
|
{{/operation}}
|
||||||
|
{{/useSingleRequestParameter}}
|
||||||
|
/**
|
||||||
|
* {{classname}} - object-oriented interface{{#description}}
|
||||||
|
* {{{.}}}{{/description}}
|
||||||
|
* @export
|
||||||
|
* @class {{classname}}
|
||||||
|
* @extends {BaseAPI}
|
||||||
|
*/
|
||||||
|
{{#withInterfaces}}
|
||||||
|
export class {{classname}} extends BaseAPI implements {{classname}}Interface {
|
||||||
|
{{/withInterfaces}}
|
||||||
|
{{^withInterfaces}}
|
||||||
|
export class {{classname}} extends BaseAPI {
|
||||||
|
{{/withInterfaces}}
|
||||||
|
{{#operation}}
|
||||||
|
/**
|
||||||
|
* {{¬es}}
|
||||||
|
{{#summary}}
|
||||||
|
* @summary {{&summary}}
|
||||||
|
{{/summary}}
|
||||||
|
{{#useSingleRequestParameter}}
|
||||||
|
{{#allParams.0}}
|
||||||
|
* @param {{=<% %>=}}{<%& classname %><%& operationIdCamelCase %>Request}<%={{ }}=%> requestParameters Request parameters.
|
||||||
|
{{/allParams.0}}
|
||||||
|
{{/useSingleRequestParameter}}
|
||||||
|
{{^useSingleRequestParameter}}
|
||||||
|
{{#allParams}}
|
||||||
|
* @param {{=<% %>=}}{<%&dataType%>}<%={{ }}=%> {{^required}}[{{/required}}{{paramName}}{{^required}}]{{/required}} {{description}}
|
||||||
|
{{/allParams}}
|
||||||
|
{{/useSingleRequestParameter}}
|
||||||
|
* @param {*} [options] Override http request option.{{#isDeprecated}}
|
||||||
|
* @deprecated{{/isDeprecated}}
|
||||||
|
* @throws {RequiredError}
|
||||||
|
* @memberof {{classname}}
|
||||||
|
*/
|
||||||
|
{{#useSingleRequestParameter}}
|
||||||
|
public {{nickname}}({{#allParams.0}}requestParameters: {{classname}}{{operationIdCamelCase}}Request{{^hasRequiredParams}} = {}{{/hasRequiredParams}}, {{/allParams.0}}options?: AxiosRequestConfig) {
|
||||||
|
return {{classname}}Fp(this.configuration).{{nickname}}({{#allParams.0}}{{#allParams}}requestParameters.{{paramName}}, {{/allParams}}{{/allParams.0}}options).then((request) => request(this.axios, this.basePath));
|
||||||
|
}
|
||||||
|
{{/useSingleRequestParameter}}
|
||||||
|
{{^useSingleRequestParameter}}
|
||||||
|
public {{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}options?: AxiosRequestConfig) {
|
||||||
|
return {{classname}}Fp(this.configuration).{{nickname}}({{#allParams}}{{paramName}}, {{/allParams}}options).then((request) => request(this.axios, this.basePath));
|
||||||
|
}
|
||||||
|
{{/useSingleRequestParameter}}
|
||||||
|
{{^-last}}
|
||||||
|
|
||||||
|
{{/-last}}
|
||||||
|
{{/operation}}
|
||||||
|
}
|
||||||
|
{{/operations}}
|
390
server/openapi-generator/templates/cli/apiInner.mustache.orig
Normal file
390
server/openapi-generator/templates/cli/apiInner.mustache.orig
Normal file
|
@ -0,0 +1,390 @@
|
||||||
|
{{#withSeparateModelsAndApi}}
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
{{>licenseInfo}}
|
||||||
|
|
||||||
|
import type { Configuration } from '{{apiRelativeToRoot}}configuration';
|
||||||
|
import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||||
|
import globalAxios from 'axios';
|
||||||
|
{{#withNodeImports}}
|
||||||
|
// URLSearchParams not necessarily used
|
||||||
|
// @ts-ignore
|
||||||
|
import { URL, URLSearchParams } from 'url';
|
||||||
|
{{#multipartFormData}}
|
||||||
|
import FormData from 'form-data'
|
||||||
|
{{/multipartFormData}}
|
||||||
|
{{/withNodeImports}}
|
||||||
|
// Some imports not used depending on template conditions
|
||||||
|
// @ts-ignore
|
||||||
|
import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '{{apiRelativeToRoot}}common';
|
||||||
|
// @ts-ignore
|
||||||
|
import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError } from '{{apiRelativeToRoot}}base';
|
||||||
|
{{#imports}}
|
||||||
|
// @ts-ignore
|
||||||
|
import { {{classname}} } from '{{apiRelativeToRoot}}{{tsModelPackage}}';
|
||||||
|
{{/imports}}
|
||||||
|
{{/withSeparateModelsAndApi}}
|
||||||
|
{{^withSeparateModelsAndApi}}
|
||||||
|
{{/withSeparateModelsAndApi}}
|
||||||
|
{{#operations}}
|
||||||
|
/**
|
||||||
|
* {{classname}} - axios parameter creator{{#description}}
|
||||||
|
* {{&description}}{{/description}}
|
||||||
|
* @export
|
||||||
|
*/
|
||||||
|
export const {{classname}}AxiosParamCreator = function (configuration?: Configuration) {
|
||||||
|
return {
|
||||||
|
{{#operation}}
|
||||||
|
/**
|
||||||
|
* {{¬es}}
|
||||||
|
{{#summary}}
|
||||||
|
* @summary {{&summary}}
|
||||||
|
{{/summary}}
|
||||||
|
{{#allParams}}
|
||||||
|
* @param {{=<% %>=}}{<%&dataType%>}<%={{ }}=%> {{^required}}[{{/required}}{{paramName}}{{^required}}]{{/required}} {{description}}
|
||||||
|
{{/allParams}}
|
||||||
|
* @param {*} [options] Override http request option.{{#isDeprecated}}
|
||||||
|
* @deprecated{{/isDeprecated}}
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
{{nickname}}: async ({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
|
{{#allParams}}
|
||||||
|
{{#required}}
|
||||||
|
// verify required parameter '{{paramName}}' is not null or undefined
|
||||||
|
assertParamExists('{{nickname}}', '{{paramName}}', {{paramName}})
|
||||||
|
{{/required}}
|
||||||
|
{{/allParams}}
|
||||||
|
const localVarPath = `{{{path}}}`{{#pathParams}}
|
||||||
|
.replace(`{${"{{baseName}}"}}`, encodeURIComponent(String({{paramName}}))){{/pathParams}};
|
||||||
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
|
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||||
|
let baseOptions;
|
||||||
|
if (configuration) {
|
||||||
|
baseOptions = configuration.baseOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localVarRequestOptions = { method: '{{httpMethod}}', ...baseOptions, ...options};
|
||||||
|
const localVarHeaderParameter = {} as any;
|
||||||
|
const localVarQueryParameter = {} as any;{{#vendorExtensions}}{{#hasFormParams}}
|
||||||
|
const localVarFormParams = new {{^multipartFormData}}URLSearchParams(){{/multipartFormData}}{{#multipartFormData}}((configuration && configuration.formDataCtor) || FormData)(){{/multipartFormData}};{{/hasFormParams}}{{/vendorExtensions}}
|
||||||
|
|
||||||
|
{{#authMethods}}
|
||||||
|
// authentication {{name}} required
|
||||||
|
{{#isApiKey}}
|
||||||
|
{{#isKeyInHeader}}
|
||||||
|
await setApiKeyToObject(localVarHeaderParameter, "{{keyParamName}}", configuration)
|
||||||
|
{{/isKeyInHeader}}
|
||||||
|
{{#isKeyInQuery}}
|
||||||
|
await setApiKeyToObject(localVarQueryParameter, "{{keyParamName}}", configuration)
|
||||||
|
{{/isKeyInQuery}}
|
||||||
|
{{/isApiKey}}
|
||||||
|
{{#isBasicBasic}}
|
||||||
|
// http basic authentication required
|
||||||
|
setBasicAuthToObject(localVarRequestOptions, configuration)
|
||||||
|
{{/isBasicBasic}}
|
||||||
|
{{#isBasicBearer}}
|
||||||
|
// http bearer authentication required
|
||||||
|
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||||
|
{{/isBasicBearer}}
|
||||||
|
{{#isOAuth}}
|
||||||
|
// oauth required
|
||||||
|
await setOAuthToObject(localVarHeaderParameter, "{{name}}", [{{#scopes}}"{{{scope}}}"{{^-last}}, {{/-last}}{{/scopes}}], configuration)
|
||||||
|
{{/isOAuth}}
|
||||||
|
|
||||||
|
{{/authMethods}}
|
||||||
|
{{#queryParams}}
|
||||||
|
{{#isArray}}
|
||||||
|
if ({{paramName}}) {
|
||||||
|
{{#isCollectionFormatMulti}}
|
||||||
|
{{#uniqueItems}}
|
||||||
|
localVarQueryParameter['{{baseName}}'] = Array.from({{paramName}});
|
||||||
|
{{/uniqueItems}}
|
||||||
|
{{^uniqueItems}}
|
||||||
|
localVarQueryParameter['{{baseName}}'] = {{paramName}};
|
||||||
|
{{/uniqueItems}}
|
||||||
|
{{/isCollectionFormatMulti}}
|
||||||
|
{{^isCollectionFormatMulti}}
|
||||||
|
{{#uniqueItems}}
|
||||||
|
localVarQueryParameter['{{baseName}}'] = Array.from({{paramName}}).join(COLLECTION_FORMATS.{{collectionFormat}});
|
||||||
|
{{/uniqueItems}}
|
||||||
|
{{^uniqueItems}}
|
||||||
|
localVarQueryParameter['{{baseName}}'] = {{paramName}}.join(COLLECTION_FORMATS.{{collectionFormat}});
|
||||||
|
{{/uniqueItems}}
|
||||||
|
{{/isCollectionFormatMulti}}
|
||||||
|
}
|
||||||
|
{{/isArray}}
|
||||||
|
{{^isArray}}
|
||||||
|
if ({{paramName}} !== undefined) {
|
||||||
|
{{#isDateTime}}
|
||||||
|
localVarQueryParameter['{{baseName}}'] = ({{paramName}} as any instanceof Date) ?
|
||||||
|
({{paramName}} as any).toISOString() :
|
||||||
|
{{paramName}};
|
||||||
|
{{/isDateTime}}
|
||||||
|
{{^isDateTime}}
|
||||||
|
{{#isDate}}
|
||||||
|
localVarQueryParameter['{{baseName}}'] = ({{paramName}} as any instanceof Date) ?
|
||||||
|
({{paramName}} as any).toISOString().substr(0,10) :
|
||||||
|
{{paramName}};
|
||||||
|
{{/isDate}}
|
||||||
|
{{^isDate}}
|
||||||
|
localVarQueryParameter['{{baseName}}'] = {{paramName}};
|
||||||
|
{{/isDate}}
|
||||||
|
{{/isDateTime}}
|
||||||
|
}
|
||||||
|
{{/isArray}}
|
||||||
|
|
||||||
|
{{/queryParams}}
|
||||||
|
{{#headerParams}}
|
||||||
|
{{#isArray}}
|
||||||
|
if ({{paramName}}) {
|
||||||
|
{{#uniqueItems}}
|
||||||
|
let mapped = Array.from({{paramName}}).map(value => (<any>"{{{dataType}}}" !== "Set<string>") ? JSON.stringify(value) : (value || ""));
|
||||||
|
{{/uniqueItems}}
|
||||||
|
{{^uniqueItems}}
|
||||||
|
let mapped = {{paramName}}.map(value => (<any>"{{{dataType}}}" !== "Array<string>") ? JSON.stringify(value) : (value || ""));
|
||||||
|
{{/uniqueItems}}
|
||||||
|
localVarHeaderParameter['{{baseName}}'] = mapped.join(COLLECTION_FORMATS["{{collectionFormat}}"]);
|
||||||
|
}
|
||||||
|
{{/isArray}}
|
||||||
|
{{^isArray}}
|
||||||
|
{{! `val == null` covers for both `null` and `undefined`}}
|
||||||
|
if ({{paramName}} != null) {
|
||||||
|
{{#isString}}
|
||||||
|
localVarHeaderParameter['{{baseName}}'] = String({{paramName}});
|
||||||
|
{{/isString}}
|
||||||
|
{{^isString}}
|
||||||
|
{{! isString is falsy also for $ref that defines a string or enum type}}
|
||||||
|
localVarHeaderParameter['{{baseName}}'] = typeof {{paramName}} === 'string'
|
||||||
|
? {{paramName}}
|
||||||
|
: JSON.stringify({{paramName}});
|
||||||
|
{{/isString}}
|
||||||
|
}
|
||||||
|
{{/isArray}}
|
||||||
|
|
||||||
|
{{/headerParams}}
|
||||||
|
{{#vendorExtensions}}
|
||||||
|
{{#formParams}}
|
||||||
|
{{#isArray}}
|
||||||
|
if ({{paramName}}) {
|
||||||
|
{{#isCollectionFormatMulti}}
|
||||||
|
{{paramName}}.forEach((element) => {
|
||||||
|
localVarFormParams.{{#multipartFormData}}append{{/multipartFormData}}{{^multipartFormData}}set{{/multipartFormData}}('{{baseName}}', element as any);
|
||||||
|
})
|
||||||
|
{{/isCollectionFormatMulti}}
|
||||||
|
{{^isCollectionFormatMulti}}
|
||||||
|
localVarFormParams.{{#multipartFormData}}append{{/multipartFormData}}{{^multipartFormData}}set{{/multipartFormData}}('{{baseName}}', {{paramName}}.join(COLLECTION_FORMATS.{{collectionFormat}}));
|
||||||
|
{{/isCollectionFormatMulti}}
|
||||||
|
}{{/isArray}}
|
||||||
|
{{^isArray}}
|
||||||
|
if ({{paramName}} !== undefined) { {{^multipartFormData}}
|
||||||
|
localVarFormParams.set('{{baseName}}', {{paramName}} as any);{{/multipartFormData}}{{#multipartFormData}}{{#isPrimitiveType}}
|
||||||
|
localVarFormParams.append('{{baseName}}', {{paramName}} as any);{{/isPrimitiveType}}{{^isPrimitiveType}}
|
||||||
|
localVarFormParams.append('{{baseName}}', new Blob([JSON.stringify({{paramName}})], { type: "application/json", }));{{/isPrimitiveType}}{{/multipartFormData}}
|
||||||
|
}{{/isArray}}
|
||||||
|
{{/formParams}}{{/vendorExtensions}}
|
||||||
|
{{#vendorExtensions}}{{#hasFormParams}}{{^multipartFormData}}
|
||||||
|
localVarHeaderParameter['Content-Type'] = 'application/x-www-form-urlencoded';{{/multipartFormData}}{{#multipartFormData}}
|
||||||
|
localVarHeaderParameter['Content-Type'] = 'multipart/form-data';{{/multipartFormData}}
|
||||||
|
{{/hasFormParams}}{{/vendorExtensions}}
|
||||||
|
{{#bodyParam}}
|
||||||
|
{{^consumes}}
|
||||||
|
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||||
|
{{/consumes}}
|
||||||
|
{{#consumes.0}}
|
||||||
|
localVarHeaderParameter['Content-Type'] = '{{{mediaType}}}';
|
||||||
|
{{/consumes.0}}
|
||||||
|
|
||||||
|
{{/bodyParam}}
|
||||||
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions,{{#hasFormParams}}{{#multipartFormData}} ...(localVarFormParams as any).getHeaders?.(),{{/multipartFormData}}{{/hasFormParams}} ...options.headers};
|
||||||
|
{{#hasFormParams}}
|
||||||
|
localVarRequestOptions.data = localVarFormParams{{#vendorExtensions}}{{^multipartFormData}}.toString(){{/multipartFormData}}{{/vendorExtensions}};
|
||||||
|
{{/hasFormParams}}
|
||||||
|
{{#bodyParam}}
|
||||||
|
localVarRequestOptions.data = serializeDataIfNeeded({{paramName}}, localVarRequestOptions, configuration)
|
||||||
|
{{/bodyParam}}
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: toPathString(localVarUrlObj),
|
||||||
|
options: localVarRequestOptions,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{{/operation}}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {{classname}} - functional programming interface{{#description}}
|
||||||
|
* {{{.}}}{{/description}}
|
||||||
|
* @export
|
||||||
|
*/
|
||||||
|
export const {{classname}}Fp = function(configuration?: Configuration) {
|
||||||
|
const localVarAxiosParamCreator = {{classname}}AxiosParamCreator(configuration)
|
||||||
|
return {
|
||||||
|
{{#operation}}
|
||||||
|
/**
|
||||||
|
* {{¬es}}
|
||||||
|
{{#summary}}
|
||||||
|
* @summary {{&summary}}
|
||||||
|
{{/summary}}
|
||||||
|
{{#allParams}}
|
||||||
|
* @param {{=<% %>=}}{<%&dataType%>}<%={{ }}=%> {{^required}}[{{/required}}{{paramName}}{{^required}}]{{/required}} {{description}}
|
||||||
|
{{/allParams}}
|
||||||
|
* @param {*} [options] Override http request option.{{#isDeprecated}}
|
||||||
|
* @deprecated{{/isDeprecated}}
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
async {{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<{{{returnType}}}{{^returnType}}void{{/returnType}}>> {
|
||||||
|
const localVarAxiosArgs = await localVarAxiosParamCreator.{{nickname}}({{#allParams}}{{paramName}}, {{/allParams}}options);
|
||||||
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
|
},
|
||||||
|
{{/operation}}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {{classname}} - factory interface{{#description}}
|
||||||
|
* {{&description}}{{/description}}
|
||||||
|
* @export
|
||||||
|
*/
|
||||||
|
export const {{classname}}Factory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||||
|
const localVarFp = {{classname}}Fp(configuration)
|
||||||
|
return {
|
||||||
|
{{#operation}}
|
||||||
|
/**
|
||||||
|
* {{¬es}}
|
||||||
|
{{#summary}}
|
||||||
|
* @summary {{&summary}}
|
||||||
|
{{/summary}}
|
||||||
|
{{#useSingleRequestParameter}}
|
||||||
|
{{#allParams.0}}
|
||||||
|
* @param {{=<% %>=}}{<%& classname %><%& operationIdCamelCase %>Request}<%={{ }}=%> requestParameters Request parameters.
|
||||||
|
{{/allParams.0}}
|
||||||
|
{{/useSingleRequestParameter}}
|
||||||
|
{{^useSingleRequestParameter}}
|
||||||
|
{{#allParams}}
|
||||||
|
* @param {{=<% %>=}}{<%&dataType%>}<%={{ }}=%> {{^required}}[{{/required}}{{paramName}}{{^required}}]{{/required}} {{description}}
|
||||||
|
{{/allParams}}
|
||||||
|
{{/useSingleRequestParameter}}
|
||||||
|
* @param {*} [options] Override http request option.{{#isDeprecated}}
|
||||||
|
* @deprecated{{/isDeprecated}}
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
{{#useSingleRequestParameter}}
|
||||||
|
{{nickname}}({{#allParams.0}}requestParameters: {{classname}}{{operationIdCamelCase}}Request{{^hasRequiredParams}} = {}{{/hasRequiredParams}}, {{/allParams.0}}options?: AxiosRequestConfig): AxiosPromise<{{{returnType}}}{{^returnType}}void{{/returnType}}> {
|
||||||
|
return localVarFp.{{nickname}}({{#allParams.0}}{{#allParams}}requestParameters.{{paramName}}, {{/allParams}}{{/allParams.0}}options).then((request) => request(axios, basePath));
|
||||||
|
},
|
||||||
|
{{/useSingleRequestParameter}}
|
||||||
|
{{^useSingleRequestParameter}}
|
||||||
|
{{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}options?: any): AxiosPromise<{{{returnType}}}{{^returnType}}void{{/returnType}}> {
|
||||||
|
return localVarFp.{{nickname}}({{#allParams}}{{paramName}}, {{/allParams}}options).then((request) => request(axios, basePath));
|
||||||
|
},
|
||||||
|
{{/useSingleRequestParameter}}
|
||||||
|
{{/operation}}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
{{#withInterfaces}}
|
||||||
|
/**
|
||||||
|
* {{classname}} - interface{{#description}}
|
||||||
|
* {{&description}}{{/description}}
|
||||||
|
* @export
|
||||||
|
* @interface {{classname}}
|
||||||
|
*/
|
||||||
|
export interface {{classname}}Interface {
|
||||||
|
{{#operation}}
|
||||||
|
/**
|
||||||
|
* {{¬es}}
|
||||||
|
{{#summary}}
|
||||||
|
* @summary {{&summary}}
|
||||||
|
{{/summary}}
|
||||||
|
{{#allParams}}
|
||||||
|
* @param {{=<% %>=}}{<%&dataType%>}<%={{ }}=%> {{^required}}[{{/required}}{{paramName}}{{^required}}]{{/required}} {{description}}
|
||||||
|
{{/allParams}}
|
||||||
|
* @param {*} [options] Override http request option.{{#isDeprecated}}
|
||||||
|
* @deprecated{{/isDeprecated}}
|
||||||
|
* @throws {RequiredError}
|
||||||
|
* @memberof {{classname}}Interface
|
||||||
|
*/
|
||||||
|
{{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}options?: AxiosRequestConfig): AxiosPromise<{{{returnType}}}{{^returnType}}void{{/returnType}}>;
|
||||||
|
|
||||||
|
{{/operation}}
|
||||||
|
}
|
||||||
|
|
||||||
|
{{/withInterfaces}}
|
||||||
|
{{#useSingleRequestParameter}}
|
||||||
|
{{#operation}}
|
||||||
|
{{#allParams.0}}
|
||||||
|
/**
|
||||||
|
* Request parameters for {{nickname}} operation in {{classname}}.
|
||||||
|
* @export
|
||||||
|
* @interface {{classname}}{{operationIdCamelCase}}Request
|
||||||
|
*/
|
||||||
|
export interface {{classname}}{{operationIdCamelCase}}Request {
|
||||||
|
{{#allParams}}
|
||||||
|
/**
|
||||||
|
* {{description}}
|
||||||
|
* @type {{=<% %>=}}{<%&dataType%>}<%={{ }}=%>
|
||||||
|
* @memberof {{classname}}{{operationIdCamelCase}}
|
||||||
|
*/
|
||||||
|
readonly {{paramName}}{{^required}}?{{/required}}: {{{dataType}}}
|
||||||
|
{{^-last}}
|
||||||
|
|
||||||
|
{{/-last}}
|
||||||
|
{{/allParams}}
|
||||||
|
}
|
||||||
|
|
||||||
|
{{/allParams.0}}
|
||||||
|
{{/operation}}
|
||||||
|
{{/useSingleRequestParameter}}
|
||||||
|
/**
|
||||||
|
* {{classname}} - object-oriented interface{{#description}}
|
||||||
|
* {{{.}}}{{/description}}
|
||||||
|
* @export
|
||||||
|
* @class {{classname}}
|
||||||
|
* @extends {BaseAPI}
|
||||||
|
*/
|
||||||
|
{{#withInterfaces}}
|
||||||
|
export class {{classname}} extends BaseAPI implements {{classname}}Interface {
|
||||||
|
{{/withInterfaces}}
|
||||||
|
{{^withInterfaces}}
|
||||||
|
export class {{classname}} extends BaseAPI {
|
||||||
|
{{/withInterfaces}}
|
||||||
|
{{#operation}}
|
||||||
|
/**
|
||||||
|
* {{¬es}}
|
||||||
|
{{#summary}}
|
||||||
|
* @summary {{&summary}}
|
||||||
|
{{/summary}}
|
||||||
|
{{#useSingleRequestParameter}}
|
||||||
|
{{#allParams.0}}
|
||||||
|
* @param {{=<% %>=}}{<%& classname %><%& operationIdCamelCase %>Request}<%={{ }}=%> requestParameters Request parameters.
|
||||||
|
{{/allParams.0}}
|
||||||
|
{{/useSingleRequestParameter}}
|
||||||
|
{{^useSingleRequestParameter}}
|
||||||
|
{{#allParams}}
|
||||||
|
* @param {{=<% %>=}}{<%&dataType%>}<%={{ }}=%> {{^required}}[{{/required}}{{paramName}}{{^required}}]{{/required}} {{description}}
|
||||||
|
{{/allParams}}
|
||||||
|
{{/useSingleRequestParameter}}
|
||||||
|
* @param {*} [options] Override http request option.{{#isDeprecated}}
|
||||||
|
* @deprecated{{/isDeprecated}}
|
||||||
|
* @throws {RequiredError}
|
||||||
|
* @memberof {{classname}}
|
||||||
|
*/
|
||||||
|
{{#useSingleRequestParameter}}
|
||||||
|
public {{nickname}}({{#allParams.0}}requestParameters: {{classname}}{{operationIdCamelCase}}Request{{^hasRequiredParams}} = {}{{/hasRequiredParams}}, {{/allParams.0}}options?: AxiosRequestConfig) {
|
||||||
|
return {{classname}}Fp(this.configuration).{{nickname}}({{#allParams.0}}{{#allParams}}requestParameters.{{paramName}}, {{/allParams}}{{/allParams.0}}options).then((request) => request(this.axios, this.basePath));
|
||||||
|
}
|
||||||
|
{{/useSingleRequestParameter}}
|
||||||
|
{{^useSingleRequestParameter}}
|
||||||
|
public {{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}options?: AxiosRequestConfig) {
|
||||||
|
return {{classname}}Fp(this.configuration).{{nickname}}({{#allParams}}{{paramName}}, {{/allParams}}options).then((request) => request(this.axios, this.basePath));
|
||||||
|
}
|
||||||
|
{{/useSingleRequestParameter}}
|
||||||
|
{{^-last}}
|
||||||
|
|
||||||
|
{{/-last}}
|
||||||
|
{{/operation}}
|
||||||
|
}
|
||||||
|
{{/operations}}
|
|
@ -0,0 +1,14 @@
|
||||||
|
--- apiInner.mustache 2023-02-10 17:44:20.945845049 +0000
|
||||||
|
+++ apiInner.mustache.patch 2023-02-10 17:46:28.669054112 +0000
|
||||||
|
@@ -173,8 +173,9 @@
|
||||||
|
{{^isArray}}
|
||||||
|
if ({{paramName}} !== undefined) { {{^multipartFormData}}
|
||||||
|
localVarFormParams.set('{{baseName}}', {{paramName}} as any);{{/multipartFormData}}{{#multipartFormData}}{{#isPrimitiveType}}
|
||||||
|
- localVarFormParams.append('{{baseName}}', {{paramName}} as any);{{/isPrimitiveType}}{{^isPrimitiveType}}
|
||||||
|
- localVarFormParams.append('{{baseName}}', new Blob([JSON.stringify({{paramName}})], { type: "application/json", }));{{/isPrimitiveType}}{{/multipartFormData}}
|
||||||
|
+ localVarFormParams.append('{{baseName}}', {{paramName}} as any);{{/isPrimitiveType}}{{^isPrimitiveType}}{{#isEnum}}
|
||||||
|
+ localVarFormParams.append('{{baseName}}', {{paramName}} as any);{{/isEnum}}{{^isEnum}}
|
||||||
|
+ localVarFormParams.append('{{baseName}}', new Blob([JSON.stringify({{paramName}})], { type: "application/json", }));{{/isEnum}}{{/isPrimitiveType}}{{/multipartFormData}}
|
||||||
|
}{{/isArray}}
|
||||||
|
{{/formParams}}{{/vendorExtensions}}
|
||||||
|
{{#vendorExtensions}}{{#hasFormParams}}{{^multipartFormData}}
|
Loading…
Reference in a new issue