1
0
forked from Cutlery/immich

Merge branch 'main' into fix/edit-faces-notification

This commit is contained in:
martabal 2024-03-03 15:04:50 +01:00
commit c263607515
No known key found for this signature in database
GPG Key ID: C00196E3148A52BD
231 changed files with 5487 additions and 23845 deletions

View File

@ -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/

View File

@ -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
View File

@ -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

View File

@ -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
View File

@ -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": {

View File

@ -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:

View File

@ -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

View File

@ -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}

View File

@ -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 |

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
View 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
View 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
View File

@ -0,0 +1,8 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 120,
"semi": true,
"organizeImportsSkipDestructiveCodeActions": true,
"plugins": ["prettier-plugin-organize-imports"]
}

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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 () => {

View File

@ -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);

View File

@ -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);
});
});
}); });

View File

@ -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) },
), ),
]); ]);

View File

@ -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);
}); });

View File

@ -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');
}); });
}); });
}); });

View 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`,
});
});
});
});

View File

@ -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',
])
);
}); });
}); });
}); });

View File

@ -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' })
);
}); });
}); });
}); });

View File

@ -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}`)

View File

@ -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'],

View File

@ -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);

View File

@ -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',
])
);
} }
}); });

View File

@ -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);

View File

@ -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({

View File

@ -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',

View File

@ -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 });

View File

@ -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
View 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;
};

View File

@ -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,

View File

@ -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())
);
}; };
}; };

View File

@ -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',

View File

@ -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');

View File

@ -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 }) => {

View File

@ -18,5 +18,6 @@
"rootDirs": ["src"], "rootDirs": ["src"],
"baseUrl": "./" "baseUrl": "./"
}, },
"include": ["src/**/*.ts"],
"exclude": ["dist", "node_modules"] "exclude": ["dist", "node_modules"]
} }

View File

@ -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,

View File

@ -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

View File

@ -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"

View File

@ -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"

View File

@ -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

View File

@ -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')

View File

@ -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>

View File

@ -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;

View File

@ -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>

View File

@ -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,

View File

@ -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>

View File

@ -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,

View File

@ -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),
),
),
), ),
], ],
); );

View File

@ -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,

View File

@ -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

View File

@ -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,
);
},
); );
} }
} }

View File

@ -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) {

View File

@ -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;

View File

@ -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 |

View File

@ -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]

View File

@ -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)

View File

@ -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** | |

View File

@ -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');
} }
``` ```

View File

@ -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** | |

View File

@ -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]

View File

@ -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)

View File

@ -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)

View File

@ -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]

View File

@ -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]

View File

@ -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));
} }

View File

@ -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));
} }

View File

@ -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'])!,
); );
} }

View File

@ -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')!,

View File

@ -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',

View File

@ -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'),

View File

@ -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',
}; };
} }

View File

@ -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',
}; };
} }

View File

@ -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'),

View File

@ -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',

View File

@ -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
}); });

View File

@ -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

View File

@ -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

View File

@ -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
}); });

View File

@ -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

View File

@ -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

View File

@ -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
});
}); });

View File

@ -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
});
}); });

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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:

View File

@ -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
} }

View File

@ -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",

View File

@ -1,4 +0,0 @@
wwwroot/*.js
node_modules
typings
dist

View File

@ -1 +0,0 @@
# empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm

View File

@ -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