forked from Cutlery/immich
Merge branch 'main' into fix/edit-faces-notification
This commit is contained in:
commit
c263607515
@ -1,30 +1,31 @@
|
|||||||
.vscode/
|
.vscode/
|
||||||
|
.github/
|
||||||
|
.git/
|
||||||
|
|
||||||
design/
|
design/
|
||||||
docker/
|
docker/
|
||||||
docs/
|
docs/
|
||||||
|
e2e/
|
||||||
fastlane/
|
fastlane/
|
||||||
machine-learning/
|
machine-learning/
|
||||||
misc/
|
misc/
|
||||||
mobile/
|
mobile/
|
||||||
|
|
||||||
server/node_modules/
|
cli/coverage/
|
||||||
|
cli/dist/
|
||||||
|
cli/node_modules/
|
||||||
|
|
||||||
|
open-api/typescript-sdk/build/
|
||||||
|
open-api/typescript-sdk/node_modules/
|
||||||
|
|
||||||
server/coverage/
|
server/coverage/
|
||||||
server/.reverse-geocoding-dump/
|
server/node_modules/
|
||||||
server/upload/
|
server/upload/
|
||||||
server/dist/
|
server/dist/
|
||||||
|
server/www/
|
||||||
|
server/test/assets/
|
||||||
|
|
||||||
web/node_modules/
|
web/node_modules/
|
||||||
web/coverage/
|
web/coverage/
|
||||||
web/.svelte-kit
|
web/.svelte-kit
|
||||||
web/build/
|
web/build/
|
||||||
|
|
||||||
cli/node_modules/
|
|
||||||
cli/.reverse-geocoding-dump/
|
|
||||||
cli/upload/
|
|
||||||
cli/dist/
|
|
||||||
|
|
||||||
e2e/
|
|
||||||
|
|
||||||
open-api/typescript-sdk/node_modules/
|
|
||||||
open-api/typescript-sdk/build/
|
|
||||||
|
@ -16,4 +16,4 @@ max_line_length = off
|
|||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
[*.{yml,yaml}]
|
[*.{yml,yaml}]
|
||||||
quote_type = double
|
quote_type = single
|
||||||
|
2
.gitattributes
vendored
2
.gitattributes
vendored
@ -8,8 +8,6 @@ mobile/openapi/.openapi-generator/FILES linguist-generated=true
|
|||||||
mobile/lib/**/*.g.dart -diff -merge
|
mobile/lib/**/*.g.dart -diff -merge
|
||||||
mobile/lib/**/*.g.dart linguist-generated=true
|
mobile/lib/**/*.g.dart linguist-generated=true
|
||||||
|
|
||||||
open-api/typescript-sdk/axios-client/**/* -diff -merge
|
|
||||||
open-api/typescript-sdk/axios-client/**/* linguist-generated=true
|
|
||||||
open-api/typescript-sdk/fetch-client.ts -diff -merge
|
open-api/typescript-sdk/fetch-client.ts -diff -merge
|
||||||
open-api/typescript-sdk/fetch-client.ts linguist-generated=true
|
open-api/typescript-sdk/fetch-client.ts linguist-generated=true
|
||||||
|
|
||||||
|
33
.github/workflows/test.yml
vendored
33
.github/workflows/test.yml
vendored
@ -35,7 +35,7 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: "recursive"
|
submodules: 'recursive'
|
||||||
|
|
||||||
- name: Run e2e tests
|
- name: Run e2e tests
|
||||||
run: make server-e2e-jobs
|
run: make server-e2e-jobs
|
||||||
@ -184,7 +184,7 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: "recursive"
|
submodules: 'recursive'
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@ -194,25 +194,40 @@ jobs:
|
|||||||
- name: Run setup typescript-sdk
|
- name: Run setup typescript-sdk
|
||||||
run: npm ci && npm run build
|
run: npm ci && npm run build
|
||||||
working-directory: ./open-api/typescript-sdk
|
working-directory: ./open-api/typescript-sdk
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run setup cli
|
- name: Run setup cli
|
||||||
run: npm ci && npm run build
|
run: npm ci && npm run build
|
||||||
working-directory: ./cli
|
working-directory: ./cli
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
|
- name: Run linter
|
||||||
|
run: npm run lint
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
|
- name: Run formatter
|
||||||
|
run: npm run format
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
run: npx playwright install --with-deps
|
run: npx playwright install --with-deps chromium
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Docker build
|
- name: Docker build
|
||||||
run: docker compose build
|
run: docker compose build
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run e2e tests (api & cli)
|
- name: Run e2e tests (api & cli)
|
||||||
run: npm run test
|
run: npm run test
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
- name: Run e2e tests (web)
|
- name: Run e2e tests (web)
|
||||||
run: npx playwright test
|
run: npx playwright test
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
mobile-unit-tests:
|
mobile-unit-tests:
|
||||||
name: Mobile
|
name: Mobile
|
||||||
@ -222,8 +237,8 @@ jobs:
|
|||||||
- name: Setup Flutter SDK
|
- name: Setup Flutter SDK
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: "stable"
|
channel: 'stable'
|
||||||
flutter-version: "3.16.9"
|
flutter-version: '3.16.9'
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
run: flutter test -j 1
|
run: flutter test -j 1
|
||||||
@ -241,7 +256,7 @@ jobs:
|
|||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.11
|
||||||
cache: "poetry"
|
cache: 'poetry'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
poetry install --with dev --with cpu
|
poetry install --with dev --with cpu
|
||||||
@ -279,7 +294,7 @@ jobs:
|
|||||||
- name: Run API generation
|
- name: Run API generation
|
||||||
run: make open-api
|
run: make open-api
|
||||||
- name: Find file changes
|
- name: Find file changes
|
||||||
uses: tj-actions/verify-changed-files@v18
|
uses: tj-actions/verify-changed-files@v19
|
||||||
id: verify-changed-files
|
id: verify-changed-files
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
@ -334,7 +349,7 @@ jobs:
|
|||||||
run: npm run typeorm:migrations:generate ./src/infra/migrations/TestMigration
|
run: npm run typeorm:migrations:generate ./src/infra/migrations/TestMigration
|
||||||
|
|
||||||
- name: Find file changes
|
- name: Find file changes
|
||||||
uses: tj-actions/verify-changed-files@v18
|
uses: tj-actions/verify-changed-files@v19
|
||||||
id: verify-changed-files
|
id: verify-changed-files
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
@ -352,7 +367,7 @@ jobs:
|
|||||||
DB_URL: postgres://postgres:postgres@localhost:5432/immich
|
DB_URL: postgres://postgres:postgres@localhost:5432/immich
|
||||||
|
|
||||||
- name: Find file changes
|
- name: Find file changes
|
||||||
uses: tj-actions/verify-changed-files@v18
|
uses: tj-actions/verify-changed-files@v19
|
||||||
id: verify-changed-sql-files
|
id: verify-changed-sql-files
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
|
8
cli/package-lock.json
generated
8
cli/package-lock.json
generated
@ -54,14 +54,6 @@
|
|||||||
"@oazapfts/runtime": "^1.0.0",
|
"@oazapfts/runtime": "^1.0.0",
|
||||||
"@types/node": "^20.11.0",
|
"@types/node": "^20.11.0",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"axios": "^1.6.7"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"axios": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"../server": {
|
"../server": {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
# - https://immich.app/docs/developer/setup
|
# - https://immich.app/docs/developer/setup
|
||||||
# - https://immich.app/docs/developer/troubleshooting
|
# - https://immich.app/docs/developer/troubleshooting
|
||||||
|
|
||||||
version: "3.8"
|
version: '3.8'
|
||||||
|
|
||||||
name: immich-dev
|
name: immich-dev
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ x-server-build: &server-common
|
|||||||
services:
|
services:
|
||||||
immich-server:
|
immich-server:
|
||||||
container_name: immich_server
|
container_name: immich_server
|
||||||
command: [ "/usr/src/app/bin/immich-dev", "immich" ]
|
command: ['/usr/src/app/bin/immich-dev', 'immich']
|
||||||
<<: *server-common
|
<<: *server-common
|
||||||
ports:
|
ports:
|
||||||
- 3001:3001
|
- 3001:3001
|
||||||
@ -41,7 +41,7 @@ services:
|
|||||||
|
|
||||||
immich-microservices:
|
immich-microservices:
|
||||||
container_name: immich_microservices
|
container_name: immich_microservices
|
||||||
command: [ "/usr/src/app/bin/immich-dev", "microservices" ]
|
command: ['/usr/src/app/bin/immich-dev', 'microservices']
|
||||||
<<: *server-common
|
<<: *server-common
|
||||||
# extends:
|
# extends:
|
||||||
# file: hwaccel.transcoding.yml
|
# file: hwaccel.transcoding.yml
|
||||||
@ -57,7 +57,7 @@ services:
|
|||||||
image: immich-web-dev:latest
|
image: immich-web-dev:latest
|
||||||
build:
|
build:
|
||||||
context: ../web
|
context: ../web
|
||||||
command: [ "/usr/src/app/bin/immich-web" ]
|
command: ['/usr/src/app/bin/immich-web']
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
ports:
|
ports:
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
version: "3.8"
|
version: '3.8'
|
||||||
|
|
||||||
name: immich-prod
|
name: immich-prod
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ x-server-build: &server-common
|
|||||||
services:
|
services:
|
||||||
immich-server:
|
immich-server:
|
||||||
container_name: immich_server
|
container_name: immich_server
|
||||||
command: [ "start.sh", "immich" ]
|
command: ['start.sh', 'immich']
|
||||||
<<: *server-common
|
<<: *server-common
|
||||||
ports:
|
ports:
|
||||||
- 2283:3001
|
- 2283:3001
|
||||||
@ -27,7 +27,7 @@ services:
|
|||||||
|
|
||||||
immich-microservices:
|
immich-microservices:
|
||||||
container_name: immich_microservices
|
container_name: immich_microservices
|
||||||
command: [ "start.sh", "microservices" ]
|
command: ['start.sh', 'microservices']
|
||||||
<<: *server-common
|
<<: *server-common
|
||||||
# extends:
|
# extends:
|
||||||
# file: hwaccel.transcoding.yml
|
# file: hwaccel.transcoding.yml
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
version: "3.8"
|
version: '3.8'
|
||||||
|
|
||||||
#
|
#
|
||||||
# WARNING: Make sure to use the docker-compose.yml of the current release:
|
# WARNING: Make sure to use the docker-compose.yml of the current release:
|
||||||
@ -14,7 +14,7 @@ services:
|
|||||||
immich-server:
|
immich-server:
|
||||||
container_name: immich_server
|
container_name: immich_server
|
||||||
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
|
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
|
||||||
command: [ "start.sh", "immich" ]
|
command: ['start.sh', 'immich']
|
||||||
volumes:
|
volumes:
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
@ -33,7 +33,7 @@ services:
|
|||||||
# extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/hardware-transcoding
|
# extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/hardware-transcoding
|
||||||
# file: hwaccel.transcoding.yml
|
# file: hwaccel.transcoding.yml
|
||||||
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
|
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
|
||||||
command: [ "start.sh", "microservices" ]
|
command: ['start.sh', 'microservices']
|
||||||
volumes:
|
volumes:
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
@ -60,12 +60,12 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
|
image: registry.hub.docker.com/library/redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
image: registry.hub.docker.com/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
POSTGRES_USER: ${DB_USERNAME}
|
POSTGRES_USER: ${DB_USERNAME}
|
||||||
|
@ -67,9 +67,11 @@ Once you have a new OAuth client application configured, Immich can be configure
|
|||||||
| Client Secret | string | (required) | Required. Client Secret (previous step) |
|
| Client Secret | string | (required) | Required. Client Secret (previous step) |
|
||||||
| Scope | string | openid email profile | Full list of scopes to send with the request (space delimited) |
|
| Scope | string | openid email profile | Full list of scopes to send with the request (space delimited) |
|
||||||
| Signing Algorithm | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
|
| Signing Algorithm | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
|
||||||
|
| Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label |
|
||||||
|
| Storage Quota Claim | string | immich_quota | Claim mapping for the user's storage |
|
||||||
|
| Default Storage Quota (GiB) | number | 0 | Default quota for user without storage quota claim (Enter 0 for unlimited quota) |
|
||||||
| Button Text | string | Login with OAuth | Text for the OAuth button on the web |
|
| Button Text | string | Login with OAuth | Text for the OAuth button on the web |
|
||||||
| Auto Register | boolean | true | When true, will automatically register a user the first time they sign in |
|
| Auto Register | boolean | true | When true, will automatically register a user the first time they sign in |
|
||||||
| Storage Claim | string | preferred_username | Claim mapping for the user's storage label |
|
|
||||||
| [Auto Launch](#auto-launch) | boolean | false | When true, will skip the login page and automatically start the OAuth login process |
|
| [Auto Launch](#auto-launch) | boolean | false | When true, will skip the login page and automatically start the OAuth login process |
|
||||||
| [Mobile Redirect URI Override](#mobile-redirect-uri) | URL | (empty) | Http(s) alternative mobile redirect URI |
|
| [Mobile Redirect URI Override](#mobile-redirect-uri) | URL | (empty) | Http(s) alternative mobile redirect URI |
|
||||||
|
|
||||||
|
@ -88,10 +88,7 @@ Some basic examples:
|
|||||||
|
|
||||||
This feature - currently hidden in the config file - is considered experimental and for advanced users only. If enabled, it will allow automatic watching of the filesystem which means new assets are automatically imported to Immich without needing to rescan. Deleted assets are, as always, marked as offline and can be removed with the "Remove offline files" button.
|
This feature - currently hidden in the config file - is considered experimental and for advanced users only. If enabled, it will allow automatic watching of the filesystem which means new assets are automatically imported to Immich without needing to rescan. Deleted assets are, as always, marked as offline and can be removed with the "Remove offline files" button.
|
||||||
|
|
||||||
If your photos are on a network drive you will likely have to enable filesystem polling. The performance hit for polling large libraries is currently unknown, feel free to test this feature and report back. In addition to the boolean feature flag, the configuration file allows customization of the following parameters, please see the [chokidar documentation](https://github.com/paulmillr/chokidar?tab=readme-ov-file#performance) for reference.
|
If your photos are on a network drive, automatic file watching likely won't work. In that case, you will have to rely on a periodic library refresh to pull in your changes.
|
||||||
|
|
||||||
- `usePolling` (default: `false`).
|
|
||||||
- `interval`. (default: 10000). When using polling, this is how often (in milliseconds) the filesystem is polled.
|
|
||||||
|
|
||||||
### Nightly job
|
### Nightly job
|
||||||
|
|
||||||
|
@ -95,13 +95,16 @@ The default configuration looks like this:
|
|||||||
"issuerUrl": "",
|
"issuerUrl": "",
|
||||||
"clientId": "",
|
"clientId": "",
|
||||||
"clientSecret": "",
|
"clientSecret": "",
|
||||||
"mobileOverrideEnabled": false,
|
|
||||||
"mobileRedirectUri": "",
|
|
||||||
"scope": "openid email profile",
|
"scope": "openid email profile",
|
||||||
|
"signingAlgorithm": "RS256",
|
||||||
"storageLabelClaim": "preferred_username",
|
"storageLabelClaim": "preferred_username",
|
||||||
|
"storageQuotaClaim": "immich_quota",
|
||||||
|
"defaultStorageQuota": 0,
|
||||||
"buttonText": "Login with OAuth",
|
"buttonText": "Login with OAuth",
|
||||||
"autoRegister": true,
|
"autoRegister": true,
|
||||||
"autoLaunch": false
|
"autoLaunch": false,
|
||||||
|
"mobileOverrideEnabled": false,
|
||||||
|
"mobileRedirectUri": ""
|
||||||
},
|
},
|
||||||
"passwordLogin": {
|
"passwordLogin": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
|
@ -67,7 +67,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N
|
|||||||
| `DB_PORT` | Database Port | `5432` | server, microservices |
|
| `DB_PORT` | Database Port | `5432` | server, microservices |
|
||||||
| `DB_USERNAME` | Database User | `postgres` | server, microservices |
|
| `DB_USERNAME` | Database User | `postgres` | server, microservices |
|
||||||
| `DB_PASSWORD` | Database Password | `postgres` | server, microservices |
|
| `DB_PASSWORD` | Database Password | `postgres` | server, microservices |
|
||||||
| `DB_DATABASE` | Database Name | `immich` | server, microservices |
|
| `DB_DATABASE_NAME` | Database Name | `immich` | server, microservices |
|
||||||
| `DB_VECTOR_EXTENSION`<sup>\*1</sup> | Database Vector Extension (one of [`pgvector`, `pgvecto.rs`]) | `pgvecto.rs` | 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
|
\*1: This setting cannot be changed after the server has successfully started up
|
||||||
|
@ -50,12 +50,22 @@ import {
|
|||||||
mdiVectorCombine,
|
mdiVectorCombine,
|
||||||
mdiVideo,
|
mdiVideo,
|
||||||
mdiWeb,
|
mdiWeb,
|
||||||
|
mdiScaleBalance,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import Layout from '@theme/Layout';
|
import Layout from '@theme/Layout';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Timeline, { DateType, Item } from '../components/timeline';
|
import Timeline, { DateType, Item } from '../components/timeline';
|
||||||
|
|
||||||
const items: Item[] = [
|
const items: Item[] = [
|
||||||
|
{
|
||||||
|
icon: mdiScaleBalance,
|
||||||
|
description: 'Immich switches to AGPLv3 license',
|
||||||
|
title: 'AGPL License',
|
||||||
|
release: 'v1.95.0',
|
||||||
|
tag: 'v1.95.0',
|
||||||
|
date: new Date(2024, 1, 20),
|
||||||
|
dateType: DateType.RELEASE,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: mdiEyeRefreshOutline,
|
icon: mdiEyeRefreshOutline,
|
||||||
description: 'Automatically import files in external libraries when the operating system detects changes.',
|
description: 'Automatically import files in external libraries when the operating system detects changes.',
|
||||||
|
31
e2e/.eslintrc.cjs
Normal file
31
e2e/.eslintrc.cjs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
module.exports = {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
project: 'tsconfig.json',
|
||||||
|
sourceType: 'module',
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
},
|
||||||
|
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||||
|
extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', 'plugin:unicorn/recommended'],
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
ignorePatterns: ['.eslintrc.js'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/interface-name-prefix': 'off',
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-floating-promises': 'error',
|
||||||
|
'unicorn/prefer-module': 'off',
|
||||||
|
curly: 2,
|
||||||
|
'prettier/prettier': 0,
|
||||||
|
'unicorn/prevent-abbreviations': 'off',
|
||||||
|
'unicorn/filename-case': 'off',
|
||||||
|
'unicorn/no-null': 'off',
|
||||||
|
'unicorn/prefer-top-level-await': 'off',
|
||||||
|
'unicorn/prefer-event-target': 'off',
|
||||||
|
'unicorn/no-thenable': 'off',
|
||||||
|
},
|
||||||
|
};
|
16
e2e/.prettierignore
Normal file
16
e2e/.prettierignore
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/build
|
||||||
|
/package
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
*.md
|
||||||
|
*.json
|
||||||
|
coverage
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Ignore files for PNPM, NPM and YARN
|
||||||
|
pnpm-lock.yaml
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
8
e2e/.prettierrc
Normal file
8
e2e/.prettierrc
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 120,
|
||||||
|
"semi": true,
|
||||||
|
"organizeImportsSkipDestructiveCodeActions": true,
|
||||||
|
"plugins": ["prettier-plugin-organize-imports"]
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
version: "3.8"
|
version: '3.8'
|
||||||
|
|
||||||
name: immich-e2e
|
name: immich-e2e
|
||||||
|
|
||||||
@ -16,6 +16,7 @@ x-server-build: &server-common
|
|||||||
- IMMICH_MACHINE_LEARNING_ENABLED=false
|
- IMMICH_MACHINE_LEARNING_ENABLED=false
|
||||||
volumes:
|
volumes:
|
||||||
- upload:/usr/src/app/upload
|
- upload:/usr/src/app/upload
|
||||||
|
- ../server/test/assets:/data/assets
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- database
|
- database
|
||||||
@ -23,14 +24,14 @@ x-server-build: &server-common
|
|||||||
services:
|
services:
|
||||||
immich-server:
|
immich-server:
|
||||||
container_name: immich-e2e-server
|
container_name: immich-e2e-server
|
||||||
command: [ "./start.sh", "immich" ]
|
command: ['./start.sh', 'immich']
|
||||||
<<: *server-common
|
<<: *server-common
|
||||||
ports:
|
ports:
|
||||||
- 2283:3001
|
- 2283:3001
|
||||||
|
|
||||||
immich-microservices:
|
immich-microservices:
|
||||||
container_name: immich-e2e-microservices
|
container_name: immich-e2e-microservices
|
||||||
command: [ "./start.sh", "microservices" ]
|
command: ['./start.sh', 'microservices']
|
||||||
<<: *server-common
|
<<: *server-common
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
|
2289
e2e/package-lock.json
generated
2289
e2e/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -7,7 +7,11 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest --config vitest.config.ts",
|
"test": "vitest --config vitest.config.ts",
|
||||||
"test:web": "npx playwright test",
|
"test:web": "npx playwright test",
|
||||||
"start:web": "npx playwright test --ui"
|
"start:web": "npx playwright test --ui",
|
||||||
|
"format": "prettier --check .",
|
||||||
|
"format:fix": "prettier --write .",
|
||||||
|
"lint": "eslint \"src/**/*.ts\" --max-warnings 0",
|
||||||
|
"lint:fix": "npm run lint -- --fix"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
@ -19,10 +23,21 @@
|
|||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/node": "^20.11.17",
|
"@types/node": "^20.11.17",
|
||||||
"@types/pg": "^8.11.0",
|
"@types/pg": "^8.11.0",
|
||||||
|
"@types/pngjs": "^6.0.4",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
||||||
|
"@typescript-eslint/parser": "^7.1.0",
|
||||||
"@vitest/coverage-v8": "^1.3.0",
|
"@vitest/coverage-v8": "^1.3.0",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
|
"eslint-plugin-unicorn": "^51.0.1",
|
||||||
|
"exiftool-vendored": "^24.5.0",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
|
"pngjs": "^7.0.0",
|
||||||
|
"prettier": "^3.2.5",
|
||||||
|
"prettier-plugin-organize-imports": "^3.2.4",
|
||||||
"socket.io-client": "^4.7.4",
|
"socket.io-client": "^4.7.4",
|
||||||
"supertest": "^6.3.4",
|
"supertest": "^6.3.4",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
|
@ -20,10 +20,7 @@ describe('/activity', () => {
|
|||||||
let album: AlbumResponseDto;
|
let album: AlbumResponseDto;
|
||||||
|
|
||||||
const createActivity = (dto: ActivityCreateDto, accessToken?: string) =>
|
const createActivity = (dto: ActivityCreateDto, accessToken?: string) =>
|
||||||
create(
|
create({ activityCreateDto: dto }, { headers: asBearerAuth(accessToken || admin.accessToken) });
|
||||||
{ activityCreateDto: dto },
|
|
||||||
{ headers: asBearerAuth(accessToken || admin.accessToken) },
|
|
||||||
);
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
apiUtils.setup();
|
apiUtils.setup();
|
||||||
@ -56,13 +53,9 @@ describe('/activity', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should require an albumId', async () => {
|
it('should require an albumId', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app).get('/activity').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
.get('/activity')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
|
||||||
errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject an invalid albumId', async () => {
|
it('should reject an invalid albumId', async () => {
|
||||||
@ -71,9 +64,7 @@ describe('/activity', () => {
|
|||||||
.query({ albumId: uuidDto.invalid })
|
.query({ albumId: uuidDto.invalid })
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
|
||||||
errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject an invalid assetId', async () => {
|
it('should reject an invalid assetId', async () => {
|
||||||
@ -82,9 +73,7 @@ describe('/activity', () => {
|
|||||||
.query({ albumId: uuidDto.notFound, assetId: uuidDto.invalid })
|
.query({ albumId: uuidDto.notFound, assetId: uuidDto.invalid })
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['assetId must be a UUID'])));
|
||||||
errorDto.badRequest(expect.arrayContaining(['assetId must be a UUID'])),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should start off empty', async () => {
|
it('should start off empty', async () => {
|
||||||
@ -160,9 +149,7 @@ describe('/activity', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should filter by userId', async () => {
|
it('should filter by userId', async () => {
|
||||||
const [reaction] = await Promise.all([
|
const [reaction] = await Promise.all([createActivity({ albumId: album.id, type: ReactionType.Like })]);
|
||||||
createActivity({ albumId: album.id, type: ReactionType.Like }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const response1 = await request(app)
|
const response1 = await request(app)
|
||||||
.get('/activity')
|
.get('/activity')
|
||||||
@ -215,9 +202,7 @@ describe('/activity', () => {
|
|||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ albumId: uuidDto.invalid });
|
.send({ albumId: uuidDto.invalid });
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
|
||||||
errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require a comment when type is comment', async () => {
|
it('should require a comment when type is comment', async () => {
|
||||||
@ -226,12 +211,7 @@ describe('/activity', () => {
|
|||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ albumId: uuidDto.notFound, type: 'comment', comment: null });
|
.send({ albumId: uuidDto.notFound, type: 'comment', comment: null });
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(errorDto.badRequest(['comment must be a string', 'comment should not be empty']));
|
||||||
errorDto.badRequest([
|
|
||||||
'comment must be a string',
|
|
||||||
'comment should not be empty',
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add a comment to an album', async () => {
|
it('should add a comment to an album', async () => {
|
||||||
@ -271,9 +251,7 @@ describe('/activity', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return a 200 for a duplicate like on the album', async () => {
|
it('should return a 200 for a duplicate like on the album', async () => {
|
||||||
const [reaction] = await Promise.all([
|
const [reaction] = await Promise.all([createActivity({ albumId: album.id, type: ReactionType.Like })]);
|
||||||
createActivity({ albumId: album.id, type: ReactionType.Like }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.post('/activity')
|
.post('/activity')
|
||||||
@ -356,9 +334,7 @@ describe('/activity', () => {
|
|||||||
|
|
||||||
describe('DELETE /activity/:id', () => {
|
describe('DELETE /activity/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).delete(
|
const { status, body } = await request(app).delete(`/activity/${uuidDto.notFound}`);
|
||||||
`/activity/${uuidDto.notFound}`,
|
|
||||||
);
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
@ -420,9 +396,7 @@ describe('/activity', () => {
|
|||||||
.set('Authorization', `Bearer ${nonOwner.accessToken}`);
|
.set('Authorization', `Bearer ${nonOwner.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(errorDto.badRequest('Not found or no activity.delete access'));
|
||||||
errorDto.badRequest('Not found or no activity.delete access'),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should let a non-owner remove their own comment', async () => {
|
it('should let a non-owner remove their own comment', async () => {
|
||||||
|
@ -41,7 +41,7 @@ describe('/album', () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
[user1Asset1, user1Asset2] = await Promise.all([
|
[user1Asset1, user1Asset2] = await Promise.all([
|
||||||
apiUtils.createAsset(user1.accessToken),
|
apiUtils.createAsset(user1.accessToken, { isFavorite: true }),
|
||||||
apiUtils.createAsset(user1.accessToken),
|
apiUtils.createAsset(user1.accessToken),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -93,10 +93,7 @@ describe('/album', () => {
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await deleteUser(
|
await deleteUser({ id: user3.userId }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
{ id: user3.userId },
|
|
||||||
{ headers: asBearerAuth(admin.accessToken) },
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /album', () => {
|
describe('GET /album', () => {
|
||||||
@ -111,9 +108,7 @@ describe('/album', () => {
|
|||||||
.get('/album?shared=invalid')
|
.get('/album?shared=invalid')
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(errorDto.badRequest(['shared must be a boolean value']));
|
||||||
errorDto.badRequest(['shared must be a boolean value']),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject an invalid assetId param', async () => {
|
it('should reject an invalid assetId param', async () => {
|
||||||
@ -124,6 +119,17 @@ describe('/album', () => {
|
|||||||
expect(body).toEqual(errorDto.badRequest(['assetId must be a UUID']));
|
expect(body).toEqual(errorDto.badRequest(['assetId must be a UUID']));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should not show other users' favorites", async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get(`/album/${user1Albums[0].id}?withoutAssets=false`)
|
||||||
|
.set('Authorization', `Bearer ${user2.accessToken}`);
|
||||||
|
expect(status).toEqual(200);
|
||||||
|
expect(body).toEqual({
|
||||||
|
...user1Albums[0],
|
||||||
|
assets: [expect.objectContaining({ isFavorite: false })],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should not return shared albums with a deleted owner', async () => {
|
it('should not return shared albums with a deleted owner', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/album?shared=true')
|
.get('/album?shared=true')
|
||||||
@ -153,9 +159,7 @@ describe('/album', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return the album collection including owned and shared', async () => {
|
it('should return the album collection including owned and shared', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app).get('/album').set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
.get('/album')
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toHaveLength(3);
|
expect(body).toHaveLength(3);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
@ -250,9 +254,7 @@ describe('/album', () => {
|
|||||||
|
|
||||||
describe('GET /album/:id', () => {
|
describe('GET /album/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get(
|
const { status, body } = await request(app).get(`/album/${user1Albums[0].id}`);
|
||||||
`/album/${user1Albums[0].id}`,
|
|
||||||
);
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
@ -265,7 +267,7 @@ describe('/album', () => {
|
|||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual({
|
expect(body).toEqual({
|
||||||
...user1Albums[0],
|
...user1Albums[0],
|
||||||
assets: [expect.objectContaining(user1Albums[0].assets[0])],
|
assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -277,7 +279,7 @@ describe('/album', () => {
|
|||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual({
|
expect(body).toEqual({
|
||||||
...user2Albums[0],
|
...user2Albums[0],
|
||||||
assets: [expect.objectContaining(user2Albums[0].assets[0])],
|
assets: [expect.objectContaining({ id: user2Albums[0].assets[0].id })],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -289,7 +291,7 @@ describe('/album', () => {
|
|||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual({
|
expect(body).toEqual({
|
||||||
...user1Albums[0],
|
...user1Albums[0],
|
||||||
assets: [expect.objectContaining(user1Albums[0].assets[0])],
|
assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -326,9 +328,7 @@ describe('/album', () => {
|
|||||||
|
|
||||||
describe('POST /album', () => {
|
describe('POST /album', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app).post('/album').send({ albumName: 'New album' });
|
||||||
.post('/album')
|
|
||||||
.send({ albumName: 'New album' });
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
@ -360,9 +360,7 @@ describe('/album', () => {
|
|||||||
|
|
||||||
describe('PUT /album/:id/assets', () => {
|
describe('PUT /album/:id/assets', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).put(
|
const { status, body } = await request(app).put(`/album/${user1Albums[0].id}/assets`);
|
||||||
`/album/${user1Albums[0].id}/assets`,
|
|
||||||
);
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
@ -375,9 +373,7 @@ describe('/album', () => {
|
|||||||
.send({ ids: [asset.id] });
|
.send({ ids: [asset.id] });
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([expect.objectContaining({ id: asset.id, success: true })]);
|
||||||
expect.objectContaining({ id: asset.id, success: true }),
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to add own asset to shared album', async () => {
|
it('should be able to add own asset to shared album', async () => {
|
||||||
@ -388,9 +384,7 @@ describe('/album', () => {
|
|||||||
.send({ ids: [asset.id] });
|
.send({ ids: [asset.id] });
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([expect.objectContaining({ id: asset.id, success: true })]);
|
||||||
expect.objectContaining({ id: asset.id, success: true }),
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -473,9 +467,7 @@ describe('/album', () => {
|
|||||||
.send({ ids: [user1Asset1.id] });
|
.send({ ids: [user1Asset1.id] });
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([expect.objectContaining({ id: user1Asset1.id, success: true })]);
|
||||||
expect.objectContaining({ id: user1Asset1.id, success: true }),
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to remove own asset from shared album', async () => {
|
it('should be able to remove own asset from shared album', async () => {
|
||||||
@ -485,9 +477,7 @@ describe('/album', () => {
|
|||||||
.send({ ids: [user1Asset1.id] });
|
.send({ ids: [user1Asset1.id] });
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual([
|
expect(body).toEqual([expect.objectContaining({ id: user1Asset1.id, success: true })]);
|
||||||
expect.objectContaining({ id: user1Asset1.id, success: true }),
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -501,9 +491,7 @@ describe('/album', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app).put(`/album/${user1Albums[0].id}/users`).send({ sharedUserIds: [] });
|
||||||
.put(`/album/${user1Albums[0].id}/users`)
|
|
||||||
.send({ sharedUserIds: [] });
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
@ -1,16 +1,33 @@
|
|||||||
import {
|
import {
|
||||||
AssetFileUploadResponseDto,
|
AssetFileUploadResponseDto,
|
||||||
AssetResponseDto,
|
AssetResponseDto,
|
||||||
|
AssetTypeEnum,
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
SharedLinkType,
|
SharedLinkType,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
|
import { exiftool } from 'exiftool-vendored';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
import { createHash } from 'node:crypto';
|
||||||
|
import { readFile, writeFile } from 'node:fs/promises';
|
||||||
|
import { basename, join } from 'node:path';
|
||||||
import { Socket } from 'socket.io-client';
|
import { Socket } from 'socket.io-client';
|
||||||
import { createUserDto, uuidDto } from 'src/fixtures';
|
import { createUserDto, uuidDto } from 'src/fixtures';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
import { apiUtils, app, dbUtils } from 'src/utils';
|
import { apiUtils, app, dbUtils, tempDir, testAssetDir, wsUtils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { beforeAll, describe, expect, it } from 'vitest';
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||||
|
|
||||||
|
const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
|
||||||
|
|
||||||
|
const sha1 = (bytes: Buffer) => createHash('sha1').update(bytes).digest('base64');
|
||||||
|
|
||||||
|
const readTags = async (bytes: Buffer, filename: string) => {
|
||||||
|
const filepath = join(tempDir, filename);
|
||||||
|
await writeFile(filepath, bytes);
|
||||||
|
return exiftool.read(filepath);
|
||||||
|
};
|
||||||
|
|
||||||
const today = DateTime.fromObject({
|
const today = DateTime.fromObject({
|
||||||
year: 2023,
|
year: 2023,
|
||||||
@ -24,67 +41,81 @@ describe('/asset', () => {
|
|||||||
let user1: LoginResponseDto;
|
let user1: LoginResponseDto;
|
||||||
let user2: LoginResponseDto;
|
let user2: LoginResponseDto;
|
||||||
let userStats: LoginResponseDto;
|
let userStats: LoginResponseDto;
|
||||||
let asset1: AssetFileUploadResponseDto;
|
let user1Assets: AssetFileUploadResponseDto[];
|
||||||
let asset2: AssetFileUploadResponseDto;
|
let user2Assets: AssetFileUploadResponseDto[];
|
||||||
let asset3: AssetFileUploadResponseDto;
|
let assetLocation: AssetFileUploadResponseDto;
|
||||||
let asset4: AssetFileUploadResponseDto; // user2 asset
|
|
||||||
let asset5: AssetFileUploadResponseDto;
|
|
||||||
let asset6: AssetFileUploadResponseDto;
|
|
||||||
let ws: Socket;
|
let ws: Socket;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
apiUtils.setup();
|
apiUtils.setup();
|
||||||
await dbUtils.reset();
|
await dbUtils.reset();
|
||||||
admin = await apiUtils.adminSetup({ onboarding: false });
|
admin = await apiUtils.adminSetup({ onboarding: false });
|
||||||
[user1, user2, userStats] = await Promise.all([
|
|
||||||
|
[ws, user1, user2, userStats] = await Promise.all([
|
||||||
|
wsUtils.connect(admin.accessToken),
|
||||||
apiUtils.userSetup(admin.accessToken, createUserDto.user1),
|
apiUtils.userSetup(admin.accessToken, createUserDto.user1),
|
||||||
apiUtils.userSetup(admin.accessToken, createUserDto.user2),
|
apiUtils.userSetup(admin.accessToken, createUserDto.user2),
|
||||||
apiUtils.userSetup(admin.accessToken, createUserDto.user3),
|
apiUtils.userSetup(admin.accessToken, createUserDto.user3),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
[asset1, asset2, asset3, asset4, asset5, asset6] = await Promise.all([
|
// asset location
|
||||||
apiUtils.createAsset(user1.accessToken),
|
assetLocation = await apiUtils.createAsset(admin.accessToken, {
|
||||||
apiUtils.createAsset(user1.accessToken),
|
assetData: {
|
||||||
apiUtils.createAsset(
|
filename: 'thompson-springs.jpg',
|
||||||
user1.accessToken,
|
bytes: await readFile(locationAssetFilepath),
|
||||||
{
|
},
|
||||||
isFavorite: true,
|
});
|
||||||
isExternal: true,
|
|
||||||
isReadOnly: true,
|
|
||||||
fileCreatedAt: yesterday.toISO(),
|
|
||||||
fileModifiedAt: yesterday.toISO(),
|
|
||||||
},
|
|
||||||
{ filename: 'example.mp4' },
|
|
||||||
),
|
|
||||||
apiUtils.createAsset(user2.accessToken),
|
|
||||||
apiUtils.createAsset(user1.accessToken),
|
|
||||||
apiUtils.createAsset(user1.accessToken),
|
|
||||||
|
|
||||||
|
await wsUtils.waitForEvent({ event: 'upload', assetId: assetLocation.id });
|
||||||
|
|
||||||
|
user1Assets = await Promise.all([
|
||||||
|
apiUtils.createAsset(user1.accessToken),
|
||||||
|
apiUtils.createAsset(user1.accessToken),
|
||||||
|
apiUtils.createAsset(user1.accessToken, {
|
||||||
|
isFavorite: true,
|
||||||
|
isReadOnly: true,
|
||||||
|
fileCreatedAt: yesterday.toISO(),
|
||||||
|
fileModifiedAt: yesterday.toISO(),
|
||||||
|
assetData: { filename: 'example.mp4' },
|
||||||
|
}),
|
||||||
|
apiUtils.createAsset(user1.accessToken),
|
||||||
|
apiUtils.createAsset(user1.accessToken),
|
||||||
|
]);
|
||||||
|
|
||||||
|
user2Assets = await Promise.all([apiUtils.createAsset(user2.accessToken)]);
|
||||||
|
|
||||||
|
for (const asset of [...user1Assets, ...user2Assets]) {
|
||||||
|
expect(asset.duplicate).toBe(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
// stats
|
// stats
|
||||||
apiUtils.createAsset(userStats.accessToken),
|
apiUtils.createAsset(userStats.accessToken),
|
||||||
apiUtils.createAsset(userStats.accessToken, { isFavorite: true }),
|
apiUtils.createAsset(userStats.accessToken, { isFavorite: true }),
|
||||||
apiUtils.createAsset(userStats.accessToken, { isArchived: true }),
|
apiUtils.createAsset(userStats.accessToken, { isArchived: true }),
|
||||||
apiUtils.createAsset(
|
apiUtils.createAsset(userStats.accessToken, {
|
||||||
userStats.accessToken,
|
isArchived: true,
|
||||||
{
|
isFavorite: true,
|
||||||
isArchived: true,
|
assetData: { filename: 'example.mp4' },
|
||||||
isFavorite: true,
|
}),
|
||||||
},
|
|
||||||
{ filename: 'example.mp4' },
|
|
||||||
),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const person1 = await apiUtils.createPerson(user1.accessToken, {
|
const person1 = await apiUtils.createPerson(user1.accessToken, {
|
||||||
name: 'Test Person',
|
name: 'Test Person',
|
||||||
});
|
});
|
||||||
await dbUtils.createFace({ assetId: asset1.id, personId: person1.id });
|
await dbUtils.createFace({
|
||||||
|
assetId: user1Assets[0].id,
|
||||||
|
personId: person1.id,
|
||||||
|
});
|
||||||
|
}, 30_000);
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
wsUtils.disconnect(ws);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /asset/:id', () => {
|
describe('GET /asset/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get(
|
const { status, body } = await request(app).get(`/asset/${uuidDto.notFound}`);
|
||||||
`/asset/${uuidDto.notFound}`,
|
|
||||||
);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
});
|
});
|
||||||
@ -99,7 +130,7 @@ describe('/asset', () => {
|
|||||||
|
|
||||||
it('should require access', async () => {
|
it('should require access', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get(`/asset/${asset4.id}`)
|
.get(`/asset/${user2Assets[0].id}`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.noPermission);
|
expect(body).toEqual(errorDto.noPermission);
|
||||||
@ -107,33 +138,31 @@ describe('/asset', () => {
|
|||||||
|
|
||||||
it('should get the asset info', async () => {
|
it('should get the asset info', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get(`/asset/${asset1.id}`)
|
.get(`/asset/${user1Assets[0].id}`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toMatchObject({ id: asset1.id });
|
expect(body).toMatchObject({ id: user1Assets[0].id });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work with a shared link', async () => {
|
it('should work with a shared link', async () => {
|
||||||
const sharedLink = await apiUtils.createSharedLink(user1.accessToken, {
|
const sharedLink = await apiUtils.createSharedLink(user1.accessToken, {
|
||||||
type: SharedLinkType.Individual,
|
type: SharedLinkType.Individual,
|
||||||
assetIds: [asset1.id],
|
assetIds: [user1Assets[0].id],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { status, body } = await request(app).get(
|
const { status, body } = await request(app).get(`/asset/${user1Assets[0].id}?key=${sharedLink.key}`);
|
||||||
`/asset/${asset1.id}?key=${sharedLink.key}`,
|
|
||||||
);
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toMatchObject({ id: asset1.id });
|
expect(body).toMatchObject({ id: user1Assets[0].id });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not send people data for shared links for un-authenticated users', async () => {
|
it('should not send people data for shared links for un-authenticated users', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get(`/asset/${asset1.id}`)
|
.get(`/asset/${user1Assets[0].id}`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
|
||||||
expect(status).toEqual(200);
|
expect(status).toEqual(200);
|
||||||
expect(body).toMatchObject({
|
expect(body).toMatchObject({
|
||||||
id: asset1.id,
|
id: user1Assets[0].id,
|
||||||
isFavorite: false,
|
isFavorite: false,
|
||||||
people: [
|
people: [
|
||||||
{
|
{
|
||||||
@ -148,12 +177,10 @@ describe('/asset', () => {
|
|||||||
|
|
||||||
const sharedLink = await apiUtils.createSharedLink(user1.accessToken, {
|
const sharedLink = await apiUtils.createSharedLink(user1.accessToken, {
|
||||||
type: SharedLinkType.Individual,
|
type: SharedLinkType.Individual,
|
||||||
assetIds: [asset1.id],
|
assetIds: [user1Assets[0].id],
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await request(app).get(
|
const data = await request(app).get(`/asset/${user1Assets[0].id}?key=${sharedLink.key}`);
|
||||||
`/asset/${asset1.id}?key=${sharedLink.key}`,
|
|
||||||
);
|
|
||||||
expect(data.status).toBe(200);
|
expect(data.status).toBe(200);
|
||||||
expect(data.body).toMatchObject({ people: [] });
|
expect(data.body).toMatchObject({ people: [] });
|
||||||
});
|
});
|
||||||
@ -236,7 +263,7 @@ describe('/asset', () => {
|
|||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each(Array(10))('should return 1 random assets', async () => {
|
it.each(TEN_TIMES)('should return 1 random assets', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/asset/random')
|
.get('/asset/random')
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
@ -246,14 +273,9 @@ describe('/asset', () => {
|
|||||||
const assets: AssetResponseDto[] = body;
|
const assets: AssetResponseDto[] = body;
|
||||||
expect(assets.length).toBe(1);
|
expect(assets.length).toBe(1);
|
||||||
expect(assets[0].ownerId).toBe(user1.userId);
|
expect(assets[0].ownerId).toBe(user1.userId);
|
||||||
//
|
|
||||||
// assets owned by user2
|
|
||||||
expect(assets[0].id).not.toBe(asset4.id);
|
|
||||||
// assets owned by user1
|
|
||||||
expect([asset1.id, asset2.id, asset3.id]).toContain(assets[0].id);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each(Array(10))('should return 2 random assets', async () => {
|
it.each(TEN_TIMES)('should return 2 random assets', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/asset/random?count=2')
|
.get('/asset/random?count=2')
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
@ -265,22 +287,18 @@ describe('/asset', () => {
|
|||||||
|
|
||||||
for (const asset of assets) {
|
for (const asset of assets) {
|
||||||
expect(asset.ownerId).toBe(user1.userId);
|
expect(asset.ownerId).toBe(user1.userId);
|
||||||
// assets owned by user1
|
|
||||||
expect([asset1.id, asset2.id, asset3.id]).toContain(asset.id);
|
|
||||||
// assets owned by user2
|
|
||||||
expect(asset.id).not.toBe(asset4.id);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each(Array(10))(
|
it.each(TEN_TIMES)(
|
||||||
'should return 1 asset if there are 10 assets in the database but user 2 only has 1',
|
'should return 1 asset if there are 10 assets in the database but user 2 only has 1',
|
||||||
async () => {
|
async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/[]asset/random')
|
.get('/asset/random')
|
||||||
.set('Authorization', `Bearer ${user2.accessToken}`);
|
.set('Authorization', `Bearer ${user2.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual([expect.objectContaining({ id: asset4.id })]);
|
expect(body).toEqual([expect.objectContaining({ id: user2Assets[0].id })]);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -295,9 +313,7 @@ describe('/asset', () => {
|
|||||||
|
|
||||||
describe('PUT /asset/:id', () => {
|
describe('PUT /asset/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).put(
|
const { status, body } = await request(app).put(`/asset/:${uuidDto.notFound}`);
|
||||||
`/asset/:${uuidDto.notFound}`,
|
|
||||||
);
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
@ -312,44 +328,44 @@ describe('/asset', () => {
|
|||||||
|
|
||||||
it('should require access', async () => {
|
it('should require access', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/asset/${asset4.id}`)
|
.put(`/asset/${user2Assets[0].id}`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.noPermission);
|
expect(body).toEqual(errorDto.noPermission);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should favorite an asset', async () => {
|
it('should favorite an asset', async () => {
|
||||||
const before = await apiUtils.getAssetInfo(user1.accessToken, asset1.id);
|
const before = await apiUtils.getAssetInfo(user1.accessToken, user1Assets[0].id);
|
||||||
expect(before.isFavorite).toBe(false);
|
expect(before.isFavorite).toBe(false);
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/asset/${asset1.id}`)
|
.put(`/asset/${user1Assets[0].id}`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
.send({ isFavorite: true });
|
.send({ isFavorite: true });
|
||||||
expect(body).toMatchObject({ id: asset1.id, isFavorite: true });
|
expect(body).toMatchObject({ id: user1Assets[0].id, isFavorite: true });
|
||||||
expect(status).toEqual(200);
|
expect(status).toEqual(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should archive an asset', async () => {
|
it('should archive an asset', async () => {
|
||||||
const before = await apiUtils.getAssetInfo(user1.accessToken, asset1.id);
|
const before = await apiUtils.getAssetInfo(user1.accessToken, user1Assets[0].id);
|
||||||
expect(before.isArchived).toBe(false);
|
expect(before.isArchived).toBe(false);
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/asset/${asset1.id}`)
|
.put(`/asset/${user1Assets[0].id}`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
.send({ isArchived: true });
|
.send({ isArchived: true });
|
||||||
expect(body).toMatchObject({ id: asset1.id, isArchived: true });
|
expect(body).toMatchObject({ id: user1Assets[0].id, isArchived: true });
|
||||||
expect(status).toEqual(200);
|
expect(status).toEqual(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update date time original', async () => {
|
it('should update date time original', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/asset/${asset1.id}`)
|
.put(`/asset/${user1Assets[0].id}`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
.send({ dateTimeOriginal: '2023-11-19T18:11:00.000-07:00' });
|
.send({ dateTimeOriginal: '2023-11-19T18:11:00.000-07:00' });
|
||||||
|
|
||||||
expect(body).toMatchObject({
|
expect(body).toMatchObject({
|
||||||
id: asset1.id,
|
id: user1Assets[0].id,
|
||||||
exifInfo: expect.objectContaining({
|
exifInfo: expect.objectContaining({
|
||||||
dateTimeOriginal: '2023-11-20T01:11:00.000Z',
|
dateTimeOriginal: '2023-11-20T01:11:00.000Z',
|
||||||
}),
|
}),
|
||||||
@ -371,7 +387,7 @@ describe('/asset', () => {
|
|||||||
{ latitude: 12, longitude: 181 },
|
{ latitude: 12, longitude: 181 },
|
||||||
]) {
|
]) {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/asset/${asset1.id}`)
|
.put(`/asset/${user1Assets[0].id}`)
|
||||||
.send(test)
|
.send(test)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
@ -381,12 +397,12 @@ describe('/asset', () => {
|
|||||||
|
|
||||||
it('should update gps data', async () => {
|
it('should update gps data', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/asset/${asset1.id}`)
|
.put(`/asset/${user1Assets[0].id}`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
.send({ latitude: 12, longitude: 12 });
|
.send({ latitude: 12, longitude: 12 });
|
||||||
|
|
||||||
expect(body).toMatchObject({
|
expect(body).toMatchObject({
|
||||||
id: asset1.id,
|
id: user1Assets[0].id,
|
||||||
exifInfo: expect.objectContaining({ latitude: 12, longitude: 12 }),
|
exifInfo: expect.objectContaining({ latitude: 12, longitude: 12 }),
|
||||||
});
|
});
|
||||||
expect(status).toEqual(200);
|
expect(status).toEqual(200);
|
||||||
@ -394,11 +410,11 @@ describe('/asset', () => {
|
|||||||
|
|
||||||
it('should set the description', async () => {
|
it('should set the description', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/asset/${asset1.id}`)
|
.put(`/asset/${user1Assets[0].id}`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
.send({ description: 'Test asset description' });
|
.send({ description: 'Test asset description' });
|
||||||
expect(body).toMatchObject({
|
expect(body).toMatchObject({
|
||||||
id: asset1.id,
|
id: user1Assets[0].id,
|
||||||
exifInfo: expect.objectContaining({
|
exifInfo: expect.objectContaining({
|
||||||
description: 'Test asset description',
|
description: 'Test asset description',
|
||||||
}),
|
}),
|
||||||
@ -408,12 +424,12 @@ describe('/asset', () => {
|
|||||||
|
|
||||||
it('should return tagged people', async () => {
|
it('should return tagged people', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/asset/${asset1.id}`)
|
.put(`/asset/${user1Assets[0].id}`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
.send({ isFavorite: true });
|
.send({ isFavorite: true });
|
||||||
expect(status).toEqual(200);
|
expect(status).toEqual(200);
|
||||||
expect(body).toMatchObject({
|
expect(body).toMatchObject({
|
||||||
id: asset1.id,
|
id: user1Assets[0].id,
|
||||||
isFavorite: true,
|
isFavorite: true,
|
||||||
people: [
|
people: [
|
||||||
{
|
{
|
||||||
@ -445,9 +461,7 @@ describe('/asset', () => {
|
|||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID']));
|
||||||
errorDto.badRequest(['each value in ids must be a UUID']),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error when the id is not found', async () => {
|
it('should throw an error when the id is not found', async () => {
|
||||||
@ -457,9 +471,7 @@ describe('/asset', () => {
|
|||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(errorDto.badRequest('Not found or no asset.delete access'));
|
||||||
errorDto.badRequest('Not found or no asset.delete access'),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should move an asset to the trash', async () => {
|
it('should move an asset to the trash', async () => {
|
||||||
@ -478,4 +490,260 @@ describe('/asset', () => {
|
|||||||
expect(after.isTrashed).toBe(true);
|
expect(after.isTrashed).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('POST /asset/upload', () => {
|
||||||
|
const tests = [
|
||||||
|
{
|
||||||
|
input: 'formats/jpg/el_torcal_rocks.jpg',
|
||||||
|
expected: {
|
||||||
|
type: AssetTypeEnum.Image,
|
||||||
|
originalFileName: 'el_torcal_rocks',
|
||||||
|
resized: true,
|
||||||
|
exifInfo: {
|
||||||
|
dateTimeOriginal: '2012-08-05T11:39:59.000Z',
|
||||||
|
exifImageWidth: 512,
|
||||||
|
exifImageHeight: 341,
|
||||||
|
latitude: null,
|
||||||
|
longitude: null,
|
||||||
|
focalLength: 75,
|
||||||
|
iso: 200,
|
||||||
|
fNumber: 11,
|
||||||
|
exposureTime: '1/160',
|
||||||
|
fileSizeInByte: 53_493,
|
||||||
|
make: 'SONY',
|
||||||
|
model: 'DSLR-A550',
|
||||||
|
orientation: null,
|
||||||
|
description: 'SONY DSC',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'formats/heic/IMG_2682.heic',
|
||||||
|
expected: {
|
||||||
|
type: AssetTypeEnum.Image,
|
||||||
|
originalFileName: 'IMG_2682',
|
||||||
|
resized: true,
|
||||||
|
fileCreatedAt: '2019-03-21T16:04:22.348Z',
|
||||||
|
exifInfo: {
|
||||||
|
dateTimeOriginal: '2019-03-21T16:04:22.348Z',
|
||||||
|
exifImageWidth: 4032,
|
||||||
|
exifImageHeight: 3024,
|
||||||
|
latitude: 41.2203,
|
||||||
|
longitude: -96.071_625,
|
||||||
|
make: 'Apple',
|
||||||
|
model: 'iPhone 7',
|
||||||
|
lensModel: 'iPhone 7 back camera 3.99mm f/1.8',
|
||||||
|
fileSizeInByte: 880_703,
|
||||||
|
exposureTime: '1/887',
|
||||||
|
iso: 20,
|
||||||
|
focalLength: 3.99,
|
||||||
|
fNumber: 1.8,
|
||||||
|
timeZone: 'America/Chicago',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'formats/png/density_plot.png',
|
||||||
|
expected: {
|
||||||
|
type: AssetTypeEnum.Image,
|
||||||
|
originalFileName: 'density_plot',
|
||||||
|
resized: true,
|
||||||
|
exifInfo: {
|
||||||
|
exifImageWidth: 800,
|
||||||
|
exifImageHeight: 800,
|
||||||
|
latitude: null,
|
||||||
|
longitude: null,
|
||||||
|
fileSizeInByte: 25_408,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'formats/raw/Nikon/D80/glarus.nef',
|
||||||
|
expected: {
|
||||||
|
type: AssetTypeEnum.Image,
|
||||||
|
originalFileName: 'glarus',
|
||||||
|
resized: true,
|
||||||
|
fileCreatedAt: '2010-07-20T17:27:12.000Z',
|
||||||
|
exifInfo: {
|
||||||
|
make: 'NIKON CORPORATION',
|
||||||
|
model: 'NIKON D80',
|
||||||
|
exposureTime: '1/200',
|
||||||
|
fNumber: 10,
|
||||||
|
focalLength: 18,
|
||||||
|
iso: 100,
|
||||||
|
fileSizeInByte: 9_057_784,
|
||||||
|
dateTimeOriginal: '2010-07-20T17:27:12.000Z',
|
||||||
|
latitude: null,
|
||||||
|
longitude: null,
|
||||||
|
orientation: '1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'formats/raw/Nikon/D700/philadelphia.nef',
|
||||||
|
expected: {
|
||||||
|
type: AssetTypeEnum.Image,
|
||||||
|
originalFileName: 'philadelphia',
|
||||||
|
resized: true,
|
||||||
|
fileCreatedAt: '2016-09-22T22:10:29.060Z',
|
||||||
|
exifInfo: {
|
||||||
|
make: 'NIKON CORPORATION',
|
||||||
|
model: 'NIKON D700',
|
||||||
|
exposureTime: '1/400',
|
||||||
|
fNumber: 11,
|
||||||
|
focalLength: 85,
|
||||||
|
iso: 200,
|
||||||
|
fileSizeInByte: 15_856_335,
|
||||||
|
dateTimeOriginal: '2016-09-22T22:10:29.060Z',
|
||||||
|
latitude: null,
|
||||||
|
longitude: null,
|
||||||
|
orientation: '1',
|
||||||
|
timeZone: 'UTC-5',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { input, expected } of tests) {
|
||||||
|
it(`should generate a thumbnail for ${input}`, async () => {
|
||||||
|
const filepath = join(testAssetDir, input);
|
||||||
|
const { id, duplicate } = await apiUtils.createAsset(admin.accessToken, {
|
||||||
|
assetData: { bytes: await readFile(filepath), filename: basename(filepath) },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(duplicate).toBe(false);
|
||||||
|
|
||||||
|
await wsUtils.waitForEvent({ event: 'upload', assetId: id });
|
||||||
|
|
||||||
|
const asset = await apiUtils.getAssetInfo(admin.accessToken, id);
|
||||||
|
|
||||||
|
expect(asset.exifInfo).toBeDefined();
|
||||||
|
expect(asset.exifInfo).toMatchObject(expected.exifInfo);
|
||||||
|
expect(asset).toMatchObject(expected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should handle a duplicate', async () => {
|
||||||
|
const filepath = 'formats/jpeg/el_torcal_rocks.jpeg';
|
||||||
|
const { duplicate } = await apiUtils.createAsset(admin.accessToken, {
|
||||||
|
assetData: {
|
||||||
|
bytes: await readFile(join(testAssetDir, filepath)),
|
||||||
|
filename: basename(filepath),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(duplicate).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// These hashes were created by copying the image files to a Samsung phone,
|
||||||
|
// exporting the video from Samsung's stock Gallery app, and hashing them locally.
|
||||||
|
// This ensures that immich+exiftool are extracting the videos the same way Samsung does.
|
||||||
|
// DO NOT assume immich+exiftool are doing things correctly and just copy whatever hash it gives
|
||||||
|
// into the test here.
|
||||||
|
const motionTests = [
|
||||||
|
{
|
||||||
|
filepath: 'formats/motionphoto/Samsung One UI 5.jpg',
|
||||||
|
checksum: 'fr14niqCq6N20HB8rJYEvpsUVtI=',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filepath: 'formats/motionphoto/Samsung One UI 6.jpg',
|
||||||
|
checksum: 'lT9Uviw/FFJYCjfIxAGPTjzAmmw=',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filepath: 'formats/motionphoto/Samsung One UI 6.heic',
|
||||||
|
checksum: '/ejgzywvgvzvVhUYVfvkLzFBAF0=',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { filepath, checksum } of motionTests) {
|
||||||
|
it(`should extract motionphoto video from ${filepath}`, async () => {
|
||||||
|
const response = await apiUtils.createAsset(admin.accessToken, {
|
||||||
|
assetData: {
|
||||||
|
bytes: await readFile(join(testAssetDir, filepath)),
|
||||||
|
filename: basename(filepath),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await wsUtils.waitForEvent({ event: 'upload', assetId: response.id });
|
||||||
|
|
||||||
|
expect(response.duplicate).toBe(false);
|
||||||
|
|
||||||
|
const asset = await apiUtils.getAssetInfo(admin.accessToken, response.id);
|
||||||
|
expect(asset.livePhotoVideoId).toBeDefined();
|
||||||
|
|
||||||
|
const video = await apiUtils.getAssetInfo(admin.accessToken, asset.livePhotoVideoId as string);
|
||||||
|
expect(video.checksum).toStrictEqual(checksum);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /asset/thumbnail/:id', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get(`/asset/thumbnail/${assetLocation.id}`);
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not include gps data for webp thumbnails', async () => {
|
||||||
|
const { status, body, type } = await request(app)
|
||||||
|
.get(`/asset/thumbnail/${assetLocation.id}?format=WEBP`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
|
await wsUtils.waitForEvent({
|
||||||
|
event: 'upload',
|
||||||
|
assetId: assetLocation.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toBeDefined();
|
||||||
|
expect(type).toBe('image/webp');
|
||||||
|
|
||||||
|
const exifData = await readTags(body, 'thumbnail.webp');
|
||||||
|
expect(exifData).not.toHaveProperty('GPSLongitude');
|
||||||
|
expect(exifData).not.toHaveProperty('GPSLatitude');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not include gps data for jpeg thumbnails', async () => {
|
||||||
|
const { status, body, type } = await request(app)
|
||||||
|
.get(`/asset/thumbnail/${assetLocation.id}?format=JPEG`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toBeDefined();
|
||||||
|
expect(type).toBe('image/jpeg');
|
||||||
|
|
||||||
|
const exifData = await readTags(body, 'thumbnail.jpg');
|
||||||
|
expect(exifData).not.toHaveProperty('GPSLongitude');
|
||||||
|
expect(exifData).not.toHaveProperty('GPSLatitude');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /asset/file/:id', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get(`/asset/thumbnail/${assetLocation.id}`);
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should download the original', async () => {
|
||||||
|
const { status, body, type } = await request(app)
|
||||||
|
.get(`/asset/file/${assetLocation.id}`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toBeDefined();
|
||||||
|
expect(type).toBe('image/jpeg');
|
||||||
|
|
||||||
|
const asset = await apiUtils.getAssetInfo(admin.accessToken, assetLocation.id);
|
||||||
|
|
||||||
|
const original = await readFile(locationAssetFilepath);
|
||||||
|
const originalChecksum = sha1(original);
|
||||||
|
const downloadChecksum = sha1(body);
|
||||||
|
|
||||||
|
expect(originalChecksum).toBe(downloadChecksum);
|
||||||
|
expect(downloadChecksum).toBe(asset.checksum);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,9 +1,4 @@
|
|||||||
import {
|
import { deleteAssets, getAuditFiles, updateAsset, type LoginResponseDto } from '@immich/sdk';
|
||||||
deleteAssets,
|
|
||||||
getAuditFiles,
|
|
||||||
updateAsset,
|
|
||||||
type LoginResponseDto,
|
|
||||||
} from '@immich/sdk';
|
|
||||||
import { apiUtils, asBearerAuth, dbUtils, fileUtils } from 'src/utils';
|
import { apiUtils, asBearerAuth, dbUtils, fileUtils } from 'src/utils';
|
||||||
import { beforeAll, describe, expect, it } from 'vitest';
|
import { beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
@ -20,23 +15,20 @@ describe('/audit', () => {
|
|||||||
|
|
||||||
describe('GET :/file-report', () => {
|
describe('GET :/file-report', () => {
|
||||||
it('excludes assets without issues from report', async () => {
|
it('excludes assets without issues from report', async () => {
|
||||||
const [trashedAsset, archivedAsset, _] = await Promise.all([
|
const [trashedAsset, archivedAsset] = await Promise.all([
|
||||||
apiUtils.createAsset(admin.accessToken),
|
apiUtils.createAsset(admin.accessToken),
|
||||||
apiUtils.createAsset(admin.accessToken),
|
apiUtils.createAsset(admin.accessToken),
|
||||||
apiUtils.createAsset(admin.accessToken),
|
apiUtils.createAsset(admin.accessToken),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
deleteAssets(
|
deleteAssets({ assetBulkDeleteDto: { ids: [trashedAsset.id] } }, { headers: asBearerAuth(admin.accessToken) }),
|
||||||
{ assetBulkDeleteDto: { ids: [trashedAsset.id] } },
|
|
||||||
{ headers: asBearerAuth(admin.accessToken) }
|
|
||||||
),
|
|
||||||
updateAsset(
|
updateAsset(
|
||||||
{
|
{
|
||||||
id: archivedAsset.id,
|
id: archivedAsset.id,
|
||||||
updateAssetDto: { isArchived: true },
|
updateAssetDto: { isArchived: true },
|
||||||
},
|
},
|
||||||
{ headers: asBearerAuth(admin.accessToken) }
|
{ headers: asBearerAuth(admin.accessToken) },
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -1,16 +1,6 @@
|
|||||||
import {
|
import { LoginResponseDto, getAuthDevices, login, signUpAdmin } from '@immich/sdk';
|
||||||
LoginResponseDto,
|
|
||||||
getAuthDevices,
|
|
||||||
login,
|
|
||||||
signUpAdmin,
|
|
||||||
} from '@immich/sdk';
|
|
||||||
import { loginDto, signupDto, uuidDto } from 'src/fixtures';
|
import { loginDto, signupDto, uuidDto } from 'src/fixtures';
|
||||||
import {
|
import { deviceDto, errorDto, loginResponseDto, signupResponseDto } from 'src/responses';
|
||||||
deviceDto,
|
|
||||||
errorDto,
|
|
||||||
loginResponseDto,
|
|
||||||
signupResponseDto,
|
|
||||||
} from 'src/responses';
|
|
||||||
import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils';
|
import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||||
@ -48,18 +38,14 @@ describe(`/auth/admin-sign-up`, () => {
|
|||||||
|
|
||||||
for (const { should, data } of invalid) {
|
for (const { should, data } of invalid) {
|
||||||
it(`should ${should}`, async () => {
|
it(`should ${should}`, async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app).post('/auth/admin-sign-up').send(data);
|
||||||
.post('/auth/admin-sign-up')
|
|
||||||
.send(data);
|
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(errorDto.badRequest());
|
expect(body).toEqual(errorDto.badRequest());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
it(`should sign up the admin`, async () => {
|
it(`should sign up the admin`, async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app).post('/auth/admin-sign-up').send(signupDto.admin);
|
||||||
.post('/auth/admin-sign-up')
|
|
||||||
.send(signupDto.admin);
|
|
||||||
expect(status).toBe(201);
|
expect(status).toBe(201);
|
||||||
expect(body).toEqual(signupResponseDto.admin);
|
expect(body).toEqual(signupResponseDto.admin);
|
||||||
});
|
});
|
||||||
@ -86,9 +72,7 @@ describe(`/auth/admin-sign-up`, () => {
|
|||||||
it('should not allow a second admin to sign up', async () => {
|
it('should not allow a second admin to sign up', async () => {
|
||||||
await signUpAdmin({ signUpDto: signupDto.admin });
|
await signUpAdmin({ signUpDto: signupDto.admin });
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app).post('/auth/admin-sign-up').send(signupDto.admin);
|
||||||
.post('/auth/admin-sign-up')
|
|
||||||
.send(signupDto.admin);
|
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.alreadyHasAdmin);
|
expect(body).toEqual(errorDto.alreadyHasAdmin);
|
||||||
@ -107,9 +91,7 @@ describe('/auth/*', () => {
|
|||||||
|
|
||||||
describe(`POST /auth/login`, () => {
|
describe(`POST /auth/login`, () => {
|
||||||
it('should reject an incorrect password', async () => {
|
it('should reject an incorrect password', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app).post('/auth/login').send({ email, password: 'incorrect' });
|
||||||
.post('/auth/login')
|
|
||||||
.send({ email, password: 'incorrect' });
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.incorrectLogin);
|
expect(body).toEqual(errorDto.incorrectLogin);
|
||||||
});
|
});
|
||||||
@ -125,9 +107,7 @@ describe('/auth/*', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
it('should accept a correct password', async () => {
|
it('should accept a correct password', async () => {
|
||||||
const { status, body, headers } = await request(app)
|
const { status, body, headers } = await request(app).post('/auth/login').send({ email, password });
|
||||||
.post('/auth/login')
|
|
||||||
.send({ email, password });
|
|
||||||
expect(status).toBe(201);
|
expect(status).toBe(201);
|
||||||
expect(body).toEqual(loginResponseDto.admin);
|
expect(body).toEqual(loginResponseDto.admin);
|
||||||
|
|
||||||
@ -136,15 +116,9 @@ describe('/auth/*', () => {
|
|||||||
|
|
||||||
const cookies = headers['set-cookie'];
|
const cookies = headers['set-cookie'];
|
||||||
expect(cookies).toHaveLength(3);
|
expect(cookies).toHaveLength(3);
|
||||||
expect(cookies[0]).toEqual(
|
expect(cookies[0]).toEqual(`immich_access_token=${token}; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;`);
|
||||||
`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;');
|
||||||
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;'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -176,18 +150,12 @@ describe('/auth/*', () => {
|
|||||||
await login({ loginCredentialDto: loginDto.admin });
|
await login({ loginCredentialDto: loginDto.admin });
|
||||||
}
|
}
|
||||||
|
|
||||||
await expect(
|
await expect(getAuthDevices({ headers: asBearerAuth(admin.accessToken) })).resolves.toHaveLength(6);
|
||||||
getAuthDevices({ headers: asBearerAuth(admin.accessToken) })
|
|
||||||
).resolves.toHaveLength(6);
|
|
||||||
|
|
||||||
const { status } = await request(app)
|
const { status } = await request(app).delete(`/auth/devices`).set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
.delete(`/auth/devices`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(status).toBe(204);
|
expect(status).toBe(204);
|
||||||
|
|
||||||
await expect(
|
await expect(getAuthDevices({ headers: asBearerAuth(admin.accessToken) })).resolves.toHaveLength(1);
|
||||||
getAuthDevices({ headers: asBearerAuth(admin.accessToken) })
|
|
||||||
).resolves.toHaveLength(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error for a non-existent device id', async () => {
|
it('should throw an error for a non-existent device id', async () => {
|
||||||
@ -195,9 +163,7 @@ describe('/auth/*', () => {
|
|||||||
.delete(`/auth/devices/${uuidDto.notFound}`)
|
.delete(`/auth/devices/${uuidDto.notFound}`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(errorDto.badRequest('Not found or no authDevice.delete access'));
|
||||||
errorDto.badRequest('Not found or no authDevice.delete access')
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should logout a device', async () => {
|
it('should logout a device', async () => {
|
||||||
@ -219,9 +185,7 @@ describe('/auth/*', () => {
|
|||||||
|
|
||||||
describe('POST /auth/validateToken', () => {
|
describe('POST /auth/validateToken', () => {
|
||||||
it('should reject an invalid token', async () => {
|
it('should reject an invalid token', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app).post(`/auth/validateToken`).set('Authorization', 'Bearer 123');
|
||||||
.post(`/auth/validateToken`)
|
|
||||||
.set('Authorization', 'Bearer 123');
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.invalidToken);
|
expect(body).toEqual(errorDto.invalidToken);
|
||||||
});
|
});
|
||||||
|
@ -42,9 +42,7 @@ describe('/download', () => {
|
|||||||
|
|
||||||
describe('POST /download/asset/:id', () => {
|
describe('POST /download/asset/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).post(
|
const { status, body } = await request(app).post(`/download/asset/${asset1.id}`);
|
||||||
`/download/asset/${asset1.id}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
@ -56,7 +54,7 @@ describe('/download', () => {
|
|||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.headers['content-type']).toEqual('image/jpeg');
|
expect(response.headers['content-type']).toEqual('image/png');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
456
e2e/src/api/specs/library.e2e-spec.ts
Normal file
456
e2e/src/api/specs/library.e2e-spec.ts
Normal file
@ -0,0 +1,456 @@
|
|||||||
|
import { LibraryResponseDto, LibraryType, LoginResponseDto, getAllLibraries } from '@immich/sdk';
|
||||||
|
import { userDto, uuidDto } from 'src/fixtures';
|
||||||
|
import { errorDto } from 'src/responses';
|
||||||
|
import { apiUtils, app, asBearerAuth, dbUtils, testAssetDirInternal } from 'src/utils';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
describe('/library', () => {
|
||||||
|
let admin: LoginResponseDto;
|
||||||
|
let user: LoginResponseDto;
|
||||||
|
let library: LibraryResponseDto;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
apiUtils.setup();
|
||||||
|
await dbUtils.reset();
|
||||||
|
admin = await apiUtils.adminSetup();
|
||||||
|
user = await apiUtils.userSetup(admin.accessToken, userDto.user1);
|
||||||
|
library = await apiUtils.createLibrary(admin.accessToken, { type: LibraryType.External });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /library', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get('/library');
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should start with a default upload library', async () => {
|
||||||
|
const { status, body } = await request(app).get('/library').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
ownerId: admin.userId,
|
||||||
|
type: LibraryType.Upload,
|
||||||
|
name: 'Default Library',
|
||||||
|
refreshedAt: null,
|
||||||
|
assetCount: 0,
|
||||||
|
importPaths: [],
|
||||||
|
exclusionPatterns: [],
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /library', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).post('/library').send({});
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require admin authentication', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post('/library')
|
||||||
|
.set('Authorization', `Bearer ${user.accessToken}`)
|
||||||
|
.send({ type: LibraryType.External });
|
||||||
|
|
||||||
|
expect(status).toBe(403);
|
||||||
|
expect(body).toEqual(errorDto.forbidden);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create an external library with defaults', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post('/library')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({ type: LibraryType.External });
|
||||||
|
|
||||||
|
expect(status).toBe(201);
|
||||||
|
expect(body).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
ownerId: admin.userId,
|
||||||
|
type: LibraryType.External,
|
||||||
|
name: 'New External Library',
|
||||||
|
refreshedAt: null,
|
||||||
|
assetCount: 0,
|
||||||
|
importPaths: [],
|
||||||
|
exclusionPatterns: [],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create an external library with options', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post('/library')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({
|
||||||
|
type: LibraryType.External,
|
||||||
|
name: 'My Awesome Library',
|
||||||
|
importPaths: ['/path/to/import'],
|
||||||
|
exclusionPatterns: ['**/Raw/**'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(status).toBe(201);
|
||||||
|
expect(body).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
name: 'My Awesome Library',
|
||||||
|
importPaths: ['/path/to/import'],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not create an external library with duplicate import paths', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post('/library')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({
|
||||||
|
type: LibraryType.External,
|
||||||
|
name: 'My Awesome Library',
|
||||||
|
importPaths: ['/path', '/path'],
|
||||||
|
exclusionPatterns: ['**/Raw/**'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(["All importPaths's elements must be unique"]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not create an external library with duplicate exclusion patterns', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post('/library')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({
|
||||||
|
type: LibraryType.External,
|
||||||
|
name: 'My Awesome Library',
|
||||||
|
importPaths: ['/path/to/import'],
|
||||||
|
exclusionPatterns: ['**/Raw/**', '**/Raw/**'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(["All exclusionPatterns's elements must be unique"]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create an upload library with defaults', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post('/library')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({ type: LibraryType.Upload });
|
||||||
|
|
||||||
|
expect(status).toBe(201);
|
||||||
|
expect(body).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
ownerId: admin.userId,
|
||||||
|
type: LibraryType.Upload,
|
||||||
|
name: 'New Upload Library',
|
||||||
|
refreshedAt: null,
|
||||||
|
assetCount: 0,
|
||||||
|
importPaths: [],
|
||||||
|
exclusionPatterns: [],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create an upload library with options', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post('/library')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({ type: LibraryType.Upload, name: 'My Awesome Library' });
|
||||||
|
|
||||||
|
expect(status).toBe(201);
|
||||||
|
expect(body).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
name: 'My Awesome Library',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow upload libraries to have import paths', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post('/library')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({ type: LibraryType.Upload, importPaths: ['/path/to/import'] });
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest('Upload libraries cannot have import paths'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow upload libraries to have exclusion patterns', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post('/library')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({ type: LibraryType.Upload, exclusionPatterns: ['**/Raw/**'] });
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest('Upload libraries cannot have exclusion patterns'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PUT /library/:id', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).put(`/library/${uuidDto.notFound}`).send({});
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change the library name', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/library/${library.id}`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({ name: 'New Library Name' });
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
name: 'New Library Name',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not set an empty name', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/library/${library.id}`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({ name: '' });
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(['name should not be empty']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change the import paths', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/library/${library.id}`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({ importPaths: [testAssetDirInternal] });
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
importPaths: [testAssetDirInternal],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject an empty import path', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/library/${library.id}`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({ importPaths: [''] });
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(['each value in importPaths should not be empty']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject duplicate import paths', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/library/${library.id}`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({ importPaths: ['/path', '/path'] });
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(["All importPaths's elements must be unique"]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change the exclusion pattern', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/library/${library.id}`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({ exclusionPatterns: ['**/Raw/**'] });
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
exclusionPatterns: ['**/Raw/**'],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject duplicate exclusion patterns', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/library/${library.id}`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({ exclusionPatterns: ['**/*.jpg', '**/*.jpg'] });
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(["All exclusionPatterns's elements must be unique"]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject an empty exclusion pattern', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/library/${library.id}`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({ exclusionPatterns: [''] });
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(['each value in exclusionPatterns should not be empty']));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /library/:id', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get(`/library/${uuidDto.notFound}`);
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require admin access', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get(`/library/${uuidDto.notFound}`)
|
||||||
|
.set('Authorization', `Bearer ${user.accessToken}`);
|
||||||
|
expect(status).toBe(403);
|
||||||
|
expect(body).toEqual(errorDto.forbidden);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get library by id', async () => {
|
||||||
|
const library = await apiUtils.createLibrary(admin.accessToken, { type: LibraryType.External });
|
||||||
|
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get(`/library/${library.id}`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
ownerId: admin.userId,
|
||||||
|
type: LibraryType.External,
|
||||||
|
name: 'New External Library',
|
||||||
|
refreshedAt: null,
|
||||||
|
assetCount: 0,
|
||||||
|
importPaths: [],
|
||||||
|
exclusionPatterns: [],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DELETE /library/:id', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).delete(`/library/${uuidDto.notFound}`);
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not delete the last upload library', async () => {
|
||||||
|
const libraries = await getAllLibraries(
|
||||||
|
{ $type: LibraryType.Upload },
|
||||||
|
{ headers: asBearerAuth(admin.accessToken) },
|
||||||
|
);
|
||||||
|
|
||||||
|
const adminLibraries = libraries.filter((library) => library.ownerId === admin.userId);
|
||||||
|
expect(adminLibraries.length).toBeGreaterThanOrEqual(1);
|
||||||
|
const lastLibrary = adminLibraries.pop() as LibraryResponseDto;
|
||||||
|
|
||||||
|
// delete all but the last upload library
|
||||||
|
for (const library of adminLibraries) {
|
||||||
|
const { status } = await request(app)
|
||||||
|
.delete(`/library/${library.id}`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
expect(status).toBe(204);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.delete(`/library/${lastLibrary.id}`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
|
expect(body).toEqual(errorDto.noDeleteUploadLibrary);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete an external library', async () => {
|
||||||
|
const library = await apiUtils.createLibrary(admin.accessToken, { type: LibraryType.External });
|
||||||
|
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.delete(`/library/${library.id}`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(204);
|
||||||
|
expect(body).toEqual({});
|
||||||
|
|
||||||
|
const libraries = await getAllLibraries({}, { headers: asBearerAuth(admin.accessToken) });
|
||||||
|
expect(libraries).not.toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: library.id,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /library/:id/statistics', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get(`/library/${uuidDto.notFound}/statistics`);
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /library/:id/scan', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).post(`/library/${uuidDto.notFound}/scan`).send({});
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /library/:id/removeOffline', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).post(`/library/${uuidDto.notFound}/removeOffline`).send({});
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /library/:id/validate', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).post(`/library/${uuidDto.notFound}/validate`).send({});
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass with no import paths', async () => {
|
||||||
|
const response = await apiUtils.validateLibrary(admin.accessToken, library.id, { importPaths: [] });
|
||||||
|
expect(response.importPaths).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail if path does not exist', async () => {
|
||||||
|
const pathToTest = `${testAssetDirInternal}/does/not/exist`;
|
||||||
|
|
||||||
|
const response = await apiUtils.validateLibrary(admin.accessToken, library.id, {
|
||||||
|
importPaths: [pathToTest],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.importPaths?.length).toEqual(1);
|
||||||
|
const pathResponse = response?.importPaths?.at(0);
|
||||||
|
|
||||||
|
expect(pathResponse).toEqual({
|
||||||
|
importPath: pathToTest,
|
||||||
|
isValid: false,
|
||||||
|
message: `Path does not exist (ENOENT)`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail if path is a file', async () => {
|
||||||
|
const pathToTest = `${testAssetDirInternal}/albums/nature/el_torcal_rocks.jpg`;
|
||||||
|
|
||||||
|
const response = await apiUtils.validateLibrary(admin.accessToken, library.id, {
|
||||||
|
importPaths: [pathToTest],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.importPaths?.length).toEqual(1);
|
||||||
|
const pathResponse = response?.importPaths?.at(0);
|
||||||
|
|
||||||
|
expect(pathResponse).toEqual({
|
||||||
|
importPath: pathToTest,
|
||||||
|
isValid: false,
|
||||||
|
message: `Not a directory`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -15,16 +15,9 @@ describe(`/oauth`, () => {
|
|||||||
|
|
||||||
describe('POST /oauth/authorize', () => {
|
describe('POST /oauth/authorize', () => {
|
||||||
it(`should throw an error if a redirect uri is not provided`, async () => {
|
it(`should throw an error if a redirect uri is not provided`, async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app).post('/oauth/authorize').send({});
|
||||||
.post('/oauth/authorize')
|
|
||||||
.send({});
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(errorDto.badRequest(['redirectUri must be a string', 'redirectUri should not be empty']));
|
||||||
errorDto.badRequest([
|
|
||||||
'redirectUri must be a string',
|
|
||||||
'redirectUri should not be empty',
|
|
||||||
])
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -24,14 +24,8 @@ describe('/partner', () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
createPartner(
|
createPartner({ id: user2.userId }, { headers: asBearerAuth(user1.accessToken) }),
|
||||||
{ id: user2.userId },
|
createPartner({ id: user1.userId }, { headers: asBearerAuth(user2.accessToken) }),
|
||||||
{ headers: asBearerAuth(user1.accessToken) }
|
|
||||||
),
|
|
||||||
createPartner(
|
|
||||||
{ id: user1.userId },
|
|
||||||
{ headers: asBearerAuth(user2.accessToken) }
|
|
||||||
),
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -66,9 +60,7 @@ describe('/partner', () => {
|
|||||||
|
|
||||||
describe('POST /partner/:id', () => {
|
describe('POST /partner/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).post(
|
const { status, body } = await request(app).post(`/partner/${user3.userId}`);
|
||||||
`/partner/${user3.userId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
@ -89,17 +81,13 @@ describe('/partner', () => {
|
|||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(expect.objectContaining({ message: 'Partner already exists' }));
|
||||||
expect.objectContaining({ message: 'Partner already exists' })
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /partner/:id', () => {
|
describe('PUT /partner/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).put(
|
const { status, body } = await request(app).put(`/partner/${user2.userId}`);
|
||||||
`/partner/${user2.userId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
@ -112,17 +100,13 @@ describe('/partner', () => {
|
|||||||
.send({ inTimeline: false });
|
.send({ inTimeline: false });
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(expect.objectContaining({ id: user2.userId, inTimeline: false }));
|
||||||
expect.objectContaining({ id: user2.userId, inTimeline: false })
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('DELETE /partner/:id', () => {
|
describe('DELETE /partner/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).delete(
|
const { status, body } = await request(app).delete(`/partner/${user3.userId}`);
|
||||||
`/partner/${user3.userId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
@ -142,9 +126,7 @@ describe('/partner', () => {
|
|||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(expect.objectContaining({ message: 'Partner not found' }));
|
||||||
expect.objectContaining({ message: 'Partner not found' })
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -65,9 +65,7 @@ describe('/activity', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return only visible people', async () => {
|
it('should return only visible people', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app).get('/person').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
.get('/person')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual({
|
expect(body).toEqual({
|
||||||
@ -80,9 +78,7 @@ describe('/activity', () => {
|
|||||||
|
|
||||||
describe('GET /person/:id', () => {
|
describe('GET /person/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get(
|
const { status, body } = await request(app).get(`/person/${uuidDto.notFound}`);
|
||||||
`/person/${uuidDto.notFound}`
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
@ -109,9 +105,7 @@ describe('/activity', () => {
|
|||||||
|
|
||||||
describe('PUT /person/:id', () => {
|
describe('PUT /person/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).put(
|
const { status, body } = await request(app).put(`/person/${uuidDto.notFound}`);
|
||||||
`/person/${uuidDto.notFound}`
|
|
||||||
);
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
@ -139,7 +133,7 @@ describe('/activity', () => {
|
|||||||
birthDate: '123567',
|
birthDate: '123567',
|
||||||
response: 'Not found or no person.write access',
|
response: 'Not found or no person.write access',
|
||||||
},
|
},
|
||||||
{ birthDate: 123567, response: 'Not found or no person.write access' },
|
{ birthDate: 123_567, response: 'Not found or no person.write access' },
|
||||||
]) {
|
]) {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/person/${uuidDto.notFound}`)
|
.put(`/person/${uuidDto.notFound}`)
|
||||||
|
@ -97,9 +97,7 @@ describe('/server-info', () => {
|
|||||||
|
|
||||||
describe('GET /server-info/statistics', () => {
|
describe('GET /server-info/statistics', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get(
|
const { status, body } = await request(app).get('/server-info/statistics');
|
||||||
'/server-info/statistics'
|
|
||||||
);
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
@ -145,9 +143,7 @@ describe('/server-info', () => {
|
|||||||
|
|
||||||
describe('GET /server-info/media-types', () => {
|
describe('GET /server-info/media-types', () => {
|
||||||
it('should return accepted media types', async () => {
|
it('should return accepted media types', async () => {
|
||||||
const { status, body } = await request(app).get(
|
const { status, body } = await request(app).get('/server-info/media-types');
|
||||||
'/server-info/media-types'
|
|
||||||
);
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual({
|
expect(body).toEqual({
|
||||||
sidecar: ['.xmp'],
|
sidecar: ['.xmp'],
|
||||||
|
@ -46,14 +46,8 @@ describe('/shared-link', () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
[album, deletedAlbum, metadataAlbum] = await Promise.all([
|
[album, deletedAlbum, metadataAlbum] = await Promise.all([
|
||||||
createAlbum(
|
createAlbum({ createAlbumDto: { albumName: 'album' } }, { headers: asBearerAuth(user1.accessToken) }),
|
||||||
{ createAlbumDto: { albumName: 'album' } },
|
createAlbum({ createAlbumDto: { albumName: 'deleted album' } }, { headers: asBearerAuth(user2.accessToken) }),
|
||||||
{ headers: asBearerAuth(user1.accessToken) },
|
|
||||||
),
|
|
||||||
createAlbum(
|
|
||||||
{ createAlbumDto: { albumName: 'deleted album' } },
|
|
||||||
{ headers: asBearerAuth(user2.accessToken) },
|
|
||||||
),
|
|
||||||
createAlbum(
|
createAlbum(
|
||||||
{
|
{
|
||||||
createAlbumDto: {
|
createAlbumDto: {
|
||||||
@ -65,47 +59,38 @@ describe('/shared-link', () => {
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
[
|
[linkWithDeletedAlbum, linkWithAlbum, linkWithAssets, linkWithPassword, linkWithMetadata, linkWithoutMetadata] =
|
||||||
linkWithDeletedAlbum,
|
await Promise.all([
|
||||||
linkWithAlbum,
|
apiUtils.createSharedLink(user2.accessToken, {
|
||||||
linkWithAssets,
|
type: SharedLinkType.Album,
|
||||||
linkWithPassword,
|
albumId: deletedAlbum.id,
|
||||||
linkWithMetadata,
|
}),
|
||||||
linkWithoutMetadata,
|
apiUtils.createSharedLink(user1.accessToken, {
|
||||||
] = await Promise.all([
|
type: SharedLinkType.Album,
|
||||||
apiUtils.createSharedLink(user2.accessToken, {
|
albumId: album.id,
|
||||||
type: SharedLinkType.Album,
|
}),
|
||||||
albumId: deletedAlbum.id,
|
apiUtils.createSharedLink(user1.accessToken, {
|
||||||
}),
|
type: SharedLinkType.Individual,
|
||||||
apiUtils.createSharedLink(user1.accessToken, {
|
assetIds: [asset1.id],
|
||||||
type: SharedLinkType.Album,
|
}),
|
||||||
albumId: album.id,
|
apiUtils.createSharedLink(user1.accessToken, {
|
||||||
}),
|
type: SharedLinkType.Album,
|
||||||
apiUtils.createSharedLink(user1.accessToken, {
|
albumId: album.id,
|
||||||
type: SharedLinkType.Individual,
|
password: 'foo',
|
||||||
assetIds: [asset1.id],
|
}),
|
||||||
}),
|
apiUtils.createSharedLink(user1.accessToken, {
|
||||||
apiUtils.createSharedLink(user1.accessToken, {
|
type: SharedLinkType.Album,
|
||||||
type: SharedLinkType.Album,
|
albumId: metadataAlbum.id,
|
||||||
albumId: album.id,
|
showMetadata: true,
|
||||||
password: 'foo',
|
}),
|
||||||
}),
|
apiUtils.createSharedLink(user1.accessToken, {
|
||||||
apiUtils.createSharedLink(user1.accessToken, {
|
type: SharedLinkType.Album,
|
||||||
type: SharedLinkType.Album,
|
albumId: metadataAlbum.id,
|
||||||
albumId: metadataAlbum.id,
|
showMetadata: false,
|
||||||
showMetadata: true,
|
}),
|
||||||
}),
|
]);
|
||||||
apiUtils.createSharedLink(user1.accessToken, {
|
|
||||||
type: SharedLinkType.Album,
|
|
||||||
albumId: metadataAlbum.id,
|
|
||||||
showMetadata: false,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
await deleteUser(
|
await deleteUser({ id: user2.userId }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
{ id: user2.userId },
|
|
||||||
{ headers: asBearerAuth(admin.accessToken) },
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /shared-link', () => {
|
describe('GET /shared-link', () => {
|
||||||
@ -146,17 +131,13 @@ describe('/shared-link', () => {
|
|||||||
|
|
||||||
describe('GET /shared-link/me', () => {
|
describe('GET /shared-link/me', () => {
|
||||||
it('should not require admin authentication', async () => {
|
it('should not require admin authentication', async () => {
|
||||||
const { status } = await request(app)
|
const { status } = await request(app).get('/shared-link/me').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
.get('/shared-link/me')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(403);
|
expect(status).toBe(403);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get data for correct shared link', async () => {
|
it('should get data for correct shared link', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithAlbum.key });
|
||||||
.get('/shared-link/me')
|
|
||||||
.query({ key: linkWithAlbum.key });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
@ -178,18 +159,14 @@ describe('/shared-link', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return unauthorized if target has been soft deleted', async () => {
|
it('should return unauthorized if target has been soft deleted', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithDeletedAlbum.key });
|
||||||
.get('/shared-link/me')
|
|
||||||
.query({ key: linkWithDeletedAlbum.key });
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.invalidShareKey);
|
expect(body).toEqual(errorDto.invalidShareKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return unauthorized for password protected link', async () => {
|
it('should return unauthorized for password protected link', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithPassword.key });
|
||||||
.get('/shared-link/me')
|
|
||||||
.query({ key: linkWithPassword.key });
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.invalidSharePassword);
|
expect(body).toEqual(errorDto.invalidSharePassword);
|
||||||
@ -211,9 +188,7 @@ describe('/shared-link', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return metadata for album shared link', async () => {
|
it('should return metadata for album shared link', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithMetadata.key });
|
||||||
.get('/shared-link/me')
|
|
||||||
.query({ key: linkWithMetadata.key });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body.assets).toHaveLength(1);
|
expect(body.assets).toHaveLength(1);
|
||||||
@ -229,9 +204,7 @@ describe('/shared-link', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not return metadata for album shared link without metadata', async () => {
|
it('should not return metadata for album shared link without metadata', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithoutMetadata.key });
|
||||||
.get('/shared-link/me')
|
|
||||||
.query({ key: linkWithoutMetadata.key });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body.assets).toHaveLength(1);
|
expect(body.assets).toHaveLength(1);
|
||||||
@ -247,9 +220,7 @@ describe('/shared-link', () => {
|
|||||||
|
|
||||||
describe('GET /shared-link/:id', () => {
|
describe('GET /shared-link/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get(
|
const { status, body } = await request(app).get(`/shared-link/${linkWithAlbum.id}`);
|
||||||
`/shared-link/${linkWithAlbum.id}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
@ -276,9 +247,7 @@ describe('/shared-link', () => {
|
|||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(expect.objectContaining({ message: 'Shared link not found' }));
|
||||||
expect.objectContaining({ message: 'Shared link not found' }),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -308,9 +277,7 @@ describe('/shared-link', () => {
|
|||||||
.send({ type: SharedLinkType.Album });
|
.send({ type: SharedLinkType.Album });
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(expect.objectContaining({ message: 'Invalid albumId' }));
|
||||||
expect.objectContaining({ message: 'Invalid albumId' }),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require a valid asset id', async () => {
|
it('should require a valid asset id', async () => {
|
||||||
@ -320,9 +287,7 @@ describe('/shared-link', () => {
|
|||||||
.send({ type: SharedLinkType.Individual, assetId: uuidDto.notFound });
|
.send({ type: SharedLinkType.Individual, assetId: uuidDto.notFound });
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(expect.objectContaining({ message: 'Invalid assetIds' }));
|
||||||
expect.objectContaining({ message: 'Invalid assetIds' }),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create a shared link', async () => {
|
it('should create a shared link', async () => {
|
||||||
@ -424,9 +389,7 @@ describe('/shared-link', () => {
|
|||||||
|
|
||||||
describe('DELETE /shared-link/:id', () => {
|
describe('DELETE /shared-link/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).delete(
|
const { status, body } = await request(app).delete(`/shared-link/${linkWithAlbum.id}`);
|
||||||
`/shared-link/${linkWithAlbum.id}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
@ -18,9 +18,7 @@ describe('/system-config', () => {
|
|||||||
|
|
||||||
describe('GET /system-config/map/style.json', () => {
|
describe('GET /system-config/map/style.json', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get(
|
const { status, body } = await request(app).get('/system-config/map/style.json');
|
||||||
'/system-config/map/style.json'
|
|
||||||
);
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
@ -32,11 +30,7 @@ describe('/system-config', () => {
|
|||||||
.query({ theme })
|
.query({ theme })
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(errorDto.badRequest(['theme must be one of the following values: light, dark']));
|
||||||
errorDto.badRequest([
|
|
||||||
'theme must be one of the following values: light, dark',
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -32,24 +32,16 @@ describe('/trash', () => {
|
|||||||
const { id: assetId } = await apiUtils.createAsset(admin.accessToken);
|
const { id: assetId } = await apiUtils.createAsset(admin.accessToken);
|
||||||
await apiUtils.deleteAssets(admin.accessToken, [assetId]);
|
await apiUtils.deleteAssets(admin.accessToken, [assetId]);
|
||||||
|
|
||||||
const before = await getAllAssets(
|
const before = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) });
|
||||||
{},
|
|
||||||
{ headers: asBearerAuth(admin.accessToken) },
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(before.length).toBeGreaterThanOrEqual(1);
|
expect(before.length).toBeGreaterThanOrEqual(1);
|
||||||
|
|
||||||
const { status } = await request(app)
|
const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
.post('/trash/empty')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(status).toBe(204);
|
expect(status).toBe(204);
|
||||||
|
|
||||||
await wsUtils.once(ws, 'on_asset_delete');
|
await wsUtils.waitForEvent({ event: 'delete', assetId });
|
||||||
|
|
||||||
const after = await getAllAssets(
|
const after = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) });
|
||||||
{},
|
|
||||||
{ headers: asBearerAuth(admin.accessToken) },
|
|
||||||
);
|
|
||||||
expect(after.length).toBe(0);
|
expect(after.length).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -69,9 +61,7 @@ describe('/trash', () => {
|
|||||||
const before = await apiUtils.getAssetInfo(admin.accessToken, assetId);
|
const before = await apiUtils.getAssetInfo(admin.accessToken, assetId);
|
||||||
expect(before.isTrashed).toBe(true);
|
expect(before.isTrashed).toBe(true);
|
||||||
|
|
||||||
const { status } = await request(app)
|
const { status } = await request(app).post('/trash/restore').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
.post('/trash/restore')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(status).toBe(204);
|
expect(status).toBe(204);
|
||||||
|
|
||||||
const after = await apiUtils.getAssetInfo(admin.accessToken, assetId);
|
const after = await apiUtils.getAssetInfo(admin.accessToken, assetId);
|
||||||
|
@ -22,10 +22,7 @@ describe('/server-info', () => {
|
|||||||
apiUtils.userSetup(admin.accessToken, createUserDto.user3),
|
apiUtils.userSetup(admin.accessToken, createUserDto.user3),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await deleteUser(
|
await deleteUser({ id: deletedUser.userId }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
{ id: deletedUser.userId },
|
|
||||||
{ headers: asBearerAuth(admin.accessToken) }
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /user', () => {
|
describe('GET /user', () => {
|
||||||
@ -36,9 +33,7 @@ describe('/server-info', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should get users', async () => {
|
it('should get users', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app).get('/user').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
.get('/user')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(status).toEqual(200);
|
expect(status).toEqual(200);
|
||||||
expect(body).toHaveLength(4);
|
expect(body).toHaveLength(4);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
@ -47,7 +42,7 @@ describe('/server-info', () => {
|
|||||||
expect.objectContaining({ email: 'user1@immich.cloud' }),
|
expect.objectContaining({ email: 'user1@immich.cloud' }),
|
||||||
expect.objectContaining({ email: 'user2@immich.cloud' }),
|
expect.objectContaining({ email: 'user2@immich.cloud' }),
|
||||||
expect.objectContaining({ email: 'user3@immich.cloud' }),
|
expect.objectContaining({ email: 'user3@immich.cloud' }),
|
||||||
])
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -63,7 +58,7 @@ describe('/server-info', () => {
|
|||||||
expect.objectContaining({ email: 'admin@immich.cloud' }),
|
expect.objectContaining({ email: 'admin@immich.cloud' }),
|
||||||
expect.objectContaining({ email: 'user2@immich.cloud' }),
|
expect.objectContaining({ email: 'user2@immich.cloud' }),
|
||||||
expect.objectContaining({ email: 'user3@immich.cloud' }),
|
expect.objectContaining({ email: 'user3@immich.cloud' }),
|
||||||
])
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -81,7 +76,7 @@ describe('/server-info', () => {
|
|||||||
expect.objectContaining({ email: 'user1@immich.cloud' }),
|
expect.objectContaining({ email: 'user1@immich.cloud' }),
|
||||||
expect.objectContaining({ email: 'user2@immich.cloud' }),
|
expect.objectContaining({ email: 'user2@immich.cloud' }),
|
||||||
expect.objectContaining({ email: 'user3@immich.cloud' }),
|
expect.objectContaining({ email: 'user3@immich.cloud' }),
|
||||||
])
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -112,9 +107,7 @@ describe('/server-info', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should get my info', async () => {
|
it('should get my info', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app).get(`/user/me`).set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
.get(`/user/me`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toMatchObject({
|
expect(body).toMatchObject({
|
||||||
id: admin.userId,
|
id: admin.userId,
|
||||||
@ -125,9 +118,7 @@ describe('/server-info', () => {
|
|||||||
|
|
||||||
describe('POST /user', () => {
|
describe('POST /user', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app).post(`/user`).send(createUserDto.user1);
|
||||||
.post(`/user`)
|
|
||||||
.send(createUserDto.user1);
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
@ -181,9 +172,7 @@ describe('/server-info', () => {
|
|||||||
|
|
||||||
describe('DELETE /user/:id', () => {
|
describe('DELETE /user/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).delete(
|
const { status, body } = await request(app).delete(`/user/${userToDelete.userId}`);
|
||||||
`/user/${userToDelete.userId}`
|
|
||||||
);
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
@ -241,10 +230,7 @@ describe('/server-info', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should ignore updates to createdAt, updatedAt and deletedAt', async () => {
|
it('should ignore updates to createdAt, updatedAt and deletedAt', async () => {
|
||||||
const before = await getUserById(
|
const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
{ id: admin.userId },
|
|
||||||
{ headers: asBearerAuth(admin.accessToken) }
|
|
||||||
);
|
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/user`)
|
.put(`/user`)
|
||||||
@ -261,10 +247,7 @@ describe('/server-info', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should update first and last name', async () => {
|
it('should update first and last name', async () => {
|
||||||
const before = await getUserById(
|
const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
{ id: admin.userId },
|
|
||||||
{ headers: asBearerAuth(admin.accessToken) }
|
|
||||||
);
|
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/user`)
|
.put(`/user`)
|
||||||
@ -284,10 +267,7 @@ describe('/server-info', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should update memories enabled', async () => {
|
it('should update memories enabled', async () => {
|
||||||
const before = await getUserById(
|
const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
{ id: admin.userId },
|
|
||||||
{ headers: asBearerAuth(admin.accessToken) }
|
|
||||||
);
|
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/user`)
|
.put(`/user`)
|
||||||
.send({
|
.send({
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { stat } from 'node:fs/promises';
|
import { stat } from 'node:fs/promises';
|
||||||
import { apiUtils, app, dbUtils, immichCli } from 'src/utils';
|
import { apiUtils, app, dbUtils, immichCli } from 'src/utils';
|
||||||
import { beforeEach, beforeAll, describe, expect, it } from 'vitest';
|
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
describe(`immich login-key`, () => {
|
describe(`immich login-key`, () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
@ -24,25 +24,15 @@ describe(`immich login-key`, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should require a valid key', async () => {
|
it('should require a valid key', async () => {
|
||||||
const { stderr, exitCode } = await immichCli([
|
const { stderr, exitCode } = await immichCli(['login-key', app, 'immich-is-so-cool']);
|
||||||
'login-key',
|
expect(stderr).toContain('Failed to connect to server http://127.0.0.1:2283/api: Error: 401');
|
||||||
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);
|
expect(exitCode).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should login', async () => {
|
it('should login', async () => {
|
||||||
const admin = await apiUtils.adminSetup();
|
const admin = await apiUtils.adminSetup();
|
||||||
const key = await apiUtils.createApiKey(admin.accessToken);
|
const key = await apiUtils.createApiKey(admin.accessToken);
|
||||||
const { stdout, stderr, exitCode } = await immichCli([
|
const { stdout, stderr, exitCode } = await immichCli(['login-key', app, `${key.secret}`]);
|
||||||
'login-key',
|
|
||||||
app,
|
|
||||||
`${key.secret}`,
|
|
||||||
]);
|
|
||||||
expect(stdout.split('\n')).toEqual([
|
expect(stdout.split('\n')).toEqual([
|
||||||
'Logging in...',
|
'Logging in...',
|
||||||
'Logged in as admin@immich.cloud',
|
'Logged in as admin@immich.cloud',
|
||||||
|
@ -1,13 +1,6 @@
|
|||||||
import { getAllAlbums, getAllAssets } from '@immich/sdk';
|
import { getAllAlbums, getAllAssets } from '@immich/sdk';
|
||||||
import { mkdir, readdir, rm, symlink } from 'fs/promises';
|
import { mkdir, readdir, rm, symlink } from 'node:fs/promises';
|
||||||
import {
|
import { apiUtils, asKeyAuth, cliUtils, dbUtils, immichCli, testAssetDir } from 'src/utils';
|
||||||
apiUtils,
|
|
||||||
asKeyAuth,
|
|
||||||
cliUtils,
|
|
||||||
dbUtils,
|
|
||||||
immichCli,
|
|
||||||
testAssetDir,
|
|
||||||
} from 'src/utils';
|
|
||||||
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
describe(`immich upload`, () => {
|
describe(`immich upload`, () => {
|
||||||
@ -25,16 +18,10 @@ describe(`immich upload`, () => {
|
|||||||
|
|
||||||
describe('immich upload --recursive', () => {
|
describe('immich upload --recursive', () => {
|
||||||
it('should upload a folder recursively', async () => {
|
it('should upload a folder recursively', async () => {
|
||||||
const { stderr, stdout, exitCode } = await immichCli([
|
const { stderr, stdout, exitCode } = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--recursive']);
|
||||||
'upload',
|
|
||||||
`${testAssetDir}/albums/nature/`,
|
|
||||||
'--recursive',
|
|
||||||
]);
|
|
||||||
expect(stderr).toBe('');
|
expect(stderr).toBe('');
|
||||||
expect(stdout.split('\n')).toEqual(
|
expect(stdout.split('\n')).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([expect.stringContaining('Successfully uploaded 9 assets')]),
|
||||||
expect.stringContaining('Successfully uploaded 9 assets'),
|
|
||||||
]),
|
|
||||||
);
|
);
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
|
||||||
@ -70,15 +57,9 @@ describe(`immich upload`, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should add existing assets to albums', async () => {
|
it('should add existing assets to albums', async () => {
|
||||||
const response1 = await immichCli([
|
const response1 = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--recursive']);
|
||||||
'upload',
|
|
||||||
`${testAssetDir}/albums/nature/`,
|
|
||||||
'--recursive',
|
|
||||||
]);
|
|
||||||
expect(response1.stdout.split('\n')).toEqual(
|
expect(response1.stdout.split('\n')).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([expect.stringContaining('Successfully uploaded 9 assets')]),
|
||||||
expect.stringContaining('Successfully uploaded 9 assets'),
|
|
||||||
]),
|
|
||||||
);
|
);
|
||||||
expect(response1.stderr).toBe('');
|
expect(response1.stderr).toBe('');
|
||||||
expect(response1.exitCode).toBe(0);
|
expect(response1.exitCode).toBe(0);
|
||||||
@ -89,17 +70,10 @@ describe(`immich upload`, () => {
|
|||||||
const albums1 = await getAllAlbums({}, { headers: asKeyAuth(key) });
|
const albums1 = await getAllAlbums({}, { headers: asKeyAuth(key) });
|
||||||
expect(albums1.length).toBe(0);
|
expect(albums1.length).toBe(0);
|
||||||
|
|
||||||
const response2 = await immichCli([
|
const response2 = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--recursive', '--album']);
|
||||||
'upload',
|
|
||||||
`${testAssetDir}/albums/nature/`,
|
|
||||||
'--recursive',
|
|
||||||
'--album',
|
|
||||||
]);
|
|
||||||
expect(response2.stdout.split('\n')).toEqual(
|
expect(response2.stdout.split('\n')).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.stringContaining(
|
expect.stringContaining('All assets were already uploaded, nothing to do.'),
|
||||||
'All assets were already uploaded, nothing to do.',
|
|
||||||
),
|
|
||||||
expect.stringContaining('Successfully updated 9 assets'),
|
expect.stringContaining('Successfully updated 9 assets'),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
@ -147,17 +121,10 @@ describe(`immich upload`, () => {
|
|||||||
await mkdir(`/tmp/albums/nature`, { recursive: true });
|
await mkdir(`/tmp/albums/nature`, { recursive: true });
|
||||||
const filesToLink = await readdir(`${testAssetDir}/albums/nature`);
|
const filesToLink = await readdir(`${testAssetDir}/albums/nature`);
|
||||||
for (const file of filesToLink) {
|
for (const file of filesToLink) {
|
||||||
await symlink(
|
await symlink(`${testAssetDir}/albums/nature/${file}`, `/tmp/albums/nature/${file}`);
|
||||||
`${testAssetDir}/albums/nature/${file}`,
|
|
||||||
`/tmp/albums/nature/${file}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { stderr, stdout, exitCode } = await immichCli([
|
const { stderr, stdout, exitCode } = await immichCli(['upload', `/tmp/albums/nature`, '--delete']);
|
||||||
'upload',
|
|
||||||
`/tmp/albums/nature`,
|
|
||||||
'--delete',
|
|
||||||
]);
|
|
||||||
|
|
||||||
const files = await readdir(`/tmp/albums/nature`);
|
const files = await readdir(`/tmp/albums/nature`);
|
||||||
await rm(`/tmp/albums/nature`, { recursive: true });
|
await rm(`/tmp/albums/nature`, { recursive: true });
|
||||||
|
@ -44,7 +44,6 @@ export const userDto = {
|
|||||||
email: signupDto.admin.email,
|
email: signupDto.admin.email,
|
||||||
password: signupDto.admin.password,
|
password: signupDto.admin.password,
|
||||||
storageLabel: 'admin',
|
storageLabel: 'admin',
|
||||||
externalPath: null,
|
|
||||||
oauthId: '',
|
oauthId: '',
|
||||||
shouldChangePassword: false,
|
shouldChangePassword: false,
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
@ -63,7 +62,6 @@ export const userDto = {
|
|||||||
email: createUserDto.user1.email,
|
email: createUserDto.user1.email,
|
||||||
password: createUserDto.user1.password,
|
password: createUserDto.user1.password,
|
||||||
storageLabel: null,
|
storageLabel: null,
|
||||||
externalPath: null,
|
|
||||||
oauthId: '',
|
oauthId: '',
|
||||||
shouldChangePassword: false,
|
shouldChangePassword: false,
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
|
31
e2e/src/generators.ts
Normal file
31
e2e/src/generators.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { PNG } from 'pngjs';
|
||||||
|
|
||||||
|
const createPNG = (r: number, g: number, b: number) => {
|
||||||
|
const image = new PNG({ width: 1, height: 1 });
|
||||||
|
image.data[0] = r;
|
||||||
|
image.data[1] = g;
|
||||||
|
image.data[2] = b;
|
||||||
|
image.data[3] = 255;
|
||||||
|
return PNG.sync.write(image);
|
||||||
|
};
|
||||||
|
|
||||||
|
function* newPngFactory() {
|
||||||
|
for (let r = 0; r < 255; r++) {
|
||||||
|
for (let g = 0; g < 255; g++) {
|
||||||
|
for (let b = 0; b < 255; b++) {
|
||||||
|
yield createPNG(r, g, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pngFactory = newPngFactory();
|
||||||
|
|
||||||
|
export const makeRandomImage = () => {
|
||||||
|
const { value } = pngFactory.next();
|
||||||
|
if (!value) {
|
||||||
|
throw new Error('Ran out of random asset data');
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
@ -65,7 +65,6 @@ export const signupResponseDto = {
|
|||||||
name: 'Immich Admin',
|
name: 'Immich Admin',
|
||||||
email: 'admin@immich.cloud',
|
email: 'admin@immich.cloud',
|
||||||
storageLabel: 'admin',
|
storageLabel: 'admin',
|
||||||
externalPath: null,
|
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
// why? lol
|
// why? lol
|
||||||
shouldChangePassword: true,
|
shouldChangePassword: true,
|
||||||
|
@ -1,26 +1,24 @@
|
|||||||
import { spawn, exec } from 'child_process';
|
import { exec, spawn } from 'node:child_process';
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
let _resolve: () => unknown;
|
let _resolve: () => unknown;
|
||||||
const promise = new Promise<void>((resolve) => (_resolve = resolve));
|
const ready = new Promise<void>((resolve) => (_resolve = resolve));
|
||||||
|
|
||||||
const child = spawn('docker', ['compose', 'up'], { stdio: 'pipe' });
|
const child = spawn('docker', ['compose', 'up'], { stdio: 'pipe' });
|
||||||
|
|
||||||
child.stdout.on('data', (data) => {
|
child.stdout.on('data', (data) => {
|
||||||
const input = data.toString();
|
const input = data.toString();
|
||||||
console.log(input);
|
console.log(input);
|
||||||
if (input.includes('Immich Server is listening')) {
|
if (input.includes('Immich Microservices is listening')) {
|
||||||
_resolve();
|
_resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
child.stderr.on('data', (data) => console.log(data.toString()));
|
child.stderr.on('data', (data) => console.log(data.toString()));
|
||||||
|
|
||||||
await promise;
|
await ready;
|
||||||
|
|
||||||
return async () => {
|
return async () => {
|
||||||
await new Promise<void>((resolve) =>
|
await new Promise<void>((resolve) => exec('docker compose down', () => resolve()));
|
||||||
exec('docker compose down', () => resolve())
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
169
e2e/src/utils.ts
169
e2e/src/utils.ts
@ -1,12 +1,16 @@
|
|||||||
import {
|
import {
|
||||||
AssetFileUploadResponseDto,
|
AssetFileUploadResponseDto,
|
||||||
|
AssetResponseDto,
|
||||||
CreateAlbumDto,
|
CreateAlbumDto,
|
||||||
CreateAssetDto,
|
CreateAssetDto,
|
||||||
|
CreateLibraryDto,
|
||||||
CreateUserDto,
|
CreateUserDto,
|
||||||
PersonUpdateDto,
|
PersonUpdateDto,
|
||||||
SharedLinkCreateDto,
|
SharedLinkCreateDto,
|
||||||
|
ValidateLibraryDto,
|
||||||
createAlbum,
|
createAlbum,
|
||||||
createApiKey,
|
createApiKey,
|
||||||
|
createLibrary,
|
||||||
createPerson,
|
createPerson,
|
||||||
createSharedLink,
|
createSharedLink,
|
||||||
createUser,
|
createUser,
|
||||||
@ -17,16 +21,18 @@ import {
|
|||||||
setAdminOnboarding,
|
setAdminOnboarding,
|
||||||
signUpAdmin,
|
signUpAdmin,
|
||||||
updatePerson,
|
updatePerson,
|
||||||
|
validate,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { BrowserContext } from '@playwright/test';
|
import { BrowserContext } from '@playwright/test';
|
||||||
import { exec, spawn } from 'child_process';
|
import { exec, spawn } from 'node:child_process';
|
||||||
import { randomBytes } from 'node:crypto';
|
|
||||||
import { access } from 'node:fs/promises';
|
import { access } from 'node:fs/promises';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
import pg from 'pg';
|
import pg from 'pg';
|
||||||
import { io, type Socket } from 'socket.io-client';
|
import { io, type Socket } from 'socket.io-client';
|
||||||
import { loginDto, signupDto } from 'src/fixtures';
|
import { loginDto, signupDto } from 'src/fixtures';
|
||||||
|
import { makeRandomImage } from 'src/generators';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
|
|
||||||
const execPromise = promisify(exec);
|
const execPromise = promisify(exec);
|
||||||
@ -40,6 +46,8 @@ const directoryExists = (directory: string) =>
|
|||||||
|
|
||||||
// TODO move test assets into e2e/assets
|
// TODO move test assets into e2e/assets
|
||||||
export const testAssetDir = path.resolve(`./../server/test/assets/`);
|
export const testAssetDir = path.resolve(`./../server/test/assets/`);
|
||||||
|
export const testAssetDirInternal = '/data/assets';
|
||||||
|
export const tempDir = tmpdir();
|
||||||
|
|
||||||
const serverContainerName = 'immich-e2e-server';
|
const serverContainerName = 'immich-e2e-server';
|
||||||
const mediaDir = '/usr/src/app/upload';
|
const mediaDir = '/usr/src/app/upload';
|
||||||
@ -47,6 +55,7 @@ const dirs = [
|
|||||||
`"${mediaDir}/thumbs"`,
|
`"${mediaDir}/thumbs"`,
|
||||||
`"${mediaDir}/upload"`,
|
`"${mediaDir}/upload"`,
|
||||||
`"${mediaDir}/library"`,
|
`"${mediaDir}/library"`,
|
||||||
|
`"${mediaDir}/encoded-video"`,
|
||||||
].join(' ');
|
].join(' ');
|
||||||
|
|
||||||
if (!(await directoryExists(`${testAssetDir}/albums`))) {
|
if (!(await directoryExists(`${testAssetDir}/albums`))) {
|
||||||
@ -65,20 +74,12 @@ let client: pg.Client | null = null;
|
|||||||
|
|
||||||
export const fileUtils = {
|
export const fileUtils = {
|
||||||
reset: async () => {
|
reset: async () => {
|
||||||
await execPromise(
|
await execPromise(`docker exec -i "${serverContainerName}" /bin/bash -c "rm -rf ${dirs} && mkdir ${dirs}"`);
|
||||||
`docker exec -i "${serverContainerName}" /bin/bash -c "rm -rf ${dirs} && mkdir ${dirs}"`,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dbUtils = {
|
export const dbUtils = {
|
||||||
createFace: async ({
|
createFace: async ({ assetId, personId }: { assetId: string; personId: string }) => {
|
||||||
assetId,
|
|
||||||
personId,
|
|
||||||
}: {
|
|
||||||
assetId: string;
|
|
||||||
personId: string;
|
|
||||||
}) => {
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -86,31 +87,28 @@ export const dbUtils = {
|
|||||||
const vector = Array.from({ length: 512 }, Math.random);
|
const vector = Array.from({ length: 512 }, Math.random);
|
||||||
const embedding = `[${vector.join(',')}]`;
|
const embedding = `[${vector.join(',')}]`;
|
||||||
|
|
||||||
await client.query(
|
await client.query('INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)', [
|
||||||
'INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)',
|
assetId,
|
||||||
[assetId, personId, embedding],
|
personId,
|
||||||
);
|
embedding,
|
||||||
|
]);
|
||||||
},
|
},
|
||||||
setPersonThumbnail: async (personId: string) => {
|
setPersonThumbnail: async (personId: string) => {
|
||||||
if (!client) {
|
if (!client) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.query(
|
await client.query(`UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`, [personId]);
|
||||||
`UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`,
|
|
||||||
[personId],
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
reset: async (tables?: string[]) => {
|
reset: async (tables?: string[]) => {
|
||||||
try {
|
try {
|
||||||
if (!client) {
|
if (!client) {
|
||||||
client = new pg.Client(
|
client = new pg.Client('postgres://postgres:postgres@127.0.0.1:5433/immich');
|
||||||
'postgres://postgres:postgres@127.0.0.1:5433/immich',
|
|
||||||
);
|
|
||||||
await client.connect();
|
await client.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
tables = tables || [
|
tables = tables || [
|
||||||
|
'libraries',
|
||||||
'shared_links',
|
'shared_links',
|
||||||
'person',
|
'person',
|
||||||
'albums',
|
'albums',
|
||||||
@ -177,37 +175,80 @@ export interface AdminSetupOptions {
|
|||||||
onboarding?: boolean;
|
onboarding?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum SocketEvent {
|
||||||
|
UPLOAD = 'upload',
|
||||||
|
DELETE = 'delete',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EventType = 'upload' | 'delete';
|
||||||
|
export interface WaitOptions {
|
||||||
|
event: EventType;
|
||||||
|
assetId: string;
|
||||||
|
timeout?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const events: Record<EventType, Set<string>> = {
|
||||||
|
upload: new Set<string>(),
|
||||||
|
delete: new Set<string>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const callbacks: Record<string, () => void> = {};
|
||||||
|
|
||||||
|
const onEvent = ({ event, assetId }: { event: EventType; assetId: string }) => {
|
||||||
|
events[event].add(assetId);
|
||||||
|
const callback = callbacks[assetId];
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
delete callbacks[assetId];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const wsUtils = {
|
export const wsUtils = {
|
||||||
connect: async (accessToken: string) => {
|
connect: async (accessToken: string) => {
|
||||||
const websocket = io('http://127.0.0.1:2283', {
|
const websocket = io('http://127.0.0.1:2283', {
|
||||||
path: '/api/socket.io',
|
path: '/api/socket.io',
|
||||||
transports: ['websocket'],
|
transports: ['websocket'],
|
||||||
extraHeaders: { Authorization: `Bearer ${accessToken}` },
|
extraHeaders: { Authorization: `Bearer ${accessToken}` },
|
||||||
autoConnect: false,
|
autoConnect: true,
|
||||||
forceNew: true,
|
forceNew: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Promise<Socket>((resolve) => {
|
return new Promise<Socket>((resolve) => {
|
||||||
websocket.on('connect', () => resolve(websocket));
|
websocket
|
||||||
websocket.connect();
|
.on('connect', () => resolve(websocket))
|
||||||
|
.on('on_upload_success', (data: AssetResponseDto) => onEvent({ event: 'upload', assetId: data.id }))
|
||||||
|
.on('on_asset_delete', (assetId: string) => onEvent({ event: 'delete', assetId }))
|
||||||
|
.connect();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
disconnect: (ws: Socket) => {
|
disconnect: (ws: Socket) => {
|
||||||
if (ws?.connected) {
|
if (ws?.connected) {
|
||||||
ws.disconnect();
|
ws.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const set of Object.values(events)) {
|
||||||
|
set.clear();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
once: <T = any>(ws: Socket, event: string): Promise<T> => {
|
waitForEvent: async ({ event, assetId, timeout: ms }: WaitOptions): Promise<void> => {
|
||||||
return new Promise<T>((resolve, reject) => {
|
const set = events[event];
|
||||||
const timeout = setTimeout(() => reject(new Error('Timeout')), 4000);
|
if (set.has(assetId)) {
|
||||||
ws.once(event, (data: T) => {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
const timeout = setTimeout(() => reject(new Error(`Timed out waiting for ${event} event`)), ms || 5000);
|
||||||
|
|
||||||
|
callbacks[assetId] = () => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
resolve(data);
|
resolve();
|
||||||
});
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type AssetData = { bytes?: Buffer; filename: string };
|
||||||
|
|
||||||
export const apiUtils = {
|
export const apiUtils = {
|
||||||
setup: () => {
|
setup: () => {
|
||||||
defaults.baseUrl = app;
|
defaults.baseUrl = app;
|
||||||
@ -224,86 +265,64 @@ export const apiUtils = {
|
|||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
userSetup: async (accessToken: string, dto: CreateUserDto) => {
|
userSetup: async (accessToken: string, dto: CreateUserDto) => {
|
||||||
await createUser(
|
await createUser({ createUserDto: dto }, { headers: asBearerAuth(accessToken) });
|
||||||
{ createUserDto: dto },
|
|
||||||
{ headers: asBearerAuth(accessToken) },
|
|
||||||
);
|
|
||||||
return login({
|
return login({
|
||||||
loginCredentialDto: { email: dto.email, password: dto.password },
|
loginCredentialDto: { email: dto.email, password: dto.password },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
createApiKey: (accessToken: string) => {
|
createApiKey: (accessToken: string) => {
|
||||||
return createApiKey(
|
return createApiKey({ apiKeyCreateDto: { name: 'e2e' } }, { headers: asBearerAuth(accessToken) });
|
||||||
{ apiKeyCreateDto: { name: 'e2e' } },
|
|
||||||
{ headers: asBearerAuth(accessToken) },
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
createAlbum: (accessToken: string, dto: CreateAlbumDto) =>
|
createAlbum: (accessToken: string, dto: CreateAlbumDto) =>
|
||||||
createAlbum(
|
createAlbum({ createAlbumDto: dto }, { headers: asBearerAuth(accessToken) }),
|
||||||
{ createAlbumDto: dto },
|
|
||||||
{ headers: asBearerAuth(accessToken) },
|
|
||||||
),
|
|
||||||
createAsset: async (
|
createAsset: async (
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
dto?: Partial<Omit<CreateAssetDto, 'assetData'>>,
|
dto?: Partial<Omit<CreateAssetDto, 'assetData'>> & { assetData?: AssetData },
|
||||||
data?: {
|
|
||||||
bytes?: Buffer;
|
|
||||||
filename?: string;
|
|
||||||
},
|
|
||||||
) => {
|
) => {
|
||||||
const _dto = {
|
const _dto = {
|
||||||
deviceAssetId: 'test-1',
|
deviceAssetId: 'test-1',
|
||||||
deviceId: 'test',
|
deviceId: 'test',
|
||||||
fileCreatedAt: new Date().toISOString(),
|
fileCreatedAt: new Date().toISOString(),
|
||||||
fileModifiedAt: new Date().toISOString(),
|
fileModifiedAt: new Date().toISOString(),
|
||||||
...(dto || {}),
|
...dto,
|
||||||
};
|
};
|
||||||
|
|
||||||
const _assetData = {
|
const assetData = dto?.assetData?.bytes || makeRandomImage();
|
||||||
bytes: randomBytes(32),
|
const filename = dto?.assetData?.filename || 'example.png';
|
||||||
filename: 'example.jpg',
|
|
||||||
...(data || {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const builder = request(app)
|
const builder = request(app)
|
||||||
.post(`/asset/upload`)
|
.post(`/asset/upload`)
|
||||||
.attach('assetData', _assetData.bytes, _assetData.filename)
|
.attach('assetData', assetData, filename)
|
||||||
.set('Authorization', `Bearer ${accessToken}`);
|
.set('Authorization', `Bearer ${accessToken}`);
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(_dto)) {
|
for (const [key, value] of Object.entries(_dto)) {
|
||||||
builder.field(key, String(value));
|
void builder.field(key, String(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
const { body } = await builder;
|
const { body } = await builder;
|
||||||
|
|
||||||
return body as AssetFileUploadResponseDto;
|
return body as AssetFileUploadResponseDto;
|
||||||
},
|
},
|
||||||
getAssetInfo: (accessToken: string, id: string) =>
|
getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
|
||||||
getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
|
|
||||||
deleteAssets: (accessToken: string, ids: string[]) =>
|
deleteAssets: (accessToken: string, ids: string[]) =>
|
||||||
deleteAssets(
|
deleteAssets({ assetBulkDeleteDto: { ids } }, { headers: asBearerAuth(accessToken) }),
|
||||||
{ assetBulkDeleteDto: { ids } },
|
|
||||||
{ headers: asBearerAuth(accessToken) },
|
|
||||||
),
|
|
||||||
createPerson: async (accessToken: string, dto?: PersonUpdateDto) => {
|
createPerson: async (accessToken: string, dto?: PersonUpdateDto) => {
|
||||||
// TODO fix createPerson to accept a body
|
// TODO fix createPerson to accept a body
|
||||||
let person = await createPerson({ headers: asBearerAuth(accessToken) });
|
const person = await createPerson({ headers: asBearerAuth(accessToken) });
|
||||||
await dbUtils.setPersonThumbnail(person.id);
|
await dbUtils.setPersonThumbnail(person.id);
|
||||||
|
|
||||||
if (!dto) {
|
if (!dto) {
|
||||||
return person;
|
return person;
|
||||||
}
|
}
|
||||||
|
|
||||||
return updatePerson(
|
return updatePerson({ id: person.id, personUpdateDto: dto }, { headers: asBearerAuth(accessToken) });
|
||||||
{ id: person.id, personUpdateDto: dto },
|
|
||||||
{ headers: asBearerAuth(accessToken) },
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
createSharedLink: (accessToken: string, dto: SharedLinkCreateDto) =>
|
createSharedLink: (accessToken: string, dto: SharedLinkCreateDto) =>
|
||||||
createSharedLink(
|
createSharedLink({ sharedLinkCreateDto: dto }, { headers: asBearerAuth(accessToken) }),
|
||||||
{ sharedLinkCreateDto: dto },
|
createLibrary: (accessToken: string, dto: CreateLibraryDto) =>
|
||||||
{ headers: asBearerAuth(accessToken) },
|
createLibrary({ createLibraryDto: dto }, { headers: asBearerAuth(accessToken) }),
|
||||||
),
|
validateLibrary: (accessToken: string, id: string, dto: ValidateLibraryDto) =>
|
||||||
|
validate({ id, validateLibraryDto: dto }, { headers: asBearerAuth(accessToken) }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cliUtils = {
|
export const cliUtils = {
|
||||||
@ -323,7 +342,7 @@ export const webUtils = {
|
|||||||
value: accessToken,
|
value: accessToken,
|
||||||
domain: '127.0.0.1',
|
domain: '127.0.0.1',
|
||||||
path: '/',
|
path: '/',
|
||||||
expires: 1742402728,
|
expires: 1_742_402_728,
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
sameSite: 'Lax',
|
sameSite: 'Lax',
|
||||||
@ -333,7 +352,7 @@ export const webUtils = {
|
|||||||
value: 'password',
|
value: 'password',
|
||||||
domain: '127.0.0.1',
|
domain: '127.0.0.1',
|
||||||
path: '/',
|
path: '/',
|
||||||
expires: 1742402728,
|
expires: 1_742_402_728,
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
sameSite: 'Lax',
|
sameSite: 'Lax',
|
||||||
@ -343,7 +362,7 @@ export const webUtils = {
|
|||||||
value: 'true',
|
value: 'true',
|
||||||
domain: '127.0.0.1',
|
domain: '127.0.0.1',
|
||||||
path: '/',
|
path: '/',
|
||||||
expires: 1742402728,
|
expires: 1_742_402_728,
|
||||||
httpOnly: false,
|
httpOnly: false,
|
||||||
secure: false,
|
secure: false,
|
||||||
sameSite: 'Lax',
|
sameSite: 'Lax',
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
import { apiUtils, dbUtils, webUtils } from 'src/utils';
|
import { apiUtils, dbUtils, webUtils } from 'src/utils';
|
||||||
|
|
||||||
test.describe('Registration', () => {
|
test.describe('Registration', () => {
|
||||||
@ -68,7 +68,7 @@ test.describe('Registration', () => {
|
|||||||
await page.getByRole('button', { name: 'Login' }).click();
|
await page.getByRole('button', { name: 'Login' }).click();
|
||||||
|
|
||||||
// change password
|
// change password
|
||||||
expect(page.getByRole('heading')).toHaveText('Change Password');
|
await expect(page.getByRole('heading')).toHaveText('Change Password');
|
||||||
await expect(page).toHaveURL('/auth/change-password');
|
await expect(page).toHaveURL('/auth/change-password');
|
||||||
await page.getByLabel('New Password').fill('new-password');
|
await page.getByLabel('New Password').fill('new-password');
|
||||||
await page.getByLabel('Confirm Password').fill('new-password');
|
await page.getByLabel('Confirm Password').fill('new-password');
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
import {
|
import {
|
||||||
AlbumResponseDto,
|
AlbumResponseDto,
|
||||||
AssetResponseDto,
|
AssetFileUploadResponseDto,
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
SharedLinkResponseDto,
|
SharedLinkResponseDto,
|
||||||
SharedLinkType,
|
SharedLinkType,
|
||||||
createAlbum,
|
createAlbum,
|
||||||
createSharedLink,
|
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { test } from '@playwright/test';
|
import { test } from '@playwright/test';
|
||||||
import { apiUtils, asBearerAuth, dbUtils } from 'src/utils';
|
import { apiUtils, asBearerAuth, dbUtils } from 'src/utils';
|
||||||
|
|
||||||
test.describe('Shared Links', () => {
|
test.describe('Shared Links', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let asset: AssetResponseDto;
|
let asset: AssetFileUploadResponseDto;
|
||||||
let album: AlbumResponseDto;
|
let album: AlbumResponseDto;
|
||||||
let sharedLink: SharedLinkResponseDto;
|
let sharedLink: SharedLinkResponseDto;
|
||||||
let sharedLinkPassword: SharedLinkResponseDto;
|
let sharedLinkPassword: SharedLinkResponseDto;
|
||||||
@ -29,7 +28,7 @@ test.describe('Shared Links', () => {
|
|||||||
assetIds: [asset.id],
|
assetIds: [asset.id],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ headers: asBearerAuth(admin.accessToken) }
|
{ headers: asBearerAuth(admin.accessToken) },
|
||||||
);
|
);
|
||||||
sharedLink = await apiUtils.createSharedLink(admin.accessToken, {
|
sharedLink = await apiUtils.createSharedLink(admin.accessToken, {
|
||||||
type: SharedLinkType.Album,
|
type: SharedLinkType.Album,
|
||||||
@ -53,7 +52,7 @@ test.describe('Shared Links', () => {
|
|||||||
await page.waitForSelector('#asset-group-by-date svg');
|
await page.waitForSelector('#asset-group-by-date svg');
|
||||||
await page.getByRole('checkbox').click();
|
await page.getByRole('checkbox').click();
|
||||||
await page.getByRole('button', { name: 'Download' }).click();
|
await page.getByRole('button', { name: 'Download' }).click();
|
||||||
await page.getByText('DOWNLOADING').waitFor();
|
await page.getByText('DOWNLOADING', { exact: true }).waitFor();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('enter password for a shared link', async ({ page }) => {
|
test('enter password for a shared link', async ({ page }) => {
|
||||||
|
@ -18,5 +18,6 @@
|
|||||||
"rootDirs": ["src"],
|
"rootDirs": ["src"],
|
||||||
"baseUrl": "./"
|
"baseUrl": "./"
|
||||||
},
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
"exclude": ["dist", "node_modules"]
|
"exclude": ["dist", "node_modules"]
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,17 @@
|
|||||||
import { defineConfig } from 'vitest/config';
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
// skip `docker compose up` if `make e2e` was already run
|
||||||
|
const globalSetup: string[] = [];
|
||||||
|
try {
|
||||||
|
await fetch('http://127.0.0.1:2283/api/server-info/ping');
|
||||||
|
} catch {
|
||||||
|
globalSetup.push('src/setup.ts');
|
||||||
|
}
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
include: ['src/{api,cli}/specs/*.e2e-spec.ts'],
|
include: ['src/{api,cli}/specs/*.e2e-spec.ts'],
|
||||||
globalSetup: ['src/setup.ts'],
|
globalSetup,
|
||||||
poolOptions: {
|
poolOptions: {
|
||||||
threads: {
|
threads: {
|
||||||
singleThread: true,
|
singleThread: true,
|
||||||
|
@ -39,7 +39,7 @@ FROM python:3.11-slim-bookworm@sha256:ce81dc539f0aedc9114cae640f8352fad83d37461c
|
|||||||
FROM openvino/ubuntu22_runtime:2023.1.0@sha256:002842a9005ba01543b7169ff6f14ecbec82287f09c4d1dd37717f0a8e8754a7 as prod-openvino
|
FROM openvino/ubuntu22_runtime:2023.1.0@sha256:002842a9005ba01543b7169ff6f14ecbec82287f09c4d1dd37717f0a8e8754a7 as prod-openvino
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
FROM nvidia/cuda:11.8.0-cudnn8-runtime-ubuntu22.04@sha256:85fb7ac694079fff1061a0140fd5b5a641997880e12112d92589c3bbb1e8b7ca as prod-cuda
|
FROM nvidia/cuda:12.2.2-cudnn8-runtime-ubuntu22.04@sha256:8b51b1fe922964d73c482a267b5b519e990d90bf744ec7a40419923737caff6d as prod-cuda
|
||||||
|
|
||||||
COPY --from=builder-cuda /usr/local/bin/python3 /usr/local/bin/python3
|
COPY --from=builder-cuda /usr/local/bin/python3 /usr/local/bin/python3
|
||||||
COPY --from=builder-cuda /usr/local/lib/python3.11 /usr/local/lib/python3.11
|
COPY --from=builder-cuda /usr/local/lib/python3.11 /usr/local/lib/python3.11
|
||||||
|
32
machine-learning/poetry.lock
generated
32
machine-learning/poetry.lock
generated
@ -1,4 +1,4 @@
|
|||||||
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
|
# This file is automatically @generated by Poetry 1.8.1 and should not be changed by hand.
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiocache"
|
name = "aiocache"
|
||||||
@ -2030,21 +2030,21 @@ sympy = "*"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "onnxruntime-gpu"
|
name = "onnxruntime-gpu"
|
||||||
version = "1.17.0"
|
version = "1.17.1"
|
||||||
description = "ONNX Runtime is a runtime accelerator for Machine Learning models"
|
description = "ONNX Runtime is a runtime accelerator for Machine Learning models"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
files = [
|
files = [
|
||||||
{file = "onnxruntime_gpu-1.17.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1f2a4e0468ac0bd8246996c3d5dbba92cbbaca874bcd7f9cee4e99ce6eb27f5b"},
|
{file = "onnxruntime_gpu-1.17.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:e34ecb2b527ee1265135ae74cd99ea198ff344b8221929a920596a1e461e2bbb"},
|
||||||
{file = "onnxruntime_gpu-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:0721b7930d7abed3730b2335e639e60d94ec411bb4d35a0347cc9c8b52c34540"},
|
{file = "onnxruntime_gpu-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:37786c0f225be90da0a66ca413fe125a925a0900263301cc4dbcad4ff0404673"},
|
||||||
{file = "onnxruntime_gpu-1.17.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:be0314afe399943904de7c1ca797cbcc63e6fad60eb85d3df6422f81dd94e79e"},
|
{file = "onnxruntime_gpu-1.17.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:3bde190a683ec84ecf61bd390f3c275d388efe72404633df374c52c557ce6d4d"},
|
||||||
{file = "onnxruntime_gpu-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:52125c24b21406d1431e43de1c98cea29c21e0cceba80db530b7e4c9216d86ea"},
|
{file = "onnxruntime_gpu-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:5206c84caa770efcc2ca819f71ec007a244ed748ca04e7ff76b86df1a096d2c8"},
|
||||||
{file = "onnxruntime_gpu-1.17.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:bb802d8033885c412269f8bc8877d8779b0dc874df6fb9df8b796cba7276ad66"},
|
{file = "onnxruntime_gpu-1.17.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0396ec73de565a64509d96dff154f531f8da8023c191f771ceba47a3f4efc266"},
|
||||||
{file = "onnxruntime_gpu-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:8c43533e3e5335eaa78059fb86b849a4faded513a00c1feaaa205ca5af51c40f"},
|
{file = "onnxruntime_gpu-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:8531d4a833c8e978c5ff1de7b3bcc4126bbe58ea71fae54ddce58fe8777cb136"},
|
||||||
{file = "onnxruntime_gpu-1.17.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:1d461455bba160836d6c11c648c8fd4e4500d5c17096a13e6c2c9d22a4abd436"},
|
{file = "onnxruntime_gpu-1.17.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7b831f9eafd626f3d44955420a4b1b84f9ffcb987712a0ab6a37d1ee9f2f7a45"},
|
||||||
{file = "onnxruntime_gpu-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4398f2175a92f4b35d95279a6294a89c462f24de058a2736ee1d498bab5a16"},
|
{file = "onnxruntime_gpu-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:a389334d3797519d4b12077db32b8764f1ce54374d0f89235edc04efe8bc192c"},
|
||||||
{file = "onnxruntime_gpu-1.17.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1d0e3805cd1c024aba7f4ae576fd08545fc27530a2aaad2b3c8ac0ee889fbd05"},
|
{file = "onnxruntime_gpu-1.17.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:27aeaa36385e459b3867577ed7f68c1756de79aa68f57141d4ae2a31c84f6a33"},
|
||||||
{file = "onnxruntime_gpu-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc1da5b93363ee600b5b220b04eeec51ad2c2b3e96f0b7615b16b8a173c88001"},
|
{file = "onnxruntime_gpu-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:8b46094ea348aff6c6494402ac4260e2d2aba0522ae13e1ae29d98a29384ed70"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@ -2055,6 +2055,11 @@ packaging = "*"
|
|||||||
protobuf = "*"
|
protobuf = "*"
|
||||||
sympy = "*"
|
sympy = "*"
|
||||||
|
|
||||||
|
[package.source]
|
||||||
|
type = "legacy"
|
||||||
|
url = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple"
|
||||||
|
reference = "cuda12"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "onnxruntime-openvino"
|
name = "onnxruntime-openvino"
|
||||||
version = "1.15.0"
|
version = "1.15.0"
|
||||||
@ -2628,7 +2633,6 @@ files = [
|
|||||||
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
|
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
|
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
|
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
|
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
|
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
|
||||||
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
|
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
|
||||||
@ -3619,4 +3623,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = ">=3.10,<3.12"
|
python-versions = ">=3.10,<3.12"
|
||||||
content-hash = "c982d5c5fee76ca102d823010a538f287ac98583f330ebee3c0775c5f42f117d"
|
content-hash = "c947090d326e81179054b7ce4dded311df8b7ca5a56680d5e9459cf8ca18df1a"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "machine-learning"
|
name = "machine-learning"
|
||||||
version = "1.95.1"
|
version = "1.97.0"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@ -45,7 +45,7 @@ onnxruntime = "^1.15.0"
|
|||||||
optional = true
|
optional = true
|
||||||
|
|
||||||
[tool.poetry.group.cuda.dependencies]
|
[tool.poetry.group.cuda.dependencies]
|
||||||
onnxruntime-gpu = "^1.15.0"
|
onnxruntime-gpu = {version = "^1.17.0", source = "cuda12"}
|
||||||
|
|
||||||
[tool.poetry.group.openvino]
|
[tool.poetry.group.openvino]
|
||||||
optional = true
|
optional = true
|
||||||
@ -59,6 +59,11 @@ optional = true
|
|||||||
[tool.poetry.group.armnn.dependencies]
|
[tool.poetry.group.armnn.dependencies]
|
||||||
onnxruntime = "^1.15.0"
|
onnxruntime = "^1.15.0"
|
||||||
|
|
||||||
|
[[tool.poetry.source]]
|
||||||
|
name = "cuda12"
|
||||||
|
url = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/"
|
||||||
|
priority = "explicit"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
@ -63,6 +63,7 @@ if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then
|
|||||||
echo "Pumping Server: $CURRENT_SERVER => $NEXT_SERVER"
|
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"
|
npm --prefix web version "$SERVER_PUMP"
|
||||||
|
npm --prefix open-api/typescript-sdk version "$SERVER_PUMP"
|
||||||
make open-api
|
make open-api
|
||||||
poetry --directory machine-learning version "$SERVER_PUMP"
|
poetry --directory machine-learning version "$SERVER_PUMP"
|
||||||
fi
|
fi
|
||||||
|
@ -35,8 +35,8 @@ platform :android do
|
|||||||
task: 'bundle',
|
task: 'bundle',
|
||||||
build_type: 'Release',
|
build_type: 'Release',
|
||||||
properties: {
|
properties: {
|
||||||
"android.injected.version.code" => 123,
|
"android.injected.version.code" => 125,
|
||||||
"android.injected.version.name" => "1.95.1",
|
"android.injected.version.name" => "1.97.0",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
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')
|
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.000232">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000266">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="78.881681">
|
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="81.342186">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="32.080999">
|
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="48.746195">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
@ -379,7 +379,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 139;
|
CURRENT_PROJECT_VERSION = 141;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@ -515,7 +515,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 139;
|
CURRENT_PROJECT_VERSION = 141;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@ -543,7 +543,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 139;
|
CURRENT_PROJECT_VERSION = 141;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
@ -55,11 +55,11 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.95.0</string>
|
<string>1.97.0</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>139</string>
|
<string>141</string>
|
||||||
<key>FLTEnableImpeller</key>
|
<key>FLTEnableImpeller</key>
|
||||||
<true />
|
<true />
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
@ -19,7 +19,7 @@ platform :ios do
|
|||||||
desc "iOS Beta"
|
desc "iOS Beta"
|
||||||
lane :beta do
|
lane :beta do
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: "1.95.1"
|
version_number: "1.97.0"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
@ -5,32 +5,32 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000255">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000304">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.157832">
|
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.272646">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="4.825919">
|
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="3.560896">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.18815">
|
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.235745">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="4: build_app" time="110.912709">
|
<testcase classname="fastlane.lanes" name="4: build_app" time="114.820395">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="78.396901">
|
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="68.950812">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
@ -132,7 +133,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
void toggleFavorite(Asset asset) =>
|
void toggleFavorite(Asset asset) =>
|
||||||
ref.read(assetProvider.notifier).toggleFavorite([asset]);
|
ref.read(assetProvider.notifier).toggleFavorite([asset]);
|
||||||
|
|
||||||
void precacheNextImage(int index) {
|
Future<void> precacheNextImage(int index) async {
|
||||||
void onError(Object exception, StackTrace? stackTrace) {
|
void onError(Object exception, StackTrace? stackTrace) {
|
||||||
// swallow error silently
|
// swallow error silently
|
||||||
debugPrint('Error precaching next image: $exception, $stackTrace');
|
debugPrint('Error precaching next image: $exception, $stackTrace');
|
||||||
@ -140,7 +141,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (index < totalAssets && index >= 0) {
|
if (index < totalAssets && index >= 0) {
|
||||||
final asset = loadAsset(index);
|
final asset = loadAsset(index);
|
||||||
precacheImage(
|
await precacheImage(
|
||||||
ImmichImage.imageProvider(asset: asset),
|
ImmichImage.imageProvider(asset: asset),
|
||||||
context,
|
context,
|
||||||
onError: onError,
|
onError: onError,
|
||||||
@ -711,6 +712,21 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
// No need to await this
|
||||||
|
unawaited(
|
||||||
|
// Delay this a bit so we can finish loading the page
|
||||||
|
Future.delayed(const Duration(milliseconds: 400)).then(
|
||||||
|
// Precache the next image
|
||||||
|
(_) => precacheNextImage(currentIndex.value + 1),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
ref.listen(showControlsProvider, (_, show) {
|
ref.listen(showControlsProvider, (_, show) {
|
||||||
if (show) {
|
if (show) {
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
@ -735,14 +751,21 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
isZoomed.value = state != PhotoViewScaleState.initial;
|
isZoomed.value = state != PhotoViewScaleState.initial;
|
||||||
ref.read(showControlsProvider.notifier).show = !isZoomed.value;
|
ref.read(showControlsProvider.notifier).show = !isZoomed.value;
|
||||||
},
|
},
|
||||||
loadingBuilder: (context, event, index) => ImageFiltered(
|
loadingBuilder: (context, event, index) => ClipRect(
|
||||||
imageFilter: ui.ImageFilter.blur(
|
child: Stack(
|
||||||
sigmaX: 1,
|
fit: StackFit.expand,
|
||||||
sigmaY: 1,
|
children: [
|
||||||
),
|
BackdropFilter(
|
||||||
child: ImmichThumbnail(
|
filter: ui.ImageFilter.blur(
|
||||||
asset: asset(),
|
sigmaX: 10,
|
||||||
fit: BoxFit.contain,
|
sigmaY: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ImmichThumbnail(
|
||||||
|
asset: asset(),
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
pageController: controller,
|
pageController: controller,
|
||||||
@ -754,12 +777,16 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
itemCount: totalAssets,
|
itemCount: totalAssets,
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
onPageChanged: (value) {
|
onPageChanged: (value) async {
|
||||||
final next = currentIndex.value < value ? value + 1 : value - 1;
|
final next = currentIndex.value < value ? value + 1 : value - 1;
|
||||||
precacheNextImage(next);
|
HapticFeedback.selectionClick();
|
||||||
currentIndex.value = value;
|
currentIndex.value = value;
|
||||||
stackIndex.value = -1;
|
stackIndex.value = -1;
|
||||||
HapticFeedback.selectionClick();
|
|
||||||
|
// Wait for page change animation to finish
|
||||||
|
await Future.delayed(const Duration(milliseconds: 400));
|
||||||
|
// Then precache the next image
|
||||||
|
unawaited(precacheNextImage(next));
|
||||||
},
|
},
|
||||||
builder: (context, index) {
|
builder: (context, index) {
|
||||||
final a =
|
final a =
|
||||||
@ -818,7 +845,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
isMotionVideo: isPlayingMotionVideo.value,
|
isMotionVideo: isPlayingMotionVideo.value,
|
||||||
placeholder: Image(
|
placeholder: Image(
|
||||||
image: provider,
|
image: provider,
|
||||||
fit: BoxFit.fitWidth,
|
fit: BoxFit.contain,
|
||||||
height: context.height,
|
height: context.height,
|
||||||
width: context.width,
|
width: context.width,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
|
@ -40,7 +40,7 @@ class VideoViewerPage extends HookWidget {
|
|||||||
controlsSafeAreaMinimum: const EdgeInsets.only(
|
controlsSafeAreaMinimum: const EdgeInsets.only(
|
||||||
bottom: 100,
|
bottom: 100,
|
||||||
),
|
),
|
||||||
placeholder: SizedBox.expand(child: placeholder),
|
placeholder: placeholder,
|
||||||
showControls: showControls && !isMotionVideo,
|
showControls: showControls && !isMotionVideo,
|
||||||
hideControlsTimer: hideControlsTimer,
|
hideControlsTimer: hideControlsTimer,
|
||||||
customControls: const VideoPlayerControls(),
|
customControls: const VideoPlayerControls(),
|
||||||
@ -58,9 +58,13 @@ class VideoViewerPage extends HookWidget {
|
|||||||
if (controller == null) {
|
if (controller == null) {
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
if (placeholder != null) SizedBox.expand(child: placeholder!),
|
if (placeholder != null) placeholder!,
|
||||||
const DelayedLoadingIndicator(
|
const Positioned.fill(
|
||||||
fadeInDuration: Duration(milliseconds: 500),
|
child: Center(
|
||||||
|
child: DelayedLoadingIndicator(
|
||||||
|
fadeInDuration: Duration(milliseconds: 500),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -226,7 +226,7 @@ class ControlBottomAppBar extends ConsumerWidget {
|
|||||||
if (selectionAssetState.hasLocal)
|
if (selectionAssetState.hasLocal)
|
||||||
ControlBoxButton(
|
ControlBoxButton(
|
||||||
iconData: Icons.backup_outlined,
|
iconData: Icons.backup_outlined,
|
||||||
label: "Upload",
|
label: "control_bottom_app_bar_upload".tr(),
|
||||||
onPressed: enabled
|
onPressed: enabled
|
||||||
? () => showDialog(
|
? () => showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
|
@ -124,11 +124,14 @@ class MemoryPage extends HookConsumerWidget {
|
|||||||
.then((_) => precacheAsset(1));
|
.then((_) => precacheAsset(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
onAssetChanged(int otherIndex) {
|
Future<void> onAssetChanged(int otherIndex) async {
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
currentAssetPage.value = otherIndex;
|
currentAssetPage.value = otherIndex;
|
||||||
precacheAsset(otherIndex + 1);
|
|
||||||
updateProgressText();
|
updateProgressText();
|
||||||
|
// Wait for page change animation to finish
|
||||||
|
await Future.delayed(const Duration(milliseconds: 400));
|
||||||
|
// And then precache the next asset
|
||||||
|
await precacheAsset(otherIndex + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Notification listener is used instead of OnPageChanged callback since OnPageChanged is called
|
/* Notification listener is used instead of OnPageChanged callback since OnPageChanged is called
|
||||||
|
@ -20,21 +20,24 @@ class DelayedLoadingIndicator extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AnimatedSwitcher(
|
return FutureBuilder(
|
||||||
duration: fadeInDuration ?? Duration.zero,
|
future: Future.delayed(delay),
|
||||||
child: FutureBuilder(
|
builder: (context, snapshot) {
|
||||||
future: Future.delayed(delay),
|
late Widget c;
|
||||||
builder: (context, snapshot) {
|
if (snapshot.connectionState == ConnectionState.done) {
|
||||||
if (snapshot.connectionState == ConnectionState.done) {
|
c = child ??
|
||||||
return child ??
|
const ImmichLoadingIndicator(
|
||||||
const ImmichLoadingIndicator(
|
key: ValueKey('loading'),
|
||||||
key: ValueKey('loading'),
|
);
|
||||||
);
|
} else {
|
||||||
}
|
c = Container(key: const ValueKey('hiding'));
|
||||||
|
}
|
||||||
|
|
||||||
return Container(key: const ValueKey('hiding'));
|
return AnimatedSwitcher(
|
||||||
},
|
duration: fadeInDuration ?? Duration.zero,
|
||||||
),
|
child: c,
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,9 +58,11 @@ class ImmichImage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Whether to use the local asset image provider or a remote one
|
||||||
static bool useLocal(Asset asset) =>
|
static bool useLocal(Asset asset) =>
|
||||||
!asset.isRemote ||
|
!asset.isRemote ||
|
||||||
asset.isLocal && !Store.get(StoreKey.preferRemoteImage, false);
|
asset.isLocal && !Store.get(StoreKey.preferRemoteImage, false);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (asset == null) {
|
if (asset == null) {
|
||||||
|
@ -6,6 +6,7 @@ import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_local_
|
|||||||
import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_image_provider.dart';
|
import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_image_provider.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/ui/hooks/blurhash_hook.dart';
|
import 'package:immich_mobile/shared/ui/hooks/blurhash_hook.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||||
import 'package:immich_mobile/shared/ui/thumbhash_placeholder.dart';
|
import 'package:immich_mobile/shared/ui/thumbhash_placeholder.dart';
|
||||||
import 'package:octo_image/octo_image.dart';
|
import 'package:octo_image/octo_image.dart';
|
||||||
|
|
||||||
@ -43,7 +44,7 @@ class ImmichThumbnail extends HookWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useLocal(asset)) {
|
if (ImmichImage.useLocal(asset)) {
|
||||||
return ImmichLocalThumbnailProvider(
|
return ImmichLocalThumbnailProvider(
|
||||||
asset: asset,
|
asset: asset,
|
||||||
height: thumbnailSize,
|
height: thumbnailSize,
|
||||||
@ -57,8 +58,6 @@ class ImmichThumbnail extends HookWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool useLocal(Asset asset) => !asset.isRemote || asset.isLocal;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Uint8List? blurhash = useBlurHashRef(asset).value;
|
Uint8List? blurhash = useBlurHashRef(asset).value;
|
||||||
|
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:
|
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||||
|
|
||||||
- API version: 1.95.1
|
- API version: 1.97.0
|
||||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
@ -135,8 +135,8 @@ Class | Method | HTTP request | Description
|
|||||||
*JobApi* | [**sendJobCommand**](doc//JobApi.md#sendjobcommand) | **PUT** /jobs/{id} |
|
*JobApi* | [**sendJobCommand**](doc//JobApi.md#sendjobcommand) | **PUT** /jobs/{id} |
|
||||||
*LibraryApi* | [**createLibrary**](doc//LibraryApi.md#createlibrary) | **POST** /library |
|
*LibraryApi* | [**createLibrary**](doc//LibraryApi.md#createlibrary) | **POST** /library |
|
||||||
*LibraryApi* | [**deleteLibrary**](doc//LibraryApi.md#deletelibrary) | **DELETE** /library/{id} |
|
*LibraryApi* | [**deleteLibrary**](doc//LibraryApi.md#deletelibrary) | **DELETE** /library/{id} |
|
||||||
*LibraryApi* | [**getLibraries**](doc//LibraryApi.md#getlibraries) | **GET** /library |
|
*LibraryApi* | [**getAllLibraries**](doc//LibraryApi.md#getalllibraries) | **GET** /library |
|
||||||
*LibraryApi* | [**getLibraryInfo**](doc//LibraryApi.md#getlibraryinfo) | **GET** /library/{id} |
|
*LibraryApi* | [**getLibrary**](doc//LibraryApi.md#getlibrary) | **GET** /library/{id} |
|
||||||
*LibraryApi* | [**getLibraryStatistics**](doc//LibraryApi.md#getlibrarystatistics) | **GET** /library/{id}/statistics |
|
*LibraryApi* | [**getLibraryStatistics**](doc//LibraryApi.md#getlibrarystatistics) | **GET** /library/{id}/statistics |
|
||||||
*LibraryApi* | [**removeOfflineFiles**](doc//LibraryApi.md#removeofflinefiles) | **POST** /library/{id}/removeOffline |
|
*LibraryApi* | [**removeOfflineFiles**](doc//LibraryApi.md#removeofflinefiles) | **POST** /library/{id}/removeOffline |
|
||||||
*LibraryApi* | [**scanLibrary**](doc//LibraryApi.md#scanlibrary) | **POST** /library/{id}/scan |
|
*LibraryApi* | [**scanLibrary**](doc//LibraryApi.md#scanlibrary) | **POST** /library/{id}/scan |
|
||||||
|
6
mobile/openapi/doc/AssetApi.md
generated
6
mobile/openapi/doc/AssetApi.md
generated
@ -1401,7 +1401,7 @@ void (empty response body)
|
|||||||
[[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)
|
[[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)
|
||||||
|
|
||||||
# **uploadFile**
|
# **uploadFile**
|
||||||
> AssetFileUploadResponseDto uploadFile(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key, duration, isArchived, isExternal, isFavorite, isOffline, isReadOnly, isVisible, libraryId, livePhotoData, sidecarData)
|
> AssetFileUploadResponseDto uploadFile(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key, duration, isArchived, isFavorite, isOffline, isReadOnly, isVisible, libraryId, livePhotoData, sidecarData)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -1432,7 +1432,6 @@ final fileModifiedAt = 2013-10-20T19:20:30+01:00; // DateTime |
|
|||||||
final key = key_example; // String |
|
final key = key_example; // String |
|
||||||
final duration = duration_example; // String |
|
final duration = duration_example; // String |
|
||||||
final isArchived = true; // bool |
|
final isArchived = true; // bool |
|
||||||
final isExternal = true; // bool |
|
|
||||||
final isFavorite = true; // bool |
|
final isFavorite = true; // bool |
|
||||||
final isOffline = true; // bool |
|
final isOffline = true; // bool |
|
||||||
final isReadOnly = true; // bool |
|
final isReadOnly = true; // bool |
|
||||||
@ -1442,7 +1441,7 @@ final livePhotoData = BINARY_DATA_HERE; // MultipartFile |
|
|||||||
final sidecarData = BINARY_DATA_HERE; // MultipartFile |
|
final sidecarData = BINARY_DATA_HERE; // MultipartFile |
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final result = api_instance.uploadFile(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key, duration, isArchived, isExternal, isFavorite, isOffline, isReadOnly, isVisible, libraryId, livePhotoData, sidecarData);
|
final result = api_instance.uploadFile(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key, duration, isArchived, isFavorite, isOffline, isReadOnly, isVisible, libraryId, livePhotoData, sidecarData);
|
||||||
print(result);
|
print(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Exception when calling AssetApi->uploadFile: $e\n');
|
print('Exception when calling AssetApi->uploadFile: $e\n');
|
||||||
@ -1461,7 +1460,6 @@ Name | Type | Description | Notes
|
|||||||
**key** | **String**| | [optional]
|
**key** | **String**| | [optional]
|
||||||
**duration** | **String**| | [optional]
|
**duration** | **String**| | [optional]
|
||||||
**isArchived** | **bool**| | [optional]
|
**isArchived** | **bool**| | [optional]
|
||||||
**isExternal** | **bool**| | [optional]
|
|
||||||
**isFavorite** | **bool**| | [optional]
|
**isFavorite** | **bool**| | [optional]
|
||||||
**isOffline** | **bool**| | [optional]
|
**isOffline** | **bool**| | [optional]
|
||||||
**isReadOnly** | **bool**| | [optional]
|
**isReadOnly** | **bool**| | [optional]
|
||||||
|
1
mobile/openapi/doc/CreateLibraryDto.md
generated
1
mobile/openapi/doc/CreateLibraryDto.md
generated
@ -13,6 +13,7 @@ Name | Type | Description | Notes
|
|||||||
**isVisible** | **bool** | | [optional]
|
**isVisible** | **bool** | | [optional]
|
||||||
**isWatched** | **bool** | | [optional]
|
**isWatched** | **bool** | | [optional]
|
||||||
**name** | **String** | | [optional]
|
**name** | **String** | | [optional]
|
||||||
|
**ownerId** | **String** | | [optional]
|
||||||
**type** | [**LibraryType**](LibraryType.md) | |
|
**type** | [**LibraryType**](LibraryType.md) | |
|
||||||
|
|
||||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||||
|
1
mobile/openapi/doc/CreateUserDto.md
generated
1
mobile/openapi/doc/CreateUserDto.md
generated
@ -9,7 +9,6 @@ import 'package:openapi/api.dart';
|
|||||||
Name | Type | Description | Notes
|
Name | Type | Description | Notes
|
||||||
------------ | ------------- | ------------- | -------------
|
------------ | ------------- | ------------- | -------------
|
||||||
**email** | **String** | |
|
**email** | **String** | |
|
||||||
**externalPath** | **String** | | [optional]
|
|
||||||
**memoriesEnabled** | **bool** | | [optional]
|
**memoriesEnabled** | **bool** | | [optional]
|
||||||
**name** | **String** | |
|
**name** | **String** | |
|
||||||
**password** | **String** | |
|
**password** | **String** | |
|
||||||
|
26
mobile/openapi/doc/LibraryApi.md
generated
26
mobile/openapi/doc/LibraryApi.md
generated
@ -11,8 +11,8 @@ Method | HTTP request | Description
|
|||||||
------------- | ------------- | -------------
|
------------- | ------------- | -------------
|
||||||
[**createLibrary**](LibraryApi.md#createlibrary) | **POST** /library |
|
[**createLibrary**](LibraryApi.md#createlibrary) | **POST** /library |
|
||||||
[**deleteLibrary**](LibraryApi.md#deletelibrary) | **DELETE** /library/{id} |
|
[**deleteLibrary**](LibraryApi.md#deletelibrary) | **DELETE** /library/{id} |
|
||||||
[**getLibraries**](LibraryApi.md#getlibraries) | **GET** /library |
|
[**getAllLibraries**](LibraryApi.md#getalllibraries) | **GET** /library |
|
||||||
[**getLibraryInfo**](LibraryApi.md#getlibraryinfo) | **GET** /library/{id} |
|
[**getLibrary**](LibraryApi.md#getlibrary) | **GET** /library/{id} |
|
||||||
[**getLibraryStatistics**](LibraryApi.md#getlibrarystatistics) | **GET** /library/{id}/statistics |
|
[**getLibraryStatistics**](LibraryApi.md#getlibrarystatistics) | **GET** /library/{id}/statistics |
|
||||||
[**removeOfflineFiles**](LibraryApi.md#removeofflinefiles) | **POST** /library/{id}/removeOffline |
|
[**removeOfflineFiles**](LibraryApi.md#removeofflinefiles) | **POST** /library/{id}/removeOffline |
|
||||||
[**scanLibrary**](LibraryApi.md#scanlibrary) | **POST** /library/{id}/scan |
|
[**scanLibrary**](LibraryApi.md#scanlibrary) | **POST** /library/{id}/scan |
|
||||||
@ -129,8 +129,8 @@ void (empty response body)
|
|||||||
|
|
||||||
[[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)
|
[[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)
|
||||||
|
|
||||||
# **getLibraries**
|
# **getAllLibraries**
|
||||||
> List<LibraryResponseDto> getLibraries()
|
> List<LibraryResponseDto> getAllLibraries(type)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -153,17 +153,21 @@ import 'package:openapi/api.dart';
|
|||||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||||
|
|
||||||
final api_instance = LibraryApi();
|
final api_instance = LibraryApi();
|
||||||
|
final type = ; // LibraryType |
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final result = api_instance.getLibraries();
|
final result = api_instance.getAllLibraries(type);
|
||||||
print(result);
|
print(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Exception when calling LibraryApi->getLibraries: $e\n');
|
print('Exception when calling LibraryApi->getAllLibraries: $e\n');
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Parameters
|
### Parameters
|
||||||
This endpoint does not need any parameter.
|
|
||||||
|
Name | Type | Description | Notes
|
||||||
|
------------- | ------------- | ------------- | -------------
|
||||||
|
**type** | [**LibraryType**](.md)| | [optional]
|
||||||
|
|
||||||
### Return type
|
### Return type
|
||||||
|
|
||||||
@ -180,8 +184,8 @@ This endpoint does not need any parameter.
|
|||||||
|
|
||||||
[[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)
|
[[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)
|
||||||
|
|
||||||
# **getLibraryInfo**
|
# **getLibrary**
|
||||||
> LibraryResponseDto getLibraryInfo(id)
|
> LibraryResponseDto getLibrary(id)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -207,10 +211,10 @@ final api_instance = LibraryApi();
|
|||||||
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final result = api_instance.getLibraryInfo(id);
|
final result = api_instance.getLibrary(id);
|
||||||
print(result);
|
print(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Exception when calling LibraryApi->getLibraryInfo: $e\n');
|
print('Exception when calling LibraryApi->getLibrary: $e\n');
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
1
mobile/openapi/doc/PartnerResponseDto.md
generated
1
mobile/openapi/doc/PartnerResponseDto.md
generated
@ -12,7 +12,6 @@ Name | Type | Description | Notes
|
|||||||
**createdAt** | [**DateTime**](DateTime.md) | |
|
**createdAt** | [**DateTime**](DateTime.md) | |
|
||||||
**deletedAt** | [**DateTime**](DateTime.md) | |
|
**deletedAt** | [**DateTime**](DateTime.md) | |
|
||||||
**email** | **String** | |
|
**email** | **String** | |
|
||||||
**externalPath** | **String** | |
|
|
||||||
**id** | **String** | |
|
**id** | **String** | |
|
||||||
**inTimeline** | **bool** | | [optional]
|
**inTimeline** | **bool** | | [optional]
|
||||||
**isAdmin** | **bool** | |
|
**isAdmin** | **bool** | |
|
||||||
|
1
mobile/openapi/doc/SmartSearchDto.md
generated
1
mobile/openapi/doc/SmartSearchDto.md
generated
@ -27,6 +27,7 @@ Name | Type | Description | Notes
|
|||||||
**make** | **String** | | [optional]
|
**make** | **String** | | [optional]
|
||||||
**model** | **String** | | [optional]
|
**model** | **String** | | [optional]
|
||||||
**page** | **num** | | [optional]
|
**page** | **num** | | [optional]
|
||||||
|
**personIds** | **List<String>** | | [optional] [default to const []]
|
||||||
**query** | **String** | |
|
**query** | **String** | |
|
||||||
**size** | **num** | | [optional]
|
**size** | **num** | | [optional]
|
||||||
**state** | **String** | | [optional]
|
**state** | **String** | | [optional]
|
||||||
|
@ -9,8 +9,6 @@ import 'package:openapi/api.dart';
|
|||||||
Name | Type | Description | Notes
|
Name | Type | Description | Notes
|
||||||
------------ | ------------- | ------------- | -------------
|
------------ | ------------- | ------------- | -------------
|
||||||
**enabled** | **bool** | |
|
**enabled** | **bool** | |
|
||||||
**interval** | **int** | |
|
|
||||||
**usePolling** | **bool** | |
|
|
||||||
|
|
||||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
2
mobile/openapi/doc/SystemConfigOAuthDto.md
generated
2
mobile/openapi/doc/SystemConfigOAuthDto.md
generated
@ -13,6 +13,7 @@ Name | Type | Description | Notes
|
|||||||
**buttonText** | **String** | |
|
**buttonText** | **String** | |
|
||||||
**clientId** | **String** | |
|
**clientId** | **String** | |
|
||||||
**clientSecret** | **String** | |
|
**clientSecret** | **String** | |
|
||||||
|
**defaultStorageQuota** | **num** | |
|
||||||
**enabled** | **bool** | |
|
**enabled** | **bool** | |
|
||||||
**issuerUrl** | **String** | |
|
**issuerUrl** | **String** | |
|
||||||
**mobileOverrideEnabled** | **bool** | |
|
**mobileOverrideEnabled** | **bool** | |
|
||||||
@ -20,6 +21,7 @@ Name | Type | Description | Notes
|
|||||||
**scope** | **String** | |
|
**scope** | **String** | |
|
||||||
**signingAlgorithm** | **String** | |
|
**signingAlgorithm** | **String** | |
|
||||||
**storageLabelClaim** | **String** | |
|
**storageLabelClaim** | **String** | |
|
||||||
|
**storageQuotaClaim** | **String** | |
|
||||||
|
|
||||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
1
mobile/openapi/doc/UpdateUserDto.md
generated
1
mobile/openapi/doc/UpdateUserDto.md
generated
@ -10,7 +10,6 @@ Name | Type | Description | Notes
|
|||||||
------------ | ------------- | ------------- | -------------
|
------------ | ------------- | ------------- | -------------
|
||||||
**avatarColor** | [**UserAvatarColor**](UserAvatarColor.md) | | [optional]
|
**avatarColor** | [**UserAvatarColor**](UserAvatarColor.md) | | [optional]
|
||||||
**email** | **String** | | [optional]
|
**email** | **String** | | [optional]
|
||||||
**externalPath** | **String** | | [optional]
|
|
||||||
**id** | **String** | |
|
**id** | **String** | |
|
||||||
**isAdmin** | **bool** | | [optional]
|
**isAdmin** | **bool** | | [optional]
|
||||||
**memoriesEnabled** | **bool** | | [optional]
|
**memoriesEnabled** | **bool** | | [optional]
|
||||||
|
1
mobile/openapi/doc/UserResponseDto.md
generated
1
mobile/openapi/doc/UserResponseDto.md
generated
@ -12,7 +12,6 @@ Name | Type | Description | Notes
|
|||||||
**createdAt** | [**DateTime**](DateTime.md) | |
|
**createdAt** | [**DateTime**](DateTime.md) | |
|
||||||
**deletedAt** | [**DateTime**](DateTime.md) | |
|
**deletedAt** | [**DateTime**](DateTime.md) | |
|
||||||
**email** | **String** | |
|
**email** | **String** | |
|
||||||
**externalPath** | **String** | |
|
|
||||||
**id** | **String** | |
|
**id** | **String** | |
|
||||||
**isAdmin** | **bool** | |
|
**isAdmin** | **bool** | |
|
||||||
**memoriesEnabled** | **bool** | | [optional]
|
**memoriesEnabled** | **bool** | | [optional]
|
||||||
|
14
mobile/openapi/lib/api/asset_api.dart
generated
14
mobile/openapi/lib/api/asset_api.dart
generated
@ -1676,8 +1676,6 @@ class AssetApi {
|
|||||||
///
|
///
|
||||||
/// * [bool] isArchived:
|
/// * [bool] isArchived:
|
||||||
///
|
///
|
||||||
/// * [bool] isExternal:
|
|
||||||
///
|
|
||||||
/// * [bool] isFavorite:
|
/// * [bool] isFavorite:
|
||||||
///
|
///
|
||||||
/// * [bool] isOffline:
|
/// * [bool] isOffline:
|
||||||
@ -1691,7 +1689,7 @@ class AssetApi {
|
|||||||
/// * [MultipartFile] livePhotoData:
|
/// * [MultipartFile] livePhotoData:
|
||||||
///
|
///
|
||||||
/// * [MultipartFile] sidecarData:
|
/// * [MultipartFile] sidecarData:
|
||||||
Future<Response> uploadFileWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? duration, bool? isArchived, bool? isExternal, bool? isFavorite, bool? isOffline, bool? isReadOnly, bool? isVisible, String? libraryId, MultipartFile? livePhotoData, MultipartFile? sidecarData, }) async {
|
Future<Response> uploadFileWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? duration, bool? isArchived, bool? isFavorite, bool? isOffline, bool? isReadOnly, bool? isVisible, String? libraryId, MultipartFile? livePhotoData, MultipartFile? sidecarData, }) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final path = r'/asset/upload';
|
final path = r'/asset/upload';
|
||||||
|
|
||||||
@ -1739,10 +1737,6 @@ class AssetApi {
|
|||||||
hasFields = true;
|
hasFields = true;
|
||||||
mp.fields[r'isArchived'] = parameterToString(isArchived);
|
mp.fields[r'isArchived'] = parameterToString(isArchived);
|
||||||
}
|
}
|
||||||
if (isExternal != null) {
|
|
||||||
hasFields = true;
|
|
||||||
mp.fields[r'isExternal'] = parameterToString(isExternal);
|
|
||||||
}
|
|
||||||
if (isFavorite != null) {
|
if (isFavorite != null) {
|
||||||
hasFields = true;
|
hasFields = true;
|
||||||
mp.fields[r'isFavorite'] = parameterToString(isFavorite);
|
mp.fields[r'isFavorite'] = parameterToString(isFavorite);
|
||||||
@ -1806,8 +1800,6 @@ class AssetApi {
|
|||||||
///
|
///
|
||||||
/// * [bool] isArchived:
|
/// * [bool] isArchived:
|
||||||
///
|
///
|
||||||
/// * [bool] isExternal:
|
|
||||||
///
|
|
||||||
/// * [bool] isFavorite:
|
/// * [bool] isFavorite:
|
||||||
///
|
///
|
||||||
/// * [bool] isOffline:
|
/// * [bool] isOffline:
|
||||||
@ -1821,8 +1813,8 @@ class AssetApi {
|
|||||||
/// * [MultipartFile] livePhotoData:
|
/// * [MultipartFile] livePhotoData:
|
||||||
///
|
///
|
||||||
/// * [MultipartFile] sidecarData:
|
/// * [MultipartFile] sidecarData:
|
||||||
Future<AssetFileUploadResponseDto?> uploadFile(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? duration, bool? isArchived, bool? isExternal, bool? isFavorite, bool? isOffline, bool? isReadOnly, bool? isVisible, String? libraryId, MultipartFile? livePhotoData, MultipartFile? sidecarData, }) async {
|
Future<AssetFileUploadResponseDto?> uploadFile(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? duration, bool? isArchived, bool? isFavorite, bool? isOffline, bool? isReadOnly, bool? isVisible, String? libraryId, MultipartFile? livePhotoData, MultipartFile? sidecarData, }) async {
|
||||||
final response = await uploadFileWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, duration: duration, isArchived: isArchived, isExternal: isExternal, isFavorite: isFavorite, isOffline: isOffline, isReadOnly: isReadOnly, isVisible: isVisible, libraryId: libraryId, livePhotoData: livePhotoData, sidecarData: sidecarData, );
|
final response = await uploadFileWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, duration: duration, isArchived: isArchived, isFavorite: isFavorite, isOffline: isOffline, isReadOnly: isReadOnly, isVisible: isVisible, libraryId: libraryId, livePhotoData: livePhotoData, sidecarData: sidecarData, );
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
|
22
mobile/openapi/lib/api/library_api.dart
generated
22
mobile/openapi/lib/api/library_api.dart
generated
@ -104,7 +104,10 @@ class LibraryApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'GET /library' operation and returns the [Response].
|
/// Performs an HTTP 'GET /library' operation and returns the [Response].
|
||||||
Future<Response> getLibrariesWithHttpInfo() async {
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [LibraryType] type:
|
||||||
|
Future<Response> getAllLibrariesWithHttpInfo({ LibraryType? type, }) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final path = r'/library';
|
final path = r'/library';
|
||||||
|
|
||||||
@ -115,6 +118,10 @@ class LibraryApi {
|
|||||||
final headerParams = <String, String>{};
|
final headerParams = <String, String>{};
|
||||||
final formParams = <String, String>{};
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
if (type != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'type', type));
|
||||||
|
}
|
||||||
|
|
||||||
const contentTypes = <String>[];
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
@ -129,8 +136,11 @@ class LibraryApi {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<LibraryResponseDto>?> getLibraries() async {
|
/// Parameters:
|
||||||
final response = await getLibrariesWithHttpInfo();
|
///
|
||||||
|
/// * [LibraryType] type:
|
||||||
|
Future<List<LibraryResponseDto>?> getAllLibraries({ LibraryType? type, }) async {
|
||||||
|
final response = await getAllLibrariesWithHttpInfo( type: type, );
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
@ -151,7 +161,7 @@ class LibraryApi {
|
|||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
/// * [String] id (required):
|
/// * [String] id (required):
|
||||||
Future<Response> getLibraryInfoWithHttpInfo(String id,) async {
|
Future<Response> getLibraryWithHttpInfo(String id,) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final path = r'/library/{id}'
|
final path = r'/library/{id}'
|
||||||
.replaceAll('{id}', id);
|
.replaceAll('{id}', id);
|
||||||
@ -180,8 +190,8 @@ class LibraryApi {
|
|||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
/// * [String] id (required):
|
/// * [String] id (required):
|
||||||
Future<LibraryResponseDto?> getLibraryInfo(String id,) async {
|
Future<LibraryResponseDto?> getLibrary(String id,) async {
|
||||||
final response = await getLibraryInfoWithHttpInfo(id,);
|
final response = await getLibraryWithHttpInfo(id,);
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
|
19
mobile/openapi/lib/model/create_library_dto.dart
generated
19
mobile/openapi/lib/model/create_library_dto.dart
generated
@ -18,6 +18,7 @@ class CreateLibraryDto {
|
|||||||
this.isVisible,
|
this.isVisible,
|
||||||
this.isWatched,
|
this.isWatched,
|
||||||
this.name,
|
this.name,
|
||||||
|
this.ownerId,
|
||||||
required this.type,
|
required this.type,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -49,6 +50,14 @@ class CreateLibraryDto {
|
|||||||
///
|
///
|
||||||
String? name;
|
String? name;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// 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? ownerId;
|
||||||
|
|
||||||
LibraryType type;
|
LibraryType type;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -58,6 +67,7 @@ class CreateLibraryDto {
|
|||||||
other.isVisible == isVisible &&
|
other.isVisible == isVisible &&
|
||||||
other.isWatched == isWatched &&
|
other.isWatched == isWatched &&
|
||||||
other.name == name &&
|
other.name == name &&
|
||||||
|
other.ownerId == ownerId &&
|
||||||
other.type == type;
|
other.type == type;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -68,10 +78,11 @@ class CreateLibraryDto {
|
|||||||
(isVisible == null ? 0 : isVisible!.hashCode) +
|
(isVisible == null ? 0 : isVisible!.hashCode) +
|
||||||
(isWatched == null ? 0 : isWatched!.hashCode) +
|
(isWatched == null ? 0 : isWatched!.hashCode) +
|
||||||
(name == null ? 0 : name!.hashCode) +
|
(name == null ? 0 : name!.hashCode) +
|
||||||
|
(ownerId == null ? 0 : ownerId!.hashCode) +
|
||||||
(type.hashCode);
|
(type.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'CreateLibraryDto[exclusionPatterns=$exclusionPatterns, importPaths=$importPaths, isVisible=$isVisible, isWatched=$isWatched, name=$name, type=$type]';
|
String toString() => 'CreateLibraryDto[exclusionPatterns=$exclusionPatterns, importPaths=$importPaths, isVisible=$isVisible, isWatched=$isWatched, name=$name, ownerId=$ownerId, type=$type]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -91,6 +102,11 @@ class CreateLibraryDto {
|
|||||||
json[r'name'] = this.name;
|
json[r'name'] = this.name;
|
||||||
} else {
|
} else {
|
||||||
// json[r'name'] = null;
|
// json[r'name'] = null;
|
||||||
|
}
|
||||||
|
if (this.ownerId != null) {
|
||||||
|
json[r'ownerId'] = this.ownerId;
|
||||||
|
} else {
|
||||||
|
// json[r'ownerId'] = null;
|
||||||
}
|
}
|
||||||
json[r'type'] = this.type;
|
json[r'type'] = this.type;
|
||||||
return json;
|
return json;
|
||||||
@ -113,6 +129,7 @@ class CreateLibraryDto {
|
|||||||
isVisible: mapValueOfType<bool>(json, r'isVisible'),
|
isVisible: mapValueOfType<bool>(json, r'isVisible'),
|
||||||
isWatched: mapValueOfType<bool>(json, r'isWatched'),
|
isWatched: mapValueOfType<bool>(json, r'isWatched'),
|
||||||
name: mapValueOfType<String>(json, r'name'),
|
name: mapValueOfType<String>(json, r'name'),
|
||||||
|
ownerId: mapValueOfType<String>(json, r'ownerId'),
|
||||||
type: LibraryType.fromJson(json[r'type'])!,
|
type: LibraryType.fromJson(json[r'type'])!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
13
mobile/openapi/lib/model/create_user_dto.dart
generated
13
mobile/openapi/lib/model/create_user_dto.dart
generated
@ -14,7 +14,6 @@ class CreateUserDto {
|
|||||||
/// Returns a new [CreateUserDto] instance.
|
/// Returns a new [CreateUserDto] instance.
|
||||||
CreateUserDto({
|
CreateUserDto({
|
||||||
required this.email,
|
required this.email,
|
||||||
this.externalPath,
|
|
||||||
this.memoriesEnabled,
|
this.memoriesEnabled,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.password,
|
required this.password,
|
||||||
@ -24,8 +23,6 @@ class CreateUserDto {
|
|||||||
|
|
||||||
String email;
|
String email;
|
||||||
|
|
||||||
String? externalPath;
|
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
/// 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
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
@ -45,7 +42,6 @@ class CreateUserDto {
|
|||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is CreateUserDto &&
|
bool operator ==(Object other) => identical(this, other) || other is CreateUserDto &&
|
||||||
other.email == email &&
|
other.email == email &&
|
||||||
other.externalPath == externalPath &&
|
|
||||||
other.memoriesEnabled == memoriesEnabled &&
|
other.memoriesEnabled == memoriesEnabled &&
|
||||||
other.name == name &&
|
other.name == name &&
|
||||||
other.password == password &&
|
other.password == password &&
|
||||||
@ -56,7 +52,6 @@ class CreateUserDto {
|
|||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(email.hashCode) +
|
(email.hashCode) +
|
||||||
(externalPath == null ? 0 : externalPath!.hashCode) +
|
|
||||||
(memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) +
|
(memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) +
|
||||||
(name.hashCode) +
|
(name.hashCode) +
|
||||||
(password.hashCode) +
|
(password.hashCode) +
|
||||||
@ -64,16 +59,11 @@ class CreateUserDto {
|
|||||||
(storageLabel == null ? 0 : storageLabel!.hashCode);
|
(storageLabel == null ? 0 : storageLabel!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'CreateUserDto[email=$email, externalPath=$externalPath, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, storageLabel=$storageLabel]';
|
String toString() => 'CreateUserDto[email=$email, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, storageLabel=$storageLabel]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
json[r'email'] = this.email;
|
json[r'email'] = this.email;
|
||||||
if (this.externalPath != null) {
|
|
||||||
json[r'externalPath'] = this.externalPath;
|
|
||||||
} else {
|
|
||||||
// json[r'externalPath'] = null;
|
|
||||||
}
|
|
||||||
if (this.memoriesEnabled != null) {
|
if (this.memoriesEnabled != null) {
|
||||||
json[r'memoriesEnabled'] = this.memoriesEnabled;
|
json[r'memoriesEnabled'] = this.memoriesEnabled;
|
||||||
} else {
|
} else {
|
||||||
@ -103,7 +93,6 @@ class CreateUserDto {
|
|||||||
|
|
||||||
return CreateUserDto(
|
return CreateUserDto(
|
||||||
email: mapValueOfType<String>(json, r'email')!,
|
email: mapValueOfType<String>(json, r'email')!,
|
||||||
externalPath: mapValueOfType<String>(json, r'externalPath'),
|
|
||||||
memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled'),
|
memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled'),
|
||||||
name: mapValueOfType<String>(json, r'name')!,
|
name: mapValueOfType<String>(json, r'name')!,
|
||||||
password: mapValueOfType<String>(json, r'password')!,
|
password: mapValueOfType<String>(json, r'password')!,
|
||||||
|
14
mobile/openapi/lib/model/partner_response_dto.dart
generated
14
mobile/openapi/lib/model/partner_response_dto.dart
generated
@ -17,7 +17,6 @@ class PartnerResponseDto {
|
|||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.deletedAt,
|
required this.deletedAt,
|
||||||
required this.email,
|
required this.email,
|
||||||
required this.externalPath,
|
|
||||||
required this.id,
|
required this.id,
|
||||||
this.inTimeline,
|
this.inTimeline,
|
||||||
required this.isAdmin,
|
required this.isAdmin,
|
||||||
@ -40,8 +39,6 @@ class PartnerResponseDto {
|
|||||||
|
|
||||||
String email;
|
String email;
|
||||||
|
|
||||||
String? externalPath;
|
|
||||||
|
|
||||||
String id;
|
String id;
|
||||||
|
|
||||||
///
|
///
|
||||||
@ -84,7 +81,6 @@ class PartnerResponseDto {
|
|||||||
other.createdAt == createdAt &&
|
other.createdAt == createdAt &&
|
||||||
other.deletedAt == deletedAt &&
|
other.deletedAt == deletedAt &&
|
||||||
other.email == email &&
|
other.email == email &&
|
||||||
other.externalPath == externalPath &&
|
|
||||||
other.id == id &&
|
other.id == id &&
|
||||||
other.inTimeline == inTimeline &&
|
other.inTimeline == inTimeline &&
|
||||||
other.isAdmin == isAdmin &&
|
other.isAdmin == isAdmin &&
|
||||||
@ -105,7 +101,6 @@ class PartnerResponseDto {
|
|||||||
(createdAt.hashCode) +
|
(createdAt.hashCode) +
|
||||||
(deletedAt == null ? 0 : deletedAt!.hashCode) +
|
(deletedAt == null ? 0 : deletedAt!.hashCode) +
|
||||||
(email.hashCode) +
|
(email.hashCode) +
|
||||||
(externalPath == null ? 0 : externalPath!.hashCode) +
|
|
||||||
(id.hashCode) +
|
(id.hashCode) +
|
||||||
(inTimeline == null ? 0 : inTimeline!.hashCode) +
|
(inTimeline == null ? 0 : inTimeline!.hashCode) +
|
||||||
(isAdmin.hashCode) +
|
(isAdmin.hashCode) +
|
||||||
@ -120,7 +115,7 @@ class PartnerResponseDto {
|
|||||||
(updatedAt.hashCode);
|
(updatedAt.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'PartnerResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, externalPath=$externalPath, id=$id, inTimeline=$inTimeline, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]';
|
String toString() => 'PartnerResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, id=$id, inTimeline=$inTimeline, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -132,11 +127,6 @@ class PartnerResponseDto {
|
|||||||
// json[r'deletedAt'] = null;
|
// json[r'deletedAt'] = null;
|
||||||
}
|
}
|
||||||
json[r'email'] = this.email;
|
json[r'email'] = this.email;
|
||||||
if (this.externalPath != null) {
|
|
||||||
json[r'externalPath'] = this.externalPath;
|
|
||||||
} else {
|
|
||||||
// json[r'externalPath'] = null;
|
|
||||||
}
|
|
||||||
json[r'id'] = this.id;
|
json[r'id'] = this.id;
|
||||||
if (this.inTimeline != null) {
|
if (this.inTimeline != null) {
|
||||||
json[r'inTimeline'] = this.inTimeline;
|
json[r'inTimeline'] = this.inTimeline;
|
||||||
@ -184,7 +174,6 @@ class PartnerResponseDto {
|
|||||||
createdAt: mapDateTime(json, r'createdAt', r'')!,
|
createdAt: mapDateTime(json, r'createdAt', r'')!,
|
||||||
deletedAt: mapDateTime(json, r'deletedAt', r''),
|
deletedAt: mapDateTime(json, r'deletedAt', r''),
|
||||||
email: mapValueOfType<String>(json, r'email')!,
|
email: mapValueOfType<String>(json, r'email')!,
|
||||||
externalPath: mapValueOfType<String>(json, r'externalPath'),
|
|
||||||
id: mapValueOfType<String>(json, r'id')!,
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
inTimeline: mapValueOfType<bool>(json, r'inTimeline'),
|
inTimeline: mapValueOfType<bool>(json, r'inTimeline'),
|
||||||
isAdmin: mapValueOfType<bool>(json, r'isAdmin')!,
|
isAdmin: mapValueOfType<bool>(json, r'isAdmin')!,
|
||||||
@ -248,7 +237,6 @@ class PartnerResponseDto {
|
|||||||
'createdAt',
|
'createdAt',
|
||||||
'deletedAt',
|
'deletedAt',
|
||||||
'email',
|
'email',
|
||||||
'externalPath',
|
|
||||||
'id',
|
'id',
|
||||||
'isAdmin',
|
'isAdmin',
|
||||||
'name',
|
'name',
|
||||||
|
11
mobile/openapi/lib/model/smart_search_dto.dart
generated
11
mobile/openapi/lib/model/smart_search_dto.dart
generated
@ -32,6 +32,7 @@ class SmartSearchDto {
|
|||||||
this.make,
|
this.make,
|
||||||
this.model,
|
this.model,
|
||||||
this.page,
|
this.page,
|
||||||
|
this.personIds = const [],
|
||||||
required this.query,
|
required this.query,
|
||||||
this.size,
|
this.size,
|
||||||
this.state,
|
this.state,
|
||||||
@ -199,6 +200,8 @@ class SmartSearchDto {
|
|||||||
///
|
///
|
||||||
num? page;
|
num? page;
|
||||||
|
|
||||||
|
List<String> personIds;
|
||||||
|
|
||||||
String query;
|
String query;
|
||||||
|
|
||||||
///
|
///
|
||||||
@ -312,6 +315,7 @@ class SmartSearchDto {
|
|||||||
other.make == make &&
|
other.make == make &&
|
||||||
other.model == model &&
|
other.model == model &&
|
||||||
other.page == page &&
|
other.page == page &&
|
||||||
|
_deepEquality.equals(other.personIds, personIds) &&
|
||||||
other.query == query &&
|
other.query == query &&
|
||||||
other.size == size &&
|
other.size == size &&
|
||||||
other.state == state &&
|
other.state == state &&
|
||||||
@ -348,6 +352,7 @@ class SmartSearchDto {
|
|||||||
(make == null ? 0 : make!.hashCode) +
|
(make == null ? 0 : make!.hashCode) +
|
||||||
(model == null ? 0 : model!.hashCode) +
|
(model == null ? 0 : model!.hashCode) +
|
||||||
(page == null ? 0 : page!.hashCode) +
|
(page == null ? 0 : page!.hashCode) +
|
||||||
|
(personIds.hashCode) +
|
||||||
(query.hashCode) +
|
(query.hashCode) +
|
||||||
(size == null ? 0 : size!.hashCode) +
|
(size == null ? 0 : size!.hashCode) +
|
||||||
(state == null ? 0 : state!.hashCode) +
|
(state == null ? 0 : state!.hashCode) +
|
||||||
@ -363,7 +368,7 @@ class SmartSearchDto {
|
|||||||
(withExif == null ? 0 : withExif!.hashCode);
|
(withExif == null ? 0 : withExif!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
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]';
|
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, personIds=$personIds, 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() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -462,6 +467,7 @@ class SmartSearchDto {
|
|||||||
} else {
|
} else {
|
||||||
// json[r'page'] = null;
|
// json[r'page'] = null;
|
||||||
}
|
}
|
||||||
|
json[r'personIds'] = this.personIds;
|
||||||
json[r'query'] = this.query;
|
json[r'query'] = this.query;
|
||||||
if (this.size != null) {
|
if (this.size != null) {
|
||||||
json[r'size'] = this.size;
|
json[r'size'] = this.size;
|
||||||
@ -549,6 +555,9 @@ class SmartSearchDto {
|
|||||||
make: mapValueOfType<String>(json, r'make'),
|
make: mapValueOfType<String>(json, r'make'),
|
||||||
model: mapValueOfType<String>(json, r'model'),
|
model: mapValueOfType<String>(json, r'model'),
|
||||||
page: num.parse('${json[r'page']}'),
|
page: num.parse('${json[r'page']}'),
|
||||||
|
personIds: json[r'personIds'] is Iterable
|
||||||
|
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
|
||||||
|
: const [],
|
||||||
query: mapValueOfType<String>(json, r'query')!,
|
query: mapValueOfType<String>(json, r'query')!,
|
||||||
size: num.parse('${json[r'size']}'),
|
size: num.parse('${json[r'size']}'),
|
||||||
state: mapValueOfType<String>(json, r'state'),
|
state: mapValueOfType<String>(json, r'state'),
|
||||||
|
@ -14,37 +14,25 @@ class SystemConfigLibraryWatchDto {
|
|||||||
/// Returns a new [SystemConfigLibraryWatchDto] instance.
|
/// Returns a new [SystemConfigLibraryWatchDto] instance.
|
||||||
SystemConfigLibraryWatchDto({
|
SystemConfigLibraryWatchDto({
|
||||||
required this.enabled,
|
required this.enabled,
|
||||||
required this.interval,
|
|
||||||
required this.usePolling,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
bool enabled;
|
bool enabled;
|
||||||
|
|
||||||
int interval;
|
|
||||||
|
|
||||||
bool usePolling;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is SystemConfigLibraryWatchDto &&
|
bool operator ==(Object other) => identical(this, other) || other is SystemConfigLibraryWatchDto &&
|
||||||
other.enabled == enabled &&
|
other.enabled == enabled;
|
||||||
other.interval == interval &&
|
|
||||||
other.usePolling == usePolling;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(enabled.hashCode) +
|
(enabled.hashCode);
|
||||||
(interval.hashCode) +
|
|
||||||
(usePolling.hashCode);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SystemConfigLibraryWatchDto[enabled=$enabled, interval=$interval, usePolling=$usePolling]';
|
String toString() => 'SystemConfigLibraryWatchDto[enabled=$enabled]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
json[r'enabled'] = this.enabled;
|
json[r'enabled'] = this.enabled;
|
||||||
json[r'interval'] = this.interval;
|
|
||||||
json[r'usePolling'] = this.usePolling;
|
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,8 +45,6 @@ class SystemConfigLibraryWatchDto {
|
|||||||
|
|
||||||
return SystemConfigLibraryWatchDto(
|
return SystemConfigLibraryWatchDto(
|
||||||
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||||
interval: mapValueOfType<int>(json, r'interval')!,
|
|
||||||
usePolling: mapValueOfType<bool>(json, r'usePolling')!,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -107,8 +93,6 @@ class SystemConfigLibraryWatchDto {
|
|||||||
/// The list of required keys that must be present in a JSON.
|
/// The list of required keys that must be present in a JSON.
|
||||||
static const requiredKeys = <String>{
|
static const requiredKeys = <String>{
|
||||||
'enabled',
|
'enabled',
|
||||||
'interval',
|
|
||||||
'usePolling',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ class SystemConfigOAuthDto {
|
|||||||
required this.buttonText,
|
required this.buttonText,
|
||||||
required this.clientId,
|
required this.clientId,
|
||||||
required this.clientSecret,
|
required this.clientSecret,
|
||||||
|
required this.defaultStorageQuota,
|
||||||
required this.enabled,
|
required this.enabled,
|
||||||
required this.issuerUrl,
|
required this.issuerUrl,
|
||||||
required this.mobileOverrideEnabled,
|
required this.mobileOverrideEnabled,
|
||||||
@ -25,6 +26,7 @@ class SystemConfigOAuthDto {
|
|||||||
required this.scope,
|
required this.scope,
|
||||||
required this.signingAlgorithm,
|
required this.signingAlgorithm,
|
||||||
required this.storageLabelClaim,
|
required this.storageLabelClaim,
|
||||||
|
required this.storageQuotaClaim,
|
||||||
});
|
});
|
||||||
|
|
||||||
bool autoLaunch;
|
bool autoLaunch;
|
||||||
@ -37,6 +39,8 @@ class SystemConfigOAuthDto {
|
|||||||
|
|
||||||
String clientSecret;
|
String clientSecret;
|
||||||
|
|
||||||
|
num defaultStorageQuota;
|
||||||
|
|
||||||
bool enabled;
|
bool enabled;
|
||||||
|
|
||||||
String issuerUrl;
|
String issuerUrl;
|
||||||
@ -51,6 +55,8 @@ class SystemConfigOAuthDto {
|
|||||||
|
|
||||||
String storageLabelClaim;
|
String storageLabelClaim;
|
||||||
|
|
||||||
|
String storageQuotaClaim;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is SystemConfigOAuthDto &&
|
bool operator ==(Object other) => identical(this, other) || other is SystemConfigOAuthDto &&
|
||||||
other.autoLaunch == autoLaunch &&
|
other.autoLaunch == autoLaunch &&
|
||||||
@ -58,13 +64,15 @@ class SystemConfigOAuthDto {
|
|||||||
other.buttonText == buttonText &&
|
other.buttonText == buttonText &&
|
||||||
other.clientId == clientId &&
|
other.clientId == clientId &&
|
||||||
other.clientSecret == clientSecret &&
|
other.clientSecret == clientSecret &&
|
||||||
|
other.defaultStorageQuota == defaultStorageQuota &&
|
||||||
other.enabled == enabled &&
|
other.enabled == enabled &&
|
||||||
other.issuerUrl == issuerUrl &&
|
other.issuerUrl == issuerUrl &&
|
||||||
other.mobileOverrideEnabled == mobileOverrideEnabled &&
|
other.mobileOverrideEnabled == mobileOverrideEnabled &&
|
||||||
other.mobileRedirectUri == mobileRedirectUri &&
|
other.mobileRedirectUri == mobileRedirectUri &&
|
||||||
other.scope == scope &&
|
other.scope == scope &&
|
||||||
other.signingAlgorithm == signingAlgorithm &&
|
other.signingAlgorithm == signingAlgorithm &&
|
||||||
other.storageLabelClaim == storageLabelClaim;
|
other.storageLabelClaim == storageLabelClaim &&
|
||||||
|
other.storageQuotaClaim == storageQuotaClaim;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
@ -74,16 +82,18 @@ class SystemConfigOAuthDto {
|
|||||||
(buttonText.hashCode) +
|
(buttonText.hashCode) +
|
||||||
(clientId.hashCode) +
|
(clientId.hashCode) +
|
||||||
(clientSecret.hashCode) +
|
(clientSecret.hashCode) +
|
||||||
|
(defaultStorageQuota.hashCode) +
|
||||||
(enabled.hashCode) +
|
(enabled.hashCode) +
|
||||||
(issuerUrl.hashCode) +
|
(issuerUrl.hashCode) +
|
||||||
(mobileOverrideEnabled.hashCode) +
|
(mobileOverrideEnabled.hashCode) +
|
||||||
(mobileRedirectUri.hashCode) +
|
(mobileRedirectUri.hashCode) +
|
||||||
(scope.hashCode) +
|
(scope.hashCode) +
|
||||||
(signingAlgorithm.hashCode) +
|
(signingAlgorithm.hashCode) +
|
||||||
(storageLabelClaim.hashCode);
|
(storageLabelClaim.hashCode) +
|
||||||
|
(storageQuotaClaim.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SystemConfigOAuthDto[autoLaunch=$autoLaunch, autoRegister=$autoRegister, buttonText=$buttonText, clientId=$clientId, clientSecret=$clientSecret, enabled=$enabled, issuerUrl=$issuerUrl, mobileOverrideEnabled=$mobileOverrideEnabled, mobileRedirectUri=$mobileRedirectUri, scope=$scope, signingAlgorithm=$signingAlgorithm, storageLabelClaim=$storageLabelClaim]';
|
String toString() => 'SystemConfigOAuthDto[autoLaunch=$autoLaunch, autoRegister=$autoRegister, buttonText=$buttonText, clientId=$clientId, clientSecret=$clientSecret, defaultStorageQuota=$defaultStorageQuota, enabled=$enabled, issuerUrl=$issuerUrl, mobileOverrideEnabled=$mobileOverrideEnabled, mobileRedirectUri=$mobileRedirectUri, scope=$scope, signingAlgorithm=$signingAlgorithm, storageLabelClaim=$storageLabelClaim, storageQuotaClaim=$storageQuotaClaim]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -92,6 +102,7 @@ class SystemConfigOAuthDto {
|
|||||||
json[r'buttonText'] = this.buttonText;
|
json[r'buttonText'] = this.buttonText;
|
||||||
json[r'clientId'] = this.clientId;
|
json[r'clientId'] = this.clientId;
|
||||||
json[r'clientSecret'] = this.clientSecret;
|
json[r'clientSecret'] = this.clientSecret;
|
||||||
|
json[r'defaultStorageQuota'] = this.defaultStorageQuota;
|
||||||
json[r'enabled'] = this.enabled;
|
json[r'enabled'] = this.enabled;
|
||||||
json[r'issuerUrl'] = this.issuerUrl;
|
json[r'issuerUrl'] = this.issuerUrl;
|
||||||
json[r'mobileOverrideEnabled'] = this.mobileOverrideEnabled;
|
json[r'mobileOverrideEnabled'] = this.mobileOverrideEnabled;
|
||||||
@ -99,6 +110,7 @@ class SystemConfigOAuthDto {
|
|||||||
json[r'scope'] = this.scope;
|
json[r'scope'] = this.scope;
|
||||||
json[r'signingAlgorithm'] = this.signingAlgorithm;
|
json[r'signingAlgorithm'] = this.signingAlgorithm;
|
||||||
json[r'storageLabelClaim'] = this.storageLabelClaim;
|
json[r'storageLabelClaim'] = this.storageLabelClaim;
|
||||||
|
json[r'storageQuotaClaim'] = this.storageQuotaClaim;
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,6 +127,7 @@ class SystemConfigOAuthDto {
|
|||||||
buttonText: mapValueOfType<String>(json, r'buttonText')!,
|
buttonText: mapValueOfType<String>(json, r'buttonText')!,
|
||||||
clientId: mapValueOfType<String>(json, r'clientId')!,
|
clientId: mapValueOfType<String>(json, r'clientId')!,
|
||||||
clientSecret: mapValueOfType<String>(json, r'clientSecret')!,
|
clientSecret: mapValueOfType<String>(json, r'clientSecret')!,
|
||||||
|
defaultStorageQuota: num.parse('${json[r'defaultStorageQuota']}'),
|
||||||
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||||
issuerUrl: mapValueOfType<String>(json, r'issuerUrl')!,
|
issuerUrl: mapValueOfType<String>(json, r'issuerUrl')!,
|
||||||
mobileOverrideEnabled: mapValueOfType<bool>(json, r'mobileOverrideEnabled')!,
|
mobileOverrideEnabled: mapValueOfType<bool>(json, r'mobileOverrideEnabled')!,
|
||||||
@ -122,6 +135,7 @@ class SystemConfigOAuthDto {
|
|||||||
scope: mapValueOfType<String>(json, r'scope')!,
|
scope: mapValueOfType<String>(json, r'scope')!,
|
||||||
signingAlgorithm: mapValueOfType<String>(json, r'signingAlgorithm')!,
|
signingAlgorithm: mapValueOfType<String>(json, r'signingAlgorithm')!,
|
||||||
storageLabelClaim: mapValueOfType<String>(json, r'storageLabelClaim')!,
|
storageLabelClaim: mapValueOfType<String>(json, r'storageLabelClaim')!,
|
||||||
|
storageQuotaClaim: mapValueOfType<String>(json, r'storageQuotaClaim')!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -174,6 +188,7 @@ class SystemConfigOAuthDto {
|
|||||||
'buttonText',
|
'buttonText',
|
||||||
'clientId',
|
'clientId',
|
||||||
'clientSecret',
|
'clientSecret',
|
||||||
|
'defaultStorageQuota',
|
||||||
'enabled',
|
'enabled',
|
||||||
'issuerUrl',
|
'issuerUrl',
|
||||||
'mobileOverrideEnabled',
|
'mobileOverrideEnabled',
|
||||||
@ -181,6 +196,7 @@ class SystemConfigOAuthDto {
|
|||||||
'scope',
|
'scope',
|
||||||
'signingAlgorithm',
|
'signingAlgorithm',
|
||||||
'storageLabelClaim',
|
'storageLabelClaim',
|
||||||
|
'storageQuotaClaim',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
19
mobile/openapi/lib/model/update_user_dto.dart
generated
19
mobile/openapi/lib/model/update_user_dto.dart
generated
@ -15,7 +15,6 @@ class UpdateUserDto {
|
|||||||
UpdateUserDto({
|
UpdateUserDto({
|
||||||
this.avatarColor,
|
this.avatarColor,
|
||||||
this.email,
|
this.email,
|
||||||
this.externalPath,
|
|
||||||
required this.id,
|
required this.id,
|
||||||
this.isAdmin,
|
this.isAdmin,
|
||||||
this.memoriesEnabled,
|
this.memoriesEnabled,
|
||||||
@ -42,14 +41,6 @@ class UpdateUserDto {
|
|||||||
///
|
///
|
||||||
String? email;
|
String? email;
|
||||||
|
|
||||||
///
|
|
||||||
/// 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? externalPath;
|
|
||||||
|
|
||||||
String id;
|
String id;
|
||||||
|
|
||||||
///
|
///
|
||||||
@ -106,7 +97,6 @@ class UpdateUserDto {
|
|||||||
bool operator ==(Object other) => identical(this, other) || other is UpdateUserDto &&
|
bool operator ==(Object other) => identical(this, other) || other is UpdateUserDto &&
|
||||||
other.avatarColor == avatarColor &&
|
other.avatarColor == avatarColor &&
|
||||||
other.email == email &&
|
other.email == email &&
|
||||||
other.externalPath == externalPath &&
|
|
||||||
other.id == id &&
|
other.id == id &&
|
||||||
other.isAdmin == isAdmin &&
|
other.isAdmin == isAdmin &&
|
||||||
other.memoriesEnabled == memoriesEnabled &&
|
other.memoriesEnabled == memoriesEnabled &&
|
||||||
@ -121,7 +111,6 @@ class UpdateUserDto {
|
|||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(avatarColor == null ? 0 : avatarColor!.hashCode) +
|
(avatarColor == null ? 0 : avatarColor!.hashCode) +
|
||||||
(email == null ? 0 : email!.hashCode) +
|
(email == null ? 0 : email!.hashCode) +
|
||||||
(externalPath == null ? 0 : externalPath!.hashCode) +
|
|
||||||
(id.hashCode) +
|
(id.hashCode) +
|
||||||
(isAdmin == null ? 0 : isAdmin!.hashCode) +
|
(isAdmin == null ? 0 : isAdmin!.hashCode) +
|
||||||
(memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) +
|
(memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) +
|
||||||
@ -132,7 +121,7 @@ class UpdateUserDto {
|
|||||||
(storageLabel == null ? 0 : storageLabel!.hashCode);
|
(storageLabel == null ? 0 : storageLabel!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'UpdateUserDto[avatarColor=$avatarColor, email=$email, externalPath=$externalPath, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
|
String toString() => 'UpdateUserDto[avatarColor=$avatarColor, email=$email, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -145,11 +134,6 @@ class UpdateUserDto {
|
|||||||
json[r'email'] = this.email;
|
json[r'email'] = this.email;
|
||||||
} else {
|
} else {
|
||||||
// json[r'email'] = null;
|
// json[r'email'] = null;
|
||||||
}
|
|
||||||
if (this.externalPath != null) {
|
|
||||||
json[r'externalPath'] = this.externalPath;
|
|
||||||
} else {
|
|
||||||
// json[r'externalPath'] = null;
|
|
||||||
}
|
}
|
||||||
json[r'id'] = this.id;
|
json[r'id'] = this.id;
|
||||||
if (this.isAdmin != null) {
|
if (this.isAdmin != null) {
|
||||||
@ -200,7 +184,6 @@ class UpdateUserDto {
|
|||||||
return UpdateUserDto(
|
return UpdateUserDto(
|
||||||
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']),
|
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']),
|
||||||
email: mapValueOfType<String>(json, r'email'),
|
email: mapValueOfType<String>(json, r'email'),
|
||||||
externalPath: mapValueOfType<String>(json, r'externalPath'),
|
|
||||||
id: mapValueOfType<String>(json, r'id')!,
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
isAdmin: mapValueOfType<bool>(json, r'isAdmin'),
|
isAdmin: mapValueOfType<bool>(json, r'isAdmin'),
|
||||||
memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled'),
|
memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled'),
|
||||||
|
14
mobile/openapi/lib/model/user_response_dto.dart
generated
14
mobile/openapi/lib/model/user_response_dto.dart
generated
@ -17,7 +17,6 @@ class UserResponseDto {
|
|||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.deletedAt,
|
required this.deletedAt,
|
||||||
required this.email,
|
required this.email,
|
||||||
required this.externalPath,
|
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.isAdmin,
|
required this.isAdmin,
|
||||||
this.memoriesEnabled,
|
this.memoriesEnabled,
|
||||||
@ -39,8 +38,6 @@ class UserResponseDto {
|
|||||||
|
|
||||||
String email;
|
String email;
|
||||||
|
|
||||||
String? externalPath;
|
|
||||||
|
|
||||||
String id;
|
String id;
|
||||||
|
|
||||||
bool isAdmin;
|
bool isAdmin;
|
||||||
@ -75,7 +72,6 @@ class UserResponseDto {
|
|||||||
other.createdAt == createdAt &&
|
other.createdAt == createdAt &&
|
||||||
other.deletedAt == deletedAt &&
|
other.deletedAt == deletedAt &&
|
||||||
other.email == email &&
|
other.email == email &&
|
||||||
other.externalPath == externalPath &&
|
|
||||||
other.id == id &&
|
other.id == id &&
|
||||||
other.isAdmin == isAdmin &&
|
other.isAdmin == isAdmin &&
|
||||||
other.memoriesEnabled == memoriesEnabled &&
|
other.memoriesEnabled == memoriesEnabled &&
|
||||||
@ -95,7 +91,6 @@ class UserResponseDto {
|
|||||||
(createdAt.hashCode) +
|
(createdAt.hashCode) +
|
||||||
(deletedAt == null ? 0 : deletedAt!.hashCode) +
|
(deletedAt == null ? 0 : deletedAt!.hashCode) +
|
||||||
(email.hashCode) +
|
(email.hashCode) +
|
||||||
(externalPath == null ? 0 : externalPath!.hashCode) +
|
|
||||||
(id.hashCode) +
|
(id.hashCode) +
|
||||||
(isAdmin.hashCode) +
|
(isAdmin.hashCode) +
|
||||||
(memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) +
|
(memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) +
|
||||||
@ -109,7 +104,7 @@ class UserResponseDto {
|
|||||||
(updatedAt.hashCode);
|
(updatedAt.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'UserResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, externalPath=$externalPath, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]';
|
String toString() => 'UserResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -121,11 +116,6 @@ class UserResponseDto {
|
|||||||
// json[r'deletedAt'] = null;
|
// json[r'deletedAt'] = null;
|
||||||
}
|
}
|
||||||
json[r'email'] = this.email;
|
json[r'email'] = this.email;
|
||||||
if (this.externalPath != null) {
|
|
||||||
json[r'externalPath'] = this.externalPath;
|
|
||||||
} else {
|
|
||||||
// json[r'externalPath'] = null;
|
|
||||||
}
|
|
||||||
json[r'id'] = this.id;
|
json[r'id'] = this.id;
|
||||||
json[r'isAdmin'] = this.isAdmin;
|
json[r'isAdmin'] = this.isAdmin;
|
||||||
if (this.memoriesEnabled != null) {
|
if (this.memoriesEnabled != null) {
|
||||||
@ -168,7 +158,6 @@ class UserResponseDto {
|
|||||||
createdAt: mapDateTime(json, r'createdAt', r'')!,
|
createdAt: mapDateTime(json, r'createdAt', r'')!,
|
||||||
deletedAt: mapDateTime(json, r'deletedAt', r''),
|
deletedAt: mapDateTime(json, r'deletedAt', r''),
|
||||||
email: mapValueOfType<String>(json, r'email')!,
|
email: mapValueOfType<String>(json, r'email')!,
|
||||||
externalPath: mapValueOfType<String>(json, r'externalPath'),
|
|
||||||
id: mapValueOfType<String>(json, r'id')!,
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
isAdmin: mapValueOfType<bool>(json, r'isAdmin')!,
|
isAdmin: mapValueOfType<bool>(json, r'isAdmin')!,
|
||||||
memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled'),
|
memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled'),
|
||||||
@ -231,7 +220,6 @@ class UserResponseDto {
|
|||||||
'createdAt',
|
'createdAt',
|
||||||
'deletedAt',
|
'deletedAt',
|
||||||
'email',
|
'email',
|
||||||
'externalPath',
|
|
||||||
'id',
|
'id',
|
||||||
'isAdmin',
|
'isAdmin',
|
||||||
'name',
|
'name',
|
||||||
|
2
mobile/openapi/test/asset_api_test.dart
generated
2
mobile/openapi/test/asset_api_test.dart
generated
@ -135,7 +135,7 @@ void main() {
|
|||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
//Future<AssetFileUploadResponseDto> uploadFile(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String key, String duration, bool isArchived, bool isExternal, bool isFavorite, bool isOffline, bool isReadOnly, bool isVisible, String libraryId, MultipartFile livePhotoData, MultipartFile sidecarData }) async
|
//Future<AssetFileUploadResponseDto> uploadFile(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String key, String duration, bool isArchived, bool isFavorite, bool isOffline, bool isReadOnly, bool isVisible, String libraryId, MultipartFile livePhotoData, MultipartFile sidecarData }) async
|
||||||
test('test uploadFile', () async {
|
test('test uploadFile', () async {
|
||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
5
mobile/openapi/test/create_library_dto_test.dart
generated
5
mobile/openapi/test/create_library_dto_test.dart
generated
@ -41,6 +41,11 @@ void main() {
|
|||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// String ownerId
|
||||||
|
test('to test the property `ownerId`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
// LibraryType type
|
// LibraryType type
|
||||||
test('to test the property `type`', () async {
|
test('to test the property `type`', () async {
|
||||||
// TODO
|
// TODO
|
||||||
|
5
mobile/openapi/test/create_user_dto_test.dart
generated
5
mobile/openapi/test/create_user_dto_test.dart
generated
@ -21,11 +21,6 @@ void main() {
|
|||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
// String externalPath
|
|
||||||
test('to test the property `externalPath`', () async {
|
|
||||||
// TODO
|
|
||||||
});
|
|
||||||
|
|
||||||
// bool memoriesEnabled
|
// bool memoriesEnabled
|
||||||
test('to test the property `memoriesEnabled`', () async {
|
test('to test the property `memoriesEnabled`', () async {
|
||||||
// TODO
|
// TODO
|
||||||
|
8
mobile/openapi/test/library_api_test.dart
generated
8
mobile/openapi/test/library_api_test.dart
generated
@ -27,13 +27,13 @@ void main() {
|
|||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
//Future<List<LibraryResponseDto>> getLibraries() async
|
//Future<List<LibraryResponseDto>> getAllLibraries({ LibraryType type }) async
|
||||||
test('test getLibraries', () async {
|
test('test getAllLibraries', () async {
|
||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
//Future<LibraryResponseDto> getLibraryInfo(String id) async
|
//Future<LibraryResponseDto> getLibrary(String id) async
|
||||||
test('test getLibraryInfo', () async {
|
test('test getLibrary', () async {
|
||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -36,11 +36,6 @@ void main() {
|
|||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
// String externalPath
|
|
||||||
test('to test the property `externalPath`', () async {
|
|
||||||
// TODO
|
|
||||||
});
|
|
||||||
|
|
||||||
// String id
|
// String id
|
||||||
test('to test the property `id`', () async {
|
test('to test the property `id`', () async {
|
||||||
// TODO
|
// TODO
|
||||||
|
5
mobile/openapi/test/smart_search_dto_test.dart
generated
5
mobile/openapi/test/smart_search_dto_test.dart
generated
@ -111,6 +111,11 @@ void main() {
|
|||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// List<String> personIds (default value: const [])
|
||||||
|
test('to test the property `personIds`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
// String query
|
// String query
|
||||||
test('to test the property `query`', () async {
|
test('to test the property `query`', () async {
|
||||||
// TODO
|
// TODO
|
||||||
|
@ -21,16 +21,6 @@ void main() {
|
|||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
// int interval
|
|
||||||
test('to test the property `interval`', () async {
|
|
||||||
// TODO
|
|
||||||
});
|
|
||||||
|
|
||||||
// bool usePolling
|
|
||||||
test('to test the property `usePolling`', () async {
|
|
||||||
// TODO
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -41,6 +41,11 @@ void main() {
|
|||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// num defaultStorageQuota
|
||||||
|
test('to test the property `defaultStorageQuota`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
// bool enabled
|
// bool enabled
|
||||||
test('to test the property `enabled`', () async {
|
test('to test the property `enabled`', () async {
|
||||||
// TODO
|
// TODO
|
||||||
@ -76,6 +81,11 @@ void main() {
|
|||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// String storageQuotaClaim
|
||||||
|
test('to test the property `storageQuotaClaim`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
5
mobile/openapi/test/update_user_dto_test.dart
generated
5
mobile/openapi/test/update_user_dto_test.dart
generated
@ -26,11 +26,6 @@ void main() {
|
|||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
// String externalPath
|
|
||||||
test('to test the property `externalPath`', () async {
|
|
||||||
// TODO
|
|
||||||
});
|
|
||||||
|
|
||||||
// String id
|
// String id
|
||||||
test('to test the property `id`', () async {
|
test('to test the property `id`', () async {
|
||||||
// TODO
|
// TODO
|
||||||
|
5
mobile/openapi/test/user_response_dto_test.dart
generated
5
mobile/openapi/test/user_response_dto_test.dart
generated
@ -36,11 +36,6 @@ void main() {
|
|||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
// String externalPath
|
|
||||||
test('to test the property `externalPath`', () async {
|
|
||||||
// TODO
|
|
||||||
});
|
|
||||||
|
|
||||||
// String id
|
// String id
|
||||||
test('to test the property `id`', () async {
|
test('to test the property `id`', () async {
|
||||||
// TODO
|
// TODO
|
||||||
|
@ -413,10 +413,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: file
|
name: file
|
||||||
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
|
sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.0"
|
version: "6.1.4"
|
||||||
file_selector_linux:
|
file_selector_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -860,30 +860,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.8.1"
|
version: "4.8.1"
|
||||||
leak_tracker:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: leak_tracker
|
|
||||||
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "10.0.0"
|
|
||||||
leak_tracker_flutter_testing:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: leak_tracker_flutter_testing
|
|
||||||
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.1"
|
|
||||||
leak_tracker_testing:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: leak_tracker_testing
|
|
||||||
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.1"
|
|
||||||
lints:
|
lints:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -931,18 +907,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.16+1"
|
version: "0.12.16"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.0"
|
version: "0.5.0"
|
||||||
meta:
|
meta:
|
||||||
dependency: "direct overridden"
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
@ -1026,10 +1002,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.0"
|
version: "1.8.3"
|
||||||
path_provider:
|
path_provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1162,10 +1138,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: platform
|
name: platform
|
||||||
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
|
sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.4"
|
version: "3.1.2"
|
||||||
plugin_platform_interface:
|
plugin_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1194,10 +1170,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: process
|
name: process
|
||||||
sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32"
|
sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.2"
|
version: "4.2.4"
|
||||||
provider:
|
provider:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1663,10 +1639,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
|
sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "13.0.0"
|
version: "11.10.0"
|
||||||
wakelock_plus:
|
wakelock_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1711,10 +1687,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: webdriver
|
name: webdriver
|
||||||
sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e"
|
sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.3"
|
version: "3.0.2"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -2,7 +2,7 @@ name: immich_mobile
|
|||||||
description: Immich - selfhosted backup media file on mobile phone
|
description: Immich - selfhosted backup media file on mobile phone
|
||||||
|
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 1.95.1+123
|
version: 1.97.0+125
|
||||||
isar_version: &isar_version 3.1.0+1
|
isar_version: &isar_version 3.1.0+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
@ -17,9 +17,7 @@ function dart {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function typescript {
|
function typescript {
|
||||||
rm -rf ./typescript-sdk/client
|
npx --yes oazapfts --optimistic --argumentStyle=object --useEnumType immich-openapi-specs.json typescript-sdk/src/fetch-client.ts
|
||||||
npx --yes @openapitools/openapi-generator-cli generate -g typescript-axios -i ./immich-openapi-specs.json -o ./typescript-sdk/axios-client --additional-properties=useSingleRequestParameter=true,supportsES6=true
|
|
||||||
npx --yes oazapfts --optimistic --argumentStyle=object --useEnumType immich-openapi-specs.json typescript-sdk/fetch-client.ts
|
|
||||||
npm --prefix typescript-sdk ci && npm --prefix typescript-sdk run build
|
npm --prefix typescript-sdk ci && npm --prefix typescript-sdk run build
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3299,8 +3299,17 @@
|
|||||||
},
|
},
|
||||||
"/library": {
|
"/library": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getLibraries",
|
"operationId": "getAllLibraries",
|
||||||
"parameters": [],
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "type",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/LibraryType"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"content": {
|
"content": {
|
||||||
@ -3387,7 +3396,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"204": {
|
||||||
"description": ""
|
"description": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -3407,7 +3416,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getLibraryInfo",
|
"operationId": "getLibrary",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "id",
|
"name": "id",
|
||||||
@ -3512,7 +3521,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"201": {
|
"204": {
|
||||||
"description": ""
|
"description": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -3557,7 +3566,7 @@
|
|||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"201": {
|
"204": {
|
||||||
"description": ""
|
"description": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -6458,7 +6467,7 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"title": "Immich",
|
"title": "Immich",
|
||||||
"description": "Immich API",
|
"description": "Immich API",
|
||||||
"version": "1.95.1",
|
"version": "1.97.0",
|
||||||
"contact": {}
|
"contact": {}
|
||||||
},
|
},
|
||||||
"tags": [],
|
"tags": [],
|
||||||
@ -7532,9 +7541,6 @@
|
|||||||
"isArchived": {
|
"isArchived": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"isExternal": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"isFavorite": {
|
"isFavorite": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
@ -7592,6 +7598,10 @@
|
|||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"ownerId": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"$ref": "#/components/schemas/LibraryType"
|
"$ref": "#/components/schemas/LibraryType"
|
||||||
}
|
}
|
||||||
@ -7648,10 +7658,6 @@
|
|||||||
"email": {
|
"email": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"externalPath": {
|
|
||||||
"nullable": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"memoriesEnabled": {
|
"memoriesEnabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
@ -8549,10 +8555,6 @@
|
|||||||
"email": {
|
"email": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"externalPath": {
|
|
||||||
"nullable": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -8601,7 +8603,6 @@
|
|||||||
"createdAt",
|
"createdAt",
|
||||||
"deletedAt",
|
"deletedAt",
|
||||||
"email",
|
"email",
|
||||||
"externalPath",
|
|
||||||
"id",
|
"id",
|
||||||
"isAdmin",
|
"isAdmin",
|
||||||
"name",
|
"name",
|
||||||
@ -9538,6 +9539,12 @@
|
|||||||
"page": {
|
"page": {
|
||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
},
|
||||||
|
"personIds": {
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
"query": {
|
"query": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -9831,18 +9838,10 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
|
||||||
"interval": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"usePolling": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"enabled",
|
"enabled"
|
||||||
"interval",
|
|
||||||
"usePolling"
|
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
@ -9931,6 +9930,9 @@
|
|||||||
"clientSecret": {
|
"clientSecret": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"defaultStorageQuota": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
@ -9951,6 +9953,9 @@
|
|||||||
},
|
},
|
||||||
"storageLabelClaim": {
|
"storageLabelClaim": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"storageQuotaClaim": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@ -9959,13 +9964,15 @@
|
|||||||
"buttonText",
|
"buttonText",
|
||||||
"clientId",
|
"clientId",
|
||||||
"clientSecret",
|
"clientSecret",
|
||||||
|
"defaultStorageQuota",
|
||||||
"enabled",
|
"enabled",
|
||||||
"issuerUrl",
|
"issuerUrl",
|
||||||
"mobileOverrideEnabled",
|
"mobileOverrideEnabled",
|
||||||
"mobileRedirectUri",
|
"mobileRedirectUri",
|
||||||
"scope",
|
"scope",
|
||||||
"signingAlgorithm",
|
"signingAlgorithm",
|
||||||
"storageLabelClaim"
|
"storageLabelClaim",
|
||||||
|
"storageQuotaClaim"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
@ -10334,9 +10341,6 @@
|
|||||||
"email": {
|
"email": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"externalPath": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"id": {
|
"id": {
|
||||||
"format": "uuid",
|
"format": "uuid",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -10463,10 +10467,6 @@
|
|||||||
"email": {
|
"email": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"externalPath": {
|
|
||||||
"nullable": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -10512,7 +10512,6 @@
|
|||||||
"createdAt",
|
"createdAt",
|
||||||
"deletedAt",
|
"deletedAt",
|
||||||
"email",
|
"email",
|
||||||
"externalPath",
|
|
||||||
"id",
|
"id",
|
||||||
"isAdmin",
|
"isAdmin",
|
||||||
"name",
|
"name",
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
wwwroot/*.js
|
|
||||||
node_modules
|
|
||||||
typings
|
|
||||||
dist
|
|
@ -1 +0,0 @@
|
|||||||
# empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm
|
|
@ -1,23 +0,0 @@
|
|||||||
# OpenAPI Generator Ignore
|
|
||||||
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
|
|
||||||
|
|
||||||
# Use this file to prevent files from being overwritten by the generator.
|
|
||||||
# The patterns follow closely to .gitignore or .dockerignore.
|
|
||||||
|
|
||||||
# As an example, the C# client generator defines ApiClient.cs.
|
|
||||||
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
|
|
||||||
#ApiClient.cs
|
|
||||||
|
|
||||||
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
|
|
||||||
#foo/*/qux
|
|
||||||
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
|
|
||||||
|
|
||||||
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
|
|
||||||
#foo/**/qux
|
|
||||||
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
|
|
||||||
|
|
||||||
# You can also negate patterns with an exclamation (!).
|
|
||||||
# For example, you can ignore all files in a docs folder with the file extension .md:
|
|
||||||
#docs/*.md
|
|
||||||
# Then explicitly reverse the ignore rule for a single file:
|
|
||||||
#!docs/README.md
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user