mirror of
https://github.com/immich-app/immich.git
synced 2025-12-08 09:13:07 +03:00
Compare commits
41 Commits
object-sto
...
v1.95.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c34d0595e | ||
|
|
eb73f6605b | ||
|
|
bb5236ae65 | ||
|
|
e33fd40b4c | ||
|
|
73825918c0 | ||
|
|
a22bf99206 | ||
|
|
578b71b961 | ||
|
|
302d98ebe1 | ||
|
|
e338e4def6 | ||
|
|
b896d45ee7 | ||
|
|
b3c7bebbd4 | ||
|
|
e7a875eadd | ||
|
|
7f5459f050 | ||
|
|
7158706296 | ||
|
|
9b20604a70 | ||
|
|
02b9f3ee88 | ||
|
|
f14a2ae099 | ||
|
|
42ce8c5093 | ||
|
|
6690e8edf2 | ||
|
|
14ca471dea | ||
|
|
84b2fc80a4 | ||
|
|
d5ef91b1ae | ||
|
|
947bcf2d68 | ||
|
|
870d517ce3 | ||
|
|
529a83cc72 | ||
|
|
5e1498a279 | ||
|
|
ea4be83ee9 | ||
|
|
a03b37ca86 | ||
|
|
59f8a886e7 | ||
|
|
c50d318152 | ||
|
|
a8f177066b | ||
|
|
dd53795953 | ||
|
|
9e7cb52413 | ||
|
|
0795410a41 | ||
|
|
66d3daa074 | ||
|
|
ddae707ea9 | ||
|
|
4b46bb49d7 | ||
|
|
072f61927c | ||
|
|
36e5d298db | ||
|
|
3480fe5326 | ||
|
|
857ec0451d |
6
.github/workflows/static_analysis.yml
vendored
6
.github/workflows/static_analysis.yml
vendored
@@ -32,7 +32,11 @@ jobs:
|
||||
- name: Run dart analyze
|
||||
run: dart analyze --fatal-infos
|
||||
working-directory: ./mobile
|
||||
|
||||
|
||||
- name: Run dart format
|
||||
run: dart format lib/ --set-exit-if-changed
|
||||
working-directory: ./mobile
|
||||
|
||||
# Enable after riverpod generator migration is completed
|
||||
# - name: Run dart custom lint
|
||||
# run: dart run custom_lint
|
||||
|
||||
68
.github/workflows/test.yml
vendored
68
.github/workflows/test.yml
vendored
@@ -135,38 +135,6 @@ jobs:
|
||||
run: npm run test:cov
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
cli-e2e-tests:
|
||||
name: CLI (e2e)
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./cli
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Run setup typescript-sdk
|
||||
run: npm ci && npm run build
|
||||
working-directory: ./open-api/typescript-sdk
|
||||
|
||||
- name: Run npm install (cli)
|
||||
run: npm ci
|
||||
|
||||
- name: Run npm install (server)
|
||||
run: npm ci && npm run build
|
||||
working-directory: ./server
|
||||
|
||||
- name: Run e2e tests
|
||||
run: npm run test:e2e
|
||||
|
||||
web-unit-tests:
|
||||
name: Web
|
||||
runs-on: ubuntu-latest
|
||||
@@ -205,8 +173,8 @@ jobs:
|
||||
run: npm run test:cov
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
web-e2e-tests:
|
||||
name: Web (e2e)
|
||||
e2e-tests:
|
||||
name: End-to-End Tests
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
@@ -215,11 +183,22 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Run setup typescript-sdk
|
||||
run: npm ci && npm run build
|
||||
working-directory: ./open-api/typescript-sdk
|
||||
|
||||
- name: Run setup cli
|
||||
run: npm ci && npm run build
|
||||
working-directory: ./cli
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
@@ -227,10 +206,12 @@ jobs:
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
- name: Docker build
|
||||
run: docker compose -f docker/docker-compose.e2e.yml build
|
||||
working-directory: ./
|
||||
run: docker compose build
|
||||
|
||||
- name: Run e2e tests
|
||||
- name: Run e2e tests (api & cli)
|
||||
run: npm run test
|
||||
|
||||
- name: Run e2e tests (web)
|
||||
run: npx playwright test
|
||||
|
||||
mobile-unit-tests:
|
||||
@@ -277,6 +258,19 @@ jobs:
|
||||
run: |
|
||||
poetry run pytest app --cov=app --cov-report term-missing
|
||||
|
||||
shellcheck:
|
||||
name: ShellCheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run ShellCheck
|
||||
uses: ludeeus/action-shellcheck@master
|
||||
with:
|
||||
ignore_paths: >-
|
||||
**/open-api/**
|
||||
**/openapi/**
|
||||
**/node_modules/**
|
||||
|
||||
generated-api-up-to-date:
|
||||
name: OpenAPI Clients
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
2
Makefile
2
Makefile
@@ -24,7 +24,7 @@ server-e2e-api:
|
||||
|
||||
.PHONY: e2e
|
||||
e2e:
|
||||
docker compose -f ./docker/docker-compose.e2e.yml up --build -V --remove-orphans
|
||||
docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
||||
|
||||
prod:
|
||||
docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
|
||||
|
||||
@@ -10,7 +10,6 @@ module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
ignorePatterns: ['.eslintrc.js'],
|
||||
rules: {
|
||||
@@ -22,11 +21,6 @@ module.exports = {
|
||||
'unicorn/prefer-module': 'off',
|
||||
curly: 2,
|
||||
'prettier/prettier': 0,
|
||||
'unicorn/prevent-abbreviations': [
|
||||
'error',
|
||||
{
|
||||
ignore: ['\\.e2e-spec$', /^ignore/i],
|
||||
},
|
||||
],
|
||||
'unicorn/prevent-abbreviations': 'error',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
**/*.spec.js
|
||||
test/**
|
||||
upload/**
|
||||
.editorconfig
|
||||
.eslintignore
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:20-alpine3.19 as core
|
||||
FROM node:20-alpine3.19@sha256:c0a3badbd8a0a760de903e00cedbca94588e609299820557e72cba2a53dbaa2c as core
|
||||
|
||||
WORKDIR /usr/src/open-api/typescript-sdk
|
||||
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
||||
|
||||
354
cli/package-lock.json
generated
354
cli/package-lock.json
generated
@@ -8,6 +8,9 @@
|
||||
"name": "@immich/cli",
|
||||
"version": "2.0.8",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"lodash-es": "^4.17.21"
|
||||
},
|
||||
"bin": {
|
||||
"immich": "dist/index.js"
|
||||
},
|
||||
@@ -16,10 +19,11 @@
|
||||
"@testcontainers/postgresql": "^10.7.1",
|
||||
"@types/byte-size": "^8.1.0",
|
||||
"@types/cli-progress": "^3.11.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^20.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"@vitest/coverage-v8": "^1.2.2",
|
||||
"byte-size": "^8.1.1",
|
||||
"cli-progress": "^3.12.0",
|
||||
@@ -29,7 +33,6 @@
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^51.0.0",
|
||||
"glob": "^10.3.1",
|
||||
"immich": "file:../server",
|
||||
"mock-fs": "^5.2.0",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
@@ -64,7 +67,7 @@
|
||||
"../server": {
|
||||
"name": "immich",
|
||||
"version": "1.94.1",
|
||||
"dev": true,
|
||||
"extraneous": true,
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.22.11",
|
||||
@@ -134,8 +137,8 @@
|
||||
"@types/sharp": "^0.31.1",
|
||||
"@types/supertest": "^6.0.0",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@typescript-eslint/eslint-plugin": "^6.4.1",
|
||||
"@typescript-eslint/parser": "^6.4.1",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
@@ -1297,6 +1300,21 @@
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.14.202",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz",
|
||||
"integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/lodash-es": {
|
||||
"version": "4.17.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
||||
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mock-fs": {
|
||||
"version": "4.13.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.4.tgz",
|
||||
@@ -1322,9 +1340,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/semver": {
|
||||
"version": "7.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
|
||||
"integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==",
|
||||
"version": "7.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz",
|
||||
"integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/ssh2": {
|
||||
@@ -1355,16 +1373,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
|
||||
"integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.1.tgz",
|
||||
"integrity": "sha512-OLvgeBv3vXlnnJGIAgCLYKjgMEU+wBGj07MQ/nxAaON+3mLzX7mJbhRYrVGiVvFiXtwFlkcBa/TtmglHy0UbzQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.5.1",
|
||||
"@typescript-eslint/scope-manager": "6.21.0",
|
||||
"@typescript-eslint/type-utils": "6.21.0",
|
||||
"@typescript-eslint/utils": "6.21.0",
|
||||
"@typescript-eslint/visitor-keys": "6.21.0",
|
||||
"@typescript-eslint/scope-manager": "7.0.1",
|
||||
"@typescript-eslint/type-utils": "7.0.1",
|
||||
"@typescript-eslint/utils": "7.0.1",
|
||||
"@typescript-eslint/visitor-keys": "7.0.1",
|
||||
"debug": "^4.3.4",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.2.4",
|
||||
@@ -1380,8 +1398,8 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha",
|
||||
"eslint": "^7.0.0 || ^8.0.0"
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"eslint": "^8.56.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
@@ -1390,15 +1408,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz",
|
||||
"integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.1.tgz",
|
||||
"integrity": "sha512-8GcRRZNzaHxKzBPU3tKtFNing571/GwPBeCvmAUw0yBtfE2XVd0zFKJIMSWkHJcPQi0ekxjIts6L/rrZq5cxGQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "6.21.0",
|
||||
"@typescript-eslint/types": "6.21.0",
|
||||
"@typescript-eslint/typescript-estree": "6.21.0",
|
||||
"@typescript-eslint/visitor-keys": "6.21.0",
|
||||
"@typescript-eslint/scope-manager": "7.0.1",
|
||||
"@typescript-eslint/types": "7.0.1",
|
||||
"@typescript-eslint/typescript-estree": "7.0.1",
|
||||
"@typescript-eslint/visitor-keys": "7.0.1",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1409,7 +1427,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^7.0.0 || ^8.0.0"
|
||||
"eslint": "^8.56.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
@@ -1418,13 +1436,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz",
|
||||
"integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.1.tgz",
|
||||
"integrity": "sha512-v7/T7As10g3bcWOOPAcbnMDuvctHzCFYCG/8R4bK4iYzdFqsZTbXGln0cZNVcwQcwewsYU2BJLay8j0/4zOk4w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.21.0",
|
||||
"@typescript-eslint/visitor-keys": "6.21.0"
|
||||
"@typescript-eslint/types": "7.0.1",
|
||||
"@typescript-eslint/visitor-keys": "7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
@@ -1435,13 +1453,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz",
|
||||
"integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.1.tgz",
|
||||
"integrity": "sha512-YtT9UcstTG5Yqy4xtLiClm1ZpM/pWVGFnkAa90UfdkkZsR1eP2mR/1jbHeYp8Ay1l1JHPyGvoUYR6o3On5Nhmw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "6.21.0",
|
||||
"@typescript-eslint/utils": "6.21.0",
|
||||
"@typescript-eslint/typescript-estree": "7.0.1",
|
||||
"@typescript-eslint/utils": "7.0.1",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^1.0.1"
|
||||
},
|
||||
@@ -1453,7 +1471,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^7.0.0 || ^8.0.0"
|
||||
"eslint": "^8.56.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
@@ -1462,9 +1480,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz",
|
||||
"integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.1.tgz",
|
||||
"integrity": "sha512-uJDfmirz4FHib6ENju/7cz9SdMSkeVvJDK3VcMFvf/hAShg8C74FW+06MaQPODHfDJp/z/zHfgawIJRjlu0RLg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
@@ -1475,13 +1493,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz",
|
||||
"integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.1.tgz",
|
||||
"integrity": "sha512-SO9wHb6ph0/FN5OJxH4MiPscGah5wjOd0RRpaLvuBv9g8565Fgu0uMySFEPqwPHiQU90yzJ2FjRYKGrAhS1xig==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.21.0",
|
||||
"@typescript-eslint/visitor-keys": "6.21.0",
|
||||
"@typescript-eslint/types": "7.0.1",
|
||||
"@typescript-eslint/visitor-keys": "7.0.1",
|
||||
"debug": "^4.3.4",
|
||||
"globby": "^11.1.0",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -1527,17 +1545,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz",
|
||||
"integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.1.tgz",
|
||||
"integrity": "sha512-oe4his30JgPbnv+9Vef1h48jm0S6ft4mNwi9wj7bX10joGn07QRfqIqFHoMiajrtoU88cIhXf8ahwgrcbNLgPA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@types/json-schema": "^7.0.12",
|
||||
"@types/semver": "^7.5.0",
|
||||
"@typescript-eslint/scope-manager": "6.21.0",
|
||||
"@typescript-eslint/types": "6.21.0",
|
||||
"@typescript-eslint/typescript-estree": "6.21.0",
|
||||
"@typescript-eslint/scope-manager": "7.0.1",
|
||||
"@typescript-eslint/types": "7.0.1",
|
||||
"@typescript-eslint/typescript-estree": "7.0.1",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1548,16 +1566,16 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^7.0.0 || ^8.0.0"
|
||||
"eslint": "^8.56.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz",
|
||||
"integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.1.tgz",
|
||||
"integrity": "sha512-hwAgrOyk++RTXrP4KzCg7zB2U0xt7RUU0ZdMSCsqF3eKUwkdXUMyTb0qdCuji7VIbcpG62kKTU9M1J1c9UpFBw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.21.0",
|
||||
"@typescript-eslint/types": "7.0.1",
|
||||
"eslint-visitor-keys": "^3.4.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -3168,10 +3186,6 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/immich": {
|
||||
"resolved": "../server",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||
@@ -3544,6 +3558,11 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
|
||||
},
|
||||
"node_modules/lodash.defaults": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||
@@ -5221,9 +5240,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.1.0.tgz",
|
||||
"integrity": "sha512-STmSFzhY4ljuhz14bg9LkMTk3d98IO6DIArnTY6MeBwiD1Za2StcQtz7fzOUnRCqrHSD5+OS2reg4HOz1eoLnw==",
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.1.2.tgz",
|
||||
"integrity": "sha512-uwiFebQbTWRIGbCaTEBVAfKqgqKNKMJ2uPXsXeLIZxM8MVMjoS3j0cG8NrPxdDIadaWnPSjrkLWffLSC+uiP3Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.19.3",
|
||||
@@ -6437,6 +6456,21 @@
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/lodash": {
|
||||
"version": "4.14.202",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz",
|
||||
"integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/lodash-es": {
|
||||
"version": "4.17.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
||||
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"@types/mock-fs": {
|
||||
"version": "4.13.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.4.tgz",
|
||||
@@ -6462,9 +6496,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@types/semver": {
|
||||
"version": "7.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
|
||||
"integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==",
|
||||
"version": "7.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz",
|
||||
"integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/ssh2": {
|
||||
@@ -6497,16 +6531,16 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/eslint-plugin": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
|
||||
"integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.1.tgz",
|
||||
"integrity": "sha512-OLvgeBv3vXlnnJGIAgCLYKjgMEU+wBGj07MQ/nxAaON+3mLzX7mJbhRYrVGiVvFiXtwFlkcBa/TtmglHy0UbzQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@eslint-community/regexpp": "^4.5.1",
|
||||
"@typescript-eslint/scope-manager": "6.21.0",
|
||||
"@typescript-eslint/type-utils": "6.21.0",
|
||||
"@typescript-eslint/utils": "6.21.0",
|
||||
"@typescript-eslint/visitor-keys": "6.21.0",
|
||||
"@typescript-eslint/scope-manager": "7.0.1",
|
||||
"@typescript-eslint/type-utils": "7.0.1",
|
||||
"@typescript-eslint/utils": "7.0.1",
|
||||
"@typescript-eslint/visitor-keys": "7.0.1",
|
||||
"debug": "^4.3.4",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.2.4",
|
||||
@@ -6516,54 +6550,54 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/parser": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz",
|
||||
"integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.1.tgz",
|
||||
"integrity": "sha512-8GcRRZNzaHxKzBPU3tKtFNing571/GwPBeCvmAUw0yBtfE2XVd0zFKJIMSWkHJcPQi0ekxjIts6L/rrZq5cxGQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/scope-manager": "6.21.0",
|
||||
"@typescript-eslint/types": "6.21.0",
|
||||
"@typescript-eslint/typescript-estree": "6.21.0",
|
||||
"@typescript-eslint/visitor-keys": "6.21.0",
|
||||
"@typescript-eslint/scope-manager": "7.0.1",
|
||||
"@typescript-eslint/types": "7.0.1",
|
||||
"@typescript-eslint/typescript-estree": "7.0.1",
|
||||
"@typescript-eslint/visitor-keys": "7.0.1",
|
||||
"debug": "^4.3.4"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/scope-manager": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz",
|
||||
"integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.1.tgz",
|
||||
"integrity": "sha512-v7/T7As10g3bcWOOPAcbnMDuvctHzCFYCG/8R4bK4iYzdFqsZTbXGln0cZNVcwQcwewsYU2BJLay8j0/4zOk4w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "6.21.0",
|
||||
"@typescript-eslint/visitor-keys": "6.21.0"
|
||||
"@typescript-eslint/types": "7.0.1",
|
||||
"@typescript-eslint/visitor-keys": "7.0.1"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/type-utils": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz",
|
||||
"integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.1.tgz",
|
||||
"integrity": "sha512-YtT9UcstTG5Yqy4xtLiClm1ZpM/pWVGFnkAa90UfdkkZsR1eP2mR/1jbHeYp8Ay1l1JHPyGvoUYR6o3On5Nhmw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/typescript-estree": "6.21.0",
|
||||
"@typescript-eslint/utils": "6.21.0",
|
||||
"@typescript-eslint/typescript-estree": "7.0.1",
|
||||
"@typescript-eslint/utils": "7.0.1",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz",
|
||||
"integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.1.tgz",
|
||||
"integrity": "sha512-uJDfmirz4FHib6ENju/7cz9SdMSkeVvJDK3VcMFvf/hAShg8C74FW+06MaQPODHfDJp/z/zHfgawIJRjlu0RLg==",
|
||||
"dev": true
|
||||
},
|
||||
"@typescript-eslint/typescript-estree": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz",
|
||||
"integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.1.tgz",
|
||||
"integrity": "sha512-SO9wHb6ph0/FN5OJxH4MiPscGah5wjOd0RRpaLvuBv9g8565Fgu0uMySFEPqwPHiQU90yzJ2FjRYKGrAhS1xig==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "6.21.0",
|
||||
"@typescript-eslint/visitor-keys": "6.21.0",
|
||||
"@typescript-eslint/types": "7.0.1",
|
||||
"@typescript-eslint/visitor-keys": "7.0.1",
|
||||
"debug": "^4.3.4",
|
||||
"globby": "^11.1.0",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -6593,27 +6627,27 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/utils": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz",
|
||||
"integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.1.tgz",
|
||||
"integrity": "sha512-oe4his30JgPbnv+9Vef1h48jm0S6ft4mNwi9wj7bX10joGn07QRfqIqFHoMiajrtoU88cIhXf8ahwgrcbNLgPA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@types/json-schema": "^7.0.12",
|
||||
"@types/semver": "^7.5.0",
|
||||
"@typescript-eslint/scope-manager": "6.21.0",
|
||||
"@typescript-eslint/types": "6.21.0",
|
||||
"@typescript-eslint/typescript-estree": "6.21.0",
|
||||
"@typescript-eslint/scope-manager": "7.0.1",
|
||||
"@typescript-eslint/types": "7.0.1",
|
||||
"@typescript-eslint/typescript-estree": "7.0.1",
|
||||
"semver": "^7.5.4"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/visitor-keys": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz",
|
||||
"integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.1.tgz",
|
||||
"integrity": "sha512-hwAgrOyk++RTXrP4KzCg7zB2U0xt7RUU0ZdMSCsqF3eKUwkdXUMyTb0qdCuji7VIbcpG62kKTU9M1J1c9UpFBw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "6.21.0",
|
||||
"@typescript-eslint/types": "7.0.1",
|
||||
"eslint-visitor-keys": "^3.4.1"
|
||||
}
|
||||
},
|
||||
@@ -7794,99 +7828,6 @@
|
||||
"integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
|
||||
"dev": true
|
||||
},
|
||||
"immich": {
|
||||
"version": "file:../server",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.22.11",
|
||||
"@immich/cli": "^2.0.7",
|
||||
"@nestjs/bullmq": "^10.0.1",
|
||||
"@nestjs/cli": "^10.1.16",
|
||||
"@nestjs/common": "^10.2.2",
|
||||
"@nestjs/config": "^3.0.0",
|
||||
"@nestjs/core": "^10.2.2",
|
||||
"@nestjs/platform-express": "^10.2.2",
|
||||
"@nestjs/platform-socket.io": "^10.2.2",
|
||||
"@nestjs/schedule": "^4.0.0",
|
||||
"@nestjs/schematics": "^10.0.2",
|
||||
"@nestjs/swagger": "^7.1.8",
|
||||
"@nestjs/testing": "^10.2.2",
|
||||
"@nestjs/typeorm": "^10.0.0",
|
||||
"@nestjs/websockets": "^10.2.2",
|
||||
"@socket.io/postgres-adapter": "^0.3.1",
|
||||
"@testcontainers/postgresql": "^10.2.1",
|
||||
"@types/archiver": "^6.0.0",
|
||||
"@types/async-lock": "^1.4.2",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/cookie-parser": "^1.4.3",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/fluent-ffmpeg": "^2.1.21",
|
||||
"@types/imagemin": "^8.0.1",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/jest-when": "^3.5.2",
|
||||
"@types/lodash": "^4.14.197",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^20.5.7",
|
||||
"@types/picomatch": "^2.3.3",
|
||||
"@types/sharp": "^0.31.1",
|
||||
"@types/supertest": "^6.0.0",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@typescript-eslint/eslint-plugin": "^6.4.1",
|
||||
"@typescript-eslint/parser": "^6.4.1",
|
||||
"archiver": "^6.0.0",
|
||||
"async-lock": "^1.4.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bullmq": "^4.8.0",
|
||||
"chokidar": "^3.5.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^51.0.0",
|
||||
"exiftool-vendored": "~24.4.0",
|
||||
"exiftool-vendored.pl": "12.73",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"geo-tz": "^8.0.0",
|
||||
"glob": "^10.3.3",
|
||||
"handlebars": "^4.7.8",
|
||||
"i18n-iso-countries": "^7.6.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"jest": "^29.6.4",
|
||||
"jest-when": "^3.6.0",
|
||||
"joi": "^17.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^3.4.2",
|
||||
"mock-fs": "^5.2.0",
|
||||
"nest-commander": "^3.11.1",
|
||||
"node-addon-api": "^7.0.0",
|
||||
"openid-client": "^5.4.3",
|
||||
"pg": "^8.11.3",
|
||||
"picomatch": "^4.0.0",
|
||||
"prettier": "^3.0.2",
|
||||
"prettier-plugin-organize-imports": "^3.2.3",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^5.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"sharp": "^0.33.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"sql-formatter": "^15.0.0",
|
||||
"supertest": "^6.3.3",
|
||||
"testcontainers": "^10.2.1",
|
||||
"thumbhash": "^0.1.1",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-loader": "^9.4.4",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typeorm": "^0.3.17",
|
||||
"typescript": "^5.3.3",
|
||||
"ua-parser-js": "^1.0.35",
|
||||
"utimes": "^5.2.1"
|
||||
}
|
||||
},
|
||||
"import-fresh": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||
@@ -8178,6 +8119,11 @@
|
||||
"p-locate": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"lodash-es": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
|
||||
},
|
||||
"lodash.defaults": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||
@@ -9418,9 +9364,9 @@
|
||||
}
|
||||
},
|
||||
"vite": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.1.0.tgz",
|
||||
"integrity": "sha512-STmSFzhY4ljuhz14bg9LkMTk3d98IO6DIArnTY6MeBwiD1Za2StcQtz7fzOUnRCqrHSD5+OS2reg4HOz1eoLnw==",
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.1.2.tgz",
|
||||
"integrity": "sha512-uwiFebQbTWRIGbCaTEBVAfKqgqKNKMJ2uPXsXeLIZxM8MVMjoS3j0cG8NrPxdDIadaWnPSjrkLWffLSC+uiP3Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"esbuild": "^0.19.3",
|
||||
|
||||
@@ -17,10 +17,11 @@
|
||||
"@testcontainers/postgresql": "^10.7.1",
|
||||
"@types/byte-size": "^8.1.0",
|
||||
"@types/cli-progress": "^3.11.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^20.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"@vitest/coverage-v8": "^1.2.2",
|
||||
"byte-size": "^8.1.1",
|
||||
"cli-progress": "^3.12.0",
|
||||
@@ -30,7 +31,6 @@
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^51.0.0",
|
||||
"glob": "^10.3.1",
|
||||
"immich": "file:../server",
|
||||
"mock-fs": "^5.2.0",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
@@ -41,15 +41,14 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\" --max-warnings 0",
|
||||
"lint": "eslint \"src/**/*.ts\" --max-warnings 0",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"prepack": "npm run build",
|
||||
"test": "vitest",
|
||||
"test:cov": "vitest --coverage",
|
||||
"format": "prettier --check .",
|
||||
"format:fix": "prettier --write .",
|
||||
"check": "tsc --noEmit",
|
||||
"test:e2e": "vitest --config test/e2e/vitest.config.ts"
|
||||
"check": "tsc --noEmit"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -58,5 +57,8 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash-es": "^4.17.21"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { AssetBulkUploadCheckResult } from '@immich/sdk';
|
||||
import byteSize from 'byte-size';
|
||||
import cliProgress from 'cli-progress';
|
||||
import { chunk, zip } from 'lodash-es';
|
||||
import { createHash } from 'node:crypto';
|
||||
import fs, { createReadStream } from 'node:fs';
|
||||
import { access, constants, stat, unlink } from 'node:fs/promises';
|
||||
@@ -9,15 +11,23 @@ import { ImmichApi } from 'src/services/api.service';
|
||||
import { CrawlService } from '../services/crawl.service';
|
||||
import { BaseCommand } from './base-command';
|
||||
|
||||
const zipDefined = zip as <T, U>(a: T[], b: U[]) => [T, U][];
|
||||
|
||||
enum CheckResponseStatus {
|
||||
ACCEPT = 'accept',
|
||||
REJECT = 'reject',
|
||||
DUPLICATE = 'duplicate',
|
||||
}
|
||||
|
||||
class Asset {
|
||||
readonly path: string;
|
||||
readonly deviceId!: string;
|
||||
|
||||
id?: string;
|
||||
deviceAssetId?: string;
|
||||
fileCreatedAt?: Date;
|
||||
fileModifiedAt?: Date;
|
||||
sidecarPath?: string;
|
||||
fileSize!: number;
|
||||
fileSize?: number;
|
||||
albumName?: string;
|
||||
|
||||
constructor(path: string) {
|
||||
@@ -105,17 +115,141 @@ export class UploadOptionsDto {
|
||||
album? = false;
|
||||
albumName? = '';
|
||||
includeHidden? = false;
|
||||
concurrency? = 4;
|
||||
}
|
||||
|
||||
export class UploadCommand extends BaseCommand {
|
||||
uploadLength!: number;
|
||||
api!: ImmichApi;
|
||||
|
||||
public async run(paths: string[], options: UploadOptionsDto): Promise<void> {
|
||||
const api = await this.connect();
|
||||
this.api = await this.connect();
|
||||
|
||||
const formatResponse = await api.getSupportedMediaTypes();
|
||||
const crawlService = new CrawlService(formatResponse.image, formatResponse.video);
|
||||
console.log('Crawling for assets...');
|
||||
const files = await this.getFiles(paths, options);
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log('No assets found, exiting');
|
||||
return;
|
||||
}
|
||||
|
||||
const assetsToCheck = files.map((path) => new Asset(path));
|
||||
|
||||
const { newAssets, duplicateAssets } = await this.checkAssets(assetsToCheck, options.concurrency ?? 4);
|
||||
|
||||
const totalSizeUploaded = await this.upload(newAssets, options);
|
||||
const messageStart = options.dryRun ? 'Would have' : 'Successfully';
|
||||
if (newAssets.length === 0) {
|
||||
console.log('All assets were already uploaded, nothing to do.');
|
||||
} else {
|
||||
console.log(
|
||||
`${messageStart} uploaded ${newAssets.length} asset${newAssets.length === 1 ? '' : 's'} (${byteSize(totalSizeUploaded)})`,
|
||||
);
|
||||
}
|
||||
|
||||
if (options.album || options.albumName) {
|
||||
const { createdAlbumCount, updatedAssetCount } = await this.updateAlbums(
|
||||
[...newAssets, ...duplicateAssets],
|
||||
options,
|
||||
);
|
||||
console.log(`${messageStart} created ${createdAlbumCount} new album${createdAlbumCount === 1 ? '' : 's'}`);
|
||||
console.log(`${messageStart} updated ${updatedAssetCount} asset${updatedAssetCount === 1 ? '' : 's'}`);
|
||||
}
|
||||
|
||||
if (!options.delete) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
console.log(`Would now have deleted assets, but skipped due to dry run`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Deleting assets that have been uploaded...');
|
||||
|
||||
await this.deleteAssets(newAssets, options);
|
||||
}
|
||||
|
||||
public async checkAssets(
|
||||
assetsToCheck: Asset[],
|
||||
concurrency: number,
|
||||
): Promise<{ newAssets: Asset[]; duplicateAssets: Asset[]; rejectedAssets: Asset[] }> {
|
||||
for (const assets of chunk(assetsToCheck, concurrency)) {
|
||||
await Promise.all(assets.map((asset: Asset) => asset.prepare()));
|
||||
}
|
||||
|
||||
const checkProgress = new cliProgress.SingleBar(
|
||||
{ format: 'Checking assets | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets' },
|
||||
cliProgress.Presets.shades_classic,
|
||||
);
|
||||
checkProgress.start(assetsToCheck.length, 0);
|
||||
|
||||
const newAssets = [];
|
||||
const duplicateAssets = [];
|
||||
const rejectedAssets = [];
|
||||
try {
|
||||
for (const assets of chunk(assetsToCheck, concurrency)) {
|
||||
const checkedAssets = await this.getStatus(assets);
|
||||
for (const checked of checkedAssets) {
|
||||
if (checked.status === CheckResponseStatus.ACCEPT) {
|
||||
newAssets.push(checked.asset);
|
||||
} else if (checked.status === CheckResponseStatus.DUPLICATE) {
|
||||
duplicateAssets.push(checked.asset);
|
||||
} else {
|
||||
rejectedAssets.push(checked.asset);
|
||||
}
|
||||
checkProgress.increment();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
checkProgress.stop();
|
||||
}
|
||||
|
||||
return { newAssets, duplicateAssets, rejectedAssets };
|
||||
}
|
||||
|
||||
public async upload(assetsToUpload: Asset[], options: UploadOptionsDto): Promise<number> {
|
||||
let totalSize = 0;
|
||||
|
||||
// Compute total size first
|
||||
for (const asset of assetsToUpload) {
|
||||
totalSize += asset.fileSize ?? 0;
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
const uploadProgress = new cliProgress.SingleBar(
|
||||
{
|
||||
format: 'Uploading assets | {bar} | {percentage}% | ETA: {eta_formatted} | {value_formatted}/{total_formatted}',
|
||||
},
|
||||
cliProgress.Presets.shades_classic,
|
||||
);
|
||||
uploadProgress.start(totalSize, 0);
|
||||
uploadProgress.update({ value_formatted: 0, total_formatted: byteSize(totalSize) });
|
||||
|
||||
let totalSizeUploaded = 0;
|
||||
try {
|
||||
for (const assets of chunk(assetsToUpload, options.concurrency)) {
|
||||
const ids = await this.uploadAssets(assets);
|
||||
for (const [asset, id] of zipDefined(assets, ids)) {
|
||||
asset.id = id;
|
||||
if (asset.fileSize) {
|
||||
totalSizeUploaded += asset.fileSize ?? 0;
|
||||
} else {
|
||||
console.log(`Could not determine file size for ${asset.path}`);
|
||||
}
|
||||
}
|
||||
uploadProgress.update(totalSizeUploaded, { value_formatted: byteSize(totalSizeUploaded) });
|
||||
}
|
||||
} finally {
|
||||
uploadProgress.stop();
|
||||
}
|
||||
|
||||
return totalSizeUploaded;
|
||||
}
|
||||
|
||||
public async getFiles(paths: string[], options: UploadOptionsDto): Promise<string[]> {
|
||||
const inputFiles: string[] = [];
|
||||
for (const pathArgument of paths) {
|
||||
const fileStat = await fs.promises.lstat(pathArgument);
|
||||
@@ -124,151 +258,187 @@ export class UploadCommand extends BaseCommand {
|
||||
}
|
||||
}
|
||||
|
||||
const files: string[] = await crawlService.crawl({
|
||||
const files: string[] = await this.crawl(paths, options);
|
||||
files.push(...inputFiles);
|
||||
return files;
|
||||
}
|
||||
|
||||
public async getAlbums(): Promise<Map<string, string>> {
|
||||
const existingAlbums = await this.api.getAllAlbums();
|
||||
|
||||
const albumMapping = new Map<string, string>();
|
||||
for (const album of existingAlbums) {
|
||||
albumMapping.set(album.albumName, album.id);
|
||||
}
|
||||
|
||||
return albumMapping;
|
||||
}
|
||||
|
||||
public async updateAlbums(
|
||||
assets: Asset[],
|
||||
options: UploadOptionsDto,
|
||||
): Promise<{ createdAlbumCount: number; updatedAssetCount: number }> {
|
||||
if (options.albumName) {
|
||||
for (const asset of assets) {
|
||||
asset.albumName = options.albumName;
|
||||
}
|
||||
}
|
||||
|
||||
const existingAlbums = await this.getAlbums();
|
||||
const assetsToUpdate = assets.filter(
|
||||
(asset): asset is Asset & { albumName: string; id: string } => !!(asset.albumName && asset.id),
|
||||
);
|
||||
|
||||
const newAlbumsSet: Set<string> = new Set();
|
||||
for (const asset of assetsToUpdate) {
|
||||
if (!existingAlbums.has(asset.albumName)) {
|
||||
newAlbumsSet.add(asset.albumName);
|
||||
}
|
||||
}
|
||||
|
||||
const newAlbums = [...newAlbumsSet];
|
||||
|
||||
if (options.dryRun) {
|
||||
return { createdAlbumCount: newAlbums.length, updatedAssetCount: assetsToUpdate.length };
|
||||
}
|
||||
|
||||
const albumCreationProgress = new cliProgress.SingleBar(
|
||||
{
|
||||
format: 'Creating albums | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} albums',
|
||||
},
|
||||
cliProgress.Presets.shades_classic,
|
||||
);
|
||||
albumCreationProgress.start(newAlbums.length, 0);
|
||||
|
||||
try {
|
||||
for (const albumNames of chunk(newAlbums, options.concurrency)) {
|
||||
const newAlbumIds = await Promise.all(
|
||||
albumNames.map((albumName: string) => this.api.createAlbum({ albumName }).then((r) => r.id)),
|
||||
);
|
||||
|
||||
for (const [albumName, albumId] of zipDefined(albumNames, newAlbumIds)) {
|
||||
existingAlbums.set(albumName, albumId);
|
||||
}
|
||||
|
||||
albumCreationProgress.increment(albumNames.length);
|
||||
}
|
||||
} finally {
|
||||
albumCreationProgress.stop();
|
||||
}
|
||||
|
||||
const albumToAssets = new Map<string, string[]>();
|
||||
for (const asset of assetsToUpdate) {
|
||||
const albumId = existingAlbums.get(asset.albumName);
|
||||
if (albumId) {
|
||||
if (!albumToAssets.has(albumId)) {
|
||||
albumToAssets.set(albumId, []);
|
||||
}
|
||||
albumToAssets.get(albumId)?.push(asset.id);
|
||||
}
|
||||
}
|
||||
|
||||
const albumUpdateProgress = new cliProgress.SingleBar(
|
||||
{
|
||||
format: 'Adding assets to albums | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets',
|
||||
},
|
||||
cliProgress.Presets.shades_classic,
|
||||
);
|
||||
albumUpdateProgress.start(assetsToUpdate.length, 0);
|
||||
|
||||
try {
|
||||
for (const [albumId, assets] of albumToAssets.entries()) {
|
||||
for (const assetBatch of chunk(assets, Math.min(1000 * (options.concurrency ?? 4), 65_000))) {
|
||||
await this.api.addAssetsToAlbum(albumId, { ids: assetBatch });
|
||||
albumUpdateProgress.increment(assetBatch.length);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
albumUpdateProgress.stop();
|
||||
}
|
||||
|
||||
return { createdAlbumCount: newAlbums.length, updatedAssetCount: assetsToUpdate.length };
|
||||
}
|
||||
|
||||
public async deleteAssets(assets: Asset[], options: UploadOptionsDto): Promise<void> {
|
||||
const deletionProgress = new cliProgress.SingleBar(
|
||||
{
|
||||
format: 'Deleting local assets | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets',
|
||||
},
|
||||
cliProgress.Presets.shades_classic,
|
||||
);
|
||||
deletionProgress.start(assets.length, 0);
|
||||
|
||||
try {
|
||||
for (const assetBatch of chunk(assets, options.concurrency)) {
|
||||
await Promise.all(assetBatch.map((asset: Asset) => asset.delete()));
|
||||
deletionProgress.update(assetBatch.length);
|
||||
}
|
||||
} finally {
|
||||
deletionProgress.stop();
|
||||
}
|
||||
}
|
||||
|
||||
private async getStatus(assets: Asset[]): Promise<{ asset: Asset; status: CheckResponseStatus }[]> {
|
||||
const checkResponse = await this.checkHashes(assets);
|
||||
|
||||
const responses = [];
|
||||
for (const [check, asset] of zipDefined(checkResponse, assets)) {
|
||||
if (check.assetId) {
|
||||
asset.id = check.assetId;
|
||||
}
|
||||
|
||||
if (check.action === 'accept') {
|
||||
responses.push({ asset, status: CheckResponseStatus.ACCEPT });
|
||||
} else if (check.reason === 'duplicate') {
|
||||
responses.push({ asset, status: CheckResponseStatus.DUPLICATE });
|
||||
} else {
|
||||
responses.push({ asset, status: CheckResponseStatus.REJECT });
|
||||
}
|
||||
}
|
||||
|
||||
return responses;
|
||||
}
|
||||
|
||||
private async checkHashes(assetsToCheck: Asset[]): Promise<AssetBulkUploadCheckResult[]> {
|
||||
const checksums = await Promise.all(assetsToCheck.map((asset) => asset.hash()));
|
||||
const assetBulkUploadCheckDto = {
|
||||
assets: zipDefined(assetsToCheck, checksums).map(([asset, checksum]) => ({ id: asset.path, checksum })),
|
||||
};
|
||||
const checkResponse = await this.api.checkBulkUpload(assetBulkUploadCheckDto);
|
||||
return checkResponse.results;
|
||||
}
|
||||
|
||||
private async uploadAssets(assets: Asset[]): Promise<string[]> {
|
||||
const fileRequests = await Promise.all(assets.map((asset) => asset.getUploadFormData()));
|
||||
return Promise.all(fileRequests.map((request) => this.uploadAsset(request).then((response) => response.id)));
|
||||
}
|
||||
|
||||
private async crawl(paths: string[], options: UploadOptionsDto): Promise<string[]> {
|
||||
const formatResponse = await this.api.getSupportedMediaTypes();
|
||||
const crawlService = new CrawlService(formatResponse.image, formatResponse.video);
|
||||
|
||||
return crawlService.crawl({
|
||||
pathsToCrawl: paths,
|
||||
recursive: options.recursive,
|
||||
exclusionPatterns: options.exclusionPatterns,
|
||||
includeHidden: options.includeHidden,
|
||||
});
|
||||
|
||||
files.push(...inputFiles);
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log('No assets found, exiting');
|
||||
return;
|
||||
}
|
||||
|
||||
const assetsToUpload = files.map((path) => new Asset(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.prepare();
|
||||
totalSize += asset.fileSize;
|
||||
|
||||
if (options.albumName) {
|
||||
asset.albumName = options.albumName;
|
||||
}
|
||||
}
|
||||
|
||||
const existingAlbums = await api.getAllAlbums();
|
||||
|
||||
uploadProgress.start(totalSize, 0);
|
||||
uploadProgress.update({ value_formatted: 0, total_formatted: byteSize(totalSize) });
|
||||
|
||||
try {
|
||||
for (const asset of assetsToUpload) {
|
||||
uploadProgress.update({
|
||||
filename: asset.path,
|
||||
});
|
||||
|
||||
let skipUpload = false;
|
||||
|
||||
let skipAsset = false;
|
||||
let existingAssetId: string | undefined = undefined;
|
||||
|
||||
if (!options.skipHash) {
|
||||
const assetBulkUploadCheckDto = { assets: [{ id: asset.path, checksum: await asset.hash() }] };
|
||||
|
||||
const checkResponse = await api.checkBulkUpload(assetBulkUploadCheckDto);
|
||||
|
||||
skipUpload = checkResponse.results[0].action === 'reject';
|
||||
|
||||
const isDuplicate = checkResponse.results[0].reason === 'duplicate';
|
||||
if (isDuplicate) {
|
||||
existingAssetId = checkResponse.results[0].assetId;
|
||||
}
|
||||
|
||||
skipAsset = skipUpload && !isDuplicate;
|
||||
}
|
||||
|
||||
if (!skipAsset && !options.dryRun) {
|
||||
if (!skipUpload) {
|
||||
const formData = await asset.getUploadFormData();
|
||||
const response = await this.uploadAsset(api, formData);
|
||||
const json = await response.json();
|
||||
existingAssetId = json.id;
|
||||
uploadCounter++;
|
||||
totalSizeUploaded += asset.fileSize;
|
||||
}
|
||||
|
||||
if ((options.album || options.albumName) && asset.albumName !== undefined) {
|
||||
let album = existingAlbums.find((album) => album.albumName === asset.albumName);
|
||||
if (!album) {
|
||||
const response = await api.createAlbum({ albumName: asset.albumName });
|
||||
album = response;
|
||||
existingAlbums.push(album);
|
||||
}
|
||||
|
||||
if (existingAssetId) {
|
||||
await api.addAssetsToAlbum(album.id, {
|
||||
ids: [existingAssetId],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sizeSoFar += asset.fileSize;
|
||||
|
||||
uploadProgress.update(sizeSoFar, { value_formatted: byteSize(sizeSoFar) });
|
||||
}
|
||||
} finally {
|
||||
uploadProgress.stop();
|
||||
}
|
||||
|
||||
const messageStart = options.dryRun ? 'Would have' : 'Successfully';
|
||||
|
||||
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 (options.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(files.length, 0);
|
||||
|
||||
for (const asset of assetsToUpload) {
|
||||
if (!options.dryRun) {
|
||||
await asset.delete();
|
||||
}
|
||||
deletionProgress.increment();
|
||||
}
|
||||
deletionProgress.stop();
|
||||
console.log('Deletion complete');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async uploadAsset(api: ImmichApi, data: FormData): Promise<Response> {
|
||||
const url = api.instanceUrl + '/asset/upload';
|
||||
private async uploadAsset(data: FormData): Promise<{ id: string }> {
|
||||
const url = this.api.instanceUrl + '/asset/upload';
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'post',
|
||||
redirect: 'error',
|
||||
headers: {
|
||||
'x-api-key': api.apiKey,
|
||||
'x-api-key': this.api.apiKey,
|
||||
},
|
||||
body: data,
|
||||
});
|
||||
if (response.status !== 200 && response.status !== 201) {
|
||||
throw new Error(await response.text());
|
||||
}
|
||||
return response;
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ const program = new Command()
|
||||
.version(version)
|
||||
.description('Command line interface for Immich')
|
||||
.addOption(
|
||||
new Option('-d, --config-directory', 'Configuration directory where auth.yml will be stored')
|
||||
new Option('-d, --config-directory <directory>', 'Configuration directory where auth.yml will be stored')
|
||||
.env('IMMICH_CONFIG_DIR')
|
||||
.default(defaultConfigDirectory),
|
||||
);
|
||||
@@ -43,6 +43,11 @@ program
|
||||
.env('IMMICH_DRY_RUN')
|
||||
.default(false),
|
||||
)
|
||||
.addOption(
|
||||
new Option('-c, --concurrency', 'Number of assets to upload at the same time')
|
||||
.env('IMMICH_UPLOAD_CONCURRENCY')
|
||||
.default(4),
|
||||
)
|
||||
.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(async (paths, options) => {
|
||||
@@ -60,10 +65,10 @@ program
|
||||
program
|
||||
.command('login-key')
|
||||
.description('Login using an API key')
|
||||
.argument('[instanceUrl]')
|
||||
.argument('[apiKey]')
|
||||
.action(async (paths, options) => {
|
||||
await new LoginCommand(program.opts()).run(paths, options);
|
||||
.argument('url')
|
||||
.argument('key')
|
||||
.action(async (url, key) => {
|
||||
await new LoginCommand(program.opts()).run(url, key);
|
||||
});
|
||||
|
||||
program
|
||||
|
||||
@@ -1,17 +1,41 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import yaml from 'yaml';
|
||||
import {
|
||||
TEST_AUTH_FILE,
|
||||
TEST_CONFIG_DIR,
|
||||
TEST_IMMICH_API_KEY,
|
||||
TEST_IMMICH_INSTANCE_URL,
|
||||
createTestAuthFile,
|
||||
deleteAuthFile,
|
||||
readTestAuthFile,
|
||||
spyOnConsole,
|
||||
} from '../../test/cli-test-utils';
|
||||
import { SessionService } from './session.service';
|
||||
|
||||
const TEST_CONFIG_DIR = '/tmp/immich/';
|
||||
const TEST_AUTH_FILE = path.join(TEST_CONFIG_DIR, 'auth.yml');
|
||||
const TEST_IMMICH_INSTANCE_URL = 'https://test/api';
|
||||
const TEST_IMMICH_API_KEY = 'pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg';
|
||||
|
||||
const spyOnConsole = () => vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
||||
const createTestAuthFile = async (contents: string) => {
|
||||
if (!fs.existsSync(TEST_CONFIG_DIR)) {
|
||||
// Create config folder if it doesn't exist
|
||||
const created = await fs.promises.mkdir(TEST_CONFIG_DIR, { recursive: true });
|
||||
if (!created) {
|
||||
throw new Error(`Failed to create config folder ${TEST_CONFIG_DIR}`);
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(TEST_AUTH_FILE, contents);
|
||||
};
|
||||
|
||||
const readTestAuthFile = async (): Promise<string> => {
|
||||
return await fs.promises.readFile(TEST_AUTH_FILE, 'utf8');
|
||||
};
|
||||
|
||||
const deleteAuthFile = () => {
|
||||
try {
|
||||
fs.unlinkSync(TEST_AUTH_FILE);
|
||||
} catch (error: any) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
return {
|
||||
getMyUserInfo: vi.fn(() => Promise.resolve({ email: 'admin@example.com' })),
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { ImmichApi } from 'src/services/api.service';
|
||||
|
||||
export const TEST_CONFIG_DIR = '/tmp/immich/';
|
||||
export const TEST_AUTH_FILE = path.join(TEST_CONFIG_DIR, 'auth.yml');
|
||||
export const TEST_IMMICH_INSTANCE_URL = 'https://test/api';
|
||||
export const TEST_IMMICH_API_KEY = 'pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg';
|
||||
|
||||
export const CLI_BASE_OPTIONS = { configDirectory: TEST_CONFIG_DIR };
|
||||
|
||||
export const setup = async () => {
|
||||
const api = new ImmichApi(process.env.IMMICH_INSTANCE_URL as string, '');
|
||||
await api.signUpAdmin({ email: 'cli@immich.app', password: 'password', name: 'Administrator' });
|
||||
const admin = await api.login({ email: 'cli@immich.app', password: 'password' });
|
||||
const apiKey = await api.createApiKey(
|
||||
{ name: 'CLI Test' },
|
||||
{ headers: { Authorization: `Bearer ${admin.accessToken}` } },
|
||||
);
|
||||
|
||||
api.setApiKey(apiKey.secret);
|
||||
|
||||
return api;
|
||||
};
|
||||
|
||||
export const spyOnConsole = () => vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
||||
export const createTestAuthFile = async (contents: string) => {
|
||||
if (!fs.existsSync(TEST_CONFIG_DIR)) {
|
||||
// Create config folder if it doesn't exist
|
||||
const created = await fs.promises.mkdir(TEST_CONFIG_DIR, { recursive: true });
|
||||
if (!created) {
|
||||
throw new Error(`Failed to create config folder ${TEST_CONFIG_DIR}`);
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(TEST_AUTH_FILE, contents);
|
||||
};
|
||||
|
||||
export const readTestAuthFile = async (): Promise<string> => {
|
||||
return await fs.promises.readFile(TEST_AUTH_FILE, 'utf8');
|
||||
};
|
||||
|
||||
export const deleteAuthFile = () => {
|
||||
try {
|
||||
fs.unlinkSync(TEST_AUTH_FILE);
|
||||
} catch (error: any) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,65 +0,0 @@
|
||||
import { restoreTempFolder, testApp } from '@test-utils';
|
||||
import { readFile, stat } from 'node:fs/promises';
|
||||
import { CLI_BASE_OPTIONS, TEST_AUTH_FILE, deleteAuthFile, setup, spyOnConsole } from 'test/cli-test-utils';
|
||||
import yaml from 'yaml';
|
||||
import { LoginCommand } from '../../src/commands/login.command';
|
||||
|
||||
describe(`login-key (e2e)`, () => {
|
||||
let apiKey: string;
|
||||
let instanceUrl: string;
|
||||
|
||||
spyOnConsole();
|
||||
|
||||
beforeAll(async () => {
|
||||
await testApp.create();
|
||||
if (process.env.IMMICH_INSTANCE_URL) {
|
||||
instanceUrl = process.env.IMMICH_INSTANCE_URL;
|
||||
} else {
|
||||
throw new Error('IMMICH_INSTANCE_URL environment variable not set');
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testApp.teardown();
|
||||
await restoreTempFolder();
|
||||
deleteAuthFile();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await testApp.reset();
|
||||
await restoreTempFolder();
|
||||
|
||||
const api = await setup();
|
||||
apiKey = api.apiKey;
|
||||
|
||||
deleteAuthFile();
|
||||
});
|
||||
|
||||
it('should error when providing an invalid API key', async () => {
|
||||
await expect(new LoginCommand(CLI_BASE_OPTIONS).run(instanceUrl, 'invalid')).rejects.toThrow(
|
||||
`Failed to connect to server ${instanceUrl}: Error: 401`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should log in when providing the correct API key', async () => {
|
||||
await new LoginCommand(CLI_BASE_OPTIONS).run(instanceUrl, apiKey);
|
||||
});
|
||||
|
||||
it('should create an auth file when logging in', async () => {
|
||||
await new LoginCommand(CLI_BASE_OPTIONS).run(instanceUrl, apiKey);
|
||||
|
||||
const data: string = await readFile(TEST_AUTH_FILE, 'utf8');
|
||||
const parsedConfig = yaml.parse(data);
|
||||
|
||||
expect(parsedConfig).toEqual(expect.objectContaining({ instanceUrl, apiKey }));
|
||||
});
|
||||
|
||||
it('should create an auth file with chmod 600', async () => {
|
||||
await new LoginCommand(CLI_BASE_OPTIONS).run(instanceUrl, apiKey);
|
||||
|
||||
const stats = await stat(TEST_AUTH_FILE);
|
||||
const mode = (stats.mode & 0o777).toString(8);
|
||||
|
||||
expect(mode).toEqual('600');
|
||||
});
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
import { restoreTempFolder, testApp } from '@test-utils';
|
||||
import { CLI_BASE_OPTIONS, setup, spyOnConsole } from 'test/cli-test-utils';
|
||||
import { ServerInfoCommand } from '../../src/commands/server-info.command';
|
||||
|
||||
describe(`server-info (e2e)`, () => {
|
||||
const consoleSpy = spyOnConsole();
|
||||
|
||||
beforeAll(async () => {
|
||||
await testApp.create();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testApp.teardown();
|
||||
await restoreTempFolder();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await testApp.reset();
|
||||
await restoreTempFolder();
|
||||
const api = await setup();
|
||||
process.env.IMMICH_API_KEY = api.apiKey;
|
||||
});
|
||||
|
||||
it('should show server version', async () => {
|
||||
await new ServerInfoCommand(CLI_BASE_OPTIONS).run();
|
||||
|
||||
expect(consoleSpy.mock.calls).toEqual([
|
||||
[expect.stringMatching(new RegExp('Server Version: \\d+.\\d+.\\d+'))],
|
||||
[expect.stringMatching('Image Types: .*')],
|
||||
[expect.stringMatching('Video Types: .*')],
|
||||
['Statistics:\n Images: 0\n Videos: 0\n Total: 0'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,42 +0,0 @@
|
||||
import { PostgreSqlContainer } from '@testcontainers/postgresql';
|
||||
import { access } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
export const directoryExists = (directory: string) =>
|
||||
access(directory)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
export default async () => {
|
||||
let IMMICH_TEST_ASSET_PATH: string = '';
|
||||
|
||||
if (process.env.IMMICH_TEST_ASSET_PATH === undefined) {
|
||||
IMMICH_TEST_ASSET_PATH = path.normalize(`${__dirname}/../../../server/test/assets/`);
|
||||
process.env.IMMICH_TEST_ASSET_PATH = IMMICH_TEST_ASSET_PATH;
|
||||
} else {
|
||||
IMMICH_TEST_ASSET_PATH = process.env.IMMICH_TEST_ASSET_PATH;
|
||||
}
|
||||
|
||||
if (!(await directoryExists(`${IMMICH_TEST_ASSET_PATH}/albums`))) {
|
||||
throw new Error(
|
||||
`Test assets not found. Please checkout https://github.com/immich-app/test-assets into ${IMMICH_TEST_ASSET_PATH} before testing`,
|
||||
);
|
||||
}
|
||||
|
||||
if (process.env.DB_HOSTNAME === undefined) {
|
||||
// DB hostname not set which likely means we're not running e2e through docker compose. Start a local postgres container.
|
||||
const pg = await new PostgreSqlContainer('tensorchord/pgvecto-rs:pg14-v0.2.0')
|
||||
.withExposedPorts(5432)
|
||||
.withDatabase('immich')
|
||||
.withUsername('postgres')
|
||||
.withPassword('postgres')
|
||||
.withReuse()
|
||||
.start();
|
||||
|
||||
process.env.DB_URL = pg.getConnectionUri();
|
||||
}
|
||||
|
||||
process.env.NODE_ENV = 'development';
|
||||
process.env.IMMICH_CONFIG_FILE = path.normalize(`${__dirname}/../../../server/e2e/jobs/immich-e2e-config.json`);
|
||||
process.env.TZ = 'Z';
|
||||
};
|
||||
@@ -1,79 +0,0 @@
|
||||
import { IMMICH_TEST_ASSET_PATH, restoreTempFolder, testApp } from '@test-utils';
|
||||
import { ImmichApi } from 'src/services/api.service';
|
||||
import { CLI_BASE_OPTIONS, setup, spyOnConsole } from 'test/cli-test-utils';
|
||||
import { UploadCommand } from '../../src/commands/upload.command';
|
||||
|
||||
describe(`upload (e2e)`, () => {
|
||||
let api: ImmichApi;
|
||||
|
||||
spyOnConsole();
|
||||
|
||||
beforeAll(async () => {
|
||||
await testApp.create();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testApp.teardown();
|
||||
await restoreTempFolder();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await testApp.reset();
|
||||
await restoreTempFolder();
|
||||
api = await setup();
|
||||
process.env.IMMICH_API_KEY = api.apiKey;
|
||||
});
|
||||
|
||||
it('should upload a folder recursively', async () => {
|
||||
await new UploadCommand(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], { recursive: true });
|
||||
const assets = await api.getAllAssets();
|
||||
expect(assets.length).toBeGreaterThan(4);
|
||||
});
|
||||
|
||||
it('should not create a new album', async () => {
|
||||
await new UploadCommand(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], { recursive: true });
|
||||
const albums = await api.getAllAlbums();
|
||||
expect(albums.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should create album from folder name', async () => {
|
||||
await new UploadCommand(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], {
|
||||
recursive: true,
|
||||
album: true,
|
||||
});
|
||||
|
||||
const albums = await api.getAllAlbums();
|
||||
expect(albums.length).toEqual(1);
|
||||
const natureAlbum = albums[0];
|
||||
expect(natureAlbum.albumName).toEqual('nature');
|
||||
});
|
||||
|
||||
it('should add existing assets to album', async () => {
|
||||
await new UploadCommand(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], {
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
// upload again, but this time add to album
|
||||
await new UploadCommand(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], {
|
||||
recursive: true,
|
||||
album: true,
|
||||
});
|
||||
|
||||
const albums = await api.getAllAlbums();
|
||||
expect(albums.length).toEqual(1);
|
||||
const natureAlbum = albums[0];
|
||||
expect(natureAlbum.albumName).toEqual('nature');
|
||||
});
|
||||
|
||||
it('should upload to the specified album name', async () => {
|
||||
await new UploadCommand(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], {
|
||||
recursive: true,
|
||||
albumName: 'testAlbum',
|
||||
});
|
||||
|
||||
const albums = await api.getAllAlbums();
|
||||
expect(albums.length).toEqual(1);
|
||||
const testAlbum = albums[0];
|
||||
expect(testAlbum.albumName).toEqual('testAlbum');
|
||||
});
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'@test-utils': new URL('../../../server/dist/test-utils/utils.js', import.meta.url).pathname,
|
||||
},
|
||||
},
|
||||
test: {
|
||||
include: ['**/*.e2e-spec.ts'],
|
||||
globals: true,
|
||||
globalSetup: 'test/e2e/setup.ts',
|
||||
pool: 'forks',
|
||||
poolOptions: {
|
||||
forks: {
|
||||
maxForks: 1,
|
||||
minForks: 1,
|
||||
},
|
||||
},
|
||||
testTimeout: 10_000,
|
||||
},
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = async () => {
|
||||
process.env.TZ = 'UTC';
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
// add all jest-extended matchers
|
||||
import * as matchers from 'jest-extended';
|
||||
expect.extend(matchers);
|
||||
@@ -7,7 +7,7 @@ When contributing code through a pull request, please check the following:
|
||||
- [ ] `npm run lint` (linting via ESLint)
|
||||
- [ ] `npm run format` (formatting via Prettier)
|
||||
- [ ] `npm run check:svelte` (Type checking via SvelteKit)
|
||||
- [ ] `npm test` (Tests via Jest)
|
||||
- [ ] `npm test` (unit tests)
|
||||
|
||||
:::tip
|
||||
Run all web checks with `npm run check:all`
|
||||
@@ -18,7 +18,7 @@ Run all web checks with `npm run check:all`
|
||||
- [ ] `npm run lint` (linting via ESLint)
|
||||
- [ ] `npm run format` (formatting via Prettier)
|
||||
- [ ] `npm run check` (Type checking via `tsc`)
|
||||
- [ ] `npm test` (Tests via Jest)
|
||||
- [ ] `npm test` (unit tests)
|
||||
|
||||
:::tip
|
||||
Run all server checks with `npm run check:all`
|
||||
|
||||
@@ -42,23 +42,24 @@ Finally, files can be deleted from Immich via the `Remove Offline Files` job. Th
|
||||
|
||||
### Import Paths
|
||||
|
||||
External libraries use import paths to determine which files to scan. Each library can have multiple import paths so that files from different locations can be added to the same library. Import paths are scanned recursively, and if a file is in multiple import paths, it will only be added once. If the import paths are edited in a way that an external file is no longer in any import path, it will be removed from the library in the same way a deleted file would. If the file is moved back to an import path, it will be added again as if it was a new file.
|
||||
External libraries use import paths to determine which files to scan. Each library can have multiple import paths so that files from different locations can be added to the same library. Import paths are scanned recursively, and if a file is in multiple import paths, it will only be added once. Each import file must be a readable directory that exists on the filesystem; the import path dialog will alert you of any paths that are not accessible.
|
||||
|
||||
If the import paths are edited in a way that an external file is no longer in any import path, it will be removed from the library in the same way a deleted file would. If the file is moved back to an import path, it will be added again as if it was a new file.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
Sometimes, an external library will not scan correctly. This can happen if the immich_server or immich_microservices can't access the files. Here are some things to check:
|
||||
Sometimes, an external library will not scan correctly. This can happen if immich_server or immich_microservices can't access the files. Here are some things to check:
|
||||
|
||||
- Is the external path set correctly?
|
||||
- Is the external path set correctly? Each import path must be contained in the external path.
|
||||
- Make sure the external path does not contain spaces
|
||||
- In the docker-compose file, are the volumes mounted correctly?
|
||||
- Are the volumes identical between the `server` and `microservices` container?
|
||||
- Are the import paths set correctly, and do they match the path set in docker-compose file?
|
||||
- Are you using symbolic link in your import library?
|
||||
- Make sure you don't use symlinks in your import libraries, and that you aren't linking across docker mounts.
|
||||
- Are the permissions set correctly?
|
||||
- Are you using forward slashes everywhere? (`/`)
|
||||
- Are you using symlink across docker mounts?
|
||||
- Are you using [spaces in the internal path](/docs/features/libraries#:~:text=can%20be%20accessed.-,NOTE,-Spaces%20in%20the)?
|
||||
- Make sure you are using forward slashes (`/`) and not backward slashes.
|
||||
|
||||
If all else fails, you can always start a shell inside the container and check if the path is accessible. For example, `docker exec -it immich_microservices /bin/bash` will start a bash shell. If your import path, for instance, is `/data/import/photos`, you can check if the files are accessible by running `ls /data/import/photos`. Also check the `immich_server` container in the same way.
|
||||
To validate that Immich can reach your external library, start a shell inside the container. Run `docker exec -it immich_microservices /bin/bash` to a bash shell. If your import path is `/data/import/photos`, check it with `ls /data/import/photos`. Do the same check for the `immich_server` container. If you cannot access this directory in both the `microservices` and `server` containers, Immich won't be able to import files.
|
||||
|
||||
### Security Considerations
|
||||
|
||||
|
||||
@@ -60,14 +60,17 @@ These environment variables are used by the `docker-compose.yml` file and do **N
|
||||
|
||||
## Database
|
||||
|
||||
| Variable | Description | Default | Services |
|
||||
| :------------ | :---------------- | :---------: | :-------------------- |
|
||||
| `DB_URL` | Database URL | | server, microservices |
|
||||
| `DB_HOSTNAME` | Database Host | `localhost` | server, microservices |
|
||||
| `DB_PORT` | Database Port | `5432` | server, microservices |
|
||||
| `DB_USERNAME` | Database User | `postgres` | server, microservices |
|
||||
| `DB_PASSWORD` | Database Password | `postgres` | server, microservices |
|
||||
| `DB_DATABASE` | Database Name | `immich` | server, microservices |
|
||||
| Variable | Description | Default | Services |
|
||||
| :---------------------------------- | :------------------------------------------------------------ | :----------: | :-------------------- |
|
||||
| `DB_URL` | Database URL | | server, microservices |
|
||||
| `DB_HOSTNAME` | Database Host | `localhost` | server, microservices |
|
||||
| `DB_PORT` | Database Port | `5432` | server, microservices |
|
||||
| `DB_USERNAME` | Database User | `postgres` | server, microservices |
|
||||
| `DB_PASSWORD` | Database Password | `postgres` | server, microservices |
|
||||
| `DB_DATABASE` | Database Name | `immich` | server, microservices |
|
||||
| `DB_VECTOR_EXTENSION`<sup>\*1</sup> | Database Vector Extension (one of [`pgvector`, `pgvecto.rs`]) | `pgvecto.rs` | server, microservices |
|
||||
|
||||
\*1: This setting cannot be changed after the server has successfully started up
|
||||
|
||||
:::info
|
||||
|
||||
|
||||
6
docs/package-lock.json
generated
6
docs/package-lock.json
generated
@@ -2964,9 +2964,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mdx-js/react": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.0.0.tgz",
|
||||
"integrity": "sha512-nDctevR9KyYFyV+m+/+S4cpzCWHqj+iHDHq3QrsWezcC+B17uZdIWgCguESUkwFhM3n/56KxWVE3V6EokrmONQ==",
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.0.1.tgz",
|
||||
"integrity": "sha512-9ZrPIU4MGf6et1m1ov3zKf+q9+deetI51zprKB1D/z3NOb+rUxxtEl3mCjW5wTGh6VhRdwPueh1oRzi6ezkA8A==",
|
||||
"dependencies": {
|
||||
"@types/mdx": "^2.0.0"
|
||||
},
|
||||
|
||||
@@ -13,6 +13,7 @@ x-server-build: &server-common
|
||||
- DB_PASSWORD=postgres
|
||||
- DB_DATABASE_NAME=immich
|
||||
- REDIS_HOSTNAME=redis
|
||||
- IMMICH_MACHINE_LEARNING_ENABLED=false
|
||||
volumes:
|
||||
- upload:/usr/src/app/upload
|
||||
depends_on:
|
||||
@@ -26,23 +27,22 @@ services:
|
||||
ports:
|
||||
- 2283:3001
|
||||
|
||||
immich-microservices:
|
||||
command: [ "./start.sh", "microservices" ]
|
||||
<<: *server-common
|
||||
|
||||
# immich-microservices:
|
||||
# command: [ "./start.sh", "microservices" ]
|
||||
# <<: *server-common
|
||||
|
||||
redis:
|
||||
image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
|
||||
restart: always
|
||||
|
||||
database:
|
||||
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
||||
command: -c fsync=off -c shared_preload_libraries=vectors.so
|
||||
environment:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: immich
|
||||
ports:
|
||||
- 5432:5432
|
||||
- 5433:5432
|
||||
|
||||
volumes:
|
||||
model-cache:
|
||||
2388
e2e/package-lock.json
generated
2388
e2e/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,18 +5,24 @@
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "npx playwright test",
|
||||
"build": "tsc"
|
||||
"test": "vitest --config vitest.config.ts",
|
||||
"test:web": "npx playwright test",
|
||||
"start:web": "npx playwright test --ui"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"devDependencies": {
|
||||
"@immich/cli": "file:../cli",
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@playwright/test": "^1.41.2",
|
||||
"@types/node": "^20.11.17",
|
||||
"@types/pg": "^8.11.0",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@vitest/coverage-v8": "^1.3.0",
|
||||
"pg": "^8.11.3",
|
||||
"typescript": "^5.3.3"
|
||||
"supertest": "^6.3.4",
|
||||
"typescript": "^5.3.3",
|
||||
"vitest": "^1.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './specs/',
|
||||
testDir: './src/web/specs',
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
@@ -53,8 +53,7 @@ export default defineConfig({
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command:
|
||||
'docker compose -f ../docker/docker-compose.e2e.yml up --build -V --remove-orphans',
|
||||
command: 'docker compose up --build -V --remove-orphans',
|
||||
url: 'http://127.0.0.1:2283',
|
||||
reuseExistingServer: true,
|
||||
},
|
||||
|
||||
291
e2e/src/api/specs/auth.e2e-spec.ts
Normal file
291
e2e/src/api/specs/auth.e2e-spec.ts
Normal file
@@ -0,0 +1,291 @@
|
||||
import {
|
||||
LoginResponseDto,
|
||||
getAuthDevices,
|
||||
login,
|
||||
signUpAdmin,
|
||||
} from '@immich/sdk';
|
||||
import { loginDto, signupDto, uuidDto } from 'src/fixtures';
|
||||
import {
|
||||
deviceDto,
|
||||
errorDto,
|
||||
loginResponseDto,
|
||||
signupResponseDto,
|
||||
} from 'src/responses';
|
||||
import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
const { name, email, password } = signupDto.admin;
|
||||
|
||||
describe(`/auth/admin-sign-up`, () => {
|
||||
beforeAll(() => {
|
||||
apiUtils.setup();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await dbUtils.reset();
|
||||
});
|
||||
|
||||
describe('POST /auth/admin-sign-up', () => {
|
||||
const invalid = [
|
||||
{
|
||||
should: 'require an email address',
|
||||
data: { name, password },
|
||||
},
|
||||
{
|
||||
should: 'require a password',
|
||||
data: { name, email },
|
||||
},
|
||||
{
|
||||
should: 'require a name',
|
||||
data: { email, password },
|
||||
},
|
||||
{
|
||||
should: 'require a valid email',
|
||||
data: { name, email: 'immich', password },
|
||||
},
|
||||
];
|
||||
|
||||
for (const { should, data } of invalid) {
|
||||
it(`should ${should}`, async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/auth/admin-sign-up')
|
||||
.send(data);
|
||||
expect(status).toEqual(400);
|
||||
expect(body).toEqual(errorDto.badRequest());
|
||||
});
|
||||
}
|
||||
|
||||
it(`should sign up the admin`, async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/auth/admin-sign-up')
|
||||
.send(signupDto.admin);
|
||||
expect(status).toBe(201);
|
||||
expect(body).toEqual(signupResponseDto.admin);
|
||||
});
|
||||
|
||||
it('should sign up the admin with a local domain', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/auth/admin-sign-up')
|
||||
.send({ ...signupDto.admin, email: 'admin@local' });
|
||||
expect(status).toEqual(201);
|
||||
expect(body).toEqual({
|
||||
...signupResponseDto.admin,
|
||||
email: 'admin@local',
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform email to lower case', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/auth/admin-sign-up')
|
||||
.send({ ...signupDto.admin, email: 'aDmIn@IMMICH.cloud' });
|
||||
expect(status).toEqual(201);
|
||||
expect(body).toEqual(signupResponseDto.admin);
|
||||
});
|
||||
|
||||
it('should not allow a second admin to sign up', async () => {
|
||||
await signUpAdmin({ signUpDto: signupDto.admin });
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.post('/auth/admin-sign-up')
|
||||
.send(signupDto.admin);
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.alreadyHasAdmin);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/auth/*', () => {
|
||||
let admin: LoginResponseDto;
|
||||
|
||||
beforeEach(async () => {
|
||||
await dbUtils.reset();
|
||||
await signUpAdmin({ signUpDto: signupDto.admin });
|
||||
admin = await login({ loginCredentialDto: loginDto.admin });
|
||||
});
|
||||
|
||||
describe(`POST /auth/login`, () => {
|
||||
it('should reject an incorrect password', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/auth/login')
|
||||
.send({ email, password: 'incorrect' });
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.incorrectLogin);
|
||||
});
|
||||
|
||||
for (const key of Object.keys(loginDto.admin)) {
|
||||
it(`should not allow null ${key}`, async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/auth/login')
|
||||
.send({ ...loginDto.admin, [key]: null });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest());
|
||||
});
|
||||
}
|
||||
|
||||
it('should accept a correct password', async () => {
|
||||
const { status, body, headers } = await request(app)
|
||||
.post('/auth/login')
|
||||
.send({ email, password });
|
||||
expect(status).toBe(201);
|
||||
expect(body).toEqual(loginResponseDto.admin);
|
||||
|
||||
const token = body.accessToken;
|
||||
expect(token).toBeDefined();
|
||||
|
||||
const cookies = headers['set-cookie'];
|
||||
expect(cookies).toHaveLength(3);
|
||||
expect(cookies[0]).toEqual(
|
||||
`immich_access_token=${token}; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;`
|
||||
);
|
||||
expect(cookies[1]).toEqual(
|
||||
'immich_auth_type=password; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;'
|
||||
);
|
||||
expect(cookies[2]).toEqual(
|
||||
'immich_is_authenticated=true; Path=/; Max-Age=34560000; SameSite=Lax;'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /auth/devices', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get('/auth/devices');
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should get a list of authorized devices', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/auth/devices')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual([deviceDto.current]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /auth/devices', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).delete(`/auth/devices`);
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should logout all devices (except the current one)', async () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await login({ loginCredentialDto: loginDto.admin });
|
||||
}
|
||||
|
||||
await expect(
|
||||
getAuthDevices({ headers: asBearerAuth(admin.accessToken) })
|
||||
).resolves.toHaveLength(6);
|
||||
|
||||
const { status } = await request(app)
|
||||
.delete(`/auth/devices`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(204);
|
||||
|
||||
await expect(
|
||||
getAuthDevices({ headers: asBearerAuth(admin.accessToken) })
|
||||
).resolves.toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should throw an error for a non-existent device id', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/auth/devices/${uuidDto.notFound}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest('Not found or no authDevice.delete access')
|
||||
);
|
||||
});
|
||||
|
||||
it('should logout a device', async () => {
|
||||
const [device] = await getAuthDevices({
|
||||
headers: asBearerAuth(admin.accessToken),
|
||||
});
|
||||
const { status } = await request(app)
|
||||
.delete(`/auth/devices/${device.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(204);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/auth/validateToken')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(response.body).toEqual(errorDto.invalidToken);
|
||||
expect(response.status).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /auth/validateToken', () => {
|
||||
it('should reject an invalid token', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post(`/auth/validateToken`)
|
||||
.set('Authorization', 'Bearer 123');
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.invalidToken);
|
||||
});
|
||||
|
||||
it('should accept a valid token', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post(`/auth/validateToken`)
|
||||
.send({})
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({ authStatus: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /auth/change-password', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post(`/auth/change-password`)
|
||||
.send({ password, newPassword: 'Password1234' });
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should require the current password', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post(`/auth/change-password`)
|
||||
.send({ password: 'wrong-password', newPassword: 'Password1234' })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.wrongPassword);
|
||||
});
|
||||
|
||||
it('should change the password', async () => {
|
||||
const { status } = await request(app)
|
||||
.post(`/auth/change-password`)
|
||||
.send({ password, newPassword: 'Password1234' })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
|
||||
await login({
|
||||
loginCredentialDto: {
|
||||
email: 'admin@immich.cloud',
|
||||
password: 'Password1234',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /auth/logout', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).post(`/auth/logout`);
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should logout the user', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post(`/auth/logout`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
successful: true,
|
||||
redirectUri: '/auth/login?autoLaunch=0',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
62
e2e/src/api/specs/download.e2e-spec.ts
Normal file
62
e2e/src/api/specs/download.e2e-spec.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { AssetResponseDto, LoginResponseDto } from '@immich/sdk';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { apiUtils, app, dbUtils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
describe('/download', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let asset1: AssetResponseDto;
|
||||
|
||||
beforeAll(async () => {
|
||||
apiUtils.setup();
|
||||
await dbUtils.reset();
|
||||
admin = await apiUtils.adminSetup();
|
||||
asset1 = await apiUtils.createAsset(admin.accessToken);
|
||||
});
|
||||
|
||||
describe('POST /download/info', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post(`/download/info`)
|
||||
.send({ assetIds: [asset1.id] });
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should download info', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/download/info')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ assetIds: [asset1.id] });
|
||||
|
||||
expect(status).toBe(201);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({
|
||||
archives: [expect.objectContaining({ assetIds: [asset1.id] })],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /download/asset/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).post(
|
||||
`/download/asset/${asset1.id}`
|
||||
);
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should download file', async () => {
|
||||
const response = await request(app)
|
||||
.post(`/download/asset/${asset1.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers['content-type']).toEqual('image/jpeg');
|
||||
});
|
||||
});
|
||||
});
|
||||
30
e2e/src/api/specs/oauth.e2e-spec.ts
Normal file
30
e2e/src/api/specs/oauth.e2e-spec.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { errorDto } from 'src/responses';
|
||||
import { apiUtils, app, dbUtils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
describe(`/oauth`, () => {
|
||||
beforeAll(() => {
|
||||
apiUtils.setup();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await dbUtils.reset();
|
||||
await apiUtils.adminSetup();
|
||||
});
|
||||
|
||||
describe('POST /oauth/authorize', () => {
|
||||
it(`should throw an error if a redirect uri is not provided`, async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/oauth/authorize')
|
||||
.send({});
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest([
|
||||
'redirectUri must be a string',
|
||||
'redirectUri should not be empty',
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,68 +1,63 @@
|
||||
import { LoginResponseDto, PartnerDirection } from '@app/domain';
|
||||
import { PartnerController } from '@app/immich';
|
||||
import { errorStub, userDto } from '@test/fixtures';
|
||||
import { LoginResponseDto, createPartner } from '@immich/sdk';
|
||||
import { createUserDto } from 'src/fixtures';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { api } from '../../client';
|
||||
import { testApp } from '../utils';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
describe(`${PartnerController.name} (e2e)`, () => {
|
||||
let server: any;
|
||||
describe('/partner', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let user1: LoginResponseDto;
|
||||
let user2: LoginResponseDto;
|
||||
let user3: LoginResponseDto;
|
||||
|
||||
beforeAll(async () => {
|
||||
server = (await testApp.create()).getHttpServer();
|
||||
apiUtils.setup();
|
||||
await dbUtils.reset();
|
||||
|
||||
await testApp.reset();
|
||||
await api.authApi.adminSignUp(server);
|
||||
const admin = await api.authApi.adminLogin(server);
|
||||
|
||||
await Promise.all([
|
||||
api.userApi.create(server, admin.accessToken, userDto.user1),
|
||||
api.userApi.create(server, admin.accessToken, userDto.user2),
|
||||
api.userApi.create(server, admin.accessToken, userDto.user3),
|
||||
]);
|
||||
admin = await apiUtils.adminSetup();
|
||||
|
||||
[user1, user2, user3] = await Promise.all([
|
||||
api.authApi.login(server, userDto.user1),
|
||||
api.authApi.login(server, userDto.user2),
|
||||
api.authApi.login(server, userDto.user3),
|
||||
apiUtils.userSetup(admin.accessToken, createUserDto.user1),
|
||||
apiUtils.userSetup(admin.accessToken, createUserDto.user2),
|
||||
apiUtils.userSetup(admin.accessToken, createUserDto.user3),
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
api.partnerApi.create(server, user1.accessToken, user2.userId),
|
||||
api.partnerApi.create(server, user2.accessToken, user1.userId),
|
||||
createPartner(
|
||||
{ id: user2.userId },
|
||||
{ headers: asBearerAuth(user1.accessToken) }
|
||||
),
|
||||
createPartner(
|
||||
{ id: user1.userId },
|
||||
{ headers: asBearerAuth(user2.accessToken) }
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testApp.teardown();
|
||||
});
|
||||
|
||||
describe('GET /partner', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(server).get('/partner');
|
||||
const { status, body } = await request(app).get('/partner');
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorStub.unauthorized);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should get all partners shared by user', async () => {
|
||||
const { status, body } = await request(server)
|
||||
const { status, body } = await request(app)
|
||||
.get('/partner')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.query({ direction: PartnerDirection.SharedBy });
|
||||
.query({ direction: 'shared-by' });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual([expect.objectContaining({ id: user2.userId })]);
|
||||
});
|
||||
|
||||
it('should get all partners that share with user', async () => {
|
||||
const { status, body } = await request(server)
|
||||
const { status, body } = await request(app)
|
||||
.get('/partner')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.query({ direction: PartnerDirection.SharedWith });
|
||||
.query({ direction: 'shared-with' });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual([expect.objectContaining({ id: user2.userId })]);
|
||||
@@ -71,14 +66,16 @@ describe(`${PartnerController.name} (e2e)`, () => {
|
||||
|
||||
describe('POST /partner/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(server).post(`/partner/${user3.userId}`);
|
||||
const { status, body } = await request(app).post(
|
||||
`/partner/${user3.userId}`
|
||||
);
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorStub.unauthorized);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should share with new partner', async () => {
|
||||
const { status, body } = await request(server)
|
||||
const { status, body } = await request(app)
|
||||
.post(`/partner/${user3.userId}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
|
||||
@@ -87,44 +84,52 @@ describe(`${PartnerController.name} (e2e)`, () => {
|
||||
});
|
||||
|
||||
it('should not share with new partner if already sharing with this partner', async () => {
|
||||
const { status, body } = await request(server)
|
||||
const { status, body } = await request(app)
|
||||
.post(`/partner/${user2.userId}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(expect.objectContaining({ message: 'Partner already exists' }));
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({ message: 'Partner already exists' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /partner/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(server).put(`/partner/${user2.userId}`);
|
||||
const { status, body } = await request(app).put(
|
||||
`/partner/${user2.userId}`
|
||||
);
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorStub.unauthorized);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should update partner', async () => {
|
||||
const { status, body } = await request(server)
|
||||
const { status, body } = await request(app)
|
||||
.put(`/partner/${user2.userId}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({ inTimeline: false });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(expect.objectContaining({ id: user2.userId, inTimeline: false }));
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({ id: user2.userId, inTimeline: false })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /partner/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(server).delete(`/partner/${user3.userId}`);
|
||||
const { status, body } = await request(app).delete(
|
||||
`/partner/${user3.userId}`
|
||||
);
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorStub.unauthorized);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should delete partner', async () => {
|
||||
const { status } = await request(server)
|
||||
const { status } = await request(app)
|
||||
.delete(`/partner/${user3.userId}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
|
||||
@@ -132,12 +137,14 @@ describe(`${PartnerController.name} (e2e)`, () => {
|
||||
});
|
||||
|
||||
it('should throw a bad request if partner not found', async () => {
|
||||
const { status, body } = await request(server)
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/partner/${user3.userId}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(expect.objectContaining({ message: 'Partner not found' }));
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({ message: 'Partner not found' })
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,38 +1,30 @@
|
||||
import { LoginResponseDto } from '@app/domain';
|
||||
import { ServerInfoController } from '@app/immich';
|
||||
import { errorStub, userDto } from '@test/fixtures';
|
||||
import { LoginResponseDto, getServerConfig } from '@immich/sdk';
|
||||
import { createUserDto } from 'src/fixtures';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { apiUtils, app, dbUtils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { api } from '../../client';
|
||||
import { testApp } from '../utils';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
describe(`${ServerInfoController.name} (e2e)`, () => {
|
||||
let server: any;
|
||||
describe('/server-info', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let nonAdmin: LoginResponseDto;
|
||||
|
||||
beforeAll(async () => {
|
||||
server = (await testApp.create()).getHttpServer();
|
||||
|
||||
await testApp.reset();
|
||||
await api.authApi.adminSignUp(server);
|
||||
admin = await api.authApi.adminLogin(server);
|
||||
await api.userApi.create(server, admin.accessToken, userDto.user1);
|
||||
nonAdmin = await api.authApi.login(server, userDto.user1);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testApp.teardown();
|
||||
apiUtils.setup();
|
||||
await dbUtils.reset();
|
||||
admin = await apiUtils.adminSetup({ onboarding: false });
|
||||
nonAdmin = await apiUtils.userSetup(admin.accessToken, createUserDto.user1);
|
||||
});
|
||||
|
||||
describe('GET /server-info', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(server).get('/server-info');
|
||||
const { status, body } = await request(app).get('/server-info');
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorStub.unauthorized);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should return the disk information', async () => {
|
||||
const { status, body } = await request(server)
|
||||
const { status, body } = await request(app)
|
||||
.get('/server-info')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
@@ -50,7 +42,7 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
|
||||
|
||||
describe('GET /server-info/ping', () => {
|
||||
it('should respond with pong', async () => {
|
||||
const { status, body } = await request(server).get('/server-info/ping');
|
||||
const { status, body } = await request(app).get('/server-info/ping');
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({ res: 'pong' });
|
||||
});
|
||||
@@ -58,7 +50,7 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
|
||||
|
||||
describe('GET /server-info/version', () => {
|
||||
it('should respond with the server version', async () => {
|
||||
const { status, body } = await request(server).get('/server-info/version');
|
||||
const { status, body } = await request(app).get('/server-info/version');
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
major: expect.any(Number),
|
||||
@@ -70,12 +62,12 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
|
||||
|
||||
describe('GET /server-info/features', () => {
|
||||
it('should respond with the server features', async () => {
|
||||
const { status, body } = await request(server).get('/server-info/features');
|
||||
const { status, body } = await request(app).get('/server-info/features');
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
smartSearch: true,
|
||||
smartSearch: false,
|
||||
configFile: false,
|
||||
facialRecognition: true,
|
||||
facialRecognition: false,
|
||||
map: true,
|
||||
reverseGeocoding: true,
|
||||
oauth: false,
|
||||
@@ -90,7 +82,7 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
|
||||
|
||||
describe('GET /server-info/config', () => {
|
||||
it('should respond with the server configuration', async () => {
|
||||
const { status, body } = await request(server).get('/server-info/config');
|
||||
const { status, body } = await request(app).get('/server-info/config');
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
loginPageMessage: '',
|
||||
@@ -105,21 +97,23 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
|
||||
|
||||
describe('GET /server-info/statistics', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(server).get('/server-info/statistics');
|
||||
const { status, body } = await request(app).get(
|
||||
'/server-info/statistics'
|
||||
);
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorStub.unauthorized);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should only work for admins', async () => {
|
||||
const { status, body } = await request(server)
|
||||
const { status, body } = await request(app)
|
||||
.get('/server-info/statistics')
|
||||
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
||||
expect(status).toBe(403);
|
||||
expect(body).toEqual(errorStub.forbidden);
|
||||
expect(body).toEqual(errorDto.forbidden);
|
||||
});
|
||||
|
||||
it('should return the server stats', async () => {
|
||||
const { status, body } = await request(server)
|
||||
const { status, body } = await request(app)
|
||||
.get('/server-info/statistics')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
@@ -151,7 +145,9 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
|
||||
|
||||
describe('GET /server-info/media-types', () => {
|
||||
it('should return accepted media types', async () => {
|
||||
const { status, body } = await request(server).get('/server-info/media-types');
|
||||
const { status, body } = await request(app).get(
|
||||
'/server-info/media-types'
|
||||
);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
sidecar: ['.xmp'],
|
||||
@@ -163,7 +159,7 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
|
||||
|
||||
describe('GET /server-info/theme', () => {
|
||||
it('should respond with the server theme', async () => {
|
||||
const { status, body } = await request(server).get('/server-info/theme');
|
||||
const { status, body } = await request(app).get('/server-info/theme');
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
customCss: '',
|
||||
@@ -173,15 +169,15 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
|
||||
|
||||
describe('POST /server-info/admin-onboarding', () => {
|
||||
it('should set admin onboarding', async () => {
|
||||
const config = await api.serverInfoApi.getConfig(server);
|
||||
const config = await getServerConfig({});
|
||||
expect(config.isOnboarded).toBe(false);
|
||||
|
||||
const { status } = await request(server)
|
||||
const { status } = await request(app)
|
||||
.post('/server-info/admin-onboarding')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(204);
|
||||
|
||||
const newConfig = await api.serverInfoApi.getConfig(server);
|
||||
const newConfig = await getServerConfig({});
|
||||
expect(newConfig.isOnboarded).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,49 +1,47 @@
|
||||
import { LoginResponseDto } from '@app/domain';
|
||||
import { SystemConfigController } from '@app/immich';
|
||||
import { errorStub, userDto } from '@test/fixtures';
|
||||
import { LoginResponseDto } from '@immich/sdk';
|
||||
import { createUserDto } from 'src/fixtures';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { apiUtils, app, dbUtils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { api } from '../../client';
|
||||
import { testApp } from '../utils';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
describe(`${SystemConfigController.name} (e2e)`, () => {
|
||||
let server: any;
|
||||
describe('/system-config', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let nonAdmin: LoginResponseDto;
|
||||
|
||||
beforeAll(async () => {
|
||||
server = (await testApp.create()).getHttpServer();
|
||||
|
||||
await testApp.reset();
|
||||
await api.authApi.adminSignUp(server);
|
||||
admin = await api.authApi.adminLogin(server);
|
||||
await api.userApi.create(server, admin.accessToken, userDto.user1);
|
||||
nonAdmin = await api.authApi.login(server, userDto.user1);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testApp.teardown();
|
||||
apiUtils.setup();
|
||||
await dbUtils.reset();
|
||||
admin = await apiUtils.adminSetup();
|
||||
nonAdmin = await apiUtils.userSetup(admin.accessToken, createUserDto.user1);
|
||||
});
|
||||
|
||||
describe('GET /system-config/map/style.json', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(server).get('/system-config/map/style.json');
|
||||
const { status, body } = await request(app).get(
|
||||
'/system-config/map/style.json'
|
||||
);
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorStub.unauthorized);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should throw an error if a theme is not light or dark', async () => {
|
||||
for (const theme of ['dark1', true, 123, '', null, undefined]) {
|
||||
const { status, body } = await request(server)
|
||||
const { status, body } = await request(app)
|
||||
.get('/system-config/map/style.json')
|
||||
.query({ theme })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorStub.badRequest(['theme must be one of the following values: light, dark']));
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest([
|
||||
'theme must be one of the following values: light, dark',
|
||||
])
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should return the light style.json', async () => {
|
||||
const { status, body } = await request(server)
|
||||
const { status, body } = await request(app)
|
||||
.get('/system-config/map/style.json')
|
||||
.query({ theme: 'light' })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
@@ -52,7 +50,7 @@ describe(`${SystemConfigController.name} (e2e)`, () => {
|
||||
});
|
||||
|
||||
it('should return the dark style.json', async () => {
|
||||
const { status, body } = await request(server)
|
||||
const { status, body } = await request(app)
|
||||
.get('/system-config/map/style.json')
|
||||
.query({ theme: 'dark' })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
@@ -61,7 +59,7 @@ describe(`${SystemConfigController.name} (e2e)`, () => {
|
||||
});
|
||||
|
||||
it('should not require admin authentication', async () => {
|
||||
const { status, body } = await request(server)
|
||||
const { status, body } = await request(app)
|
||||
.get('/system-config/map/style.json')
|
||||
.query({ theme: 'dark' })
|
||||
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
||||
323
e2e/src/api/specs/user.e2e-spec.ts
Normal file
323
e2e/src/api/specs/user.e2e-spec.ts
Normal file
@@ -0,0 +1,323 @@
|
||||
import {
|
||||
LoginResponseDto,
|
||||
UserResponseDto,
|
||||
createUser,
|
||||
deleteUser,
|
||||
getUserById,
|
||||
} from '@immich/sdk';
|
||||
import { createUserDto, userDto } from 'src/fixtures';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
describe('/server-info', () => {
|
||||
let admin: LoginResponseDto;
|
||||
|
||||
beforeAll(async () => {
|
||||
apiUtils.setup();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await dbUtils.reset();
|
||||
admin = await apiUtils.adminSetup({ onboarding: false });
|
||||
});
|
||||
|
||||
describe('GET /user', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get('/user');
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should start with the admin', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/user')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toEqual(200);
|
||||
expect(body).toHaveLength(1);
|
||||
expect(body[0]).toMatchObject({ email: 'admin@immich.cloud' });
|
||||
});
|
||||
|
||||
it('should hide deleted users', async () => {
|
||||
const user1 = await apiUtils.userSetup(
|
||||
admin.accessToken,
|
||||
createUserDto.user1
|
||||
);
|
||||
await deleteUser(
|
||||
{ id: user1.userId },
|
||||
{ headers: asBearerAuth(admin.accessToken) }
|
||||
);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.get(`/user`)
|
||||
.query({ isAll: true })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toHaveLength(1);
|
||||
expect(body[0]).toMatchObject({ email: 'admin@immich.cloud' });
|
||||
});
|
||||
|
||||
it('should include deleted users', async () => {
|
||||
const user1 = await apiUtils.userSetup(
|
||||
admin.accessToken,
|
||||
createUserDto.user1
|
||||
);
|
||||
await deleteUser(
|
||||
{ id: user1.userId },
|
||||
{ headers: asBearerAuth(admin.accessToken) }
|
||||
);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.get(`/user`)
|
||||
.query({ isAll: false })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toHaveLength(2);
|
||||
expect(body[0]).toMatchObject({
|
||||
id: user1.userId,
|
||||
email: 'user1@immich.cloud',
|
||||
deletedAt: expect.any(String),
|
||||
});
|
||||
expect(body[1]).toMatchObject({
|
||||
id: admin.userId,
|
||||
email: 'admin@immich.cloud',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /user/info/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status } = await request(app).get(`/user/info/${admin.userId}`);
|
||||
expect(status).toEqual(401);
|
||||
});
|
||||
|
||||
it('should get the user info', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get(`/user/info/${admin.userId}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toMatchObject({
|
||||
id: admin.userId,
|
||||
email: 'admin@immich.cloud',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /user/me', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get(`/user/me`);
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should get my info', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get(`/user/me`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toMatchObject({
|
||||
id: admin.userId,
|
||||
email: 'admin@immich.cloud',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /user', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post(`/user`)
|
||||
.send(createUserDto.user1);
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
for (const key of Object.keys(createUserDto.user1)) {
|
||||
it(`should not allow null ${key}`, async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post(`/user`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ ...createUserDto.user1, [key]: null });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest());
|
||||
});
|
||||
}
|
||||
|
||||
it('should ignore `isAdmin`', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post(`/user`)
|
||||
.send({
|
||||
isAdmin: true,
|
||||
email: 'user1@immich.cloud',
|
||||
password: 'Password123',
|
||||
name: 'Immich',
|
||||
})
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(body).toMatchObject({
|
||||
email: 'user1@immich.cloud',
|
||||
isAdmin: false,
|
||||
shouldChangePassword: true,
|
||||
});
|
||||
expect(status).toBe(201);
|
||||
});
|
||||
|
||||
it('should create a user without memories enabled', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post(`/user`)
|
||||
.send({
|
||||
email: 'no-memories@immich.cloud',
|
||||
password: 'Password123',
|
||||
name: 'No Memories',
|
||||
memoriesEnabled: false,
|
||||
})
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(body).toMatchObject({
|
||||
email: 'no-memories@immich.cloud',
|
||||
memoriesEnabled: false,
|
||||
});
|
||||
expect(status).toBe(201);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /user/:id', () => {
|
||||
let userToDelete: UserResponseDto;
|
||||
|
||||
beforeEach(async () => {
|
||||
userToDelete = await createUser(
|
||||
{ createUserDto: createUserDto.user1 },
|
||||
{ headers: asBearerAuth(admin.accessToken) }
|
||||
);
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).delete(
|
||||
`/user/${userToDelete.id}`
|
||||
);
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should delete user', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/user/${userToDelete.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
...userToDelete,
|
||||
updatedAt: expect.any(String),
|
||||
deletedAt: expect.any(String),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /user', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).put(`/user`);
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
for (const key of Object.keys(userDto.admin)) {
|
||||
it(`should not allow null ${key}`, async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/user`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ ...userDto.admin, [key]: null });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest());
|
||||
});
|
||||
}
|
||||
|
||||
it('should not allow a non-admin to become an admin', async () => {
|
||||
const user = await apiUtils.userSetup(
|
||||
admin.accessToken,
|
||||
createUserDto.user1
|
||||
);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.put(`/user`)
|
||||
.send({ isAdmin: true, id: user.userId })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.alreadyHasAdmin);
|
||||
});
|
||||
|
||||
it('ignores updates to profileImagePath', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/user`)
|
||||
.send({ id: admin.userId, profileImagePath: 'invalid.jpg' })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toMatchObject({ id: admin.userId, profileImagePath: '' });
|
||||
});
|
||||
|
||||
it('should ignore updates to createdAt, updatedAt and deletedAt', async () => {
|
||||
const before = await getUserById(
|
||||
{ id: admin.userId },
|
||||
{ headers: asBearerAuth(admin.accessToken) }
|
||||
);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.put(`/user`)
|
||||
.send({
|
||||
id: admin.userId,
|
||||
createdAt: '2023-01-01T00:00:00.000Z',
|
||||
updatedAt: '2023-01-01T00:00:00.000Z',
|
||||
deletedAt: '2023-01-01T00:00:00.000Z',
|
||||
})
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toStrictEqual(before);
|
||||
});
|
||||
|
||||
it('should update first and last name', async () => {
|
||||
const before = await getUserById(
|
||||
{ id: admin.userId },
|
||||
{ headers: asBearerAuth(admin.accessToken) }
|
||||
);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.put(`/user`)
|
||||
.send({
|
||||
id: admin.userId,
|
||||
name: 'Name',
|
||||
})
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
...before,
|
||||
updatedAt: expect.any(String),
|
||||
name: 'Name',
|
||||
});
|
||||
expect(before.updatedAt).not.toEqual(body.updatedAt);
|
||||
});
|
||||
|
||||
it('should update memories enabled', async () => {
|
||||
const before = await getUserById(
|
||||
{ id: admin.userId },
|
||||
{ headers: asBearerAuth(admin.accessToken) }
|
||||
);
|
||||
const { status, body } = await request(app)
|
||||
.put(`/user`)
|
||||
.send({
|
||||
id: admin.userId,
|
||||
memoriesEnabled: false,
|
||||
})
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toMatchObject({
|
||||
...before,
|
||||
updatedAt: expect.anything(),
|
||||
memoriesEnabled: false,
|
||||
});
|
||||
expect(before.updatedAt).not.toEqual(body.updatedAt);
|
||||
});
|
||||
});
|
||||
});
|
||||
58
e2e/src/cli/specs/login.e2e-spec.ts
Normal file
58
e2e/src/cli/specs/login.e2e-spec.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { stat } from 'node:fs/promises';
|
||||
import { apiUtils, app, dbUtils, immichCli } from 'src/utils';
|
||||
import { beforeEach, beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
describe(`immich login-key`, () => {
|
||||
beforeAll(() => {
|
||||
apiUtils.setup();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await dbUtils.reset();
|
||||
});
|
||||
|
||||
it('should require a url', async () => {
|
||||
const { stderr, exitCode } = await immichCli(['login-key']);
|
||||
expect(stderr).toBe("error: missing required argument 'url'");
|
||||
expect(exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it('should require a key', async () => {
|
||||
const { stderr, exitCode } = await immichCli(['login-key', app]);
|
||||
expect(stderr).toBe("error: missing required argument 'key'");
|
||||
expect(exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it('should require a valid key', async () => {
|
||||
const { stderr, exitCode } = await immichCli([
|
||||
'login-key',
|
||||
app,
|
||||
'immich-is-so-cool',
|
||||
]);
|
||||
expect(stderr).toContain(
|
||||
'Failed to connect to server http://127.0.0.1:2283/api: Error: 401'
|
||||
);
|
||||
expect(exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it('should login', async () => {
|
||||
const admin = await apiUtils.adminSetup();
|
||||
const key = await apiUtils.createApiKey(admin.accessToken);
|
||||
const { stdout, stderr, exitCode } = await immichCli([
|
||||
'login-key',
|
||||
app,
|
||||
`${key.secret}`,
|
||||
]);
|
||||
expect(stdout.split('\n')).toEqual([
|
||||
'Logging in...',
|
||||
'Logged in as admin@immich.cloud',
|
||||
'Wrote auth info to /tmp/immich/auth.yml',
|
||||
]);
|
||||
expect(stderr).toBe('');
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
const stats = await stat('/tmp/immich/auth.yml');
|
||||
const mode = (stats.mode & 0o777).toString(8);
|
||||
expect(mode).toEqual('600');
|
||||
});
|
||||
});
|
||||
28
e2e/src/cli/specs/server-info.e2e-spec.ts
Normal file
28
e2e/src/cli/specs/server-info.e2e-spec.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { apiUtils, cliUtils, dbUtils, immichCli } from 'src/utils';
|
||||
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
describe(`immich server-info`, () => {
|
||||
beforeAll(() => {
|
||||
apiUtils.setup();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await dbUtils.reset();
|
||||
await cliUtils.login();
|
||||
});
|
||||
|
||||
it('should return the server info', async () => {
|
||||
const { stderr, stdout, exitCode } = await immichCli(['server-info']);
|
||||
expect(stdout.split('\n')).toEqual([
|
||||
expect.stringContaining('Server Version:'),
|
||||
expect.stringContaining('Image Types:'),
|
||||
expect.stringContaining('Video Types:'),
|
||||
'Statistics:',
|
||||
' Images: 0',
|
||||
' Videos: 0',
|
||||
' Total: 0',
|
||||
]);
|
||||
expect(stderr).toBe('');
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
178
e2e/src/cli/specs/upload.e2e-spec.ts
Normal file
178
e2e/src/cli/specs/upload.e2e-spec.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import { getAllAlbums, getAllAssets } from '@immich/sdk';
|
||||
import {
|
||||
apiUtils,
|
||||
asKeyAuth,
|
||||
cliUtils,
|
||||
dbUtils,
|
||||
immichCli,
|
||||
testAssetDir,
|
||||
} from 'src/utils';
|
||||
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { mkdir, readdir, rm, symlink } from 'fs/promises';
|
||||
|
||||
describe(`immich upload`, () => {
|
||||
let key: string;
|
||||
|
||||
beforeAll(() => {
|
||||
apiUtils.setup();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await dbUtils.reset();
|
||||
key = await cliUtils.login();
|
||||
});
|
||||
|
||||
describe('immich upload --recursive', () => {
|
||||
it('should upload a folder recursively', async () => {
|
||||
const { stderr, stdout, exitCode } = await immichCli([
|
||||
'upload',
|
||||
`${testAssetDir}/albums/nature/`,
|
||||
'--recursive',
|
||||
]);
|
||||
expect(stderr).toBe('');
|
||||
expect(stdout.split('\n')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining('Successfully uploaded 9 assets'),
|
||||
])
|
||||
);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.length).toBe(9);
|
||||
});
|
||||
});
|
||||
|
||||
describe('immich upload --recursive --album', () => {
|
||||
it('should create albums from folder names', async () => {
|
||||
const { stderr, stdout, exitCode } = await immichCli([
|
||||
'upload',
|
||||
`${testAssetDir}/albums/nature/`,
|
||||
'--recursive',
|
||||
'--album',
|
||||
]);
|
||||
expect(stdout.split('\n')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining('Successfully uploaded 9 assets'),
|
||||
expect.stringContaining('Successfully created 1 new album'),
|
||||
expect.stringContaining('Successfully updated 9 assets'),
|
||||
])
|
||||
);
|
||||
expect(stderr).toBe('');
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.length).toBe(9);
|
||||
|
||||
const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
|
||||
expect(albums.length).toBe(1);
|
||||
expect(albums[0].albumName).toBe('nature');
|
||||
});
|
||||
|
||||
it('should add existing assets to albums', async () => {
|
||||
const response1 = await immichCli([
|
||||
'upload',
|
||||
`${testAssetDir}/albums/nature/`,
|
||||
'--recursive',
|
||||
]);
|
||||
expect(response1.stdout.split('\n')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining('Successfully uploaded 9 assets'),
|
||||
])
|
||||
);
|
||||
expect(response1.stderr).toBe('');
|
||||
expect(response1.exitCode).toBe(0);
|
||||
|
||||
const assets1 = await getAllAssets({}, { headers: asKeyAuth(key) });
|
||||
expect(assets1.length).toBe(9);
|
||||
|
||||
const albums1 = await getAllAlbums({}, { headers: asKeyAuth(key) });
|
||||
expect(albums1.length).toBe(0);
|
||||
|
||||
const response2 = await immichCli([
|
||||
'upload',
|
||||
`${testAssetDir}/albums/nature/`,
|
||||
'--recursive',
|
||||
'--album',
|
||||
]);
|
||||
expect(response2.stdout.split('\n')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining(
|
||||
'All assets were already uploaded, nothing to do.'
|
||||
),
|
||||
expect.stringContaining('Successfully updated 9 assets'),
|
||||
])
|
||||
);
|
||||
expect(response2.stderr).toBe('');
|
||||
expect(response2.exitCode).toBe(0);
|
||||
|
||||
const assets2 = await getAllAssets({}, { headers: asKeyAuth(key) });
|
||||
expect(assets2.length).toBe(9);
|
||||
|
||||
const albums2 = await getAllAlbums({}, { headers: asKeyAuth(key) });
|
||||
expect(albums2.length).toBe(1);
|
||||
expect(albums2[0].albumName).toBe('nature');
|
||||
});
|
||||
});
|
||||
|
||||
describe('immich upload --recursive --album-name=e2e', () => {
|
||||
it('should create a named album', async () => {
|
||||
const { stderr, stdout, exitCode } = await immichCli([
|
||||
'upload',
|
||||
`${testAssetDir}/albums/nature/`,
|
||||
'--recursive',
|
||||
'--album-name=e2e',
|
||||
]);
|
||||
expect(stdout.split('\n')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining('Successfully uploaded 9 assets'),
|
||||
expect.stringContaining('Successfully created 1 new album'),
|
||||
expect.stringContaining('Successfully updated 9 assets'),
|
||||
])
|
||||
);
|
||||
expect(stderr).toBe('');
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.length).toBe(9);
|
||||
|
||||
const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
|
||||
expect(albums.length).toBe(1);
|
||||
expect(albums[0].albumName).toBe('e2e');
|
||||
});
|
||||
});
|
||||
|
||||
describe('immich upload --delete', () => {
|
||||
it('should delete local files if specified', async () => {
|
||||
await mkdir(`/tmp/albums/nature`, { recursive: true });
|
||||
const filesToLink = await readdir(`${testAssetDir}/albums/nature`);
|
||||
for (const file of filesToLink) {
|
||||
await symlink(
|
||||
`${testAssetDir}/albums/nature/${file}`,
|
||||
`/tmp/albums/nature/${file}`
|
||||
);
|
||||
}
|
||||
|
||||
const { stderr, stdout, exitCode } = await immichCli([
|
||||
'upload',
|
||||
`/tmp/albums/nature`,
|
||||
'--delete',
|
||||
]);
|
||||
|
||||
const files = await readdir(`/tmp/albums/nature`);
|
||||
await rm(`/tmp/albums/nature`, { recursive: true });
|
||||
expect(files).toEqual([]);
|
||||
|
||||
expect(stdout.split('\n')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining('Successfully uploaded 9 assets'),
|
||||
expect.stringContaining('Deleting assets that have been uploaded'),
|
||||
])
|
||||
);
|
||||
expect(stderr).toBe('');
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.length).toBe(9);
|
||||
});
|
||||
});
|
||||
});
|
||||
29
e2e/src/cli/specs/version.e2e-spec.ts
Normal file
29
e2e/src/cli/specs/version.e2e-spec.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { apiUtils, immichCli } from 'src/utils';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
const pkg = JSON.parse(readFileSync('../cli/package.json', 'utf8'));
|
||||
|
||||
describe(`immich --version`, () => {
|
||||
beforeAll(() => {
|
||||
apiUtils.setup();
|
||||
});
|
||||
|
||||
describe('immich --version', () => {
|
||||
it('should print the cli version', async () => {
|
||||
const { stdout, stderr, exitCode } = await immichCli(['--version']);
|
||||
expect(stdout).toEqual(pkg.version);
|
||||
expect(stderr).toEqual('');
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('immich -V', () => {
|
||||
it('should print the cli version', async () => {
|
||||
const { stdout, stderr, exitCode } = await immichCli(['-V']);
|
||||
expect(stdout).toEqual(pkg.version);
|
||||
expect(stderr).toEqual('');
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
80
e2e/src/fixtures.ts
Normal file
80
e2e/src/fixtures.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { UserAvatarColor } from '@immich/sdk';
|
||||
|
||||
export const uuidDto = {
|
||||
invalid: 'invalid-uuid',
|
||||
// valid uuid v4
|
||||
notFound: '00000000-0000-4000-a000-000000000000',
|
||||
};
|
||||
|
||||
const adminLoginDto = {
|
||||
email: 'admin@immich.cloud',
|
||||
password: 'password',
|
||||
};
|
||||
const adminSignupDto = { ...adminLoginDto, name: 'Immich Admin' };
|
||||
|
||||
export const loginDto = {
|
||||
admin: adminLoginDto,
|
||||
};
|
||||
|
||||
export const signupDto = {
|
||||
admin: adminSignupDto,
|
||||
};
|
||||
|
||||
export const createUserDto = {
|
||||
user1: {
|
||||
email: 'user1@immich.cloud',
|
||||
name: 'User 1',
|
||||
password: 'password1',
|
||||
},
|
||||
user2: {
|
||||
email: 'user2@immich.cloud',
|
||||
name: 'User 2',
|
||||
password: 'password12',
|
||||
},
|
||||
user3: {
|
||||
email: 'user3@immich.cloud',
|
||||
name: 'User 3',
|
||||
password: 'password123',
|
||||
},
|
||||
};
|
||||
|
||||
export const userDto = {
|
||||
admin: {
|
||||
name: signupDto.admin.name,
|
||||
email: signupDto.admin.email,
|
||||
password: signupDto.admin.password,
|
||||
storageLabel: 'admin',
|
||||
externalPath: null,
|
||||
oauthId: '',
|
||||
shouldChangePassword: false,
|
||||
profileImagePath: '',
|
||||
createdAt: new Date('2021-01-01'),
|
||||
deletedAt: null,
|
||||
updatedAt: new Date('2021-01-01'),
|
||||
tags: [],
|
||||
assets: [],
|
||||
memoriesEnabled: true,
|
||||
avatarColor: UserAvatarColor.Primary,
|
||||
quotaSizeInBytes: null,
|
||||
quotaUsageInBytes: 0,
|
||||
},
|
||||
user1: {
|
||||
name: createUserDto.user1.name,
|
||||
email: createUserDto.user1.email,
|
||||
password: createUserDto.user1.password,
|
||||
storageLabel: null,
|
||||
externalPath: null,
|
||||
oauthId: '',
|
||||
shouldChangePassword: false,
|
||||
profileImagePath: '',
|
||||
createdAt: new Date('2021-01-01'),
|
||||
deletedAt: null,
|
||||
updatedAt: new Date('2021-01-01'),
|
||||
tags: [],
|
||||
assets: [],
|
||||
memoriesEnabled: true,
|
||||
avatarColor: UserAvatarColor.Primary,
|
||||
quotaSizeInBytes: null,
|
||||
quotaUsageInBytes: 0,
|
||||
},
|
||||
};
|
||||
103
e2e/src/responses.ts
Normal file
103
e2e/src/responses.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { expect } from 'vitest';
|
||||
|
||||
export const errorDto = {
|
||||
unauthorized: {
|
||||
error: 'Unauthorized',
|
||||
statusCode: 401,
|
||||
message: 'Authentication required',
|
||||
},
|
||||
forbidden: {
|
||||
error: 'Forbidden',
|
||||
statusCode: 403,
|
||||
message: expect.any(String),
|
||||
},
|
||||
wrongPassword: {
|
||||
error: 'Bad Request',
|
||||
statusCode: 400,
|
||||
message: 'Wrong password',
|
||||
},
|
||||
invalidToken: {
|
||||
error: 'Unauthorized',
|
||||
statusCode: 401,
|
||||
message: 'Invalid user token',
|
||||
},
|
||||
invalidShareKey: {
|
||||
error: 'Unauthorized',
|
||||
statusCode: 401,
|
||||
message: 'Invalid share key',
|
||||
},
|
||||
invalidSharePassword: {
|
||||
error: 'Unauthorized',
|
||||
statusCode: 401,
|
||||
message: 'Invalid password',
|
||||
},
|
||||
badRequest: (message: any = null) => ({
|
||||
error: 'Bad Request',
|
||||
statusCode: 400,
|
||||
message: message ?? expect.anything(),
|
||||
}),
|
||||
noPermission: {
|
||||
error: 'Bad Request',
|
||||
statusCode: 400,
|
||||
message: expect.stringContaining('Not found or no'),
|
||||
},
|
||||
incorrectLogin: {
|
||||
error: 'Unauthorized',
|
||||
statusCode: 401,
|
||||
message: 'Incorrect email or password',
|
||||
},
|
||||
alreadyHasAdmin: {
|
||||
error: 'Bad Request',
|
||||
statusCode: 400,
|
||||
message: 'The server already has an admin',
|
||||
},
|
||||
noDeleteUploadLibrary: {
|
||||
error: 'Bad Request',
|
||||
statusCode: 400,
|
||||
message: 'Cannot delete the last upload library',
|
||||
},
|
||||
};
|
||||
|
||||
export const signupResponseDto = {
|
||||
admin: {
|
||||
avatarColor: expect.any(String),
|
||||
id: expect.any(String),
|
||||
name: 'Immich Admin',
|
||||
email: 'admin@immich.cloud',
|
||||
storageLabel: 'admin',
|
||||
externalPath: null,
|
||||
profileImagePath: '',
|
||||
// why? lol
|
||||
shouldChangePassword: true,
|
||||
isAdmin: true,
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
deletedAt: null,
|
||||
oauthId: '',
|
||||
memoriesEnabled: true,
|
||||
quotaUsageInBytes: 0,
|
||||
quotaSizeInBytes: null,
|
||||
},
|
||||
};
|
||||
|
||||
export const loginResponseDto = {
|
||||
admin: {
|
||||
accessToken: expect.any(String),
|
||||
name: 'Immich Admin',
|
||||
isAdmin: true,
|
||||
profileImagePath: '',
|
||||
shouldChangePassword: true,
|
||||
userEmail: 'admin@immich.cloud',
|
||||
userId: expect.any(String),
|
||||
},
|
||||
};
|
||||
export const deviceDto = {
|
||||
current: {
|
||||
id: expect.any(String),
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
current: true,
|
||||
deviceOS: '',
|
||||
deviceType: '',
|
||||
},
|
||||
};
|
||||
26
e2e/src/setup.ts
Normal file
26
e2e/src/setup.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { spawn, exec } from 'child_process';
|
||||
|
||||
export default async () => {
|
||||
let _resolve: () => unknown;
|
||||
const promise = new Promise<void>((resolve) => (_resolve = resolve));
|
||||
|
||||
const child = spawn('docker', ['compose', 'up'], { stdio: 'pipe' });
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
const input = data.toString();
|
||||
console.log(input);
|
||||
if (input.includes('Immich Server is listening')) {
|
||||
_resolve();
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => console.log(data.toString()));
|
||||
|
||||
await promise;
|
||||
|
||||
return async () => {
|
||||
await new Promise<void>((resolve) =>
|
||||
exec('docker compose down', () => resolve())
|
||||
);
|
||||
};
|
||||
};
|
||||
213
e2e/src/utils.ts
Normal file
213
e2e/src/utils.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import {
|
||||
AssetResponseDto,
|
||||
CreateAssetDto,
|
||||
CreateUserDto,
|
||||
LoginResponseDto,
|
||||
createApiKey,
|
||||
createUser,
|
||||
defaults,
|
||||
login,
|
||||
setAdminOnboarding,
|
||||
signUpAdmin,
|
||||
} from '@immich/sdk';
|
||||
import { BrowserContext } from '@playwright/test';
|
||||
import { spawn } from 'child_process';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { access } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import pg from 'pg';
|
||||
import { loginDto, signupDto } from 'src/fixtures';
|
||||
import request from 'supertest';
|
||||
|
||||
export const app = 'http://127.0.0.1:2283/api';
|
||||
|
||||
const directoryExists = (directory: string) =>
|
||||
access(directory)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
// TODO move test assets into e2e/assets
|
||||
export const testAssetDir = path.resolve(`./../server/test/assets/`);
|
||||
|
||||
if (!(await directoryExists(`${testAssetDir}/albums`))) {
|
||||
throw new Error(
|
||||
`Test assets not found. Please checkout https://github.com/immich-app/test-assets into ${testAssetDir} before testing`
|
||||
);
|
||||
}
|
||||
|
||||
const setBaseUrl = () => (defaults.baseUrl = app);
|
||||
export const asBearerAuth = (accessToken: string) => ({
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
});
|
||||
|
||||
export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
|
||||
|
||||
let client: pg.Client | null = null;
|
||||
|
||||
export const dbUtils = {
|
||||
reset: async () => {
|
||||
try {
|
||||
if (!client) {
|
||||
client = new pg.Client(
|
||||
'postgres://postgres:postgres@127.0.0.1:5433/immich'
|
||||
);
|
||||
await client.connect();
|
||||
}
|
||||
|
||||
for (const table of [
|
||||
'albums',
|
||||
'assets',
|
||||
'api_keys',
|
||||
'user_token',
|
||||
'users',
|
||||
'system_metadata',
|
||||
]) {
|
||||
await client.query(`DELETE FROM ${table} CASCADE;`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to reset database', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
teardown: async () => {
|
||||
try {
|
||||
if (client) {
|
||||
await client.end();
|
||||
client = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to teardown database', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
export interface CliResponse {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
exitCode: number | null;
|
||||
}
|
||||
|
||||
export const immichCli = async (args: string[]) => {
|
||||
let _resolve: (value: CliResponse) => void;
|
||||
const deferred = new Promise<CliResponse>((resolve) => (_resolve = resolve));
|
||||
const _args = ['node_modules/.bin/immich', '-d', '/tmp/immich/', ...args];
|
||||
const child = spawn('node', _args, {
|
||||
stdio: 'pipe',
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (data) => (stdout += data.toString()));
|
||||
child.stderr.on('data', (data) => (stderr += data.toString()));
|
||||
child.on('exit', (exitCode) => {
|
||||
_resolve({
|
||||
stdout: stdout.trim(),
|
||||
stderr: stderr.trim(),
|
||||
exitCode,
|
||||
});
|
||||
});
|
||||
|
||||
return deferred;
|
||||
};
|
||||
|
||||
export interface AdminSetupOptions {
|
||||
onboarding?: boolean;
|
||||
}
|
||||
|
||||
export const apiUtils = {
|
||||
setup: () => {
|
||||
setBaseUrl();
|
||||
},
|
||||
adminSetup: async (options?: AdminSetupOptions) => {
|
||||
options = options || { onboarding: true };
|
||||
|
||||
await signUpAdmin({ signUpDto: signupDto.admin });
|
||||
const response = await login({ loginCredentialDto: loginDto.admin });
|
||||
if (options.onboarding) {
|
||||
await setAdminOnboarding({ headers: asBearerAuth(response.accessToken) });
|
||||
}
|
||||
return response;
|
||||
},
|
||||
userSetup: async (accessToken: string, dto: CreateUserDto) => {
|
||||
await createUser(
|
||||
{ createUserDto: dto },
|
||||
{ headers: asBearerAuth(accessToken) }
|
||||
);
|
||||
return login({
|
||||
loginCredentialDto: { email: dto.email, password: dto.password },
|
||||
});
|
||||
},
|
||||
createApiKey: (accessToken: string) => {
|
||||
return createApiKey(
|
||||
{ apiKeyCreateDto: { name: 'e2e' } },
|
||||
{ headers: asBearerAuth(accessToken) }
|
||||
);
|
||||
},
|
||||
createAsset: async (
|
||||
accessToken: string,
|
||||
dto?: Omit<CreateAssetDto, 'assetData'>
|
||||
) => {
|
||||
dto = dto || {
|
||||
deviceAssetId: 'test-1',
|
||||
deviceId: 'test',
|
||||
fileCreatedAt: new Date().toISOString(),
|
||||
fileModifiedAt: new Date().toISOString(),
|
||||
};
|
||||
const { body } = await request(app)
|
||||
.post(`/asset/upload`)
|
||||
.field('deviceAssetId', dto.deviceAssetId)
|
||||
.field('deviceId', dto.deviceId)
|
||||
.field('fileCreatedAt', dto.fileCreatedAt)
|
||||
.field('fileModifiedAt', dto.fileModifiedAt)
|
||||
.attach('assetData', randomBytes(32), 'example.jpg')
|
||||
.set('Authorization', `Bearer ${accessToken}`);
|
||||
|
||||
return body as AssetResponseDto;
|
||||
},
|
||||
};
|
||||
|
||||
export const cliUtils = {
|
||||
login: async () => {
|
||||
const admin = await apiUtils.adminSetup();
|
||||
const key = await apiUtils.createApiKey(admin.accessToken);
|
||||
await immichCli(['login-key', app, `${key.secret}`]);
|
||||
return key.secret;
|
||||
},
|
||||
};
|
||||
|
||||
export const webUtils = {
|
||||
setAuthCookies: async (context: BrowserContext, accessToken: string) =>
|
||||
await context.addCookies([
|
||||
{
|
||||
name: 'immich_access_token',
|
||||
value: accessToken,
|
||||
domain: '127.0.0.1',
|
||||
path: '/',
|
||||
expires: 1742402728,
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
sameSite: 'Lax',
|
||||
},
|
||||
{
|
||||
name: 'immich_auth_type',
|
||||
value: 'password',
|
||||
domain: '127.0.0.1',
|
||||
path: '/',
|
||||
expires: 1742402728,
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
sameSite: 'Lax',
|
||||
},
|
||||
{
|
||||
name: 'immich_is_authenticated',
|
||||
value: 'true',
|
||||
domain: '127.0.0.1',
|
||||
path: '/',
|
||||
expires: 1742402728,
|
||||
httpOnly: false,
|
||||
secure: false,
|
||||
sameSite: 'Lax',
|
||||
},
|
||||
]),
|
||||
};
|
||||
@@ -1,13 +1,17 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { app } from '../test-utils';
|
||||
import { apiUtils, dbUtils, webUtils } from 'src/utils';
|
||||
|
||||
test.describe('Registration', () => {
|
||||
test.beforeAll(() => {
|
||||
apiUtils.setup();
|
||||
});
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await app.reset();
|
||||
await dbUtils.reset();
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await app.teardown();
|
||||
await dbUtils.teardown();
|
||||
});
|
||||
|
||||
test('admin registration', async ({ page }) => {
|
||||
@@ -41,7 +45,8 @@ test.describe('Registration', () => {
|
||||
});
|
||||
|
||||
test('user registration', async ({ context, page }) => {
|
||||
await app.adminSetup(context);
|
||||
const admin = await apiUtils.adminSetup();
|
||||
await webUtils.setAuthCookies(context, admin.accessToken);
|
||||
|
||||
// create user
|
||||
await page.goto('/admin/user-management');
|
||||
58
e2e/src/web/specs/shared-link.e2e-spec.ts
Normal file
58
e2e/src/web/specs/shared-link.e2e-spec.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
AlbumResponseDto,
|
||||
AssetResponseDto,
|
||||
LoginResponseDto,
|
||||
SharedLinkResponseDto,
|
||||
SharedLinkType,
|
||||
createAlbum,
|
||||
createSharedLink,
|
||||
} from '@immich/sdk';
|
||||
import { test } from '@playwright/test';
|
||||
import { apiUtils, asBearerAuth, dbUtils } from 'src/utils';
|
||||
|
||||
test.describe('Shared Links', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let asset: AssetResponseDto;
|
||||
let album: AlbumResponseDto;
|
||||
let sharedLink: SharedLinkResponseDto;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
apiUtils.setup();
|
||||
await dbUtils.reset();
|
||||
admin = await apiUtils.adminSetup();
|
||||
asset = await apiUtils.createAsset(admin.accessToken);
|
||||
album = await createAlbum(
|
||||
{
|
||||
createAlbumDto: {
|
||||
albumName: 'Test Album',
|
||||
assetIds: [asset.id],
|
||||
},
|
||||
},
|
||||
{ headers: asBearerAuth(admin.accessToken) }
|
||||
// { headers: asBearerAuth(admin.accessToken)},
|
||||
);
|
||||
sharedLink = await createSharedLink(
|
||||
{
|
||||
sharedLinkCreateDto: {
|
||||
type: SharedLinkType.Album,
|
||||
albumId: album.id,
|
||||
},
|
||||
},
|
||||
{ headers: asBearerAuth(admin.accessToken) }
|
||||
);
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await dbUtils.teardown();
|
||||
});
|
||||
|
||||
test('download from a shared link', async ({ page }) => {
|
||||
await page.goto(`/share/${sharedLink.key}`);
|
||||
await page.getByRole('heading', { name: 'Test Album' }).waitFor();
|
||||
await page.locator('.group > div').first().hover();
|
||||
await page.waitForSelector('#asset-group-by-date svg');
|
||||
await page.getByRole('checkbox').click();
|
||||
await page.getByRole('button', { name: 'Download' }).click();
|
||||
await page.getByText('DOWNLOADING').waitFor();
|
||||
});
|
||||
});
|
||||
@@ -1,88 +0,0 @@
|
||||
import pg from 'pg';
|
||||
import { defaults, login, setAdminOnboarding, signUpAdmin } from '@immich/sdk';
|
||||
import { BrowserContext } from '@playwright/test';
|
||||
|
||||
const client = new pg.Client(
|
||||
'postgres://postgres:postgres@localhost:5432/immich'
|
||||
);
|
||||
let connected = false;
|
||||
|
||||
const loginCredentialDto = {
|
||||
email: 'admin@immich.cloud',
|
||||
password: 'password',
|
||||
};
|
||||
const signUpDto = { ...loginCredentialDto, name: 'Immich Admin' };
|
||||
|
||||
const setBaseUrl = () => (defaults.baseUrl = 'http://127.0.0.1:2283/api');
|
||||
const asAuthHeader = (accessToken: string) => ({
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
});
|
||||
|
||||
export const app = {
|
||||
adminSetup: async (context: BrowserContext) => {
|
||||
setBaseUrl();
|
||||
await signUpAdmin({ signUpDto });
|
||||
|
||||
const response = await login({ loginCredentialDto });
|
||||
|
||||
await context.addCookies([
|
||||
{
|
||||
name: 'immich_access_token',
|
||||
value: response.accessToken,
|
||||
domain: '127.0.0.1',
|
||||
path: '/',
|
||||
expires: 1742402728,
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
sameSite: 'Lax',
|
||||
},
|
||||
{
|
||||
name: 'immich_auth_type',
|
||||
value: 'password',
|
||||
domain: '127.0.0.1',
|
||||
path: '/',
|
||||
expires: 1742402728,
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
sameSite: 'Lax',
|
||||
},
|
||||
{
|
||||
name: 'immich_is_authenticated',
|
||||
value: 'true',
|
||||
domain: '127.0.0.1',
|
||||
path: '/',
|
||||
expires: 1742402728,
|
||||
httpOnly: false,
|
||||
secure: false,
|
||||
sameSite: 'Lax',
|
||||
},
|
||||
]);
|
||||
|
||||
await setAdminOnboarding({ headers: asAuthHeader(response.accessToken) });
|
||||
|
||||
return response;
|
||||
},
|
||||
reset: async () => {
|
||||
try {
|
||||
if (!connected) {
|
||||
await client.connect();
|
||||
connected = true;
|
||||
}
|
||||
|
||||
for (const table of ['user_token', 'users', 'system_metadata']) {
|
||||
await client.query(`DELETE FROM ${table} CASCADE;`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to reset database', error);
|
||||
}
|
||||
},
|
||||
teardown: async () => {
|
||||
try {
|
||||
if (connected) {
|
||||
await client.end();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to teardown database', error);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -16,8 +16,7 @@
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"rootDirs": ["src"],
|
||||
"baseUrl": "./",
|
||||
"types": ["vitest/globals"]
|
||||
"baseUrl": "./"
|
||||
},
|
||||
"exclude": ["dist", "node_modules"]
|
||||
}
|
||||
|
||||
13
e2e/vitest.config.ts
Normal file
13
e2e/vitest.config.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ['src/{api,cli}/specs/*.e2e-spec.ts'],
|
||||
globalSetup: ['src/setup.ts'],
|
||||
poolOptions: {
|
||||
threads: {
|
||||
singleThread: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
16
install.sh
16
install.sh
@@ -1,15 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo "Starting Immich installation..."
|
||||
|
||||
ip_address=$(hostname -I | awk '{print $1}')
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\032[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
create_immich_directory() {
|
||||
echo "Creating Immich directory..."
|
||||
mkdir -p ./immich-app/immich-data
|
||||
cd ./immich-app
|
||||
cd ./immich-app || exit
|
||||
}
|
||||
|
||||
download_docker_compose_file() {
|
||||
@@ -34,18 +32,18 @@ replace_env_value() {
|
||||
populate_upload_location() {
|
||||
echo "Populating default UPLOAD_LOCATION value..."
|
||||
upload_location=$(pwd)/immich-data
|
||||
replace_env_value "UPLOAD_LOCATION" $upload_location
|
||||
replace_env_value "UPLOAD_LOCATION" "$upload_location"
|
||||
}
|
||||
|
||||
start_docker_compose() {
|
||||
echo "Starting Immich's docker containers"
|
||||
|
||||
if docker compose > /dev/null 2>&1; then
|
||||
if docker compose >/dev/null 2>&1; then
|
||||
docker_bin="docker compose"
|
||||
elif docker-compose > /dev/null 2>&1; then
|
||||
elif docker-compose >/dev/null 2>&1; then
|
||||
docker_bin="docker-compose"
|
||||
else
|
||||
echo 'Cannot find `docker compose` or `docker-compose`.'
|
||||
echo "Cannot find \`docker compose\` or \`docker-compose\`."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
g++ -shared -O3 -o libann.so -fuse-ld=gold -std=c++17 -I$ARMNN_PATH/include -larmnn -larmnnDeserializer -larmnnTfLiteParser -larmnnOnnxParser -L$ARMNN_PATH ann.cpp
|
||||
#!/usr/bin/env sh
|
||||
|
||||
g++ -shared -O3 -o libann.so -fuse-ld=gold -std=c++17 -I"$ARMNN_PATH"/include -larmnn -larmnnDeserializer -larmnnTfLiteParser -larmnnOnnxParser -L"$ARMNN_PATH" ann.cpp
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
#!/usr/bin/env sh
|
||||
|
||||
cd armnn-23.11/
|
||||
cd armnn-23.11/ || exit
|
||||
g++ -o ../armnnconverter -O1 -DARMNN_ONNX_PARSER -DARMNN_SERIALIZER -DARMNN_TF_LITE_PARSER -fuse-ld=gold -std=c++17 -Iinclude -Isrc/armnnUtils -Ithird-party -larmnn -larmnnDeserializer -larmnnTfLiteParser -larmnnOnnxParser -larmnnSerializer -L../armnn src/armnnConverter/ArmnnConverter.cpp
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "machine-learning"
|
||||
version = "1.94.1"
|
||||
version = "1.95.1"
|
||||
description = ""
|
||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
||||
readme = "README.md"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
export LD_PRELOAD="/usr/lib/$(arch)-linux-gnu/libmimalloc.so.2"
|
||||
lib_path="/usr/lib/$(arch)-linux-gnu/libmimalloc.so.2"
|
||||
export LD_PRELOAD="$lib_path"
|
||||
export LD_BIND_NOW=1
|
||||
|
||||
: "${MACHINE_LEARNING_HOST:=0.0.0.0}"
|
||||
@@ -10,8 +11,8 @@ export LD_BIND_NOW=1
|
||||
|
||||
gunicorn app.main:app \
|
||||
-k app.config.CustomUvicornWorker \
|
||||
-w $MACHINE_LEARNING_WORKERS \
|
||||
-b $MACHINE_LEARNING_HOST:$MACHINE_LEARNING_PORT \
|
||||
-t $MACHINE_LEARNING_WORKER_TIMEOUT \
|
||||
-w "$MACHINE_LEARNING_WORKERS" \
|
||||
-b "$MACHINE_LEARNING_HOST":"$MACHINE_LEARNING_PORT" \
|
||||
-t "$MACHINE_LEARNING_WORKER_TIMEOUT" \
|
||||
--log-config-json log_conf.json \
|
||||
--graceful-timeout 0
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#
|
||||
# Pump one or both of the server/mobile versions in appropriate files
|
||||
@@ -25,10 +25,10 @@ while getopts 's:m:' flag; do
|
||||
esac
|
||||
done
|
||||
|
||||
CURRENT_SERVER=$(cat server/package.json | jq -r '.version')
|
||||
MAJOR=$(echo $CURRENT_SERVER | cut -d '.' -f1)
|
||||
MINOR=$(echo $CURRENT_SERVER | cut -d '.' -f2)
|
||||
PATCH=$(echo $CURRENT_SERVER | cut -d '.' -f3)
|
||||
CURRENT_SERVER=$(jq -r '.version' server/package.json)
|
||||
MAJOR=$(echo "$CURRENT_SERVER" | cut -d '.' -f1)
|
||||
MINOR=$(echo "$CURRENT_SERVER" | cut -d '.' -f2)
|
||||
PATCH=$(echo "$CURRENT_SERVER" | cut -d '.' -f3)
|
||||
|
||||
if [[ $SERVER_PUMP == "major" ]]; then
|
||||
MAJOR=$((MAJOR + 1))
|
||||
@@ -48,7 +48,7 @@ fi
|
||||
|
||||
NEXT_SERVER=$MAJOR.$MINOR.$PATCH
|
||||
|
||||
CURRENT_MOBILE=$(cat mobile/pubspec.yaml | grep "^version: .*+[0-9]\+$" | cut -d "+" -f2)
|
||||
CURRENT_MOBILE=$(grep "^version: .*+[0-9]\+$" mobile/pubspec.yaml | cut -d "+" -f2)
|
||||
NEXT_MOBILE=$CURRENT_MOBILE
|
||||
if [[ $MOBILE_PUMP == "true" ]]; then
|
||||
set $((NEXT_MOBILE++))
|
||||
@@ -61,9 +61,10 @@ fi
|
||||
|
||||
if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then
|
||||
echo "Pumping Server: $CURRENT_SERVER => $NEXT_SERVER"
|
||||
npm --prefix server version $SERVER_PUMP
|
||||
npm --prefix server version "$SERVER_PUMP"
|
||||
npm --prefix web version "$SERVER_PUMP"
|
||||
make open-api
|
||||
poetry --directory machine-learning version $SERVER_PUMP
|
||||
poetry --directory machine-learning version "$SERVER_PUMP"
|
||||
fi
|
||||
|
||||
if [ "$CURRENT_MOBILE" != "$NEXT_MOBILE" ]; then
|
||||
@@ -75,4 +76,4 @@ sed -i "s/version_number: \"$CURRENT_SERVER\"$/version_number: \"$NEXT_SERVER\"/
|
||||
sed -i "s/\"android\.injected\.version\.code\" => $CURRENT_MOBILE,/\"android\.injected\.version\.code\" => $NEXT_MOBILE,/" mobile/android/fastlane/Fastfile
|
||||
sed -i "s/^version: $CURRENT_SERVER+$CURRENT_MOBILE$/version: $NEXT_SERVER+$NEXT_MOBILE/" mobile/pubspec.yaml
|
||||
|
||||
echo "IMMICH_VERSION=v$NEXT_SERVER" >>$GITHUB_ENV
|
||||
echo "IMMICH_VERSION=v$NEXT_SERVER" >>"$GITHUB_ENV"
|
||||
|
||||
@@ -35,8 +35,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 121,
|
||||
"android.injected.version.name" => "1.94.1",
|
||||
"android.injected.version.code" => 123,
|
||||
"android.injected.version.name" => "1.95.1",
|
||||
}
|
||||
)
|
||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||
|
||||
@@ -5,17 +5,17 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000276">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000232">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="22.651725">
|
||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="78.881681">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="31.263395">
|
||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="32.080999">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -379,7 +379,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 137;
|
||||
CURRENT_PROJECT_VERSION = 139;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -515,7 +515,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 137;
|
||||
CURRENT_PROJECT_VERSION = 139;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -543,7 +543,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 137;
|
||||
CURRENT_PROJECT_VERSION = 139;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
|
||||
@@ -55,11 +55,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.94.1</string>
|
||||
<string>1.95.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>137</string>
|
||||
<string>139</string>
|
||||
<key>FLTEnableImpeller</key>
|
||||
<true />
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# The default execution directory of this script is the ci_scripts directory.
|
||||
cd $CI_WORKSPACE/mobile
|
||||
cd "$CI_WORKSPACE"/mobile || exit
|
||||
|
||||
# Install Flutter using git.
|
||||
git clone https://github.com/flutter/flutter.git --depth 1 -b stable $HOME/flutter
|
||||
git clone https://github.com/flutter/flutter.git --depth 1 -b stable "$HOME"/flutter
|
||||
export PATH="$PATH:$HOME/flutter/bin"
|
||||
|
||||
# Install Flutter artifacts for iOS (--ios), or macOS (--macos) platforms.
|
||||
@@ -14,7 +14,7 @@ flutter precache --ios
|
||||
flutter pub get
|
||||
|
||||
# Install CocoaPods using Homebrew.
|
||||
HOMEBREW_NO_AUTO_UPDATE=1 # disable homebrew's automatic updates.
|
||||
export HOMEBREW_NO_AUTO_UPDATE=1 # disable homebrew's automatic updates.
|
||||
brew install cocoapods
|
||||
|
||||
# Install CocoaPods dependencies.
|
||||
|
||||
@@ -19,7 +19,7 @@ platform :ios do
|
||||
desc "iOS Beta"
|
||||
lane :beta do
|
||||
increment_version_number(
|
||||
version_number: "1.94.1"
|
||||
version_number: "1.95.1"
|
||||
)
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number + 1,
|
||||
|
||||
@@ -5,32 +5,32 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000247">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000255">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.238661">
|
||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.157832">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="2.965603">
|
||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="4.825919">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.228067">
|
||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.18815">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="4: build_app" time="108.628243">
|
||||
<testcase classname="fastlane.lanes" name="4: build_app" time="110.912709">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="71.752027">
|
||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="78.396901">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -290,7 +290,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
||||
final now = Timeline.now;
|
||||
if (now > (_hapticFeedbackTS + feedbackInterval)) {
|
||||
_hapticFeedbackTS = now;
|
||||
HapticFeedback.heavyImpact();
|
||||
HapticFeedback.mediumImpact();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
|
||||
class MemoryCard extends StatelessWidget {
|
||||
final Asset asset;
|
||||
@@ -44,14 +42,9 @@ class MemoryCard extends StatelessWidget {
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: CachedNetworkImageProvider(
|
||||
getThumbnailUrl(
|
||||
asset,
|
||||
),
|
||||
cacheKey: getThumbnailCacheKey(
|
||||
asset,
|
||||
),
|
||||
headers: {"x-immich-user-token": accessToken},
|
||||
image: ImmichImage.imageProvider(
|
||||
asset: asset,
|
||||
isThumbnail: true,
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/constants/immich_colors.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
|
||||
class MemoryProgressIndicator extends StatelessWidget {
|
||||
/// The number of ticks in the progress indicator
|
||||
@@ -39,9 +38,9 @@ class MemoryProgressIndicator extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
border: i == 0
|
||||
? null
|
||||
: Border(
|
||||
: const Border(
|
||||
left: BorderSide(
|
||||
color: context.colorScheme.onSecondaryContainer,
|
||||
color: Colors.black,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -110,7 +110,8 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
final endpoint = Uri.parse(Store.get(StoreKey.serverEndpoint));
|
||||
final headers = {"x-immich-user-token": accessToken};
|
||||
if (endpoint.userInfo.isNotEmpty) {
|
||||
headers["Authorization"] = "Basic ${base64.encode(utf8.encode(endpoint.userInfo))}";
|
||||
headers["Authorization"] =
|
||||
"Basic ${base64.encode(utf8.encode(endpoint.userInfo))}";
|
||||
}
|
||||
|
||||
debugPrint("Attempting to connect to websocket");
|
||||
@@ -161,6 +162,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
socket.on('on_asset_trash', _handleServerUpdates);
|
||||
socket.on('on_asset_restore', _handleServerUpdates);
|
||||
socket.on('on_asset_update', _handleServerUpdates);
|
||||
socket.on('on_asset_stack_update', _handleServerUpdates);
|
||||
socket.on('on_asset_hidden', _handleOnAssetHidden);
|
||||
socket.on('on_new_release', _handleReleaseUpdates);
|
||||
} catch (e) {
|
||||
|
||||
@@ -63,7 +63,7 @@ class AssetService {
|
||||
|
||||
/// Returns `null` if the server state did not change, else list of assets
|
||||
Future<List<Asset>?> _getRemoteAssets(User user) async {
|
||||
const int chunkSize = 5000;
|
||||
const int chunkSize = 10000;
|
||||
try {
|
||||
final DateTime now = DateTime.now().toUtc();
|
||||
final List<Asset> allAssets = [];
|
||||
|
||||
@@ -27,7 +27,12 @@ class ShareService {
|
||||
final downloadedXFiles = <XFile>[];
|
||||
|
||||
for (var asset in assets) {
|
||||
if (asset.isRemote) {
|
||||
if (asset.isLocal) {
|
||||
// Prefer local assets to share
|
||||
File? f = await asset.local!.file;
|
||||
downloadedXFiles.add(XFile(f!.path));
|
||||
} else if (asset.isRemote) {
|
||||
// Download remote asset otherwise
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final fileName = asset.fileName;
|
||||
final tempFile = await File('${tempDir.path}/$fileName').create();
|
||||
@@ -43,9 +48,6 @@ class ShareService {
|
||||
|
||||
tempFile.writeAsBytesSync(res.bodyBytes);
|
||||
downloadedXFiles.add(XFile(tempFile.path));
|
||||
} else {
|
||||
File? f = await asset.local!.file;
|
||||
downloadedXFiles.add(XFile(f!.path));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ class ImmichImage extends StatelessWidget {
|
||||
|
||||
return OctoImage(
|
||||
fadeInDuration: const Duration(milliseconds: 0),
|
||||
fadeOutDuration: const Duration(milliseconds: 400),
|
||||
fadeOutDuration: const Duration(milliseconds: 200),
|
||||
placeholderBuilder: (context) {
|
||||
if (placeholder != null) {
|
||||
// Use the gray box placeholder
|
||||
|
||||
9
mobile/openapi/.openapi-generator/FILES
generated
9
mobile/openapi/.openapi-generator/FILES
generated
@@ -182,6 +182,9 @@ doc/UserAvatarColor.md
|
||||
doc/UserDto.md
|
||||
doc/UserResponseDto.md
|
||||
doc/ValidateAccessTokenResponseDto.md
|
||||
doc/ValidateLibraryDto.md
|
||||
doc/ValidateLibraryImportPathResponseDto.md
|
||||
doc/ValidateLibraryResponseDto.md
|
||||
doc/VideoCodec.md
|
||||
git_push.sh
|
||||
lib/api.dart
|
||||
@@ -372,6 +375,9 @@ lib/model/user_avatar_color.dart
|
||||
lib/model/user_dto.dart
|
||||
lib/model/user_response_dto.dart
|
||||
lib/model/validate_access_token_response_dto.dart
|
||||
lib/model/validate_library_dto.dart
|
||||
lib/model/validate_library_import_path_response_dto.dart
|
||||
lib/model/validate_library_response_dto.dart
|
||||
lib/model/video_codec.dart
|
||||
pubspec.yaml
|
||||
test/activity_api_test.dart
|
||||
@@ -553,4 +559,7 @@ test/user_avatar_color_test.dart
|
||||
test/user_dto_test.dart
|
||||
test/user_response_dto_test.dart
|
||||
test/validate_access_token_response_dto_test.dart
|
||||
test/validate_library_dto_test.dart
|
||||
test/validate_library_import_path_response_dto_test.dart
|
||||
test/validate_library_response_dto_test.dart
|
||||
test/video_codec_test.dart
|
||||
|
||||
6
mobile/openapi/README.md
generated
6
mobile/openapi/README.md
generated
@@ -3,7 +3,7 @@ Immich API
|
||||
|
||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 1.94.1
|
||||
- API version: 1.95.1
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
## Requirements
|
||||
@@ -141,6 +141,7 @@ Class | Method | HTTP request | Description
|
||||
*LibraryApi* | [**removeOfflineFiles**](doc//LibraryApi.md#removeofflinefiles) | **POST** /library/{id}/removeOffline |
|
||||
*LibraryApi* | [**scanLibrary**](doc//LibraryApi.md#scanlibrary) | **POST** /library/{id}/scan |
|
||||
*LibraryApi* | [**updateLibrary**](doc//LibraryApi.md#updatelibrary) | **PUT** /library/{id} |
|
||||
*LibraryApi* | [**validate**](doc//LibraryApi.md#validate) | **POST** /library/{id}/validate |
|
||||
*OAuthApi* | [**finishOAuth**](doc//OAuthApi.md#finishoauth) | **POST** /oauth/callback |
|
||||
*OAuthApi* | [**linkOAuthAccount**](doc//OAuthApi.md#linkoauthaccount) | **POST** /oauth/link |
|
||||
*OAuthApi* | [**redirectOAuthToMobile**](doc//OAuthApi.md#redirectoauthtomobile) | **GET** /oauth/mobile-redirect |
|
||||
@@ -372,6 +373,9 @@ Class | Method | HTTP request | Description
|
||||
- [UserDto](doc//UserDto.md)
|
||||
- [UserResponseDto](doc//UserResponseDto.md)
|
||||
- [ValidateAccessTokenResponseDto](doc//ValidateAccessTokenResponseDto.md)
|
||||
- [ValidateLibraryDto](doc//ValidateLibraryDto.md)
|
||||
- [ValidateLibraryImportPathResponseDto](doc//ValidateLibraryImportPathResponseDto.md)
|
||||
- [ValidateLibraryResponseDto](doc//ValidateLibraryResponseDto.md)
|
||||
- [VideoCodec](doc//VideoCodec.md)
|
||||
|
||||
|
||||
|
||||
2
mobile/openapi/doc/AssetApi.md
generated
2
mobile/openapi/doc/AssetApi.md
generated
@@ -1153,7 +1153,7 @@ Name | Type | Description | Notes
|
||||
**updatedAfter** | **DateTime**| | [optional]
|
||||
**updatedBefore** | **DateTime**| | [optional]
|
||||
**webpPath** | **String**| | [optional]
|
||||
**withArchived** | **bool**| | [optional]
|
||||
**withArchived** | **bool**| | [optional] [default to false]
|
||||
**withDeleted** | **bool**| | [optional]
|
||||
**withExif** | **bool**| | [optional]
|
||||
**withPeople** | **bool**| | [optional]
|
||||
|
||||
58
mobile/openapi/doc/LibraryApi.md
generated
58
mobile/openapi/doc/LibraryApi.md
generated
@@ -17,6 +17,7 @@ Method | HTTP request | Description
|
||||
[**removeOfflineFiles**](LibraryApi.md#removeofflinefiles) | **POST** /library/{id}/removeOffline |
|
||||
[**scanLibrary**](LibraryApi.md#scanlibrary) | **POST** /library/{id}/scan |
|
||||
[**updateLibrary**](LibraryApi.md#updatelibrary) | **PUT** /library/{id} |
|
||||
[**validate**](LibraryApi.md#validate) | **POST** /library/{id}/validate |
|
||||
|
||||
|
||||
# **createLibrary**
|
||||
@@ -456,3 +457,60 @@ Name | Type | Description | Notes
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **validate**
|
||||
> ValidateLibraryResponseDto validate(id, validateLibraryDto)
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
// TODO Configure API key authorization: cookie
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure API key authorization: api_key
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure HTTP Bearer authorization: bearer
|
||||
// Case 1. Use String Token
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||
// Case 2. Use Function which generate token.
|
||||
// String yourTokenGeneratorFunction() { ... }
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||
|
||||
final api_instance = LibraryApi();
|
||||
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
||||
final validateLibraryDto = ValidateLibraryDto(); // ValidateLibraryDto |
|
||||
|
||||
try {
|
||||
final result = api_instance.validate(id, validateLibraryDto);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling LibraryApi->validate: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**id** | **String**| |
|
||||
**validateLibraryDto** | [**ValidateLibraryDto**](ValidateLibraryDto.md)| |
|
||||
|
||||
### Return type
|
||||
|
||||
[**ValidateLibraryResponseDto**](ValidateLibraryResponseDto.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: application/json
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
2
mobile/openapi/doc/MetadataSearchDto.md
generated
2
mobile/openapi/doc/MetadataSearchDto.md
generated
@@ -46,7 +46,7 @@ Name | Type | Description | Notes
|
||||
**updatedAfter** | [**DateTime**](DateTime.md) | | [optional]
|
||||
**updatedBefore** | [**DateTime**](DateTime.md) | | [optional]
|
||||
**webpPath** | **String** | | [optional]
|
||||
**withArchived** | **bool** | | [optional]
|
||||
**withArchived** | **bool** | | [optional] [default to false]
|
||||
**withDeleted** | **bool** | | [optional]
|
||||
**withExif** | **bool** | | [optional]
|
||||
**withPeople** | **bool** | | [optional]
|
||||
|
||||
3
mobile/openapi/doc/SmartSearchDto.md
generated
3
mobile/openapi/doc/SmartSearchDto.md
generated
@@ -18,6 +18,7 @@ Name | Type | Description | Notes
|
||||
**isExternal** | **bool** | | [optional]
|
||||
**isFavorite** | **bool** | | [optional]
|
||||
**isMotion** | **bool** | | [optional]
|
||||
**isNotInAlbum** | **bool** | | [optional]
|
||||
**isOffline** | **bool** | | [optional]
|
||||
**isReadOnly** | **bool** | | [optional]
|
||||
**isVisible** | **bool** | | [optional]
|
||||
@@ -36,7 +37,7 @@ Name | Type | Description | Notes
|
||||
**type** | [**AssetTypeEnum**](AssetTypeEnum.md) | | [optional]
|
||||
**updatedAfter** | [**DateTime**](DateTime.md) | | [optional]
|
||||
**updatedBefore** | [**DateTime**](DateTime.md) | | [optional]
|
||||
**withArchived** | **bool** | | [optional]
|
||||
**withArchived** | **bool** | | [optional] [default to false]
|
||||
**withDeleted** | **bool** | | [optional]
|
||||
**withExif** | **bool** | | [optional]
|
||||
|
||||
|
||||
16
mobile/openapi/doc/ValidateLibraryDto.md
generated
Normal file
16
mobile/openapi/doc/ValidateLibraryDto.md
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
# openapi.model.ValidateLibraryDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**exclusionPatterns** | **List<String>** | | [optional] [default to const []]
|
||||
**importPaths** | **List<String>** | | [optional] [default to const []]
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
17
mobile/openapi/doc/ValidateLibraryImportPathResponseDto.md
generated
Normal file
17
mobile/openapi/doc/ValidateLibraryImportPathResponseDto.md
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
# openapi.model.ValidateLibraryImportPathResponseDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**importPath** | **String** | |
|
||||
**isValid** | **bool** | | [optional] [default to false]
|
||||
**message** | **String** | | [optional]
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
15
mobile/openapi/doc/ValidateLibraryResponseDto.md
generated
Normal file
15
mobile/openapi/doc/ValidateLibraryResponseDto.md
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
# openapi.model.ValidateLibraryResponseDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**importPaths** | [**List<ValidateLibraryImportPathResponseDto>**](ValidateLibraryImportPathResponseDto.md) | | [optional] [default to const []]
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
3
mobile/openapi/lib/api.dart
generated
3
mobile/openapi/lib/api.dart
generated
@@ -209,6 +209,9 @@ part 'model/user_avatar_color.dart';
|
||||
part 'model/user_dto.dart';
|
||||
part 'model/user_response_dto.dart';
|
||||
part 'model/validate_access_token_response_dto.dart';
|
||||
part 'model/validate_library_dto.dart';
|
||||
part 'model/validate_library_import_path_response_dto.dart';
|
||||
part 'model/validate_library_response_dto.dart';
|
||||
part 'model/video_codec.dart';
|
||||
|
||||
|
||||
|
||||
52
mobile/openapi/lib/api/library_api.dart
generated
52
mobile/openapi/lib/api/library_api.dart
generated
@@ -378,4 +378,56 @@ class LibraryApi {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'POST /library/{id}/validate' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [ValidateLibraryDto] validateLibraryDto (required):
|
||||
Future<Response> validateWithHttpInfo(String id, ValidateLibraryDto validateLibraryDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/library/{id}/validate'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = validateLibraryDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'POST',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [ValidateLibraryDto] validateLibraryDto (required):
|
||||
Future<ValidateLibraryResponseDto?> validate(String id, ValidateLibraryDto validateLibraryDto,) async {
|
||||
final response = await validateWithHttpInfo(id, validateLibraryDto,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ValidateLibraryResponseDto',) as ValidateLibraryResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
6
mobile/openapi/lib/api_client.dart
generated
6
mobile/openapi/lib/api_client.dart
generated
@@ -500,6 +500,12 @@ class ApiClient {
|
||||
return UserResponseDto.fromJson(value);
|
||||
case 'ValidateAccessTokenResponseDto':
|
||||
return ValidateAccessTokenResponseDto.fromJson(value);
|
||||
case 'ValidateLibraryDto':
|
||||
return ValidateLibraryDto.fromJson(value);
|
||||
case 'ValidateLibraryImportPathResponseDto':
|
||||
return ValidateLibraryImportPathResponseDto.fromJson(value);
|
||||
case 'ValidateLibraryResponseDto':
|
||||
return ValidateLibraryResponseDto.fromJson(value);
|
||||
case 'VideoCodec':
|
||||
return VideoCodecTypeTransformer().decode(value);
|
||||
default:
|
||||
|
||||
18
mobile/openapi/lib/model/metadata_search_dto.dart
generated
18
mobile/openapi/lib/model/metadata_search_dto.dart
generated
@@ -51,7 +51,7 @@ class MetadataSearchDto {
|
||||
this.updatedAfter,
|
||||
this.updatedBefore,
|
||||
this.webpPath,
|
||||
this.withArchived,
|
||||
this.withArchived = false,
|
||||
this.withDeleted,
|
||||
this.withExif,
|
||||
this.withPeople,
|
||||
@@ -356,13 +356,7 @@ class MetadataSearchDto {
|
||||
///
|
||||
String? webpPath;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
bool? withArchived;
|
||||
bool withArchived;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
@@ -483,7 +477,7 @@ class MetadataSearchDto {
|
||||
(updatedAfter == null ? 0 : updatedAfter!.hashCode) +
|
||||
(updatedBefore == null ? 0 : updatedBefore!.hashCode) +
|
||||
(webpPath == null ? 0 : webpPath!.hashCode) +
|
||||
(withArchived == null ? 0 : withArchived!.hashCode) +
|
||||
(withArchived.hashCode) +
|
||||
(withDeleted == null ? 0 : withDeleted!.hashCode) +
|
||||
(withExif == null ? 0 : withExif!.hashCode) +
|
||||
(withPeople == null ? 0 : withPeople!.hashCode) +
|
||||
@@ -680,11 +674,7 @@ class MetadataSearchDto {
|
||||
} else {
|
||||
// json[r'webpPath'] = null;
|
||||
}
|
||||
if (this.withArchived != null) {
|
||||
json[r'withArchived'] = this.withArchived;
|
||||
} else {
|
||||
// json[r'withArchived'] = null;
|
||||
}
|
||||
if (this.withDeleted != null) {
|
||||
json[r'withDeleted'] = this.withDeleted;
|
||||
} else {
|
||||
@@ -756,7 +746,7 @@ class MetadataSearchDto {
|
||||
updatedAfter: mapDateTime(json, r'updatedAfter', r''),
|
||||
updatedBefore: mapDateTime(json, r'updatedBefore', r''),
|
||||
webpPath: mapValueOfType<String>(json, r'webpPath'),
|
||||
withArchived: mapValueOfType<bool>(json, r'withArchived'),
|
||||
withArchived: mapValueOfType<bool>(json, r'withArchived') ?? false,
|
||||
withDeleted: mapValueOfType<bool>(json, r'withDeleted'),
|
||||
withExif: mapValueOfType<bool>(json, r'withExif'),
|
||||
withPeople: mapValueOfType<bool>(json, r'withPeople'),
|
||||
|
||||
37
mobile/openapi/lib/model/smart_search_dto.dart
generated
37
mobile/openapi/lib/model/smart_search_dto.dart
generated
@@ -23,6 +23,7 @@ class SmartSearchDto {
|
||||
this.isExternal,
|
||||
this.isFavorite,
|
||||
this.isMotion,
|
||||
this.isNotInAlbum,
|
||||
this.isOffline,
|
||||
this.isReadOnly,
|
||||
this.isVisible,
|
||||
@@ -41,7 +42,7 @@ class SmartSearchDto {
|
||||
this.type,
|
||||
this.updatedAfter,
|
||||
this.updatedBefore,
|
||||
this.withArchived,
|
||||
this.withArchived = false,
|
||||
this.withDeleted,
|
||||
this.withExif,
|
||||
});
|
||||
@@ -126,6 +127,14 @@ class SmartSearchDto {
|
||||
///
|
||||
bool? isMotion;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
bool? isNotInAlbum;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
@@ -264,13 +273,7 @@ class SmartSearchDto {
|
||||
///
|
||||
DateTime? updatedBefore;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
bool? withArchived;
|
||||
bool withArchived;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
@@ -300,6 +303,7 @@ class SmartSearchDto {
|
||||
other.isExternal == isExternal &&
|
||||
other.isFavorite == isFavorite &&
|
||||
other.isMotion == isMotion &&
|
||||
other.isNotInAlbum == isNotInAlbum &&
|
||||
other.isOffline == isOffline &&
|
||||
other.isReadOnly == isReadOnly &&
|
||||
other.isVisible == isVisible &&
|
||||
@@ -335,6 +339,7 @@ class SmartSearchDto {
|
||||
(isExternal == null ? 0 : isExternal!.hashCode) +
|
||||
(isFavorite == null ? 0 : isFavorite!.hashCode) +
|
||||
(isMotion == null ? 0 : isMotion!.hashCode) +
|
||||
(isNotInAlbum == null ? 0 : isNotInAlbum!.hashCode) +
|
||||
(isOffline == null ? 0 : isOffline!.hashCode) +
|
||||
(isReadOnly == null ? 0 : isReadOnly!.hashCode) +
|
||||
(isVisible == null ? 0 : isVisible!.hashCode) +
|
||||
@@ -353,12 +358,12 @@ class SmartSearchDto {
|
||||
(type == null ? 0 : type!.hashCode) +
|
||||
(updatedAfter == null ? 0 : updatedAfter!.hashCode) +
|
||||
(updatedBefore == null ? 0 : updatedBefore!.hashCode) +
|
||||
(withArchived == null ? 0 : withArchived!.hashCode) +
|
||||
(withArchived.hashCode) +
|
||||
(withDeleted == null ? 0 : withDeleted!.hashCode) +
|
||||
(withExif == null ? 0 : withExif!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SmartSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isArchived=$isArchived, isEncoded=$isEncoded, isExternal=$isExternal, isFavorite=$isFavorite, isMotion=$isMotion, isOffline=$isOffline, isReadOnly=$isReadOnly, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, query=$query, size=$size, state=$state, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif]';
|
||||
String toString() => 'SmartSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isArchived=$isArchived, isEncoded=$isEncoded, isExternal=$isExternal, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isReadOnly=$isReadOnly, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, query=$query, size=$size, state=$state, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -412,6 +417,11 @@ class SmartSearchDto {
|
||||
} else {
|
||||
// json[r'isMotion'] = null;
|
||||
}
|
||||
if (this.isNotInAlbum != null) {
|
||||
json[r'isNotInAlbum'] = this.isNotInAlbum;
|
||||
} else {
|
||||
// json[r'isNotInAlbum'] = null;
|
||||
}
|
||||
if (this.isOffline != null) {
|
||||
json[r'isOffline'] = this.isOffline;
|
||||
} else {
|
||||
@@ -498,11 +508,7 @@ class SmartSearchDto {
|
||||
} else {
|
||||
// json[r'updatedBefore'] = null;
|
||||
}
|
||||
if (this.withArchived != null) {
|
||||
json[r'withArchived'] = this.withArchived;
|
||||
} else {
|
||||
// json[r'withArchived'] = null;
|
||||
}
|
||||
if (this.withDeleted != null) {
|
||||
json[r'withDeleted'] = this.withDeleted;
|
||||
} else {
|
||||
@@ -534,6 +540,7 @@ class SmartSearchDto {
|
||||
isExternal: mapValueOfType<bool>(json, r'isExternal'),
|
||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
||||
isMotion: mapValueOfType<bool>(json, r'isMotion'),
|
||||
isNotInAlbum: mapValueOfType<bool>(json, r'isNotInAlbum'),
|
||||
isOffline: mapValueOfType<bool>(json, r'isOffline'),
|
||||
isReadOnly: mapValueOfType<bool>(json, r'isReadOnly'),
|
||||
isVisible: mapValueOfType<bool>(json, r'isVisible'),
|
||||
@@ -552,7 +559,7 @@ class SmartSearchDto {
|
||||
type: AssetTypeEnum.fromJson(json[r'type']),
|
||||
updatedAfter: mapDateTime(json, r'updatedAfter', r''),
|
||||
updatedBefore: mapDateTime(json, r'updatedBefore', r''),
|
||||
withArchived: mapValueOfType<bool>(json, r'withArchived'),
|
||||
withArchived: mapValueOfType<bool>(json, r'withArchived') ?? false,
|
||||
withDeleted: mapValueOfType<bool>(json, r'withDeleted'),
|
||||
withExif: mapValueOfType<bool>(json, r'withExif'),
|
||||
);
|
||||
|
||||
108
mobile/openapi/lib/model/validate_library_dto.dart
generated
Normal file
108
mobile/openapi/lib/model/validate_library_dto.dart
generated
Normal file
@@ -0,0 +1,108 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class ValidateLibraryDto {
|
||||
/// Returns a new [ValidateLibraryDto] instance.
|
||||
ValidateLibraryDto({
|
||||
this.exclusionPatterns = const [],
|
||||
this.importPaths = const [],
|
||||
});
|
||||
|
||||
List<String> exclusionPatterns;
|
||||
|
||||
List<String> importPaths;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is ValidateLibraryDto &&
|
||||
_deepEquality.equals(other.exclusionPatterns, exclusionPatterns) &&
|
||||
_deepEquality.equals(other.importPaths, importPaths);
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(exclusionPatterns.hashCode) +
|
||||
(importPaths.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'ValidateLibraryDto[exclusionPatterns=$exclusionPatterns, importPaths=$importPaths]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'exclusionPatterns'] = this.exclusionPatterns;
|
||||
json[r'importPaths'] = this.importPaths;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [ValidateLibraryDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static ValidateLibraryDto? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return ValidateLibraryDto(
|
||||
exclusionPatterns: json[r'exclusionPatterns'] is Iterable
|
||||
? (json[r'exclusionPatterns'] as Iterable).cast<String>().toList(growable: false)
|
||||
: const [],
|
||||
importPaths: json[r'importPaths'] is Iterable
|
||||
? (json[r'importPaths'] as Iterable).cast<String>().toList(growable: false)
|
||||
: const [],
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<ValidateLibraryDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <ValidateLibraryDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = ValidateLibraryDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, ValidateLibraryDto> mapFromJson(dynamic json) {
|
||||
final map = <String, ValidateLibraryDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = ValidateLibraryDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of ValidateLibraryDto-objects as value to a dart map
|
||||
static Map<String, List<ValidateLibraryDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<ValidateLibraryDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = ValidateLibraryDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
};
|
||||
}
|
||||
|
||||
122
mobile/openapi/lib/model/validate_library_import_path_response_dto.dart
generated
Normal file
122
mobile/openapi/lib/model/validate_library_import_path_response_dto.dart
generated
Normal file
@@ -0,0 +1,122 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class ValidateLibraryImportPathResponseDto {
|
||||
/// Returns a new [ValidateLibraryImportPathResponseDto] instance.
|
||||
ValidateLibraryImportPathResponseDto({
|
||||
required this.importPath,
|
||||
this.isValid = false,
|
||||
this.message,
|
||||
});
|
||||
|
||||
String importPath;
|
||||
|
||||
bool isValid;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? message;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is ValidateLibraryImportPathResponseDto &&
|
||||
other.importPath == importPath &&
|
||||
other.isValid == isValid &&
|
||||
other.message == message;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(importPath.hashCode) +
|
||||
(isValid.hashCode) +
|
||||
(message == null ? 0 : message!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'ValidateLibraryImportPathResponseDto[importPath=$importPath, isValid=$isValid, message=$message]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'importPath'] = this.importPath;
|
||||
json[r'isValid'] = this.isValid;
|
||||
if (this.message != null) {
|
||||
json[r'message'] = this.message;
|
||||
} else {
|
||||
// json[r'message'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [ValidateLibraryImportPathResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static ValidateLibraryImportPathResponseDto? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return ValidateLibraryImportPathResponseDto(
|
||||
importPath: mapValueOfType<String>(json, r'importPath')!,
|
||||
isValid: mapValueOfType<bool>(json, r'isValid') ?? false,
|
||||
message: mapValueOfType<String>(json, r'message'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<ValidateLibraryImportPathResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <ValidateLibraryImportPathResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = ValidateLibraryImportPathResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, ValidateLibraryImportPathResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, ValidateLibraryImportPathResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = ValidateLibraryImportPathResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of ValidateLibraryImportPathResponseDto-objects as value to a dart map
|
||||
static Map<String, List<ValidateLibraryImportPathResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<ValidateLibraryImportPathResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = ValidateLibraryImportPathResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'importPath',
|
||||
};
|
||||
}
|
||||
|
||||
97
mobile/openapi/lib/model/validate_library_response_dto.dart
generated
Normal file
97
mobile/openapi/lib/model/validate_library_response_dto.dart
generated
Normal file
@@ -0,0 +1,97 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class ValidateLibraryResponseDto {
|
||||
/// Returns a new [ValidateLibraryResponseDto] instance.
|
||||
ValidateLibraryResponseDto({
|
||||
this.importPaths = const [],
|
||||
});
|
||||
|
||||
List<ValidateLibraryImportPathResponseDto> importPaths;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is ValidateLibraryResponseDto &&
|
||||
_deepEquality.equals(other.importPaths, importPaths);
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(importPaths.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'ValidateLibraryResponseDto[importPaths=$importPaths]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'importPaths'] = this.importPaths;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [ValidateLibraryResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static ValidateLibraryResponseDto? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return ValidateLibraryResponseDto(
|
||||
importPaths: ValidateLibraryImportPathResponseDto.listFromJson(json[r'importPaths']),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<ValidateLibraryResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <ValidateLibraryResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = ValidateLibraryResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, ValidateLibraryResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, ValidateLibraryResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = ValidateLibraryResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of ValidateLibraryResponseDto-objects as value to a dart map
|
||||
static Map<String, List<ValidateLibraryResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<ValidateLibraryResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = ValidateLibraryResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
};
|
||||
}
|
||||
|
||||
5
mobile/openapi/test/library_api_test.dart
generated
5
mobile/openapi/test/library_api_test.dart
generated
@@ -57,5 +57,10 @@ void main() {
|
||||
// TODO
|
||||
});
|
||||
|
||||
//Future<ValidateLibraryResponseDto> validate(String id, ValidateLibraryDto validateLibraryDto) async
|
||||
test('test validate', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@ void main() {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// bool withArchived
|
||||
// bool withArchived (default value: false)
|
||||
test('to test the property `withArchived`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
7
mobile/openapi/test/smart_search_dto_test.dart
generated
7
mobile/openapi/test/smart_search_dto_test.dart
generated
@@ -66,6 +66,11 @@ void main() {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// bool isNotInAlbum
|
||||
test('to test the property `isNotInAlbum`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// bool isOffline
|
||||
test('to test the property `isOffline`', () async {
|
||||
// TODO
|
||||
@@ -156,7 +161,7 @@ void main() {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// bool withArchived
|
||||
// bool withArchived (default value: false)
|
||||
test('to test the property `withArchived`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
32
mobile/openapi/test/validate_library_dto_test.dart
generated
Normal file
32
mobile/openapi/test/validate_library_dto_test.dart
generated
Normal file
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
// tests for ValidateLibraryDto
|
||||
void main() {
|
||||
// final instance = ValidateLibraryDto();
|
||||
|
||||
group('test ValidateLibraryDto', () {
|
||||
// List<String> exclusionPatterns (default value: const [])
|
||||
test('to test the property `exclusionPatterns`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// List<String> importPaths (default value: const [])
|
||||
test('to test the property `importPaths`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
37
mobile/openapi/test/validate_library_import_path_response_dto_test.dart
generated
Normal file
37
mobile/openapi/test/validate_library_import_path_response_dto_test.dart
generated
Normal file
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
// tests for ValidateLibraryImportPathResponseDto
|
||||
void main() {
|
||||
// final instance = ValidateLibraryImportPathResponseDto();
|
||||
|
||||
group('test ValidateLibraryImportPathResponseDto', () {
|
||||
// String importPath
|
||||
test('to test the property `importPath`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// bool isValid (default value: false)
|
||||
test('to test the property `isValid`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String message
|
||||
test('to test the property `message`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
27
mobile/openapi/test/validate_library_response_dto_test.dart
generated
Normal file
27
mobile/openapi/test/validate_library_response_dto_test.dart
generated
Normal file
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
// tests for ValidateLibraryResponseDto
|
||||
void main() {
|
||||
// final instance = ValidateLibraryResponseDto();
|
||||
|
||||
group('test ValidateLibraryResponseDto', () {
|
||||
// List<ValidateLibraryImportPathResponseDto> importPaths (default value: const [])
|
||||
test('to test the property `importPaths`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: "none"
|
||||
version: 1.94.1+121
|
||||
version: 1.95.1+123
|
||||
isar_version: &isar_version 3.1.0+1
|
||||
|
||||
environment:
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
cd .isar
|
||||
#!/usr/bin/env sh
|
||||
|
||||
cd .isar || exit
|
||||
bash tool/build_android.sh x86
|
||||
bash tool/build_android.sh x64
|
||||
bash tool/build_android.sh armv7
|
||||
|
||||
@@ -2463,6 +2463,7 @@
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
@@ -3618,6 +3619,58 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/library/{id}/validate": {
|
||||
"post": {
|
||||
"operationId": "validate",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ValidateLibraryDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ValidateLibraryResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Library"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/oauth/authorize": {
|
||||
"post": {
|
||||
"operationId": "startOAuth",
|
||||
@@ -6361,7 +6414,7 @@
|
||||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "1.94.1",
|
||||
"version": "1.95.1",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [],
|
||||
@@ -8377,6 +8430,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"withArchived": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"withDeleted": {
|
||||
@@ -9383,6 +9437,9 @@
|
||||
"isMotion": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isNotInAlbum": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isOffline": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -9445,6 +9502,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"withArchived": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"withDeleted": {
|
||||
@@ -10406,6 +10464,52 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ValidateLibraryDto": {
|
||||
"properties": {
|
||||
"exclusionPatterns": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"importPaths": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ValidateLibraryImportPathResponseDto": {
|
||||
"properties": {
|
||||
"importPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"isValid": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"importPath"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ValidateLibraryResponseDto": {
|
||||
"properties": {
|
||||
"importPaths": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ValidateLibraryImportPathResponseDto"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"VideoCodec": {
|
||||
"enum": [
|
||||
"h264",
|
||||
|
||||
167
open-api/typescript-sdk/axios-client/api.ts
generated
167
open-api/typescript-sdk/axios-client/api.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.94.1
|
||||
* The version of the OpenAPI document: 1.95.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
@@ -3880,6 +3880,12 @@ export interface SmartSearchDto {
|
||||
* @memberof SmartSearchDto
|
||||
*/
|
||||
'isMotion'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof SmartSearchDto
|
||||
*/
|
||||
'isNotInAlbum'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
@@ -5316,6 +5322,63 @@ export interface ValidateAccessTokenResponseDto {
|
||||
*/
|
||||
'authStatus': boolean;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface ValidateLibraryDto
|
||||
*/
|
||||
export interface ValidateLibraryDto {
|
||||
/**
|
||||
*
|
||||
* @type {Array<string>}
|
||||
* @memberof ValidateLibraryDto
|
||||
*/
|
||||
'exclusionPatterns'?: Array<string>;
|
||||
/**
|
||||
*
|
||||
* @type {Array<string>}
|
||||
* @memberof ValidateLibraryDto
|
||||
*/
|
||||
'importPaths'?: Array<string>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface ValidateLibraryImportPathResponseDto
|
||||
*/
|
||||
export interface ValidateLibraryImportPathResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ValidateLibraryImportPathResponseDto
|
||||
*/
|
||||
'importPath': string;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof ValidateLibraryImportPathResponseDto
|
||||
*/
|
||||
'isValid'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ValidateLibraryImportPathResponseDto
|
||||
*/
|
||||
'message'?: string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface ValidateLibraryResponseDto
|
||||
*/
|
||||
export interface ValidateLibraryResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {Array<ValidateLibraryImportPathResponseDto>}
|
||||
* @memberof ValidateLibraryResponseDto
|
||||
*/
|
||||
'importPaths'?: Array<ValidateLibraryImportPathResponseDto>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@@ -12813,6 +12876,54 @@ export const LibraryApiAxiosParamCreator = function (configuration?: Configurati
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(updateLibraryDto, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {ValidateLibraryDto} validateLibraryDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
validate: async (id: string, validateLibraryDto: ValidateLibraryDto, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('validate', 'id', id)
|
||||
// verify required parameter 'validateLibraryDto' is not null or undefined
|
||||
assertParamExists('validate', 'validateLibraryDto', validateLibraryDto)
|
||||
const localVarPath = `/library/{id}/validate`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// 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: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(validateLibraryDto, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
@@ -12925,6 +13036,19 @@ export const LibraryApiFp = function(configuration?: Configuration) {
|
||||
const operationBasePath = operationServerMap['LibraryApi.updateLibrary']?.[index]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {ValidateLibraryDto} validateLibraryDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async validate(id: string, validateLibraryDto: ValidateLibraryDto, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ValidateLibraryResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.validate(id, validateLibraryDto, options);
|
||||
const index = configuration?.serverIndex ?? 0;
|
||||
const operationBasePath = operationServerMap['LibraryApi.validate']?.[index]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -13006,6 +13130,15 @@ export const LibraryApiFactory = function (configuration?: Configuration, basePa
|
||||
updateLibrary(requestParameters: LibraryApiUpdateLibraryRequest, options?: RawAxiosRequestConfig): AxiosPromise<LibraryResponseDto> {
|
||||
return localVarFp.updateLibrary(requestParameters.id, requestParameters.updateLibraryDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {LibraryApiValidateRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
validate(requestParameters: LibraryApiValidateRequest, options?: RawAxiosRequestConfig): AxiosPromise<ValidateLibraryResponseDto> {
|
||||
return localVarFp.validate(requestParameters.id, requestParameters.validateLibraryDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -13121,6 +13254,27 @@ export interface LibraryApiUpdateLibraryRequest {
|
||||
readonly updateLibraryDto: UpdateLibraryDto
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for validate operation in LibraryApi.
|
||||
* @export
|
||||
* @interface LibraryApiValidateRequest
|
||||
*/
|
||||
export interface LibraryApiValidateRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof LibraryApiValidate
|
||||
*/
|
||||
readonly id: string
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {ValidateLibraryDto}
|
||||
* @memberof LibraryApiValidate
|
||||
*/
|
||||
readonly validateLibraryDto: ValidateLibraryDto
|
||||
}
|
||||
|
||||
/**
|
||||
* LibraryApi - object-oriented interface
|
||||
* @export
|
||||
@@ -13214,6 +13368,17 @@ export class LibraryApi extends BaseAPI {
|
||||
public updateLibrary(requestParameters: LibraryApiUpdateLibraryRequest, options?: RawAxiosRequestConfig) {
|
||||
return LibraryApiFp(this.configuration).updateLibrary(requestParameters.id, requestParameters.updateLibraryDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {LibraryApiValidateRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof LibraryApi
|
||||
*/
|
||||
public validate(requestParameters: LibraryApiValidateRequest, options?: RawAxiosRequestConfig) {
|
||||
return LibraryApiFp(this.configuration).validate(requestParameters.id, requestParameters.validateLibraryDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
2
open-api/typescript-sdk/axios-client/base.ts
generated
2
open-api/typescript-sdk/axios-client/base.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.94.1
|
||||
* The version of the OpenAPI document: 1.95.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
open-api/typescript-sdk/axios-client/common.ts
generated
2
open-api/typescript-sdk/axios-client/common.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.94.1
|
||||
* The version of the OpenAPI document: 1.95.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.94.1
|
||||
* The version of the OpenAPI document: 1.95.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
open-api/typescript-sdk/axios-client/index.ts
generated
2
open-api/typescript-sdk/axios-client/index.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.94.1
|
||||
* The version of the OpenAPI document: 1.95.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
28
open-api/typescript-sdk/fetch-client.ts
generated
28
open-api/typescript-sdk/fetch-client.ts
generated
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Immich
|
||||
* 1.94.1
|
||||
* 1.95.1
|
||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||
* See https://www.npmjs.com/package/oazapfts
|
||||
*/
|
||||
@@ -480,6 +480,18 @@ export type LibraryStatsResponseDto = {
|
||||
usage: number;
|
||||
videos: number;
|
||||
};
|
||||
export type ValidateLibraryDto = {
|
||||
exclusionPatterns?: string[];
|
||||
importPaths?: string[];
|
||||
};
|
||||
export type ValidateLibraryImportPathResponseDto = {
|
||||
importPath: string;
|
||||
isValid?: boolean;
|
||||
message?: string;
|
||||
};
|
||||
export type ValidateLibraryResponseDto = {
|
||||
importPaths?: ValidateLibraryImportPathResponseDto[];
|
||||
};
|
||||
export type OAuthConfigDto = {
|
||||
redirectUri: string;
|
||||
};
|
||||
@@ -644,6 +656,7 @@ export type SmartSearchDto = {
|
||||
isExternal?: boolean;
|
||||
isFavorite?: boolean;
|
||||
isMotion?: boolean;
|
||||
isNotInAlbum?: boolean;
|
||||
isOffline?: boolean;
|
||||
isReadOnly?: boolean;
|
||||
isVisible?: boolean;
|
||||
@@ -1901,6 +1914,19 @@ export function getLibraryStatistics({ id }: {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function validate({ id, validateLibraryDto }: {
|
||||
id: string;
|
||||
validateLibraryDto: ValidateLibraryDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: ValidateLibraryResponseDto;
|
||||
}>(`/library/${encodeURIComponent(id)}/validate`, oazapfts.json({
|
||||
...opts,
|
||||
method: "POST",
|
||||
body: validateLibraryDto
|
||||
})));
|
||||
}
|
||||
export function startOAuth({ oAuthConfigDto }: {
|
||||
oAuthConfigDto: OAuthConfigDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
|
||||
@@ -24,7 +24,7 @@ RUN npm prune --omit=dev --omit=optional
|
||||
COPY --from=dev /usr/src/app/node_modules/@img ./node_modules/@img
|
||||
|
||||
# web build
|
||||
FROM node:iron-alpine3.18 as web
|
||||
FROM node:iron-alpine3.18@sha256:a02826c7340c37a29179152723190bcc3044f933c925f3c2d78abb20f794de3f as web
|
||||
|
||||
WORKDIR /usr/src/open-api/typescript-sdk
|
||||
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
||||
@@ -59,4 +59,4 @@ COPY LICENSE /LICENSE
|
||||
ENV PATH="${PATH}:/usr/src/app/bin"
|
||||
VOLUME /usr/src/app/upload
|
||||
EXPOSE 3001
|
||||
ENTRYPOINT ["tini", "--", "/bin/sh"]
|
||||
ENTRYPOINT ["tini", "--", "/bin/bash"]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user