mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +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:
		
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							37edef834e
						
					
				
				
					commit
					6f4449d5e9
				
			
							
								
								
									
										26
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										26
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							| @@ -73,6 +73,32 @@ jobs: | ||||
|         run: npm run test:cov | ||||
|         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: | ||||
|     name: Run web unit test suites and checks | ||||
|     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 | ||||
							
								
								
									
										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 | ||||
| } | ||||
|  | ||||
| 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 | ||||
|   mobile | ||||
| elif [[ $1 == 'web' ]]; then | ||||
|   web | ||||
| elif [[ $1 == 'cli' ]]; then | ||||
|   cli | ||||
| else | ||||
|   mobile | ||||
|   web | ||||
|   cli | ||||
| 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}} | ||||
		Reference in New Issue
	
	Block a user