Compare commits

...

221 Commits

Author SHA1 Message Date
mertalev
f48318168b add extension to tmp path 2024-05-27 18:47:36 -04:00
mertalev
dda736840b save to tmp path then rename 2024-05-27 18:33:58 -04:00
Mert
4c7347d653 fix: re-add extends section for server in Compose files (#9806)
re-add extends section
2024-05-27 21:04:07 +00:00
Mert
dca420ef70 chore: refactor transcode config routing (#9800)
* chore: refactor transcode config

* rename parameter

* handle no /dev/dri

* prefer undefined
2024-05-27 15:20:07 -04:00
Mert
21bd20fd75 fix(server): nvenc not working when there are no filters (#9802)
don't add format=nv12
2024-05-27 15:18:01 -04:00
Mert
351dd647a9 feat(server): better video thumbnails (#9784) 2024-05-27 12:08:38 -04:00
Michel Heusschen
298370b7be fix(web): validation of number input fields (#9789) 2024-05-27 15:19:08 +07:00
aviv926
e3d39837d0 docs: Add Google OAuth example (#9778)
* Add Google OAuth example

* npm run format:fix

* fix

* PR feedback

* Fix
2024-05-27 03:39:59 -04:00
Michel Heusschen
38f4a02a14 fix(web): require button type (#9786) 2024-05-27 14:06:15 +07:00
Michel Heusschen
fc5615eff6 fix(web): memories year missing (#9787) 2024-05-27 14:01:33 +07:00
Alex
6879bcb7a4 chore(server): duplication default settings (#9781) 2024-05-26 20:51:41 -04:00
Conner Hnatiuk
11152f9b3d fix(mobile): appBar on album viewer screen animates out and doesnt alter asset grid position (#9741)
* Animated out top bar added, currenlty need to move up current app bar as it is too far down

* album viewer appBar animates out and does not cause screen shift

* Formatting

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-05-27 00:13:32 +00:00
Jason Rasmussen
75830a4878 refactor(server): user endpoints (#9730)
* refactor(server): user endpoints

* fix repos

* fix unit tests

---------

Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-05-27 05:15:52 +07:00
Mert
e7c8501930 fix(server): search duplicates of the same asset type (#9747)
* search by type

* make sql

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-05-26 22:04:23 +00:00
Alexandre Bouijoux
50f9b2d44e docs: update README fr (#9764)
Update README_fr_FR.md
2024-05-27 04:45:05 +07:00
Ben
9628ea2d24 fix(web): keyboard event propagation in modals (#9713)
* fix: key events propagating from modal, visible close button focus

* feat: set initial focus on the text field for album creation

* chore: step back duplicated changes
2024-05-27 04:43:30 +07:00
safehome-jdev
4d4bb8b6a7 fix(server): Properly build ML predict URL (#9751)
New URL via URL constructor and not string concatenation
2024-05-26 08:21:10 -04:00
Michel Heusschen
99f0aa868a fix(web): detail panel asset description (#9765) 2024-05-26 08:10:01 -04:00
Michel Heusschen
459fee9ee4 fix(web): add location modal invisible on safari (#9756) 2024-05-25 15:36:36 -04:00
Matthew Momjian
871f3ea468 fix(docs): docker version -> name in ML (#9755)
fix docker
2024-05-25 15:14:22 +00:00
Michel Heusschen
98c4c683ae fix(web): profile picture url (#9754) 2024-05-25 11:13:03 -04:00
Michel Heusschen
8a7b0f66a4 fix(server): partner can view archived assets (#9750)
* fix(server): partner can view archived assets

* update sql queries
2024-05-25 06:53:57 -04:00
Jason Rasmussen
9e71256191 chore(server): remove unused code (#9746) 2024-05-25 12:15:07 +02:00
Min Idzelis
d5cf8e4bfe refactor(server): move checkExistingAssets(), checkBulkUpdate() remove getAllAssets() (#9715)
* Refactor controller methods, non-breaking change

* Remove getAllAssets

* used imports

* sync:sql

* missing mock

* Removing remaining references

* chore: remove unused code

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-05-24 21:02:22 -04:00
Jason Rasmussen
95012dc19b fix: config error logging (#9738) 2024-05-24 16:44:50 -04:00
Lukas
f197f5d530 fix(server): use correct file extension for motion photo videos (#8659)
* fix(server): use mp4 file extension for motion photo videos in archive download

* always use mp4 for videos

* get file extension from originalPath

* remove console log

* store motion assets with mp4 extension

* add migration

* set originalFileName for live photo asset stubs

* leave down migration empty

* only set originalFileName for livePhotoStillAsset

* use separate stub

* shorter stub name
2024-05-24 16:38:18 -04:00
Jason Rasmussen
7168707395 refactor(server): remove unused code (#9737) 2024-05-24 16:37:29 -04:00
Snowknight26
847cb90038 fix(web): fix asset grid keyboard navigation (#9448)
* fix(web): fix asset grid keyboard navigation

* Ignore eslint rule

* Pass page up/down keys after focusing on grid

* Remove unneeded event listener, use existing class
2024-05-24 22:11:55 +02:00
bo0tzz
602f0a3499 fix(docs): Duplicate user key in example config.json (#9735)
related: #9734
2024-05-24 16:06:08 -04:00
Michel Heusschen
fdaa0e5413 fix(web): shared link isOwner check (#9729)
* fix(web): shared link isOwner check

* add e2e tests + update playwright

* fix formatting
2024-05-24 17:59:19 +00:00
Zack Pollard
39d2c4f37b chore: remove all deprecated endpoints/properties from server and mobile app (#9724)
* chore: remove deprecated title property from MemoryLaneResponseDto

* chore: remove deprecated webpPath and resizePath from MetadataSearchDto

* chore: remove deprecated sharedUserIds property from Album AddUsersDto

* chore: remove deprecated sharedUsers property from AlbumResponseDto

* chore: remove deprecated sharedWithUserIds property from CreateAlbumDto

* chore: remove deprecated isExternal and isReadOnly properties from AssetResponseDto

* chore: remove deprecated /server-info endpoint

* chore: bloody linters
2024-05-24 15:37:01 +01:00
Kedas
1f5d82e9d9 fix(mobile): respect SSL override during background sync (#9587) 2024-05-24 10:16:14 +01:00
Julian Collins
e98744f222 chore(docs): Russian readme update (#9691)
* Fix many typos, update features, add Activity and Star history sections

* Add clarity

* Add clarity
2024-05-24 10:14:07 +01:00
François-Xavier Payet
56ea07bcba fix(mobile): use correct Focus Node for latitude and longitude (#9699)
FIx focus node for longitude

Co-authored-by: François-Xavier Payet <fxpayet@salesapps.io>
2024-05-24 09:59:05 +01:00
Lukas
b3b258f32f fix(web): allow copying text in photo viewer (#9705)
* fix(web): allow copying text in photo viewer

* use default browser copy

* revert changes

* fix lint
2024-05-24 09:56:36 +01:00
Mert
69b5eb005f fix(server): use qsv format for hwmap (#9722)
use qsv format for hwmap
2024-05-24 09:50:28 +01:00
renovate[bot]
3f44a33eac chore(deps): update docker.io/redis:6.2-alpine docker digest to e31ca60 (#9717)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-24 09:35:59 +01:00
renovate[bot]
b2a0422efb chore(deps): update redis:6.2-alpine docker digest to e31ca60 (#9718)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-24 09:34:49 +01:00
Min Idzelis
562c43b6f5 test: reorder tests in asset.e2e-spec.ts (#9714)
* Reorder tests; make tests independent of ordering

* use it.each
2024-05-23 22:10:38 -04:00
Min Idzelis
4f21f6a2e1 feat: API operation replaceAsset, POST /api/asset/:id/file (#9684)
* impl and unit tests for replaceAsset

* Remove it.only

* Typo in generated spec +regen

* Remove unused dtos

* Dto removal fallout/bugfix

* fix - missed a line

* sql:generate

* Review comments

* Unused imports

* chore: clean up

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-05-23 20:26:22 -04:00
Lukas
76fdcc9863 fix(web): show api key copy button in Firefox (#9704) 2024-05-23 17:16:38 -04:00
Alex
57d94bce68 feat(web): deduplication UI (#9540) 2024-05-23 12:57:25 -05:00
martin
832d728940 refactor(web): svelte actions (#9701) 2024-05-23 12:56:48 -05:00
Michel Heusschen
8bfa6769a5 fix(web): hide detail panel for shared links with hidden metadata (#9700) 2024-05-23 12:39:06 -04:00
Jason Rasmussen
e7aa50425c test: sync open api spec (#9687)
test: sync spec file
2024-05-23 07:40:57 -04:00
Mert
a5e8b451b2 feat(server): qsv hardware decoding and tone-mapping (#9689)
* qsv hw decoding and tone-mapping

* fix vaapi

* add tests

* formatting

* handle device name without path
2024-05-23 03:58:29 +00:00
Jason Rasmussen
13cbdf6851 refactor(server): cli service (#9672) 2024-05-22 22:23:47 +02:00
Jason Rasmussen
967d195a05 chore(server): remove unused code (#9670) 2024-05-22 15:53:57 -04:00
Jason Rasmussen
8f37784eae refactor(server): /user profile endpoint (#9669)
* refactor(server): user profile endpoint

* chore: open api
2024-05-22 14:31:12 -04:00
Jason Rasmussen
ecd018a826 refactor(server): user info endpoint (#9668)
* refactor(server): user info endpoint

* chore: open api
2024-05-22 14:15:33 -04:00
Jason Rasmussen
202745f14b refactor(server): plural endpoints (#9667) 2024-05-22 13:24:57 -04:00
CodaBool
6a4c2e97c0 feat: add docker healthchecks to server and ml (#9583)
* add healthcheck

* format, import, IMMICH_PORT, and eslint change

* chore: clean up nodejs healthcheck

* fix ruff formating

* add healthcheck

* format, import, IMMICH_PORT, and eslint change

* chore: clean up nodejs healthcheck

* fix ruff formating

* add healthcheck to dockerfile

* poetry run ruff check --fix

* removed 2 of 3 console calls

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-05-22 16:54:29 +00:00
Jason Rasmussen
f6f82a5662 feat(web): s (#9663) 2024-05-22 09:33:37 -04:00
Michel Heusschen
ae21781442 fix(web): albums dark mode contrast + a11y issue (#9662) 2024-05-22 08:14:53 -04:00
Jason Rasmussen
06ce8247cc feat(server): user metadata (#9650)
* feat(server): user metadata

* add missing method to user mock

* update migration to include cascades

* update sql files

* test: fix e2e

* chore: clean up

---------

Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2024-05-22 08:13:36 -04:00
dependabot[bot]
a4887bfa7e chore(deps): bump ytanikin/PRConventionalCommits from 1.1.0 to 1.2.0 (#9661)
---
updated-dependencies:
- dependency-name: ytanikin/PRConventionalCommits
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-22 11:43:46 +01:00
renovate[bot]
27a02c75dc chore(deps): update dependency fastlane to v2.220.0 (#9653)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-22 09:46:53 +00:00
Matthew Momjian
f8ee977b9e feat(server): healthchecks for PG and redis (#9590)
* HCs -> docker compose

---------

Co-authored-by: Zack Pollard <zackpollard@ymail.com>
2024-05-22 09:28:12 +00:00
Zack Pollard
a3e7e8cc31 refactor: deprecate /server-info and replace with /server-info/storage (#9645) 2024-05-22 10:25:55 +01:00
Snowknight26
a341ab0050 refactor(web): refactor album selection modal and album summary component (#9658) 2024-05-22 00:15:28 -05:00
Lukas
61b850f0ce fix(web): emit updated date when pressing enter (#9640) 2024-05-21 16:58:57 +00:00
Jason Rasmussen
a3489d604b chore: remove unused stubs (#9647) 2024-05-21 18:35:26 +02:00
renovate[bot]
00b5ad3421 chore(deps): update dependency @types/lodash to v4.17.3 (#9644)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-21 16:18:35 +00:00
Jason Rasmussen
2ada4a8426 chore: gitignore open api docs/tests (#9643) 2024-05-21 16:35:20 +01:00
renovate[bot]
924e9f08cd chore(deps): update mobile (#9629)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-21 13:40:21 +00:00
Jason Rasmussen
91b835cfeb fix: auth sub override (#9635) 2024-05-21 09:07:34 -04:00
renovate[bot]
bb79df655d fix(deps): update dependency sharp to v0.33.4 (#9633)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-21 13:54:56 +01:00
Zack Pollard
02e755bd92 ci: add conventional commit validation for PR titles (#9634) 2024-05-21 13:54:21 +01:00
renovate[bot]
0963a32a95 chore(deps): update base-image to v20240521 (major) (#9632)
chore(deps): update base-image to v20240521

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-21 11:53:47 +00:00
renovate[bot]
6119618ae2 chore(deps): update mambaorg/micromamba:bookworm-slim docker digest to d5b8281 (#9616)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-21 09:48:11 +01:00
renovate[bot]
4d044658a0 fix(deps): update dependency svelte-maplibre to v0.9.3 (#9626)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-21 09:35:16 +01:00
renovate[bot]
67fa598f44 chore(deps): update mobile (#9621)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-21 08:31:53 +00:00
waclaw66
9222b9d130 fix(mobile): invite user list (#9624)
* fix(mobile): invite user list

* make it dense as before
2024-05-21 09:27:17 +01:00
Zack Pollard
d6757fc2dd chore: update fvmrc flutter version to 3.22.0 (#9625) 2024-05-21 09:22:22 +01:00
Alex
4f838eabbe fix(server): semver in development (#9620) 2024-05-20 23:03:28 -04:00
renovate[bot]
143b9d6828 fix(deps): update typescript-projects (#9617)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-21 00:32:18 +00:00
Jason Rasmussen
1df7be8436 refactor(server): version logic (#9615)
* refactor(server): version

* test: better version and log checks
2024-05-20 20:31:36 -04:00
renovate[bot]
5f25f28c42 chore(deps): update redis:6.2-alpine docker digest to c0634a0 (#9577)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-20 20:19:43 -04:00
renovate[bot]
ae92422df7 chore(deps): update grafana/grafana docker tag to v11 (#9599)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-20 20:19:14 -04:00
Jason Rasmussen
84d824d6a7 refactor: library type (#9525) 2024-05-20 18:09:10 -04:00
Eric Barch
4353153fe6 fix(web): render failed upload buttons correctly on mobile (#9601)
fix(web): Failed upload buttons render correctly on mobile
2024-05-20 09:59:27 -05:00
Parsa
c37bf9d5d0 fix: docker compose pull rate limit (#9600)
* fix: docker compose pull rate limit

with "registry.hub.docker.com/" behind the image name, there was an issue where "docker compose up -d" would throw a rate-limiting error, even when logged in using a docker account.

it doesn't really matter where the image is downloaded from as long as it has the same sha256 hash in docker-compose.yml

* fix: use `docker.io/` for image reference in docker-compose.yml
2024-05-20 09:58:47 -05:00
Snowknight26
5c8c0b2f5b fix(web): prevent asset grid dates from being truncated (#9603) 2024-05-20 08:21:01 -04:00
Snowknight26
39129721fa fix(web): fix add to album modal text spacing (#9606)
* fix(web): fix add to album modal text spacing

* Fix formatting
2024-05-20 08:20:08 -04:00
Matthew Momjian
451416ec88 docs: community docs typos (#9604)
typos
2024-05-19 15:50:46 -04:00
Robert Schäfer
e39ee8a16f docs: Add pgadmin4 to docker-compose.yml (#9556)
* docs: Add pgadmin4 to docker-compose.yml

Motivation
----------
The current documentation encourages to install pgAdmin3 on the host
system. It's much simpler to add `pgAdmin4` to the `docker-compose.yml`:

1. No configuration needs to be modified, just added.
2. Better security because no additional ports need to be opened on the host.
3. Easier installation. E.g. on Archlinux there is no package pgAdmin3
   anymore.
4. `pgAdmin3` does not seem to be maintained.

How to test
-----------
1. Follow the documentation.
2. See if you can connect to the immich database

* docs: better use separate config file

I assume most users will not edit the `docker-compose.yml` and forget
about `pgAdmin` once they're done. So, `pgAdmin` might get exposed to
the internet with default credentials, which is not good.

Better to leave a hint to change the credentials and keep the
configuration separate, so users start `pgAdmin` knowingly and turn it
off once they're done.
2024-05-19 07:43:40 -05:00
renovate[bot]
1e56352b04 chore(deps): update registry.hub.docker.com/library/redis:6.2-alpine docker digest to c0634a0 (#9578)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-19 12:34:09 +02:00
Jason Rasmussen
470c95d614 chore: vs code formatter settings (#9562) 2024-05-18 13:50:53 -05:00
Alex
1ad04f0b17 chore(server): openapi generation (#9585) 2024-05-18 13:50:28 -05:00
Alex
60427f18ce chore(server): return duplicate assets as group (#9576)
* chore(server): return duplicate assets as group

* file name
2024-05-18 13:15:56 -05:00
Mert
9aed736911 fix(ml): openvino not working with kernel 6.7.5 or later (#9541)
* add envs

* move to Dockerfile
2024-05-18 00:00:20 +00:00
Robert Schäfer
6b369e84fc docs: use npm in README (#9566)
Motivation
----------
Looks like `npm` is being used, `package-lock.json` is checked in
whereas `yarn.lock` is gitignored.

So, let's use `npm` in the README.

How to test
-----------
1. Follow the README.
2. It behaves just like before.
2024-05-17 16:49:23 -04:00
Jason Rasmussen
136bb69bd0 refactor: sdk init (#9563) 2024-05-17 16:48:29 -04:00
Nicholas Flamy
975f2351ec fix(server): Disable duplicate detection when smart search disabled (#9565) 2024-05-17 16:37:26 -04:00
Jason Rasmussen
2e62c7b417 refactor: node_env => immich_env (#9561) 2024-05-17 13:30:05 -04:00
Alex
2689178a35 chore(server): openapi generation for Duplicate controller (#9560) 2024-05-17 12:05:23 -05:00
Jason Rasmussen
d61418886f refactor!: port env (#9559)
refactor: port env
2024-05-17 12:59:05 -04:00
Jason Rasmussen
c03981ac1d refactor(server): new version check (#9555) 2024-05-17 12:22:39 -04:00
Jason Rasmussen
4807fc40a6 refactor!: LOG_LEVEL => IMMICH_LOG_LEVEL (#9557)
refactor: LOG_LEVEL => IMMICH_LOG_LEVEL
2024-05-17 11:44:22 -04:00
Alex
101bc290f9 chore(web): light theme text color improvement (#9553)
* chore(web): light theme text color improvement

* openapi

* openapi
2024-05-17 15:05:59 +00:00
Alex
df42352f84 chore(server): openapi generation (#9554) 2024-05-17 14:58:55 +00:00
Zack Pollard
c8aa6a62c2 fix: when using old script args, just set the workers include var (#9552)
* fix: when using old script args, just set the workers include var and move on

* fix: set process.title when using new bootstrap worker startup method
2024-05-17 15:10:57 +01:00
Zack Pollard
85aca2bb54 feat: microservices be gone (#9551)
* feat: microservices be gone and api is a worker now too

* chore: remove very old startup scripts, surely nobody is using these anymore, right?

right?....
2024-05-17 14:44:30 +01:00
Mert
ff52300624 refactor(server): duplicate controller and service (#9542)
* duplicate controller and service

* change endpoint name

* fix search tests

* remove unused import

* add to index
2024-05-16 19:39:33 -04:00
Jason Rasmussen
936a46b4ed fix(server): use jasonnnnnnnnnb (#9539) 2024-05-16 17:24:54 -04:00
Mert
d8eca168ca feat(server): fully accelerated nvenc (#9452)
* use arrayContaining

* libplacebo for nvenc

update dockerfile

* tweaks

* update nvenc options

* tweak settings

* refactor

* toggle for hardware decoding, software / hardware decoding for nvenc and rkmpp

* fix software tone-mapping not being applied

* separate configs for hw/sw

* update api

* add hw decode toggle

* fix mutating config

* remove `version` flag

* fix config type

* remove submodule

* handle temporal AQ

* remove duplicate tests

* use `tonemap_opencl`

* wording

* update docs
2024-05-16 13:30:26 -04:00
Mert
64636c0618 feat(server): near-duplicate detection (#8228)
* duplicate detection job, entity, config

* queueing

* job panel, update api

* use embedding in db instead of fetching

* disable concurrency

* only queue visible assets

* handle multiple duplicateIds

* update concurrent queue check

* add provider

* add web placeholder, server endpoint, migration, various fixes

* update sql

* select embedding by default

* rename variable

* simplify

* remove separate entity, handle re-running with different threshold, set default back to 0.02

* fix tests

* add tests

* add index to entity

* formatting

* update asset mock

* fix `upsertJobStatus` signature

* update sql

* formatting

* default to 0.03

* optimize clustering

* use asset's `duplicateId` if present

* update sql

* update tests

* expose admin setting

* refactor

* formatting

* skip if ml is disabled

* debug trash e2e

* remove from web

* remove from sidebar

* test if ml is disabled

* update sql

* separate duplicate detection from clip in config, disable by default for now

* fix doc

* lower minimum `maxDistance`

* update api

* Add and Use Duplicate Detection Feature Flag (#9364)

* Add Duplicate Detection Flag

* Use Duplicate Detection Flag

* Attempt Fixes for Failing Checks

* lower minimum `maxDistance`

* fix tests

---------

Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com>

* chore: fixes and additions after rebase

* chore: update api (remove new Role enum)

* fix: left join smart search so getAll works without machine learning

* test: trash e2e go back to checking length of assets is zero

* chore: regen api after rebase

* test: fix tests after rebase

* redundant join

---------

Co-authored-by: Nicholas Flamy <30300649+NicholasFlamy@users.noreply.github.com>
Co-authored-by: Zack Pollard <zackpollard@ymail.com>
Co-authored-by: Zack Pollard <zack@futo.org>
2024-05-16 18:08:37 +01:00
Alex
673e97e71d chore(mobile): upgrade flutter to 3.22 (#9518)
* chore(mobile): upgrade flutter sdk

* gha

* update kotlin

* refactor

* ios build

* remove patch files

* not touching openapi pubpsec file
2024-05-16 10:58:02 -05:00
Jason Rasmussen
984aa8fb41 refactor(server): system config (#9517) 2024-05-15 18:58:23 -04:00
renovate[bot]
7f0f016f2e chore(deps): update dependency eslint-plugin-unicorn to v53 (#9502)
* chore(deps): update dependency eslint-plugin-unicorn to v53

* use structured clone to match new eslint rules

* use raw string instead of escaping slash

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2024-05-15 18:06:25 -04:00
Jason Rasmussen
0589575154 chore: bump open-api (#9522) 2024-05-15 16:52:52 -05:00
Jason Rasmussen
73bf8f343a chore(server): remove unused property (#9521) 2024-05-15 15:17:48 -04:00
Michel Heusschen
581b467b4b fix(server): smtp certificate validation (#9506) 2024-05-15 07:21:35 -04:00
renovate[bot]
efb844c6cd fix(deps): update dependency @zoom-image/svelte to v0.2.12 (#9487)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-14 19:33:53 -04:00
Alex The Bot
88d4338348 Version v1.105.1 2024-05-14 21:31:24 +00:00
Jason Rasmussen
ce7bbe88f9 fix(server): skip originals when deleting a library (#9496) 2024-05-14 16:29:57 -05:00
kleinMaggus
d62e90424e feat(web,mobile) Allow videos to be looped in the detail viewer (#8615)
* First version of video looping for the web

* Use prop for slideshow state

* refactor asset settings and add autoloop video setting

* rename variables and adjust description

* loop videos based on user settings in gallery viewer

* make asset viewer setting a stateless widget

* do not update video playback value if looping is enabled

* add some translations

* adjust description

* add missing id

* WIP

* chore: clean up

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-05-14 14:31:47 -05:00
Jason Rasmussen
0f129cae4a refactor(server): feature flags (#9492) 2024-05-14 15:31:36 -04:00
renovate[bot]
5583635947 chore(deps): update python:3.11-bookworm docker digest to 96de1ea (#9490) 2024-05-14 19:13:56 +00:00
renovate[bot]
b7715305b3 chore(deps): update dependency fastlane to v2.220.0 (#9491)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-14 14:12:27 -05:00
shenlong
6c008176c9 deps(mobile): update dependency auto_route to v8 (#9456)
deps: update dependency auto_route to v8

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-05-14 14:07:31 -05:00
renovate[bot]
72b1d582ba chore(deps): update dependency flutter_lints to v4 (#9488)
* chore(deps): update dependency flutter_lints to v4

* fix lints

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-05-14 18:52:07 +00:00
Jason Rasmussen
7b1112f3e3 refactor(server): system config (#9484) 2024-05-14 14:43:49 -04:00
renovate[bot]
42d0fc85ca chore(deps): update mobile (#9453)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-14 13:39:48 -05:00
Alex Tran
d32b621750 Merge branch 'main' of github.com:immich-app/immich 2024-05-14 13:38:15 -05:00
Alex Tran
d551003311 chore: post release tasks 2024-05-14 13:38:12 -05:00
dependabot[bot]
dd64379a4e chore(deps-dev): bump flask-cors from 4.0.0 to 4.0.1 in /machine-learning (#9485) 2024-05-14 14:06:40 -04:00
dependabot[bot]
c1cdda0ac4 chore(deps-dev): bump jinja2 from 3.1.3 to 3.1.4 in /machine-learning (#9483) 2024-05-14 17:08:29 +00:00
Alex The Bot
596ab39293 Version v1.105.0 2024-05-14 17:07:25 +00:00
renovate[bot]
31e57d27a7 fix(deps): update dependency @zoom-image/svelte to v0.2.11 (#9482)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-14 18:04:44 +01:00
Alex
f28b4e7c99 fix(server): sync issue when delete remotes assets (#9479) 2024-05-14 11:51:15 -05:00
dependabot[bot]
f01cf63c70 chore(deps): bump tqdm from 4.66.1 to 4.66.3 in /machine-learning (#9481) 2024-05-14 16:51:07 +00:00
dependabot[bot]
e55c5091d9 chore(deps-dev): bump werkzeug from 3.0.1 to 3.0.3 in /machine-learning (#9480) 2024-05-14 16:37:50 +00:00
klahr
e8cdf1c300 Added Swedish translation of README. (#9464) 2024-05-14 10:35:52 -05:00
Fynn Petersen-Frey
116043b2b4 feat(mobile): use efficient sync (#8842)
* feat(mobile): use efficient sync

review feedback

* adapt to changed  server endpoints

* formatting

* fix memory lane bug

* fix: bad merge

* fix call not returning correct number of asset

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-05-14 10:35:37 -05:00
Michel Heusschen
acc611a3d9 fix(web): admin settings number input validation (#9470)
* fix(web): admin settings number input validation

* fix import by creating *.ts file

* just ignore import error
2024-05-14 15:35:16 +00:00
Fynn Petersen-Frey
4d7aa7effd fix(server): new full sync return stacked assets individually (#9189)
* fix(server): new full sync return stacked assets individually

* return archived partner assets (like old getAllAssets)

* fix

* fix test

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-05-14 10:30:33 -05:00
renovate[bot]
77b8c2f330 chore(deps): update machine-learning (#9478)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-14 10:57:15 -04:00
renovate[bot]
09e9e91b6a fix(deps): update machine-learning (#9304)
* fix(deps): update machine-learning

* use fastapi-slim

* fix lock

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com>
2024-05-14 14:46:20 +00:00
Mert
ad915ccd64 docs(ml): update link (#9477)
update link
2024-05-14 15:34:36 +01:00
Zack Pollard
1ea55d642e feat(server): run microservices in worker thread (#9426)
feat: start microservices in worker thread and add internal microservices for the server
2024-05-14 15:28:20 +01:00
renovate[bot]
3d5e55bdaa chore(deps): update base-image to v20240514 (major) (#9469)
chore(deps): update base-image to v20240514

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-14 08:55:09 -04:00
Jason Rasmussen
46868b3336 refactor(server): logger (#9472) 2024-05-14 08:48:49 -04:00
Ben McCann
b1ca5455b5 docs: remove mention of external assets being read-only (#9465) 2024-05-14 11:02:26 +01:00
xiagw
462f0f76a4 fix install.sh add random password for .env (#9282)
* fix install.sh add random password for .env

* fix generate random password

* remove comment
2024-05-14 10:58:28 +01:00
renovate[bot]
3d4ae9c210 chore(deps): update node.js to 53108f6 (#9450)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-14 10:53:04 +01:00
Snowknight26
f2be310aec fix(web): decrease asset viewer navigation area size (#9455)
* fix(web): decrease asset viewer navigation area size

* Remove unneeded class

* Reduce wrapping div area
2024-05-14 10:52:39 +01:00
Eric Barch
6fd6a8ba15 fix(server): addAssets and removeAssets handle duplicate assetIds (#9436)
* fix(server): addAssets and removeAssets handle duplicate assetIds

* chore(server): Add e2e tests for duplicate album additions and removals
2024-05-14 03:29:32 +00:00
David Munn
e479e556bc Fix typo in error page title (#9451)
Fixes #9447
2024-05-14 02:14:44 +00:00
renovate[bot]
bf036f2f58 fix(deps): update typescript-projects (#9454)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-13 21:42:48 -04:00
renovate[bot]
6d575e692b chore(deps): update node.js to 291e84d (#9449)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-13 21:01:34 -04:00
Sushain Cherivirala
4e6aeeda4d fix(server): support special characters in library paths (#9385)
Support special characters in library paths
2024-05-13 21:44:21 +00:00
Jason Rasmussen
a05c990718 feat(web): combine auth settings (#9427) 2024-05-13 16:40:33 -04:00
Jason Rasmussen
844f5a16a1 chore(server): remove unused column (#9431)
* chore(server): remove unused column

* fix: broken migrations
2024-05-13 16:40:16 -04:00
Jason Rasmussen
1bebc7368c fix(server): regenerate (extract) motion videos (#9438) 2024-05-13 16:38:11 -04:00
Jason Rasmussen
b7ebf3152f fix(web): show w x h correctly when media is rotated (#9435) 2024-05-13 15:03:36 -05:00
Alex Tran
5985f72643 chore: post release tasks 2024-05-13 14:17:28 -05:00
Alex The Bot
a6f557c24c Version v1.104.0 2024-05-13 18:31:57 +00:00
Alex
1045957add Localizely: Translations update (#9432)
chore(mobile): translation update
2024-05-13 13:27:21 -05:00
FedericoCalzoni
540e568e9d docs: update external-library.md (#9420)
* Update external-library.md

I believe that displaying the code for both sections, even if it seems a bit repetitive, can help prevent fast readers from overlooking it

* Apply suggestions from code review

Co-authored-by: Matthew Momjian <50788000+mmomjian@users.noreply.github.com>

---------

Co-authored-by: Matthew Momjian <50788000+mmomjian@users.noreply.github.com>
2024-05-13 12:49:30 -04:00
Jason Rasmussen
9c5a2b97bf fix(server): put system config (#9425) 2024-05-13 16:29:39 +00:00
Alex
06402aa9fb chore(server): email notification button png (#9423)
* chore(server): email notification button png

* valid user name

---------

Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2024-05-13 15:36:35 +00:00
Matthew Momjian
45316f985b Update portainer install docs (#9421)
* Update portainer.md

* Update portainer.md

* chore: cleanup

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-05-13 09:46:38 -04:00
Andreas Gerstmayr
48927f5fb9 feat(server, web): include pictures of shared albums on map (#7439)
* feat(server, web): include pictures of shared albums on map

* run prettier

* re-create api clients

* implement suggestions from code review

* shared from partner -> shared from partners

* rename to 'include shared partner assets'

* chore: fix tsc error in server and prettier in web

* fix: include assets shared via owner albums

---------

Co-authored-by: Zack Pollard <zackpollard@ymail.com>
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-05-13 09:28:57 -04:00
renovate[bot]
d121903b38 fix(deps): update dependency nestjs-otel to v6 (#9415)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-13 08:23:12 -04:00
Matthew Momjian
e2a31323bb docs: add info about postgres database checksums (#9391)
* docs staging for next release

* linting

* newline

* remove old info
2024-05-13 11:23:20 +01:00
martin
aa1dc68867 feat(web): allow hiding all unnamed faces and reset hidden faces (#9378)
* feat: hide all unnamed

* feat: remove dispatch event

* pr feedback
2024-05-13 11:17:52 +01:00
Deedikjepijn
2ae44022c2 docs: update README_nl_NL.md with missing sections from english readme (#9399)
* Update README_nl_NL.md

Added missing Features for the dutch translation of the readme file,

Added "Activities" to the dutch version,

Added "star history",

Added "Contributors".

* Update README_nl_NL.md

Translated one additional word on the dutch activities panel

* Update readme_i18n/README_nl_NL.md

Co-authored-by: bo0tzz <git@bo0tzz.me>

* Update readme_i18n/README_nl_NL.md

Co-authored-by: bo0tzz <git@bo0tzz.me>

---------

Co-authored-by: Zack Pollard <zackpollard@ymail.com>
Co-authored-by: bo0tzz <git@bo0tzz.me>
2024-05-13 10:17:13 +00:00
Nicholas Flamy
a97e0caeb9 cho(web): Icon consistency and improvements (#9403)
* Add change from outline to regular icon in sidebar when page selected to more icons. Also change Favorites to single heart consistent with mobile app.

* Forgot to remove a few unused lines. Fixed.
2024-05-12 13:08:17 -05:00
Mert
f667c9597b chore(server): less rigid transcoding tests (#9401)
* use arrayContaining

* clean up
2024-05-12 00:04:31 -04:00
martin
8fdcabaf70 fix(web): page down shortcut (#9397)
fix: page down shortcut
2024-05-11 16:49:39 -05:00
Matthew Momjian
55a7e54011 feat(server): enable data page checksums (#9384) 2024-05-11 11:11:14 -04:00
aviv926
6f82f220b8 docs: small changes&updates (#9059)
* Updates

* Adding information to the PR Checklist

* fix  #5644

* PR feedback

* suggestions

* Revert dbname
2024-05-10 23:15:38 -05:00
Tushar Harsora
dd8d7732de fix(web): Fixed video unmutes when scrubbing (#9382)
Fixed video unmutes when scrubbing

Co-authored-by: Tushar Harsora <tushar.harsora.harsora@oracle.com>
2024-05-10 16:28:21 -04:00
Mert
bb4843747b perf: cache transcoding devices (#9381)
cache transcoding devices
2024-05-10 15:03:47 -04:00
Mert
f3fbb9b588 perf: cache getConfig (#9377)
* cache `getConfig`

* critical fix

Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>

---------

Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
2024-05-10 18:15:25 +00:00
Daniel Dietzler
35ef82ab6b docs: add discord link (#9379)
add discord link to docs
2024-05-10 12:38:35 -05:00
Alex
fa4cd74dfd fix(web): autofocus change name field (#9376) 2024-05-10 17:18:10 +00:00
Alex
fed8d11fb8 refactor(mobile): remove shared module (#9363) 2024-05-10 11:40:41 -05:00
Matthew Momjian
3a68190b99 docs: warn against use on NTFS / WSL (#9371)
* DB filesystem

* updates to requirements

* wording

* OS update

* Update environment-variables.md

* Update FAQ.mdx

* Update requirements.md
2024-05-10 11:02:48 -04:00
martin
757840c2fd fix(web): multiple fixes for people (#9343)
fix: multiple fixes for people

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-05-09 16:23:49 -05:00
Alex
a8abf2753e chore: put back warning banner (#9362) 2024-05-09 15:53:48 -05:00
Jason Rasmussen
8743e17528 refactor(server): auth route metadata (#9344) 2024-05-09 13:58:44 -04:00
Conner Hnatiuk
34d8879d32 fix(mobile) timeline slider no longer dissapears (#9358)
fix(mobile) added heightOffset, scrollbar accounts for main appBar and does not get squished

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-05-09 17:31:25 +00:00
Alex
55031cc117 Revert "feat(android) Check server is reachable before starting background backup (#8989)" (#9324)
This reverts commit 0435de50f8.
2024-05-09 12:16:53 -05:00
Snowknight26
f9dc870166 fix(web): fix logo size on shared pages (#9346) 2024-05-09 09:21:45 -05:00
Me-Mr
87053c8c0d docs: adds hyperlinks for other readme translations (#9345)
* Translation

* Translation

* fix

---------

Co-authored-by: Abdullah Karazon <abdullahkarazon@MacBook-Air.local>
2024-05-09 14:20:38 +00:00
Tushar Harsora
57429ddc76 fix(web): Selected face in search filter doesn't show border highlight when hovered in light theme (#9348)
use border-transparent only when person is not selected

Co-authored-by: Tushar Harsora <tushar.harsora.harsora@oracle.com>
2024-05-09 09:19:26 -05:00
Matthew Momjian
13a62715e4 docs: backup restore - add warning to not backup DB_DATA_LOCATION (#9353)
backup restore add warning for backing up folder
2024-05-09 09:18:52 -05:00
Me-Mr
f3407860a1 chore: add Arabic translations to readme (#9342)
* Translation

* Translation

---------

Co-authored-by: Abdullah Karazon <abdullahkarazon@MacBook-Air.local>
2024-05-08 20:03:25 -04:00
renovate[bot]
004074b25a chore(deps): update prom/prometheus docker digest to 5c43564 (#9341)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-08 19:20:58 -04:00
The Linux Cast
98f87b275b docs: update portainer.md (#9337)
Changed #3 to lowercase, as Portainer stacks do not accept capital letters.
2024-05-08 16:54:20 -05:00
Jason Rasmussen
8e9895df27 chore(server): remove unused deps (#9334) 2024-05-08 16:39:35 -04:00
Mert
1167f0f2b7 feat(server): optimize person thumbnail generation (#7513)
* do crop and resize together

* redundant `pipelineColorspace` call

* formatting

* fix rebase

* handle orientation

* remove unused import

* formatting

* use oriented dimensions for half size calculation

* default case for orientation

* simplify orientation code

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-05-08 08:09:34 -05:00
Me-Mr
81e4b69caf chore: arabic translations (#9325)
* Add Arabic translations for readme file

* Update ReadMe with Arabic Translations

* Delete readme_i18n/README_ar_JO.md

* Updated Arabic Translations for Readme

---------

Co-authored-by: Abdullah Karazon <abdullahkarazon@MacBook-Air.local>
2024-05-08 00:03:41 -04:00
Dhrumil Shah
e4b777ecef feat(mobile): Add pull to refresh to sharing page (#9274)
* Add pull to refresh to sharing page

* Fix format
2024-05-07 22:52:56 -05:00
Jason Rasmussen
4e7966c8e8 chore: move test assets submodule (#9321) 2024-05-07 22:33:17 -05:00
renovate[bot]
f10fb0723d chore(deps): update mobile (#7229)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-07 22:24:37 -05:00
Dhrumil Shah
ae08abde24 fix(mobile): Fix asset selector title bar text (#9273)
Fix title bar text
2024-05-07 22:24:30 -05:00
dependabot[bot]
3790d8fcbc chore(deps): bump tj-actions/verify-changed-files from 19 to 20 (#9280)
Bumps [tj-actions/verify-changed-files](https://github.com/tj-actions/verify-changed-files) from 19 to 20.
- [Release notes](https://github.com/tj-actions/verify-changed-files/releases)
- [Changelog](https://github.com/tj-actions/verify-changed-files/blob/main/HISTORY.md)
- [Commits](https://github.com/tj-actions/verify-changed-files/compare/v19...v20)

---
updated-dependencies:
- dependency-name: tj-actions/verify-changed-files
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-07 22:22:22 -05:00
Jan
535c7a8618 Random Endpoint use querybuilder and return exifInfo (#9301)
* Random Endpoint use querybuilder and return exifInfo

* Add GenerateSql

* Commit missing sql changes
2024-05-07 23:14:04 -04:00
renovate[bot]
725d594027 chore(deps): update node.js to fac6f74 (#9317)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-07 23:12:21 -04:00
renovate[bot]
8c56c1c22b chore(deps): update typescript-projects (#9314)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-07 23:07:26 -04:00
Snowknight26
6a5bc156a6 fix(web): allow deselecting all assets from select bar (#9320)
* fix(web): allow deselecting all assets from select bar

* Change deselect logo

* select remove

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-05-08 03:05:19 +00:00
aryiu
5eaf489ecf fix(mobile): fix ca-CA.json file name (#9288)
Update and rename ca.json to ca-CA.json
2024-05-08 03:03:14 +00:00
renovate[bot]
8c54c13307 chore(deps): update node.js to fe31b16 (#9318)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-07 23:01:04 -04:00
Alex
675fdc1d93 chore: remove funding file (#9319) 2024-05-07 22:35:57 -04:00
Jason Rasmussen
e79d1b1ec2 refactor: create album users (#9315) 2024-05-07 16:38:09 -04:00
vovchyk bratyk
e9f99673b9 docs: add custom map styles community guide (#9311)
* Create custom-map-styles.md

* add immich_map_styles_publish.png

* Update custom-map-styles.md
2024-05-07 20:17:21 +00:00
Alex
ee6995783f fix(mobile): add user to album using new dto property (#9312)
* fix(mobile): add user to album using new DTO property

* fix(mobile): add user to album using new DTO property
2024-05-07 20:00:38 +00:00
renovate[bot]
78b5990d2a chore(deps): update dependency @types/nodemailer to v6.4.15 (#9309)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-07 15:15:24 -04:00
renovate[bot]
f13100e261 fix(deps): update dependency exiftool-vendored to v26 (#9294)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-07 15:14:49 -04:00
renovate[bot]
21fe829a2c chore(deps): update base-image to v20240507 (major) (#9303)
chore(deps): update base-image to v20240507

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-07 11:10:21 -04:00
renovate[bot]
ad404d79d4 fix(deps): update typescript-projects (#9293)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-07 11:07:50 -04:00
Alex
5806a3ce25 refactor(mobile): widgets (#9291)
* refactor(mobile): widgets

* update
2024-05-06 23:04:21 -05:00
Alex
7520ffd6c3 fix(mobile): allow select empty album as backup album (#9290) 2024-05-06 23:00:52 -05:00
Alex
bbb9453e1a fix(server): show partners archived photos on mobile timeline (#9194)
fix(mobile): show partners archived photos on timeline
2024-05-06 22:49:56 -05:00
renovate[bot]
8ff397527f chore(deps): update machine-learning (#9292) 2024-05-06 22:21:24 -04:00
Alex
a2c5eaa73e Localizely: Translations update (#9287)
chore(mobile): translation update
2024-05-06 15:39:53 -05:00
Tushar Harsora
065f1410f8 chore(mobile) Linked pt-BR.json with Mobile app and localizely.yml (#9281)
Linked pt-BR.json with Android app and localizely.yml

Co-authored-by: Tushar Harsora <tushar.harsora.harsora@oracle.com>
2024-05-06 11:22:27 -05:00
Vietbao Tran
bc31404909 fix(web): stream 360 video instead of fetching entire video (#9279) 2024-05-06 11:20:20 -05:00
1446 changed files with 20161 additions and 38407 deletions

View File

@@ -23,7 +23,6 @@ server/node_modules/
server/upload/
server/dist/
server/www/
server/test/assets/
web/node_modules/
web/coverage/

2
.gitattributes vendored
View File

@@ -2,8 +2,6 @@ mobile/openapi/**/*.md -diff -merge
mobile/openapi/**/*.md linguist-generated=true
mobile/openapi/**/*.dart -diff -merge
mobile/openapi/**/*.dart linguist-generated=true
mobile/openapi/.openapi-generator/FILES -diff -merge
mobile/openapi/.openapi-generator/FILES linguist-generated=true
mobile/lib/**/*.g.dart -diff -merge
mobile/lib/**/*.g.dart linguist-generated=true

5
.github/FUNDING.yml vendored
View File

@@ -1,5 +0,0 @@
# These are supported funding model platforms
github: immich-app
liberapay: alex.tran1502
custom: https://www.buymeacoffee.com/altran1502

View File

@@ -45,7 +45,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: '3.19.3'
flutter-version: '3.22.0'
cache: true
- name: Create the Keystore

View File

@@ -0,0 +1,15 @@
name: PR Conventional Commit Validation
on:
pull_request:
types: [opened, synchronize, reopened, edited]
jobs:
validate-pr-title:
runs-on: ubuntu-latest
steps:
- name: PR Conventional Commit Validation
uses: ytanikin/PRConventionalCommits@1.2.0
with:
task_types: '["feat","fix","docs","test","ci","refactor","perf","chore","revert"]'
add_label: 'false'

View File

@@ -22,8 +22,8 @@ jobs:
- name: Setup Flutter SDK
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: "3.19.3"
channel: 'stable'
flutter-version: '3.22.0'
- name: Install dependencies
run: dart pub get

View File

@@ -208,7 +208,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: '3.19.3'
flutter-version: '3.22.0'
- name: Run tests
working-directory: ./mobile
run: flutter test -j 1
@@ -260,16 +260,27 @@ jobs:
name: OpenAPI Clients
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Checkout code
uses: actions/checkout@v4
- name: Install server dependencies
run: npm --prefix=server ci
- name: Build the app
run: npm --prefix=server run build
- name: Run API generation
run: make open-api
- name: Find file changes
uses: tj-actions/verify-changed-files@v19
uses: tj-actions/verify-changed-files@v20
id: verify-changed-files
with:
files: |
mobile/openapi
open-api/typescript-sdk
open-api/immich-openapi-specs.json
- name: Verify files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true'
run: |
@@ -319,7 +330,7 @@ jobs:
run: npm run typeorm:migrations:generate ./src/migrations/TestMigration
- name: Find file changes
uses: tj-actions/verify-changed-files@v19
uses: tj-actions/verify-changed-files@v20
id: verify-changed-files
with:
files: |
@@ -332,12 +343,12 @@ jobs:
exit 1
- name: Run SQL generation
run: npm run sql:generate
run: npm run sync:sql
env:
DB_URL: postgres://postgres:postgres@localhost:5432/immich
- name: Find file changes
uses: tj-actions/verify-changed-files@v19
uses: tj-actions/verify-changed-files@v20
id: verify-changed-sql-files
with:
files: |

5
.gitignore vendored
View File

@@ -14,7 +14,10 @@ mobile/gradle.properties
mobile/openapi/pubspec.lock
mobile/*.jks
mobile/libisar.dylib
mobile/openapi/test
mobile/openapi/doc
mobile/openapi/.openapi-generator/FILES
open-api/typescript-sdk/build
mobile/android/fastlane/report.xml
mobile/ios/fastlane/report.xml
mobile/ios/fastlane/report.xml

2
.gitmodules vendored
View File

@@ -2,5 +2,5 @@
path = mobile/.isar
url = https://github.com/isar/isar
[submodule "server/test/assets"]
path = server/test/assets
path = e2e/test-assets
url = https://github.com/immich-app/test-assets

14
.vscode/settings.json vendored
View File

@@ -1,6 +1,16 @@
{
"editor.formatOnSave": true,
"[javascript][typescript][css]": {
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 2,
"editor.formatOnSave": true
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 2,
"editor.formatOnSave": true
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 2,
"editor.formatOnSave": true
@@ -31,4 +41,4 @@
"explorer.fileNesting.patterns": {
"*.ts": "${capture}.spec.ts,${capture}.mock.ts"
}
}
}

View File

@@ -37,7 +37,7 @@ open-api-typescript:
cd ./open-api && bash ./bin/generate-open-api.sh typescript
sql:
npm --prefix server run sql:generate
npm --prefix server run sync:sql
attach-server:
docker exec -it docker_immich-server_1 sh

View File

@@ -18,6 +18,7 @@
</a>
<br/>
<p align="center">
<a href="readme_i18n/README_ca_ES.md">Català</a>
<a href="readme_i18n/README_es_ES.md">Español</a>
<a href="readme_i18n/README_fr_FR.md">Français</a>
@@ -30,6 +31,7 @@
<a href="readme_i18n/README_zh_CN.md">中文</a>
<a href="readme_i18n/README_ru_RU.md">Русский</a>
<a href="readme_i18n/README_pt_BR.md">Português Brasileiro</a>
<a href="readme_i18n/README_ar_JO.md">العربية</a>
</p>
## Disclaimer
@@ -70,6 +72,7 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
```
## Activities
![Activities](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Repobeats analytics image")
## Features

View File

@@ -1 +1 @@
v20.12
20.13

View File

@@ -1,4 +1,4 @@
FROM node:20-alpine3.19@sha256:7a91aa397f2e2dfbfcdad2e2d72599f374e0b0172be1d86eeb73f1d33f36a4b2 as core
FROM node:20-alpine3.19@sha256:291e84d956f1aff38454bbd3da38941461ad569a185c20aa289f71f37ea08e23 as core
WORKDIR /usr/src/open-api/typescript-sdk
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./

358
cli/package-lock.json generated
View File

@@ -31,7 +31,7 @@
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^52.0.0",
"eslint-plugin-unicorn": "^53.0.0",
"mock-fs": "^5.2.0",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
@@ -47,14 +47,14 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.103.1",
"version": "1.105.1",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.3"
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^20.11.0",
"@types/node": "^20.12.12",
"typescript": "^5.3.3"
}
},
@@ -174,9 +174,9 @@
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"version": "7.24.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz",
"integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==",
"dev": true,
"engines": {
"node": ">=6.9.0"
@@ -1113,12 +1113,6 @@
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true
},
"node_modules/@types/lodash": {
"version": "4.17.0",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz",
@@ -1144,10 +1138,11 @@
}
},
"node_modules/@types/node": {
"version": "20.12.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz",
"integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==",
"version": "20.12.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
"integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
}
@@ -1158,28 +1153,21 @@
"integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
"dev": true
},
"node_modules/@types/semver": {
"version": "7.5.8",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.7.1.tgz",
"integrity": "sha512-KwfdWXJBOviaBVhxO3p5TJiLpNuh2iyXyjmWN0f1nU87pwyvfS0EmjC6ukQVYVFJd/K1+0NWGPDXiyEyQorn0Q==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.9.0.tgz",
"integrity": "sha512-6e+X0X3sFe/G/54aC3jt0txuMTURqLyekmEHViqyA2VnxhLMpvA6nqmcjIy+Cr9tLDHPssA74BP5Mx9HQIxBEA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.7.1",
"@typescript-eslint/type-utils": "7.7.1",
"@typescript-eslint/utils": "7.7.1",
"@typescript-eslint/visitor-keys": "7.7.1",
"debug": "^4.3.4",
"@typescript-eslint/scope-manager": "7.9.0",
"@typescript-eslint/type-utils": "7.9.0",
"@typescript-eslint/utils": "7.9.0",
"@typescript-eslint/visitor-keys": "7.9.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
"semver": "^7.6.0",
"ts-api-utils": "^1.3.0"
},
"engines": {
@@ -1200,15 +1188,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.7.1.tgz",
"integrity": "sha512-vmPzBOOtz48F6JAGVS/kZYk4EkXao6iGrD838sp1w3NQQC0W8ry/q641KU4PrG7AKNAf56NOcR8GOpH8l9FPCw==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.9.0.tgz",
"integrity": "sha512-qHMJfkL5qvgQB2aLvhUSXxbK7OLnDkwPzFalg458pxQgfxKDfT1ZDbHQM/I6mDIf/svlMkj21kzKuQ2ixJlatQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/scope-manager": "7.7.1",
"@typescript-eslint/types": "7.7.1",
"@typescript-eslint/typescript-estree": "7.7.1",
"@typescript-eslint/visitor-keys": "7.7.1",
"@typescript-eslint/scope-manager": "7.9.0",
"@typescript-eslint/types": "7.9.0",
"@typescript-eslint/typescript-estree": "7.9.0",
"@typescript-eslint/visitor-keys": "7.9.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1228,13 +1217,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.7.1.tgz",
"integrity": "sha512-PytBif2SF+9SpEUKynYn5g1RHFddJUcyynGpztX3l/ik7KmZEv19WCMhUBkHXPU9es/VWGD3/zg3wg90+Dh2rA==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.9.0.tgz",
"integrity": "sha512-ZwPK4DeCDxr3GJltRz5iZejPFAAr4Wk3+2WIBaj1L5PYK5RgxExu/Y68FFVclN0y6GGwH8q+KgKRCvaTmFBbgQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.7.1",
"@typescript-eslint/visitor-keys": "7.7.1"
"@typescript-eslint/types": "7.9.0",
"@typescript-eslint/visitor-keys": "7.9.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1245,13 +1235,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.7.1.tgz",
"integrity": "sha512-ZksJLW3WF7o75zaBPScdW1Gbkwhd/lyeXGf1kQCxJaOeITscoSl0MjynVvCzuV5boUz/3fOI06Lz8La55mu29Q==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.9.0.tgz",
"integrity": "sha512-6Qy8dfut0PFrFRAZsGzuLoM4hre4gjzWJB6sUvdunCYZsYemTkzZNwF1rnGea326PHPT3zn5Lmg32M/xfJfByA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "7.7.1",
"@typescript-eslint/utils": "7.7.1",
"@typescript-eslint/typescript-estree": "7.9.0",
"@typescript-eslint/utils": "7.9.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@@ -1272,10 +1263,11 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.7.1.tgz",
"integrity": "sha512-AmPmnGW1ZLTpWa+/2omPrPfR7BcbUU4oha5VIbSbS1a1Tv966bklvLNXxp3mrbc+P2j4MNOTfDffNsk4o0c6/w==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.9.0.tgz",
"integrity": "sha512-oZQD9HEWQanl9UfsbGVcZ2cGaR0YT5476xfWE0oE5kQa2sNK2frxOlkeacLOTh9po4AlUT5rtkGyYM5kew0z5w==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || >=20.0.0"
},
@@ -1285,13 +1277,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.7.1.tgz",
"integrity": "sha512-CXe0JHCXru8Fa36dteXqmH2YxngKJjkQLjxzoj6LYwzZ7qZvgsLSc+eqItCrqIop8Vl2UKoAi0StVWu97FQZIQ==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.9.0.tgz",
"integrity": "sha512-zBCMCkrb2YjpKV3LA0ZJubtKCDxLttxfdGmwZvTqqWevUPN0FZvSI26FalGFFUZU/9YQK/A4xcQF9o/VVaCKAg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "7.7.1",
"@typescript-eslint/visitor-keys": "7.7.1",
"@typescript-eslint/types": "7.9.0",
"@typescript-eslint/visitor-keys": "7.9.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -1313,18 +1306,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.7.1.tgz",
"integrity": "sha512-QUvBxPEaBXf41ZBbaidKICgVL8Hin0p6prQDu6bbetWo39BKbWJxRsErOzMNT1rXvTll+J7ChrbmMCXM9rsvOQ==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.9.0.tgz",
"integrity": "sha512-5KVRQCzZajmT4Ep+NEgjXCvjuypVvYHUW7RHlXzNPuak2oWpVoD1jf5xCP0dPAuNIchjC7uQyvbdaSTFaLqSdA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.15",
"@types/semver": "^7.5.8",
"@typescript-eslint/scope-manager": "7.7.1",
"@typescript-eslint/types": "7.7.1",
"@typescript-eslint/typescript-estree": "7.7.1",
"semver": "^7.6.0"
"@typescript-eslint/scope-manager": "7.9.0",
"@typescript-eslint/types": "7.9.0",
"@typescript-eslint/typescript-estree": "7.9.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1338,12 +1329,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.7.1.tgz",
"integrity": "sha512-gBL3Eq25uADw1LQ9kVpf3hRM+DWzs0uZknHYK3hq4jcTPqVCClHGDnB6UUUV2SFeBeA4KWHWbbLqmbGcZ4FYbw==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.9.0.tgz",
"integrity": "sha512-iESPx2TNLDNGQLyjKhUvIKprlP49XNEK+MvIf9nIO7ZZaZdbnfWKHnXAgufpxqfA0YryH8XToi4+CjBgVnFTSQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.7.1",
"@typescript-eslint/types": "7.9.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@@ -1361,9 +1353,9 @@
"dev": true
},
"node_modules/@vitest/coverage-v8": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.5.2.tgz",
"integrity": "sha512-QJqxRnbCwNtbbegK9E93rBmhN3dbfG1bC/o52Bqr0zGCYhQzwgwvrJBG7Q8vw3zilX6Ryy6oa/mkZku2lLJx1Q==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.0.tgz",
"integrity": "sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==",
"dev": true,
"dependencies": {
"@ampproject/remapping": "^2.2.1",
@@ -1384,17 +1376,17 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"vitest": "1.5.2"
"vitest": "1.6.0"
}
},
"node_modules/@vitest/expect": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.5.2.tgz",
"integrity": "sha512-rf7MTD1WCoDlN3FfYJ9Llfp0PbdtOMZ3FIF0AVkDnKbp3oiMW1c8AmvRZBcqbAhDUAvF52e9zx4WQM1r3oraVA==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz",
"integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==",
"dev": true,
"dependencies": {
"@vitest/spy": "1.5.2",
"@vitest/utils": "1.5.2",
"@vitest/spy": "1.6.0",
"@vitest/utils": "1.6.0",
"chai": "^4.3.10"
},
"funding": {
@@ -1402,12 +1394,12 @@
}
},
"node_modules/@vitest/runner": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.5.2.tgz",
"integrity": "sha512-7IJ7sJhMZrqx7HIEpv3WrMYcq8ZNz9L6alo81Y6f8hV5mIE6yVZsFoivLZmr0D777klm1ReqonE9LyChdcmw6g==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz",
"integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==",
"dev": true,
"dependencies": {
"@vitest/utils": "1.5.2",
"@vitest/utils": "1.6.0",
"p-limit": "^5.0.0",
"pathe": "^1.1.1"
},
@@ -1443,9 +1435,9 @@
}
},
"node_modules/@vitest/snapshot": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.5.2.tgz",
"integrity": "sha512-CTEp/lTYos8fuCc9+Z55Ga5NVPKUgExritjF5VY7heRFUfheoAqBneUlvXSUJHUZPjnPmyZA96yLRJDP1QATFQ==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz",
"integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==",
"dev": true,
"dependencies": {
"magic-string": "^0.30.5",
@@ -1457,9 +1449,9 @@
}
},
"node_modules/@vitest/spy": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.5.2.tgz",
"integrity": "sha512-xCcPvI8JpCtgikT9nLpHPL1/81AYqZy1GCy4+MCHBE7xi8jgsYkULpW5hrx5PGLgOQjUpb6fd15lqcriJ40tfQ==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz",
"integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==",
"dev": true,
"dependencies": {
"tinyspy": "^2.2.0"
@@ -1469,9 +1461,9 @@
}
},
"node_modules/@vitest/utils": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.5.2.tgz",
"integrity": "sha512-sWOmyofuXLJ85VvXNsroZur7mOJGiQeM0JN3/0D1uU8U9bGFM69X1iqHaRXl6R8BwaLY6yPCogP257zxTzkUdA==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz",
"integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==",
"dev": true,
"dependencies": {
"diff-sequences": "^29.6.3",
@@ -1564,6 +1556,7 @@
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
@@ -1588,6 +1581,7 @@
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
@@ -1822,12 +1816,12 @@
"dev": true
},
"node_modules/core-js-compat": {
"version": "3.36.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.0.tgz",
"integrity": "sha512-iV9Pd/PsgjNWBXeq8XRtWVSgz2tKAfhfvBs7qxYty+RlRd+OCksaWmOnc4JKrTc1cToXL1N0s3l/vwlxPtdElw==",
"version": "3.37.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz",
"integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==",
"dev": true,
"dependencies": {
"browserslist": "^4.22.3"
"browserslist": "^4.23.0"
},
"funding": {
"type": "opencollective",
@@ -1897,6 +1891,7 @@
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-type": "^4.0.0"
},
@@ -2094,17 +2089,17 @@
}
},
"node_modules/eslint-plugin-unicorn": {
"version": "52.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-52.0.0.tgz",
"integrity": "sha512-1Yzm7/m+0R4djH0tjDjfVei/ju2w3AzUGjG6q8JnuNIL5xIwsflyCooW5sfBvQp2pMYQFSWWCFONsjCax1EHng==",
"version": "53.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-53.0.0.tgz",
"integrity": "sha512-kuTcNo9IwwUCfyHGwQFOK/HjJAYzbODHN3wP0PgqbW+jbXqpNWxNVpVhj2tO9SixBwuAdmal8rVcWKBxwFnGuw==",
"dev": true,
"dependencies": {
"@babel/helper-validator-identifier": "^7.22.20",
"@babel/helper-validator-identifier": "^7.24.5",
"@eslint-community/eslint-utils": "^4.4.0",
"@eslint/eslintrc": "^2.1.4",
"@eslint/eslintrc": "^3.0.2",
"ci-info": "^4.0.0",
"clean-regexp": "^1.0.0",
"core-js-compat": "^3.34.0",
"core-js-compat": "^3.37.0",
"esquery": "^1.5.0",
"indent-string": "^4.0.0",
"is-builtin-module": "^3.2.1",
@@ -2113,11 +2108,11 @@
"read-pkg-up": "^7.0.1",
"regexp-tree": "^0.1.27",
"regjsparser": "^0.10.0",
"semver": "^7.5.4",
"semver": "^7.6.1",
"strip-indent": "^3.0.0"
},
"engines": {
"node": ">=16"
"node": ">=18.18"
},
"funding": {
"url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
@@ -2126,6 +2121,92 @@
"eslint": ">=8.56.0"
}
},
"node_modules/eslint-plugin-unicorn/node_modules/@eslint/eslintrc": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.0.2.tgz",
"integrity": "sha512-wV19ZEGEMAC1eHgrS7UQPqsdEiCIbTKTasEfcXAigzoXICcqZSjBZEHlZwNVvKg6UBCjSlos84XiLqsRJnIcIg==",
"dev": true,
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
"espree": "^10.0.1",
"globals": "^14.0.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
"minimatch": "^3.1.2",
"strip-json-comments": "^3.1.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-plugin-unicorn/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/eslint-plugin-unicorn/node_modules/eslint-visitor-keys": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
"integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
"dev": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-plugin-unicorn/node_modules/espree": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz",
"integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==",
"dev": true,
"dependencies": {
"acorn": "^8.11.3",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^4.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-plugin-unicorn/node_modules/globals": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
"dev": true,
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint-plugin-unicorn/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/eslint-scope": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
@@ -2466,6 +2547,7 @@
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
"integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"array-union": "^2.1.0",
"dir-glob": "^3.0.1",
@@ -2969,6 +3051,7 @@
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
@@ -3232,6 +3315,7 @@
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
@@ -3711,13 +3795,10 @@
}
},
"node_modules/semver": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
"integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
@@ -3725,18 +3806,6 @@
"node": ">=10"
}
},
"node_modules/semver/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -3781,6 +3850,7 @@
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
@@ -4210,9 +4280,9 @@
}
},
"node_modules/vite": {
"version": "5.2.10",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.10.tgz",
"integrity": "sha512-PAzgUZbP7msvQvqdSD+ErD5qGnSFiGOoWmV5yAKUEI0kdhjbH6nMWVyZQC/hSc4aXwc0oJ9aEdIiF9Oje0JFCw==",
"version": "5.2.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz",
"integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==",
"dev": true,
"dependencies": {
"esbuild": "^0.20.1",
@@ -4265,9 +4335,9 @@
}
},
"node_modules/vite-node": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.5.2.tgz",
"integrity": "sha512-Y8p91kz9zU+bWtF7HGt6DVw2JbhyuB2RlZix3FPYAYmUyZ3n7iTp8eSyLyY6sxtPegvxQtmlTMhfPhUfCUF93A==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz",
"integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==",
"dev": true,
"dependencies": {
"cac": "^6.7.14",
@@ -4306,16 +4376,16 @@
}
},
"node_modules/vitest": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.5.2.tgz",
"integrity": "sha512-l9gwIkq16ug3xY7BxHwcBQovLZG75zZL0PlsiYQbf76Rz6QGs54416UWMtC0jXeihvHvcHrf2ROEjkQRVpoZYw==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz",
"integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==",
"dev": true,
"dependencies": {
"@vitest/expect": "1.5.2",
"@vitest/runner": "1.5.2",
"@vitest/snapshot": "1.5.2",
"@vitest/spy": "1.5.2",
"@vitest/utils": "1.5.2",
"@vitest/expect": "1.6.0",
"@vitest/runner": "1.6.0",
"@vitest/snapshot": "1.6.0",
"@vitest/spy": "1.6.0",
"@vitest/utils": "1.6.0",
"acorn-walk": "^8.3.2",
"chai": "^4.3.10",
"debug": "^4.3.4",
@@ -4329,7 +4399,7 @@
"tinybench": "^2.5.1",
"tinypool": "^0.8.3",
"vite": "^5.0.0",
"vite-node": "1.5.2",
"vite-node": "1.6.0",
"why-is-node-running": "^2.2.2"
},
"bin": {
@@ -4344,8 +4414,8 @@
"peerDependencies": {
"@edge-runtime/vm": "*",
"@types/node": "^18.0.0 || >=20.0.0",
"@vitest/browser": "1.5.2",
"@vitest/ui": "1.5.2",
"@vitest/browser": "1.6.0",
"@vitest/ui": "1.6.0",
"happy-dom": "*",
"jsdom": "*"
},
@@ -4407,16 +4477,10 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/yaml": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz",
"integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==",
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz",
"integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==",
"dev": true,
"bin": {
"yaml": "bin.mjs"

View File

@@ -28,7 +28,7 @@
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^52.0.0",
"eslint-plugin-unicorn": "^53.0.0",
"mock-fs": "^5.2.0",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
@@ -62,6 +62,6 @@
"lodash-es": "^4.17.21"
},
"volta": {
"node": "20.12.2"
"node": "20.13.1"
}
}

View File

@@ -1,4 +1,4 @@
import { getMyUserInfo } from '@immich/sdk';
import { getMyUser } from '@immich/sdk';
import { existsSync } from 'node:fs';
import { mkdir, unlink } from 'node:fs/promises';
import { BaseOptions, connect, getAuthFilePath, logError, withError, writeAuthFile } from 'src/utils';
@@ -10,13 +10,13 @@ export const login = async (url: string, key: string, options: BaseOptions) => {
await connect(url, key);
const [error, userInfo] = await withError(getMyUserInfo());
const [error, user] = await withError(getMyUser());
if (error) {
logError(error, 'Failed to load user info');
process.exit(1);
}
console.log(`Logged in as ${userInfo.email}`);
console.log(`Logged in as ${user.email}`);
if (!existsSync(configDir)) {
// Create config folder if it doesn't exist

View File

@@ -1,4 +1,4 @@
import { getAssetStatistics, getMyUserInfo, getServerVersion, getSupportedMediaTypes } from '@immich/sdk';
import { getAssetStatistics, getMyUser, getServerVersion, getSupportedMediaTypes } from '@immich/sdk';
import { BaseOptions, authenticate } from 'src/utils';
export const serverInfo = async (options: BaseOptions) => {
@@ -8,7 +8,7 @@ export const serverInfo = async (options: BaseOptions) => {
getServerVersion(),
getSupportedMediaTypes(),
getAssetStatistics({}),
getMyUserInfo(),
getMyUser(),
]);
console.log(`Server Info (via ${userInfo.email})`);

View File

@@ -1,4 +1,4 @@
import { defaults, getMyUserInfo, isHttpError } from '@immich/sdk';
import { getMyUser, init, isHttpError } from '@immich/sdk';
import { glob } from 'fast-glob';
import { createHash } from 'node:crypto';
import { createReadStream } from 'node:fs';
@@ -46,10 +46,9 @@ export const connect = async (url: string, key: string) => {
// noop
}
defaults.baseUrl = url;
defaults.headers = { 'x-api-key': key };
init({ baseUrl: url, apiKey: key });
const [error] = await withError(getMyUserInfo());
const [error] = await withError(getMyUser());
if (isHttpError(error)) {
logError(error, 'Failed to connect to server');
process.exit(1);

View File

@@ -4,32 +4,32 @@
name: immich-dev
x-server-build: &server-common
image: immich-server-dev:latest
build:
context: ../
dockerfile: server/Dockerfile
target: dev
restart: always
volumes:
- ../server:/usr/src/app
- ../open-api:/usr/src/open-api
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload
- /usr/src/app/node_modules
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
ulimits:
nofile:
soft: 1048576
hard: 1048576
services:
immich-server:
container_name: immich_server
command: ['/usr/src/app/bin/immich-dev', 'immich']
<<: *server-common
command: ['/usr/src/app/bin/immich-dev']
image: immich-server-dev:latest
# extends:
# file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
build:
context: ../
dockerfile: server/Dockerfile
target: dev
restart: always
volumes:
- ../server:/usr/src/app
- ../open-api:/usr/src/open-api
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload
- /usr/src/app/node_modules
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
ulimits:
nofile:
soft: 1048576
hard: 1048576
ports:
- 3001:3001
- 9230:9230
@@ -37,19 +37,6 @@ services:
- redis
- database
immich-microservices:
container_name: immich_microservices
command: ['/usr/src/app/bin/immich-dev', 'microservices']
<<: *server-common
# extends:
# file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
ports:
- 9231:9230
depends_on:
- database
- immich-server
immich-web:
container_name: immich_web
image: immich-web-dev:latest
@@ -97,7 +84,9 @@ services:
redis:
container_name: immich_redis
image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
image: redis:6.2-alpine@sha256:e31ca60b18f7e9b78b573d156702471d4eda038803c0b8e6f01559f350031e93
healthcheck:
test: redis-cli ping || exit 1
database:
container_name: immich_postgres
@@ -108,11 +97,18 @@ services:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME}
POSTGRES_INITDB_ARGS: '--data-checksums'
volumes:
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
ports:
- 5432:5432
healthcheck:
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT SUM(checksum_failures) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
interval: 5m
start_interval: 30s
start_period: 5m
command: ["postgres", "-c" ,"shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"]
# set IMMICH_METRICS=true in .env to enable metrics
# immich-prometheus:
# container_name: immich_prometheus

View File

@@ -1,39 +1,26 @@
name: immich-prod
x-server-build: &server-common
image: immich-server:latest
build:
context: ../
dockerfile: server/Dockerfile
volumes:
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
restart: always
services:
immich-server:
container_name: immich_server
command: ['start.sh', 'immich']
<<: *server-common
image: immich-server:latest
# extends:
# file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
build:
context: ../
dockerfile: server/Dockerfile
volumes:
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
ports:
- 2283:3001
depends_on:
- redis
- database
immich-microservices:
container_name: immich_microservices
command: ['start.sh', 'microservices']
<<: *server-common
# extends:
# file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
depends_on:
- redis
- database
- immich-server
restart: always
immich-machine-learning:
container_name: immich_machine_learning
@@ -54,7 +41,9 @@ services:
redis:
container_name: immich_redis
image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
image: redis:6.2-alpine@sha256:e31ca60b18f7e9b78b573d156702471d4eda038803c0b8e6f01559f350031e93
healthcheck:
test: redis-cli ping || exit 1
restart: always
database:
@@ -66,17 +55,25 @@ services:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME}
POSTGRES_INITDB_ARGS: '--data-checksums'
volumes:
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
ports:
- 5432:5432
healthcheck:
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT SUM(checksum_failures) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
interval: 5m
start_interval: 30s
start_period: 5m
command: ["postgres", "-c" ,"shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"]
restart: always
# set IMMICH_METRICS=true in .env to enable metrics
immich-prometheus:
container_name: immich_prometheus
ports:
- 9090:9090
image: prom/prometheus@sha256:4f6c47e39a9064028766e8c95890ed15690c30f00c4ba14e7ce6ae1ded0295b1
image: prom/prometheus@sha256:5c435642ca4d8427ca26f4901c11114023004709037880cd7860d5b7176aa731
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
@@ -88,7 +85,7 @@ services:
command: ['./run.sh', '-disable-reporting']
ports:
- 3000:3000
image: grafana/grafana:10.4.2-ubuntu@sha256:4f55071b556fb03f12b41423c98a185ed6695ed9ff2558e35805f0dd765fd958
image: grafana/grafana:11.0.0-ubuntu@sha256:02e99d1ee0b52dc9d3000c7b5314e7a07e0dfd69cc49bb3f8ce323491ed3406b
volumes:
- grafana-data:/var/lib/grafana

View File

@@ -12,7 +12,9 @@ services:
immich-server:
container_name: immich_server
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
command: ['start.sh', 'immich']
# extends:
# file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
@@ -25,23 +27,6 @@ services:
- database
restart: always
immich-microservices:
container_name: immich_microservices
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
# extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/hardware-transcoding
# file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
command: ['start.sh', 'microservices']
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
depends_on:
- redis
- database
restart: always
immich-machine-learning:
container_name: immich_machine_learning
# For hardware acceleration, add one of -[armnn, cuda, openvino] to the image tag.
@@ -58,18 +43,27 @@ services:
redis:
container_name: immich_redis
image: registry.hub.docker.com/library/redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
image: docker.io/redis:6.2-alpine@sha256:e31ca60b18f7e9b78b573d156702471d4eda038803c0b8e6f01559f350031e93
healthcheck:
test: redis-cli ping || exit 1
restart: always
database:
container_name: immich_postgres
image: registry.hub.docker.com/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
image: docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME}
POSTGRES_INITDB_ARGS: '--data-checksums'
volumes:
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
healthcheck:
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT SUM(checksum_failures) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
interval: 5m
start_interval: 30s
start_period: 5m
command: ["postgres", "-c" ,"shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"]
restart: always
volumes:

View File

@@ -1,9 +1,7 @@
version: "3.8"
# Configurations for hardware-accelerated machine learning
# If using Unraid or another platform that doesn't allow multiple Compose files,
# you can inline the config for a backend by copying its contents
# you can inline the config for a backend by copying its contents
# into the immich-machine-learning service in the docker-compose.yml file.
# See https://immich.app/docs/features/ml-hardware-acceleration for info on usage.
@@ -30,7 +28,7 @@ services:
openvino:
device_cgroup_rules:
- "c 189:* rmw"
- 'c 189:* rmw'
devices:
- /dev/dri:/dev/dri
volumes:

View File

@@ -1,5 +1,3 @@
version: "3.8"
# Configurations for hardware-accelerated transcoding
# If using Unraid or another platform that doesn't allow multiple Compose files,

View File

@@ -1 +1 @@
v20.12
20.13

View File

@@ -5,13 +5,13 @@ This website is built using [Docusaurus](https://docusaurus.io/), a modern stati
### Installation
```
$ yarn
$ npm install
```
### Local Development
```
$ yarn start
$ npm run start
```
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
@@ -19,7 +19,7 @@ This command starts a local development server and opens up a browser window. Mo
### Build
```
$ yarn build
$ npm run build
```
This command generates static content into the `build` directory and can be served using any static contents hosting service.
@@ -29,13 +29,13 @@ This command generates static content into the `build` directory and can be serv
Using SSH:
```
$ USE_SSH=true yarn deploy
$ USE_SSH=true npm run deploy
```
Not using SSH:
```
$ GIT_USER=<Your GitHub username> yarn deploy
$ GIT_USER=<Your GitHub username> npm run deploy
```
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.

View File

@@ -399,8 +399,47 @@ If it mentions SIGILL (note the lack of a K) or error code 132, it most likely m
If your version of Immich is below 1.92.0 and the crash occurs after logs about tracing or exporting a model, consider either upgrading or disabling the Tag Objects job.
### Why does Immich log migration errors on startup?
## Database
Sometimes Immich logs errors such as "duplicate key value violates unique constraint" or "column (...) of relation (...) already exists". Because of Immich's container structure, this error can be seen when both immich and immich-microservices start at the same time and attempt to migrate or create the database structure. Since the database migration is run sequentially and inside of transactions, this error message does not cause harm to your installation of Immich and can safely be ignored. If needed, you can manually restart Immich by running `docker restart immich immich-microservices`.
### Why am I getting database ownership errors?
If you get database errors such as `FATAL: data directory "/var/lib/postgresql/data" has wrong ownership` upon database startup, this is likely due to an issue with your filesystem.
NTFS and ex/FAT/32 filesystems are not supported. See [here](/docs/install/environment-variables#supported-filesystems) for more details.
### How can I verify the integrity of my database?
If you installed Immich using v1.104.0 or later, you likely have database checksums enabled by default. You can check this by running the following command.
A result of `on` means that checksums are enabled.
<details>
<summary>Check if checksums are enabled</summary>
```bash
docker exec -it immich_postgres psql --dbname=immich --username=<DB_USERNAME> --command="show data_checksums"
data_checksums
----------------
on
(1 row)
```
</details>
If checksums are enabled, you can check the status of the database with the following command. A normal result is all zeroes.
<details>
<summary>Check for database corruption</summary>
```bash
docker exec -it immich_postgres psql --dbname=immich --username=<DB_USERNAME> --command="SELECT datname, checksum_failures, checksum_last_failure FROM pg_stat_database WHERE datname IS NOT NULL"
datname | checksum_failures | checksum_last_failure
-----------+-------------------+-----------------------
postgres | 0 |
immich | 0 |
template1 | 0 |
template0 | 0 |
(4 rows)
```
</details>
[huggingface]: https://huggingface.co/immich-app

View File

@@ -17,6 +17,10 @@ Refer to the official [postgres documentation](https://www.postgresql.org/docs/c
The recommended way to backup and restore the Immich database is to use the `pg_dumpall` command. When restoring, you need to delete the `DB_DATA_LOCATION` folder (if it exists) to reset the database.
:::caution
It is not recommended to directly backup the `DB_DATA_LOCATION` folder. Doing so while the database is running can lead to a corrupted backup that cannot be restored.
:::
<Tabs>
<TabItem value="Linux system" label="Linux system" default>
@@ -51,7 +55,7 @@ docker compose pull # Update to latest version of Immich (if desired)
docker compose create # Create Docker containers for Immich apps without running them.
docker start immich_postgres # Start Postgres server
sleep 10 # Wait for Postgres server to start up
gc "C:\path\to\backup\dump.sql" | docker exec -i immich_postgres psql --username=postgres # Restore Backup
gc "C:\path\to\backup\dump.sql" | docker exec -i immich_postgres psql --username=postgres # Restore Backup
docker compose up -d # Start remainder of Immich apps
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

View File

@@ -110,8 +110,44 @@ Immich has a route (`/api/oauth/mobile-redirect`) that is already configured to
## Example Configuration
<details>
<summary>Authentik Example</summary>
### Authentik Example
Here's an example of OAuth configured for Authentik:
![OAuth Settings](./img/oauth-settings.png)
<img src={require('./img/oauth-settings.png').default} title="OAuth settings" />
</details>
<details>
<summary>Google Example</summary>
### Google Example
Configuration of Authorised redirect URIs (Google Console)
<img src={require('./img/google-example.webp').default} width='50%' title="Authorised redirect URIs" />
Configuration of OAuth in System Settings
| Setting | Value |
| ---------------------------- | ------------------------------------------------------------------------------------------------------ |
| Issuer URL | [https://accounts.google.com](https://accounts.google.com) |
| Client ID | 7\***\*\*\*\*\*\*\***\*\*\***\*\*\*\*\*\*\***vuls.apps.googleusercontent.com |
| Client Secret | G\***\*\*\*\*\*\*\***\*\*\***\*\*\*\*\*\*\***OO |
| Scope | openid email profile |
| Signing Algorithm | RS256 |
| Storage Label Claim | preferred_username |
| Storage Quota Claim | immich_quota |
| Default Storage Quota (GiB) | 0 (0 for unlimited quota) |
| Button Text | Sign in with Google (optional) |
| Auto Register | Enabled (optional) |
| Auto Launch | Enabled |
| Mobile Redirect URI Override | Enabled (required) |
| Mobile Redirect URI | [https://demo.immich.app/api/oauth/mobile-redirect](https://demo.immich.app/api/oauth/mobile-redirect) |
</details>
[oidc]: https://openid.net/connect/

View File

@@ -5,7 +5,7 @@ While not officially recommended, it is possible to run Immich using a pre-exist
By default, Immich expects superuser permission on the Postgres database and requires certain extensions to be installed. This guide outlines the steps required to prepare a pre-existing Postgres server to be used by Immich.
:::tip
Running with a pre-existing Postgres server can unlock powerful administrative features, including logical replication, data page checksums, and streaming write-ahead log backups using programs like pgBackRest or Barman.
Running with a pre-existing Postgres server can unlock powerful administrative features, including logical replication and streaming write-ahead log backups using programs like pgBackRest or Barman.
:::
## Prerequisites

View File

@@ -18,7 +18,7 @@ In any other situation, there are 3 different options that can appear:
- MATCHES - These files are matched by their checksums.
- OFFLINE PATHS - These files are the result of manually deleting files in the upload library or a failed file move in the past (losing track of a file).
- OFFLINE PATHS - These files are the result of manually deleting files from immich or a failed file move in the past (losing track of a file).
- UNTRACKED FILES - These files are not tracked by the application. They can be the result of failed moves, interrupted uploads, or left behind due to a bug.

View File

@@ -9,6 +9,11 @@ When contributing code through a pull request, please check the following:
- [ ] `npm run check:svelte` (Type checking via SvelteKit)
- [ ] `npm test` (unit tests)
## Documentation
- [ ] `npm run format` (formatting via Prettier)
- [ ] Update the `_redirects` file if you have renamed a page or removed it from the documentation.
:::tip AIO
Run all web checks with `npm run check:all`
:::

View File

@@ -22,7 +22,8 @@ You do not need to redo any transcoding jobs after enabling hardware acceleratio
- WSL2 does not support Quick Sync.
- Raspberry Pi is currently not supported.
- Two-pass mode is only supported for NVENC. Other APIs will ignore this setting.
- Only encoding is currently hardware accelerated, so the CPU is still used for software decoding and tone-mapping.
- By default, only encoding is currently hardware accelerated. This means the CPU is still used for software decoding and tone-mapping.
- NVENC and RKMPP can be fully accelerated by enabling hardware decoding in the video transcoding settings.
- Hardware dependent
- Codec support varies, but H.264 and HEVC are usually supported.
- Notably, NVIDIA and AMD GPUs do not support VP9 encoding.
@@ -33,7 +34,7 @@ You do not need to redo any transcoding jobs after enabling hardware acceleratio
#### NVENC
- You must have the official NVIDIA driver installed on the server.
- On Linux (except for WSL2), you also need to have [NVIDIA Container Runtime][nvcr] installed.
- On Linux (except for WSL2), you also need to have [NVIDIA Container Toolkit][nvct] installed.
#### QSV
@@ -65,6 +66,7 @@ For RKMPP to work:
3. Redeploy the `immich-microservices` container with these updated settings.
4. In the Admin page under `Video transcoding settings`, change the hardware acceleration setting to the appropriate option and save.
5. (Optional) If using a compatible backend, you may enable hardware decoding for optimal performance.
#### Single Compose File
@@ -122,7 +124,7 @@ Once this is done, you can continue to step 3 of "Basic Setup".
- While you can use VAAPI with NVIDIA and Intel devices, prefer the more specific APIs since they're more optimized for their respective devices
[hw-file]: https://github.com/immich-app/immich/releases/latest/download/hwaccel.transcoding.yml
[nvcr]: https://github.com/NVIDIA/nvidia-container-runtime/
[nvct]: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html
[jellyfin-lp]: https://jellyfin.org/docs/general/administration/hardware-acceleration/intel/#configure-and-verify-lp-mode-on-linux
[jellyfin-kernel-bug]: https://jellyfin.org/docs/general/administration/hardware-acceleration/intel/#known-issues-and-limitations
[libmali-rockchip]: https://github.com/tsukumijima/libmali-rockchip/releases

View File

@@ -4,13 +4,9 @@
Immich supports the creation of libraries which is a top-level asset container. Currently, there are two types of libraries: traditional upload libraries that can sync with a mobile device, and external libraries, that keeps up to date with files on disk. Libraries are different from albums in that an asset can belong to multiple albums but only one library, and deleting a library deletes all assets contained within. As of August 2023, this is a new feature and libraries have a lot of potential for future development beyond what is documented here. This document attempts to describe the current state of libraries.
## The Upload Library
Immich comes preconfigured with an upload library for each user. All assets uploaded to Immich are added to this library. This library can be renamed, but not deleted. The upload library is the only library that can be synced with a mobile device. No items in an upload library is allowed to have the same sha1 hash as another item in the same library in order to prevent duplicates.
## External Libraries
External libraries tracks assets stored outside of immich, i.e. in the file system. Immich will only read data from the files, and will not modify them in any way. Therefore, the delete button is disabled for external assets. When the external library is scanned, immich will read the metadata from the file and create an asset in the library for each image or video file. These items will then be shown in the main timeline, and they will look and behave like any other asset, including viewing on the map, adding to albums, etc.
External libraries tracks assets stored outside of Immich, i.e. in the file system. When the external library is scanned, Immich will read the metadata from the file and create an asset in the library for each image or video file. These items will then be shown in the main timeline, and they will look and behave like any other asset, including viewing on the map, adding to albums, etc.
If a file is modified outside of Immich, the changes will not be reflected in immich until the library is scanned again. There are different ways to scan a library depending on the use case:

View File

@@ -38,7 +38,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele
- The GPU must have compute capability 5.2 or greater.
- The server must have the official NVIDIA driver installed.
- The installed driver must be >= 535 (it must support CUDA 12.2).
- On Linux (except for WSL2), you also need to have [NVIDIA Container Runtime][nvcr] installed.
- On Linux (except for WSL2), you also need to have [NVIDIA Container Toolkit][nvct] installed.
#### OpenVINO
@@ -95,11 +95,11 @@ immich-machine-learning:
Once this is done, you can redeploy the `immich-machine-learning` container.
:::info
You can confirm the device is being recognized and used by checking its utilization (via `nvtop` for CUDA, `intel_gpu_top` for OpenVINO, etc.). You can also enable debug logging by setting `LOG_LEVEL=debug` in the `.env` file and restarting the `immich-machine-learning` container. When a Smart Search or Face Detection job begins, you should see a log for `Available ORT providers` containing the relevant provider. In the case of ARM NN, the absence of a `Could not load ANN shared libraries` log entry means it loaded successfully.
You can confirm the device is being recognized and used by checking its utilization (via `nvtop` for CUDA, `intel_gpu_top` for OpenVINO, etc.). You can also enable debug logging by setting `IMMICH_LOG_LEVEL=debug` in the `.env` file and restarting the `immich-machine-learning` container. When a Smart Search or Face Detection job begins, you should see a log for `Available ORT providers` containing the relevant provider. In the case of ARM NN, the absence of a `Could not load ANN shared libraries` log entry means it loaded successfully.
:::
[hw-file]: https://github.com/immich-app/immich/releases/latest/download/hwaccel.ml.yml
[nvcr]: https://github.com/NVIDIA/nvidia-container-runtime/
[nvct]: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html
## Tips

View File

@@ -0,0 +1,16 @@
# Create Custom Map Styles for Immich Using Maptiler
You may decide that you'd like to modify the style document which is used to draw the maps in Immich. This can be done easily using Maptiler, if you do not want to write an entire JSON document by hand.
## Steps
1. Create a free account at https://cloud.maptiler.com
2. Once logged in, you can either create a brand new map by clicking on **New Map**, selecting a starter map, and then clicking **Customize**, OR by selecting a **Standard Map** and customizing it from there.
3. The **editor** interface is self-explanatory. You can change colors, remove visible layers, or add optional layers (e.g., administrative, topo, hydro, etc.) in the composer.
4. Once you have your map composed, click on **Save** at the top right. Give it a unique name to save it to your account.
5. Next, **Publish** your style using the **Publish** button at the top right. This will deploy it to production, which means it is able to be exposed over the Internet. Maptiler will present an interactive side-by-side map with the original and your changes prior to publication.<br/>![Maptiler Publication Settings](img/immich_map_styles_publish.png)
6. Maptiler will warn you that changing the map will change it across all apps using the map. Since no apps are using the map yet, this is okay.
7. Clicking on the name of your new map at the top left will bring you to the item's **details** page. From here, copy the link to the JSON style under **Use vector style**. This link will automatically contain your personal API key to Maptiler.
8. In **Immich**, navigate to **Administration --> Settings --> Map & GPS Settings** and expand the **Map Settings** subsection.
9. Paste the link to your JSON style in either the **Light Style** or **Dark Style**. (You can add different styles which will help make the map style more appropriate depending on whether you set **Immich** to Light or Dark mode.
10. Save your selections. Reload the map, and enjoy your custom map style!

View File

@@ -2,48 +2,52 @@
A short guide on connecting [pgAdmin](https://www.pgadmin.org/) to Immich.
:::note
In order to connect to the database the immich_postgres container **must be running**.
The passwords and usernames used below match the ones specified in the example `.env` file. If changed, please use actual values instead.
**Optional:** To connect to the database **outside** of your Docker's network:
- Expose port 5432 in your `docker-compose.yml` file.
- Edit the PostgreSQL [`pg_hba.conf`](https://www.postgresql.org/docs/current/auth-pg-hba-conf.html) file.
- Make sure your firewall does not block access to port 5432.
Note that exposing the database port increases the risk of getting attacked by hackers.
Make sure to remove the binding port after finishing the database's tasks.
:::
## 1. Install pgAdmin
Download and install [pgAdmin](https://www.pgadmin.org/download/) following the official documentation.
Add a file `docker-compose-pgadmin.yml` next to your `docker-compose.yml` with the following content:
```
name: immich
services:
pgadmin:
image: dpage/pgadmin4
container_name: pgadmin4_container
restart: always
ports:
- "8888:80"
environment:
PGADMIN_DEFAULT_EMAIL: user-name@domain-name.com
PGADMIN_DEFAULT_PASSWORD: strong-password
volumes:
- pgadmin-data:/var/lib/pgadmin
volumes:
pgadmin-data:
```
Change the values of `PGADMIN_DEFAULT_EMAIL` and `PGADMIN_DEFAULT_PASSWORD` in this file.
Run `docker compose -f docker-compose.yml -f docker-compose-pgadmin.yml up` to start immich along with `pgAdmin`.
## 2. Add a Server
Open pgAdmin and click "Add New Server".
Open [localhost:8888](http://localhost:8888) and login with the default credentials from above.
<img src={require('./img/add-new-server-option.png').default} width="50%" title="new server option" />
Right click on `Servers` and click on `Register >> Server..` then enter the values below in the `Connection` tab.
## 3. Enter Connection Details
<img src={require('./img/pgadmin-add-new-server.png').default} width="50%" title="new server option" />
| Name | Value |
| -------------------- | ----------- |
| Host name/address | `localhost` |
| Port | `5432` |
| Maintenance database | `immich` |
| Username | `postgres` |
| Password | `postgres` |
:::note
The parameters used here match those specified in the example `.env` file. If you have changed your `.env` file, you'll need to adjust accordingly.
:::
<img src={require('./img/Connection-Pgadmin.png').default} width="75%" title="Connection" />
## 4. Save Connection
| Name | Value |
| -------------------- | ----------------- |
| Host name/address | `immich_postgres` |
| Port | `5432` |
| Maintenance database | `immich` |
| Username | `postgres` |
| Password | `postgres` |
Click on "Save" to connect to the Immich database.
:::tip
View [Database Queries](/docs/guides/database-queries/) for common database queries.
:::

View File

@@ -96,7 +96,7 @@ SELECT * FROM "users";
## System Config
```sql title="Custom settings"
SELECT "key", "value" FROM "system_config";
SELECT "key", "value" FROM "system_metadata" WHERE "key" = 'system-config';
```
(Only used when not using the [config file](/docs/install/config-file))

View File

@@ -6,15 +6,19 @@ in a directory on the same machine.
# Mount the directory into the containers.
Edit `docker-compose.yml` to add two new mount points under `volumes:`
Edit `docker-compose.yml` to add two new mount points in the sections `immich-server:` and `immich-microservices:` under `volumes:`
```
immich-server:
```diff
immich-server:
volumes:
- ${EXTERNAL_PATH}:/usr/src/app/external
+ - ${EXTERNAL_PATH}:/usr/src/app/external
immich-microservices:
volumes:
+ - ${EXTERNAL_PATH}:/usr/src/app/external
```
Be sure to add exactly the same line to both `immich-server:` and `immich-microservices:`.
Be sure to add exactly the same path to both services.
Edit `.env` to define `EXTERNAL_PATH`, substituting in the correct path for your computer:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -15,7 +15,7 @@ The [hwaccel.ml.yml](https://github.com/immich-app/immich/releases/latest/downlo
:::
```yaml
version: '3.8'
name: immich_remote_ml
services:
immich-machine-learning:

View File

@@ -77,6 +77,10 @@ The default configuration looks like this:
"enabled": true,
"modelName": "ViT-B-32__openai"
},
"duplicateDetection": {
"enabled": false,
"maxDistance": 0.03
},
"facialRecognition": {
"enabled": true,
"modelName": "buffalo_l",
@@ -153,9 +157,6 @@ The default configuration looks like this:
"server": {
"externalDomain": "",
"loginPageMessage": ""
},
"user": {
"deleteDelay": 7
}
}
```

View File

@@ -24,18 +24,25 @@ If this should not work, try running `docker compose up -d --force-recreate`.
| `DB_DATA_LOCATION` | Host Path for Postgres database | | database |
:::tip
These environment variables are used by the `docker-compose.yml` file and do **NOT** affect the containers directly.
:::
### Supported filesystems
The Immich Postgres database (`DB_DATA_LOCATION`) must be located on a filesystem that supports user/group
ownership and permissions (EXT2/3/4, ZFS, APFS, BTRFS, XFS, etc.). It will not work on any filesystem formatted in NTFS or ex/FAT/32.
It will not work in WSL (Windows Subsystem for Linux) when using a mounted host directory (commonly under `/mnt`).
If this is an issue, you can change the bind mount to a Docker volume instead.
Regardless of filesystem, it is not recommended to use a network share for your database location due to performance and possible data loss issues.
## General
| Variable | Description | Default | Services |
| :------------------------------ | :------------------------------------------- | :----------------------: | :-------------------------------------- |
| `TZ` | Timezone | | microservices |
| `NODE_ENV` | Environment (production, development) | `production` | server, microservices, machine learning |
| `LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, microservices, machine learning |
| `IMMICH_ENV` | Environment (production, development) | `production` | server, microservices, machine learning |
| `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, microservices, machine learning |
| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload`<sup>\*1</sup> | server, microservices |
| `IMMICH_CONFIG_FILE` | Path to config file | | server, microservices |
| `IMMICH_WEB_ROOT` | Path of root index.html | `/usr/src/app/www` | server |
@@ -52,13 +59,10 @@ It only need to be set if the Immich deployment method is changing.
## Ports
| Variable | Description | Default | Services |
| :---------------------- | :-------------------- | :-------: | :-------------------- |
| `HOST` | Host | `0.0.0.0` | server, microservices |
| `SERVER_PORT` | Server Port | `3001` | server |
| `MICROSERVICES_PORT` | Microservices Port | `3002` | microservices |
| `MACHINE_LEARNING_HOST` | Machine Learning Host | `0.0.0.0` | machine learning |
| `MACHINE_LEARNING_PORT` | Machine Learning Port | `3003` | machine learning |
| Variable | Description | Default |
| :------------ | :------------- | :------------------------------------: |
| `IMMICH_HOST` | Listening host | `0.0.0.0` |
| `IMMICH_PORT` | Listening port | 3001 (server), 3003 (machine learning) |
## Database

View File

@@ -8,7 +8,7 @@ Install Immich using Portainer's Stack feature.
1. Go to "**Stacks**" in the left sidebar.
2. Click on "**Add stack**".
3. Give the stack a name (i.e. Immich), and select "**Web Editor**" as the build method.
3. Give the stack a name (i.e. immich), and select "**Web Editor**" as the build method.
4. Copy the content of the `docker-compose.yml` file from the [GitHub repository](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml).
5. Replace `.env` with `stack.env` for all containers that need to use environment variables in the web editor.
@@ -38,8 +38,9 @@ style={{border: '1px solid #ddd'}}
alt="Dot Env Example"
/>
- Populate custom database information if necessary.
- Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets.
- Change the default `DB_PASSWORD`, and add custom database connection information if necessary.
- Change `DB_DATA_LOCATION` to a folder where the database will be saved to disk.
- Change `UPLOAD_LOCATION` to a folder where media (uploaded and generated) will be stored.
11. Click on "**Deploy the stack**".

View File

@@ -15,12 +15,15 @@ Hardware and software requirements for Immich
Immich requires the command `docker compose` - the similarly named `docker-compose` is [deprecated](https://docs.docker.com/compose/migrate/) and is no longer compatible with Immich.
:::
:::info Podman
You can also use Podman to run the application. However, additional configuration might be required.
:::
## Hardware
- **OS**: Preferred unix-based operating system (Ubuntu, Debian, MacOS, etc). Windows works too, with [Docker Desktop on Windows](https://docs.docker.com/desktop/install/windows-install/)
- **RAM**: At least 4GB, preferred 6GB.
- **CPU**: At least 2 cores, preferred 4 cores.
- **OS**: Recommended Linux operating system (Ubuntu, Debian, etc).
- Windows is supported with [Docker Desktop on Windows](https://docs.docker.com/desktop/install/windows-install/) or [WSL 2](https://docs.docker.com/desktop/wsl/).
- macOS is supported with [Docker Desktop on Mac](https://docs.docker.com/desktop/install/mac-install/).
- **RAM**: Minimum 4GB, recommended 6GB.
- **CPU**: Minimum 2 cores, recommended 4 cores.
- **Storage**: Recommended Unix-compatible filesystem (EXT4, ZFS, APFS, etc.) with support for user/group ownership and permissions.
- This can present an issue for Windows users. See [here](/docs/install/environment-variables#supported-filesystems)
for more details and alternatives.
- The generation of thumbnails and transcoded video can increase the size of the photo library by 10-20% on average.
- Network shares are supported for the storage of image and video assets only.

View File

@@ -77,13 +77,9 @@ const config = {
},
announcementBar: {
id: 'site_announcement_immich',
content:
'Immich joins Futo! Read about the announcement <a href="/blog/2024/immich-core-team-goes-fulltime">here</a>.',
backgroundColor: '#4250af',
textColor: 'white',
// content: `⚠️ The project is under <strong>very active</strong> development. Expect bugs and changes. Do not use it as <strong>the only way</strong> to store your photos and videos!`,
// backgroundColor: '#593f00',
// textColor: '#ffefc9',
content: `⚠️ The project is under <strong>very active</strong> development. Expect bugs and changes. Do not use it as <strong>the only way</strong> to store your photos and videos!`,
backgroundColor: '#593f00',
textColor: '#ffefc9',
isCloseable: false,
},
docs: {
@@ -123,6 +119,11 @@ const config = {
label: 'GitHub',
position: 'right',
},
{
href: 'https://discord.gg/D8JsnBEuKb',
label: 'Discord',
position: 'right',
},
],
},
footer: {

1275
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -56,6 +56,6 @@
"node": ">=20"
},
"volta": {
"node": "20.12.2"
"node": "20.13.1"
}
}

View File

@@ -10,17 +10,17 @@ interface CommunityGuidesProps {
const guides: CommunityGuidesProps[] = [
{
title: 'Cloudflare Tunnels with SSO/OAuth',
description: `Setting up Cloudflare Tunnels and a SaaS App for immich.`,
description: `Setting up Cloudflare Tunnels and a SaaS App for Immich.`,
url: 'https://github.com/immich-app/immich/discussions/8299',
},
{
title: 'Database backup in Truenas',
description: `Create a database backup with pgAdmin in Truenas.`,
title: 'Database backup in TrueNAS',
description: `Create a database backup with pgAdmin in TrueNAS.`,
url: 'https://github.com/immich-app/immich/discussions/8809',
},
{
title: 'Unraid backup scripts',
description: `Back up your assets in Unarid with a pre-prepared script.`,
description: `Back up your assets in Unraid with a pre-prepared script.`,
url: 'https://github.com/immich-app/immich/discussions/8416',
},
{
@@ -33,6 +33,11 @@ const guides: CommunityGuidesProps[] = [
description: 'Documentation for simple podman setup using quadlets.',
url: 'https://github.com/tbelway/immich-podman-quadlets/blob/main/docs/install/podman-quadlet.md',
},
{
title: 'Google Photos import + albums',
description: 'Import your Google Photos files into Immich and add your albums',
url: 'https://github.com/immich-app/immich/discussions/1340',
},
];
function CommunityGuide({ title, description, url }: CommunityGuidesProps): JSX.Element {

View File

@@ -10,7 +10,7 @@ interface CommunityProjectProps {
const projects: CommunityProjectProps[] = [
{
title: 'immich-go',
description: `An alternative to the immich-CLI command that doesn't depend on nodejs installation. It tries its best for importing google photos takeout archives.`,
description: `An alternative to the immich-CLI that doesn't depend on nodejs. It specializes in importing Google Photos Takeout archives.`,
url: 'https://github.com/simulot/immich-go',
},
{
@@ -33,6 +33,11 @@ const projects: CommunityProjectProps[] = [
description: 'A Python script to create albums based on the folder structure of an external library.',
url: 'https://github.com/Salvoxia/immich-folder-album-creator',
},
{
title: 'Immich-Tools',
description: 'Provides scripts for handling problems on the repair page.',
url: 'https://github.com/clumsyCoder00/Immich-Tools',
},
{
title: 'Lightroom Publisher: mi.Immich.Publisher',
description: 'Lightroom plugin to publish photos from Lightroom collections to Immich albums.',

View File

@@ -33,6 +33,13 @@ function HomepageHeader() {
>
Demo portal
</Link>
<Link
className="flex place-items-center place-content-center py-3 px-8 border bg-immich-dark-primary dark:bg-immich-primary rounded-full hover:no-underline text-immich-primary dark:text-immich-dark-bg font-bold uppercase"
to="https://discord.gg/D8JsnBEuKb"
>
Discord
</Link>
</div>
<img src="/img/immich-screenshots.png" alt="screenshots" width={'70%'} />
<div className="flex flex-col sm:flex-row place-items-center place-content-center mt-4 gap-1">

BIN
docs/static/img/ios-app-store-badge.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1 +1 @@
v20.12
20.13

View File

@@ -2,40 +2,32 @@ version: '3.8'
name: immich-e2e
x-server-build: &server-common
image: immich-server:latest
build:
context: ../
dockerfile: server/Dockerfile
environment:
- DB_HOSTNAME=database
- DB_USERNAME=postgres
- DB_PASSWORD=postgres
- DB_DATABASE_NAME=immich
- IMMICH_MACHINE_LEARNING_ENABLED=false
- IMMICH_METRICS=true
volumes:
- upload:/usr/src/app/upload
- ../server/test/assets:/data/assets
depends_on:
- redis
- database
services:
immich-server:
container_name: immich-e2e-server
command: ['./start.sh', 'immich']
<<: *server-common
command: ['./start.sh']
image: immich-server:latest
build:
context: ../
dockerfile: server/Dockerfile
environment:
- DB_HOSTNAME=database
- DB_USERNAME=postgres
- DB_PASSWORD=postgres
- DB_DATABASE_NAME=immich
- IMMICH_MACHINE_LEARNING_ENABLED=false
- IMMICH_METRICS=true
volumes:
- upload:/usr/src/app/upload
- ./test-assets:/test-assets
depends_on:
- redis
- database
ports:
- 2283:3001
immich-microservices:
container_name: immich-e2e-microservices
command: ['./start.sh', 'microservices']
<<: *server-common
redis:
image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
image: redis:6.2-alpine@sha256:e31ca60b18f7e9b78b573d156702471d4eda038803c0b8e6f01559f350031e93
database:
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0

405
e2e/package-lock.json generated
View File

@@ -1,17 +1,17 @@
{
"name": "immich-e2e",
"version": "1.103.1",
"version": "1.105.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-e2e",
"version": "1.103.1",
"version": "1.105.1",
"license": "GNU Affero General Public License version 3",
"devDependencies": {
"@immich/cli": "file:../cli",
"@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.41.2",
"@playwright/test": "^1.44.1",
"@types/luxon": "^3.4.2",
"@types/node": "^20.11.17",
"@types/pg": "^8.11.0",
@@ -23,8 +23,8 @@
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^52.0.0",
"exiftool-vendored": "^24.5.0",
"eslint-plugin-unicorn": "^53.0.0",
"exiftool-vendored": "^26.0.0",
"luxon": "^3.4.4",
"pg": "^8.11.3",
"pngjs": "^7.0.0",
@@ -65,7 +65,7 @@
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^52.0.0",
"eslint-plugin-unicorn": "^53.0.0",
"mock-fs": "^5.2.0",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
@@ -81,11 +81,11 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.103.1",
"version": "1.105.1",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.3"
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^20.11.0",
@@ -208,9 +208,9 @@
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"version": "7.24.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz",
"integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==",
"dev": true,
"engines": {
"node": ">=6.9.0"
@@ -953,9 +953,9 @@
}
},
"node_modules/@photostructure/tz-lookup": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-9.0.2.tgz",
"integrity": "sha512-H8+tTt7ilJNkFyb+QgPnLEGUjQzGwiMb9n7lwRZNBgSKL3VZs9AkjI1E//FcwPjNafwAH932U92+xTqJiF3Bbw==",
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-10.0.0.tgz",
"integrity": "sha512-8ZAjoj/irCuvUlyEinQ/HB6A8hP3bD1dgTOZvfl1b9nAwqniutFDHOQRcGM6Crea68bOwPj010f0Z4KkmuLHEA==",
"dev": true
},
"node_modules/@pkgr/core": {
@@ -971,12 +971,12 @@
}
},
"node_modules/@playwright/test": {
"version": "1.43.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.43.1.tgz",
"integrity": "sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==",
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz",
"integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==",
"dev": true,
"dependencies": {
"playwright": "1.43.1"
"playwright": "1.44.1"
},
"bin": {
"playwright": "cli.js"
@@ -1217,12 +1217,6 @@
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true
},
"node_modules/@types/luxon": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz",
@@ -1236,10 +1230,11 @@
"dev": true
},
"node_modules/@types/node": {
"version": "20.12.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz",
"integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==",
"version": "20.12.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
"integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
}
@@ -1251,9 +1246,9 @@
"dev": true
},
"node_modules/@types/pg": {
"version": "8.11.5",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.5.tgz",
"integrity": "sha512-2xMjVviMxneZHDHX5p5S6tsRRs7TpDHeeK7kTTMe/kAC/mRRNjWHjZg0rkiY+e17jXSZV3zJYDxXV8Cy72/Vuw==",
"version": "8.11.6",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.6.tgz",
"integrity": "sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==",
"dev": true,
"dependencies": {
"@types/node": "*",
@@ -1319,20 +1314,14 @@
}
},
"node_modules/@types/pngjs": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.4.tgz",
"integrity": "sha512-atAK9xLKOnxiuArxcHovmnOUUGBZOQ3f0vCf43FnoKs6XnqiambT1kkJWmdo71IR+BoXSh+CueeFR0GfH3dTlQ==",
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.5.tgz",
"integrity": "sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/semver": {
"version": "7.5.8",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
"dev": true
},
"node_modules/@types/superagent": {
"version": "8.1.3",
"resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.3.tgz",
@@ -1355,21 +1344,20 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.7.1.tgz",
"integrity": "sha512-KwfdWXJBOviaBVhxO3p5TJiLpNuh2iyXyjmWN0f1nU87pwyvfS0EmjC6ukQVYVFJd/K1+0NWGPDXiyEyQorn0Q==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.9.0.tgz",
"integrity": "sha512-6e+X0X3sFe/G/54aC3jt0txuMTURqLyekmEHViqyA2VnxhLMpvA6nqmcjIy+Cr9tLDHPssA74BP5Mx9HQIxBEA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.7.1",
"@typescript-eslint/type-utils": "7.7.1",
"@typescript-eslint/utils": "7.7.1",
"@typescript-eslint/visitor-keys": "7.7.1",
"debug": "^4.3.4",
"@typescript-eslint/scope-manager": "7.9.0",
"@typescript-eslint/type-utils": "7.9.0",
"@typescript-eslint/utils": "7.9.0",
"@typescript-eslint/visitor-keys": "7.9.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
"semver": "^7.6.0",
"ts-api-utils": "^1.3.0"
},
"engines": {
@@ -1390,15 +1378,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.7.1.tgz",
"integrity": "sha512-vmPzBOOtz48F6JAGVS/kZYk4EkXao6iGrD838sp1w3NQQC0W8ry/q641KU4PrG7AKNAf56NOcR8GOpH8l9FPCw==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.9.0.tgz",
"integrity": "sha512-qHMJfkL5qvgQB2aLvhUSXxbK7OLnDkwPzFalg458pxQgfxKDfT1ZDbHQM/I6mDIf/svlMkj21kzKuQ2ixJlatQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/scope-manager": "7.7.1",
"@typescript-eslint/types": "7.7.1",
"@typescript-eslint/typescript-estree": "7.7.1",
"@typescript-eslint/visitor-keys": "7.7.1",
"@typescript-eslint/scope-manager": "7.9.0",
"@typescript-eslint/types": "7.9.0",
"@typescript-eslint/typescript-estree": "7.9.0",
"@typescript-eslint/visitor-keys": "7.9.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1418,13 +1407,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.7.1.tgz",
"integrity": "sha512-PytBif2SF+9SpEUKynYn5g1RHFddJUcyynGpztX3l/ik7KmZEv19WCMhUBkHXPU9es/VWGD3/zg3wg90+Dh2rA==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.9.0.tgz",
"integrity": "sha512-ZwPK4DeCDxr3GJltRz5iZejPFAAr4Wk3+2WIBaj1L5PYK5RgxExu/Y68FFVclN0y6GGwH8q+KgKRCvaTmFBbgQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.7.1",
"@typescript-eslint/visitor-keys": "7.7.1"
"@typescript-eslint/types": "7.9.0",
"@typescript-eslint/visitor-keys": "7.9.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1435,13 +1425,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.7.1.tgz",
"integrity": "sha512-ZksJLW3WF7o75zaBPScdW1Gbkwhd/lyeXGf1kQCxJaOeITscoSl0MjynVvCzuV5boUz/3fOI06Lz8La55mu29Q==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.9.0.tgz",
"integrity": "sha512-6Qy8dfut0PFrFRAZsGzuLoM4hre4gjzWJB6sUvdunCYZsYemTkzZNwF1rnGea326PHPT3zn5Lmg32M/xfJfByA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "7.7.1",
"@typescript-eslint/utils": "7.7.1",
"@typescript-eslint/typescript-estree": "7.9.0",
"@typescript-eslint/utils": "7.9.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@@ -1462,10 +1453,11 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.7.1.tgz",
"integrity": "sha512-AmPmnGW1ZLTpWa+/2omPrPfR7BcbUU4oha5VIbSbS1a1Tv966bklvLNXxp3mrbc+P2j4MNOTfDffNsk4o0c6/w==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.9.0.tgz",
"integrity": "sha512-oZQD9HEWQanl9UfsbGVcZ2cGaR0YT5476xfWE0oE5kQa2sNK2frxOlkeacLOTh9po4AlUT5rtkGyYM5kew0z5w==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || >=20.0.0"
},
@@ -1475,13 +1467,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.7.1.tgz",
"integrity": "sha512-CXe0JHCXru8Fa36dteXqmH2YxngKJjkQLjxzoj6LYwzZ7qZvgsLSc+eqItCrqIop8Vl2UKoAi0StVWu97FQZIQ==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.9.0.tgz",
"integrity": "sha512-zBCMCkrb2YjpKV3LA0ZJubtKCDxLttxfdGmwZvTqqWevUPN0FZvSI26FalGFFUZU/9YQK/A4xcQF9o/VVaCKAg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "7.7.1",
"@typescript-eslint/visitor-keys": "7.7.1",
"@typescript-eslint/types": "7.9.0",
"@typescript-eslint/visitor-keys": "7.9.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -1507,6 +1500,7 @@
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
@@ -1516,6 +1510,7 @@
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
@@ -1527,18 +1522,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.7.1.tgz",
"integrity": "sha512-QUvBxPEaBXf41ZBbaidKICgVL8Hin0p6prQDu6bbetWo39BKbWJxRsErOzMNT1rXvTll+J7ChrbmMCXM9rsvOQ==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.9.0.tgz",
"integrity": "sha512-5KVRQCzZajmT4Ep+NEgjXCvjuypVvYHUW7RHlXzNPuak2oWpVoD1jf5xCP0dPAuNIchjC7uQyvbdaSTFaLqSdA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.15",
"@types/semver": "^7.5.8",
"@typescript-eslint/scope-manager": "7.7.1",
"@typescript-eslint/types": "7.7.1",
"@typescript-eslint/typescript-estree": "7.7.1",
"semver": "^7.6.0"
"@typescript-eslint/scope-manager": "7.9.0",
"@typescript-eslint/types": "7.9.0",
"@typescript-eslint/typescript-estree": "7.9.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1552,12 +1545,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.7.1.tgz",
"integrity": "sha512-gBL3Eq25uADw1LQ9kVpf3hRM+DWzs0uZknHYK3hq4jcTPqVCClHGDnB6UUUV2SFeBeA4KWHWbbLqmbGcZ4FYbw==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.9.0.tgz",
"integrity": "sha512-iESPx2TNLDNGQLyjKhUvIKprlP49XNEK+MvIf9nIO7ZZaZdbnfWKHnXAgufpxqfA0YryH8XToi4+CjBgVnFTSQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.7.1",
"@typescript-eslint/types": "7.9.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@@ -1575,9 +1569,9 @@
"dev": true
},
"node_modules/@vitest/coverage-v8": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.5.2.tgz",
"integrity": "sha512-QJqxRnbCwNtbbegK9E93rBmhN3dbfG1bC/o52Bqr0zGCYhQzwgwvrJBG7Q8vw3zilX6Ryy6oa/mkZku2lLJx1Q==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.0.tgz",
"integrity": "sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==",
"dev": true,
"dependencies": {
"@ampproject/remapping": "^2.2.1",
@@ -1598,17 +1592,17 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"vitest": "1.5.2"
"vitest": "1.6.0"
}
},
"node_modules/@vitest/expect": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.5.2.tgz",
"integrity": "sha512-rf7MTD1WCoDlN3FfYJ9Llfp0PbdtOMZ3FIF0AVkDnKbp3oiMW1c8AmvRZBcqbAhDUAvF52e9zx4WQM1r3oraVA==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz",
"integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==",
"dev": true,
"dependencies": {
"@vitest/spy": "1.5.2",
"@vitest/utils": "1.5.2",
"@vitest/spy": "1.6.0",
"@vitest/utils": "1.6.0",
"chai": "^4.3.10"
},
"funding": {
@@ -1616,12 +1610,12 @@
}
},
"node_modules/@vitest/runner": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.5.2.tgz",
"integrity": "sha512-7IJ7sJhMZrqx7HIEpv3WrMYcq8ZNz9L6alo81Y6f8hV5mIE6yVZsFoivLZmr0D777klm1ReqonE9LyChdcmw6g==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz",
"integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==",
"dev": true,
"dependencies": {
"@vitest/utils": "1.5.2",
"@vitest/utils": "1.6.0",
"p-limit": "^5.0.0",
"pathe": "^1.1.1"
},
@@ -1630,9 +1624,9 @@
}
},
"node_modules/@vitest/snapshot": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.5.2.tgz",
"integrity": "sha512-CTEp/lTYos8fuCc9+Z55Ga5NVPKUgExritjF5VY7heRFUfheoAqBneUlvXSUJHUZPjnPmyZA96yLRJDP1QATFQ==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz",
"integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==",
"dev": true,
"dependencies": {
"magic-string": "^0.30.5",
@@ -1644,9 +1638,9 @@
}
},
"node_modules/@vitest/spy": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.5.2.tgz",
"integrity": "sha512-xCcPvI8JpCtgikT9nLpHPL1/81AYqZy1GCy4+MCHBE7xi8jgsYkULpW5hrx5PGLgOQjUpb6fd15lqcriJ40tfQ==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz",
"integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==",
"dev": true,
"dependencies": {
"tinyspy": "^2.2.0"
@@ -1656,9 +1650,9 @@
}
},
"node_modules/@vitest/utils": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.5.2.tgz",
"integrity": "sha512-sWOmyofuXLJ85VvXNsroZur7mOJGiQeM0JN3/0D1uU8U9bGFM69X1iqHaRXl6R8BwaLY6yPCogP257zxTzkUdA==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz",
"integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==",
"dev": true,
"dependencies": {
"diff-sequences": "^29.6.3",
@@ -1785,6 +1779,7 @@
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
@@ -1840,6 +1835,7 @@
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.0.1"
},
@@ -2121,12 +2117,12 @@
"dev": true
},
"node_modules/core-js-compat": {
"version": "3.36.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.0.tgz",
"integrity": "sha512-iV9Pd/PsgjNWBXeq8XRtWVSgz2tKAfhfvBs7qxYty+RlRd+OCksaWmOnc4JKrTc1cToXL1N0s3l/vwlxPtdElw==",
"version": "3.37.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz",
"integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==",
"dev": true,
"dependencies": {
"browserslist": "^4.22.3"
"browserslist": "^4.23.0"
},
"funding": {
"type": "opencollective",
@@ -2247,6 +2243,7 @@
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-type": "^4.0.0"
},
@@ -2487,17 +2484,17 @@
}
},
"node_modules/eslint-plugin-unicorn": {
"version": "52.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-52.0.0.tgz",
"integrity": "sha512-1Yzm7/m+0R4djH0tjDjfVei/ju2w3AzUGjG6q8JnuNIL5xIwsflyCooW5sfBvQp2pMYQFSWWCFONsjCax1EHng==",
"version": "53.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-53.0.0.tgz",
"integrity": "sha512-kuTcNo9IwwUCfyHGwQFOK/HjJAYzbODHN3wP0PgqbW+jbXqpNWxNVpVhj2tO9SixBwuAdmal8rVcWKBxwFnGuw==",
"dev": true,
"dependencies": {
"@babel/helper-validator-identifier": "^7.22.20",
"@babel/helper-validator-identifier": "^7.24.5",
"@eslint-community/eslint-utils": "^4.4.0",
"@eslint/eslintrc": "^2.1.4",
"@eslint/eslintrc": "^3.0.2",
"ci-info": "^4.0.0",
"clean-regexp": "^1.0.0",
"core-js-compat": "^3.34.0",
"core-js-compat": "^3.37.0",
"esquery": "^1.5.0",
"indent-string": "^4.0.0",
"is-builtin-module": "^3.2.1",
@@ -2506,11 +2503,11 @@
"read-pkg-up": "^7.0.1",
"regexp-tree": "^0.1.27",
"regjsparser": "^0.10.0",
"semver": "^7.5.4",
"semver": "^7.6.1",
"strip-indent": "^3.0.0"
},
"engines": {
"node": ">=16"
"node": ">=18.18"
},
"funding": {
"url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
@@ -2519,6 +2516,70 @@
"eslint": ">=8.56.0"
}
},
"node_modules/eslint-plugin-unicorn/node_modules/@eslint/eslintrc": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.0.2.tgz",
"integrity": "sha512-wV19ZEGEMAC1eHgrS7UQPqsdEiCIbTKTasEfcXAigzoXICcqZSjBZEHlZwNVvKg6UBCjSlos84XiLqsRJnIcIg==",
"dev": true,
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
"espree": "^10.0.1",
"globals": "^14.0.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
"minimatch": "^3.1.2",
"strip-json-comments": "^3.1.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-plugin-unicorn/node_modules/eslint-visitor-keys": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
"integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
"dev": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-plugin-unicorn/node_modules/espree": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz",
"integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==",
"dev": true,
"dependencies": {
"acorn": "^8.11.3",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^4.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-plugin-unicorn/node_modules/globals": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
"dev": true,
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint-scope": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
@@ -2639,26 +2700,26 @@
}
},
"node_modules/exiftool-vendored": {
"version": "24.6.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-24.6.0.tgz",
"integrity": "sha512-jGjsoeYmR9VUrlZn0j1wcxMVi5y8C7A4FAa4vm3/l7ThT8d0f+jRcBqtdjaf+P5Ds/F4OgUq+ee/fRVhLy2DrA==",
"version": "26.0.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-26.0.0.tgz",
"integrity": "sha512-2TRxx21ovD95VvdSzHb/sTYYcwhiizQIhhVAbrgua9KoL902QRieREGvaUtfBZNjsptdjonuyku2kUBJCPqsgw==",
"dev": true,
"dependencies": {
"@photostructure/tz-lookup": "^9.0.2",
"@photostructure/tz-lookup": "^10.0.0",
"@types/luxon": "^3.4.2",
"batch-cluster": "^13.0.0",
"he": "^1.2.0",
"luxon": "^3.4.4"
},
"optionalDependencies": {
"exiftool-vendored.exe": "12.78.0",
"exiftool-vendored.pl": "12.78.0"
"exiftool-vendored.exe": "12.84.0",
"exiftool-vendored.pl": "12.84.0"
}
},
"node_modules/exiftool-vendored.exe": {
"version": "12.78.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.78.0.tgz",
"integrity": "sha512-eMN7L67sb89xi8sN7INPg19uwa1KibG2oOyGcfOvB47h+1hzmGgivVu/SZIMeOToVIbLRwUl+AFwLYSTNXsJEg==",
"version": "12.84.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.84.0.tgz",
"integrity": "sha512-9ocqJb0Pr9k0TownEMd75payF/XOQLF/swr/l0Ep49D+m609uIZsW09CtowhXmk1KrIFobS3+SkdXK04CSyUwQ==",
"dev": true,
"optional": true,
"os": [
@@ -2666,9 +2727,9 @@
]
},
"node_modules/exiftool-vendored.pl": {
"version": "12.78.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.78.0.tgz",
"integrity": "sha512-K8j9NgxRpTFskFuXEl0AGsc692yYyThe4i3SXgx7xc0fu/vwD2c7tRGljkEtvaweYnMmfrF4DhCpuTu0aux6sg==",
"version": "12.84.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.84.0.tgz",
"integrity": "sha512-TxvMRaVYtd24Vupn48zy24LOYItIIWEu4dgt/VlqLwxQItTpvJTV9YH04iZRvaNh9ZdPRgVKWMuuUDBBHv+lAg==",
"dev": true,
"optional": true,
"os": [
@@ -2692,6 +2753,7 @@
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
@@ -2708,6 +2770,7 @@
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
@@ -2759,6 +2822,7 @@
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
@@ -3001,6 +3065,7 @@
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
"integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"array-union": "^2.1.0",
"dir-glob": "^3.0.1",
@@ -3276,6 +3341,7 @@
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
@@ -3491,18 +3557,6 @@
"get-func-name": "^2.0.1"
}
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/luxon": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
@@ -3561,6 +3615,7 @@
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
}
@@ -3579,6 +3634,7 @@
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.2",
"picomatch": "^2.3.1"
@@ -4047,6 +4103,7 @@
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
@@ -4175,6 +4232,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
@@ -4194,12 +4252,12 @@
}
},
"node_modules/playwright": {
"version": "1.43.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.1.tgz",
"integrity": "sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==",
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz",
"integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==",
"dev": true,
"dependencies": {
"playwright-core": "1.43.1"
"playwright-core": "1.44.1"
},
"bin": {
"playwright": "cli.js"
@@ -4212,9 +4270,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.43.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.1.tgz",
"integrity": "sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==",
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz",
"integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
@@ -4710,13 +4768,10 @@
]
},
"node_modules/semver": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
"integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
@@ -4809,6 +4864,7 @@
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
@@ -5135,6 +5191,7 @@
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},
@@ -5294,9 +5351,9 @@
}
},
"node_modules/vite": {
"version": "5.2.10",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.10.tgz",
"integrity": "sha512-PAzgUZbP7msvQvqdSD+ErD5qGnSFiGOoWmV5yAKUEI0kdhjbH6nMWVyZQC/hSc4aXwc0oJ9aEdIiF9Oje0JFCw==",
"version": "5.2.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz",
"integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==",
"dev": true,
"dependencies": {
"esbuild": "^0.20.1",
@@ -5349,9 +5406,9 @@
}
},
"node_modules/vite-node": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.5.2.tgz",
"integrity": "sha512-Y8p91kz9zU+bWtF7HGt6DVw2JbhyuB2RlZix3FPYAYmUyZ3n7iTp8eSyLyY6sxtPegvxQtmlTMhfPhUfCUF93A==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz",
"integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==",
"dev": true,
"dependencies": {
"cac": "^6.7.14",
@@ -5385,16 +5442,16 @@
}
},
"node_modules/vitest": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.5.2.tgz",
"integrity": "sha512-l9gwIkq16ug3xY7BxHwcBQovLZG75zZL0PlsiYQbf76Rz6QGs54416UWMtC0jXeihvHvcHrf2ROEjkQRVpoZYw==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz",
"integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==",
"dev": true,
"dependencies": {
"@vitest/expect": "1.5.2",
"@vitest/runner": "1.5.2",
"@vitest/snapshot": "1.5.2",
"@vitest/spy": "1.5.2",
"@vitest/utils": "1.5.2",
"@vitest/expect": "1.6.0",
"@vitest/runner": "1.6.0",
"@vitest/snapshot": "1.6.0",
"@vitest/spy": "1.6.0",
"@vitest/utils": "1.6.0",
"acorn-walk": "^8.3.2",
"chai": "^4.3.10",
"debug": "^4.3.4",
@@ -5408,7 +5465,7 @@
"tinybench": "^2.5.1",
"tinypool": "^0.8.3",
"vite": "^5.0.0",
"vite-node": "1.5.2",
"vite-node": "1.6.0",
"why-is-node-running": "^2.2.2"
},
"bin": {
@@ -5423,8 +5480,8 @@
"peerDependencies": {
"@edge-runtime/vm": "*",
"@types/node": "^18.0.0 || >=20.0.0",
"@vitest/browser": "1.5.2",
"@vitest/ui": "1.5.2",
"@vitest/browser": "1.6.0",
"@vitest/ui": "1.6.0",
"happy-dom": "*",
"jsdom": "*"
},

View File

@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "1.103.1",
"version": "1.105.1",
"description": "",
"main": "index.js",
"type": "module",
@@ -21,7 +21,7 @@
"devDependencies": {
"@immich/cli": "file:../cli",
"@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.41.2",
"@playwright/test": "^1.44.1",
"@types/luxon": "^3.4.2",
"@types/node": "^20.11.17",
"@types/pg": "^8.11.0",
@@ -33,8 +33,8 @@
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^52.0.0",
"exiftool-vendored": "^24.5.0",
"eslint-plugin-unicorn": "^53.0.0",
"exiftool-vendored": "^26.0.0",
"luxon": "^3.4.4",
"pg": "^8.11.3",
"pngjs": "^7.0.0",
@@ -47,6 +47,6 @@
"vitest": "^1.3.0"
},
"volta": {
"node": "20.12.2"
"node": "20.13.1"
}
}

View File

@@ -1,6 +1,7 @@
import {
ActivityCreateDto,
AlbumResponseDto,
AlbumUserRole,
AssetFileUploadResponseDto,
LoginResponseDto,
ReactionType,
@@ -13,7 +14,7 @@ import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
describe('/activity', () => {
describe('/activities', () => {
let admin: LoginResponseDto;
let nonOwner: LoginResponseDto;
let asset: AssetFileUploadResponseDto;
@@ -33,7 +34,7 @@ describe('/activity', () => {
createAlbumDto: {
albumName: 'Album 1',
assetIds: [asset.id],
sharedWithUserIds: [nonOwner.userId],
albumUsers: [{ userId: nonOwner.userId, role: AlbumUserRole.Editor }],
},
},
{ headers: asBearerAuth(admin.accessToken) },
@@ -44,22 +45,24 @@ describe('/activity', () => {
await utils.resetDatabase(['activity']);
});
describe('GET /activity', () => {
describe('GET /activities', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/activity');
const { status, body } = await request(app).get('/activities');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require an albumId', async () => {
const { status, body } = await request(app).get('/activity').set('Authorization', `Bearer ${admin.accessToken}`);
const { status, body } = await request(app)
.get('/activities')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
});
it('should reject an invalid albumId', async () => {
const { status, body } = await request(app)
.get('/activity')
.get('/activities')
.query({ albumId: uuidDto.invalid })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400);
@@ -68,7 +71,7 @@ describe('/activity', () => {
it('should reject an invalid assetId', async () => {
const { status, body } = await request(app)
.get('/activity')
.get('/activities')
.query({ albumId: uuidDto.notFound, assetId: uuidDto.invalid })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400);
@@ -77,7 +80,7 @@ describe('/activity', () => {
it('should start off empty', async () => {
const { status, body } = await request(app)
.get('/activity')
.get('/activities')
.query({ albumId: album.id })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toEqual([]);
@@ -101,7 +104,7 @@ describe('/activity', () => {
]);
const { status, body } = await request(app)
.get('/activity')
.get('/activities')
.query({ albumId: album.id })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(200);
@@ -120,7 +123,7 @@ describe('/activity', () => {
]);
const { status, body } = await request(app)
.get('/activity')
.get('/activities')
.query({ albumId: album.id, type: 'comment' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(200);
@@ -139,7 +142,7 @@ describe('/activity', () => {
]);
const { status, body } = await request(app)
.get('/activity')
.get('/activities')
.query({ albumId: album.id, type: 'like' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(200);
@@ -151,7 +154,7 @@ describe('/activity', () => {
const reaction = await createActivity({ albumId: album.id, type: ReactionType.Like });
const response1 = await request(app)
.get('/activity')
.get('/activities')
.query({ albumId: album.id, userId: uuidDto.notFound })
.set('Authorization', `Bearer ${admin.accessToken}`);
@@ -159,7 +162,7 @@ describe('/activity', () => {
expect(response1.body.length).toBe(0);
const response2 = await request(app)
.get('/activity')
.get('/activities')
.query({ albumId: album.id, userId: admin.userId })
.set('Authorization', `Bearer ${admin.accessToken}`);
@@ -179,7 +182,7 @@ describe('/activity', () => {
]);
const { status, body } = await request(app)
.get('/activity')
.get('/activities')
.query({ albumId: album.id, assetId: asset.id })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(200);
@@ -188,16 +191,16 @@ describe('/activity', () => {
});
});
describe('POST /activity', () => {
describe('POST /activities', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/activity');
const { status, body } = await request(app).post('/activities');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require an albumId', async () => {
const { status, body } = await request(app)
.post('/activity')
.post('/activities')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: uuidDto.invalid });
expect(status).toEqual(400);
@@ -206,7 +209,7 @@ describe('/activity', () => {
it('should require a comment when type is comment', async () => {
const { status, body } = await request(app)
.post('/activity')
.post('/activities')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: uuidDto.notFound, type: 'comment', comment: null });
expect(status).toEqual(400);
@@ -215,7 +218,7 @@ describe('/activity', () => {
it('should add a comment to an album', async () => {
const { status, body } = await request(app)
.post('/activity')
.post('/activities')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({
albumId: album.id,
@@ -235,7 +238,7 @@ describe('/activity', () => {
it('should add a like to an album', async () => {
const { status, body } = await request(app)
.post('/activity')
.post('/activities')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: album.id, type: 'like' });
expect(status).toEqual(201);
@@ -252,7 +255,7 @@ describe('/activity', () => {
it('should return a 200 for a duplicate like on the album', async () => {
const reaction = await createActivity({ albumId: album.id, type: ReactionType.Like });
const { status, body } = await request(app)
.post('/activity')
.post('/activities')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: album.id, type: 'like' });
expect(status).toEqual(200);
@@ -266,7 +269,7 @@ describe('/activity', () => {
type: ReactionType.Like,
});
const { status, body } = await request(app)
.post('/activity')
.post('/activities')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: album.id, type: 'like' });
expect(status).toEqual(201);
@@ -275,7 +278,7 @@ describe('/activity', () => {
it('should add a comment to an asset', async () => {
const { status, body } = await request(app)
.post('/activity')
.post('/activities')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({
albumId: album.id,
@@ -296,7 +299,7 @@ describe('/activity', () => {
it('should add a like to an asset', async () => {
const { status, body } = await request(app)
.post('/activity')
.post('/activities')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: album.id, assetId: asset.id, type: 'like' });
expect(status).toEqual(201);
@@ -318,7 +321,7 @@ describe('/activity', () => {
});
const { status, body } = await request(app)
.post('/activity')
.post('/activities')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: album.id, assetId: asset.id, type: 'like' });
expect(status).toEqual(200);
@@ -326,16 +329,16 @@ describe('/activity', () => {
});
});
describe('DELETE /activity/:id', () => {
describe('DELETE /activities/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/activity/${uuidDto.notFound}`);
const { status, body } = await request(app).delete(`/activities/${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require a valid uuid', async () => {
const { status, body } = await request(app)
.delete(`/activity/${uuidDto.invalid}`)
.delete(`/activities/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
@@ -348,7 +351,7 @@ describe('/activity', () => {
comment: 'This is a test comment',
});
const { status } = await request(app)
.delete(`/activity/${reaction.id}`)
.delete(`/activities/${reaction.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(204);
});
@@ -359,7 +362,7 @@ describe('/activity', () => {
type: ReactionType.Like,
});
const { status } = await request(app)
.delete(`/activity/${reaction.id}`)
.delete(`/activities/${reaction.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(204);
});
@@ -372,7 +375,7 @@ describe('/activity', () => {
});
const { status } = await request(app)
.delete(`/activity/${reaction.id}`)
.delete(`/activities/${reaction.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(204);
@@ -386,7 +389,7 @@ describe('/activity', () => {
});
const { status, body } = await request(app)
.delete(`/activity/${reaction.id}`)
.delete(`/activities/${reaction.id}`)
.set('Authorization', `Bearer ${nonOwner.accessToken}`);
expect(status).toBe(400);
@@ -404,7 +407,7 @@ describe('/activity', () => {
);
const { status } = await request(app)
.delete(`/activity/${reaction.id}`)
.delete(`/activities/${reaction.id}`)
.set('Authorization', `Bearer ${nonOwner.accessToken}`);
expect(status).toBe(204);

View File

@@ -4,7 +4,7 @@ import {
AlbumUserRole,
AssetFileUploadResponseDto,
AssetOrder,
deleteUser,
deleteUserAdmin,
getAlbumInfo,
LoginResponseDto,
SharedLinkType,
@@ -23,7 +23,7 @@ const user2SharedUser = 'user2SharedUser';
const user2SharedLink = 'user2SharedLink';
const user2NotShared = 'user2NotShared';
describe('/album', () => {
describe('/albums', () => {
let admin: LoginResponseDto;
let user1: LoginResponseDto;
let user1Asset1: AssetFileUploadResponseDto;
@@ -49,72 +49,50 @@ describe('/album', () => {
utils.createAsset(user1.accessToken),
]);
const albums = await Promise.all([
// user 1
/* 0 */
user1Albums = await Promise.all([
utils.createAlbum(user1.accessToken, {
albumName: user1SharedEditorUser,
sharedWithUserIds: [user2.userId],
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }],
assetIds: [user1Asset1.id],
}),
/* 1 */
utils.createAlbum(user1.accessToken, {
albumName: user1SharedLink,
assetIds: [user1Asset1.id],
}),
/* 2 */
utils.createAlbum(user1.accessToken, {
albumName: user1NotShared,
assetIds: [user1Asset1.id, user1Asset2.id],
}),
// user 2
/* 3 */
utils.createAlbum(user2.accessToken, {
albumName: user2SharedUser,
sharedWithUserIds: [user1.userId, user3.userId],
}),
/* 4 */
utils.createAlbum(user2.accessToken, { albumName: user2SharedLink }),
/* 5 */
utils.createAlbum(user2.accessToken, { albumName: user2NotShared }),
// user 3
/* 6 */
utils.createAlbum(user3.accessToken, {
albumName: 'Deleted',
sharedWithUserIds: [user1.userId],
}),
// user1 shared with an editor
/* 7 */
utils.createAlbum(user1.accessToken, {
albumName: user1SharedViewerUser,
sharedWithUserIds: [user2.userId],
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }],
assetIds: [user1Asset1.id],
}),
]);
// Make viewer
await utils.updateAlbumUser(user1.accessToken, {
id: albums[7].id,
userId: user2.userId,
updateAlbumUserDto: { role: AlbumUserRole.Viewer },
user2Albums = await Promise.all([
utils.createAlbum(user2.accessToken, {
albumName: user2SharedUser,
albumUsers: [
{ userId: user1.userId, role: AlbumUserRole.Editor },
{ userId: user3.userId, role: AlbumUserRole.Editor },
],
}),
utils.createAlbum(user2.accessToken, { albumName: user2SharedLink }),
utils.createAlbum(user2.accessToken, { albumName: user2NotShared }),
]);
await utils.createAlbum(user3.accessToken, {
albumName: 'Deleted',
albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }],
});
albums[0].albumUsers[0].role = AlbumUserRole.Editor;
albums[3].albumUsers[0].role = AlbumUserRole.Editor;
albums[6].albumUsers[0].role = AlbumUserRole.Editor;
await addAssetsToAlbum(
{ id: albums[3].id, bulkIdsDto: { ids: [user1Asset1.id] } },
{ id: user2Albums[0].id, bulkIdsDto: { ids: [user1Asset1.id] } },
{ headers: asBearerAuth(user1.accessToken) },
);
albums[3] = await getAlbumInfo({ id: albums[3].id }, { headers: asBearerAuth(user2.accessToken) });
user1Albums = [...albums.slice(0, 3), albums[7]];
user2Albums = albums.slice(3, 6);
user2Albums[0] = await getAlbumInfo({ id: user2Albums[0].id }, { headers: asBearerAuth(user2.accessToken) });
await Promise.all([
// add shared link to user1SharedLink album
@@ -129,19 +107,19 @@ describe('/album', () => {
}),
]);
await deleteUser({ id: user3.userId, deleteUserDto: {} }, { headers: asBearerAuth(admin.accessToken) });
await deleteUserAdmin({ id: user3.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) });
});
describe('GET /album', () => {
describe('GET /albums', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/album');
const { status, body } = await request(app).get('/albums');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should reject an invalid shared param', async () => {
const { status, body } = await request(app)
.get('/album?shared=invalid')
.get('/albums?shared=invalid')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest(['shared must be a boolean value']));
@@ -149,7 +127,7 @@ describe('/album', () => {
it('should reject an invalid assetId param', async () => {
const { status, body } = await request(app)
.get('/album?assetId=invalid')
.get('/albums?assetId=invalid')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest(['assetId must be a UUID']));
@@ -157,7 +135,7 @@ describe('/album', () => {
it("should not show other users' favorites", async () => {
const { status, body } = await request(app)
.get(`/album/${user1Albums[0].id}?withoutAssets=false`)
.get(`/albums/${user1Albums[0].id}?withoutAssets=false`)
.set('Authorization', `Bearer ${user2.accessToken}`);
expect(status).toEqual(200);
expect(body).toEqual({
@@ -168,7 +146,7 @@ describe('/album', () => {
it('should not return shared albums with a deleted owner', async () => {
const { status, body } = await request(app)
.get('/album?shared=true')
.get('/albums?shared=true')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
@@ -200,7 +178,7 @@ describe('/album', () => {
});
it('should return the album collection including owned and shared', async () => {
const { status, body } = await request(app).get('/album').set('Authorization', `Bearer ${user1.accessToken}`);
const { status, body } = await request(app).get('/albums').set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(4);
expect(body).toEqual(
@@ -231,7 +209,7 @@ describe('/album', () => {
it('should return the album collection filtered by shared', async () => {
const { status, body } = await request(app)
.get('/album?shared=true')
.get('/albums?shared=true')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(4);
@@ -263,7 +241,7 @@ describe('/album', () => {
it('should return the album collection filtered by NOT shared', async () => {
const { status, body } = await request(app)
.get('/album?shared=false')
.get('/albums?shared=false')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(1);
@@ -280,7 +258,7 @@ describe('/album', () => {
it('should return the album collection filtered by assetId', async () => {
const { status, body } = await request(app)
.get(`/album?assetId=${user1Asset2.id}`)
.get(`/albums?assetId=${user1Asset2.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(1);
@@ -288,7 +266,7 @@ describe('/album', () => {
it('should return the album collection filtered by assetId and ignores shared=true', async () => {
const { status, body } = await request(app)
.get(`/album?shared=true&assetId=${user1Asset1.id}`)
.get(`/albums?shared=true&assetId=${user1Asset1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(5);
@@ -296,23 +274,23 @@ describe('/album', () => {
it('should return the album collection filtered by assetId and ignores shared=false', async () => {
const { status, body } = await request(app)
.get(`/album?shared=false&assetId=${user1Asset1.id}`)
.get(`/albums?shared=false&assetId=${user1Asset1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(5);
});
});
describe('GET /album/:id', () => {
describe('GET /albums/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/album/${user1Albums[0].id}`);
const { status, body } = await request(app).get(`/albums/${user1Albums[0].id}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should return album info for own album', async () => {
const { status, body } = await request(app)
.get(`/album/${user1Albums[0].id}?withoutAssets=false`)
.get(`/albums/${user1Albums[0].id}?withoutAssets=false`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
@@ -324,7 +302,7 @@ describe('/album', () => {
it('should return album info for shared album (editor)', async () => {
const { status, body } = await request(app)
.get(`/album/${user2Albums[0].id}?withoutAssets=false`)
.get(`/albums/${user2Albums[0].id}?withoutAssets=false`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
@@ -333,7 +311,7 @@ describe('/album', () => {
it('should return album info for shared album (viewer)', async () => {
const { status, body } = await request(app)
.get(`/album/${user1Albums[3].id}?withoutAssets=false`)
.get(`/albums/${user1Albums[3].id}?withoutAssets=false`)
.set('Authorization', `Bearer ${user2.accessToken}`);
expect(status).toBe(200);
@@ -342,7 +320,7 @@ describe('/album', () => {
it('should return album info with assets when withoutAssets is undefined', async () => {
const { status, body } = await request(app)
.get(`/album/${user1Albums[0].id}`)
.get(`/albums/${user1Albums[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
@@ -354,7 +332,7 @@ describe('/album', () => {
it('should return album info without assets when withoutAssets is true', async () => {
const { status, body } = await request(app)
.get(`/album/${user1Albums[0].id}?withoutAssets=true`)
.get(`/albums/${user1Albums[0].id}?withoutAssets=true`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
@@ -366,16 +344,16 @@ describe('/album', () => {
});
});
describe('GET /album/count', () => {
describe('GET /albums/count', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/album/count');
const { status, body } = await request(app).get('/albums/count');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should return total count of albums the user has access to', async () => {
const { status, body } = await request(app)
.get('/album/count')
.get('/albums/count')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
@@ -383,16 +361,16 @@ describe('/album', () => {
});
});
describe('POST /album', () => {
describe('POST /albums', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/album').send({ albumName: 'New album' });
const { status, body } = await request(app).post('/albums').send({ albumName: 'New album' });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should create an album', async () => {
const { status, body } = await request(app)
.post('/album')
.post('/albums')
.send({ albumName: 'New album' })
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(201);
@@ -405,7 +383,6 @@ describe('/album', () => {
description: '',
albumThumbnailAssetId: null,
shared: false,
sharedUsers: [],
albumUsers: [],
hasSharedLink: false,
assets: [],
@@ -417,9 +394,9 @@ describe('/album', () => {
});
});
describe('PUT /album/:id/assets', () => {
describe('PUT /albums/:id/assets', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/album/${user1Albums[0].id}/assets`);
const { status, body } = await request(app).put(`/albums/${user1Albums[0].id}/assets`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
@@ -427,7 +404,7 @@ describe('/album', () => {
it('should be able to add own asset to own album', async () => {
const asset = await utils.createAsset(user1.accessToken);
const { status, body } = await request(app)
.put(`/album/${user1Albums[0].id}/assets`)
.put(`/albums/${user1Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [asset.id] });
@@ -438,7 +415,7 @@ describe('/album', () => {
it('should be able to add own asset to shared album', async () => {
const asset = await utils.createAsset(user1.accessToken);
const { status, body } = await request(app)
.put(`/album/${user2Albums[0].id}/assets`)
.put(`/albums/${user2Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [asset.id] });
@@ -449,19 +426,33 @@ describe('/album', () => {
it('should not be able to add assets to album as a viewer', async () => {
const asset = await utils.createAsset(user2.accessToken);
const { status, body } = await request(app)
.put(`/album/${user1Albums[3].id}/assets`)
.put(`/albums/${user1Albums[3].id}/assets`)
.set('Authorization', `Bearer ${user2.accessToken}`)
.send({ ids: [asset.id] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Not found or no album.addAsset access'));
});
it('should add duplicate assets only once', async () => {
const asset = await utils.createAsset(user1.accessToken);
const { status, body } = await request(app)
.put(`/albums/${user1Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [asset.id, asset.id] });
expect(status).toBe(200);
expect(body).toEqual([
expect.objectContaining({ id: asset.id, success: true }),
expect.objectContaining({ id: asset.id, success: false, error: 'duplicate' }),
]);
});
});
describe('PATCH /album/:id', () => {
describe('PATCH /albums/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app)
.patch(`/album/${uuidDto.notFound}`)
.patch(`/albums/${uuidDto.notFound}`)
.send({ albumName: 'New album name' });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -472,7 +463,7 @@ describe('/album', () => {
albumName: 'New album',
});
const { status, body } = await request(app)
.patch(`/album/${album.id}`)
.patch(`/albums/${album.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({
albumName: 'New album name',
@@ -489,7 +480,7 @@ describe('/album', () => {
it('should not be able to update as a viewer', async () => {
const { status, body } = await request(app)
.patch(`/album/${user1Albums[3].id}`)
.patch(`/albums/${user1Albums[3].id}`)
.set('Authorization', `Bearer ${user2.accessToken}`)
.send({ albumName: 'New album name' });
@@ -499,7 +490,7 @@ describe('/album', () => {
it('should not be able to update as an editor', async () => {
const { status, body } = await request(app)
.patch(`/album/${user1Albums[0].id}`)
.patch(`/albums/${user1Albums[0].id}`)
.set('Authorization', `Bearer ${user2.accessToken}`)
.send({ albumName: 'New album name' });
@@ -508,10 +499,10 @@ describe('/album', () => {
});
});
describe('DELETE /album/:id/assets', () => {
describe('DELETE /albums/:id/assets', () => {
it('should require authentication', async () => {
const { status, body } = await request(app)
.delete(`/album/${user1Albums[0].id}/assets`)
.delete(`/albums/${user1Albums[0].id}/assets`)
.send({ ids: [user1Asset1.id] });
expect(status).toBe(401);
@@ -520,7 +511,7 @@ describe('/album', () => {
it('should not be able to remove foreign asset from own album', async () => {
const { status, body } = await request(app)
.delete(`/album/${user2Albums[0].id}/assets`)
.delete(`/albums/${user2Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user2.accessToken}`)
.send({ ids: [user1Asset1.id] });
@@ -536,7 +527,7 @@ describe('/album', () => {
it('should not be able to remove foreign asset from foreign album', async () => {
const { status, body } = await request(app)
.delete(`/album/${user1Albums[0].id}/assets`)
.delete(`/albums/${user1Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user2.accessToken}`)
.send({ ids: [user1Asset1.id] });
@@ -552,7 +543,7 @@ describe('/album', () => {
it('should be able to remove own asset from own album', async () => {
const { status, body } = await request(app)
.delete(`/album/${user1Albums[0].id}/assets`)
.delete(`/albums/${user1Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [user1Asset1.id] });
@@ -562,7 +553,7 @@ describe('/album', () => {
it('should be able to remove own asset from shared album', async () => {
const { status, body } = await request(app)
.delete(`/album/${user2Albums[0].id}/assets`)
.delete(`/albums/${user2Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [user1Asset1.id] });
@@ -572,13 +563,26 @@ describe('/album', () => {
it('should not be able to remove assets from album as a viewer', async () => {
const { status, body } = await request(app)
.delete(`/album/${user1Albums[3].id}/assets`)
.delete(`/albums/${user1Albums[3].id}/assets`)
.set('Authorization', `Bearer ${user2.accessToken}`)
.send({ ids: [user1Asset1.id] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Not found or no album.removeAsset access'));
});
it('should remove duplicate assets only once', async () => {
const { status, body } = await request(app)
.delete(`/albums/${user1Albums[1].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [user1Asset1.id, user1Asset1.id] });
expect(status).toBe(200);
expect(body).toEqual([
expect.objectContaining({ id: user1Asset1.id, success: true }),
expect.objectContaining({ id: user1Asset1.id, success: false, error: 'not_found' }),
]);
});
});
describe('PUT :id/users', () => {
@@ -591,7 +595,7 @@ describe('/album', () => {
});
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/album/${user1Albums[0].id}/users`).send({ sharedUserIds: [] });
const { status, body } = await request(app).put(`/albums/${user1Albums[0].id}/users`).send({ sharedUserIds: [] });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -599,21 +603,25 @@ describe('/album', () => {
it('should be able to add user to own album', async () => {
const { status, body } = await request(app)
.put(`/album/${album.id}/users`)
.put(`/albums/${album.id}/users`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }] });
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
sharedUsers: [expect.objectContaining({ id: user2.userId })],
albumUsers: [
expect.objectContaining({
user: expect.objectContaining({ id: user2.userId }),
}),
],
}),
);
});
it('should not be able to share album with owner', async () => {
const { status, body } = await request(app)
.put(`/album/${album.id}/users`)
.put(`/albums/${album.id}/users`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }] });
@@ -623,12 +631,12 @@ describe('/album', () => {
it('should not be able to add existing user to shared album', async () => {
await request(app)
.put(`/album/${album.id}/users`)
.put(`/albums/${album.id}/users`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }] });
const { status, body } = await request(app)
.put(`/album/${album.id}/users`)
.put(`/albums/${album.id}/users`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }] });
@@ -641,18 +649,22 @@ describe('/album', () => {
it('should allow the album owner to change the role of a shared user', async () => {
const album = await utils.createAlbum(user1.accessToken, {
albumName: 'testAlbum',
sharedWithUserIds: [user2.userId],
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }],
});
expect(album.albumUsers[0].role).toEqual(AlbumUserRole.Viewer);
const { status } = await request(app)
.put(`/album/${album.id}/user/${user2.userId}`)
.put(`/albums/${album.id}/user/${user2.userId}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ role: AlbumUserRole.Editor });
expect(status).toBe(200);
// Get album to verify the role change
const { body } = await request(app).get(`/album/${album.id}`).set('Authorization', `Bearer ${user1.accessToken}`);
const { body } = await request(app)
.get(`/albums/${album.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(body).toEqual(
expect.objectContaining({
albumUsers: [expect.objectContaining({ role: AlbumUserRole.Editor })],
@@ -663,11 +675,13 @@ describe('/album', () => {
it('should not allow a shared user to change the role of another shared user', async () => {
const album = await utils.createAlbum(user1.accessToken, {
albumName: 'testAlbum',
sharedWithUserIds: [user2.userId],
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }],
});
expect(album.albumUsers[0].role).toEqual(AlbumUserRole.Viewer);
const { status, body } = await request(app)
.put(`/album/${album.id}/user/${user2.userId}`)
.put(`/albums/${album.id}/user/${user2.userId}`)
.set('Authorization', `Bearer ${user2.accessToken}`)
.send({ role: AlbumUserRole.Editor });

View File

@@ -2,11 +2,10 @@ import {
AssetFileUploadResponseDto,
AssetResponseDto,
AssetTypeEnum,
LibraryResponseDto,
LoginResponseDto,
SharedLinkType,
getAllLibraries,
getAssetInfo,
getMyUser,
updateAssets,
} from '@immich/sdk';
import { exiftool } from 'exiftool-vendored';
@@ -73,7 +72,7 @@ describe('/asset', () => {
let stackAssets: AssetFileUploadResponseDto[];
let locationAsset: AssetFileUploadResponseDto;
beforeAll(async () => {
const setupTests = async () => {
await utils.resetDatabase();
admin = await utils.adminSetup({ onboarding: false });
@@ -87,6 +86,8 @@ describe('/asset', () => {
utils.userSetup(admin.accessToken, createUserDto.create('stack')),
]);
await utils.createPartner(user1.accessToken, user2.userId);
// asset location
locationAsset = await utils.createAsset(admin.accessToken, {
assetData: {
@@ -156,7 +157,8 @@ describe('/asset', () => {
assetId: user1Assets[0].id,
personId: person1.id,
});
}, 30_000);
};
beforeAll(setupTests, 30_000);
afterAll(() => {
utils.disconnectWebsocket(websocket);
@@ -233,6 +235,35 @@ describe('/asset', () => {
expect(data.status).toBe(200);
expect(data.body).toMatchObject({ people: [] });
});
describe('partner assets', () => {
it('should get the asset info', async () => {
const { status, body } = await request(app)
.get(`/asset/${user1Assets[0].id}`)
.set('Authorization', `Bearer ${user2.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ id: user1Assets[0].id });
});
it('disallows viewing archived assets', async () => {
const asset = await utils.createAsset(user1.accessToken, { isArchived: true });
const { status } = await request(app)
.get(`/asset/${asset.id}`)
.set('Authorization', `Bearer ${user2.accessToken}`);
expect(status).toBe(400);
});
it('disallows viewing trashed assets', async () => {
const asset = await utils.createAsset(user1.accessToken);
await utils.deleteAssets(user1.accessToken, [asset.id]);
const { status } = await request(app)
.get(`/asset/${asset.id}`)
.set('Authorization', `Bearer ${user2.accessToken}`);
expect(status).toBe(400);
});
});
});
describe('GET /asset/statistics', () => {
@@ -540,14 +571,321 @@ describe('/asset', () => {
});
});
describe('GET /asset/thumbnail/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/asset/thumbnail/${locationAsset.id}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should not include gps data for webp thumbnails', async () => {
await utils.waitForWebsocketEvent({
event: 'assetUpload',
id: locationAsset.id,
});
const { status, body, type } = await request(app)
.get(`/asset/thumbnail/${locationAsset.id}?format=WEBP`)
.set('Authorization', `Bearer ${admin.accessToken}`);
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/${locationAsset.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/${locationAsset.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/${locationAsset.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toBeDefined();
expect(type).toBe('image/jpeg');
const asset = await utils.getAssetInfo(admin.accessToken, locationAsset.id);
const original = await readFile(locationAssetFilepath);
const originalChecksum = utils.sha1(original);
const downloadChecksum = utils.sha1(body);
expect(originalChecksum).toBe(downloadChecksum);
expect(downloadChecksum).toBe(asset.checksum);
});
});
describe('GET /asset/map-marker', () => {
beforeAll(async () => {
const files = [
'formats/avif/8bit-sRGB.avif',
'formats/jpg/el_torcal_rocks.jpg',
'formats/jxl/8bit-sRGB.jxl',
'formats/heic/IMG_2682.heic',
'formats/png/density_plot.png',
'formats/raw/Nikon/D80/glarus.nef',
'formats/raw/Nikon/D700/philadelphia.nef',
'formats/raw/Panasonic/DMC-GH4/4_3.rw2',
'formats/raw/Sony/ILCE-6300/12bit-compressed-(3_2).arw',
'formats/raw/Sony/ILCE-7M2/14bit-uncompressed-(3_2).arw',
];
utils.resetEvents();
const uploadFile = async (input: string) => {
const filepath = join(testAssetDir, input);
const { id } = await utils.createAsset(admin.accessToken, {
assetData: { bytes: await readFile(filepath), filename: basename(filepath) },
});
await utils.waitForWebsocketEvent({ event: 'assetUpload', id });
};
const uploads = files.map((f) => uploadFile(f));
await Promise.all(uploads);
}, 30_000);
it('should require authentication', async () => {
const { status, body } = await request(app).get('/asset/map-marker');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
// TODO archive one of these assets
it('should get map markers for all non-archived assets', async () => {
const { status, body } = await request(app)
.get('/asset/map-marker')
.query({ isArchived: false })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(2);
expect(body).toEqual([
{
city: 'Palisade',
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(39.115),
lon: expect.closeTo(-108.400_968),
state: 'Colorado',
},
{
city: 'Ralston',
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(41.2203),
lon: expect.closeTo(-96.071_625),
state: 'Nebraska',
},
]);
});
// TODO archive one of these assets
it('should get all map markers', async () => {
const { status, body } = await request(app)
.get('/asset/map-marker')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual([
{
city: 'Palisade',
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(39.115),
lon: expect.closeTo(-108.400_968),
state: 'Colorado',
},
{
city: 'Ralston',
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(41.2203),
lon: expect.closeTo(-96.071_625),
state: 'Nebraska',
},
]);
});
});
describe('PUT /asset', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put('/asset');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require a valid parent id', async () => {
const { status, body } = await request(app)
.put('/asset')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ stackParentId: uuidDto.invalid, ids: [stackAssets[0].id] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['stackParentId must be a UUID']));
});
it('should require access to the parent', async () => {
const { status, body } = await request(app)
.put('/asset')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ stackParentId: stackAssets[3].id, ids: [user1Assets[0].id] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should add stack children', async () => {
const { status } = await request(app)
.put('/asset')
.set('Authorization', `Bearer ${stackUser.accessToken}`)
.send({ stackParentId: stackAssets[0].id, ids: [stackAssets[3].id] });
expect(status).toBe(204);
const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
expect(asset.stack).not.toBeUndefined();
expect(asset.stack).toEqual(expect.arrayContaining([expect.objectContaining({ id: stackAssets[3].id })]));
});
it('should remove stack children', async () => {
const { status } = await request(app)
.put('/asset')
.set('Authorization', `Bearer ${stackUser.accessToken}`)
.send({ removeParent: true, ids: [stackAssets[1].id] });
expect(status).toBe(204);
const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
expect(asset.stack).not.toBeUndefined();
expect(asset.stack).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: stackAssets[2].id }),
expect.objectContaining({ id: stackAssets[3].id }),
]),
);
});
it('should remove all stack children', async () => {
const { status } = await request(app)
.put('/asset')
.set('Authorization', `Bearer ${stackUser.accessToken}`)
.send({ removeParent: true, ids: [stackAssets[2].id, stackAssets[3].id] });
expect(status).toBe(204);
const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
expect(asset.stack).toBeUndefined();
});
it('should merge stack children', async () => {
// create stack after previous test removed stack children
await updateAssets(
{ assetBulkUpdateDto: { stackParentId: stackAssets[0].id, ids: [stackAssets[1].id, stackAssets[2].id] } },
{ headers: asBearerAuth(stackUser.accessToken) },
);
const { status } = await request(app)
.put('/asset')
.set('Authorization', `Bearer ${stackUser.accessToken}`)
.send({ stackParentId: stackAssets[3].id, ids: [stackAssets[0].id] });
expect(status).toBe(204);
const asset = await getAssetInfo({ id: stackAssets[3].id }, { headers: asBearerAuth(stackUser.accessToken) });
expect(asset.stack).not.toBeUndefined();
expect(asset.stack).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: stackAssets[0].id }),
expect.objectContaining({ id: stackAssets[1].id }),
expect.objectContaining({ id: stackAssets[2].id }),
]),
);
});
});
describe('PUT /asset/stack/parent', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put('/asset/stack/parent');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require a valid id', async () => {
const { status, body } = await request(app)
.put('/asset/stack/parent')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ oldParentId: uuidDto.invalid, newParentId: uuidDto.invalid });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
it('should require access', async () => {
const { status, body } = await request(app)
.put('/asset/stack/parent')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ oldParentId: stackAssets[3].id, newParentId: stackAssets[0].id });
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should make old parent child of new parent', async () => {
const { status } = await request(app)
.put('/asset/stack/parent')
.set('Authorization', `Bearer ${stackUser.accessToken}`)
.send({ oldParentId: stackAssets[3].id, newParentId: stackAssets[0].id });
expect(status).toBe(200);
const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
// new parent
expect(asset.stack).not.toBeUndefined();
expect(asset.stack).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: stackAssets[1].id }),
expect.objectContaining({ id: stackAssets[2].id }),
expect.objectContaining({ id: stackAssets[3].id }),
]),
);
});
});
describe('POST /asset/upload', () => {
beforeAll(setupTests, 30_000);
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/asset/upload`);
expect(body).toEqual(errorDto.unauthorized);
expect(status).toBe(401);
});
const invalid = [
it.each([
{ should: 'require `deviceAssetId`', dto: { ...makeUploadDto({ omit: 'deviceAssetId' }) } },
{ should: 'require `deviceId`', dto: { ...makeUploadDto({ omit: 'deviceId' }) } },
{ should: 'require `fileCreatedAt`', dto: { ...makeUploadDto({ omit: 'fileCreatedAt' }) } },
@@ -556,21 +894,17 @@ describe('/asset', () => {
{ should: 'throw if `isFavorite` is not a boolean', dto: { ...makeUploadDto(), isFavorite: 'not-a-boolean' } },
{ should: 'throw if `isVisible` is not a boolean', dto: { ...makeUploadDto(), isVisible: 'not-a-boolean' } },
{ should: 'throw if `isArchived` is not a boolean', dto: { ...makeUploadDto(), isArchived: 'not-a-boolean' } },
];
])('should $should', async ({ dto }) => {
const { status, body } = await request(app)
.post('/asset/upload')
.set('Authorization', `Bearer ${user1.accessToken}`)
.attach('assetData', makeRandomImage(), 'example.png')
.field(dto);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
for (const { should, dto } of invalid) {
it(`should ${should}`, async () => {
const { status, body } = await request(app)
.post('/asset/upload')
.set('Authorization', `Bearer ${user1.accessToken}`)
.attach('assetData', makeRandomImage(), 'example.png')
.field(dto);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
}
const tests = [
it.each([
{
input: 'formats/avif/8bit-sRGB.avif',
expected: {
@@ -786,26 +1120,22 @@ describe('/asset', () => {
},
},
},
];
for (const { input, expected } of tests) {
it(`should upload and generate a thumbnail for ${input}`, async () => {
const filepath = join(testAssetDir, input);
const { id, duplicate } = await utils.createAsset(admin.accessToken, {
assetData: { bytes: await readFile(filepath), filename: basename(filepath) },
});
expect(duplicate).toBe(false);
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: id });
const asset = await utils.getAssetInfo(admin.accessToken, id);
expect(asset.exifInfo).toBeDefined();
expect(asset.exifInfo).toMatchObject(expected.exifInfo);
expect(asset).toMatchObject(expected);
])(`should upload and generate a thumbnail for $input`, async ({ input, expected }) => {
const filepath = join(testAssetDir, input);
const { id, duplicate } = await utils.createAsset(admin.accessToken, {
assetData: { bytes: await readFile(filepath), filename: basename(filepath) },
});
}
expect(duplicate).toBe(false);
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: id });
const asset = await utils.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';
@@ -819,25 +1149,6 @@ describe('/asset', () => {
expect(duplicate).toBe(true);
});
it("should not upload to another user's library", async () => {
const libraries = await getAllLibraries({}, { headers: asBearerAuth(admin.accessToken) });
const library = libraries.find((library) => library.ownerId === user1.userId) as LibraryResponseDto;
const { body, status } = await request(app)
.post('/asset/upload')
.set('Authorization', `Bearer ${admin.accessToken}`)
.field('libraryId', library.id)
.field('deviceAssetId', 'example-image')
.field('deviceId', 'e2e')
.field('fileCreatedAt', new Date().toISOString())
.field('fileModifiedAt', new Date().toISOString())
.field('duration', '0:00:00.000000')
.attach('assetData', makeRandomImage(), 'example.png');
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Not found or no asset.upload access'));
});
it('should update the used quota', async () => {
const { body, status } = await request(app)
.post('/asset/upload')
@@ -851,7 +1162,7 @@ describe('/asset', () => {
expect(body).toEqual({ id: expect.any(String), duplicate: false });
expect(status).toBe(201);
const { body: user } = await request(app).get('/user/me').set('Authorization', `Bearer ${quotaUser.accessToken}`);
const user = await getMyUser({ headers: asBearerAuth(quotaUser.accessToken) });
expect(user).toEqual(expect.objectContaining({ quotaUsageInBytes: 70 }));
});
@@ -875,7 +1186,7 @@ describe('/asset', () => {
// 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 = [
it.each([
{
filepath: 'formats/motionphoto/Samsung One UI 5.jpg',
checksum: 'fr14niqCq6N20HB8rJYEvpsUVtI=',
@@ -888,329 +1199,23 @@ describe('/asset', () => {
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 utils.createAsset(admin.accessToken, {
assetData: {
bytes: await readFile(join(testAssetDir, filepath)),
filename: basename(filepath),
},
});
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: response.id });
expect(response.duplicate).toBe(false);
const asset = await utils.getAssetInfo(admin.accessToken, response.id);
expect(asset.livePhotoVideoId).toBeDefined();
const video = await utils.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/${locationAsset.id}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should not include gps data for webp thumbnails', async () => {
await utils.waitForWebsocketEvent({
event: 'assetUpload',
id: locationAsset.id,
])(`should extract motionphoto video from $filepath`, async ({ filepath, checksum }) => {
const response = await utils.createAsset(admin.accessToken, {
assetData: {
bytes: await readFile(join(testAssetDir, filepath)),
filename: basename(filepath),
},
});
const { status, body, type } = await request(app)
.get(`/asset/thumbnail/${locationAsset.id}?format=WEBP`)
.set('Authorization', `Bearer ${admin.accessToken}`);
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: response.id });
expect(status).toBe(200);
expect(body).toBeDefined();
expect(type).toBe('image/webp');
expect(response.duplicate).toBe(false);
const exifData = await readTags(body, 'thumbnail.webp');
expect(exifData).not.toHaveProperty('GPSLongitude');
expect(exifData).not.toHaveProperty('GPSLatitude');
});
const asset = await utils.getAssetInfo(admin.accessToken, response.id);
expect(asset.livePhotoVideoId).toBeDefined();
it('should not include gps data for jpeg thumbnails', async () => {
const { status, body, type } = await request(app)
.get(`/asset/thumbnail/${locationAsset.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/${locationAsset.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/${locationAsset.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toBeDefined();
expect(type).toBe('image/jpeg');
const asset = await utils.getAssetInfo(admin.accessToken, locationAsset.id);
const original = await readFile(locationAssetFilepath);
const originalChecksum = utils.sha1(original);
const downloadChecksum = utils.sha1(body);
expect(originalChecksum).toBe(downloadChecksum);
expect(downloadChecksum).toBe(asset.checksum);
});
});
describe('GET /asset/map-marker', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/asset/map-marker');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
// TODO archive one of these assets
it('should get map markers for all non-archived assets', async () => {
const { status, body } = await request(app)
.get('/asset/map-marker')
.query({ isArchived: false })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(2);
expect(body).toEqual([
{
city: 'Palisade',
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(39.115),
lon: expect.closeTo(-108.400_968),
state: 'Colorado',
},
{
city: 'Ralston',
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(41.2203),
lon: expect.closeTo(-96.071_625),
state: 'Nebraska',
},
]);
});
// TODO archive one of these assets
it('should get all map markers', async () => {
const { status, body } = await request(app)
.get('/asset/map-marker')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual([
{
city: 'Palisade',
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(39.115),
lon: expect.closeTo(-108.400_968),
state: 'Colorado',
},
{
city: 'Ralston',
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(41.2203),
lon: expect.closeTo(-96.071_625),
state: 'Nebraska',
},
]);
});
});
describe('GET /asset', () => {
it('should return stack data', async () => {
const { status, body } = await request(app).get('/asset').set('Authorization', `Bearer ${stackUser.accessToken}`);
const stack = body.find((asset: AssetResponseDto) => asset.id === stackAssets[0].id);
expect(status).toBe(200);
expect(stack).toEqual(
expect.objectContaining({
stackCount: 3,
stack:
// Response includes children at the root level
expect.arrayContaining([
expect.objectContaining({ id: stackAssets[1].id }),
expect.objectContaining({ id: stackAssets[2].id }),
]),
}),
);
});
});
describe('PUT /asset', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put('/asset');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require a valid parent id', async () => {
const { status, body } = await request(app)
.put('/asset')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ stackParentId: uuidDto.invalid, ids: [stackAssets[0].id] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['stackParentId must be a UUID']));
});
it('should require access to the parent', async () => {
const { status, body } = await request(app)
.put('/asset')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ stackParentId: stackAssets[3].id, ids: [user1Assets[0].id] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should add stack children', async () => {
const { status } = await request(app)
.put('/asset')
.set('Authorization', `Bearer ${stackUser.accessToken}`)
.send({ stackParentId: stackAssets[0].id, ids: [stackAssets[3].id] });
expect(status).toBe(204);
const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
expect(asset.stack).not.toBeUndefined();
expect(asset.stack).toEqual(expect.arrayContaining([expect.objectContaining({ id: stackAssets[3].id })]));
});
it('should remove stack children', async () => {
const { status } = await request(app)
.put('/asset')
.set('Authorization', `Bearer ${stackUser.accessToken}`)
.send({ removeParent: true, ids: [stackAssets[1].id] });
expect(status).toBe(204);
const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
expect(asset.stack).not.toBeUndefined();
expect(asset.stack).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: stackAssets[2].id }),
expect.objectContaining({ id: stackAssets[3].id }),
]),
);
});
it('should remove all stack children', async () => {
const { status } = await request(app)
.put('/asset')
.set('Authorization', `Bearer ${stackUser.accessToken}`)
.send({ removeParent: true, ids: [stackAssets[2].id, stackAssets[3].id] });
expect(status).toBe(204);
const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
expect(asset.stack).toBeUndefined();
});
it('should merge stack children', async () => {
// create stack after previous test removed stack children
await updateAssets(
{ assetBulkUpdateDto: { stackParentId: stackAssets[0].id, ids: [stackAssets[1].id, stackAssets[2].id] } },
{ headers: asBearerAuth(stackUser.accessToken) },
);
const { status } = await request(app)
.put('/asset')
.set('Authorization', `Bearer ${stackUser.accessToken}`)
.send({ stackParentId: stackAssets[3].id, ids: [stackAssets[0].id] });
expect(status).toBe(204);
const asset = await getAssetInfo({ id: stackAssets[3].id }, { headers: asBearerAuth(stackUser.accessToken) });
expect(asset.stack).not.toBeUndefined();
expect(asset.stack).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: stackAssets[0].id }),
expect.objectContaining({ id: stackAssets[1].id }),
expect.objectContaining({ id: stackAssets[2].id }),
]),
);
});
});
describe('PUT /asset/stack/parent', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put('/asset/stack/parent');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require a valid id', async () => {
const { status, body } = await request(app)
.put('/asset/stack/parent')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ oldParentId: uuidDto.invalid, newParentId: uuidDto.invalid });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
it('should require access', async () => {
const { status, body } = await request(app)
.put('/asset/stack/parent')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ oldParentId: stackAssets[3].id, newParentId: stackAssets[0].id });
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should make old parent child of new parent', async () => {
const { status } = await request(app)
.put('/asset/stack/parent')
.set('Authorization', `Bearer ${stackUser.accessToken}`)
.send({ oldParentId: stackAssets[3].id, newParentId: stackAssets[0].id });
expect(status).toBe(200);
const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
// new parent
expect(asset.stack).not.toBeUndefined();
expect(asset.stack).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: stackAssets[1].id }),
expect.objectContaining({ id: stackAssets[2].id }),
expect.objectContaining({ id: stackAssets[3].id }),
]),
);
const video = await utils.getAssetInfo(admin.accessToken, asset.livePhotoVideoId as string);
expect(video.checksum).toStrictEqual(checksum);
});
});
});

View File

@@ -2,7 +2,7 @@ import { deleteAssets, getAuditFiles, updateAsset, type LoginResponseDto } from
import { asBearerAuth, utils } from 'src/utils';
import { beforeAll, describe, expect, it } from 'vitest';
describe('/audit', () => {
describe('/audits', () => {
let admin: LoginResponseDto;
beforeAll(async () => {

View File

@@ -1,11 +1,4 @@
import {
LibraryResponseDto,
LibraryType,
LoginResponseDto,
ScanLibraryDto,
getAllLibraries,
scanLibrary,
} from '@immich/sdk';
import { LibraryResponseDto, LoginResponseDto, ScanLibraryDto, getAllLibraries, scanLibrary } from '@immich/sdk';
import { cpSync, existsSync } from 'node:fs';
import { Socket } from 'socket.io-client';
import { userDto, uuidDto } from 'src/fixtures';
@@ -18,7 +11,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
const scan = async (accessToken: string, id: string, dto: ScanLibraryDto = {}) =>
scanLibrary({ id, scanLibraryDto: dto }, { headers: asBearerAuth(accessToken) });
describe('/library', () => {
describe('/libraries', () => {
let admin: LoginResponseDto;
let user: LoginResponseDto;
let library: LibraryResponseDto;
@@ -29,7 +22,7 @@ describe('/library', () => {
admin = await utils.adminSetup();
await utils.resetAdminConfig(admin.accessToken);
user = await utils.userSetup(admin.accessToken, userDto.user1);
library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, type: LibraryType.External });
library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId });
websocket = await utils.connectWebsocket(admin.accessToken);
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetA.png`);
utils.createImageFile(`${testAssetDir}/temp/directoryB/assetB.png`);
@@ -44,44 +37,26 @@ describe('/library', () => {
utils.resetEvents();
});
describe('GET /library', () => {
describe('GET /libraries', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/library');
const { status, body } = await request(app).get('/libraries');
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', () => {
describe('POST /libraries', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/library').send({});
const { status, body } = await request(app).post('/libraries').send({});
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require admin authentication', async () => {
const { status, body } = await request(app)
.post('/library')
.post('/libraries')
.set('Authorization', `Bearer ${user.accessToken}`)
.send({ ownerId: admin.userId, type: LibraryType.External });
.send({ ownerId: admin.userId });
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
@@ -89,15 +64,14 @@ describe('/library', () => {
it('should create an external library with defaults', async () => {
const { status, body } = await request(app)
.post('/library')
.post('/libraries')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ownerId: admin.userId, type: LibraryType.External });
.send({ ownerId: admin.userId });
expect(status).toBe(201);
expect(body).toEqual(
expect.objectContaining({
ownerId: admin.userId,
type: LibraryType.External,
name: 'New External Library',
refreshedAt: null,
assetCount: 0,
@@ -109,11 +83,10 @@ describe('/library', () => {
it('should create an external library with options', async () => {
const { status, body } = await request(app)
.post('/library')
.post('/libraries')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({
ownerId: admin.userId,
type: LibraryType.External,
name: 'My Awesome Library',
importPaths: ['/path/to/import'],
exclusionPatterns: ['**/Raw/**'],
@@ -130,11 +103,10 @@ describe('/library', () => {
it('should not create an external library with duplicate import paths', async () => {
const { status, body } = await request(app)
.post('/library')
.post('/libraries')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({
ownerId: admin.userId,
type: LibraryType.External,
name: 'My Awesome Library',
importPaths: ['/path', '/path'],
exclusionPatterns: ['**/Raw/**'],
@@ -146,11 +118,10 @@ describe('/library', () => {
it('should not create an external library with duplicate exclusion patterns', async () => {
const { status, body } = await request(app)
.post('/library')
.post('/libraries')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({
ownerId: admin.userId,
type: LibraryType.External,
name: 'My Awesome Library',
importPaths: ['/path/to/import'],
exclusionPatterns: ['**/Raw/**', '**/Raw/**'],
@@ -159,72 +130,18 @@ describe('/library', () => {
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({ ownerId: admin.userId, 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({ ownerId: admin.userId, 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({ ownerId: admin.userId, 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({ ownerId: admin.userId, type: LibraryType.Upload, exclusionPatterns: ['**/Raw/**'] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Upload libraries cannot have exclusion patterns'));
});
});
describe('PUT /library/:id', () => {
describe('PUT /libraries/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/library/${uuidDto.notFound}`).send({});
const { status, body } = await request(app).put(`/libraries/${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}`)
.put(`/libraries/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ name: 'New Library Name' });
@@ -238,7 +155,7 @@ describe('/library', () => {
it('should not set an empty name', async () => {
const { status, body } = await request(app)
.put(`/library/${library.id}`)
.put(`/libraries/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ name: '' });
@@ -248,7 +165,7 @@ describe('/library', () => {
it('should change the import paths', async () => {
const { status, body } = await request(app)
.put(`/library/${library.id}`)
.put(`/libraries/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ importPaths: [testAssetDirInternal] });
@@ -262,7 +179,7 @@ describe('/library', () => {
it('should reject an empty import path', async () => {
const { status, body } = await request(app)
.put(`/library/${library.id}`)
.put(`/libraries/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ importPaths: [''] });
@@ -272,7 +189,7 @@ describe('/library', () => {
it('should reject duplicate import paths', async () => {
const { status, body } = await request(app)
.put(`/library/${library.id}`)
.put(`/libraries/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ importPaths: ['/path', '/path'] });
@@ -282,7 +199,7 @@ describe('/library', () => {
it('should change the exclusion pattern', async () => {
const { status, body } = await request(app)
.put(`/library/${library.id}`)
.put(`/libraries/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ exclusionPatterns: ['**/Raw/**'] });
@@ -296,7 +213,7 @@ describe('/library', () => {
it('should reject duplicate exclusion patterns', async () => {
const { status, body } = await request(app)
.put(`/library/${library.id}`)
.put(`/libraries/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ exclusionPatterns: ['**/*.jpg', '**/*.jpg'] });
@@ -306,7 +223,7 @@ describe('/library', () => {
it('should reject an empty exclusion pattern', async () => {
const { status, body } = await request(app)
.put(`/library/${library.id}`)
.put(`/libraries/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ exclusionPatterns: [''] });
@@ -315,9 +232,9 @@ describe('/library', () => {
});
});
describe('GET /library/:id', () => {
describe('GET /libraries/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/library/${uuidDto.notFound}`);
const { status, body } = await request(app).get(`/libraries/${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -325,27 +242,23 @@ describe('/library', () => {
it('should require admin access', async () => {
const { status, body } = await request(app)
.get(`/library/${uuidDto.notFound}`)
.get(`/libraries/${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 utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
type: LibraryType.External,
});
const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId });
const { status, body } = await request(app)
.get(`/library/${library.id}`)
.get(`/libraries/${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,
@@ -356,41 +269,26 @@ describe('/library', () => {
});
});
describe('GET /library/:id/statistics', () => {
describe('GET /libraries/:id/statistics', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/library/${uuidDto.notFound}/statistics`);
const { status, body } = await request(app).get(`/libraries/${uuidDto.notFound}/statistics`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
});
describe('POST /library/:id/scan', () => {
describe('POST /libraries/:id/scan', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/library/${uuidDto.notFound}/scan`).send({});
const { status, body } = await request(app).post(`/libraries/${uuidDto.notFound}/scan`).send({});
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should not scan an upload library', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
type: LibraryType.Upload,
});
const { status, body } = await request(app)
.post(`/library/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Can only refresh external libraries'));
});
it('should scan external library', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp/directoryA`],
});
@@ -406,7 +304,6 @@ describe('/library', () => {
it('should scan external library with exclusion pattern', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`],
exclusionPatterns: ['**/directoryA'],
});
@@ -423,7 +320,6 @@ describe('/library', () => {
it('should scan multiple import paths', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp/directoryA`, `${testAssetDirInternal}/temp/directoryB`],
});
@@ -440,7 +336,6 @@ describe('/library', () => {
it('should pick up new files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`],
});
@@ -466,7 +361,6 @@ describe('/library', () => {
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`],
});
@@ -493,7 +387,6 @@ describe('/library', () => {
it('should scan new files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`],
});
@@ -521,7 +414,6 @@ describe('/library', () => {
it('should reimport modified files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`],
});
@@ -549,7 +441,6 @@ describe('/library', () => {
it('should not reimport unmodified files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`],
});
@@ -579,7 +470,6 @@ describe('/library', () => {
it('should reimport all files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`],
});
@@ -606,9 +496,9 @@ describe('/library', () => {
});
});
describe('POST /library/:id/removeOffline', () => {
describe('POST /libraries/:id/removeOffline', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/library/${uuidDto.notFound}/removeOffline`).send({});
const { status, body } = await request(app).post(`/libraries/${uuidDto.notFound}/removeOffline`).send({});
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -617,7 +507,6 @@ describe('/library', () => {
it('should remove offline files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`],
});
@@ -643,7 +532,7 @@ describe('/library', () => {
expect(offlineAssets.count).toBe(1);
const { status } = await request(app)
.post(`/library/${library.id}/removeOffline`)
.post(`/libraries/${library.id}/removeOffline`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
@@ -658,7 +547,6 @@ describe('/library', () => {
it('should not remove online files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`],
});
@@ -669,7 +557,7 @@ describe('/library', () => {
expect(assetsBefore.count).toBeGreaterThan(1);
const { status } = await request(app)
.post(`/library/${library.id}/removeOffline`)
.post(`/libraries/${library.id}/removeOffline`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
@@ -681,9 +569,9 @@ describe('/library', () => {
});
});
describe('POST /library/:id/validate', () => {
describe('POST /libraries/:id/validate', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/library/${uuidDto.notFound}/validate`).send({});
const { status, body } = await request(app).post(`/libraries/${uuidDto.notFound}/validate`).send({});
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -729,54 +617,25 @@ describe('/library', () => {
});
});
describe('DELETE /library/:id', () => {
describe('DELETE /libraries/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/library/${uuidDto.notFound}`);
const { status, body } = await request(app).delete(`/libraries/${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 utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
type: LibraryType.External,
});
const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId });
const { status, body } = await request(app)
.delete(`/library/${library.id}`)
.delete(`/libraries/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
expect(body).toEqual({});
const libraries = await getAllLibraries({}, { headers: asBearerAuth(admin.accessToken) });
const libraries = await getAllLibraries({ headers: asBearerAuth(admin.accessToken) });
expect(libraries).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
@@ -789,7 +648,6 @@ describe('/library', () => {
it('should delete an external library with assets', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`],
});
@@ -797,13 +655,13 @@ describe('/library', () => {
await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 2 });
const { status, body } = await request(app)
.delete(`/library/${library.id}`)
.delete(`/libraries/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
expect(body).toEqual({});
const libraries = await getAllLibraries({}, { headers: asBearerAuth(admin.accessToken) });
const libraries = await getAllLibraries({ headers: asBearerAuth(admin.accessToken) });
expect(libraries).not.toEqual(
expect.arrayContaining([
expect.objectContaining({

View File

@@ -5,7 +5,7 @@ import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest';
describe('/partner', () => {
describe('/partners', () => {
let admin: LoginResponseDto;
let user1: LoginResponseDto;
let user2: LoginResponseDto;
@@ -28,9 +28,9 @@ describe('/partner', () => {
]);
});
describe('GET /partner', () => {
describe('GET /partners', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/partner');
const { status, body } = await request(app).get('/partners');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -38,7 +38,7 @@ describe('/partner', () => {
it('should get all partners shared by user', async () => {
const { status, body } = await request(app)
.get('/partner')
.get('/partners')
.set('Authorization', `Bearer ${user1.accessToken}`)
.query({ direction: 'shared-by' });
@@ -48,7 +48,7 @@ describe('/partner', () => {
it('should get all partners that share with user', async () => {
const { status, body } = await request(app)
.get('/partner')
.get('/partners')
.set('Authorization', `Bearer ${user1.accessToken}`)
.query({ direction: 'shared-with' });
@@ -57,9 +57,9 @@ describe('/partner', () => {
});
});
describe('POST /partner/:id', () => {
describe('POST /partners/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/partner/${user3.userId}`);
const { status, body } = await request(app).post(`/partners/${user3.userId}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -67,7 +67,7 @@ describe('/partner', () => {
it('should share with new partner', async () => {
const { status, body } = await request(app)
.post(`/partner/${user3.userId}`)
.post(`/partners/${user3.userId}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(201);
@@ -76,7 +76,7 @@ describe('/partner', () => {
it('should not share with new partner if already sharing with this partner', async () => {
const { status, body } = await request(app)
.post(`/partner/${user2.userId}`)
.post(`/partners/${user2.userId}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
@@ -84,9 +84,9 @@ describe('/partner', () => {
});
});
describe('PUT /partner/:id', () => {
describe('PUT /partners/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/partner/${user2.userId}`);
const { status, body } = await request(app).put(`/partners/${user2.userId}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -94,7 +94,7 @@ describe('/partner', () => {
it('should update partner', async () => {
const { status, body } = await request(app)
.put(`/partner/${user2.userId}`)
.put(`/partners/${user2.userId}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ inTimeline: false });
@@ -103,9 +103,9 @@ describe('/partner', () => {
});
});
describe('DELETE /partner/:id', () => {
describe('DELETE /partners/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/partner/${user3.userId}`);
const { status, body } = await request(app).delete(`/partners/${user3.userId}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -113,7 +113,7 @@ describe('/partner', () => {
it('should delete partner', async () => {
const { status } = await request(app)
.delete(`/partner/${user3.userId}`)
.delete(`/partners/${user3.userId}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
@@ -121,7 +121,7 @@ describe('/partner', () => {
it('should throw a bad request if partner not found', async () => {
const { status, body } = await request(app)
.delete(`/partner/${user3.userId}`)
.delete(`/partners/${user3.userId}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);

View File

@@ -12,7 +12,7 @@ const invalidBirthday = [
{ birthDate: new Date(9999, 0, 0).toISOString(), response: ['Birth date cannot be in the future'] },
];
describe('/person', () => {
describe('/people', () => {
let admin: LoginResponseDto;
let visiblePerson: PersonResponseDto;
let hiddenPerson: PersonResponseDto;
@@ -47,11 +47,11 @@ describe('/person', () => {
]);
});
describe('GET /person', () => {
describe('GET /people', () => {
beforeEach(async () => {});
it('should require authentication', async () => {
const { status, body } = await request(app).get('/person');
const { status, body } = await request(app).get('/people');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -59,7 +59,7 @@ describe('/person', () => {
it('should return all people (including hidden)', async () => {
const { status, body } = await request(app)
.get('/person')
.get('/people')
.set('Authorization', `Bearer ${admin.accessToken}`)
.query({ withHidden: true });
@@ -76,7 +76,7 @@ describe('/person', () => {
});
it('should return only visible people', async () => {
const { status, body } = await request(app).get('/person').set('Authorization', `Bearer ${admin.accessToken}`);
const { status, body } = await request(app).get('/people').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
@@ -90,9 +90,9 @@ describe('/person', () => {
});
});
describe('GET /person/:id', () => {
describe('GET /people/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/person/${uuidDto.notFound}`);
const { status, body } = await request(app).get(`/people/${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -100,7 +100,7 @@ describe('/person', () => {
it('should throw error if person with id does not exist', async () => {
const { status, body } = await request(app)
.get(`/person/${uuidDto.notFound}`)
.get(`/people/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
@@ -109,7 +109,7 @@ describe('/person', () => {
it('should return person information', async () => {
const { status, body } = await request(app)
.get(`/person/${visiblePerson.id}`)
.get(`/people/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
@@ -117,9 +117,9 @@ describe('/person', () => {
});
});
describe('GET /person/:id/statistics', () => {
describe('GET /people/:id/statistics', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/person/${multipleAssetsPerson.id}/statistics`);
const { status, body } = await request(app).get(`/people/${multipleAssetsPerson.id}/statistics`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -127,7 +127,7 @@ describe('/person', () => {
it('should throw error if person with id does not exist', async () => {
const { status, body } = await request(app)
.get(`/person/${uuidDto.notFound}/statistics`)
.get(`/people/${uuidDto.notFound}/statistics`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
@@ -136,7 +136,7 @@ describe('/person', () => {
it('should return the correct number of assets', async () => {
const { status, body } = await request(app)
.get(`/person/${multipleAssetsPerson.id}/statistics`)
.get(`/people/${multipleAssetsPerson.id}/statistics`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
@@ -144,9 +144,9 @@ describe('/person', () => {
});
});
describe('POST /person', () => {
describe('POST /people', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/person`);
const { status, body } = await request(app).post(`/people`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
@@ -154,7 +154,7 @@ describe('/person', () => {
for (const { birthDate, response } of invalidBirthday) {
it(`should not accept an invalid birth date [${birthDate}]`, async () => {
const { status, body } = await request(app)
.post(`/person`)
.post(`/people`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ birthDate });
expect(status).toBe(400);
@@ -164,7 +164,7 @@ describe('/person', () => {
it('should create a person', async () => {
const { status, body } = await request(app)
.post(`/person`)
.post(`/people`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({
name: 'New Person',
@@ -179,9 +179,9 @@ describe('/person', () => {
});
});
describe('PUT /person/:id', () => {
describe('PUT /people/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/person/${uuidDto.notFound}`);
const { status, body } = await request(app).put(`/people/${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
@@ -193,7 +193,7 @@ describe('/person', () => {
]) {
it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app)
.put(`/person/${visiblePerson.id}`)
.put(`/people/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ [key]: null });
expect(status).toBe(400);
@@ -204,7 +204,7 @@ describe('/person', () => {
for (const { birthDate, response } of invalidBirthday) {
it(`should not accept an invalid birth date [${birthDate}]`, async () => {
const { status, body } = await request(app)
.put(`/person/${visiblePerson.id}`)
.put(`/people/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ birthDate });
expect(status).toBe(400);
@@ -214,7 +214,7 @@ describe('/person', () => {
it('should update a date of birth', async () => {
const { status, body } = await request(app)
.put(`/person/${visiblePerson.id}`)
.put(`/people/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ birthDate: '1990-01-01T05:00:00.000Z' });
expect(status).toBe(200);
@@ -223,7 +223,7 @@ describe('/person', () => {
it('should clear a date of birth', async () => {
const { status, body } = await request(app)
.put(`/person/${visiblePerson.id}`)
.put(`/people/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ birthDate: null });
expect(status).toBe(200);

View File

@@ -15,16 +15,16 @@ describe('/server-info', () => {
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
});
describe('GET /server-info', () => {
describe('GET /server-info/storage', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/server-info');
const { status, body } = await request(app).get('/server-info/storage');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should return the disk information', async () => {
const { status, body } = await request(app)
.get('/server-info')
.get('/server-info/storage')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
@@ -66,6 +66,7 @@ describe('/server-info', () => {
expect(body).toEqual({
smartSearch: false,
configFile: false,
duplicateDetection: false,
facialRecognition: false,
map: true,
reverseGeocoding: true,

View File

@@ -5,7 +5,7 @@ import {
SharedLinkResponseDto,
SharedLinkType,
createAlbum,
deleteUser,
deleteUserAdmin,
} from '@immich/sdk';
import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
@@ -13,7 +13,7 @@ import { app, asBearerAuth, shareUrl, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest';
describe('/shared-link', () => {
describe('/shared-links', () => {
let admin: LoginResponseDto;
let asset1: AssetFileUploadResponseDto;
let asset2: AssetFileUploadResponseDto;
@@ -86,7 +86,7 @@ describe('/shared-link', () => {
}),
]);
await deleteUser({ id: user2.userId, deleteUserDto: {} }, { headers: asBearerAuth(admin.accessToken) });
await deleteUserAdmin({ id: user2.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) });
});
describe('GET /share/${key}', () => {
@@ -114,9 +114,9 @@ describe('/shared-link', () => {
});
});
describe('GET /shared-link', () => {
describe('GET /shared-links', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/shared-link');
const { status, body } = await request(app).get('/shared-links');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -124,7 +124,7 @@ describe('/shared-link', () => {
it('should get all shared links created by user', async () => {
const { status, body } = await request(app)
.get('/shared-link')
.get('/shared-links')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
@@ -142,7 +142,7 @@ describe('/shared-link', () => {
it('should not get shared links created by other users', async () => {
const { status, body } = await request(app)
.get('/shared-link')
.get('/shared-links')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
@@ -150,15 +150,15 @@ describe('/shared-link', () => {
});
});
describe('GET /shared-link/me', () => {
describe('GET /shared-links/me', () => {
it('should not require admin authentication', async () => {
const { status } = await request(app).get('/shared-link/me').set('Authorization', `Bearer ${admin.accessToken}`);
const { status } = await request(app).get('/shared-links/me').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(403);
});
it('should get data for correct shared link', async () => {
const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithAlbum.key });
const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithAlbum.key });
expect(status).toBe(200);
expect(body).toEqual(
@@ -172,7 +172,7 @@ describe('/shared-link', () => {
it('should return unauthorized for incorrect shared link', async () => {
const { status, body } = await request(app)
.get('/shared-link/me')
.get('/shared-links/me')
.query({ key: linkWithAlbum.key + 'foo' });
expect(status).toBe(401);
@@ -180,14 +180,14 @@ describe('/shared-link', () => {
});
it('should return unauthorized if target has been soft deleted', async () => {
const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithDeletedAlbum.key });
const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithDeletedAlbum.key });
expect(status).toBe(401);
expect(body).toEqual(errorDto.invalidShareKey);
});
it('should return unauthorized for password protected link', async () => {
const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithPassword.key });
const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithPassword.key });
expect(status).toBe(401);
expect(body).toEqual(errorDto.invalidSharePassword);
@@ -195,7 +195,7 @@ describe('/shared-link', () => {
it('should get data for correct password protected link', async () => {
const { status, body } = await request(app)
.get('/shared-link/me')
.get('/shared-links/me')
.query({ key: linkWithPassword.key, password: 'foo' });
expect(status).toBe(200);
@@ -209,7 +209,7 @@ describe('/shared-link', () => {
});
it('should return metadata for album shared link', async () => {
const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithMetadata.key });
const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithMetadata.key });
expect(status).toBe(200);
expect(body.assets).toHaveLength(1);
@@ -225,7 +225,7 @@ describe('/shared-link', () => {
});
it('should not return metadata for album shared link without metadata', async () => {
const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithoutMetadata.key });
const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithoutMetadata.key });
expect(status).toBe(200);
expect(body.assets).toHaveLength(1);
@@ -239,9 +239,9 @@ describe('/shared-link', () => {
});
});
describe('GET /shared-link/:id', () => {
describe('GET /shared-links/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/shared-link/${linkWithAlbum.id}`);
const { status, body } = await request(app).get(`/shared-links/${linkWithAlbum.id}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -249,7 +249,7 @@ describe('/shared-link', () => {
it('should get shared link by id', async () => {
const { status, body } = await request(app)
.get(`/shared-link/${linkWithAlbum.id}`)
.get(`/shared-links/${linkWithAlbum.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
@@ -264,7 +264,7 @@ describe('/shared-link', () => {
it('should not get shared link by id if user has not created the link or it does not exist', async () => {
const { status, body } = await request(app)
.get(`/shared-link/${linkWithAlbum.id}`)
.get(`/shared-links/${linkWithAlbum.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
@@ -272,10 +272,10 @@ describe('/shared-link', () => {
});
});
describe('POST /shared-link', () => {
describe('POST /shared-links', () => {
it('should require authentication', async () => {
const { status, body } = await request(app)
.post('/shared-link')
.post('/shared-links')
.send({ type: SharedLinkType.Album, albumId: uuidDto.notFound });
expect(status).toBe(401);
@@ -284,7 +284,7 @@ describe('/shared-link', () => {
it('should require a type and the correspondent asset/album id', async () => {
const { status, body } = await request(app)
.post('/shared-link')
.post('/shared-links')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
@@ -293,7 +293,7 @@ describe('/shared-link', () => {
it('should require an asset/album id', async () => {
const { status, body } = await request(app)
.post('/shared-link')
.post('/shared-links')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ type: SharedLinkType.Album });
@@ -303,7 +303,7 @@ describe('/shared-link', () => {
it('should require a valid asset id', async () => {
const { status, body } = await request(app)
.post('/shared-link')
.post('/shared-links')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ type: SharedLinkType.Individual, assetId: uuidDto.notFound });
@@ -313,7 +313,7 @@ describe('/shared-link', () => {
it('should create a shared link', async () => {
const { status, body } = await request(app)
.post('/shared-link')
.post('/shared-links')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ type: SharedLinkType.Album, albumId: album.id });
@@ -327,10 +327,10 @@ describe('/shared-link', () => {
});
});
describe('PATCH /shared-link/:id', () => {
describe('PATCH /shared-links/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app)
.patch(`/shared-link/${linkWithAlbum.id}`)
.patch(`/shared-links/${linkWithAlbum.id}`)
.send({ description: 'foo' });
expect(status).toBe(401);
@@ -339,7 +339,7 @@ describe('/shared-link', () => {
it('should fail if invalid link', async () => {
const { status, body } = await request(app)
.patch(`/shared-link/${uuidDto.notFound}`)
.patch(`/shared-links/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ description: 'foo' });
@@ -349,7 +349,7 @@ describe('/shared-link', () => {
it('should update shared link', async () => {
const { status, body } = await request(app)
.patch(`/shared-link/${linkWithAlbum.id}`)
.patch(`/shared-links/${linkWithAlbum.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ description: 'foo' });
@@ -364,10 +364,10 @@ describe('/shared-link', () => {
});
});
describe('PUT /shared-link/:id/assets', () => {
describe('PUT /shared-links/:id/assets', () => {
it('should not add assets to shared link (album)', async () => {
const { status, body } = await request(app)
.put(`/shared-link/${linkWithAlbum.id}/assets`)
.put(`/shared-links/${linkWithAlbum.id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ assetIds: [asset2.id] });
@@ -377,7 +377,7 @@ describe('/shared-link', () => {
it('should add an assets to a shared link (individual)', async () => {
const { status, body } = await request(app)
.put(`/shared-link/${linkWithAssets.id}/assets`)
.put(`/shared-links/${linkWithAssets.id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ assetIds: [asset2.id] });
@@ -386,10 +386,10 @@ describe('/shared-link', () => {
});
});
describe('DELETE /shared-link/:id/assets', () => {
describe('DELETE /shared-links/:id/assets', () => {
it('should not remove assets from a shared link (album)', async () => {
const { status, body } = await request(app)
.delete(`/shared-link/${linkWithAlbum.id}/assets`)
.delete(`/shared-links/${linkWithAlbum.id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ assetIds: [asset2.id] });
@@ -399,7 +399,7 @@ describe('/shared-link', () => {
it('should remove assets from a shared link (individual)', async () => {
const { status, body } = await request(app)
.delete(`/shared-link/${linkWithAssets.id}/assets`)
.delete(`/shared-links/${linkWithAssets.id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ assetIds: [asset2.id] });
@@ -408,9 +408,9 @@ describe('/shared-link', () => {
});
});
describe('DELETE /shared-link/:id', () => {
describe('DELETE /shared-links/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/shared-link/${linkWithAlbum.id}`);
const { status, body } = await request(app).delete(`/shared-links/${linkWithAlbum.id}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -418,7 +418,7 @@ describe('/shared-link', () => {
it('should fail if invalid link', async () => {
const { status, body } = await request(app)
.delete(`/shared-link/${uuidDto.notFound}`)
.delete(`/shared-links/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
@@ -427,7 +427,7 @@ describe('/shared-link', () => {
it('should delete a shared link', async () => {
const { status } = await request(app)
.delete(`/shared-link/${linkWithAlbum.id}`)
.delete(`/shared-links/${linkWithAlbum.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);

View File

@@ -86,6 +86,26 @@ describe('/system-config', () => {
expect(body).toEqual(errorDto.unauthorized);
});
it('should always return the new config', async () => {
const config = await getSystemConfig(admin.accessToken);
const response1 = await request(app)
.put('/system-config')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ...config, newVersionCheck: { enabled: false } });
expect(response1.status).toBe(200);
expect(response1.body).toEqual({ ...config, newVersionCheck: { enabled: false } });
const response2 = await request(app)
.put('/system-config')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ...config, newVersionCheck: { enabled: true } });
expect(response2.status).toBe(200);
expect(response2.body).toEqual({ ...config, newVersionCheck: { enabled: true } });
});
it('should reject an invalid config entry', async () => {
const { status, body } = await request(app)
.put('/system-config')

View File

@@ -1,4 +1,4 @@
import { LoginResponseDto, getAllAssets } from '@immich/sdk';
import { LoginResponseDto, getAssetInfo, getAssetStatistics } from '@immich/sdk';
import { Socket } from 'socket.io-client';
import { errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils';
@@ -31,17 +31,16 @@ describe('/trash', () => {
const { id: assetId } = await utils.createAsset(admin.accessToken);
await utils.deleteAssets(admin.accessToken, [assetId]);
const before = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) });
expect(before.length).toBeGreaterThanOrEqual(1);
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true }));
const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
await utils.waitForWebsocketEvent({ event: 'assetDelete', id: assetId });
const after = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) });
expect(after.length).toBe(0);
const after = await getAssetStatistics({ isTrashed: true }, { headers: asBearerAuth(admin.accessToken) });
expect(after.total).toBe(0);
});
});
@@ -57,14 +56,14 @@ describe('/trash', () => {
const { id: assetId } = await utils.createAsset(admin.accessToken);
await utils.deleteAssets(admin.accessToken, [assetId]);
const before = await utils.getAssetInfo(admin.accessToken, assetId);
expect(before.isTrashed).toBe(true);
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true }));
const { status } = await request(app).post('/trash/restore').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
const after = await utils.getAssetInfo(admin.accessToken, assetId);
expect(after.isTrashed).toBe(false);
const after = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
expect(after).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: false }));
});
});

View File

@@ -0,0 +1,317 @@
import { LoginResponseDto, deleteUserAdmin, getMyUser, getUserAdmin, login } from '@immich/sdk';
import { Socket } from 'socket.io-client';
import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
describe('/admin/users', () => {
let websocket: Socket;
let admin: LoginResponseDto;
let nonAdmin: LoginResponseDto;
let deletedUser: LoginResponseDto;
let userToDelete: LoginResponseDto;
let userToHardDelete: LoginResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup({ onboarding: false });
[websocket, nonAdmin, deletedUser, userToDelete, userToHardDelete] = await Promise.all([
utils.connectWebsocket(admin.accessToken),
utils.userSetup(admin.accessToken, createUserDto.user1),
utils.userSetup(admin.accessToken, createUserDto.user2),
utils.userSetup(admin.accessToken, createUserDto.user3),
utils.userSetup(admin.accessToken, createUserDto.user4),
]);
await deleteUserAdmin(
{ id: deletedUser.userId, userAdminDeleteDto: {} },
{ headers: asBearerAuth(admin.accessToken) },
);
});
afterAll(() => {
utils.disconnectWebsocket(websocket);
});
describe('GET /admin/users', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/admin/users`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const { status, body } = await request(app)
.get(`/admin/users`)
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should hide deleted users by default', async () => {
const { status, body } = await request(app)
.get(`/admin/users`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(4);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ email: admin.userEmail }),
expect.objectContaining({ email: nonAdmin.userEmail }),
expect.objectContaining({ email: userToDelete.userEmail }),
expect.objectContaining({ email: userToHardDelete.userEmail }),
]),
);
});
it('should include deleted users', async () => {
const { status, body } = await request(app)
.get(`/admin/users?withDeleted=true`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(5);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ email: admin.userEmail }),
expect.objectContaining({ email: nonAdmin.userEmail }),
expect.objectContaining({ email: userToDelete.userEmail }),
expect.objectContaining({ email: userToHardDelete.userEmail }),
expect.objectContaining({ email: deletedUser.userEmail }),
]),
);
});
});
describe('POST /admin/users', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/admin/users`).send(createUserDto.user1);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const { status, body } = await request(app)
.post(`/admin/users`)
.set('Authorization', `Bearer ${nonAdmin.accessToken}`)
.send(createUserDto.user1);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
for (const key of [
'password',
'email',
'name',
'quotaSizeInBytes',
'shouldChangePassword',
'memoriesEnabled',
'notify',
]) {
it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app)
.post(`/admin/users`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ...createUserDto.user1, [key]: null });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
}
it('should ignore `isAdmin`', async () => {
const { status, body } = await request(app)
.post(`/admin/users`)
.send({
isAdmin: true,
email: 'user5@immich.cloud',
password: 'password123',
name: 'Immich',
})
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toMatchObject({
email: 'user5@immich.cloud',
isAdmin: false,
shouldChangePassword: true,
});
expect(status).toBe(201);
});
it('should create a user without memories enabled', async () => {
const { status, body } = await request(app)
.post(`/admin/users`)
.send({
email: 'no-memories@immich.cloud',
password: 'Password123',
name: 'No Memories',
memoriesEnabled: false,
})
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toMatchObject({
email: 'no-memories@immich.cloud',
memoriesEnabled: false,
});
expect(status).toBe(201);
});
});
describe('PUT /admin/users/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/admin/users/${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const { status, body } = await request(app)
.put(`/admin/users/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
for (const key of ['password', 'email', 'name', 'shouldChangePassword', 'memoriesEnabled']) {
it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app)
.put(`/admin/users/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ [key]: null });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
}
it('should not allow a non-admin to become an admin', async () => {
const { status, body } = await request(app)
.put(`/admin/users/${nonAdmin.userId}`)
.send({ isAdmin: true })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ isAdmin: false });
});
it('ignores updates to profileImagePath', async () => {
const { status, body } = await request(app)
.put(`/admin/users/${admin.userId}`)
.send({ profileImagePath: 'invalid.jpg' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ id: admin.userId, profileImagePath: '' });
});
it('should update first and last name', async () => {
const before = await getUserAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
const { status, body } = await request(app)
.put(`/admin/users/${admin.userId}`)
.send({ name: 'Name' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
...before,
updatedAt: expect.any(String),
name: 'Name',
});
expect(before.updatedAt).not.toEqual(body.updatedAt);
});
it('should update memories enabled', async () => {
const before = await getUserAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
const { status, body } = await request(app)
.put(`/admin/users/${admin.userId}`)
.send({ memoriesEnabled: false })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
...before,
updatedAt: expect.anything(),
memoriesEnabled: false,
});
expect(before.updatedAt).not.toEqual(body.updatedAt);
});
it('should update password', async () => {
const { status, body } = await request(app)
.put(`/admin/users/${nonAdmin.userId}`)
.send({ password: 'super-secret' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ email: nonAdmin.userEmail });
const token = await login({ loginCredentialDto: { email: nonAdmin.userEmail, password: 'super-secret' } });
expect(token.accessToken).toBeDefined();
const user = await getMyUser({ headers: asBearerAuth(token.accessToken) });
expect(user).toMatchObject({ email: nonAdmin.userEmail });
});
});
describe('DELETE /admin/users/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/admin/users/${userToDelete.userId}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const { status, body } = await request(app)
.delete(`/admin/users/${userToDelete.userId}`)
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should delete user', async () => {
const { status, body } = await request(app)
.delete(`/admin/users/${userToDelete.userId}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: userToDelete.userId,
updatedAt: expect.any(String),
deletedAt: expect.any(String),
});
});
it('should hard delete a user', async () => {
const { status, body } = await request(app)
.delete(`/admin/users/${userToHardDelete.userId}`)
.send({ force: true })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: userToHardDelete.userId,
updatedAt: expect.any(String),
deletedAt: expect.any(String),
});
await utils.waitForWebsocketEvent({ event: 'userDelete', id: userToHardDelete.userId, timeout: 5000 });
});
});
describe('POST /admin/users/:id/restore', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/admin/users/${userToDelete.userId}/restore`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const { status, body } = await request(app)
.post(`/admin/users/${userToDelete.userId}/restore`)
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
});
});

View File

@@ -1,288 +1,105 @@
import { LoginResponseDto, deleteUser, getUserById } from '@immich/sdk';
import { Socket } from 'socket.io-client';
import { createUserDto, userDto } from 'src/fixtures';
import { LoginResponseDto, SharedLinkType, deleteUserAdmin, getMyUser, login } from '@immich/sdk';
import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
describe('/user', () => {
let websocket: Socket;
import { beforeAll, describe, expect, it } from 'vitest';
describe('/users', () => {
let admin: LoginResponseDto;
let deletedUser: LoginResponseDto;
let userToDelete: LoginResponseDto;
let userToHardDelete: LoginResponseDto;
let nonAdmin: LoginResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup({ onboarding: false });
[websocket, deletedUser, nonAdmin, userToDelete, userToHardDelete] = await Promise.all([
utils.connectWebsocket(admin.accessToken),
[deletedUser, nonAdmin] = await Promise.all([
utils.userSetup(admin.accessToken, createUserDto.user1),
utils.userSetup(admin.accessToken, createUserDto.user2),
utils.userSetup(admin.accessToken, createUserDto.user3),
utils.userSetup(admin.accessToken, createUserDto.user4),
]);
await deleteUser({ id: deletedUser.userId, deleteUserDto: {} }, { headers: asBearerAuth(admin.accessToken) });
await deleteUserAdmin(
{ id: deletedUser.userId, userAdminDeleteDto: {} },
{ headers: asBearerAuth(admin.accessToken) },
);
});
afterAll(() => {
utils.disconnectWebsocket(websocket);
});
describe('GET /user', () => {
describe('GET /users', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/user');
const { status, body } = await request(app).get('/users');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should get users', async () => {
const { status, body } = await request(app).get('/user').set('Authorization', `Bearer ${admin.accessToken}`);
const { status, body } = await request(app).get('/users').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(200);
expect(body).toHaveLength(5);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ email: 'admin@immich.cloud' }),
expect.objectContaining({ email: 'user1@immich.cloud' }),
expect.objectContaining({ email: 'user2@immich.cloud' }),
expect.objectContaining({ email: 'user3@immich.cloud' }),
expect.objectContaining({ email: 'user4@immich.cloud' }),
]),
);
});
it('should hide deleted users', async () => {
const { status, body } = await request(app)
.get(`/user`)
.query({ isAll: true })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(4);
expect(body).toHaveLength(2);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ email: 'admin@immich.cloud' }),
expect.objectContaining({ email: 'user2@immich.cloud' }),
expect.objectContaining({ email: 'user3@immich.cloud' }),
expect.objectContaining({ email: 'user4@immich.cloud' }),
]),
);
});
it('should include deleted users', async () => {
const { status, body } = await request(app)
.get(`/user`)
.query({ isAll: false })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(5);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ email: 'admin@immich.cloud' }),
expect.objectContaining({ email: 'user1@immich.cloud' }),
expect.objectContaining({ email: 'user2@immich.cloud' }),
expect.objectContaining({ email: 'user3@immich.cloud' }),
expect.objectContaining({ email: 'user4@immich.cloud' }),
]),
);
});
});
describe('GET /user/info/:id', () => {
describe('GET /users/me', () => {
it('should require authentication', async () => {
const { status } = await request(app).get(`/user/info/${admin.userId}`);
expect(status).toEqual(401);
const { status, body } = await request(app).get(`/users/me`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should get the user info', async () => {
const { status, body } = await request(app)
.get(`/user/info/${admin.userId}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
it('should not work for shared links', async () => {
const album = await utils.createAlbum(admin.accessToken, { albumName: 'Album' });
const sharedLink = await utils.createSharedLink(admin.accessToken, {
type: SharedLinkType.Album,
albumId: album.id,
});
const { status, body } = await request(app).get(`/users/me?key=${sharedLink.key}`);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should get my user', async () => {
const { status, body } = await request(app).get(`/users/me`).set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: admin.userId,
email: 'admin@immich.cloud',
memoriesEnabled: true,
quotaUsageInBytes: 0,
});
});
});
describe('GET /user/me', () => {
describe('PUT /users/me', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/user/me`);
const { status, body } = await request(app).put(`/users/me`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should get my info', async () => {
const { status, body } = await request(app).get(`/user/me`).set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: admin.userId,
email: 'admin@immich.cloud',
});
});
});
describe('POST /user', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/user`).send(createUserDto.user1);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
for (const key of Object.keys(createUserDto.user1)) {
for (const key of ['email', 'name', 'memoriesEnabled', 'avatarColor']) {
it(`should not allow null ${key}`, async () => {
const dto = { [key]: null };
const { status, body } = await request(app)
.post(`/user`)
.put(`/users/me`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ...createUserDto.user1, [key]: null });
.send(dto);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
}
it('should ignore `isAdmin`', async () => {
const { status, body } = await request(app)
.post(`/user`)
.send({
isAdmin: true,
email: 'user5@immich.cloud',
password: 'password123',
name: 'Immich',
})
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toMatchObject({
email: 'user5@immich.cloud',
isAdmin: false,
shouldChangePassword: true,
});
expect(status).toBe(201);
});
it('should create a user without memories enabled', async () => {
const { status, body } = await request(app)
.post(`/user`)
.send({
email: 'no-memories@immich.cloud',
password: 'Password123',
name: 'No Memories',
memoriesEnabled: false,
})
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toMatchObject({
email: 'no-memories@immich.cloud',
memoriesEnabled: false,
});
expect(status).toBe(201);
});
});
describe('DELETE /user/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/user/${userToDelete.userId}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should delete user', async () => {
const { status, body } = await request(app)
.delete(`/user/${userToDelete.userId}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: userToDelete.userId,
updatedAt: expect.any(String),
deletedAt: expect.any(String),
});
});
it('should hard delete user', async () => {
const { status, body } = await request(app)
.delete(`/user/${userToHardDelete.userId}`)
.send({ force: true })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: userToHardDelete.userId,
updatedAt: expect.any(String),
deletedAt: expect.any(String),
});
await utils.waitForWebsocketEvent({ event: 'userDelete', id: userToHardDelete.userId, timeout: 5000 });
});
});
describe('PUT /user', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/user`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
for (const key of Object.keys(userDto.admin)) {
it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app)
.put(`/user`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ...userDto.admin, [key]: null });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
}
it('should not allow a non-admin to become an admin', async () => {
const { status, body } = await request(app)
.put(`/user`)
.send({ isAdmin: true, id: nonAdmin.userId })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.alreadyHasAdmin);
});
it('ignores updates to profileImagePath', async () => {
const { status, body } = await request(app)
.put(`/user`)
.send({ id: admin.userId, profileImagePath: 'invalid.jpg' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ id: admin.userId, profileImagePath: '' });
});
it('should ignore updates to createdAt, updatedAt and deletedAt', async () => {
const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
const { status, body } = await request(app)
.put(`/user`)
.send({
id: admin.userId,
createdAt: '2023-01-01T00:00:00.000Z',
updatedAt: '2023-01-01T00:00:00.000Z',
deletedAt: '2023-01-01T00:00:00.000Z',
})
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toStrictEqual(before);
});
it('should update first and last name', async () => {
const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
const before = await getMyUser({ headers: asBearerAuth(admin.accessToken) });
const { status, body } = await request(app)
.put(`/user`)
.send({
id: admin.userId,
name: 'Name',
})
.put(`/users/me`)
.send({ name: 'Name' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
@@ -291,17 +108,13 @@ describe('/user', () => {
updatedAt: expect.any(String),
name: 'Name',
});
expect(before.updatedAt).not.toEqual(body.updatedAt);
});
it('should update memories enabled', async () => {
const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
const before = await getMyUser({ headers: asBearerAuth(admin.accessToken) });
const { status, body } = await request(app)
.put(`/user`)
.send({
id: admin.userId,
memoriesEnabled: false,
})
.put(`/users/me`)
.send({ memoriesEnabled: false })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
@@ -310,7 +123,80 @@ describe('/user', () => {
updatedAt: expect.anything(),
memoriesEnabled: false,
});
expect(before.updatedAt).not.toEqual(body.updatedAt);
const after = await getMyUser({ headers: asBearerAuth(admin.accessToken) });
expect(after.memoriesEnabled).toBe(false);
});
/** @deprecated */
it('should allow a user to change their password (deprecated)', async () => {
const user = await getMyUser({ headers: asBearerAuth(nonAdmin.accessToken) });
expect(user.shouldChangePassword).toBe(true);
const { status, body } = await request(app)
.put(`/users/me`)
.send({ password: 'super-secret' })
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
email: nonAdmin.userEmail,
shouldChangePassword: false,
});
const token = await login({ loginCredentialDto: { email: nonAdmin.userEmail, password: 'super-secret' } });
expect(token.accessToken).toBeDefined();
});
it('should not allow user to change to a taken email', async () => {
const { status, body } = await request(app)
.put(`/users/me`)
.send({ email: 'admin@immich.cloud' })
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(400);
expect(body).toMatchObject(errorDto.badRequest('Email already in use by another account'));
});
it('should update my email', async () => {
const before = await getMyUser({ headers: asBearerAuth(nonAdmin.accessToken) });
const { status, body } = await request(app)
.put(`/users/me`)
.send({ email: 'non-admin@immich.cloud' })
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
...before,
email: 'non-admin@immich.cloud',
updatedAt: expect.anything(),
});
});
});
describe('GET /users/:id', () => {
it('should require authentication', async () => {
const { status } = await request(app).get(`/users/${admin.userId}`);
expect(status).toEqual(401);
});
it('should get the user', async () => {
const { status, body } = await request(app)
.get(`/users/${admin.userId}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: admin.userId,
email: 'admin@immich.cloud',
});
expect(body).not.toMatchObject({
shouldChangePassword: expect.anything(),
memoriesEnabled: expect.anything(),
storageLabel: expect.anything(),
});
});
});
});

View File

@@ -1,4 +1,4 @@
import { LoginResponseDto, getAllAlbums, getAllAssets } from '@immich/sdk';
import { LoginResponseDto, getAllAlbums, getAssetStatistics } from '@immich/sdk';
import { readFileSync } from 'node:fs';
import { mkdir, readdir, rm, symlink } from 'node:fs/promises';
import { asKeyAuth, immichCli, testAssetDir, utils } from 'src/utils';
@@ -28,8 +28,8 @@ describe(`immich upload`, () => {
);
expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(1);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(1);
});
it('should skip a duplicate file', async () => {
@@ -40,8 +40,8 @@ describe(`immich upload`, () => {
);
expect(first.exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(1);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(1);
const second = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]);
expect(second.stderr).toBe('');
@@ -60,8 +60,8 @@ describe(`immich upload`, () => {
expect(stdout.split('\n')).toEqual(expect.arrayContaining([expect.stringContaining('No files found, exiting')]));
expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(0);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(0);
});
it('should have accurate dry run', async () => {
@@ -76,8 +76,8 @@ describe(`immich upload`, () => {
);
expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(0);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(0);
});
it('dry run should handle duplicates', async () => {
@@ -88,8 +88,8 @@ describe(`immich upload`, () => {
);
expect(first.exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(1);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(1);
const second = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--dry-run']);
expect(second.stderr).toBe('');
@@ -112,8 +112,8 @@ describe(`immich upload`, () => {
);
expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(9);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(9);
});
});
@@ -135,8 +135,8 @@ describe(`immich upload`, () => {
expect(stderr).toBe('');
expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(9);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(9);
const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
expect(albums.length).toBe(1);
@@ -151,8 +151,8 @@ describe(`immich upload`, () => {
expect(response1.stderr).toBe('');
expect(response1.exitCode).toBe(0);
const assets1 = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets1.length).toBe(9);
const assets1 = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets1.total).toBe(9);
const albums1 = await getAllAlbums({}, { headers: asKeyAuth(key) });
expect(albums1.length).toBe(0);
@@ -167,8 +167,8 @@ describe(`immich upload`, () => {
expect(response2.stderr).toBe('');
expect(response2.exitCode).toBe(0);
const assets2 = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets2.length).toBe(9);
const assets2 = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets2.total).toBe(9);
const albums2 = await getAllAlbums({}, { headers: asKeyAuth(key) });
expect(albums2.length).toBe(1);
@@ -193,8 +193,8 @@ describe(`immich upload`, () => {
expect(stderr).toBe('');
expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(0);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(0);
const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
expect(albums.length).toBe(0);
@@ -219,8 +219,8 @@ describe(`immich upload`, () => {
expect(stderr).toBe('');
expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(9);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(9);
const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
expect(albums.length).toBe(1);
@@ -245,8 +245,8 @@ describe(`immich upload`, () => {
expect(stderr).toBe('');
expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(0);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(0);
const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
expect(albums.length).toBe(0);
@@ -276,8 +276,8 @@ describe(`immich upload`, () => {
expect(stderr).toBe('');
expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(9);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(9);
});
it('should have accurate dry run', async () => {
@@ -302,8 +302,8 @@ describe(`immich upload`, () => {
expect(stderr).toBe('');
expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(0);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(0);
});
});
@@ -328,8 +328,8 @@ describe(`immich upload`, () => {
);
expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(1);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(1);
});
it('should throw an error if attempting dry run', async () => {
@@ -344,8 +344,8 @@ describe(`immich upload`, () => {
expect(stderr).toEqual(`error: option '-n, --dry-run' cannot be used with option '-h, --skip-hash'`);
expect(exitCode).not.toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(0);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(0);
});
});
@@ -367,8 +367,8 @@ describe(`immich upload`, () => {
);
expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(9);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(9);
});
it('should reject string argument', async () => {
@@ -408,8 +408,8 @@ describe(`immich upload`, () => {
);
expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(8);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(8);
});
it('should ignore assets matching glob pattern', async () => {
@@ -429,8 +429,8 @@ describe(`immich upload`, () => {
);
expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(1);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(1);
});
it('should have accurate dry run', async () => {
@@ -451,8 +451,8 @@ describe(`immich upload`, () => {
);
expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(0);
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.total).toBe(0);
});
});
});

View File

@@ -51,11 +51,6 @@ export const errorDto = {
statusCode: 400,
message: 'The server already has an admin',
},
noDeleteUploadLibrary: {
error: 'Bad Request',
statusCode: 400,
message: 'Cannot delete the last upload library',
},
};
export const signupResponseDto = {

View File

@@ -17,7 +17,7 @@ const setup = async () => {
child.stdout.on('data', (data) => {
const input = data.toString();
console.log(input);
if (input.includes('Immich Microservices is listening')) {
if (input.includes('Immich Microservices is running')) {
_resolve();
}
});

View File

@@ -5,25 +5,25 @@ import {
CreateAlbumDto,
CreateAssetDto,
CreateLibraryDto,
CreateUserDto,
MetadataSearchDto,
PersonCreateDto,
SharedLinkCreateDto,
UserAdminCreateDto,
ValidateLibraryDto,
createAlbum,
createApiKey,
createLibrary,
createPartner,
createPerson,
createSharedLink,
createUser,
defaults,
createUserAdmin,
deleteAssets,
getAllAssets,
getAllJobsStatus,
getAssetInfo,
getConfigDefaults,
login,
searchMetadata,
setBaseUrl,
signUpAdmin,
updateAdminOnboarding,
updateAlbumUser,
@@ -56,8 +56,8 @@ const baseUrl = 'http://127.0.0.1:2283';
export const shareUrl = `${baseUrl}/share`;
export const app = `${baseUrl}/api`;
// TODO move test assets into e2e/assets
export const testAssetDir = path.resolve(`./../server/test/assets/`);
export const testAssetDirInternal = '/data/assets';
export const testAssetDir = path.resolve('./test-assets');
export const testAssetDirInternal = '/test-assets';
export const tempDir = tmpdir();
export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}` });
export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
@@ -145,7 +145,6 @@ export const utils = {
'sessions',
'users',
'system_metadata',
'system_config',
];
const sql: string[] = [];
@@ -256,8 +255,8 @@ export const utils = {
});
},
setApiEndpoint: () => {
defaults.baseUrl = app;
initSdk: () => {
setBaseUrl(app);
},
adminSetup: async (options?: AdminSetupOptions) => {
@@ -274,8 +273,8 @@ export const utils = {
return response;
},
userSetup: async (accessToken: string, dto: CreateUserDto) => {
await createUser({ createUserDto: dto }, { headers: asBearerAuth(accessToken) });
userSetup: async (accessToken: string, dto: UserAdminCreateDto) => {
await createUserAdmin({ userAdminCreateDto: dto }, { headers: asBearerAuth(accessToken) });
return login({
loginCredentialDto: { email: dto.email, password: dto.password },
});
@@ -341,8 +340,6 @@ export const utils = {
getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
getAllAssets: (accessToken: string) => getAllAssets({}, { headers: asBearerAuth(accessToken) }),
metadataSearch: async (accessToken: string, dto: MetadataSearchDto) => {
return searchMetadata({ metadataSearchDto: dto }, { headers: asBearerAuth(accessToken) });
},
@@ -389,6 +386,8 @@ export const utils = {
validateLibrary: (accessToken: string, id: string, dto: ValidateLibraryDto) =>
validate({ id, validateLibraryDto: dto }, { headers: asBearerAuth(accessToken) }),
createPartner: (accessToken: string, id: string) => createPartner({ id }, { headers: asBearerAuth(accessToken) }),
setAuthCookies: async (context: BrowserContext, accessToken: string) =>
await context.addCookies([
{
@@ -463,7 +462,7 @@ export const utils = {
},
};
utils.setApiEndpoint();
utils.initSdk();
if (!existsSync(`${testAssetDir}/albums`)) {
throw new Error(

View File

@@ -0,0 +1,60 @@
import { AssetFileUploadResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk';
import { expect, test } from '@playwright/test';
import { utils } from 'src/utils';
test.describe('Detail Panel', () => {
let admin: LoginResponseDto;
let asset: AssetFileUploadResponseDto;
test.beforeAll(async () => {
utils.initSdk();
await utils.resetDatabase();
admin = await utils.adminSetup();
asset = await utils.createAsset(admin.accessToken);
});
test('can be opened for shared links', async ({ page }) => {
const sharedLink = await utils.createSharedLink(admin.accessToken, {
type: SharedLinkType.Individual,
assetIds: [asset.id],
});
await page.goto(`/share/${sharedLink.key}/photos/${asset.id}`);
await page.waitForSelector('#immich-asset-viewer');
await expect(page.getByRole('button', { name: 'Info' })).toBeVisible();
await page.keyboard.press('i');
await expect(page.locator('#detail-panel')).toBeVisible();
await page.keyboard.press('i');
await expect(page.locator('#detail-panel')).toHaveCount(0);
});
test('cannot be opened for shared links with hidden metadata', async ({ page }) => {
const sharedLink = await utils.createSharedLink(admin.accessToken, {
type: SharedLinkType.Individual,
assetIds: [asset.id],
showMetadata: false,
});
await page.goto(`/share/${sharedLink.key}/photos/${asset.id}`);
await page.waitForSelector('#immich-asset-viewer');
await expect(page.getByRole('button', { name: 'Info' })).toHaveCount(0);
await page.keyboard.press('i');
await expect(page.locator('#detail-panel')).toHaveCount(0);
await page.keyboard.press('i');
await expect(page.locator('#detail-panel')).toHaveCount(0);
});
test('description is visible for owner on shared links', async ({ context, page }) => {
const sharedLink = await utils.createSharedLink(admin.accessToken, {
type: SharedLinkType.Individual,
assetIds: [asset.id],
});
await utils.setAuthCookies(context, admin.accessToken);
await page.goto(`/share/${sharedLink.key}/photos/${asset.id}`);
const textarea = page.getByRole('textbox', { name: 'Add a description' });
await page.getByRole('button', { name: 'Info' }).click();
await expect(textarea).toBeVisible();
await expect(textarea).not.toBeDisabled();
});
});

View File

@@ -0,0 +1,52 @@
import { AssetFileUploadResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk';
import { expect, test } from '@playwright/test';
import { utils } from 'src/utils';
test.describe('Asset Viewer Navbar', () => {
let admin: LoginResponseDto;
let asset: AssetFileUploadResponseDto;
test.beforeAll(async () => {
utils.initSdk();
await utils.resetDatabase();
admin = await utils.adminSetup();
asset = await utils.createAsset(admin.accessToken);
});
test.describe('shared link without metadata', () => {
test('visible guest actions', async ({ page }) => {
const sharedLink = await utils.createSharedLink(admin.accessToken, {
type: SharedLinkType.Individual,
assetIds: [asset.id],
showMetadata: false,
});
await page.goto(`/share/${sharedLink.key}/photos/${asset.id}`);
await page.waitForSelector('#immich-asset-viewer');
const expected = ['Zoom Image', 'Copy Image', 'Download'];
const buttons = await page.getByTestId('asset-viewer-navbar-actions').getByRole('button').all();
for (const [i, button] of buttons.entries()) {
await expect(button).toHaveAccessibleName(expected[i]);
}
});
test('visible owner actions', async ({ context, page }) => {
const sharedLink = await utils.createSharedLink(admin.accessToken, {
type: SharedLinkType.Individual,
assetIds: [asset.id],
showMetadata: false,
});
await utils.setAuthCookies(context, admin.accessToken);
await page.goto(`/share/${sharedLink.key}/photos/${asset.id}`);
await page.waitForSelector('#immich-asset-viewer');
const expected = ['Share', 'Zoom Image', 'Copy Image', 'Download'];
const buttons = await page.getByTestId('asset-viewer-navbar-actions').getByRole('button').all();
for (const [i, button] of buttons.entries()) {
await expect(button).toHaveAccessibleName(expected[i]);
}
});
});
});

View File

@@ -3,7 +3,7 @@ import { utils } from 'src/utils';
test.describe('Registration', () => {
test.beforeAll(() => {
utils.setApiEndpoint();
utils.initSdk();
});
test.beforeEach(async () => {

View File

@@ -17,7 +17,7 @@ test.describe('Shared Links', () => {
let sharedLinkPassword: SharedLinkResponseDto;
test.beforeAll(async () => {
utils.setApiEndpoint();
utils.initSdk();
await utils.resetDatabase();
admin = await utils.adminSetup();
asset = await utils.createAsset(admin.accessToken);

View File

@@ -2,14 +2,15 @@
set -o nounset
set -o pipefail
create_immich_directory() { local -r Tgt='./immich-app'
create_immich_directory() {
local -r Tgt='./immich-app'
echo "Creating Immich directory..."
if [[ -e $Tgt ]]; then
echo "Found existing directory $Tgt, will overwrite YAML files"
else
mkdir "$Tgt" || return
fi
cd "$Tgt" || return
fi
cd "$Tgt" || return 1
}
download_docker_compose_file() {
@@ -22,6 +23,16 @@ download_dot_env_file() {
"${Curl[@]}" "$RepoUrl"/example.env -o ./.env
}
generate_random_password() {
echo "Generate random password for .env file..."
rand_pass=$(echo "$RANDOM$(date)$RANDOM" | sha256sum | base64 | head -c10)
if [ -z "$rand_pass" ]; then
sed -i -e "s/DB_PASSWORD=postgres/DB_PASSWORD=postgres${RANDOM}${RANDOM}/" ./.env
else
sed -i -e "s/DB_PASSWORD=postgres/DB_PASSWORD=${rand_pass}/" ./.env
fi
}
start_docker_compose() {
echo "Starting Immich's docker containers"
@@ -40,16 +51,16 @@ start_docker_compose() {
show_friendly_message() {
local ip_address
ip_address=$(hostname -I | awk '{print $1}')
cat << EOF
cat <<EOF
Successfully deployed Immich!
You can access the website at http://$ip_address:2283 and the server URL for the mobile app is http://$ip_address:2283/api
---------------------------------------------------
If you want to configure custom information of the server, including the database, Redis information, or the backup (or upload) location, etc.
1. First bring down the containers with the command 'docker compose down' in the immich-app directory,
2. Then change the information that fits your needs in the '.env' file,
If you want to configure custom information of the server, including the database, Redis information, or the backup (or upload) location, etc.
1. First bring down the containers with the command 'docker compose down' in the immich-app directory,
2. Then change the information that fits your needs in the '.env' file,
3. Finally, bring the containers back up with the command 'docker compose up --remove-orphans -d' in the immich-app directory
EOF
}
@@ -66,11 +77,25 @@ main() {
return 14
fi
create_immich_directory || { echo 'error creating Immich directory'; return 10; }
download_docker_compose_file || { echo 'error downloading Docker Compose file'; return 11; }
download_dot_env_file || { echo 'error downloading .env'; return 12; }
start_docker_compose || { echo 'error starting Docker'; return 13; }
return 0; }
create_immich_directory || {
echo 'error creating Immich directory'
return 10
}
download_docker_compose_file || {
echo 'error downloading Docker Compose file'
return 11
}
download_dot_env_file || {
echo 'error downloading .env'
return 12
}
generate_random_password
start_docker_compose || {
echo 'error starting Docker'
return 13
}
return 0
}
main
Exit=$?

View File

@@ -30,6 +30,8 @@ download:
locale_code: pl-PL
- file: mobile/assets/i18n/fi-FI.json
locale_code: fi-FI
- file: mobile/assets/i18n/pt-BR.json
locale_code: pt-BR
- file: mobile/assets/i18n/pt-PT.json
locale_code: pt-PT
- file: mobile/assets/i18n/pt-BR.json

View File

@@ -1,6 +1,6 @@
ARG DEVICE=cpu
FROM python:3.11-bookworm@sha256:adc02660aaaa7e7b482a2689237c5fa88d5b2218aebfa003f6af8fa7c8113378 as builder-cpu
FROM python:3.11-bookworm@sha256:96de1ea4821d73fd2c1853d1fdc3cf794ccfe2fae4c3f08579e846de51760a61 as builder-cpu
FROM openvino/ubuntu22_runtime:2023.3.0@sha256:176646df619032ea6c10faf842867119c393e7497b7f88b5e307e932a0fd5aa8 as builder-openvino
USER root
@@ -36,10 +36,13 @@ RUN python3 -m venv /opt/venv
COPY poetry.lock pyproject.toml ./
RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev
FROM python:3.11-slim-bookworm@sha256:6d2502238109c929569ae99355e28890c438cb11bc88ef02cd189c173b3db07c as prod-cpu
FROM python:3.11-slim-bookworm@sha256:fc39d2e68b554c3f0a5cb8a776280c0b3d73b4c04b83dbade835e2a171ca27ef as prod-cpu
FROM openvino/ubuntu22_runtime:2023.3.0@sha256:176646df619032ea6c10faf842867119c393e7497b7f88b5e307e932a0fd5aa8 as prod-openvino
USER root
# TODO: remove this once the image has the fix for https://github.com/intel/compute-runtime/issues/710
ENV NEOReadDebugKeys=1 \
OverrideGpuAddressSpace=48
FROM nvidia/cuda:12.2.2-cudnn8-runtime-ubuntu22.04@sha256:2d913b09e6be8387e1a10976933642c73c840c0b735f0bf3c28d97fc9bc422e0 as prod-cuda
@@ -74,8 +77,7 @@ RUN apt-get update && \
rm -rf /var/lib/apt/lists/*
WORKDIR /usr/src/app
ENV NODE_ENV=production \
TRANSFORMERS_CACHE=/cache \
ENV TRANSFORMERS_CACHE=/cache \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PATH="/opt/venv/bin:$PATH" \
@@ -93,3 +95,5 @@ COPY start.sh log_conf.json ./
COPY app .
ENTRYPOINT ["tini", "--"]
CMD ["./start.sh"]
HEALTHCHECK CMD python3 healthcheck.py

View File

@@ -41,7 +41,7 @@ class Settings(BaseSettings):
class LogSettings(BaseSettings):
log_level: str = "info"
immich_log_level: str = "info"
no_color: bool = False
class Config:
@@ -77,7 +77,7 @@ LOG_LEVELS: dict[str, int] = {
settings = Settings()
log_settings = LogSettings()
LOG_LEVEL = LOG_LEVELS.get(log_settings.log_level.lower(), logging.INFO)
LOG_LEVEL = LOG_LEVELS.get(log_settings.immich_log_level.lower(), logging.INFO)
class CustomRichHandler(RichHandler):

View File

@@ -0,0 +1,14 @@
import os
import sys
import requests
port = os.getenv("IMMICH_PORT", 3003)
try:
response = requests.get(f"http://localhost:{port}/ping", timeout=2)
if response.status_code == 200:
sys.exit(0)
sys.exit(1)
except requests.RequestException:
sys.exit(1)

View File

@@ -1,7 +1,6 @@
FROM mambaorg/micromamba:bookworm-slim@sha256:f4c5c3c7c1d58556b32cdc09d1d8e721d8ee42cdbdf1d963ddafca1db9a0a57e as builder
FROM mambaorg/micromamba:bookworm-slim@sha256:d5b82811074b396275ef69aadbf31098257dd8836e231371e9cdb393128e571c as builder
ENV NODE_ENV=production \
TRANSFORMERS_CACHE=/cache \
ENV TRANSFORMERS_CACHE=/cache \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PATH="/opt/venv/bin:$PATH" \

View File

@@ -679,14 +679,14 @@ files = [
test = ["pytest (>=6)"]
[[package]]
name = "fastapi"
version = "0.110.3"
name = "fastapi-slim"
version = "0.111.0"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false
python-versions = ">=3.8"
files = [
{file = "fastapi-0.110.3-py3-none-any.whl", hash = "sha256:fd7600612f755e4050beb74001310b5a7e1796d149c2ee363124abdfa0289d32"},
{file = "fastapi-0.110.3.tar.gz", hash = "sha256:555700b0159379e94fdbfc6bb66a0f1c43f4cf7060f25239af3d84b63a656626"},
{file = "fastapi_slim-0.111.0-py3-none-any.whl", hash = "sha256:6e4b04a555496e5a2590031fcae3ef8e364ad4901b340033e2e1d8136471aca2"},
{file = "fastapi_slim-0.111.0.tar.gz", hash = "sha256:100720e4362ec4de97dee83a579b970e79fb5bf48073b37c9ce9b0e63dda4bec"},
]
[package.dependencies]
@@ -695,7 +695,8 @@ starlette = ">=0.37.2,<0.38.0"
typing-extensions = ">=4.8.0"
[package.extras]
all = ["email_validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
all = ["email_validator (>=2.0.0)", "fastapi-cli (>=0.0.2)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
standard = ["email_validator (>=2.0.0)", "fastapi-cli (>=0.0.2)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "python-multipart (>=0.0.7)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
[[package]]
name = "filelock"
@@ -737,13 +738,13 @@ dotenv = ["python-dotenv"]
[[package]]
name = "flask-cors"
version = "4.0.0"
version = "4.0.1"
description = "A Flask extension adding a decorator for CORS support"
optional = false
python-versions = "*"
files = [
{file = "Flask-Cors-4.0.0.tar.gz", hash = "sha256:f268522fcb2f73e2ecdde1ef45e2fd5c71cc48fe03cffb4b441c6d1b40684eb0"},
{file = "Flask_Cors-4.0.0-py2.py3-none-any.whl", hash = "sha256:bc3492bfd6368d27cfe79c7821df5a8a319e1a6d5eab277a3794be19bdc51783"},
{file = "Flask_Cors-4.0.1-py2.py3-none-any.whl", hash = "sha256:f2a704e4458665580c074b714c4627dd5a306b333deb9074d0b1794dfa2fb677"},
{file = "flask_cors-4.0.1.tar.gz", hash = "sha256:eeb69b342142fdbf4766ad99357a7f3876a2ceb77689dc10ff912aac06c389e4"},
]
[package.dependencies]
@@ -1235,13 +1236,13 @@ socks = ["socksio (==1.*)"]
[[package]]
name = "huggingface-hub"
version = "0.22.2"
version = "0.23.0"
description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
optional = false
python-versions = ">=3.8.0"
files = [
{file = "huggingface_hub-0.22.2-py3-none-any.whl", hash = "sha256:3429e25f38ccb834d310804a3b711e7e4953db5a9e420cc147a5e194ca90fd17"},
{file = "huggingface_hub-0.22.2.tar.gz", hash = "sha256:32e9a9a6843c92f253ff9ca16b9985def4d80a93fb357af5353f770ef74a81be"},
{file = "huggingface_hub-0.23.0-py3-none-any.whl", hash = "sha256:075c30d48ee7db2bba779190dc526d2c11d422aed6f9044c5e2fdc2c432fdb91"},
{file = "huggingface_hub-0.23.0.tar.gz", hash = "sha256:7126dedd10a4c6fac796ced4d87a8cf004efc722a5125c2c09299017fa366fa9"},
]
[package.dependencies]
@@ -1254,16 +1255,16 @@ tqdm = ">=4.42.1"
typing-extensions = ">=3.7.4.3"
[package.extras]
all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "minijinja (>=1.0)", "mypy (==1.5.1)", "numpy", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.3.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"]
all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio", "jedi", "minijinja (>=1.0)", "mypy (==1.5.1)", "numpy", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.3.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"]
cli = ["InquirerPy (==0.3.4)"]
dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "minijinja (>=1.0)", "mypy (==1.5.1)", "numpy", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.3.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"]
dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio", "jedi", "minijinja (>=1.0)", "mypy (==1.5.1)", "numpy", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.3.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"]
fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"]
hf-transfer = ["hf-transfer (>=0.1.4)"]
inference = ["aiohttp", "minijinja (>=1.0)"]
quality = ["mypy (==1.5.1)", "ruff (>=0.3.0)"]
tensorflow = ["graphviz", "pydot", "tensorflow"]
tensorflow-testing = ["keras (<3.0)", "tensorflow"]
testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "minijinja (>=1.0)", "numpy", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"]
testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio", "jedi", "minijinja (>=1.0)", "numpy", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"]
torch = ["safetensors", "torch"]
typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)"]
@@ -1373,13 +1374,13 @@ files = [
[[package]]
name = "jinja2"
version = "3.1.3"
version = "3.1.4"
description = "A very fast and expressive template engine."
optional = false
python-versions = ">=3.7"
files = [
{file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"},
{file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"},
{file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
{file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
]
[package.dependencies]
@@ -1529,13 +1530,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
[[package]]
name = "locust"
version = "2.26.0"
version = "2.27.0"
description = "Developer friendly load testing framework"
optional = false
python-versions = ">=3.9"
files = [
{file = "locust-2.26.0-py3-none-any.whl", hash = "sha256:7957d8346e5830ba35e3a7a9c1eebe0fb73b0be117e54213c61ef3bc658a1ae6"},
{file = "locust-2.26.0.tar.gz", hash = "sha256:a5cb4c96b8fa1ae5c20876ab8ca9d1e980d56148ed3c187df610cc2546705bff"},
{file = "locust-2.27.0-py3-none-any.whl", hash = "sha256:c4db5747eb9a3851216deae8147143d335db41978a9291ac32e113fa9ec8ad39"},
{file = "locust-2.27.0.tar.gz", hash = "sha256:0c6d3d2523976dafe734012c41b2f7d9ad7120cbcea76d47d80cec5d6d139905"},
]
[package.dependencies]
@@ -1550,7 +1551,6 @@ psutil = ">=5.9.1"
pywin32 = {version = "*", markers = "platform_system == \"Windows\""}
pyzmq = ">=25.0.0"
requests = ">=2.26.0"
roundrobin = ">=0.0.2"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
Werkzeug = ">=2.0.0"
@@ -2077,62 +2077,57 @@ numpy = [
[[package]]
name = "orjson"
version = "3.10.1"
version = "3.10.3"
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
optional = false
python-versions = ">=3.8"
files = [
{file = "orjson-3.10.1-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:8ec2fc456d53ea4a47768f622bb709be68acd455b0c6be57e91462259741c4f3"},
{file = "orjson-3.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e900863691d327758be14e2a491931605bd0aded3a21beb6ce133889830b659"},
{file = "orjson-3.10.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab6ecbd6fe57785ebc86ee49e183f37d45f91b46fc601380c67c5c5e9c0014a2"},
{file = "orjson-3.10.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8af7c68b01b876335cccfb4eee0beef2b5b6eae1945d46a09a7c24c9faac7a77"},
{file = "orjson-3.10.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:915abfb2e528677b488a06eba173e9d7706a20fdfe9cdb15890b74ef9791b85e"},
{file = "orjson-3.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe3fd4a36eff9c63d25503b439531d21828da9def0059c4f472e3845a081aa0b"},
{file = "orjson-3.10.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d229564e72cfc062e6481a91977a5165c5a0fdce11ddc19ced8471847a67c517"},
{file = "orjson-3.10.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9e00495b18304173ac843b5c5fbea7b6f7968564d0d49bef06bfaeca4b656f4e"},
{file = "orjson-3.10.1-cp310-none-win32.whl", hash = "sha256:fd78ec55179545c108174ba19c1795ced548d6cac4d80d014163033c047ca4ea"},
{file = "orjson-3.10.1-cp310-none-win_amd64.whl", hash = "sha256:50ca42b40d5a442a9e22eece8cf42ba3d7cd4cd0f2f20184b4d7682894f05eec"},
{file = "orjson-3.10.1-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b345a3d6953628df2f42502297f6c1e1b475cfbf6268013c94c5ac80e8abc04c"},
{file = "orjson-3.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caa7395ef51af4190d2c70a364e2f42138e0e5fcb4bc08bc9b76997659b27dab"},
{file = "orjson-3.10.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b01d701decd75ae092e5f36f7b88a1e7a1d3bb7c9b9d7694de850fb155578d5a"},
{file = "orjson-3.10.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5028981ba393f443d8fed9049211b979cadc9d0afecf162832f5a5b152c6297"},
{file = "orjson-3.10.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31ff6a222ea362b87bf21ff619598a4dc1106aaafaea32b1c4876d692891ec27"},
{file = "orjson-3.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e852a83d7803d3406135fb7a57cf0c1e4a3e73bac80ec621bd32f01c653849c5"},
{file = "orjson-3.10.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2567bc928ed3c3fcd90998009e8835de7c7dc59aabcf764b8374d36044864f3b"},
{file = "orjson-3.10.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4ce98cac60b7bb56457bdd2ed7f0d5d7f242d291fdc0ca566c83fa721b52e92d"},
{file = "orjson-3.10.1-cp311-none-win32.whl", hash = "sha256:813905e111318acb356bb8029014c77b4c647f8b03f314e7b475bd9ce6d1a8ce"},
{file = "orjson-3.10.1-cp311-none-win_amd64.whl", hash = "sha256:03a3ca0b3ed52bed1a869163a4284e8a7b0be6a0359d521e467cdef7e8e8a3ee"},
{file = "orjson-3.10.1-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f02c06cee680b1b3a8727ec26c36f4b3c0c9e2b26339d64471034d16f74f4ef5"},
{file = "orjson-3.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1aa2f127ac546e123283e437cc90b5ecce754a22306c7700b11035dad4ccf85"},
{file = "orjson-3.10.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2cf29b4b74f585225196944dffdebd549ad2af6da9e80db7115984103fb18a96"},
{file = "orjson-3.10.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1b130c20b116f413caf6059c651ad32215c28500dce9cd029a334a2d84aa66f"},
{file = "orjson-3.10.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d31f9a709e6114492136e87c7c6da5e21dfedebefa03af85f3ad72656c493ae9"},
{file = "orjson-3.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d1d169461726f271ab31633cf0e7e7353417e16fb69256a4f8ecb3246a78d6e"},
{file = "orjson-3.10.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:57c294d73825c6b7f30d11c9e5900cfec9a814893af7f14efbe06b8d0f25fba9"},
{file = "orjson-3.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d7f11dbacfa9265ec76b4019efffabaabba7a7ebf14078f6b4df9b51c3c9a8ea"},
{file = "orjson-3.10.1-cp312-none-win32.whl", hash = "sha256:d89e5ed68593226c31c76ab4de3e0d35c760bfd3fbf0a74c4b2be1383a1bf123"},
{file = "orjson-3.10.1-cp312-none-win_amd64.whl", hash = "sha256:aa76c4fe147fd162107ce1692c39f7189180cfd3a27cfbc2ab5643422812da8e"},
{file = "orjson-3.10.1-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a2c6a85c92d0e494c1ae117befc93cf8e7bca2075f7fe52e32698da650b2c6d1"},
{file = "orjson-3.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9813f43da955197d36a7365eb99bed42b83680801729ab2487fef305b9ced866"},
{file = "orjson-3.10.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec917b768e2b34b7084cb6c68941f6de5812cc26c6f1a9fecb728e36a3deb9e8"},
{file = "orjson-3.10.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5252146b3172d75c8a6d27ebca59c9ee066ffc5a277050ccec24821e68742fdf"},
{file = "orjson-3.10.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:536429bb02791a199d976118b95014ad66f74c58b7644d21061c54ad284e00f4"},
{file = "orjson-3.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dfed3c3e9b9199fb9c3355b9c7e4649b65f639e50ddf50efdf86b45c6de04b5"},
{file = "orjson-3.10.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2b230ec35f188f003f5b543644ae486b2998f6afa74ee3a98fc8ed2e45960afc"},
{file = "orjson-3.10.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:01234249ba19c6ab1eb0b8be89f13ea21218b2d72d496ef085cfd37e1bae9dd8"},
{file = "orjson-3.10.1-cp38-none-win32.whl", hash = "sha256:8a884fbf81a3cc22d264ba780920d4885442144e6acaa1411921260416ac9a54"},
{file = "orjson-3.10.1-cp38-none-win_amd64.whl", hash = "sha256:dab5f802d52b182163f307d2b1f727d30b1762e1923c64c9c56dd853f9671a49"},
{file = "orjson-3.10.1-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a51fd55d4486bc5293b7a400f9acd55a2dc3b5fc8420d5ffe9b1d6bb1a056a5e"},
{file = "orjson-3.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53521542a6db1411b3bfa1b24ddce18605a3abdc95a28a67b33f9145f26aa8f2"},
{file = "orjson-3.10.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:27d610df96ac18ace4931411d489637d20ab3b8f63562b0531bba16011998db0"},
{file = "orjson-3.10.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79244b1456e5846d44e9846534bd9e3206712936d026ea8e6a55a7374d2c0694"},
{file = "orjson-3.10.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d751efaa8a49ae15cbebdda747a62a9ae521126e396fda8143858419f3b03610"},
{file = "orjson-3.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27ff69c620a4fff33267df70cfd21e0097c2a14216e72943bd5414943e376d77"},
{file = "orjson-3.10.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ebc58693464146506fde0c4eb1216ff6d4e40213e61f7d40e2f0dde9b2f21650"},
{file = "orjson-3.10.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5be608c3972ed902e0143a5b8776d81ac1059436915d42defe5c6ae97b3137a4"},
{file = "orjson-3.10.1-cp39-none-win32.whl", hash = "sha256:4ae10753e7511d359405aadcbf96556c86e9dbf3a948d26c2c9f9a150c52b091"},
{file = "orjson-3.10.1-cp39-none-win_amd64.whl", hash = "sha256:fb5bc4caa2c192077fdb02dce4e5ef8639e7f20bec4e3a834346693907362932"},
{file = "orjson-3.10.1.tar.gz", hash = "sha256:a883b28d73370df23ed995c466b4f6c708c1f7a9bdc400fe89165c96c7603204"},
{file = "orjson-3.10.3-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9fb6c3f9f5490a3eb4ddd46fc1b6eadb0d6fc16fb3f07320149c3286a1409dd8"},
{file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:252124b198662eee80428f1af8c63f7ff077c88723fe206a25df8dc57a57b1fa"},
{file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9f3e87733823089a338ef9bbf363ef4de45e5c599a9bf50a7a9b82e86d0228da"},
{file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8334c0d87103bb9fbbe59b78129f1f40d1d1e8355bbed2ca71853af15fa4ed3"},
{file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1952c03439e4dce23482ac846e7961f9d4ec62086eb98ae76d97bd41d72644d7"},
{file = "orjson-3.10.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c0403ed9c706dcd2809f1600ed18f4aae50be263bd7112e54b50e2c2bc3ebd6d"},
{file = "orjson-3.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:382e52aa4270a037d41f325e7d1dfa395b7de0c367800b6f337d8157367bf3a7"},
{file = "orjson-3.10.3-cp310-none-win32.whl", hash = "sha256:be2aab54313752c04f2cbaab4515291ef5af8c2256ce22abc007f89f42f49109"},
{file = "orjson-3.10.3-cp310-none-win_amd64.whl", hash = "sha256:416b195f78ae461601893f482287cee1e3059ec49b4f99479aedf22a20b1098b"},
{file = "orjson-3.10.3-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:73100d9abbbe730331f2242c1fc0bcb46a3ea3b4ae3348847e5a141265479700"},
{file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:544a12eee96e3ab828dbfcb4d5a0023aa971b27143a1d35dc214c176fdfb29b3"},
{file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:520de5e2ef0b4ae546bea25129d6c7c74edb43fc6cf5213f511a927f2b28148b"},
{file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ccaa0a401fc02e8828a5bedfd80f8cd389d24f65e5ca3954d72c6582495b4bcf"},
{file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7bc9e8bc11bac40f905640acd41cbeaa87209e7e1f57ade386da658092dc16"},
{file = "orjson-3.10.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3582b34b70543a1ed6944aca75e219e1192661a63da4d039d088a09c67543b08"},
{file = "orjson-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c23dfa91481de880890d17aa7b91d586a4746a4c2aa9a145bebdbaf233768d5"},
{file = "orjson-3.10.3-cp311-none-win32.whl", hash = "sha256:1770e2a0eae728b050705206d84eda8b074b65ee835e7f85c919f5705b006c9b"},
{file = "orjson-3.10.3-cp311-none-win_amd64.whl", hash = "sha256:93433b3c1f852660eb5abdc1f4dd0ced2be031ba30900433223b28ee0140cde5"},
{file = "orjson-3.10.3-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a39aa73e53bec8d410875683bfa3a8edf61e5a1c7bb4014f65f81d36467ea098"},
{file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0943a96b3fa09bee1afdfccc2cb236c9c64715afa375b2af296c73d91c23eab2"},
{file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e852baafceff8da3c9defae29414cc8513a1586ad93e45f27b89a639c68e8176"},
{file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18566beb5acd76f3769c1d1a7ec06cdb81edc4d55d2765fb677e3eaa10fa99e0"},
{file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bd2218d5a3aa43060efe649ec564ebedec8ce6ae0a43654b81376216d5ebd42"},
{file = "orjson-3.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cf20465e74c6e17a104ecf01bf8cd3b7b252565b4ccee4548f18b012ff2f8069"},
{file = "orjson-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba7f67aa7f983c4345eeda16054a4677289011a478ca947cd69c0a86ea45e534"},
{file = "orjson-3.10.3-cp312-none-win32.whl", hash = "sha256:17e0713fc159abc261eea0f4feda611d32eabc35708b74bef6ad44f6c78d5ea0"},
{file = "orjson-3.10.3-cp312-none-win_amd64.whl", hash = "sha256:4c895383b1ec42b017dd2c75ae8a5b862fc489006afde06f14afbdd0309b2af0"},
{file = "orjson-3.10.3-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:be2719e5041e9fb76c8c2c06b9600fe8e8584e6980061ff88dcbc2691a16d20d"},
{file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0175a5798bdc878956099f5c54b9837cb62cfbf5d0b86ba6d77e43861bcec2"},
{file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:978be58a68ade24f1af7758626806e13cff7748a677faf95fbb298359aa1e20d"},
{file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16bda83b5c61586f6f788333d3cf3ed19015e3b9019188c56983b5a299210eb5"},
{file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ad1f26bea425041e0a1adad34630c4825a9e3adec49079b1fb6ac8d36f8b754"},
{file = "orjson-3.10.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9e253498bee561fe85d6325ba55ff2ff08fb5e7184cd6a4d7754133bd19c9195"},
{file = "orjson-3.10.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0a62f9968bab8a676a164263e485f30a0b748255ee2f4ae49a0224be95f4532b"},
{file = "orjson-3.10.3-cp38-none-win32.whl", hash = "sha256:8d0b84403d287d4bfa9bf7d1dc298d5c1c5d9f444f3737929a66f2fe4fb8f134"},
{file = "orjson-3.10.3-cp38-none-win_amd64.whl", hash = "sha256:8bc7a4df90da5d535e18157220d7915780d07198b54f4de0110eca6b6c11e290"},
{file = "orjson-3.10.3-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9059d15c30e675a58fdcd6f95465c1522b8426e092de9fff20edebfdc15e1cb0"},
{file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d40c7f7938c9c2b934b297412c067936d0b54e4b8ab916fd1a9eb8f54c02294"},
{file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4a654ec1de8fdaae1d80d55cee65893cb06494e124681ab335218be6a0691e7"},
{file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:831c6ef73f9aa53c5f40ae8f949ff7681b38eaddb6904aab89dca4d85099cb78"},
{file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99b880d7e34542db89f48d14ddecbd26f06838b12427d5a25d71baceb5ba119d"},
{file = "orjson-3.10.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2e5e176c994ce4bd434d7aafb9ecc893c15f347d3d2bbd8e7ce0b63071c52e25"},
{file = "orjson-3.10.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b69a58a37dab856491bf2d3bbf259775fdce262b727f96aafbda359cb1d114d8"},
{file = "orjson-3.10.3-cp39-none-win32.whl", hash = "sha256:b8d4d1a6868cde356f1402c8faeb50d62cee765a1f7ffcfd6de732ab0581e063"},
{file = "orjson-3.10.3-cp39-none-win_amd64.whl", hash = "sha256:5102f50c5fc46d94f2033fe00d392588564378260d64377aec702f21a7a22912"},
{file = "orjson-3.10.3.tar.gz", hash = "sha256:2b166507acae7ba2f7c315dcf185a9111ad5e992ac81f2d507aac39193c2c818"},
]
[[package]]
@@ -2260,13 +2255,13 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co
[[package]]
name = "pluggy"
version = "1.4.0"
version = "1.5.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"},
{file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"},
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
]
[package.extras]
@@ -2443,13 +2438,13 @@ files = [
[[package]]
name = "pytest"
version = "8.1.1"
version = "8.2.0"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"},
{file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"},
{file = "pytest-8.2.0-py3-none-any.whl", hash = "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233"},
{file = "pytest-8.2.0.tar.gz", hash = "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f"},
]
[package.dependencies]
@@ -2457,11 +2452,11 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""}
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=1.4,<2.0"
pluggy = ">=1.5,<2.0"
tomli = {version = ">=1", markers = "python_version < \"3.11\""}
[package.extras]
testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "pytest-asyncio"
@@ -2802,40 +2797,30 @@ pygments = ">=2.13.0,<3.0.0"
[package.extras]
jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "roundrobin"
version = "0.0.4"
description = "Collection of roundrobin utilities"
optional = false
python-versions = "*"
files = [
{file = "roundrobin-0.0.4.tar.gz", hash = "sha256:7e9d19a5bd6123d99993fb935fa86d25c88bb2096e493885f61737ed0f5e9abd"},
]
[[package]]
name = "ruff"
version = "0.4.2"
version = "0.4.4"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8d14dc8953f8af7e003a485ef560bbefa5f8cc1ad994eebb5b12136049bbccc5"},
{file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:24016ed18db3dc9786af103ff49c03bdf408ea253f3cb9e3638f39ac9cf2d483"},
{file = "ruff-0.4.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2e06459042ac841ed510196c350ba35a9b24a643e23db60d79b2db92af0c2b"},
{file = "ruff-0.4.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3afabaf7ba8e9c485a14ad8f4122feff6b2b93cc53cd4dad2fd24ae35112d5c5"},
{file = "ruff-0.4.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:799eb468ea6bc54b95527143a4ceaf970d5aa3613050c6cff54c85fda3fde480"},
{file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ec4ba9436a51527fb6931a8839af4c36a5481f8c19e8f5e42c2f7ad3a49f5069"},
{file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6a2243f8f434e487c2a010c7252150b1fdf019035130f41b77626f5655c9ca22"},
{file = "ruff-0.4.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8772130a063f3eebdf7095da00c0b9898bd1774c43b336272c3e98667d4fb8fa"},
{file = "ruff-0.4.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ab165ef5d72392b4ebb85a8b0fbd321f69832a632e07a74794c0e598e7a8376"},
{file = "ruff-0.4.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1f32cadf44c2020e75e0c56c3408ed1d32c024766bd41aedef92aa3ca28eef68"},
{file = "ruff-0.4.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:22e306bf15e09af45ca812bc42fa59b628646fa7c26072555f278994890bc7ac"},
{file = "ruff-0.4.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82986bb77ad83a1719c90b9528a9dd663c9206f7c0ab69282af8223566a0c34e"},
{file = "ruff-0.4.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:652e4ba553e421a6dc2a6d4868bc3b3881311702633eb3672f9f244ded8908cd"},
{file = "ruff-0.4.2-py3-none-win32.whl", hash = "sha256:7891ee376770ac094da3ad40c116258a381b86c7352552788377c6eb16d784fe"},
{file = "ruff-0.4.2-py3-none-win_amd64.whl", hash = "sha256:5ec481661fb2fd88a5d6cf1f83403d388ec90f9daaa36e40e2c003de66751798"},
{file = "ruff-0.4.2-py3-none-win_arm64.whl", hash = "sha256:cbd1e87c71bca14792948c4ccb51ee61c3296e164019d2d484f3eaa2d360dfaf"},
{file = "ruff-0.4.2.tar.gz", hash = "sha256:33bcc160aee2520664bc0859cfeaebc84bb7323becff3f303b8f1f2d81cb4edc"},
{file = "ruff-0.4.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:29d44ef5bb6a08e235c8249294fa8d431adc1426bfda99ed493119e6f9ea1bf6"},
{file = "ruff-0.4.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c4efe62b5bbb24178c950732ddd40712b878a9b96b1d02b0ff0b08a090cbd891"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c8e2f1e8fc12d07ab521a9005d68a969e167b589cbcaee354cb61e9d9de9c15"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60ed88b636a463214905c002fa3eaab19795679ed55529f91e488db3fe8976ab"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b90fc5e170fc71c712cc4d9ab0e24ea505c6a9e4ebf346787a67e691dfb72e85"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8e7e6ebc10ef16dcdc77fd5557ee60647512b400e4a60bdc4849468f076f6eef"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9ddb2c494fb79fc208cd15ffe08f32b7682519e067413dbaf5f4b01a6087bcd"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c51c928a14f9f0a871082603e25a1588059b7e08a920f2f9fa7157b5bf08cfe9"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5eb0a4bfd6400b7d07c09a7725e1a98c3b838be557fee229ac0f84d9aa49c36"},
{file = "ruff-0.4.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b1867ee9bf3acc21778dcb293db504692eda5f7a11a6e6cc40890182a9f9e595"},
{file = "ruff-0.4.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1aecced1269481ef2894cc495647392a34b0bf3e28ff53ed95a385b13aa45768"},
{file = "ruff-0.4.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9da73eb616b3241a307b837f32756dc20a0b07e2bcb694fec73699c93d04a69e"},
{file = "ruff-0.4.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:958b4ea5589706a81065e2a776237de2ecc3e763342e5cc8e02a4a4d8a5e6f95"},
{file = "ruff-0.4.4-py3-none-win32.whl", hash = "sha256:cb53473849f011bca6e754f2cdf47cafc9c4f4ff4570003a0dad0b9b6890e876"},
{file = "ruff-0.4.4-py3-none-win_amd64.whl", hash = "sha256:424e5b72597482543b684c11def82669cc6b395aa8cc69acc1858b5ef3e5daae"},
{file = "ruff-0.4.4-py3-none-win_arm64.whl", hash = "sha256:39df0537b47d3b597293edbb95baf54ff5b49589eb7ff41926d8243caa995ea6"},
{file = "ruff-0.4.4.tar.gz", hash = "sha256:f87ea42d5cdebdc6a69761a9d0bc83ae9b3b30d0ad78952005ba6568d6c022af"},
]
[[package]]
@@ -3202,13 +3187,13 @@ files = [
[[package]]
name = "tqdm"
version = "4.66.1"
version = "4.66.3"
description = "Fast, Extensible Progress Meter"
optional = false
python-versions = ">=3.7"
files = [
{file = "tqdm-4.66.1-py3-none-any.whl", hash = "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386"},
{file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"},
{file = "tqdm-4.66.3-py3-none-any.whl", hash = "sha256:4f41d54107ff9a223dca80b53efe4fb654c67efaba7f47bada3ee9d50e05bd53"},
{file = "tqdm-4.66.3.tar.gz", hash = "sha256:23097a41eba115ba99ecae40d06444c15d1c0c698d527a01c6c8bd1c5d0647e5"},
]
[package.dependencies]
@@ -3498,13 +3483,13 @@ files = [
[[package]]
name = "werkzeug"
version = "3.0.1"
version = "3.0.3"
description = "The comprehensive WSGI web application library."
optional = false
python-versions = ">=3.8"
files = [
{file = "werkzeug-3.0.1-py3-none-any.whl", hash = "sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10"},
{file = "werkzeug-3.0.1.tar.gz", hash = "sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc"},
{file = "werkzeug-3.0.3-py3-none-any.whl", hash = "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8"},
{file = "werkzeug-3.0.3.tar.gz", hash = "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18"},
]
[package.dependencies]
@@ -3587,4 +3572,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.10,<3.12"
content-hash = "1b014276ec94f9389459a70d31f0d96d1dd5a138bcc988900865e5f07a72bc62"
content-hash = "db51ad1e631b569e106927683a13124252bd80974def1f2edbe23ac87d89c461"

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "machine-learning"
version = "1.103.1"
version = "1.105.1"
description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md"
@@ -11,7 +11,7 @@ python = ">=3.10,<3.12"
insightface = ">=0.7.3,<1.0"
opencv-python-headless = ">=4.7.0.72,<5.0"
pillow = ">=9.5.0,<11.0"
fastapi = ">=0.95.2,<1.0"
fastapi-slim = ">=0.95.2,<1.0"
uvicorn = {extras = ["standard"], version = ">=0.22.0,<1.0"}
pydantic = "^1.10.8"
aiocache = ">=0.12.1,<1.0"

View File

@@ -2,20 +2,20 @@
lib_path="/usr/lib/$(arch)-linux-gnu/libmimalloc.so.2"
# mimalloc seems to increase memory usage dramatically with openvino, need to investigate
if ! [ "$DEVICE" = "openvino" ]; then
if ! [ "$DEVICE" = "openvino" ]; then
export LD_PRELOAD="$lib_path"
export LD_BIND_NOW=1
fi
: "${MACHINE_LEARNING_HOST:=[::]}"
: "${MACHINE_LEARNING_PORT:=3003}"
: "${IMMICH_HOST:=[::]}"
: "${IMMICH_PORT:=3003}"
: "${MACHINE_LEARNING_WORKERS:=1}"
: "${MACHINE_LEARNING_WORKER_TIMEOUT:=120}"
gunicorn app.main:app \
-k app.config.CustomUvicornWorker \
-b "$IMMICH_HOST":"$IMMICH_PORT" \
-w "$MACHINE_LEARNING_WORKERS" \
-b "$MACHINE_LEARNING_HOST":"$MACHINE_LEARNING_PORT" \
-t "$MACHINE_LEARNING_WORKER_TIMEOUT" \
--log-config-json log_conf.json \
--graceful-timeout 0

View File

@@ -62,6 +62,8 @@ fi
if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then
echo "Pumping Server: $CURRENT_SERVER => $NEXT_SERVER"
npm --prefix server version "$SERVER_PUMP"
npm --prefix server ci
npm --prefix server run build
make open-api
npm --prefix open-api/typescript-sdk version "$SERVER_PUMP"
npm --prefix web version "$SERVER_PUMP"

View File

@@ -1,3 +1,3 @@
{
"flutter": "3.19.3"
"flutter": "3.22.0"
}

View File

@@ -1,5 +1,5 @@
{
"dart.flutterSdkPath": ".fvm/versions/3.19.3",
"dart.flutterSdkPath": ".fvm/versions/3.22.0",
"search.exclude": {
"**/.fvm": true
},

View File

@@ -1,43 +1,45 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.5)
CFPropertyList (3.0.7)
base64
nkf
rexml
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
artifactory (3.0.15)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.2.0)
aws-partitions (1.566.0)
aws-sdk-core (3.130.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-eventstream (1.3.0)
aws-partitions (1.932.0)
aws-sdk-core (3.196.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.81.0)
aws-sdk-core (~> 3, >= 3.193.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.55.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.113.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-s3 (1.151.0)
aws-sdk-core (~> 3, >= 3.194.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.4.0)
aws-sigv4 (~> 1.8)
aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
declarative (0.0.20)
digest-crc (0.6.4)
digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.6)
domain_name (0.6.20240107)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.92.0)
faraday (1.10.0)
excon (0.110.0)
faraday (1.10.3)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
@@ -56,8 +58,8 @@ GEM
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.3)
multipart-post (>= 1.2, < 3)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
@@ -65,8 +67,8 @@ GEM
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.2.6)
fastlane (2.204.3)
fastimage (2.3.1)
fastlane (2.214.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@@ -90,7 +92,7 @@ GEM
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (~> 2.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
naturally (~> 2.2)
optparse (~> 0.1.1)
plist (>= 3.1.0, < 4.0.0)
@@ -106,9 +108,9 @@ GEM
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.16.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-core (0.4.2)
google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.3)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
@@ -116,87 +118,84 @@ GEM
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
webrick
google-apis-iamcredentials_v1 (0.10.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-playcustomapp_v1 (0.7.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-storage_v1 (0.11.0)
google-apis-core (>= 0.4, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.7.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.5.0)
faraday (>= 0.17.3, < 2.0)
google-cloud-errors (1.2.0)
google-cloud-storage (1.36.1)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.4.0)
google-cloud-storage (1.47.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.31.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.1.2)
googleauth (1.8.1)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.4)
http-cookie (1.0.5)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.6.1)
json (2.6.1)
jwt (2.3.0)
memoist (0.16.2)
mini_magick (4.11.0)
mini_mime (1.1.2)
jmespath (1.6.2)
json (2.7.2)
jwt (2.8.1)
base64
mini_magick (4.12.0)
mini_mime (1.1.5)
multi_json (1.15.0)
multipart-post (2.0.0)
multipart-post (2.4.1)
nanaimo (0.3.0)
naturally (2.2.1)
nkf (0.2.0)
optparse (0.1.1)
os (1.1.4)
plist (3.6.0)
public_suffix (4.0.6)
rake (13.0.6)
representable (3.1.1)
plist (3.7.1)
public_suffix (4.0.7)
rake (13.2.1)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.2.5)
rexml (3.2.8)
strscan (>= 3.0.9)
rouge (2.0.7)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
signet (0.16.1)
signet (0.19.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.0)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.8)
simctl (1.6.10)
CFPropertyList
naturally
strscan (3.1.0)
terminal-notifier (2.0.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.1)
tty-screen (0.8.2)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.8.1)
unicode-display_width (1.8.0)
webrick (1.7.0)
word_wrap (1.0.0)
xcodeproj (1.21.0)
xcodeproj (1.24.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
@@ -210,6 +209,7 @@ GEM
PLATFORMS
x86_64-darwin-21
x86_64-linux
DEPENDENCIES
fastlane

View File

@@ -81,11 +81,11 @@ flutter {
}
dependencies {
def kotlin_version = '1.9.23'
def kotlin_coroutines_version = '1.8.0'
def kotlin_version = '1.9.24'
def kotlin_coroutines_version = '1.8.1'
def work_version = '2.9.0'
def concurrent_version = '1.1.0'
def guava_version = '33.1.0-android'
def guava_version = '33.2.0-android'
def glide_version = '4.16.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"

View File

@@ -52,7 +52,6 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
.putBoolean(ContentObserverWorker.SHARED_PREF_SERVICE_ENABLED, true)
.putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args.get(0) as Long)
.putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args.get(1) as String)
.putString(BackupWorker.SHARED_PREF_SERVER_URL, args.get(3) as String)
.apply()
ContentObserverWorker.enable(ctx, immediate = args.get(2) as Boolean)
result.success(true)

View File

@@ -11,8 +11,8 @@ import android.os.PowerManager
import android.os.SystemClock
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.concurrent.futures.CallbackToFutureAdapter
import androidx.core.app.NotificationCompat
import androidx.concurrent.futures.ResolvableFuture
import androidx.work.BackoffPolicy
import androidx.work.Constraints
import androidx.work.ForegroundInfo
@@ -30,16 +30,6 @@ import io.flutter.embedding.engine.loader.FlutterLoader
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.view.FlutterCallbackInformation
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import java.io.IOException
import java.net.HttpURLConnection
import java.net.InetAddress
import java.net.URL
import java.util.concurrent.TimeUnit
/**
@@ -52,6 +42,7 @@ import java.util.concurrent.TimeUnit
*/
class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ctx, params), MethodChannel.MethodCallHandler {
private val resolvableFuture = ResolvableFuture.create<Result>()
private var engine: FlutterEngine? = null
private lateinit var backgroundChannel: MethodChannel
private val notificationManager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@@ -61,80 +52,35 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
private var notificationDetailBuilder: NotificationCompat.Builder? = null
private var fgFuture: ListenableFuture<Void>? = null
private val job = Job()
private lateinit var completer: CallbackToFutureAdapter.Completer<Result>
private val resolvableFuture = CallbackToFutureAdapter.getFuture { completer ->
this.completer = completer
null
}
init {
resolvableFuture.addListener(
Runnable {
if (resolvableFuture.isCancelled) {
job.cancel()
}
},
taskExecutor.serialTaskExecutor
)
}
override fun startWork(): ListenableFuture<ListenableWorker.Result> {
Log.d(TAG, "startWork")
val ctx = applicationContext
val prefs = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
prefs.getString(SHARED_PREF_SERVER_URL, null)
?.takeIf { it.isNotEmpty() }
?.let { serverUrl -> doCoroutineWork(serverUrl) }
?: doWork()
return resolvableFuture
}
/**
* This function is used to check if server URL is reachable before starting the backup work.
* Check must be done in a background to avoid blocking the main thread.
*/
private fun doCoroutineWork(serverUrl : String) {
CoroutineScope(Dispatchers.Default + job).launch {
val isReachable = isUrlReachableHttp(serverUrl)
withContext(Dispatchers.Main) {
if (isReachable) {
doWork()
} else {
// Fail when the URL is not reachable
completer.set(Result.failure())
}
}
if (!flutterLoader.initialized()) {
flutterLoader.startInitialization(ctx)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Create a Notification channel if necessary
createChannel()
}
if (isIgnoringBatteryOptimizations) {
// normal background services can only up to 10 minutes
// foreground services are allowed to run indefinitely
// requires battery optimizations to be disabled (either manually by the user
// or by the system learning that immich is important to the user)
val title = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
.getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!!
showInfo(getInfoBuilder(title, indeterminate=true).build())
}
engine = FlutterEngine(ctx)
private fun doWork() {
Log.d(TAG, "doWork")
val ctx = applicationContext
flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
runDart()
}
if (!flutterLoader.initialized()) {
flutterLoader.startInitialization(ctx)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Create a Notification channel if necessary
createChannel()
}
if (isIgnoringBatteryOptimizations) {
// normal background services can only up to 10 minutes
// foreground services are allowed to run indefinitely
// requires battery optimizations to be disabled (either manually by the user
// or by the system learning that immich is important to the user)
val title = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
.getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!!
showInfo(getInfoBuilder(title, indeterminate=true).build())
}
engine = FlutterEngine(ctx)
flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
runDart()
}
return resolvableFuture
}
/**
@@ -193,7 +139,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
engine = null
if (result != null) {
Log.d(TAG, "stopEngine result=${result}")
this.completer.set(result)
resolvableFuture.set(result)
}
waitOnSetForegroundAsync()
}
@@ -324,7 +270,6 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
const val SHARED_PREF_CALLBACK_KEY = "callbackDispatcherHandle"
const val SHARED_PREF_NOTIFICATION_TITLE = "notificationTitle"
const val SHARED_PREF_LAST_CHANGE = "lastChange"
const val SHARED_PREF_SERVER_URL = "serverUrl"
private const val TASK_NAME_BACKUP = "immich/BackupWorker"
private const val NOTIFICATION_CHANNEL_ID = "immich/backgroundService"
@@ -415,26 +360,3 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
}
private const val TAG = "BackupWorker"
/**
* Check if the given URL is reachable via HTTP
*/
suspend fun isUrlReachableHttp(url: String, timeoutMillis: Long = 5000L): Boolean {
return withTimeoutOrNull(timeoutMillis) {
var httpURLConnection: HttpURLConnection? = null
try {
httpURLConnection = (URL(url).openConnection() as HttpURLConnection).apply {
requestMethod = "HEAD"
connectTimeout = timeoutMillis.toInt()
readTimeout = timeoutMillis.toInt()
}
httpURLConnection.connect()
httpURLConnection.responseCode == HttpURLConnection.HTTP_OK
} catch (e: Exception) {
Log.e(TAG, "Failed to reach server URL: $e")
false
} finally {
httpURLConnection?.disconnect()
}
} == true
}

View File

@@ -1,4 +1,6 @@
allprojects {
ext.kotlin_version = '1.9.24'
repositories {
google()
mavenCentral()

Some files were not shown because too many files have changed in this diff Show More